redis分页查询代码实现

本文是个基于redis的分页查询实现

本人业务开发中遇到了某种需要纯redis做分页查询的操作,但网上百度之后发现千篇一律,完全没有任何系统化东西呈现。故记录下自己想到的处理方式和代码实现。
ps:写的不好勿喷,实在是并没有找到对本人实用的东西。

场景描述

需要实现收藏功能,或类似操作连贯性较高的非幂等性操作,且并发较高:

  1. 详情 能够实现对添加或删除收藏的功能
  2. 我的 界面能够对收藏目标进行分页查询。
  3. 对于数据,需要纯redis存储,并不考虑数据库一致性情况下。

Redis分页自定义包装类

/**
 * @author cy
 * @date 2022/5/26 10:15
 */
public class RedisPage<T> {
    /**
     * 当前页
     */
    private int current = 1;
    /**
     * 总数
     */
    private long total;
    /**
     * 每页条数
     */
    private int size = 5;
    /**
     * 总页数
     */
    private long totalSize;
    /**
     * 数据
     */
    private T data;

    @Override
    public String toString() {
        return "RedisPage{" +
                "current=" + current +
                ", total=" + total +
                ", size=" + size +
                ", data=" + data +
                '}';
    }

    /**
     * @description 获取当前页 
     * @param
     * @return
     */
    public int getCurrent() {
        return current;
    }

    /**
     * @description 设置当前页 
     * @param
     * @return
     */
    public void setCurrent(int current) {
        if (current > 1) {
            this.current = current;
        }
    }

    /**
     * @description 获取数据总条数 
     * @param
     * @return
     */
    public long getTotal() {
        return total;
    }

    /**
     * @description 设置总页数 
     * @param
     * @return
     */
    public void setTotal(long total) {
        this.total = total;
    }

    public int getSize() {
        return size;
    }

    /**
     * @description 设置每页条数 大于等于1小于等于50才进行设置,默认5
     * @param
     * @return
     */
    public void setSize(int size) {
        int minSize = 1, maxSize = 50;
        if (size >= minSize && size <= maxSize) {
            this.size = size;
        }
    }

    /**
     * @description 业务数据 
     * @param
     * @return
     */
    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    /**
     * @description redis分页的入参起始位置
     * @param
     * @return (当前页-1)* 每页条数
     */
    public int getFrom() {
        return (current - 1) * size;
    }

    /**
     * @description redis分页入参结束标志 
     * @param
     * @return 起始位置 + 每页条数 - 1
     */
    public int getTo() {
        int start = getFrom();
        return start + size - 1;
    }

    /**
     * @description 是否最后一页 
     * @param
     * @return 当前页 >= 总页数 ? true : false
     */
    public boolean isEnd() {
        return current >= totalSize;
    }

    /**
     * @description  获取总页数
     * @param
     * @return
     */
    public long getTotalSize() {
        return totalSize;
    }

    public void setTotalSize() {
        this.totalSize = (total + size - 1) / size;
    }
}

上述代码是用于redis的分页数据包装的,业务数据最终包在data参数中。

收藏和取消收藏biz业务处理

    public void collect(CollectVo collectVo) {
        String collectKey = RedisKeyUtil.getCollectKey(collectVo);
        String userKey = RedisKeyUtil.getUserKey(collectVo);
        String userValueKey = RedisKeyUtil.getUserValueKey(collectVo);
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(@NonNull RedisOperations redis) throws DataAccessException {
                Long member = redisTemplate.opsForZSet().rank(collectKey, collectVo.getUserId());
                redis.multi();
                if (null != member) {
                    redisTemplate.opsForZSet().remove(collectKey, collectVo.getUserId());
                    redisTemplate.opsForZSet().remove(userKey, collectVo.getCollectId());
                    redisTemplate.opsForHash().delete(userValueKey, collectVo.getCollectId());
                } else {
                    redisTemplate.opsForZSet().add(collectKey, collectVo.getUserId(), System.currentTimeMillis());
                    redisTemplate.opsForZSet().add(userKey, collectVo.getCollectId(), System.currentTimeMillis());
                    redisTemplate.opsForHash().put(userValueKey, collectVo.getCollectId(), collectVo);
                }
                return redis.exec();
            }
        });
    }

上述代码babybus:首先生成三个key,如下表格所示,先开启一个事务,使所有操作一块执行,然后rank操作首先对collectKey对应的userId的key进行排序,rank操作如果这个key不存在的话返回是空的,所以可以利用这个特点对数据进行判断,如果用户收藏了此对象,则不为空,就进行移除操作,也就是取消收藏,反之添加,在进行存储zset的索引列表时使用时间戳赋予分值进行排序,这样收藏的列表即为有序的了
需要注意:此处的事务并不是真正意义上的事务,只是为了同时执行一批操作,防止多次存取,因为redis不支持事务

redis key含义格式类型
collectKey被收藏的对象key,保存value为用户id (便于统计该对象被多少人收藏,被哪些人收藏)collect::1:id(key前缀+收藏类型+id)zset
userKey用户收藏了哪些对象key,保存value为收藏对象id (作用同上,用于索引数据)user_collect::id(key前缀+用户id)zset
userValueKey用户收藏的索引对应的数据值(相应的业务实体)user_collect_value::id(key前缀+用户id)hash

上述表格展示了三个redis key,全部的业务数据存储就使用到了这三个key,可以理解为数据库的二级索引,userKey只保存具体收藏对象的id并不存储实际数据,实际数据由hash类型的userValueKey来存储,可以看到该key由用户id进行区分,具体保存的键为收藏对象id,值为具体收藏对象业务实体。
这样设计的主要原因就是分离开之后可以对业务操作进行更多的更方便的查询,也是为什么没有像一级索引一样直接保存了业务数据而单独抽出来了一个索引key

查询收藏数

    /**
     * @param collectVo .
     * @return 查询收藏的数量
     **/
    public Long findCollectCount(CollectVo collectVo) {
        String collectKey = RedisKeyUtil.getCollectKey(collectVo);
        //直接size一下就获取到了
        Long size = redisTemplate.opsForZSet().size(collectKey);
        return Objects.nonNull(size) ? size : 0L;
    }

查询用户收藏状态

    /**
     * @param collectVo .
     * @return 查询当前收藏状态
     **/
    public Boolean findCollectStatus(CollectVo collectVo) {
        String collectKey = RedisKeyUtil.getCollectKey(collectVo);
        //rank一下如果收藏对象key里有该用户id则收藏
        Long rank = redisTemplate.opsForZSet().rank(collectKey, collectVo.getUserId());
        return rank != null;
    }

分页查询我的收藏

入参vo:

public class MyCollectVo {
    @NotNull
    //当前页
    private Integer current;
    @NotNull
    //每页条数
    private Integer size;

    @Override
    public String toString() {
        return "MyCollectVo{" +
                "current=" + current +
                ", size=" + size +
                '}';
    }

    public Integer getCurrent() {
        return current;
    }

    public void setCurrent(Integer current) {
        this.current = current;
    }

    public Integer getSize() {
        return size;
    }

    public void setSize(Integer size) {
        this.size = size;
    }
    }
//这两行不需要特别关注,就是将入参转为上述的redis分页包装类page,然后collect参数里面主要用到一个userId,进行第二行业务查询
RedisPage<List<CollectArticleVo>> page = convertApiUtil.getPage(collectVo);
RedisPage<List<CollectArticleVo>> collectArticle = collectionService.getCollectArticle(page, collect);

真正的getCollectArticle业务处理如下

    /**
     * @return 分页查询收藏文章
     **/
    public RedisPage<List<CollectArticleVo>> getCollectArticle(RedisPage<List<CollectArticleVo>> page, CollectVo collectVo) {
    	//上述的用户收藏了哪些对象id的key 也就是用户收藏索引key
        String userKey = RedisKeyUtil.getUserKey(collectVo);
        //用户收藏的索引值key
        String userValueKey = RedisKeyUtil.getUserValueKey(collectVo);
        //索引列表 倒序  reverseRange操作分页获取倒序的索引列表
        Set<Object> articleKey = redisTemplate.opsForZSet().reverseRange(userKey, page.getFrom(), page.getTo());
        //查询收藏总数
        Long size = redisTemplate.opsForZSet().size(userKey);
        if (Objects.isNull(articleKey) || Objects.isNull(size)) {
            throw new CommonException("查询size或索引列表为空!");
        }
        //设置数据总量
        page.setTotal(size);
        //设置总页数
        page.setTotalSize();
        //设置是否最后一页
        page.isEnd();
        //判空
        if (page.getCurrent() > page.getTotalSize() || articleKey.isEmpty()) {
            page.setData(new ArrayList<>());
            return page;
        }
        //接下来用到了多线程操作,因为多条数据需要多次从索引值key中获取,所以进行了异步操作,可以去掉
        //此处用map把set索引列表按序附上一个自增的score便于排序
        //因为通过reverseRange查出来的set是有序的,但下方异步操作查询之后结果是乱序的需要重排序
        ConcurrentHashMap<Object, Integer> keyMap = new ConcurrentHashMap<>(20);
        int score = 0;
        for (Object a : articleKey) {
            keyMap.put(a, score++);
        }
        //声明个计数器,异步操作时执行完一批后返回
        final CountDownLatch countDownLatch = new CountDownLatch(articleKey.size());
        //线程安全列表包一下,操作的时候不会出问题
        List<CollectArticleVo> countList = Collections.synchronizedList(new ArrayList<>());
        //索引值
        for (Map.Entry<Object, Integer> index : keyMap.entrySet()) {
            MyThread.getExecutor().submit(() -> {
                try {
                	//for循环从索引值列表里通过索引列表的id取
                    CollectVo artValue = (CollectVo) redisTemplate.opsForHash().get(userValueKey, index.getKey());
                    //获取索引值分数(时间戳,收藏时间)
                    Double timestamp = redisTemplate.opsForZSet().score(userKey, index.getKey());
                    CollectArticleVo collectArticleVo = new CollectArticleVo();
                    //此处为时间格式化,并不一定要有
                    if (timestamp != null) {
                        LocalDateTime dateTime = LocalDateTime.ofEpochSecond(timestamp.longValue() / 1000L, 0, ZoneOffset.ofHours(8));
                        String format = dateTime.format(DateTimeFormatter.ofPattern(PATTERN));
                        collectArticleVo.setCollectTime(format);
                    }
                    collectArticleVo.setId(index.getKey().toString().substring(index.getKey().toString().lastIndexOf(SPLIT) + 1));
                    collectArticleVo.setIntroduce(Objects.requireNonNull(artValue).getCollectIntroduce());
                    collectArticleVo.setSort(index.getValue());
                    countList.add(collectArticleVo);
                } catch (Exception exception) {
                    log.error("查询我的收藏出错了: ", exception);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException exception) {
            log.error("查询我的收藏出错了: ", exception);
        }
        //最后进行重排序
        Collections.sort(countList);
        page.setData(countList);
        return page;
    }

上述代码里有很多的注释解释,就不一一说明了,到此完结。

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

皮特猫.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值