适用场景:
对于资源有限的小型功能业务场景可以使用,功能无法媲美专业的mq中间件,不支持消息持久化、ack、广播等,需要自身维护数据的可靠性。
重点内容:
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());
}