小言_互联网的博客

Redis实现排行榜、延迟队列、LRU、消息已读未读(Redisson客户端实现)

450人阅读  评论(0)

目录

 

序言

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仅支持五种基本数据结构。
  • 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、添加依赖


  
  1. <!--redisson-->
  2. <dependency>
  3. <groupId>org.redisson</groupId>
  4. <artifactId>redisson-spring-boot-starter</artifactId>
  5. <version> 3.13. 6</version>
  6. <exclusions>
  7. <exclusion>
  8. <groupId>org.redisson</groupId>
  9. <artifactId>redisson-spring-data- 23</artifactId>
  10. </exclusion>
  11. </exclusions>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.redisson</groupId>
  15. <artifactId>redisson-spring-data- 21</artifactId>
  16. <version> 3.13. 6</version>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-data-redis</artifactId>
  21. </dependency>

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


  
  1. ###### redis ######
  2. redis:
  3. clients:
  4. default:
  5. mode: single
  6. address: redis: //${redission.basicHost}:${redission.basicPort}
  7. password: ${redission.baiscPwd}

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


  
  1. redission:
  2. basicHost: localhost
  3. basicPort: 6379
  4. 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文档:https://github.com/redisson/redisson/wiki/7.-distributed-collections#74-sortedset,知道ZSet如何使用后就可以开撸代码了,

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


  
  1. @Slf4j
  2. public class UserLogicTest extends BaseTest {
  3. //注入门面类
  4. @Resource
  5. private Facade facade;
  6. //注入Redis客户端
  7. @Resource
  8. private RedissonClient redissonClient;
  9. //redis排行榜单测
  10. @Test
  11. public void redisRankTest() throws ClassNotFoundException {
  12. //通过反射拿到Service层的方法名作为存储的SetName
  13. Class clazz = Class.forName( "com.hust.zhang.service.logic.impl.UserLogicImpl");
  14. Method method = clazz.getDeclaredMethods()[ 0];
  15. String SetName = Constants.REDIS_CACHE_ID + ":" + getMethodName(method);
  16. //拿到redis的ScoredSortedSet集合
  17. try {
  18. RScoredSortedSet<User> set = redissonClient.getScoredSortedSet(SetName);
  19. //从数据库拿到User集合
  20. List<User> list = facade.getDataFacade().getUserService().list();
  21. //把集合数据异步存到redis服务器中
  22. list.stream().forEach(user -> set.addAsync(user.getScore().doubleValue(), user));
  23. } catch (Exception e){
  24. log.info( "redis客户端操作失败,异常信息:", e);
  25. }
  26. }
  27. /**
  28. * 获取包含方法参数路径的方法名
  29. * @param method
  30. * @return
  31. */
  32. private static String getMethodName(Method method) {
  33. StringBuilder sb = new StringBuilder();
  34. sb.append(method.getName()).append( "(");
  35. Class[] var2 = method.getParameterTypes();
  36. int var3 = var2.length;
  37. for ( int var4 = 0; var4 < var3; ++var4) {
  38. Class<?> type = var2[var4];
  39. sb.append(type.getName()).append( ",");
  40. }
  41. if (method.getParameterTypes().length > 0) {
  42. sb.delete(sb.length() - 1, sb.length());
  43. }
  44. sb.append( ")");
  45. return sb.toString();
  46. }
  47. }

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

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

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

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

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

 

 

Redis实现延迟队列

延迟队列单元测试如下:


  
  1. @Test
  2. public void redisQueueTest() {
  3. try {
  4. //获取一个阻塞队列
  5. RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue( "my_queue");
  6. //根据阻塞队列获取一个延时队列
  7. RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
  8. //创建一个子线程,阻塞队列有数据就返回,否则wait
  9. Thread thread = new Thread(() -> {
  10. while ( true) {
  11. try {
  12. System.err.println(blockingQueue.take());
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. });
  18. thread.start();
  19. // 每秒向延迟队列放入数据,共执行5此
  20. for ( int i = 1; i <= 5; i++) {
  21. delayedQueue.offer( "test" + i, 10, TimeUnit.SECONDS);
  22. }
  23. } catch (Exception e){
  24. log.info( "redis客户端操作失败,异常信息:", e);
  25. }
  26. }

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

阻塞队列(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有界映射允许通过对其中元素按使用时间排序处理的方式,主动移除超过规定容量限制的元素。


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

可以看到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、https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

2、https://zhuanlan.zhihu.com/p/343811173

 

 


转载:https://blog.csdn.net/zkkzpp258/article/details/116978158
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场