【Redis 应用】 延时队列

一、应用场景:

对于只有一组消费者的消息队列,使用 Redis 。从而避免了 Kafka 、RabbitMQ 等专业消息队列在只有一组消费者时,还进行的繁琐的绑定过程。

注意: Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,对消息的可靠性有着极致的追求的情况下,其不适合使用。

二、实现

list 链表结构常用于作为异步消息队列使用,使用 rpush / lpush 操作入队列,使用 lpop 和 rpop 操作出队列。

问题一:空轮询

客户端通过 pop 取得消息, 处理完接着获取。如果队列空了,就会进入空轮询 ,这样会在 拉高客户端的 CPU 的同时, Redis 的 QPS 也被拉高。

解决: 使用 sleep 解决,让线程睡 1 s ,再去请求

问题二:队列延迟

因为睡眠导致的消息延迟

解决: blpop / brpop

blocking —— 阻塞读

阻塞读可以做到队列没有数据的时候进入休眠,有数据到了,立刻苏醒。

问题三:空闲连接

线程一直阻塞,导致 Redis 客户端的连接成了闲置连接,闲置过久,服务器会主动断开连接。

解决: 主动捕获异常与重试

延时队列

补充:锁冲突的解决

  1. 直接抛出异常,提醒用户重试:适合用户直接发起的请求
  2. sleep 一会,再重试:这种方式阻塞当前线程,导致队列的后续消息处理出现延迟。因为个别死锁的 key 导致加锁不成功,线程会彻底堵死。
  3. 将请求转至延时队列,过一会再试:适合异步消息处理,以避开冲突

延时队列的实现

通过 zset 实现 ,消息序列化为字符串作为 zset 的 value ,消息的到期处理时间为 svore,多个线程轮询 zset 获取到期的任务进行处理。

import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.UUID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;


@SpringBootTest
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((T) task.msg);
            }
        }
    }
    public void handleMsg(T msg) {
        System.out.println(msg);
    }
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.33.10");
        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 {
            producer.join();
            Thread.sleep(6000);
            consumer.interrupt();
            consumer.join();
        }
        catch (InterruptedException e) {
        }
    }
}

可以使用 lua scripting 进行优化,将zrangebyscore 和 zrem 一同挪到服务器端进行原子化操作,这样多个进程之间争抢任务时就不会出现浪费。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值