目录

序言

Redis客户端选型

Redis配置

Redis实现排行榜

Redis实现延迟队列

Redis LRU(Least Recently Used)使用

Redis实现消息已读未读

总结


序言

在之前的开发中,我使用redis只用来实现分布式锁和对常用方法的查询数据缓存,再就是对登录验证码的一个缓存。数据类型也只用到了String(五种基本数据类型:String、List、Hash、Set、ZSet),这篇文章主要写怎么用Redis实现排行榜功能。

Redis客户端选型

了解过的小伙伴应该知道,我前面一篇文章也提到过Redis的三个客户端,它们各有各的优劣,下面对比一下这几种客户端:

  • Jedis:Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致。Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。Jedis仅支持五种基本数据结构(String、Hash、List、Set、ZSet)。
  • Redisson:Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。而Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。Redisson不仅提供了一系列的分布式Java常用对象,基本可以与Java的基本数据结构通用,还提供了许多分布式服务。
  • Lettuce:Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供自然的反应式编程,通讯框架集成了Netty使用了非阻塞IO,5.x版本以后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API。用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。主要在一些分布式缓存框架上使用比较多。

本篇文章我选用Redisson客户端来使用

Redis配置

1、添加依赖

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
    <exclusions>
        <exclusion>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-data-23</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-data-21</artifactId>
    <version>3.13.6</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、在application.yml文件加上配置,我们知道redis有四种工作模式——单点(single)、主从、哨兵(sentinel)、集群(cluster),本机单机测试,采用的是单点模式,如下:

###### redis ######
redis:
  clients:
    default:
      mode: single
      address: redis://${redission.basicHost}:${redission.basicPort}
      password: ${redission.baiscPwd}

3、在application-local.yml文件上加上上面引用的配置,这里配置的密码是大家自己设置的,如下:

redission:
  basicHost: localhost
  basicPort: 6379
  baiscPwd: 123456

4、使用redis-server命令启动本机Redis服务器,基本命令如下,

  • redis-server  -------Redis服务器
  • redis-cli         -------Redis命令行客户端
  • redis-benchmark ---------Redis性能测试工具
  • redis-check-aof ----------AOF文件修复工具
  • redis-check-dump --------RDB文件检查工具
  • redis-cli —> auth认证 —> shutdown --------关闭redis服务器
  • redis-cli —> auth认证 —> monitor --------监控redis执行了哪些命令(线上环境慎用,比较耗redis服务器资源)

5、使用redis-cli连接客户端,使用config set requirepass 123456设置密码,使用auth 123456检测给定的密码和配置文件中的密码是否相符,config get requirepass获取配置中的密码

6、使用Medis连接redis服务器看是否正常,

Redis实现排行榜

我们都知道使用ZSet数据结构(有序集合元素数量<128且所有元素长度小于64字节则为zipList数据结构,否则为skipList数据结构)来存储所需要排序的值,下面就来看一下如何实现:

首先看官网API文档:7. Distributed collections · redisson/redisson Wiki · GitHub,知道ZSet如何使用后就可以开撸代码了,

1、服务已经搭好,在Test写单元测试,BaseTest是SpringJUnit测试类,

@Slf4j
public class UserLogicTest extends BaseTest {

    //注入门面类
    @Resource
    private Facade facade;

    //注入Redis客户端
    @Resource
    private RedissonClient redissonClient;

    //redis排行榜单测
    @Test
    public void redisRankTest() throws ClassNotFoundException {
        //通过反射拿到Service层的方法名作为存储的SetName
        Class clazz = Class.forName("com.hust.zhang.service.logic.impl.UserLogicImpl");
        Method method = clazz.getDeclaredMethods()[0];
        String SetName = Constants.REDIS_CACHE_ID + ":" + getMethodName(method);
        //拿到redis的ScoredSortedSet集合
        try {
            RScoredSortedSet<User> set = redissonClient.getScoredSortedSet(SetName);
            //从数据库拿到User集合
            List<User> list = facade.getDataFacade().getUserService().list();
            //把集合数据异步存到redis服务器中
            list.stream().forEach(user -> set.addAsync(user.getScore().doubleValue(), user));
        }catch (Exception e){
            log.info("redis客户端操作失败,异常信息:", e);
        }
    }

    /**
     * 获取包含方法参数路径的方法名
     * @param method
     * @return
     */
    private static String getMethodName(Method method) {
        StringBuilder sb = new StringBuilder();
        sb.append(method.getName()).append("(");
        Class[] var2 = method.getParameterTypes();
        int var3 = var2.length;

        for (int var4 = 0; var4 < var3; ++var4) {
            Class<?> type = var2[var4];
            sb.append(type.getName()).append(",");
        }

        if (method.getParameterTypes().length > 0) {
            sb.delete(sb.length() - 1, sb.length());
        }

        sb.append(")");
        return sb.toString();
    }
}

这里从数据库拿数据,我的数据库原始数据如下图所示,User实体类中的score为各个对象的分数(有需求可能会要对不同值进行加权求平均分),这里简单起见只用score分数就行,

2、打开终端输入redis-cli打开命令客户端,输入monitor监控redis服务器,

3、跑完单测,可以看到redis客户端执行的命令,如下图

4、medis查看存入redis服务器的数据,可以看到存入的数据类型就是ZSET且进行了排序

这就是一个简单的使用ZSET数据结构进行排名,当然各位大佬可能会有更优雅的方式。

Redis实现延迟队列

延迟队列单元测试如下:

    @Test
    public void redisQueueTest() {
        try {
            //获取一个阻塞队列
            RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue("my_queue");
            //根据阻塞队列获取一个延时队列
            RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
            //创建一个子线程,阻塞队列有数据就返回,否则wait
            Thread thread = new Thread(() -> {
                while (true) {
                    try {
                        System.err.println(blockingQueue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();

            // 每秒向延迟队列放入数据,共执行5此
            for (int i = 1; i <= 5; i++) {
                delayedQueue.offer("test" + i, 10, TimeUnit.SECONDS);
            }
        }catch (Exception e){
            log.info("redis客户端操作失败,异常信息:", e);
        }
    }

上面使用了两个队列,阻塞队列和延时队列,下面简单介绍一下这两个队列,

阻塞队列(BlockingQueue)通常最先想到的是它是一个队列,不过队列除了FIFO还有LIFO的。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。

在阻塞队列不可用时,针对下面两种情况提供了4种处理方式:

  • 在队列为空时,从队列中获取元素的消费者线程会一直等待直到队列变为非空。
  • 当队列满了时,向队列中放置元素的生产者线程会等待直到队列可用。
处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
检查方法element()peek()不可用不可用

使用monitor监控redis执行的命令,如下,

可以看到Redis执行顺序:

  1. Monitor一直ping命令Redis服务器
  2. 订阅(SubScribe)了一个固定的队列 redisson_delay_queue_channel:{my_queue}, 就是为了开启进程里面的延时任务。
  3. zrangebyscore key min max [WITHSCORES] [LIMIT offset count]:分页获取指定区间内(min-max),带有分数值(可选)的有序集成员的列表。
  4. redisson_delay_queue_timeout:{my_queue} 是一个zset,当有延时数据存入Redisson队列时,就会在此队列中插入数据,排序分数为延时的时间戳。
  5. zrange,取出第一个数,也就是判断上面的还有不有下一页。
  6. BLPOP,移出并获取 my_queue列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,这里显然没有元素,就会一直阻塞。
  7. zadd,往ZSet里面添加数据。
  8. rpush,同步一份数据到list队列。
  9. zrange+publish,取出排序好的第一个数据,也就是最临近要触发的数据,然后发送通知 (之前订阅了的客户端,可能是微服务就有多个客户端),内容为将要触发的时间。客户端收到通知后,就在自己进程里面开启延时任务(HashedWheelTimer),到时间后就可以从redis取数据发送。

具体可以参看文末链接,本质上是发布订阅模型。

Redis LRU(Least Recently Used)使用

参看官网上的API后,看到了常用的页面置换算法,选择最近最久未使用的页面予以淘汰。Redis可以看一下是怎么使用的,

Redisson提供了基于Redis的以LRU为驱逐策略的分布式LRU有界映射对象。顾名思义,分布式LRU有界映射允许通过对其中元素按使用时间排序处理的方式,主动移除超过规定容量限制的元素。

    @Test
    public void RedisLRUTest() {
        try {
            RMapCache<String, String> map = redissonClient.getMapCache("map");
            // 尝试将该映射的最大容量限制设定为10
            map.trySetMaxSize(10);
            // 将该映射的最大容量限制设定或更改为10
            map.setMaxSize(10);
            for (int i = 0; i < 20; i++) {
                map.put(String.valueOf(i), String.valueOf(i), 30, TimeUnit.SECONDS);
            }
        }catch (Exception e){
            log.info("redis客户端操作失败,异常信息:", e);
        }
    }

可以看到Redis客户端里存放的是最后几个键值对,还可以看到Encoding是使用的zipList(数据量较小时的数据结构)。

Redis实现消息已读未读

实现消息已读未读功能的思路是使用Hash存储用户上次看过的时间,另外使用ZSet存储每个模块的每个信息产生的时间,

  1. 当有新信息产生,向相关模块添加时间:根据当前时间添加到ZSet数据集中。
  2. 当用户点击某个模块时,更新用户查看该模块的上次时间:更新当前点击模块的时间到Hash集合中。
  3. 查看时,从ZSet数据集中拿到当前模块的时间值,如果为空则表明是新消息,否则看用户上次看过的时间到当前时间是否有新消息产生。

总结

Redis可以用到的地方还是挺多的,需要我们大家自己去摸索,别人给了API文档,只要花时间去看去了解,都是可以用上的。不过需要深入的地方还有很多,加油!

另外补充两点:

  1. 如果在开发中如果是把Java对象转换成JSON格式存入到Redis中,我们取出的时候也需要把JSON格式转换成我们所需的Java对象(可能需要借助阿里巴巴的fastjson的JSONObject.parseObject()方法)。
  2. Mysql存的数据也可以通过Order By score语句(默认ASC)进行检索,也可以达到相同的效果。当时Mysql的检索性能远不及Redis,不光光是因为Mysql的数据结构是B+树Redis的数据结构是跳表,更重要的是Redis是基于内存操作。

参考链接:

1、目录 · redisson/redisson Wiki · GitHub

2、Redisson 延时队列 原理 详解 - 知乎

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐