SpringBoot系列之使用Redis ZSet实现排序分页
软件环境:
-
JDK 1.8
-
SpringBoot 2.2.1
-
Maven 3.2+
-
Mysql 8.0.26
-
spring-boot-starter-data-redis 2.2.1
-
jedis3.1.0
-
开发工具
-
IntelliJ IDEA
-
smartGit
-
实现思路
相对于set来说,sorted set是一种有序的set,排序是根据每个元素的score排序的,score相同时根据key的ASCII码排序
根据ZSET的个性,我们可以实现一个排序,同时有个序号,也可以实现分页的逻辑,下面给出一个例子,看看具体的实现
项目搭建
使用Spring官网的https://start.spring.io
快速创建Spring Initializr
项目
选择maven、jdk版本
选择需要的依赖
因为pagehelper在里面搜索不到,所以手动加上
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.8</version>
</dependency>
动手实践
为了方便测试,写一个测试类,批量写入数据
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.json.JSONUtil;
import com.example.redis.model.dto.UserDto;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
@SpringBootTest
class SpringbootRedisApplicationTests {
private static final String REDIS_KEY = "testKeyRecord";
@Resource
private RedisTemplate redisTemplate;
@Test
void testPipeline() {
TimeInterval timeInterval = DateUtil.timer();
Map<Long, String> map = new HashMap<>();
IntStream.range(0, 10000).forEach(e->{
Long increment = getNextId();
UserDto userDto = UserDto.builder()
.id(increment)
.name("user"+increment)
.age(100)
.email("123456@qq.com")
.build();
map.put(increment, JSONUtil.toJsonStr(userDto));
});
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
map.forEach((score,value)->{
connection.zSetCommands().zAdd(REDIS_KEY.getBytes(), score, value.getBytes());
connection.expire(REDIS_KEY.getBytes(), getExpire(new Date()));
});
return null;
}
});
System.out.println("执行时间:"+timeInterval.intervalRestart()+"ms");
}
private Long getNextId() {
String idKey = String.format("testKeyId%s", DateUtil.format(new Date() , DatePattern.PURE_DATE_PATTERN));
Long increment = redisTemplate.opsForValue().increment(idKey);
redisTemplate.expire(idKey, getExpire(new Date()), TimeUnit.SECONDS);
return increment;
}
public static Long getExpire(Date currentDate) {
LocalDateTime midnight = LocalDateTime.ofInstant(currentDate.toInstant(),
ZoneId.systemDefault()).plusDays(1).withHour(0).withMinute(0)
.withSecond(0).withNano(0);
LocalDateTime currentDateTime = LocalDateTime.ofInstant(currentDate.toInstant(),
ZoneId.systemDefault());
return ChronoUnit.SECONDS.between(currentDateTime, midnight);
}
}
写好分页需要的参数类
package com.example.redis.common.page;
import lombok.Data;
@Data
public class PageObject {
// 当前页
private long pageNum;
// 当前页数
private long pageSize;
// 总页数
private long totalPage;
// 总数量
private long totalCount;
}
写好一个PageDataBean
类,返回分页的参数信息
package com.example.redis.common.page;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class PageDataBean<T> {
private List<T> dataList = new ArrayList<>();
private PageObject pageObj = new PageObject();
public PageDataBean(List<T> dataList , Long totalCount , Integer pageSize , Integer pageNum) {
this.dataList = dataList;
pageObj.setPageNum(pageNum);
pageObj.setPageSize(pageSize);
pageObj.setTotalCount(totalCount);
pageObj.setTotalPage(totalCount / pageSize + (totalCount % pageSize == 0 ? 0 : 1));
}
}
PageBean
传入需要的参数,并实现initPage
和加载数据逻辑
package com.example.redis.common.page;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageBean {
// 当前页
private Integer pageNum;
// 一页的条数
private Integer pageSize;
@JsonIgnore
private Page pages;
public void initPage() {
this.pages = PageHelper.startPage(pageNum , pageSize);
}
public PageDataBean loadData(List dataList) {
return new PageDataBean(dataList , pages.getTotal() , pageNum , pageSize);
}
}
分页核心逻辑,主要是使用reverseRange
使用倒序和分页的逻辑,如果要正序,可以使用range
package com.example.redis.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.example.redis.common.page.PageBean;
import com.example.redis.common.page.PageDataBean;
import com.example.redis.model.vo.UserVo;
import com.github.pagehelper.Page;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Component
public class UserHandler {
private static final String REDIS_KEY = "testKeyRecord";
@Resource
private RedisTemplate redisTemplate;
public PageDataBean<UserVo> pageUserInfo(PageBean pageBean) {
Integer pageNum = Optional.ofNullable(pageBean.getPageNum()).orElse(1);
Integer pageSize = Optional.ofNullable(pageBean.getPageSize()).orElse(10);
pageBean.initPage();
if (!redisTemplate.hasKey(REDIS_KEY)) {
return pageBean.loadData(CollUtil.newArrayList());
}
int min = (pageNum -1) * pageSize;
int max = min + pageSize - 1 ;
Long size = redisTemplate.opsForZSet().size(REDIS_KEY);
Set<String> recordSet = Optional.ofNullable(redisTemplate
.opsForZSet()
.reverseRange(REDIS_KEY, min, max))
.orElse(CollUtil.newHashSet());
List<UserVo> list = CollUtil.newArrayList();
recordSet.stream().forEach(getValue -> {
if (StrUtil.isNotBlank(getValue)) {
UserVo recordVo = null;
try {
recordVo = JSONUtil.toBean(getValue, UserVo.class);
} catch (Exception e) {
// ignore exception
}
if (recordVo != null) {
list.add(recordVo);
}
}
});
Page page = new Page();
page.setTotal(size);
pageBean.setPages(page);
return pageBean.loadData(list);
}
}
分页查询的api接口
@PostMapping(value = "/pageUserInfo")
public ResultBean<PageDataBean<UserVo>> pageUserInfo(@RequestBody PageBean pageBean) {
return ResultBean.ok(userHandler.pageUserInfo(pageBean));
}
补充:
如果是要获取倒排的最后几条数据,就可以使用
Set<String> recordSet = Optional.ofNullable(redisTemplate
.opsForZSet()
.reverseRange(REDIS_KEY, 0, num))
.orElse(CollUtil.newHashSet());