rowbounds实现的是逻辑分页(client-side paging),先全量加载数据至内存,再通过resultset游标跳过offset条、截取limit条,易引发oom、性能差、数据滞后及资源浪费。

MyBatis 用 RowBounds 实现的不是“内存分页”,而是逻辑分页(Client-side Paging)——它把符合条件的全部数据从数据库拉到 JVM 内存中,再在 ResultSet 层面跳过前 offset 条、取 limit 条。这个过程看似在“内存里分页”,但本质是全量加载 + 游标截取,不是真正意义的内存计算分页(比如 List.subList)。它的劣势非常明确,尤其在生产环境容易引发严重问题。
一次性加载全量数据,极易触发 OOM
RowBounds 不会改写 SQL,也不会加 LIMIT。例如你查“status = 'ACTIVE'”的用户,哪怕只要第 1 页 10 条,MyBatis 仍会执行 SELECT * FROM users WHERE status = 'ACTIVE'。如果该条件匹配 200 万行,这 200 万行记录就会经 JDBC 逐条读入内存(受 fetchSize 影响,但最终仍会全部加载),每条对象平均占 1KB,就是约 2GB 堆内存消耗。线上服务堆内存通常设为 2–4GB,一次请求就可能打爆。
- offset 越大,跳过的记录越多,但前面的数据仍要被加载和丢弃
- 并发稍高时,多个分页请求叠加,GC 压力剧增,频繁 Full GC 后仍无法回收,最终
java.lang.OutOfMemoryError: Java heap space - 有真实案例:用户表从 1 万增长到 120 万后,
new RowBounds(0, 10)导致凌晨三点服务崩溃
数据延迟与一致性风险
逻辑分页依赖“一次查询+多次截取”,整个结果集在查询时刻就已固化。这意味着:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 分页过程中新插入/更新/删除的数据不会反映在本次分页结果中
- 用户翻页时可能看到重复数据(如第 1 页最后一条和第 2 页第一条相同),或漏掉数据(因排序字段不唯一导致游标偏移错位)
- 不适合对实时性要求高的场景,比如订单列表、监控告警列表
数据库与网络资源浪费严重
即使应用层只想要 10 条,数据库仍要扫描、过滤、排序全部匹配行,并通过网络把完整结果集传给应用服务器:
- MySQL 执行
EXPLAIN可见 rows 扫描数巨大,索引效率被稀释 - 网络传输带宽占用高,尤其字段多、LOB 类型存在时
- 连接池中连接长时间被占用(ResultSet 未关闭前连接不可释放),并发高时易出现连接耗尽
不支持动态排序与复杂条件的稳定分页
RowBounds 分页基于 ResultSet 的绝对定位(ResultSet.absolute(offset + 1)),而 JDBC 驱动对 absolute() 的支持依赖数据库和驱动实现:
- 某些数据库(如 Oracle)不支持 scrollable ResultSet,会退化为循环 next(),性能更差
- 若 SQL 中含 GROUP BY、DISTINCT、UNION,或使用了窗口函数,ResultSet 可能不可滚动,导致 RowBounds 失效或抛异常
- 排序字段若存在大量重复值,offset 定位不稳定,翻页结果不可靠
不复杂但容易忽略:RowBounds 的便利是以牺牲系统稳定性为代价的。数据量一旦超过几千条,就该果断切换为物理分页(如手写 LIMIT/OFFSET、PageHelper 插件或 MyBatis-Plus 的分页插件)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










