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());