基于JVM的阻塞队列实现的异步秒杀问题:
内存限制问题
数据安全问题
1、消息队列引入
使用队列的好处在于 **解耦:**所谓解耦,举一个生活中的例子就是:快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,如果耦合,那么这个快递员相当于直接把快递交给你,这事固然好,但是万一你不在家,那么快递员就会一直等你,这就浪费了快递员的时间,所以这种思想在我们日常开发中,是非常有必要的。
这种场景在我们秒杀中就变成了:我们下单之后,利用redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快我们的响应速度。
2、基于Redis的List结构模拟消息队列(5种基本数据类型之一)
List:双向链表
基于List的消息队列有哪些优缺点?
优点:
* 利用Redis存储,不受限于JVM内存上限
* 基于Redis的持久化机制,数据安全性有保证 做数据存储的数据结构都支持持久化
* 可以满足消息有序性
缺点:
* 无法避免消息丢失
* 只支持单消费者
3、基于Redis的LPubSub结构消息队列(专门的发布订阅机制)
PSUBSCRIBE:pattern
基于PubSub的消息队列有哪些优缺点?
优点:
* 采用发布订阅模型,支持多生产、多消费
缺点:
发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息
* 不支持数据持久化 本身就是订阅发布机制,不同于基础数据存储数据类型,故而不支持持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃
* 无法避免消息丢失 无消费者订阅,channel的消息会丢失
* 消息堆积有上限,超出时数据丢失 如果有消费者监听,消息会存储至消费者的缓存区域(有上限)
4、基于Redis的Stream数据类型消息队列(也是一个数据类型,故而支持持久化)
Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
消息读完不会被删除
若block=0 则表示一直阻塞
STREAM类型消息队列的XREAD命令特点:
* 消息可回溯 不会丢失,永久保存于队列中
* 一个消息可以被多个消费者读取
* 可以阻塞读取 添加block指定时间
* 有消息漏读的风险 使用$时,由于只会读取最新的,导致之前的漏读
5、基于Redis的Stream的消息队列-消费者组
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
消息标示机制确保消息拿到会被消费,即使消费者拿到消息后宕机,重启后任然可以从标示处消费
消息确认机制,每个消费者都有自己的pendingList,确保每个消息至少被消费一次
消费者组方法:
消息确认:
查看指定组里的pendingList,可同时指定消费者
从pendingList读取未确认消息,只需把XReadGROUP ID 改为 0
消费者监听消息的基本思路:
STREAM类型消息队列的XREADGROUP命令特点:
* 消息可回溯
* 可以多消费者争抢消息,加快消费速度
* 可以阻塞读取
* 没有消息漏读的风险
* 有消息确认机制,保证消息至少被消费一次
三种不同类型消息队列对比:
修改秒杀下单优化案例:
手动创建消费者组 g1 和消息队列 stream.order:
127.0.0.1:6379> XGROUP CREATE stream.order g1 0 MKSTREAM
OK
向luajiaoben中添加:
-- 3.6.发送消息到队列 XADD stream.order * k1 v1 k2 v2 ...
redis.call("xadd", "stream.order", "*", "userId", userId, "voucherId", voucherId, "id", orderId)
修改voucherOrderHandler类,新建handlePendingList方法:
/**
* 秒杀优化后代码
* 基于redis的stream消息队列
*/
private class voucherOrderHandler implements Runnable {
String queuename = "stream.order";
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.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()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
//转成voucherOrder对象
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
handleVoucherOrder(voucherOrder);
// 4.确认消息 XACK key group id
stringRedisTemplate.opsForStream().acknowledge(queuename, "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
//处理异常消息
handlePendingList();
}
}
}
public void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 STREAMS s1 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queuename, ReadOffset.from("0"))
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明pending-list没有消息,继续下一次循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
//转成voucherOrder对象
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
handleVoucherOrder(voucherOrder);
// 4.确认消息 XACK key group id
stringRedisTemplate.opsForStream().acknowledge(queuename, "g1", record.getId());
//处理异常消息
} catch (Exception e) {
log.error("处理pendding-list订单异常", e);
try {
Thread.sleep(20);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
}
}
}
修改seckillVoucher():
/**
* 秒杀优化后代码
* 基于Redis的stream
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户id
Long userId = UserHolder.getUser().getId();
// 订单id
long orderId = redisIdWorker.nextId("order");
// 1.执行lua脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(), String.valueOf(orderId));
// 2.判断结果是否为0
int r = result.intValue();
if (r != 0) {
// 2.1 不为0,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "重复下单");
}
// 3.获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId);
}
测试:成功实现下单操作,数据库成功减一
秒杀(超卖、一人一单)总结: