需求
成功运行某个sql语句之后,通过日志查看运行结果,并能在日志中随时终止这个sql。
如图,完成取消执行功能
方案
终止一个正在运行的sql,可以使用JDBC提供的statement.cancel()
但服务是多节点部署,找不到对应的statement对象
使用redis的发布订阅模式
1.取消执行的请求发送到服务1上,服务1发布消息到reids集群
2.所有订阅该消息的服务都收到,开始在本地内存中寻找statement对象。根据logId寻找
3.所有服务中事先存储了各自服务器上运行的<logId,Statement>的key value Map,服务3中根据logId找到了对应的statement对象,执行cancel()成功取消执行
4.每个sql执行完成之后,服务器需要自行remove对应的map,否则会膨胀
代码
1.配置redis监听器
@Configuration
public class RedisPubSubListenerConfiguration {
/**
* 配置消息监听容器, 监听CHANNEL_LOG_STOP主题
*
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic(LogStopUtil.CHANNEL_LOG_STOP));
return container;
}
/**
* 配置消息监听适配器, 将接收到的消息传递给订阅者RedisSubscriber
* 并使用RECEIVER_ON_MESSAGE方法来处理
*
* @param subscriber
* @return
*/
@Bean
public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, LogStopUtil.RECEIVER_ON_MESSAGE);
}
}
2.执行sql,并存储对应的 Map
public class LogStopUtil {
public static final String CHANNEL_LOG_STOP = "LOG_STOP";
public static final String RECEIVER_ON_MESSAGE = "onMessage";
public static Map<String, Statement> logStatementMap = new HashMap<>();
public static Long getElapsedTime(LocalDateTime endDate, LocalDateTime startDate) {
return endDate.toInstant(ZoneOffset.of("+8")).toEpochMilli() -
startDate.toInstant(ZoneOffset.of("+8")).toEpochMilli();
}
public static String logDate2String(LocalDateTime dateTime) {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
@Override
public void run() {
// 检查自动任务参数
if (ObjectUtils.isEmpty(automaticLog, ruleId)) {
automaticLog.initLog(checkRuleEntity);
}
if (ObjectUtils.isEmpty(dataSourceEntity)) {
automaticLog.endLog(dataSourceIsNull);
dto.setTaskFlag(false);
return;
}
Connection conn = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 拼接 SQL
String sql = concatSql();
Assert.notEmpty(sql, "拼接后的SQL为空,请检查任务执行规则!");
// 初始化任务日志
initLog(LOG_IN_RUN, sql, LocalDateTime.now(), null, null);
log.info(dto.getRuleCode() + " 开始执行SQL查询任务");
// 获取数据库连接
conn = getConnection();
log.info(dto.getRuleCode() + " 获取数据库连接成功");
// 创建 Statement
statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
log.info(dto.getRuleCode() + " 创建Statement对象成功");
// 注册 Statement
logStopUtil.logStatementMap.put(automaticLog.logId, statement);
checkSql(statement); // 检查SQL
log.info(dto.getRuleCode() + " statement对象注册成功,准备执行SQL");
resultSet = statement.executeQuery(sql);
log.info(dto.getRuleCode() + " SQL执行完毕");
Assert.notNull(resultSet, "查询语句返回结果集为空,请检查SQL");
// 解析列数据
parseColumns(resultSet.getMetaData());
log.info(dto.getRuleCode() + " 列解析完毕");
// 解析结果
Integer parseInfo = parseInfo(resultSet);
log.info(dto.getRuleCode() + " 结果解析完毕");
// 初始化日志状态
initLog(
SchType.ManualRule.getCode().equals(automaticLog.schType) ?
LOG_RUN_STATUS_SUCCESS : LOG_IN_RUN,
null, null, LocalDateTime.now(), i
);
} catch (Exception e) {
// 捕获异常,记录错误日志
catchERROR(e.getMessage());
dto.setTaskFlag(false);
} finally {
// 关闭资源
close(resultSet, statement, conn);
log.info(dto.getRuleCode() + " statement执行结束");
initLog(LOG_IN_RUN, null, null, LocalDateTime.now());
dealRes(); // 处理结果
}
}
private void close(ResultSet resultSet, Statement statement, Connection conn) {
// 从服务器里map里删除
LogStopUtil.logStatementMap.remove(automaticLog.logId);
close(resultSet, ResultSet.class.getName());
close(statement, Statement.class.getName());
close(conn, Connection.class.getName());
}
3.发送中止信号的接口service
@Override
public void stopLog(String id) {
// 发布消息
redisTemplate.convertAndSend(LogStopUtil.CHANNEL_LOG_STOP, id);
}
4.接收信号并处理中止sql
/**
* 处理订阅消息
*/
@Component
public class RedisSubscriber {
@Autowired
ScheduleLogService scheduleLogService;
public void onMessage(String message, String channel) {
switch (channel) {
case LogStopUtil.CHANNEL_LOG_STOP: {
scheduleLogService.subStepMessage(message);
break;
}
}
}
}
/**
*scheduleLogService的subStepMessage方法
*/
@Override
public void subStepMessage(String message) {
String id = message.substring(1, message.length() - 1);
Statement stmt = LogStopUtil.logStatementMap.get(id);
log.info("收到停止日志执行的消息 [{}]", id);
if (stmt != null) {
try {
stmt.cancel();
log.info(id + " cancel成功");
updateLog(id);
} catch (SQLException e) {
log.error(id + " cancel失败: [{}]", e.getMessage());
updateLogError(id, e.getMessage());
}
}
}