PageHelper分页和ThreadLocal

PageHelper分页

PageHelper可以帮助我们后端数据分页,具体的使用场景如下图:
在这里插入图片描述
那么它的原理是什么呢?在讲述它的原理之间我们先来说下ThreadLocal和ThreadLocalMap的作用。

ThreadLocalMap和ThreadLocal

每一个线程都有一个ThreadLocalMap集合,这个集合的key是ThreadLocal,这个集合的value是我们要存储的某个值。
所以一个线程里面其实会牵涉到多个ThreadLocal对象。

为什么一个线程需要设置一个专属的ThreadLocalMap呢?因为避免当前线程的数据被污染,比如说当A线程使用PageHelper.startPage(1,6)方法进行分页查询的时候,当执行完这句代码之后就会把分页参数设置到ThreadLocalMap里面,比如我现在的分页参数是查询当前页pageNum为1的数据,然后每页的数据大小pageSize为6,其实也就是查询数据库表的前六条数据。那这个时候我们就需要把pageNum和pageSize这两个关键的参数设置到ThreadLocalMap保存。那么问题来了,假如这个时候别的地方的线程也调用了PageHelper.startPage方法去设置参数,这不就冲突了吗?
因此 每个线程我们都会设置一个私有数据存储的地方,这个私有区域只有当前线程才能访问,其他线程不能访问。我们每个线程的私有区域其实就是ThreadLocalMap。

那么问题又来了,既然都已经有了线程私有区域ThreadLocalMap了,为什么我们还需要设置多个ThreadLocal作为key呢?原因很简单,因为一个线程里面可以操作多个客户端,比如在netty模型里面,一个线程就可以处理多个客户端连接,假如这多个客户端都需要分页的话,那么我们需要把分页设置数据pageNum和pageSize存储到那个地方呢?这些客户端存储的数据怎么区分呢?答案就是使用ThreadLocal进行区分,所以你可以把一个ThreadLocal理解成当前线程处理的一个客户端,然后value数据就是当前线程为当前客户端保存的私有线程数据。

那么这样的话,我们分页的时候,就可以通过ThreadLocal为每个客户端保存它专属的分页设置数据了,所有的线程互不影响,并且一个线程里面的所有的客户端也互不影响。

在这里插入图片描述
可以发现每个线程都有一个ThreadLocalMap私有数据存储空间。里面的key是ThreadLocal类型,value就是具体的存储的数据。

执行完PageHelper.startPage之后,分页参数存储到哪里了?

分页参数存储到了PageHelper的父类PageMethod的ThreadLocal属性中了如下图:
在这里插入图片描述
在这里插入图片描述
存储到了PageMethod的ThreadLocal中,每个客户端对应一个ThreadLocal私有区域,里面存储的是Page分页相关信息,接下来看一下Page分页信息都有什么,如下图:
在这里插入图片描述
最常见的就是当前页数,每页大小,总数据条数,以及总页数等。

Page和List的关系?

Page是保存分页数据信息的。Page是List的一个子类,如下图:
在这里插入图片描述

PageInterceptor分页拦截器的作用?

PageHelper内部实现了一个名为PageInterceptor的拦截器,该拦截器会被MyBatis加载到拦截器链中。当MyBatis执行查询操作的时候,PageInterceptor会在真正执行查询sql语句之前,拦截sql语句,为什么呢?因为PageHelper需要去进行分页查询,而分页查询则必须去修改原先的sql查询语句,比如说拦截到sql查询语句之后,我们会去修改这个sql语句,跟句我们ThreadLocal里面之前获取的Page分页参数,去改造查询sql语句,比如说加一个limit关键字,进行适当的查询,生成最新的sql,然后去执行这个sql语句。
这样其实也就实现了我们的分页查询。

在这里插入图片描述
在执行UserMapper.selectAll相关的sql查询之前,PageInteceptor分页拦截器会拦截sql语句并进行修改。

不知道你有没有想一个问题,就是在下一句代码执行的时候,也就是new PageInfo执行的时候,它要求参数必须是Page类型才可以进行数据设置,但是我们的userList分明是个List集合啊!那么问题来了,为什么list集合在后面变成了Page类型呢?我们代码里面也没有手动的转换啊?这是怎么回事呢?其实是因为当我们的PageInteceptor分页插件拦截查询sql语句之后,修改sql语句,然后执行sql语句获取执行数据集合的时候,得到的集合其实是Page类型,然后PageInteceptor分页插件再把这个Page子类型向上转型为它的父类List类型。因此我们得到的userList集合类型是可以向下转型为Page类型的,它实际上是一个Page类型。

那么还有一个问题,为什么我们的PageInfo可以通过userList得到所有的数据条数,比如我们数据库表总数据是11条,但我们每页大小是6条,现在分页之后,我们取出第一页数据,那么在userList其实我们查询出来的数据只有前六条而已,那么为什么在使用PageInfo的构造函数之后,参数是userList,但是我们却可以得到总数据条数是11条呢?
其实这也是PageInteceptor分页插件的功劳,因为我们知道改造sql语句之前的sql是什么样子的,没有改造之前查询的就是总数据条数,我们可以在PageInteceptor分页拦截器拦截的时候获取总数据条数,然后设置给Page,最后Page转换为list。然后在PageInfo中list又会向下转型为Page,我们也就在PageInfo中得到总数据条数了。

注意如果要想PageInterceptor分页拦截器生效,那么必须需要在mybatis配置文件中声明使用PageInterceptor插件,如下图:
在这里插入图片描述
不然的话此分页插件失效,那么我们的整体的分页查询也就会失败了。

PageInfo的作用与结构?

首先说下PageInfo的作用?PageInfo是最终取分页数据的对象,比方说我们的分页查询的数据集合,当前页,每页大小,总数据条数,上一页是多少页,下一页是多少页等,我们程序员都是从PageInfo里面读取的。但是有人可能会有疑问,这些东西Page对象里面基本上也有啊,为什么不从Page对象里面读取呢?因为Page对象是面向源码的,源码读取数据的时候确实是从Page对象里面,比如PageInterceptor分页插件存储数据的时候就会用到Page对象。
但我们最终自己数据读取的时候是从PageInfo里面读取的,如下图:
在这里插入图片描述

接下来看下PageInfo对象的结构,如下图:
在这里插入图片描述
除了一些基本的分页数据,可以发现PageInfo类还继承了PageSerializable类,这个类里面主要是存储我们的分页数据list集合和查询总数据total的,如下图:
在这里插入图片描述

最后看下引入的pagehelper分页依赖坐标与mybatis坐标

在这里插入图片描述

 <dependencies>
        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.16</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>6.1.0</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.10</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
    </dependencies>

ThreadLocal的进一步总结,以及另一个使用场景

ThreadLocal创建副本

当调用ThreadLocal的set方法或者get方法的时候,会给当前线程创建一个副本,这个副本里面只保存当前线程处理的客户端的数据信息,比如说在netty中,一个线程会处理多个客户端请求,比如当前线程处理了1000个客户端请求,那么当在当前线程中调用ThreadLocal的set方法或者是get方法是,都会创建一个线程副本,怎样创建的呢?首先在ThreadLocal的set方法或者get方法中都可以得到当前线程CurrentThread,然后我们会创建一个ThreadLocalMap集合放到当前线程属性中,如下图:
在这里插入图片描述
在这里插入图片描述
上面我们分析的是ThreadLocal的set方法,从上面的方法中可以看到,当调用ThreadLocal的方法之后,会得到当前线程Thread,然后往当前线程中创建一个副本ThreadLocalMap。这个副本是一个map集合,key就是ThreadLocal类型,表示的是一个线程对应的多个客户端中的某一个客户端,然后value对应的就是给这个客户端存储的值,这个值只能当前线程的当前客户端才能使用,因此我们使用ThreadLocal类就可以避免出现线程安全问题了,因为一个客户端通过ThreadLocal存储的数据,只能这个客户端使用,既便是当前线程的其他客户端也是不能使用的。

ThreadLocal.withInitial(…)方法的作用

ThreadLocal#withInitial方法的作用主要是给当前线程对应的副本ThreadLocalMap设置value类型的,因为副本ThreadLocalMap是一个map集合类型,并且它的键也已经确定了,就是ThreadLocal类型,代表的是一个线程处理的多个客户端中的一个,但是它的value值类型并没有确定,其实也就是我们的客户端需要存储什么类型的数据。
如下图:
在这里插入图片描述

上图中我们客户端存放到线程副本ThreadLocalMap中的局部变量类型就是HashMap<String, String>类型。

需求场景

假如现在用户在某个浏览器上操作界面,要给服务端发送请求,然后服务端内部有个拦截器,会拦截用户请求,然后去得到用户操作的浏览器信息和操作时间的信息,这个时候如果要用ThreadLocal来实现,要怎么做呢?

首先我们先确定客户端需要存储什么类型的数据,这里我设置的是存储HashMap<Srting, String>类型的数据,如下图:
在这里插入图片描述
然后拦截器中会调用我们TestThreadLocal类的方法,这里先忽略掉!
接着我们就是把用户相关的数据存放到ThreadLocal对应的一个HashMap中,对应的就是setUserOperateInfo方法;
最后就是其他的业务类通过getUseBrowser方法和getOperateTime方法去获取到用户操作信息了。

思考一个问题,直接写HashMap类型行不行,不放到ThreadLocal里面

上面的需求场景其实无非也就是把用户浏览器操作信息存储到HashMap集合中嘛,那为什么我们还要把HashMap集合类型写到ThreadLocalMap副本的键ThreadLocal对应的局部变量中呢?直接写不行吗?
是的不行。
假如说你这里直接写了HashMap类型,然后把用户的浏览器操作信息都放到了这个类型里面,那么就会出现一个问题,比如说在netty模型中,一个线程是可以处理多个客户端请求的,但每个客户端需要存储的key键都是一样的,因此这就是出现线程安全问题了,并且我们最终也只能存储一份数据,就是只能存储两个key,并且所有的客户端都能任意修改其他客户端前面已经存储的数据,这显然是不合理的。因此我们不能不使用ThreadLocal。

而当我们使用了ThreadLocal之后,就不会出现上面的问题了,当我们想要存储某个客户端的key的时候,会先通过当前线程得到当前线程的副本ThreadLocalMap,然后再根据当前客户端对应的ThreadLocal值其实也就是key键值,最后我们就能通过key去线程副本ThreadLocalMap中拿到当前客户端对应的数据结构属性HashMap集合了,这个集合属性是当前客户端唯一使用的,别的客户端是访问不到的,因此这就可以避免线程安全问题了。

threadlocal的宏观理解

以后就可以把threadlocal宏观理解成一个线程常量池,这个常量池里面可以存储数据信息,好处是它可以确保每个客户端的数据信息都是互相分离的,既便有10个客户端使用的是同一个线程,它也能保证你这是个客户端里面存储的数据分别在threadlocal常量池的十个不同的地方,不会互相影响。
因此当我们取threadlocal常量池中的值的时候,可以直接threadlocal.get()先调用get方法,然后再取具体存储的值。

背后的原理就是,每个现成Thread t都会有一个ThreadLocalMap集合对象,这个集合里面的key是ThreadLocal对象,而每个ThreadLocal对象都代表一个唯一的客户端。

<think>好的,我需要帮助用户了解PageHelper分页查询中total属性的使用方法常见问题。首先,我应该回顾一下用户提供的引用内容,看看里面有没有关于total的具体信息。引用[3]提到Page分页信息包含总数据条数,也就是total,这应该用户的问题相关。 接下来,我需要整理total属性的使用方法。根据引用[1][2],PageHelper通过PageInfo对象包装查询结果,而PageInfo中就包含total属性。用户可能需要知道如何获取这个值,例如通过PageInfo的getTotal()方法。此外,引用[3]指出分页参数存储在ThreadLocal中,这可能会影响total的正确性,尤其是在多线程环境下,需要注意线程隔离的问题。 然后,常见问题部分需要考虑用户在实际使用中可能遇到的坑。比如,total为0的情况,可能是PageHelper.startPage()方法的位置不对,或者在分页语句前有未正确拦截的查询。另外,当手动设置total时,如果未关闭分页,可能会导致后续查询错误,这也是需要注意的点。此外,分页插件对数据库的支持情况(引用[1]提到的12种数据库)可能影响total的准确性,需要确保使用的数据库被支持。 最后,生成的相关问题需要围绕PageHelper的其他功能,比如分页优化、多表联查的分页处理,以及物理分页逻辑分页的区别,这些都能帮助用户更全面地理解PageHelper的使用。</think>### PageHelper分页查询中total属性的使用与常见问题 #### 一、total属性的作用 `total`属性表示**满足条件的数据总量**(忽略分页限制的总记录数),在分页查询中常用于计算总页数或展示数据规模。例如: $$总页数 = \lceil \frac{total}{pageSize} \rceil$$ #### 二、使用方法 1. **自动获取total** ```java PageHelper.startPage(1, 10); //开启分页(页码=1,每页数量=10) List<User> userList = userMapper.selectAll(); //实际SQL会被自动改写为"LIMIT 0,10" PageInfo<User> pageInfo = new PageInfo<>(userList); long total = pageInfo.getTotal(); //通过PageInfo获取total[^2] ``` 2. **手动设置total** ```java Page<User> page = PageHelper.startPage(1, 10).doSelectPage(() -> userMapper.selectAll()); page.setTotal(1000); //手动覆盖自动计算的total值 ``` #### 三、常见问题及解决方案 1. **total=0的情况** - 原因:`PageHelper.startPage()`未正确生效 - 检查点: - 确保`startPage()`在查询方法**之前调用**[^3] - 确认SQL语句被PageHelper拦截(查看控制台输出SQL是否包含分页语句) - 避免在分页查询前出现未被拦截的查询操作 2. **多线程环境异常** - 现象:分页参数错乱 - 原因:PageHelper通过`ThreadLocal`存储分页参数 - 解决:异步查询时需手动清理线程变量 ```java PageHelper.startPage(1, 10); try { userMapper.selectAll(); } finally { PageHelper.clearPage(); //强制清除ThreadLocal } ``` 3. **total值不准确** - 场景:复杂SQL(含UNION/子查询) - 解决:使用`PageHelper.count()`手动执行count查询 ```java long total = PageHelper.count(() -> userMapper.selectComplexQuery()); ``` #### 四、性能优化建议 1. 对count查询进行优化(如添加索引) 2. 使用`PageHelper.setCountSignal(true)`强制执行count查询验证准确性[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr-X~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值