使用Redis Stream来做消息队列和在Asp.Net Core中的实现

写在前面
我一直以来使用redis的时候,很多低烈度需求(并发要求不是很高)需要用到消息队列的时候,在项目本身已经使用了Redis的情况下都想直接用Redis来做消息队列,而不想引入新的服务,kafka和RabbitMQ等;
奈何这兄弟一直不给力;
虽然 Redis 的Pub/Sub 是实现了发布/订阅的,但这家伙最坑的是:丢数据
由于Pub/Sub 只是简单的实现了发布订阅模式,简单的沟通起生产者和消费者,当接收生产者的数据后并立即推送或者说转发给订阅消费者,并不会做任何的持久化、存储操作。由此:
- 消费者(客户端)掉线;
- 消费者未订阅(所以使用的时候一定记得先订阅再生产);
- 服务端宕机;
- 消费者消费不过来,消息堆积(生产数据受数据缓冲区限制);
以上情况都会导致生产数据的丢失,基于上坑,据我所知大家很少使用Pub/Sub ;
不过官方的哨兵集群通信的时候就是用的Pub/Sub;
然后,各路大佬结合队列、阻塞等等实现了各种各样的方案,主要是使用:BLPOP+LPUSH 的实现
这里就不一一展开了,有兴趣请看叶老板文章;
可能是各种实现都会带来各种的问题,redis的官方也看到了社区的挣扎。终于,到了Redis5.0,官方带来了消息队列的实现:Stream。
Redis Stream介绍
简单来说Redis Stream 就是想用Redis 做消息队列的最佳推荐;
XADD--发布消息
XADD stream1 * name hei age 18
XADD stream1 * name zhangshan age 19 #再发一条
127.0.0.1:6379> XADD stream1 * name hei age 18
"1631628884174-0"
127.0.0.1:6379> XADD stream1 * name zhangshan age 19
"1631628890025-0"
其中的'*'表示让 Redis 自动生成唯一的消息 ID,格式是 「时间戳-自增序号」
XREAD--订阅消息
订阅消息
XREAD COUNT 5 STREAMS stream1 0-0
127.0.0.1:6379> XREAD COUNT 5 STREAMS stream1 0-0
1) 1) "stream1"
2) 1) 1) "1631628884174-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
2) 1) "1631628890025-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
'0-0' 表示从开头读取
如果需继续拉取下一条,需传入上一条消息的id
阻塞等待消息
XREAD COUNT 5 BLOCK 50000 STREAMS stream1 1631628890025-0
阻塞等待消息id ‘1631628890025-0’ 后的消息
50000 阻塞时间(毫秒) ‘0’ 表示无限期阻塞
从到这里就可以看出 Pub/Sub多端订阅的最大优点,Stream也是支持的。有的同学很快就发现问题了,这里多端订阅后,没有消息确认ACK机制。
没错,因为现在所有的消费者都是订阅共同的消息,多端订阅,如果某个客户端ACK某条消息后,其他端消费不了,就实现不了多端消费了。
由此,引出 分组:GROUP
GROUP--订阅分组消息(多端订阅)
同样先发布消息
XADD stream1 * name hei age 18
XADD stream1 * name zhangshan age 19
127.0.0.1:6379> XADD stream1 * name hei age 18
"1631629080208-0"
127.0.0.1:6379> XADD stream1 * name zhangshan age 19
"1631629084083-0"
XGROUP CREATE 创建分组
创建分组1
XGROUP CREATE stream1 group1 0-0
127.0.0.1:6379> XGROUP CREATE stream1 group1 0-0
OK
‘0-0’ 表示从开头读取
'>' 表示读取最新,未被消费过的消息
XREADGROUP--分组读取
分组 group1
XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS stream1 >
consumer1 消费者名称, redis服务器会记住第一次使用的消费者名称;
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS stream1 >
1) 1) "stream1"
2) 1) 1) "1631628884174-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
2) 1) "1631628890025-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
3) 1) "1631629080208-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
4) 1) "1631629084083-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS stream1 >
(nil)
同样
‘0-0’ 表示从开头读取
'>' 表示读取最新,未被消费过的消息 (可以看到命令执行第二遍已经读不到新消息了)
分组 group2
127.0.0.1:6379> XGROUP CREATE stream1 group2 0-0
OK
127.0.0.1:6379> XREADGROUP GROUP group2 consumer1 COUNT 5 STREAMS stream1 >
1) 1) "stream1"
2) 1) 1) "1631628884174-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
2) 1) "1631628890025-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
3) 1) "1631629080208-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
4) 1) "1631629084083-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19
可以看到可以读到同样的消息,多端订阅没有问题;
当然分组也支持阻塞读取:
#和XREAD一样
XREAD COUNT 5 BLOCK 0 STREAMS queue 1618469127777-0
#分组阻塞
XREADGROUP GROUP group2 consumer1 COUNT 5 BLOCK 0 STREAMS stream1 >
‘0’ 表示无限期阻塞,单位(毫秒)
XPENDING--待处理消息
消息使用XREADGROUP 读取后会进入待处理条目列表(PEL);
我们看看:
XPENDING stream1 group2
127.0.0.1:6379> XPENDING stream1 group2
1) (integer) 4
2) "1631628884174-0"
3) "1631629084083-0"
4) 1) 1) "consumer1"
2) "4"
表示:
- (integer) 4 //表示当前消费者组的待处理消息的数量
- "1631628884174-0" //消息最大id
- "1631629084083-0" //最小id
- "consumer1" // 消费者名称
- "4" //消费者待处理消息数量
XACK--删除已处理消息(消息确认机制)
我们已经知道group2待处理消息有4条,我们从头读取看看:
XREADGROUP GROUP group2 consumer1 COUNT 5 STREAMS stream1 0-0
127.0.0.1:6379> XREADGROUP GROUP group2 consumer1 COUNT 5 STREAMS stream1 0-0
1) 1) "stream1"
2) 1) 1) "1631628884174-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
2) 1) "1631628890025-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
3) 1) "1631629080208-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
4) 1) "1631629084083-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
假设最后一条消息 ‘1631629084083-0’ 我已处理完成
127.0.0.1:6379> XACK stream1 group2 1631629084083-0
(integer) 1
再看:
127.0.0.1:6379> XREADGROUP GROUP group2 consumer1 COUNT 5 STREAMS stream1 0-0
1) 1) "stream1"
2) 1) 1) "1631628884174-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
2) 1) "1631628890025-0"
2) 1) "name"
2) "zhangshan"
3) "age"
4) "19"
3) 1) "1631629080208-0"
2) 1) "name"
2) "hei"
3) "age"
4) "18"
127.0.0.1:6379> XPENDING stream1 group2
1) (integer) 3
2) "1631628884174-0"
3) "1631629080208-0"
4) 1) 1) "consumer1"
2) "3"
可以清楚看到goroup2 待处理消息剩下3条;
这时 Redis 已经把这条消息标记为「处理完成」不再追踪;
Stream在Asp.net Core中的使用
private static string _connstr = "172.16.3.119:6379";
private static string _keyStream = "stream1";
private static string _nameGrourp = "group1";
private static string _nameConsumer = "consumer1";
发布:
csRedis.XAdd(_keyStream, "*", ("name", "message1"));
订阅:
static async Task CsRedisStreamConsumer()
{
Console.WriteLine("CsRedis StreamConsumer start!");
var csRedis = new CSRedis.CSRedisClient(_connstr);
csRedis.XAdd(_keyStream, "*", ("name", "message1"));
try
{
csRedis.XGroupCreate(_keyStream, _nameGrourp);
}
catch { }
(string key, (string id, string[] items)[] data)[] product;
(string Pid, string Platform, string Time) data = (null, null, null);
while (true)
{
try
{
product = csRedis.XReadGroup(_nameGrourp, _nameConsumer, 1, 10000, (_keyStream, ">"));
if (product?.Length > 0 == true && product[0].data?.Length > 0 == true)
{
Console.WriteLine($"message-id:{product.FirstOrDefault().data.FirstOrDefault().id}");
product.FirstOrDefault().data.FirstOrDefault().items.ToList().ForEach(value =>
{
Console.WriteLine($" {value}");
});
//csRedis.XAck(_keyStream, _nameGrourp, product[0].data[0].id);
}
}
catch (Exception)
{
//throw;
}
}
}
CSRedisCore

这里的超时报错可通过修改连接参数:syncTimeout 解决
CSRedisCore支持阻塞读取;
StackExchange.Redis
发布:
db.StreamAdd(_keyStream, "name", "message1", "*");
订阅:
static async Task StackExchangeRedisStreamConsumer()
{
Console.WriteLine("StackExchangeRedis StreamConsumer start!");
var redis = ConnectionMultiplexer.Connect(_connstr);
var db = redis.GetDatabase();
try
{
///初始化方式1
//db.StreamAdd(_keyStream, "name", "message1", "*");
//db.StreamCreateConsumerGroup(_keyStream, _nameGrourp);
//方式2
db.StreamCreateConsumerGroup(_keyStream, _nameGrourp, StreamPosition.NewMessages);
}
catch { }
StreamEntry[] data = null;
while (true)
{
data = db.StreamReadGroup(_keyStream, _nameGrourp, _nameConsumer, ">", count: 1, noAck: true);
if (data?.Length > 0 == true)
{
Console.WriteLine($"message-id:{data.FirstOrDefault().Id}");
data.FirstOrDefault().Values.ToList().ForEach(c =>
{
Console.WriteLine($" {c.Name}:{c.Value}");
});
db.StreamAcknowledge(_keyStream, _nameGrourp, data.FirstOrDefault().Id);
}
}
}

StackExchange.Redis 有点比较坑的是不存在阻塞读取;理由:https://stackexchange.github.io/StackExchange.Redis/PipelinesMultiplexers.html#multiplexing
QA
Q:Stream是否支持AOF、RDB持久化?
A:支持,其它数据类型一样,每个写操作,也都会写入到 RDB 和 AOF 中。
Q:Stream是否还是会丢数据?若是,何种情况下?;
A:会;1、AOF是定时写盘的,如果数据还在内存中时redis服务宕机就会;2、主从切换时(从库还未同步完成主库发来的数据,就被提成主库)
总结
技术中有的时候没有“银弹”,只有更适合的技术,汝之蜜糖彼之砒霜;
很多时候的技术选型都是个比较麻烦的东西,对选型人的要求很高;你可能不是只需要熟悉其中的一种路线,而是要踩过各种各样的坑,再根据当前受限的环境,选择比较适合目前需求/团队的;
回到Stream上,我认为目前Stream能满足挺大部分队列需求;
特别是“在项目本身已经使用了Redis的情况下都想直接用Redis来做消息队列,而不想引入新的更专业的mq,比如kafka和RabbitMQ的时候”
当然,最终决定需要用更专业的mq与否的,还是需求;
引用
https://database.51cto.com/art/202104/659208.htm
https://github.com/2881099/csredis/
https://stackexchange.github.io/StackExchange.Redis/Streams.html
使用Redis Stream来做消息队列和在Asp.Net Core中的实现的更多相关文章
- Api网关Kong集成Consul做服务发现及在Asp.Net Core中的使用
写在前面 Api网关我们之前是用 .netcore写的 Ocelot的,使用后并没有完全达到我们的预期,花了些时间了解后觉得kong可能是个更合适的选择. 简单说下kong对比ocelot打动我的 ...
- Redis中的Stream数据类型作为消息队列的尝试
Redis的List数据类型作为消息队列,已经比较合适了,但存在一些不足,比如只能独立消费,订阅发布又无法支持数据的持久化,相对前两者,Redis Stream作为消息队列的使用更为有优势. 相信 ...
- Redis实现简单的消息队列
1.问:什么是消息队列? 答:是一个消息的链表,是一个异步处理的数据处理引擎. 2.问:有什么好处? 答:不仅能够提高系统的负荷,还能够改善因网络阻塞导致的数据缺失. 3.问:用途有哪些? 答:邮件 ...
- redis分布式锁和消息队列
最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...
- (七)整合 Redis集群 ,实现消息队列场景
整合 Redis集群 ,实现消息队列场景 1.Redis集群简介 1.1 RedisCluster概念 2.SpringBoot整合Redis集群 2.1 核心依赖 2.2 核心配置 2.3 参数渲染 ...
- Asp.net Core中SignalR Core预览版的一些新特性前瞻,附源码(消息订阅与发送二进制数据)
目录 SignalR系列目录(注意,是ASP.NET的目录.不是Core的) 前言 一晃一个月又过去了,上个月有个比较大的项目要验收上线.所以忙的脚不沾地.现在终于可以忙里偷闲,写一篇关于Signal ...
- Asp.net Core中使用Redis 来保存Session, 读取配置文件
今天 无意看到Asp.net Core中使用Session ,首先要使用Session就必须添加Microsoft.AspNetCore.Session包,默认Session是只能存去字节,所以如果你 ...
- 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获
项目开发中的一些注意事项以及技巧总结 1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...
- ASP.NET Core教程:ASP.NET Core中使用Redis缓存
参考网址:https://www.cnblogs.com/dotnet261010/p/12033624.html 一.前言 我们这里以StackExchange.Redis为例,讲解如何在ASP.N ...
随机推荐
- 剑指 Offer 30. 包含min函数的栈
剑指 Offer 30. 包含min函数的栈 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min.push 及 pop 的时间复杂度都是 O(1). 示例 ...
- alpakka-kafka(6)-kafka应用案例,用户接口
了解了kafka原理之后,对kafka的的应用场景有了一些想法.在下面的一系列讨论中把最近一个项目中关于kafka的应用介绍一下. 先介绍一下使用kafka的起因:任何进销存系统,销售开单部分都应该算 ...
- Java方法——递归
递归(栈) package method; public class Demon04 { //递归思想 public static void main(String[] ar ...
- 谷歌内部流出Jetpack Compose最全上手指南,含项目实战演练!
简介 Jetpack Compose是在2019Google i/O大会上发布的新的库.Compose库是用响应式编程的方式对View进行构建,可以用更少更直观的代码,更强大的功能,能提高开发速度. ...
- 字节跳动五面都过了,结果被刷了,问了hr原因竟说是...
说在前面,面试时最好不要虚报工资.本来字节跳动是很想去的,几轮面试也通过了,最后没offer,自己只想到几个原因:1.虚报工资,比实际高30%:2.有更好的人选,这个可能性不大,我看还在招聘.我是面试 ...
- lerna 常用命令
lerna 介绍 lerna 处理机构 固定模式(fixed) 所有包是统一的版本号,每次升级,所有包版本统一更新,不管这个包内容改变与否 具体体现在,lerna 的配置文件 lerna.json 中 ...
- 关于数字化工厂&智能工厂建设 IT 经验总结
最近疫情闹得胆战心惊,前不久客户给我开了一个玩笑,当天我们同桌会议了一天,晚上客户回家之后就被隔离了,当他给我发这个消息的时候背都凉了一截,害怕之余在机场呆了一个晚上,捅乐鼻孔插了嗓子之后确认无事,后 ...
- [数据结构]ODT(珂朵莉树)实现及其应用,带图
[数据结构]ODT(珂朵莉树)实现及其应用,带图 本文只发布于博客园,其他地方若出现本文均是盗版 算法引入 需要一种这样的数据结构,需要支持区间的修改,区间不同值的分别操作. 一般的,我们会想到用线段 ...
- 【加解密】使用CFSSL生成证书并使用gRPC验证证书
写在前面的话 CFSSL是CloudFlare旗下的PKI/TLS工具.可以用于数字签名,签名验证和TLS证书捆绑的命令行工具和HTTP API服务器. 是使用golang语言开发的证书工具. 官方地 ...
- 【笔记】初探KNN算法(1)
KNN算法(1) 全称是K Nearest Neighbors k近邻算法: 思想简单 需要的数学知识很少 效果不错 可以解释机器学习算法使用过程中的很多细节问题 更加完整的刻画机器学习应用的流程 其 ...