适用场景:

对于资源有限的小型功能业务场景可以使用,功能无法媲美专业的mq中间件,不支持消息持久化、ack、广播等,需要自身维护数据的可靠性。

重点内容:




redis可以做延迟消费吗 redis zset 延迟队列_redis


Redis 有序队列zset,使用时间戳作为score排序

zadd 添加到集合

zrangebyscore 通过分数返回有序集合指定区间内的成员

zrem 移除有序集合中的一个或多个成员

zcount 用于计算有序集合中指定分数区间的成员数量(用于集合积压数量查看)

zrange 通过索引区间返回有序集合指定区间内的成员

zcard 集合所有元素数量

注意:此方式严格依赖服务器时间,生产端和消费端必须保证时间一致。

关键代码示例:

//生产消息:

        public async Task<IActionResult> ZAdd(long count, int sec = 0, string fix = "")
        {
            for (int i = 0; i < count; i++)
            {
                var queueModel = new QueueModel()
                {
                    data = new MessageData() { id = new Random().Next(9999999, 999999999), name = "\"" + fix + "redistest" + i + "\"" },
                    execTime = ToUnixTimeLong(DateTime.Now.AddSeconds(sec > 0 ? sec : new Random().Next(0, 10))),
                };
                _ = RedisHelper.ZAddAsync(queueKey, (queueModel.execTime, queueModel));
            }
            return Content("key:" + queueKey + ",入列时间:" + ToUnixTimeLong(DateTime.Now.AddSeconds(sec)).ToString());
        }

消费消息:
            try
            {
                while (true)
                {
                    //一条条取比取多条快, 1, 0
,获取和删除的原子性问题此处未考虑,可使用lua脚本实现
                    var dataList = await RedisHelper.ZRangeByScoreAsync(queueKey, -1, ToUnixTimeLong(DateTime.Now), 1, 0);
                    if (dataList == null || dataList.Length == 0)
                    {
                        await Task.Delay(500);// 没有数据,休眠500ms,避免CPU空转
                        //Console.WriteLine(threadId + "没有消息...");
                    }
                    else
                    {
                        foreach (var item in dataList)
                        {
                            var result = await RedisHelper.ZRemAsync(queueKey, item);
                            if (result > 0)
                            {
                                Console.WriteLine(threadId + "获取消息:" + item + ",当前时间:" + ToUnixTimeLong(DateTime.Now));
                                //处理业务逻辑,这里可以考虑并发锁,避免多实例都处理了这个任务...
                                await Task.Delay(new Random().Next(20, 200));//模拟积压

                                //如果处理失败,重新加入队列
                                //var queueModel = Newtonsoft.Json.JsonConvert.DeserializeObject<QueueModel>(item);
                                //queueModel.execTime = ToUnixTimeLong(DateTime.Now);//重新指定时间
                                //_ = RedisHelper.ZAddAsync(queueKey, (queueModel.execTime, queueModel));
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("RedisDelayConsumerService_ex,threadId=" + threadId + "," + ex.ToString());
            }


消费消息:lua脚本处理
            //# 命令说明
            //KEYS[1]:第一个key
            //ARGV[1]:第一个value
            //KEYS、ARGV不需要一一对应,个数不需要相等
            //limit 0,10 ,一次性拉取10条
            var luaScript = " local result = redis.call('zrangebyscore', KEYS[1], ARGV[1], ARGV[2], 'LIMIT', 0, " + msgLimit + ")\n" +
            "if next(result) ~= nil and #result > 0 then\n" +
            "    local re = redis.call('zrem', KEYS[1], unpack(result));\n" +
            "if re > 0 then\n" +
            "    return result;\n" +
            "end\n" +
            "else\n" +
            "    return '';\n" +
            "end";
            try
            {
                while (true)
                {
                    //lua脚本原子性处理消息,key=KEYS[1], min=ARGV[1], max=ARGV[2] 对应传入的参数值
                    var result = await RedisHelper.EvalAsync(luaScript, queueKey, new object[] { -1, ToUnixTimeLong(DateTime.Now) });
                    if (result != null && !string.IsNullOrEmpty(result.ToString()))
                    {
                        var queueList = (object[])result;//返回数组
                        foreach (var item in queueList)
                        {
                            Console.WriteLine(threadId + "获取消息:" + item.ToString() + ",当前时间:" + ToUnixTimeLong(DateTime.Now));
                            //处理业务逻辑,这里可以考虑并发锁,避免多实例都处理了这个任务...
                            await Task.Delay(new Random().Next(20, 200));//模拟积压

                            //如果处理失败,重新加入队列
                            //var queueModel = Newtonsoft.Json.JsonConvert.DeserializeObject<QueueModel>(item);
                            //queueModel.execTime = ToUnixTimeLong(DateTime.Now);//重新指定时间
                            //_ = RedisHelper.ZAddAsync(queueKey, (queueModel.execTime, queueModel));
                        }
                    }
                    else
                    {
                        await Task.Delay(500);// 没有数据,休眠500ms,避免CPU空转
                        //Console.WriteLine(threadId + "没有消息...");
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("BusinessConsumerLuaAsync_ex,threadId=" + threadId + "," + ex.ToString());
            }




积压监控:
                try
                {
                    while (true)
                    {
                        //获取积压的消息数量(到时间没消费的)
                        var queueStockCount = await RedisHelper.ZCountAsync(queueKey, -1, ToUnixTimeLong(DateTime.Now));
                        if (queueStockCount == 0)
                        {
                            await Task.Delay(500);// 没有数据,休眠500ms,避免CPU空转
                        }
                        else
                        {
                            //根据积压数量按量可进行多次报警
                            if (queueStockCount > 5000)
                            {
                                await Task.Delay(10000);//10秒预警一次
                                //通过钉钉报警
                                Console.WriteLine("当前队列消息积压数量超过5000:Sum=" + queueStockCount);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("RedisDelayMoniterService_ex," + ex.ToString());
                }