• 踩坑实录:读写分离导致批量删除逻辑问题


    最近在帮同事review代码时,发现一段逻辑看似正常但执行结果却不符合预期的代码,特此记录问题排查过程。

    原始代码实现

    @Override
    public void clearExpireOrder(LocalDate expireDate) {
        log.info("开始清理过期订单, 过期日期: {}", expireDate);
        int batchSize = 200;
        long totalDeletedCount = 0;
        while (true) {
            List expireOrders = this.list(Wrappers.lambdaQuery()
                    .select(ExposureOrder::getId).le(ExposureOrder::getAdEndDate, expireDate.atStartOfDay())
                    .last("LIMIT " + batchSize));
            if (expireOrders == null || expireOrders.isEmpty()) {
                break;
            }
            List ids = expireOrders.stream().map(ExposureOrder::getId).collect(Collectors.toList());
            int deletedCount = this.removeByIds(ids) ? ids.size() : 0;
            totalDeletedCount += deletedCount;
            log.info("本批次删除 {} 条过期订单, 累计删除 {} 条", deletedCount, totalDeletedCount);
            if (deletedCount < batchSize) {
                break;
            }
        }
        log.info("过期订单清理完成, 总计删除 {} 条数据, 过期日期: {}", totalDeletedCount, expireDate);
    }
    

    这是一个清理过期订单的定时任务,传入一个过期时间,每次查询前200个过期订单并根据ID进行删除。然而,在生产环境执行后,发现仍有过期订单存在。查询日志显示:

    5 15, 2026 @ 03:00:01.850	开始清理过期订单, 过期日期: 2026-04-15
    5 15, 2026 @ 03:00:01.850	本批次删除 200 条过期订单, 累计删除 200 
    5 15, 2026 @ 03:00:01.850	本批次删除 0 条过期订单, 累计删除 200 
    5 15, 2026 @ 03:00:01.850	过期订单清理完成, 总计删除 200 条数据, 过期日期: 2026-04-15
    

    从功能上看,先查询后删除的方式虽然不是最佳方案,但代码逻辑上确实看不出明显问题。经过反复分析,仍未能找出错误所在。

    问题发现

    突然注意到数据库连接地址为prod-bd-traffic-mysql-shm-01.rwlb.rds.aliyuncs.com:3306,这看起来像是阿里云的代理链接地址。经检查,我们的数据库配置为一主一从,并启用了数据库代理的读写分离功能。

    file-20260516123331474

    file-20260516123331469

    阿里云RDS的数据库代理地址开启读写分离功能后,会根据执行的SQL自动将请求分发到RDS的主节点或从节点,实现读写分离。回到原始代码,问题出在以下执行流程:

    1. 执行查询操作(路由到从节点)
    2. 执行删除操作(路由到主节点)
    3. 再次执行查询操作(仍然路由到从节点,但此时从节点尚未同步主节点的删除数据)
    4. 第二次查询与第一次查询结果相同
    5. 执行删除操作,受影响行数为0
    6. 认为已没有需要删除的数据,跳出循环

    问题解决方案

    找到问题根源后,修改代码策略,不再采用先查询后删除的方式,而是直接执行DELETE语句:

    @Override  
    public void clearExpireOrder(LocalDate expireDate) {  
        log.info("开始清理过期订单, 过期日期: {}", expireDate);  
        int batchSize = 100;  
        int maxBatch = 1000;  
        int currentBatch = 0;  
        long totalDeletedCount = 0;  
        while (true) {  
            LambdaUpdateWrapper w = Wrappers.lambdaUpdate()  
                    .le(ExposureOrder::getAdEndDate, expireDate)  
                    .last(" LIMIT " + batchSize);  
            int deletedCount = this.getBaseMapper().delete(w);  
            totalDeletedCount += deletedCount;  
            log.info("本批次删除 {} 条过期订单, 累计删除 {} 条", deletedCount, totalDeletedCount);  
            if (deletedCount == 0) {  
                break;  
            } else if (currentBatch > maxBatch) {  
                log.warn("本次清理超过最大处理批次, 退出清理");  
                throw new RuntimeException("本次清理超过最大处理批次, 退出清理");  
            } else {  
                try {  
                    Thread.sleep(200);  
                } catch (InterruptedException e) {  
                    log.error("线程中断, {}", e.getMessage());  
                }  
            }  
            currentBatch ++;  
        }  
        log.info("过期订单清理完成, 总计删除 {} 条数据, 过期日期: {}", totalDeletedCount, expireDate);  
    }
    

    经验总结

    这次排查过程给我带来了重要警示:有时不符合预期的现象并非源于代码逻辑问题,而是由依赖项的特性导致。我们需要具备全局意识,从整体架构出发分析问题,否则仅局限于代码层面将难以发现根本原因。

  • 相关阅读:
    数据结构——树&二叉树
    TouchGFX之自定义控件
    【Unity】苹果(IOS)开发证书保姆级申请教程
    金仓数据库KingbaseES客户端编程接口指南-Perl DBI(4. DBI 数据库句柄对象)
    createNodeIterator使用+元素替换
    路由中的meta、params传参的一些问题(可传不可传,为空,搭配,点击传递多次参数报错)
    win下使用vscode+wsl2
    C++核心编程:P18->STL----常用算法(上)
    扬帆牧哲—跨境电商行业商标有多重要?
    复制控制 copy control(非平凡的类)
  • 原文地址:https://www.cnblogs.com/gloamfox/p/20058644