当前位置: 首页 > news >正文

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

原始代码实现

@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,这看起来像是阿里云的代理链接地址。经检查,我们的数据库配置为一主一从,并启用了数据库代理的读写分离功能。

阿里云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); }
http://www.jsqmd.com/news/1149086/

相关文章:

  • Unity开发Oculus Quest虚拟键盘:官方集成与自定义方案全解析
  • DeepSEED 2023 合成启动子设计实战:3类任务成功率提升与序列多样性分析
  • 价值函数与Q函数:从贝尔曼方程到DQN实战的5个关键推导步骤
  • 写给后端新手的建议:写好日志和错误处理比写代码更重要
  • YOLO 数据集划分 3 种策略对比:7:2:1 vs 8:2 vs 分层抽样,mAP 影响实测
  • Unlock Music:开源音乐格式转换解决方案
  • Python与Java跨语言加密互操作实战:AES-CBC与RSA-OAEP详解
  • Unity异步编程优化:C-Sharp-Promise实战与架构集成指南
  • MCP服务器安全加固实战:从datagouv-mcp项目看Web服务纵深防御
  • 黑箱API微调实战指南:从原理到企业级应用部署
  • 如何快速实现LLM自动化标注:Autolabel完整指南
  • 跨设备键鼠共享终极指南:用Lan Mouse打造高效多系统工作流
  • Aximmetry+UE5虚拟演播室搭建指南:从硬件配置到实时合成全流程
  • 正点原子开拓者FPGA数码管万年历:三页轮显+物理按键校时+蜂鸣器闹钟
  • Unity热更新实战:YooAsset与HybridCLR集成全链路指南
  • 3个意想不到的用法:让Ext2Read发挥200%效能,轻松跨越Windows与Linux的鸿沟
  • Selenium自动化登录实战:从基础表单到验证码与OAuth的稳健解决方案
  • Cursor Free VIP:AI编程助手解锁完整指南,让你免费使用高级功能
  • openeuler/libarchive-rust最佳实践:Rust编码规范与内存安全技巧
  • PaddleOCR 模型量化与剪枝实战:PACT + FPGM 组合拳压缩 500KB 方向分类器
  • 打造专属B站视频下载系统:个性化配置与智能资源管理完全指南
  • 通达信缠论可视化插件:5分钟掌握专业K线分析技术
  • Unity游戏鸿蒙迁移实战:性能优化与分布式功能实现
  • 深度解析:如何高效使用unveilr进行微信小程序源码分析
  • UE4SS OpenGL3后端白屏问题深度解析与解决方案
  • Java毕业设计实战:超市进销存系统(SpringBoot+Vue)含可运行源码、数据库脚本与双端部署指南
  • Cocos Creator光照系统全解析:从Phong/PBR原理到点光源、聚光灯与全局光照实战
  • UE5载具游戏开发:零基础入门到原型实现
  • 21天学pcie--PCIe 的三种地址空间(核心篇)
  • PyTorch 2.0 CNN 图像分类实战:FashionMNIST 数据集 3 轮训练达 88% 准确率