Redis与spring结合进行切面读写分离
在高并发开发中我们难免遇到各种各样的瓶颈,此篇文章来说说我自己在读写分离中所用到的。
在上家公司中就遇到了数据库只有主备两个,但是备用数据库只用来备份数据,导致系统查询缓慢并且资源浪费的情况。后来提出想将备用数据库改为从库,将部分读的资源分流到此库中以减少系统压力。在综合多方考虑的情况下,我们在spring的切面读写分离和mycat数据库中间件中选择了前者。因为基于业务考虑,我们并不打算将所有的读操作都打到从库中,而mycat中间件不能做到。
spring读写分离操作的原理
使用spring实现mysql读写分离的原理很简单,一张图就能够说明:
V1.0版本
1.0版本与普通的spring读写分离没有区别,都是在spring配置文件中加入切面,并对切面进行数据源的切换操作。在进入Service之前,使用AOP来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库。
- 1.定义一个DynamicDataSource,用来动态获取数据源:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可
*
* 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
*
* @Author: LiuJin
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
// 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
return DynamicDataSourceHolder.getDataSourceKey();
}
}
在这里,需要注意的是AbstractRoutingDataSource需要在pom中配置下面的maven资源,否则会引用到org.springframework:spring中的包,导致代码报错。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
- 完成之后我们需要对动态获取数据源进行实现,在DynamicDataSourceHolder中通过ThreadLocal来确保线程是安全的:
/**
*
* 使用ThreadLocal技术来记录当前线程中的数据源的key
*
* @Author: LiuJin
*
*/
public class DynamicDataSourceHolder {
//写库对应的数据源key
private static final String MASTER = "master";
//读库对应的数据源key
private static final String SLAVE = "slave";
//使用ThreadLocal记录当前线程的数据源key
private static final ThreadLocal<String> holder = new ThreadLocal<String>();
/**
* 设置数据源key
* @param key
*/
public static void putDataSourceKey(String key) {
holder.set(key);
}
/**
* 获取数据源key
* @return
*/
public static String getDataSourceKey() {
return holder.get();
}
/**
* 标记写库
*/
public static void markMaster(){
putDataSourceKey(MASTER);
}
/**
* 标记读库
*/
public static void markSlave(){
putDataSourceKey(SLAVE);
}
}
- 这部我们需要定义切面进行方法名的左匹配,判断该服务是需要走读库还是写库。此处将事务的配置也加入在内,将在事务策略中标记的ReadOnly用slave数据源,其他使用默认数据源:
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.ReflectionUtils;
/**
* 定义数据源的AOP切面,该类控制了使用Master还是Slave。
*
* 如果事务管理中配置了事务策略,则采用配置的事务策略中的标记了ReadOnly的方法是用Slave,其它使用Master。
*
* 如果没有配置事务管理的策略,则采用方法名匹配的原则,以query、find、get开头方法用Slave,其它用Master。
*
* @author LiuJin
*
*/
public class DataSourceAspect {
private List<String> slaveMethodPattern = new ArrayList<String>();
private static final String[] defaultSlaveMethodStart = new String[]{ "query", "find", "get" };
private String[] slaveMethodStart;
/**
* 读取事务管理中的策略
*
* @param txAdvice
* @throws Exception
*/
@SuppressWarnings("unchecked")
public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception {
if (txAdvice == null) {
// 没有配置事务管理策略
return;
}
//从txAdvice获取到策略配置信息
TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource();
if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) {
return;
}
//使用反射技术获取到NameMatchTransactionAttributeSource对象中的nameMap属性值
NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource;
Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
nameMapField.setAccessible(true); //设置该字段可访问
//获取nameMap的值
Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField.get(matchTransactionAttributeSource);
//遍历nameMap
for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) {
if (!entry.getValue().isReadOnly()) {//判断之后定义了ReadOnly的策略才加入到slaveMethodPattern
continue;
}
slaveMethodPattern.add(entry.getKey());
}
}
/**
* 在进入Service方法之前执行
*
* @param point 切面对象
*/
public void before(JoinPoint point) {
// 获取到当前执行的方法名
String methodName = point.getSignature().getName();
boolean isSlave = false;
if (slaveMethodPattern.isEmpty()) {
// 当前Spring容器中没有配置事务策略,采用方法名匹配方式
isSlave = isSlave(methodName);
} else {
// 使用策略规则匹配
for (String mappedName : slaveMethodPattern) {
if (isMatch(methodName, mappedName)) {
isSlave = true;
break;
}
}
}
if (isSlave) {
// 标记为读库
DynamicDataSourceHolder.markSlave();
} else {
// 标记为写库
DynamicDataSourceHolder.markMaster();
}
}
/**
* 判断是否为读库
*
* @param methodName
* @return
*/
private Boolean isSlave(String methodName) {
// 方法名以query、find、get开头的方法名走从库
return StringUtils.startsWithAny(methodName, getSlaveMethodStart());
}
/**
* 通配符匹配
*
* Return if the given method name matches the mapped name.
* <p>
* The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, as well as direct
* equality. Can be overridden in subclasses.
*
* @param methodName the method name of the class
* @param mappedName the name in the descriptor
* @return if the names match
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
*/
protected boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
/**
* 用户指定slave的方法名前缀
* @param slaveMethodStart
*/
public void setSlaveMethodStart(String[] slaveMethodStart) {
this.slaveMethodStart = slaveMethodStart;
}
public String[] getSlaveMethodStart() {
if(this.slaveMethodStart == null){
// 没有指定,使用默认
return defaultSlaveMethodStart;
}
return slaveMethodStart;
}
}
- 代码方面准备完毕后,我们需要配置数据源参数:
pos.jdbc.initialSize=5
pos.jdbc.maxActive=100
pos.jdbc.minIdle=2
pos.jdbc.maxIdle=100
pos.jdbc.maxWait=60000
pos.jdbc.master.url=jdbc\:mysql\://*.*.*.*\:3306/xxx?useUnicode\=true&characterEncoding\=utf-8&allowMultiQueries\=true
pos.jdbc.master.username=root
pos.jdbc.master.password=B2yUKwlGvplD59NJllrD57+JHgCAdCoouK98Th6sW/xcABCavLoDx/Gwo/MP1pJsKFGaRikxf9UgCrGvPBWwnQ==
pos.jdbc.slave01.url=jdbc\:mysql\://*.*.*.*\:3306/xxx?useUnicode\=true&characterEncoding\=utf-8&allowMultiQueries\=true
pos.jdbc.slave01.username=root
pos.jdbc.slave01.password=B2yUKwlGvplD59NJllrD57+JHgCAdCoouK98Th6sW/xcABCavLoDx/Gwo/MP1pJsKFGaRikxf9UgCrGvPBWwnQ==
- 定义数据库连接池:
<!-- 注意当需要定义一个父数据源时,需将abstract="true"加上,否则DruidDataSource会视其为一个数据源而导致启动时会加载一个没有url定义的数据源从而启动失败 -->
<bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close" abstract="true" init-method="init" >
<!-- 驱动名称 -->
<property name="driverClassName" value="${jdbc.driverClassName}" />
<!-- 配置初始化大小、最小、最大 -->
<!-- 通常来说,只需要修改initialSize、minIdle、maxActive -->
<!-- 初始化时建立物理连接的个数,缺省值为0 -->
<property name="initialSize" value="${pos.jdbc.initialSize}" />
<!-- 最小连接池数量 -->
<property name="minIdle" value="${pos.jdbc.minIdle}" />
<!-- 最大连接池数量,缺省值为8 -->
<property name="maxActive" value="${pos.jdbc.maxIdle}" />
<!-- 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 -->
<property name="maxWait" value="${pos.jdbc.maxWait}" />
<!--
有些数据库连接的时候有超时限制(MySQL连接在8小时后断开),或者由于网络中断等原因,连接池的连接会出现失效的情况,这时候可以设置一个testWhileIdle参数为true,
如果检测到当前连接不活跃的时间超过了timeBetweenEvictionRunsMillis,则去手动检测一下当前连接的有效性,在保证确实有效后才加以使用。
在检测活跃性时,如果当前的活跃时间大于minEvictableIdleTimeMillis,则认为需要关闭当前连接。当
然,为了保证绝对的可用性,你也可以使用testOnBorrow为true(即在每次获取Connection对象时都检测其可用性),不过这样会影响性能。
-->
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒(3600000:为1小时) -->
<property name="timeBetweenEvictionRunsMillis" value="10000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒(300000:为5分钟) -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<!-- 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 -->
<property name="validationQuery" value="${jdbc.pool.validationQuery}" />
<!-- 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。建议配置为true,不影响性能,并且保证安全性。-->
<property name="testWhileIdle" value="true" />
<!-- 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。缺省值:true -->
<property name="testOnBorrow" value="false" />
<!-- 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。缺省值:false -->
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<!-- 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql5.5以下的版本中没有PSCache功能,建议关闭掉。5.5及以上版本有PSCache,建议开启。缺省值:false -->
<property name="poolPreparedStatements" value="true" />
<!-- 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100。 -->
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
<!-- 解密密码必须要配置的项 -->
<property name="filters" value="config" />
<property name="connectionProperties" value="config.decrypt=true" />
</bean>
<!-- 此处需将primary="true"加上,将主数据源设为首选,否则启动会报错 -->
<bean id="masterDataSource" parent="parentDataSource" primary="true">
<!-- 相应驱动的jdbcUrl -->
<property name="url" value="${pos.jdbc.master.url}" />
<!-- 数据库的用户名 -->
<property name="username" value="${pos.jdbc.master.username}" />
<!-- 数据库的密码 -->
<property name="password" value="${pos.jdbc.master.password}" />
</bean>
<!-- 配置连接池 -->
<bean id="slave01DataSource" parent="parentDataSource">
<!-- 相应驱动的jdbcUrl -->
<property name="url" value="${pos.jdbc.slave01.url}" />
<!-- 数据库的用户名 -->
<property name="username" value="${pos.jdbc.slave01.username}" />
<!-- 数据库的密码 -->
<property name="password" value="${pos.jdbc.slave01.password}" />
</bean>
- 对上面配置的数据源进行定义:
<bean id="dataSource" class="com.gw.core.datasource.DynamicDataSource">
<!-- 设置多个数据源 -->
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- 这个key需要和程序中的key一致 -->
<entry key="master" value-ref="masterDataSource"/>
<entry key="slave" value-ref="slave01DataSource"/>
</map>
</property>
<!-- 设置默认的数据源,这里默认走写库 -->
<property name="defaultTargetDataSource" ref="masterDataSource"/>
</bean><bean id="dataSource" class="com.gw.core.datasource.DynamicDataSource">
<!-- 设置多个数据源 -->
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- 这个key需要和程序中的key一致 -->
<entry key="master" value-ref="masterDataSource"/>
<entry key="slave" value-ref="slave01DataSource"/>
</map>
</property>
<!-- 设置默认的数据源,这里默认走写库 -->
<property name="defaultTargetDataSource" ref="masterDataSource"/>
- 定义事务管理器:
<!-- 事务管理器(由Spring管理MyBatis的事务) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 关联数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
- 通过事务策略进行管理:
<!-- 定义事务策略 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--定义查询方法都是只读的 -->
<tx:method name="query*" read-only="true" />
<tx:method name="find*" read-only="true" />
<tx:method name="get*" read-only="true" />
<tx:method name="list*" read-only="true" />
<tx:method name="count*" read-only="true" />
<tx:method name="sum*" read-only="true" />
<tx:method name="select*" read-only="true" />
<tx:method name="group*" read-only="true" />
<!-- 主库执行操作,事务传播行为定义为默认行为 -->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<!--其他方法使用默认事务策略 -->
<tx:method name="*" />
</tx:attributes>
</tx:advice>
- 定义切面,使用事务策略规则匹配:
<!-- 定义AOP切面处理器 -->
<bean class="com.gw.core.datasource.DataSourceAspect" id="dataSourceAspect">
<!-- 指定事务策略 -->
<property name="txAdvice" ref="txAdvice"/>
<!-- 指定slave方法的前缀(非必须) -->
<property name="slaveMethodStart" value="query,find,get,list,count,sum,select,group"/>
</bean>
<aop:config>
<!--pointcut元素定义一个切入点,execution中的第一个星号 用以匹配方法的返回类型, 这里星号表明匹配所有返回类型。 com.gw.facade.pos.service.impl.*.*(..)表明匹配com.gw.facade.pos.service.impl包下的所有类的所有
方法 -->
<aop:pointcut id="myPointcut"
expression="execution(* com.gw.facade.pos.service.impl.*.*(..))" />
<!--将定义好的事务处理策略应用到上述的切入点 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut" />
<!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 -->
<aop:aspect ref="dataSourceAspect" order="-9999">
<aop:before method="before" pointcut-ref="myPointcut" />
</aop:aspect>
</aop:config>
V2.0版本
V1.0版本在自测中发现有较多的缺陷,因为在之前的开发过程中并没有考虑太多,导致方法名中有很多不规范的命名从而导致了切面对方法进行左匹配时需要添加大量的配置或修改许多方法名,造成工作量的成倍增加。且每当新增一个配置时,都需要重启服务。基于上述两点考虑,故对V1.0版本进行了一定的修改。
- 将指定事务策略去除
<!-- 定义AOP切面处理器 -->
<bean class="com.gw.common.aop.DataSourceAspect" id="dataSourceAspect">
</bean>
<aop:config>
<!--pointcut元素定义一个切入点,execution中的第一个星号 用以匹配方法的返回类型, 这里星号表明匹配所有返回类型。 com.gw.facade.pos.service.impl.*.*(..)表明匹配com.gw.facade.pos.service.impl包下的所有类的所有
方法 -->
<aop:pointcut id="myPointcut"
expression="execution(* com.gw.facade.pos.service.impl.*.*(..))" />
<!--将定义好的事务处理策略应用到上述的切入点 -->
<!--<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut" />-->
<!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 -->
<aop:aspect ref="dataSourceAspect" order="-9999">
<aop:before method="before" pointcut-ref="myPointcut" />
</aop:aspect>
</aop:config>
- 修改DataSourceAspect,改为从Redis缓存中取出需要走从库的类路径和方法名,这样做的好处是如果需要添加从库方法,可以无需修改spring配置,并且无需改动大量方法名,随时可以将需要的方法添加进去:
import com.gw.common.datasource.DynamicDataSourceHolder;
import com.gw.common.utils.cache.redis.RedisUtils;
import org.aspectj.lang.JoinPoint;
/**
* @Author: LiuJin
* @Description: 定义数据源的AOP切面,该类控制了使用Master还是Slave。
*
* 通过与缓存在Redis的Hash中的数据进行匹配,若在Redis中能够找到并且状态为true,则用Slave,其他用Master。
*/
public class DataSourceAspect {
/**
* 在进入Service方法之前执行
*
* @param point 切面对象
*/
public void before(JoinPoint point) {
// 获取到当前执行的方法名
String methodName = point.getSignature().getName();
// 获取当前执行的类名
String className = point.getTarget().getClass().getName();
//拼接类名和方法名用于匹配Redis里存储的字符串List
String fullName = className.concat(".").concat(methodName);
boolean isSlave = false;
Object obj = RedisUtils.getHash("slaveMethods", fullName);
if (obj != null){
isSlave = (boolean) obj;
}
if (isSlave) {
// 标记为读库
DynamicDataSourceHolder.markSlave();
} else {
// 标记为写库
DynamicDataSourceHolder.markMaster();
}
}
}
- 在后台新增对Redis缓存读库方法管理的控制类,以便管理:
import com.gw.common.core.page.PageBean;
import com.gw.common.core.page.PageParam;
import com.gw.common.param.SlaveMessageVO;
import com.gw.common.utils.cache.redis.RedisUtils;
import com.gw.common.utils.cache.redis.SerializeUtils;
import com.gw.common.utils.string.StringUtil;
import com.gw.common.web.annotation.Permission;
import com.gw.web.boss.base.BossBaseAction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @Author: LiuJin
* @Date: 2018/10/18 09:41
* @Description: 读写分离设置
*/
public class SlaveMethodAction extends BossBaseAction {
private static final Log log = LogFactory.getLog(SlaveMethodAction.class);
@Permission("slaveConfig:slaveMethod:list")
public String listSlaveMethod() throws Exception {
//获取redis中数据
Map<byte[], byte[]> slaveMethodsMap = RedisUtils.getAllHash("slaveMethods");
//获取参数
Map<String, Object> paramMap = this.getParamMap();
String slaveMethodName = getString("slaveMethodName") == null ? "" : getString("slaveMethodName");
paramMap.put("slaveMethodName", slaveMethodName);
List<SlaveMessageVO> slaveMessageVOS = new ArrayList<>();
//根据slaveMethodName过滤列表数据
SlaveMessageVO vo;
if (slaveMethodsMap != null && slaveMethodsMap.size() > 0){
for (Map.Entry<byte[], byte[]> entry : slaveMethodsMap.entrySet()) {
vo = new SlaveMessageVO();
vo.setSlaveMethodName((String) SerializeUtils.unSerialize(entry.getKey()));
vo.setActive((Boolean) SerializeUtils.unSerialize(entry.getValue()));
if (vo.getSlaveMethodName().contains(slaveMethodName)){
slaveMessageVOS.add(vo);
}
}
} else {
slaveMessageVOS = new ArrayList<>();
}
super.pushData(this.listPage(getPageParam(), slaveMessageVOS));
this.putData("slaveMethodName", slaveMethodName);
return "slaveMethodList";
}
/**
* 添加界面
*
* @return String
*/
public String addSlaveMethodUI() {
return "addSlaveMethodUI";
}
/**
* 保存数据
*
* @return String
*/
@Permission("slaveConfig:slaveMethod:save")
public String saveSlaveMethod() {
//获取参数
String slaveMethodName = getString("slaveMethodName");
String isActive = getString("isActive");
if (StringUtil.isEmpty(slaveMethodName)) {
return operateError("去读库(类名+方法名)不能为空");
}
//获取redis中数据
boolean res = RedisUtils.addHash("slaveMethods", slaveMethodName, Integer.parseInt(isActive) == 1);
if (!res) {
log.info("create slaveMethods fail,slaveMethodName:" + slaveMethodName);
return operateError("创建失败.");
}
return operateSuccess();
}
/**
* 修改界面
*
* @return String
*/
public String updateSlaveMethodUI() {
String slaveMethodName = getString("slaveMethodName");
//根据slaveMethodName获取数据
Object object = RedisUtils.getHash("slaveMethods", slaveMethodName);
if (object != null){
boolean isActive = (boolean) object;
SlaveMessageVO slaveMessageVO = new SlaveMessageVO();
slaveMessageVO.setSlaveMethodName(slaveMethodName);
slaveMessageVO.setActive(isActive);
this.pushData(slaveMessageVO);
} else {
return operateError("找不到该配置项,请核对后再操作!");
}
return "updateSlaveMethodUI";
}
/**
* 更新数据
*
* @return String
*/
@Permission("slaveConfig:slaveMethod:update")
public String updateSlaveMethod() {
//获取参数
String orgSlaveMethodName = getString("orgSlaveMethodName");
String slaveMethodName = getString("slaveMethodName");
String isActive = getString("isActive");
if (StringUtil.isEmpty(slaveMethodName)) {
return operateError("去读库(类名+方法名)不能为空");
}
//从redis中获取数据
boolean desRes = RedisUtils.delHash("slaveMethods", orgSlaveMethodName);
if (desRes){
boolean res = RedisUtils.addHash("slaveMethods", slaveMethodName, Integer.parseInt(isActive) == 1);
if (!res) {
return operateError("Redis更新失败.");
}
return operateSuccess();
} else {
return operateError("Redis更新失败.");
}
}
/**
* 删除
*
* @return String
*/
@Permission("slaveConfig:slaveMethod:del")
public String delSlaveMethod() {
String slaveMethodName = getString("slaveMethodName");
if (StringUtil.isEmpty(slaveMethodName)) {
return operateError("去读库(类名+方法名)不能为空");
}
//从redis中移除
boolean desRes = RedisUtils.delHash("slaveMethods", slaveMethodName);
if (desRes) {
return operateSuccess();
} else {
return operateError("去读库(类名+方法名)删除失败");
}
}
/**
* 重写为redis的分页
*
* @param pageParam [分页参数]
* @return PageBean
*/
public PageBean listPage(PageParam pageParam, List<SlaveMessageVO> redisDataList) {
// 获取分页数据集
// -字符排序
Collections.reverse(redisDataList);
// 统计总记录数
Integer count = redisDataList.size();
// 是否统计当前分页条件下的数据:1:是,其他为否
return new PageBean(pageParam.getPageNum(), pageParam.getNumPerPage(), count, redisDataList, null);
}
}