Redis 基础 - 优惠券秒杀《基于Redis消息队列实现》

本文介绍了基于Redis的消息队列实现,包括使用List、PubSub和Stream结构的不同方式。重点讲解了Stream作为消息队列的优势,如数据持久化和消费者组支持,以及如何在秒杀场景中应用。文章强调了Stream在消息确认、消息回溯和多消费者消费方面的特性,并提示在大型项目中可能需要更专业的消息队列服务。
摘要由CSDN通过智能技术生成

Redis基础 - 基本类型及常用命令
Redis基础 - Java客户端
Redis 基础 - 短信验证码登录
Redis 基础 - 用Redis查询商户信息
Redis 基础 - 优惠券秒杀《非集群》
Redis 基础 - 优惠券秒杀《分布式锁(初级)》
Redis 基础 - 优惠券秒杀《分布式锁(使用Redisson)》
Redis 基础 - 优惠券秒杀《初步优化(异步秒杀)》

消息队列

消息队列(Message Queue)字面上看是存放消息的队列。

最简单的消息队列模型包括3个角色:

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

消息队列和JDK阻塞队列的区别

  • 消息队列是JVM以外的独立服务,所以她不受JVM内存的限制
  • 消息队列不仅仅是做数据存储,她还会确保数据的安全,即存到消息队列的消息,都要去做持久化,这样的话就算服务宕机或重启,数据也不会丢失。而且她还要在消息投递给消费者以后,要求消费者做消息的确认,如果消息没有确认,那么这个消息在队列当中依然存在,下一次再投递给消费者,让她继续处理,直到成功为止,即确保消息至少被消费一次。
所以消息队列解决了使用阻塞队列时存在的两个安全问题。

Redis提供的消息队列

Redis提供了三种不同的方式来实现消息队列:

  • list结构:基于list结构模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:比较完善的消息队列模型
基于list结构模拟消息队列(推荐指数 ★☆☆☆☆)

消息队列是存放消息的队列。而Redis的list数据结构是一个双向链表,很容易模拟出队列效果。队列是入口和出口不在一边,因此我们可以利用:LPUSH结合RPOP、或者RPUSH结合LPOP来实现。不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息,因此这里应该使用BRPOP或者BLPOP来实现阻塞效果(B就是block即阻塞的意思)。

终端测试
比如打开两个Redis终端客户端,一个是生产者,一个是消费者。在消费者终端执行监听:

# li是key,20(秒)是要监听的阻塞时间。
BRPOP li 20

接着在生产者终端执行存元素:

LPUSH li e1 e2

执行后在消费者终端控制台里,就能看到立马输出了e1。当然,这时候里面还有元素e2,再执行BRPOP li 20的话就能拿到e2。然后再执行的话,由于已经没元素了,所以再次卡住了,直到有新元素为止。即利用BRPOP和LPUSH就能实现阻塞队列的效果。

和JDK的阻塞队列比起来有以下几点好处:

  • 她是JVM以外的独立的存储,所以他不依赖于JVM内存,所以不用担心存储上限的问题。
  • Redis是支持数据持久化的,所以数据安全,所以一旦数据存储到队列里,就完成了持久化,哪怕宕机
    也不会数据丢失,所以安全性比JDK的阻塞队列好。
  • 她还保证了消息的有序性,即先进先出。

不过也有一些缺点:

  • 和JDK的阻塞队列一样,也有消息丢失的问题。比如假设从Redis队列中取到一条消息,取到以后还没来得及处理就挂掉了或出现异常了,那这个消息就丢失了,因为R/LPOP这个命令拿出来后直接从队列里移除。
  • 只支持单消费者,即我发送的消息一旦有一个人拿走了,就从队列移除了,其他的人就拿不到那个消息了。即没办法实现一条消息被很多人使用的效果。
基于pubsub的消息队列(推荐指数 ☆☆☆☆☆)

pubsub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息(即支持多消费者)。

常用的命令如下:

  • SUBSCRIBE channel [channel] 订阅一个或多个频道
  • PUBLISH channel msg 向一个频道发送消息
  • PSUBSCRIBE pattern[pattern] 订阅与pattern格式匹配的所有频道。(通配符的格式是比如?是一个字符,即h?llo的话可以是hello,hallo之类的;是多个字符,比如hllo可以使hllo,heeeeello之类的;[ae]的话比如h[ae]llo时只能是hallo或hello)

终端测试

比如打开三个Redis客户端控制台,即分别代表着生产者、消费者1、消费者2。消费者1先订阅频道:

# 这里频道名是“order.q1”,其实是为了方便这么取的,比如点号没有什么特殊意义。
SUBSCRIBE order.q1

执行后就会输出:

ReadingMessage... (Please ctrl+c to quit)
1)order.q1

之后就卡在那里,即这种模式是天生的阻塞式的,ReadingMessage就是正在等着要读消息。

消费者2也订阅频道,这次也可以试试用PSUBSCRIBE,即如下:

PSUBSCRIBE order.*

执行后,和上面一样也输出如下:

ReadingMessage... (Please ctrl+c to quit)
1)order.*

接下来在生产者终端执行发布消息:

PUBLISH order.q1 hello

执行后在消费者1和消费者2的终端可以看到立即输出了“hello”。即两边都收到消息了。
如果再生产者终端再执行 PUBLISH order.q2 hello2 此时消费者1不输出,但消费者2输出“hello2”,因为消费者2订阅的是“order.*”所以匹配。

基于pubsub的消息队列的优点如下:

  • 采用发布订阅模型,支持多生产、多消费。即可以把一条消息发给多个消费者,也可以发给部分消费者。

而缺点也有,比如:

  • 不支持数据持久化。list支持数据持久化是因为list本质不是消息队列,她的本质是链表,目的本来就是要数据存储,只不过我们把他当成消息队列来用了而已。而pubsub她本身设计出来就是为了消息发送,所以当我们发送一条消息时,如果这个频道没有被任何人订阅,那么这个消息直接就丢失了。即我们发出的所有消息不会在Redis里保存。
  • 无法避免消息丢失。就像上面所说,放完了没人收,就直接丢了。
  • 消息堆积有上限,超出时数据丢失。上面说消息不在内存中保存,怎么又堆积了呢,因为当我们发送一个消息时,如果有消费者监听,会在消费者那里有一个缓存区域,把这个消息缓存下来,接下来消费者去处理,如果消费者处理过程中,比如说消费者处理的比较慢,比如处理一条消息都耗时一秒,若在这1秒内又来了十几条消息,那么这些消息都缓存在消费者那里,而消费者那里的缓存空间是有上限的,若超出,就会丢失。
所以如果可靠性要求比较高,不建议使用pubsub类型用作消息队列,还不如使用list。
基于stream的消息队列 - 单消费模式(推荐指数 ★★★☆☆)

stream是Redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列。由于stream是数据类型,是用来存取数据的,所以他支持数据的持久化,即在数据安全这块儿绝对是有保障的。

发送消息的命令:

xadd key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|ID field value [field value...]
  • 可选参数NOMKSTREAM:如果队列不存在,是否自动创建队列,默认是自动创建。
  • 可选参数[MAXLEN|MINID [=|~] threshold [LIMIT count]]:设置消息队列的最大消息数量。若设置为1000,将来如果消息数量超过了1000,一直没有消费者去处理,那么一些旧的消息就会被剔除掉。不给值就是不设置上限。
  • 参数*|ID:消息的唯一id,代表由Redis自动生成。格式是“时间戳-递增数字”,例如1644565656-2,自己指定时要注意这个格式(建议使用)。
  • 参数field value [field value…]:发送到队列中的消息,一个field value称为一个Entry。格式就是多个key-value键值对。

由于可选参数较多,所以最简单的用法如下:

# 创建名为users的队列,并向其中发送一个消息,内容是:{name=jack,age=21},并且使用Redis自动生成id。
XADD users * name jack age 21

读取消息的方式之一(XREAD):

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
  • 可选参数[COUNT count]:每次读取消息的最大数量,即可以一次读多条,也可以一次读一条。
  • 可选参数[BLOCK milliseconds]:当没有消息时,是否阻塞、阻塞时长。如果不给,就不阻塞。如果给0,就是永久等待。
  • 参数STREAMS key [key …]:要从哪个队列读取消息,key就是队列名。
  • 参数ID [ID …]:比如[COUNT count]设置1,即读1个消息,那从第几个开始读?可以指定起始的消息id,即只返回大于该id的消息,0 代表从第一个消息开始,$代表读取最新的消息(没读过的)。

终端测试

在生产者控制台中,执行:

XADD s1 * k1 v1

执行后,会输出消息id。还可以用XLEN命令查看消息的数量:

XLEN s1

执行后,输出1。

在消费者1终端执行如下:

XREAD COUNT 1 STREAMS s1 0

COUNT 1 读一条,STREAMS s1 哪个队列,0 从第一条开始读。执行后输出了k1 v1以及她的消息id。
同一个消息在消费者2里执行XREAD COUNT 1 STREAMS s1 0后也可以读。然后又在消费者1这里执行XREAD COUNT 1 STREAMS s1 0后发现还能读。也就是说STREAMS这种数据类型,一个消息你读完了以后不会删除,即这个消息是永久存在的。想读最新消息,把0替换成$就行。但发现返回nil,这是因为队列中的消息,你都读过了,没有新消息,所以返回值就是nil。

如果想等待最新的消息,可以用BLOCK(BLOCK 0,0是永久阻塞):

XREAD COUNT 1 BLOCK 0 STREAMS s1 0

执行之后,就会阻塞了。然后在生产者终端里再加一条消息:

XADD s1 * k2 v2

执行后,消费者1的窗口里立马收到并输出了k2 v2以及她的id。

在业务开发中,我们可以用循环调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果,伪代码如下:

while(true) {
   
	// 尝试读取队列中的消息,最多阻塞2秒
	Object msg = redis.execute("XREAD COUNT 1 BLOCK 2000 STREAMS users $");

	if (msg == null) continue;

	// 处理消息
	handleMessage(msg);
}

注意:
$使用XREAD是有小问题的。比如当我们指定起始id为$时,代表读取最新的消息,如果我们处理一条消息的过程中(即在执行handleMessage中的时候),又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。

STREAM类型消息队列的XREAD命令特点:

  • 消息可回溯,即消息读完了之后不消失,永久的保存在队列当中。
  • 一个消息可以被多个消费者读取。
  • 可以阻塞读取。
  • 有消息漏读的风险。
基于stream的消息队列 - 消费者组(推荐指数 ★★★★☆)

在单消费模式中,如果用$用while循环获取消息时,有可能会出现漏掉消息的情况。消费者组模式可以解决这个问题。

消费者组(consumer group)是将多个消费者划分到一个组中,监听同一个队列。

  • 消息分流
    队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度。即若多个消费者在一个组里,他们之间是竞争关系,凡是进入这个组的消息,大家可以去抢,这样一来处理消息的速度大大加快,从一定程度上可以避免消息堆积的问题。
  • 消息标识
    消费者组会维护一个标示,记录最后一个被处理的消息(注意:不是最新的消息,而是最后一个被处理的消息),哪怕消费者宕机重启,还会从标示之后读取消息&#x
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值