说到消息队列中间件,我们都会想到RabbitMQ、RocketMQ和Kafka,来给应用实现异步消息传递的功能。这些都是专业的消息队列中间件,其特性之多超出了我们的理解能力。下面就带大家了解一下Redis进阶用法之消息队列,介绍一下Redis中的延时队列,一起来看一下。
在Redis中如何使用消息队列
异步消息队列
从最简单的异步消息队列开始,Redis的list数据结构常用来作为异步消息队列,通过lrpush/lpush来操作入列,通过rpop/lpop来出列。
问题一:空队列
对于pop操作来说,当消息队列空了的时候,客户端会陷入pop的死循环,造成大量的浪费生命的空轮询,导致客户端CPU拉高,同时Redis的QPS也被拉高。
对于以上问题的解决办法就是通过list结构的blpop/brpop来操作出列,其中b前缀代表的就是blocking,阻塞读。对于阻塞读在队列没有数据的时候就会进入休眠状态,一旦数据到来就会立刻醒来。完美的解决了上面这个问题。
问题二:空闲连接断开
阻塞读的方案看似完美,紧接着引出了另外一个问题:空闲连接。 如果线程一直阻塞在哪哪里,Redis的客户端连接就变成了空闲连接。空闲时间过长,Redis服务器就会主动断开连接,以减少闲置资源占用。这时候blpop/brpop就会抛出异常来。
所以,我们在编写客户端(应用程序)消费者的时候需要小心,注意捕获异常,并进行重试。
应用一:延时队列
在Redis的分布式锁中一般有三种策略来处理加锁失败的情况:
● 直接抛出异常,前端提醒用户是否要继续操作;
● sleep一会再重试;
● 将请求放到延时队列中,一会再重试;
而Redis中延时队列,我们可以通过zset(有序列表)数据结构来实现。我们将消息序列化作为一个字符串作为zse的value,而消息的到期处理时间(延时时间)作为score。然后通过轮询zset获取到期时间进行处理,通过zrem将key从zset移除代表成功消费,进而处理任务。
核心代码如下:
// 生产\
public void delay(T msg) {\
TaskItem task = new TaskItem();\
task.id = UUID.randomUUID().toString(); // 分配唯一的 uuid\
task.msg = msg;\
String s = JSON.toJSONString(task); // fastjson 序列化\
jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s); // 塞入延时队列 ,5s 后再试\
}\
// 消费\
public void loop() {\
while (!Thread.interrupted()) {\
// zrangeByScore参数中0, System.currentTimeMills()代表从redis中去score范围在0到系统当前时间的数据, 0,1表示从0开始取1个 拓展传入的score为-inf, +inf 分别表示zset中的最大值和最小值,当你不知道zset中的score最值时就可以使用inf作为参数变量\
Set values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);\
if (values.isEmpty()) {\
try {\
Thread.sleep(500); // 歇会继续\
}\
catch (InterruptedException e) {\
break;\
}\
continue;\
}\
String s = values.iterator().next(); //消费队列\
if (jedis.zrem(queueKey, s) > 0) { // 抢到了,要考虑到多线程下锁争抢的情况,只有rem成功代表成功的消费了一条消息。\
TaskItem task = JSON.parseObject(s, TaskType); // fastjson 反序列化\
this.handleMsg(task.msg);\
}\
}\
}
以上的代码在多线程中对于同一个任务被多个线程争抢的情况,虽然能够通过zrem后在处理任务来避免一个任务被多次消费的情况。但是对于那些获取到了任务但是没有成功消费的线程来说,都是白白浪费时间取了一次任务。所以可以考虑通过lua scripting来优化这个逻辑。将zrangeByScore和zrem一同挪到服务器进行原子操作,就能够完美解决了。
要上就是关于在Redis中如何使用消息队列的详细解析,翼速应用平台内有更多相关资讯,欢迎查阅!
我来说两句