共计 2326 个字符,预计需要花费 6 分钟才能阅读完成。
这篇文章主要介绍了 Redis 中如何使用消息队列,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让丸趣 TV 小编带着大家一起了解一下。
说到消息队列中间件,我们都会想到 RabbitMQ、RocketMQ 和 Kafka,来给应用实现异步消息传递的功能。这些都是专业的消息队列中间件,其特性之多超出了我们的理解能力。
而这些消息中间件使用起来的是复杂的,例如 RabbitMQ,发消息之前要创建 Exchange,还要创建 Queue,然后将 Exchange 和 Queue 通过某种规则绑定起来,发送消息的时候还要制定 routing-key,还要 控制头消息。这仅是生产者,消费者在消费消息之前也要将上面一系列的繁琐步骤再操作一遍。
那么对于那些并不要求百分百可靠,并且希望实现简单的消息队列需求时,我们可以通过 Redis 将我们从消息队列的中间件的繁琐步骤中解脱出来。
Redis 的消息队列不是专业的消息队列,他并没有消息队列中许多的高级特性,也没有 ack 保证。如果对消息的可靠性有着极致的追求,请转向专业的 MQ 中间件。
异步消息队列
从最简单的异步消息队列开始,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 一同挪到服务器进行原子操作,就能够完美解决了。
感谢你能够认真阅读完这篇文章,希望丸趣 TV 小编分享的“Redis 中如何使用消息队列”这篇文章对大家有帮助,同时也希望大家多多支持丸趣 TV,关注丸趣 TV 行业资讯频道,更多相关知识等着你来学习!