Redis发布订阅模式实战-远程中止SQL

需求

成功运行某个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());
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值