Redis应用之消息队列-Stream的使用

一、概述

众所周知,redis这个强大的中间件经常被用作处理各种分布式的业务,比如分布式锁、消息队列等等,而redis用来处理消息队列的业务时应该使用哪种数据结构呢?
在学习stream之前,我的第一反应是list,理由如下

  • list可以采用左进右出的方式保证消息处理的顺序性
  • list可以采取阻塞读取消息方式,不浪费cpu资源
  • list可以借助redis对消息进行持久化

综上所述,好像list的确可以作为一种简单的消息队列,但是它具备一种致命的缺陷:没有消息确认机制,容易导致消息丢失。此外,也没有消息回溯和广播功能。stream的出现解决了上述问题,我们来了解以下

二、Stream基础指令

(一) 向队列中添加消息:XADD

在这里插入图片描述

示例:XADD users *  name jack

(二) 向队列中读取消息:XREAD

在这里插入图片描述

示例:
XREAD 1 2 STREAMS users 0 读取users队列的第一个消息(读取后队列并不会删除该消息,可重复读取)
XREAD 1 2 STREAMS users $ 读取users队列的最新消息 

注意:使用XREAD读取最新消息的漏洞
在这里插入图片描述
当我们执行handleMessage时,又有多条消息到达消息队列时我们只会读取最新的那条
在这里插入图片描述

(三) 使用消费者组来读取消息

在这里插入图片描述
也就是说同一个消费者组中的消费者们不会重复消费同一个消息,而多个消费者组可以消费同一个消息。被消费者消费但没有确认的消息会进到pending-list中,确认后移除。

1. 消费者组的创建、增加消费者、删除消费者操作

一般我们会提前创建好消费者组,消费者可以在读取消息时创建,也可以提前创建
在这里插入图片描述

2. 消费者组读取消息

在这里插入图片描述
消费者业务伪代码

while(true){
	try{
		//1.获取消息
		 List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("group1", "consumer1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create("message.queue", ReadOffset.lastConsumed())
	//2.判断消息是否存在
	if(list == null || list.isEmpty()){
		continue;
	}
	//3.解析消息
	Map map = list.get(0).getValue();
	ENtity entity = BeanUtil.fillBeanWithMap(map,new Entity(),true);
	//4.处理消息
	handleMsg(entity);
	//5.对消息进行确认
	ack(queue_name,group,id)
	}catch(Exception e){
		while(true){
			try{
				//1.获取消息
				 List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
		                            Consumer.from("group1", "consumer1"),
		                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
		                            StreamOffset.create("message.queue", ReadOffset.from("0"))
				//2.判断消息是否存在
				if(list == null || list.isEmpty()){
					break;
				}
				//3.解析消息
				Map map = list.get(0).getValue();
				ENtity entity = BeanUtil.fillBeanWithMap(map,new Entity(),true);
				//4.处理消息
				handleMsg(entity);
				//5.对消息进行确认
				ack(queue_name,group,id)
				}catch(e){
					log.debug("消息处理异常");
				}
			}
		}
	}
}

三、业务场景

我们要实现一个秒杀商品的业务,场景如下:客户端传来商品id,我们要为该商品扣减库存,并且创建订单,要求一人一单,不能重复购买
技术点:

  • 在高并发场景下正确扣减库存,不能发生超卖
  • 保证一人一单,不能重复购买
  • 在高并发下借用消息队列保持高性能

(一) 超卖问题

业务流程
1. 查询数据库判断库存是否大于0
2. 大于0则执行库存-1

上述流程中,当遇到高并发的场景时,多个线程同时执行到第一步时都判断成功,但执行到第二步就很有可能导致超卖,那怎么解决呢?
很多同学的第一反应是加锁,让他们串行执行,但这样的效率又太低。我们可以借助乐观锁的思想,在修改库存时判断库存是否大于0,这样既能解决超卖问题又能实现高性能。

(二) 一人一单问题

业务流程
1.得到用户id,商品id
2.使用用户id、商品id查询订单表的记录数量count
3.若count>0,表示该用户对商品下过单,返回结果,否则继续执行
4.乐观锁的方式修改商品库存,返回布尔值表示修改是否成功
5.修改成功则说明符合条件,保存订单到数据库中,若不成功则直接返回

该业务流程同样存在并发安全问题,需要加锁来解决(由于没有现成的字段可当作乐观锁的校验字段,故我们不采用乐观锁),而业务场景是一人一单,不同用户之间不存在安全问题,所以我们锁的范围不宜过大,只要锁定同一个用户就行,用户id非常合适,若在分布式系统中,使用jdk的锁无法保证安全问题,故需要使用分布式锁来解决问题,设置锁的key:lock:user:id,用redission保证线程安全问题

(三) 使用消息队列提升高并发场景的性能

在上述业务场景中,要执行相当多的数据库和加锁操作,若在高并发的场景下性能必定很低,我们需要优化业务流程,保证高并发下的性能问题

该业务流程实际包含两部分:一是购买资格校验(超卖、一人一单),二是数据库的修改(修改库存、保存订单),我们可以将资格校验部分放入redis中去做,数据库操作部分

具体流程:生产者
首先将该商品的库存和已购买用户列表添加到redis中,用户下单该商品时首先执行lua脚本判断库存是否足够和该用户id是否已经存在该商品的user_list中,若不满足则代表可以执行下列操作(此时是在redis中操作,并不是在数据库中操作)

  1. 库存-1
  2. 该用户id添加进user_list
  3. 将消息发送到消息队列

消费者
消费者拿到消息后只需要对数据库进行操作就行,修改商品库存和保存订单

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值