导读:本期聚焦于小伙伴创作的《SQL任务重试记录机制如何实现?SQL保证最终一致方案有哪些?》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《SQL任务重试记录机制如何实现?SQL保证最终一致方案有哪些?》有用,将其分享出去将是对创作者最好的鼓励。

在分布式系统里,SQL任务可能因为网络波动、数据库临时不可用、业务校验失败等原因执行失败,这时候需要设计可靠的重试记录机制,同时配合合适的方案保证数据最终一致,避免重试过程中出现数据重复、数据丢失等问题。

SQL任务重试记录机制如何实现?SQL保证最终一致方案有哪些?

SQL任务重试记录机制设计

重试记录机制的核心是把每次SQL任务的执行状态、重试信息持久化存储,方便后续查询、重试调度和状态追踪,主要包含以下几个核心部分。

重试记录表设计

首先需要创建一张重试记录表,用来存储所有需要重试的SQL任务相关信息,表结构可以参考下面的设计:

CREATE TABLE sql_retry_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    task_id VARCHAR(64) NOT NULL COMMENT '业务任务唯一ID,保证幂等',
    sql_content TEXT NOT NULL COMMENT '需要执行的SQL内容',
    sql_params TEXT COMMENT 'SQL对应的参数,JSON格式存储',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待执行 1执行中 2执行成功 3执行失败 4超过最大重试次数',
    retry_count INT NOT NULL DEFAULT 0 COMMENT '当前重试次数',
    max_retry_count INT NOT NULL DEFAULT 3 COMMENT '最大重试次数',
    next_retry_time DATETIME COMMENT '下次重试时间',
    error_msg TEXT COMMENT '最后一次执行失败的错误信息',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    UNIQUE KEY uk_task_id (task_id),
    KEY idx_status_next_retry_time (status, next_retry_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SQL任务重试记录表';

重试调度逻辑实现

重试调度需要定时扫描重试记录表中状态为待执行、执行失败,且下次重试时间小于当前时间的记录,然后执行对应的SQL任务,执行完成后更新记录状态。下面是一个简单的调度逻辑示例,使用Java实现:

import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class SqlRetryScheduler {
    // 数据库连接信息
    private static final String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf8";
    private static final String JDBC_USER = "root";
    private static final String JDBC_PASSWORD = "123456";

    // 扫描待重试的任务
    public List<RetryRecord> scanPendingRetryTasks() {
        List<RetryRecord> taskList = new ArrayList<>();
        String sql = "SELECT id, task_id, sql_content, sql_params, retry_count, max_retry_count FROM sql_retry_record " +
                "WHERE status IN (0, 3) AND next_retry_time <= ? LIMIT 100";
        try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
             PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now()));
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                RetryRecord record = new RetryRecord();
                record.setId(rs.getLong("id"));
                record.setTaskId(rs.getString("task_id"));
                record.setSqlContent(rs.getString("sql_content"));
                record.setSqlParams(rs.getString("sql_params"));
                record.setRetryCount(rs.getInt("retry_count"));
                record.setMaxRetryCount(rs.getInt("max_retry_count"));
                taskList.add(record);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return taskList;
    }

    // 执行SQL任务
    public boolean executeSqlTask(RetryRecord record) {
        String updateStatusSql = "UPDATE sql_retry_record SET status = ?, retry_count = ?, error_msg = ?, next_retry_time = ?, update_time = NOW() WHERE id = ?";
        try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
            conn.setAutoCommit(false);
            // 执行目标SQL
            try (PreparedStatement ps = conn.prepareStatement(record.getSqlContent())) {
                // 这里可以解析sql_params设置参数,简化处理直接执行
                ps.executeUpdate();
                // 执行成功,更新状态为成功
                try (PreparedStatement updatePs = conn.prepareStatement(updateStatusSql)) {
                    updatePs.setInt(1, 2);
                    updatePs.setInt(2, record.getRetryCount() + 1);
                    updatePs.setString(3, null);
                    updatePs.setTimestamp(4, null);
                    updatePs.setLong(5, record.getId());
                    updatePs.executeUpdate();
                }
                conn.commit();
                return true;
            } catch (SQLException e) {
                // 执行失败,更新重试信息
                conn.rollback();
                int newRetryCount = record.getRetryCount() + 1;
                int nextStatus = newRetryCount >= record.getMaxRetryCount() ? 4 : 3;
                // 重试间隔采用指数退避策略,1s、2s、4s依次递增
                LocalDateTime nextRetryTime = nextStatus == 3 ? LocalDateTime.now().plusSeconds((long) Math.pow(2, newRetryCount)) : null;
                try (PreparedStatement updatePs = conn.prepareStatement(updateStatusSql)) {
                    updatePs.setInt(1, nextStatus);
                    updatePs.setInt(2, newRetryCount);
                    updatePs.setString(3, e.getMessage());
                    updatePs.setTimestamp(4, nextRetryTime == null ? null : Timestamp.valueOf(nextRetryTime));
                    updatePs.setLong(5, record.getId());
                    updatePs.executeUpdate();
                }
                conn.commit();
                return false;
            }
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    // 重试记录实体类
    static class RetryRecord {
        private Long id;
        private String taskId;
        private String sqlContent;
        private String sqlParams;
        private int retryCount;
        private int maxRetryCount;
        // 省略getter和setter
    }
}

SQL保证最终一致的常见方案

重试机制只能解决临时失败的问题,要保证最终一致性还需要配合其他方案,下面介绍几种常用的实现方式。

幂等性设计

幂等性是指同一个SQL任务执行多次,产生的结果和执行一次相同,这是重试场景下保证数据一致的基础。实现幂等性的方式有很多,比如使用唯一业务ID,在SQL执行前先判断记录是否存在,或者使用数据库的唯一索引避免重复插入。

比如插入用户订单的场景,订单ID是唯一的,可以在订单表上对订单ID加唯一索引,重试插入时如果已经存在就会报错,捕获异常后判断是重复插入即可标记为执行成功:

-- 订单表,order_id加唯一索引
CREATE TABLE user_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id VARCHAR(64) NOT NULL COMMENT '订单唯一ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

事务补偿机制

如果SQL任务涉及多个数据库操作,或者跨服务调用,单个事务无法覆盖所有操作,这时候可以使用事务补偿机制。也就是执行正向操作的时候记录补偿SQL,如果后续操作失败,就执行补偿SQL回滚之前的操作。

比如转账场景,从A账户扣钱,给B账户加钱,两个操作需要保证一致,实现逻辑如下:

public boolean transferAmount(String taskId, Long fromUserId, Long toUserId, BigDecimal amount) {
    // 先插入重试记录,sql_content存储正向操作SQL,sql_params存储参数
    String insertRetrySql = "INSERT INTO sql_retry_record (task_id, sql_content, sql_params, status, max_retry_count, next_retry_time) VALUES (?, ?, ?, 0, 3, NOW())";
    String forwardSql = "UPDATE user_account SET balance = balance - ? WHERE user_id = ? AND balance >= ?";
    String compensateSql = "UPDATE user_account SET balance = balance + ? WHERE user_id = ?";
    // 参数JSON格式:{"forwardParams":[amount,fromUserId,amount], "compensateParams":[amount,fromUserId], "toUserId":toUserId, "toAmount":amount}
    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        conn.setAutoCommit(false);
        // 执行正向操作1:A账户扣钱
        try (PreparedStatement ps = conn.prepareStatement(forwardSql)) {
            ps.setBigDecimal(1, amount);
            ps.setLong(2, fromUserId);
            ps.setBigDecimal(3, amount);
            int rows = ps.executeUpdate();
            if (rows == 0) {
                throw new RuntimeException("A账户余额不足");
            }
        }
        // 执行正向操作2:B账户加钱
        String addSql = "UPDATE user_account SET balance = balance + ? WHERE user_id = ?";
        try (PreparedStatement ps = conn.prepareStatement(addSql)) {
            ps.setBigDecimal(1, amount);
            ps.setLong(2, toUserId);
            ps.executeUpdate();
        }
        conn.commit();
        return true;
    } catch (Exception e) {
        // 执行补偿操作,给A账户加回钱
        try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
             PreparedStatement ps = conn.prepareStatement(compensateSql)) {
            ps.setBigDecimal(1, amount);
            ps.setLong(2, fromUserId);
            ps.executeUpdate();
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
        // 记录重试任务,后续重试
        return false;
    }
}

本地消息表方案

本地消息表方案适合跨服务的SQL任务最终一致场景,核心思路是在本地数据库维护一张消息表,执行业务SQL和插入消息表放在同一个本地事务中,之后定时扫描消息表发送消息给其他服务,其他服务执行对应的SQL操作,执行完成后回调更新消息状态。

本地消息表结构可以参考:

CREATE TABLE local_message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id VARCHAR(64) NOT NULL COMMENT '消息唯一ID',
    task_id VARCHAR(64) NOT NULL COMMENT '关联的业务任务ID',
    message_content TEXT NOT NULL COMMENT '消息内容,包含需要执行的SQL信息',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待发送 1已发送 2已消费 3消费失败',
    retry_count INT NOT NULL DEFAULT 0 COMMENT '发送重试次数',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_message_id (message_id),
    KEY idx_status_create_time (status, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表';

业务操作和消息插入在同一个事务中,保证要么都成功要么都失败:

public boolean createOrderWithLocalMessage(String orderId, Long userId, BigDecimal amount) {
    try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
        conn.setAutoCommit(false);
        // 1. 插入订单
        String insertOrderSql = "INSERT INTO user_order (order_id, user_id, order_amount) VALUES (?, ?, ?)";
        try (PreparedStatement ps = conn.prepareStatement(insertOrderSql)) {
            ps.setString(1, orderId);
            ps.setLong(2, userId);
            ps.setBigDecimal(3, amount);
            ps.executeUpdate();
        }
        // 2. 插入本地消息,通知库存服务扣减库存
        String insertMessageSql = "INSERT INTO local_message (message_id, task_id, message_content, status) VALUES (?, ?, ?, 0)";
        String messageId = "msg_" + orderId;
        String messageContent = "{"sql":"UPDATE product_stock SET stock = stock - 1 WHERE product_id = 1001 AND stock >= 1", "params":[]}";
        try (PreparedStatement ps = conn.prepareStatement(insertMessageSql)) {
            ps.setString(1, messageId);
            ps.setString(2, orderId);
            ps.setString(3, messageContent);
            ps.executeUpdate();
        }
        conn.commit();
        return true;
    } catch (SQLException e) {
        e.printStackTrace();
        return false;
    }
}

注意事项

  • 重试记录表的扫描频率不要太高,避免给数据库造成太大压力,同时扫描数量要加限制,防止一次性加载太多数据。
  • 重试间隔建议采用指数退避策略,避免失败任务短时间大量重试加重系统负担。
  • 最终一致方案的选择要根据实际业务场景,比如单库场景优先用幂等性+重试,跨服务场景可以考虑本地消息表或者事务补偿。
  • 所有的重试和补偿操作都要记录详细的日志,方便后续排查问题。

SQL任务重试记录最终一致性事务补偿幂等性修改时间:2026-07-09 14:24:48

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。