摘要
这篇文章结合一次真实开发场景,分享我在 MyBatis Plus 中处理大批量 in 条件更新的思路。相比直接一条 SQL 一把梭,我更倾向于按固定批次动态拆分、循环更新,以提升执行稳定性和可维护性。文章会从问题背景、直接写法的风险、分批方案的实现以及实际注意点几个方面展开,适合后端开发在处理大数据量更新场景时参考。
前言
在日常开发里,批量更新其实是一个非常常见的需求。
比如:
- 根据一批 id 更新状态
- 根据一批主键批量打标
- 将一批处理中数据统一改成已完成
- 根据筛选结果批量回写某个字段
这类需求表面上看起来不复杂,很多时候一条 update ... where id in (...) 就能搞定。
如果数据量不大,这么写通常也没什么问题。
但我在实际开发中遇到过一种情况:
需要更新的数据不是几十条、几百条,而是几千条甚至上万条。
这时候如果还按最直观的方式,直接把整个 idList 丢进 in 条件里,一开始可能也能执行成功,但从稳定性、数据库压力、异常排查和后续维护角度看,其实并不稳。
所以这篇文章我想结合一次真实场景,聊聊我对这个问题的处理思路:
大批量 in 条件更新,不是不能直接写,而是更推荐做动态拆分、分批循环更新。
一、背景:为什么会有大批量 in 更新
先说下场景。
业务里有一类很常见的处理:
先根据条件查出一批符合要求的数据主键,然后再把这些数据统一更新成某个状态。
伪代码大概是这样:
List<Long> idList = queryNeedUpdateIds();
// 根据 idList 批量更新状态
update status = 'Y' where id in (idList)
这个需求本身不复杂,难点不在业务逻辑,而在数据量。
实际开发里,idList 的数量往往不是固定的,可能出现下面几种情况:
- 平时几十条、几百条
- 某些特殊时间点突然变成几千条
- 批量任务场景下直接上万条
如果代码只在“小数据量思维”下设计,到了大数据量场景就容易出问题。
所以我后来处理这类需求时,会优先考虑一个问题:
这段代码不仅要“能跑”,还要“在数据量变大时也尽量稳”。
二、最直观的写法:直接 in 更新
先看最直接的写法。
如果项目里使用的是 MyBatis Plus,很多人第一反应会这么写:
lambdaUpdate()
.in(DemoEntity::getId, idList)
.set(DemoEntity::getStatus, "Y")
.update();
或者更完整一点:
demoService.lambdaUpdate()
.in(DemoEntity::getId, idList)
.set(DemoEntity::getStatus, "Y")
.set(DemoEntity::getUpdateTime, LocalDateTime.now())
.update();
这种写法有几个优点:
- 代码少
- 可读性直观
- 小数据量场景下足够好用
- 业务表达也比较清楚
所以我要先说明一点:
这种写法不是错,而是有使用边界。
如果 idList 只有几十条、几百条,大多数时候这么写没问题。
但如果 idList 很大,这种“一把梭”的写法就不太稳了
三、为什么我不建议直接一把梭
很多时候,问题不在于“能不能执行成功”,而在于:
这种写法在真实业务场景下稳不稳。
我不太建议在大数据量场景下,直接一条 in 更新到底,主要有下面几个原因。
1)SQL 可能会非常长
在代码里看,idList 只是一个集合。
但到了数据库层面,它会被拼成一个很长的 in (...) 条件。
如果集合很大,最终形成的 SQL 可能非常长,阅读不友好,日志不友好,数据库执行也不轻松。
尤其是遇到异常时,一条又长又重的 SQL 排查起来也很难受。
2)单次更新范围过大,数据库压力更集中
一条大 SQL 更新几千、上万条数据,本质上是把压力集中在一次执行里。
这时候你要考虑的就不只是“代码跑没跑通”,还包括:
- 数据库是否容易出现锁竞争
- 执行时间会不会明显变长
- 线上高峰期会不会影响别的业务
- 同一时刻是否还有其他任务也在更新
这些问题,往往在小数据量时感觉不到,但一旦数据量放大,很容易被放大出来。
3)失败成本高,重试不方便
如果一条特别大的更新 SQL 执行失败,处理起来通常比较被动:
- 很难快速定位是哪一段数据有问题
- 重试时通常还是整批重来
- 日志里信息也未必足够细
- 排查时不容易定位到具体批次或具体子集
从可控性角度看,这种方式不够友好。
4)后续扩展性差
很多代码刚写的时候只需要“批量更新一下状态”,但真实业务经常会慢慢加需求:
- 要分批打印日志
- 要记录每批处理耗时
- 要支持失败重试
- 要统计更新成功/失败数量
- 要在每批之间做一些额外判断
如果一开始就是一条 SQL 一把梭,后面再改就会比较别扭。
而如果一开始就采用分批循环的方式,扩展性会好很多。
四、我最后采用的方案:分批循环更新
我最后采用的方案其实很简单:
把大集合拆成多个小批次,每批单独执行更新。
也就是说,不追求“一条 SQL 更新完全部数据”,而是把一次大更新拆成多次小更新。
这样做的好处很直接:
- 单次 SQL 更短
- 执行压力更分散
- 失败后更容易定位问题
- 方便后续加日志、监控和重试
- 代码对数据量的适应性更好
虽然看起来代码比直接一条 in 稍微多几行,但在真实业务里,我觉得这是值得的。
五、代码实现:一个通用的分批更新写法
下面给一版我比较常用的写法。
public void batchUpdateStatus(List<Long> idList) {
if (idList == null || idList.isEmpty()) {
return;
}
// 每批数量,可根据业务和数据库情况调整
final int batchSize = 500;
for (int i = 0; i < idList.size(); i += batchSize) {
int end = Math.min(i + batchSize, idList.size());
List<Long> subList = idList.subList(i, end);
demoService.lambdaUpdate()
.in(DemoEntity::getId, subList)
.set(DemoEntity::getStatus, "Y")
.set(DemoEntity::getUpdateTime, LocalDateTime.now())
.update();
}
}
这段代码很简单,但优点不少:
- 容易理解
- 没有引入额外复杂度
- 支持任意规模的 idList
- 批次大小可调
- 后续很方便加日志和异常处理
如果你有码洁(代码洁癖)想再写得更完整一点,可以把批次日志也加上:
public void batchUpdateStatus(List<Long> idList) {
if (idList == null || idList.isEmpty()) {
return;
}
final int batchSize = 500;
int total = idList.size();
for (int i = 0; i < total; i += batchSize) {
int end = Math.min(i + batchSize, total);
List<Long> subList = idList.subList(i, end);
log.info("开始批量更新,第{}批,范围:[{}, {}), 本批数量:{}",
(i / batchSize) + 1, i, end, subList.size());
boolean success = demoService.lambdaUpdate()
.in(DemoEntity::getId, subList)
.set(DemoEntity::getStatus, "Y")
.set(DemoEntity::getUpdateTime, LocalDateTime.now())
.update();
log.info("批量更新结束,第{}批,执行结果:{}",
(i / batchSize) + 1, success);
}
}
这样一来,后续如果线上真的出问题,最起码能快速知道是哪一批出了问题。
六、为什么我更喜欢“动态循环”,而不是手工拆几段
有些同学处理这类问题时,也会想到“分段”,但实现方式可能是手工判断,比如:
- 前 1000 条一段
- 后 1000 条一段
- 再后面一段
这种思路能解决问题,但我个人更倾向于用循环动态拆分,而不是手工写死边界。
原因很简单。
1)数据量本身是不确定的
今天可能是 800 条,明天可能是 5000 条,后天可能是 12000 条。
如果边界写死,代码适应性就比较差。
2)动态循环更自然
本质上这就是一个“集合按固定大小切片”的问题,最自然的方式就是循环处理。
3)维护成本更低
循环的写法更规整,后面如果想调整批次大小,改一个参数就行。
如果是手工拆很多段,后续维护起来会更麻烦。
所以我更喜欢这种写法:
数据有多大,就按统一规则拆多少批,代码自动适配,而不是人为把边界写死。
七、批量更新时,我会额外注意这几个点
分批更新并不是把集合切一切就结束了。
在真实项目里,我通常还会注意下面这些点。
1)批次大小不要拍脑袋
batchSize 并不是越大越好,也不是越小越好。
通常我会根据以下几个因素去调:
- 单次更新数据量
- 数据库性能
- 当前业务高峰情况
- 表的更新频率
- 是否存在锁竞争风险
一般可以先从 200、500、1000 这种量级试起,再根据实际效果调整。
2)空集合要提前返回
这个虽然简单,但最好不要漏。
if (idList == null || idList.isEmpty()) {
return;
}
这样可以避免生成无意义逻辑,也避免某些场景下出现空条件更新的风险。
3)最好补批次日志
如果是后台任务或者定时任务场景,我一般会建议补上这些日志:
- 总数据量
- 当前第几批
- 每批数量
- 每批是否成功
- 是否有异常
这种日志平时看起来不起眼,但真到线上排查时会非常有用。
4)事务边界要提前想清楚
这是很多人容易忽略的一点。
分批更新以后,事务要怎么控制,其实取决于业务要求。
方案一:整批一个事务
优点是整体一致性强。
缺点是事务范围大,失败回滚代价也大。
方案二:每批一个事务
优点是单批失败影响范围小,更容易控制。
缺点是如果业务要求“全有或全无”,那就不一定适合。
所以事务不是固定答案,关键是要看你业务需要的是哪种一致性。
5)不要只盯代码层,要考虑数据库层
很多时候我们写 Java 代码时,会下意识觉得“这不就是一个 lambdaUpdate() 吗”。
但真正要落地时,一定要意识到:
代码层的一行,到了数据库层可能是一条很重的 SQL。
所以不能只看“代码优不优雅”,还得看数据库是否扛得住、执行是否稳定。
八、什么时候可以直接用 in,什么时候建议分批
这里我不想把话说得太绝对。
不是所有 in 更新都要分批,也不是一看到集合就必须切。
我更倾向于按场景判断。
可以直接 in 更新的情况
- 数据量不大,比如几十条、几百条
- 只是普通后台功能
- 对数据库压力影响很小
- 对异常重试和批次追踪要求不高
这种场景下,直接写:
lambdaUpdate()
.in(DemoEntity::getId, idList)
.set(DemoEntity::getStatus, "Y")
.update();
完全可以,简单高效。
更建议分批的情况
- 数据量大,几千条甚至上万条
- 是批量任务、定时任务或补数据任务
- 对执行稳定性要求高
- 后续可能需要日志、监控、失败重试
- 不希望一次性把数据库压力打满
这种场景下,我更推荐分批循环更新。
九、再进一步:可以封装成一个通用工具方法
如果你项目里这类场景比较多,其实可以再往前走一步,把“集合分批处理”封装成一个公共方法。
比如:
public static <T> void batchProcess(List<T> list, int batchSize, Consumer<List<T>> consumer) {
if (list == null || list.isEmpty()) {
return;
}
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<T> subList = list.subList(i, end);
consumer.accept(subList);
}
}
然后业务里这样用:
batchProcess(idList, 500, subList ->
demoService.lambdaUpdate()
.in(DemoEntity::getId, subList)
.set(DemoEntity::getStatus, "Y")
.set(DemoEntity::getUpdateTime, LocalDateTime.now())
.update()
);
这样做的好处是:
- 代码更统一
- 复用性更高
- 后续别的批量场景也能复用
- 不用每个地方都自己写一遍循环
当然,如果你项目里类似场景不多,直接在业务代码里写循环也完全没问题。
十、我的结论
最后总结一下我的看法。
在 MyBatis Plus 里,大批量 in 条件更新并不是不能写,而是:
在大数据量场景下,不建议直接一条 SQL 一把梭。
更稳的做法通常是:
- 根据数据规模动态拆分
- 按固定批次循环更新
- 结合业务要求设计事务边界
- 补必要的日志和异常处理
- 不只考虑代码能不能跑,还要考虑数据库层面稳不稳
写业务代码时,我越来越觉得一件事:
很多时候,“能跑”不等于“稳”。
尤其是这种批量更新场景,代码多写几行,换来的是更好的执行稳定性、更强的可维护性以及更低的线上风险,我觉得是值得的。
总结
这篇文章分享的其实不是某个特别高级的技巧,而是一种很实用的开发思路:
面对大数据量更新,不要只追求代码最短,而要优先考虑方案是否稳定、可控、可维护。
如果你的项目里也有类似这种“根据一大批 id 批量更新状态”的需求,可以先看下当前写法是不是默认在“小数据量前提”下设计的。
如果数据量一旦放大就可能不稳,那尽早改成分批循环更新,通常会更安心一些。