目录
Redis LRU(Least Recently Used)使用
序言
在之前的开发中,我使用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、添加依赖
-
<!--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文档:https://github.com/redisson/redisson/wiki/7.-distributed-collections#74-sortedset,知道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执行顺序:
- Monitor一直ping命令Redis服务器
- 订阅(SubScribe)了一个固定的队列 redisson_delay_queue_channel:{my_queue}, 就是为了开启进程里面的延时任务。
- zrangebyscore key min max [WITHSCORES] [LIMIT offset count]:分页获取指定区间内(min-max),带有分数值(可选)的有序集成员的列表。
- redisson_delay_queue_timeout:{my_queue} 是一个zset,当有延时数据存入Redisson队列时,就会在此队列中插入数据,排序分数为延时的时间戳。
- zrange,取出第一个数,也就是判断上面的还有不有下一页。
- BLPOP,移出并获取 my_queue列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,这里显然没有元素,就会一直阻塞。
- zadd,往ZSet里面添加数据。
- rpush,同步一份数据到list队列。
- 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存储每个模块的每个信息产生的时间,
- 当有新信息产生,向相关模块添加时间:根据当前时间添加到ZSet数据集中。
- 当用户点击某个模块时,更新用户查看该模块的上次时间:更新当前点击模块的时间到Hash集合中。
- 查看时,从ZSet数据集中拿到当前模块的时间值,如果为空则表明是新消息,否则看用户上次看过的时间到当前时间是否有新消息产生。
总结
Redis可以用到的地方还是挺多的,需要我们大家自己去摸索,别人给了API文档,只要花时间去看去了解,都是可以用上的。不过需要深入的地方还有很多,加油!
另外补充两点:
- 如果在开发中如果是把Java对象转换成JSON格式存入到Redis中,我们取出的时候也需要把JSON格式转换成我们所需的Java对象(可能需要借助阿里巴巴的fastjson的JSONObject.parseObject()方法)。
- 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