魔神公寓
94.55M · 2026-04-07
这篇文章结合一次真实开发场景,分享我在 MyBatis Plus 中处理大批量 in 条件更新的思路。相比直接一条 SQL 一把梭,我更倾向于按固定批次动态拆分、循环更新,以提升执行稳定性和可维护性。文章会从问题背景、直接写法的风险、分批方案的实现以及实际注意点几个方面展开,适合后端开发在处理大数据量更新场景时参考。
在日常开发里,批量更新其实是一个非常常见的需求。
比如:
这类需求表面上看起来不复杂,很多时候一条 update ... where id in (...) 就能搞定。
如果数据量不大,这么写通常也没什么问题。
但我在实际开发中遇到过一种情况:
需要更新的数据不是几十条、几百条,而是几千条甚至上万条。
这时候如果还按最直观的方式,直接把整个 idList 丢进 in 条件里,一开始可能也能执行成功,但从稳定性、数据库压力、异常排查和后续维护角度看,其实并不稳。
所以这篇文章我想结合一次真实场景,聊聊我对这个问题的处理思路:
先说下场景。
业务里有一类很常见的处理:
先根据条件查出一批符合要求的数据主键,然后再把这些数据统一更新成某个状态。
伪代码大概是这样:
List<Long> idList = queryNeedUpdateIds();
// 根据 idList 批量更新状态
update status = 'Y' where id in (idList)
这个需求本身不复杂,难点不在业务逻辑,而在数据量。
实际开发里,idList 的数量往往不是固定的,可能出现下面几种情况:
如果代码只在“小数据量思维”下设计,到了大数据量场景就容易出问题。
所以我后来处理这类需求时,会优先考虑一个问题:
先看最直接的写法。
如果项目里使用的是 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 更新到底,主要有下面几个原因。
在代码里看,idList 只是一个集合。
但到了数据库层面,它会被拼成一个很长的 in (...) 条件。
如果集合很大,最终形成的 SQL 可能非常长,阅读不友好,日志不友好,数据库执行也不轻松。
尤其是遇到异常时,一条又长又重的 SQL 排查起来也很难受。
一条大 SQL 更新几千、上万条数据,本质上是把压力集中在一次执行里。
这时候你要考虑的就不只是“代码跑没跑通”,还包括:
这些问题,往往在小数据量时感觉不到,但一旦数据量放大,很容易被放大出来。
如果一条特别大的更新 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();
}
}
这段代码很简单,但优点不少:
如果你有码洁(代码洁癖)想再写得更完整一点,可以把批次日志也加上:
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);
}
}
这样一来,后续如果线上真的出问题,最起码能快速知道是哪一批出了问题。
有些同学处理这类问题时,也会想到“分段”,但实现方式可能是手工判断,比如:
这种思路能解决问题,但我个人更倾向于用循环动态拆分,而不是手工写死边界。
原因很简单。
今天可能是 800 条,明天可能是 5000 条,后天可能是 12000 条。
如果边界写死,代码适应性就比较差。
本质上这就是一个“集合按固定大小切片”的问题,最自然的方式就是循环处理。
循环的写法更规整,后面如果想调整批次大小,改一个参数就行。
如果是手工拆很多段,后续维护起来会更麻烦。
所以我更喜欢这种写法:
分批更新并不是把集合切一切就结束了。
在真实项目里,我通常还会注意下面这些点。
batchSize 并不是越大越好,也不是越小越好。
通常我会根据以下几个因素去调:
一般可以先从 200、500、1000 这种量级试起,再根据实际效果调整。
这个虽然简单,但最好不要漏。
if (idList == null || idList.isEmpty()) {
return;
}
这样可以避免生成无意义逻辑,也避免某些场景下出现空条件更新的风险。
如果是后台任务或者定时任务场景,我一般会建议补上这些日志:
这种日志平时看起来不起眼,但真到线上排查时会非常有用。
这是很多人容易忽略的一点。
分批更新以后,事务要怎么控制,其实取决于业务要求。
优点是整体一致性强。
缺点是事务范围大,失败回滚代价也大。
优点是单批失败影响范围小,更容易控制。
缺点是如果业务要求“全有或全无”,那就不一定适合。
所以事务不是固定答案,关键是要看你业务需要的是哪种一致性。
很多时候我们写 Java 代码时,会下意识觉得“这不就是一个 lambdaUpdate() 吗”。
但真正要落地时,一定要意识到:
所以不能只看“代码优不优雅”,还得看数据库是否扛得住、执行是否稳定。
这里我不想把话说得太绝对。
不是所有 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 条件更新并不是不能写,而是:
更稳的做法通常是:
写业务代码时,我越来越觉得一件事:
尤其是这种批量更新场景,代码多写几行,换来的是更好的执行稳定性、更强的可维护性以及更低的线上风险,我觉得是值得的。
这篇文章分享的其实不是某个特别高级的技巧,而是一种很实用的开发思路:
如果你的项目里也有类似这种“根据一大批 id 批量更新状态”的需求,可以先看下当前写法是不是默认在“小数据量前提”下设计的。
如果数据量一旦放大就可能不稳,那尽早改成分批循环更新,通常会更安心一些。