本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!
Redis list常用作异步消息队列,没有数据时阻塞,基于命令brpop、blpop;这种情况会存在空连接的问题,闲置连接过久服务器会主动断开连接,brpop、blpop会抛出异常,需要在程序中捕获异常并重新连接
延时队列的实现
延时队列可以使用Redis的zset(有序列表)来实现。将消息序列化成一个字符串作为zset的value,这个消息的到期处理时间作为score,然后用多个线程轮询zset获取到起的任务进行处理;多个线程保障可用性,但是多个线程是并发争抢任务,保证任务不能被执行多次
ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN #添加任务
ZREM key member [member ...] #删除某个元素
ZCOUNT key [min] [max] #计算有序集合中指定分数区间的成员数量
ZRANGE key start stop [WITHSCORES] #返回有序集中,指定区间内的成员
ZRANGEBYSCORE key [min] [max] [WITHSCORES] [LIMIT offset count] #根据score的范围查找
zrange用法:https://www.runoob.com/redis/sorted-sets-zrange.html
zcount用法:https://www.runoob.com/redis/sorted-sets-zcount.html
zrangebyscore用法:https://www.runoob.com/redis/sorted-sets-zrangebyscore.html
zrangebyscore坑:
Java使用Redis延时队列
import java.lang.reflect.Type;
import java.util.Set;
import java.util.UUID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import redis.clients.jedis.Jedis;
public class RedisDelayingQueue<T> {
static class TaskItem<T> {
public String id;
public T msg;
}
// fastjson 序列化对象中存在 generic 类型时,需要使用 TypeReference
private Type TaskType = new TypeReference<TaskItem<T>>() { }.getType();
private Jedis jedis;
private String queueKey;
public RedisDelayingQueue(Jedis jedis, String queueKey) {
this.jedis = jedis;
this.queueKey = queueKey;
}
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()) {
// 只取一条
Set values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
if (values.isEmpty()) {
try {
Thread.sleep(500); // 歇会继续
} catch (InterruptedException e) {
break;
}
continue;
}
String s = (String) values.iterator().next();
if (jedis.zrem(queueKey, s) > 0) { // 抢到了
TaskItem task = JSON.parseObject(s, TaskType); // fastjson 反序列化
this.handleMsg(task.msg);
}
}
}
public void handleMsg(T msg) {
System.out.println(msg);
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
RedisDelayingQueue queue = new RedisDelayingQueue<>(jedis, "q-demo");
Thread producer = new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
queue.delay("codehole" + i);
}
}
};
Thread consumer = new Thread() {
public void run() {
queue.loop();
}
};
producer.start();
consumer.start();
try {
//main线程会等待producer线程执行完毕之后再执行
producer.join();
Thread.sleep(6000);
consumer.interrupt();
consumer.join();
}
catch (InterruptedException e) {
}
}
}
Redis 作为消息队列为什么不能保证 100% 的可靠性
如果绝对的保证数据最终一致性,保证消息百分百不丢,那么需要:
1.写入时候要求启用事务处理,保证写一定成功
2.redis配置成任何变更一定实时持久化,比如存储端是磁盘的话,每次变更马上同步写入磁盘,才算完成。redis是支持这种方式配置的,但是这么做会使它的内存数据库特性完全消失,性能变得十分低下
3.消费端也要实现事务方式,处理完成后,再回来真实删除消息
4.多线程或者多端同时并发处理,可以通过锁的方式来规避
5.4的需求需要自己实现,可以一起考虑,用另外一个队列实现的方式也可以,但是更好的方式是在队列内部实现个计数器。hash格式的加个字段加数值,list的先推一个数值打底,string的头上加个数值再加个分隔符,就可以做个简单计数器了,虽然土,胜在够实用。
除了特定的系统之外,一般不会要求这么强的一致性,实现倒不难,但是性能会很差很差。
银行类支付类业务会要求严格的事务一致性,而互联网类业务一般会用点取巧的方式,就是可以容忍极短时间内少量数据丢失的方式,换取更高性能。比如上面的redis处理,可以改为1000条数据变更的时候再真实落盘,即写入磁盘。那么极限情况下,如突然断电,存在可能丢失这1000条数据的风险。当然这种情况出现的概率也是很低的,所以大部分场景下可以接受。
其实redis虽然也实现了订阅方式,但是它本身不是专门的消息队列系统,所在做这些需求还需要自己实现大部分功能,实在不是好选择。更好的选择是rabbitmq。
如上述的获取消息的时候是同步删除还是等消息真正处理完成后再回来删除,用redis要自己写一套完整逻辑,而对rabbitmq来说,已经帮你做好了,本身就支持,只需要传递一个no ack的参数值是true还是false即可
至于性能方面,各种评测和个人的使用体验都表示rabbitmq的性能很靠谱,和redis不相上下,即使有差也是所差极小,可以忽略。
所以更好的选择是rabbitmq