文章目录
1. 消息队列
1.1 基于List结构模拟消息队列
1.2 基于PubSub的消息队列
1.3 基于Stream的消息队列
既然上述三种消息队列都有其不可避免的缺点,那我们有没有办法解决呢?
接下来我们介绍的基于Stream的消息队列—消费者组就能解决上述三种消息队列的弊端。
2. 基于Stream的消息队列—消费者组
2.1 消费者组介绍
2.2 消费者监听消息基本思路
我们分析一下下面的伪代码:
首先我们尝试监听消息队列,如果消息队列没有消息,则continue结束本次循环,重试获取信息。如果一直没有消息,则进入阻塞状态,直到消息队列存入消息。
当消息队列有消息后,就开始处理消息,处理完消息后一定要执行ACK命令确认消息已经执行。
如果处理消息出现异常,则将消息存入pendingList(待处理)队列,程序执行过程中会尝试冲待处理队列中取消息,如果待处理队列没有数据,直接退出异常处理的循环,反之就处理异常信息。
2.3 消费者组总结
3. 基于Stream的消息队列–消费者组实现异步秒杀
3.1 需求分析
3.2 代码实现
3.2.1 创建Stream类型的消息队列
通过redis客户端创建名为stream.orders的消息队列
3.2.2 编写用户下单资格的lua脚本
用户下单资格的lua脚本我们可以在之前的基础上进行修改,添加订单id,最后将用户id,优惠券id,订单id添加到消息队列。
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]
-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
3.2.3 实现异步秒杀完整代码
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker RedisIdWorker;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
//定义一个线程池,异步执行下单操作
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
//spring提供的PostConstruct注解:类初始化完毕就执行
@PostConstruct
public void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderRunnable());
}
//定义处理秒杀的线程,该线程应该在类初始化就应该开始执行任务————如何做到?
//使用spring提供的PostConstruct注解:类初始化完毕就执行
private class VoucherOrderRunnable implements Runnable{
@Override
public void run() {
String queueName = "stream.orders";
while (true){
try {
//1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAM >
//从消息队列中读消息,g1组,消费者c1,一次读一个,阻塞时间2秒 ,从stream.orders队列读,>未消费的消息
List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
//2.判断消息队列是否为空
if(list==null||list.isEmpty()){
//如果为空,则说明没有消息,进行下一次循环
continue;
}
//解析消息
//因为我们一次读一个,所以索引为0,而我们存入消息队列的是键值对,因此解析出来是map
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//3.创建订单
createVoucherOrder(voucherOrder);
//4. 确认消息,XACK stream.orders,g1,id
redisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
} catch (Exception e) {
log.error("处理订单异常",e);
handlePendingList();
}
}
}
private void handlePendingList() {
String queueName = "stream.orders";
while (true){
try {
//1.获取PendingList中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAM 0
//从消息队列中读消息,g1组,消费者c1,一次读一个,阻塞时间2秒 ,
List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queueName, ReadOffset.from("0"))
);
//2.判断消息队列是否为空
if(list==null||list.isEmpty()){
//如果为空,则说明PendingList没有消息,结束循环
break;
}
//解析消息
//因为我们一次读一个,所以索引为0,而我们存入消息队列的是键值对,因此解析出来是map
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//3.创建订单
createVoucherOrder(voucherOrder);
//4. 确认消息,XACK stream.orders,g1,id
redisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
} catch (Exception e) {
log.error("处理订单异常",e);
}
}
}
}
private void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
RLock lock = redissonClient.getLock("lock:order" + userId);
//tryLock的三个参数:最大等待时间,锁释放时间,时间单位
boolean flag = lock.tryLock();//不设置参数默认不等待,释放时间三十秒
if(!flag){
return;
}
try {
//一人一单
int count = this.query().eq("user_id", userId)
.eq("voucher_id", voucherId).count();
if(count>0){
return ;
}
//当更新时查询的库存大于0时进行库存减一
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.gt("stock", 0)
.eq("voucher_id", voucherId).update();
if (!success) {
return;
}
//6. 创建订单
this.save(voucherOrder);
return ;
} finally {
lock.unlock();
}
}
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
long voucherOrderId = RedisIdWorker.nextId("order");
//使用lua脚本执行原子级别的操作,不会因为线程阻塞导致释放锁发生错误。
Long result = redisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(),String.valueOf(voucherOrderId));
//拆箱
int res = result.intValue();
//1. 判断库存是否大于0和用户是否已经下单
if (res != 0) {
return Result.fail(res == 1 ? "库存不足" : "用户已下单");
}
return Result.ok(voucherOrderId);
}
}
读取消息队列中的消息使用的方法是lastConsumed,也就是从未消费的第一个消息开始消费。
而读取待处理队列中的消息是从队列第一个开始。
用户下单到提示用户下单成功只需要经过下面的程序,比之前同步执行下单方法的速度快了不少。我们可以使用Jmeter进行响应时间的测试。
到这里,我们这一部分通过优惠券秒杀介绍了好多内容:
- 全局唯一ID生成器
- 实现优惠券秒杀下单
- 超卖问题如何解决
- 一人一单如何控制
- 从解决一人一单的悲观锁到分布式锁
- 使用阻塞队列实现异步秒杀
- 使用消息队列实现异步秒杀
这一部分到这里就算结局了,看到这里的小伙伴不妨好好回顾一下每一部分是如何完成的,我们又是如何进行优化的。
念念不忘,必有回响!!!