惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
腾讯CDC
Jina AI
Jina AI
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
小众软件
小众软件
M
MIT News - Artificial intelligence
MyScale Blog
MyScale Blog
D
Docker
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
月光博客
月光博客
L
LangChain Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - Franky
C
Check Point Blog
U
Unit 42
人人都是产品经理
人人都是产品经理

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12)
踩坑实录:读写分离导致批量删除逻辑问题
暮色之狐 · 2026-05-16 · via 博客园_首页

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

原始代码实现

@Override
public void clearExpireOrder(LocalDate expireDate) {
    log.info("开始清理过期订单, 过期日期: {}", expireDate);
    int batchSize = 200;
    long totalDeletedCount = 0;
    while (true) {
        List<ExposureOrder> expireOrders = this.list(Wrappers.<ExposureOrder>lambdaQuery()
                .select(ExposureOrder::getId).le(ExposureOrder::getAdEndDate, expireDate.atStartOfDay())
                .last("LIMIT " + batchSize));
        if (expireOrders == null || expireOrders.isEmpty()) {
            break;
        }
        List<Long> 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<ExposureOrder> w = Wrappers.<ExposureOrder>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);  
}

经验总结

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