咱们做开发的,谁还没跟分页打过几次架呢?特别是刚入行那会儿,觉得分页嘛,不就是“上一页下一页”,能有啥技术含量?结果真动手写了才发现,这里头的弯弯绕绕,简直能让人气得当场就想摔键盘。今儿咱就抛开那些官方文档的刻板说法,以一个在工位上挠破过头的过来人身份,跟你唠唠Java里那些分页技术的嗑。这篇文章不讲虚的,全是我自个儿踩坑踩出来的经验,希望能给正在为分页发愁的你一点实在的帮助。
刚开始接触那会儿,我最先搞明白的就是最原始的那一套——纯手工打造SQL分页。那时候用MyBatis,还不会用插件,领导让做个列表展示,我脑子一热,直接在业务层把数据全查出来,然后用Java代码的subList去切。现在回想起来,那操作简直就是在作死。那时候数据量小,才几百条,感觉还挺丝滑。后来上了生产,用户一多,数据涨到几万条,系统直接就卡死了。后来才晓得,我那种搞法叫“逻辑分页”,也叫内存分页,说白了就是把数据库当仓库,自己当搬运工,先把所有货搬到自己家(内存),再慢慢挑。而真正靠谱的搞法,应该是“物理分页”,也就是利用数据库自身的LIMIT关键字,让数据库只拿需要的那一丁点数据出来。就这么一个小小的认知改变,硬是把一个濒临崩溃的接口给救了回来-3-6。
后来项目用上了Spring Boot,我发现有个叫Spring Data JPA的东西,里头自带了一个Pageable接口,哎呀那叫一个方便啊!都不用自己拼SQL了,直接往方法里传个Pageable对象,它自动就把分页逻辑给你办了。那时候我天真地以为,这就是分页的终极形态了。但好景不长,遇到那种多表关联的复杂查询,这玩意儿就开始给我脸色看了。有一次我用@Query写了个JPQL,里头用了JOIN FETCH抓取子集集合,结果发现那个分页参数Pageable完全失效了,查出来的数据乱成一锅粥。后来翻遍各种帖子才晓得,这是JPA的一个大坑,你JOIN FETCH的时候,它为了保证对象关系的完整,会把分页给忽略掉-8。那段时间为了解决这个问题,我试过用子查询先查ID,再用ID去拿数据,也试过用DTO来接,总之那段时间对“java分页技术”真是又爱又恨,但也正是这些踩坑经历,让我对它的底层原理有了更深的理解,知道了它虽然便捷,但不是万能的,得摸清它的脾气才行。
再往后,项目规模越来越大,我开始接触到MyBatis-Plus和PageHelper这些神器。特别是PageHelper,一开始用起来确实爽,只要在查询前调用一句PageHelper.startPage,后面的查询自动就给你加上了分页。但我告诉你,这东西用不好也是个祖宗。我之前接手过一个老项目,那代码里PageHelper用得飞起,但有个坑是,这玩意儿是基于ThreadLocal实现的,如果你在startPage之后,紧接着执行的查询不是你想分页的那个,或者因为异常跳过了查询,那分页参数就一直残留在当前线程里,导致后面的无辜查询也被莫名其妙地分页了。后来我们就定了规矩,startPage必须紧挨着Mapper方法调用,中间不能有任何业务逻辑,这才能把它治得服服帖帖。其实不管是MyBatis-Plus还是PageHelper,它们帮咱们做的,本质上就是动态地拦截SQL语句,然后悄悄地给它拼接上Limit或者RowNum,实现的还是物理分页,只是让咱们少写了点代码罢了-6-9。
随着数据量继续膨胀,到了几百万、几千万的级别,我发现传统的LIMIT分页也开始顶不住了。这时候你翻到第100页,数据库得扫描前100页的数据才能给你丢掉,那效率低得吓人。这时候,圈子里开始流行一种叫“游标分页”或者“键集分页”的玩法。这种分页技术不依赖页码,而是依赖上一页最后一条记录的某个字段(通常是自增主键或者时间戳)。比如你查完第一页,最后一条记录的ID是100,查下一页的时候就直接带上where id > 100。这种方式简直是扫描数据的神器,不管你翻到多后面,查询速度都嗖嗖的-1。这时候我才彻底醒悟,哪有什么万能的技术,只有最适合当前场景的方案。对于实时性要求高的动态数据,这招比传统分页靠谱多了。
到了微服务时代,分页这问题又被拔高了一个难度。数据不在一个库里,而是分库分表存在不同的节点上。这时候你要是想按时间倒序取第二页的数据,那麻烦就大了。你得从每个分片里把前两页的数据都取出来,然后在内存里重新排序、合并,最后再截取。这不仅是性能问题,还有一致性的问题——你查的过程中,某个分片的数据变了,结果就全乱套了-2。为了解决这个问题,我们有时候不得不牺牲一点灵活性,比如禁止跨分片的深分页,或者用全局ID发号器来保证排序的全局唯一性。所以说,Java分页技术这趟水,深着呢。
说了这么多,其实我就想表达一个意思:写代码不能光会调API,还得懂它背后的原理。就像咱们今天聊的java分页技术,从小数据量的物理分页,到复杂查询的JPA分页陷阱,再到大数据量的游标分页,每一步都是技术演进的结果,也是咱们程序员成长的阶梯。下次你再遇到分页问题,不妨先别急着复制粘贴,坐下来想想:我现在处理的数据有多大?分页深度有多深?需不需要跨库?想清楚了这些,你选出来的方案,一定是最香的。反正我是被这些分页技术虐过千百遍,如今也算是待它如初恋了,希望这点掏心窝子的经验,能让你少走点弯路。