原本的优惠券秒杀业务中,查询优惠券、查询订单、减库存、创建订单都是直接访问的MySQL数据库,其中减库存和创建订单是写操作,当高并发的时候会给数据库造成较大的压力。
在redis中:
利用string结构保存优惠券的库存
利用set集合(可以存多个值且不可重复)来实现一人一单功能:
同时利用lua脚本实现上述两个操作的原子性
需求:
- // 保存秒杀库存到Redis中
- stringRedisTemplate.opsForValue()
- .set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
- -- 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
- public Result seckillVoucher(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- long orderId = redisIdWorker.nextId("order");
- // 1.执行lua脚本
- Long result = stringRedisTemplate.execute(
- SECKILL_SCRIPT,
- Collections.emptyList(),
- voucherId.toString(), userId.toString(), String.valueOf(orderId)
- );
- int r = result.intValue();
- // 2.判断结果是否为0
- if (r != 0) {
- // 2.1.不为0 ,代表没有购买资格
- return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
- }
- // 3.返回订单id
- return Result.ok(orderId);
- }
- private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
-
- @PostConstruct //当前类初始化完毕就执行这个任务
- private void init() {
- SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
- }
- private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
- private class VoucherOrderHandler implements Runnable{
-
- @Override
- public void run() {
- while (true){
- try {
- // 1.获取队列中的订单信息
- VoucherOrder voucherOrder = orderTasks.take();
- // 2.创建订单
- createVoucherOrder(voucherOrder);
- } catch (Exception e) {
- log.error("处理订单异常", e);
- }
- }
- }
- }
基于JVM实现的异步秒杀存在两个问题,一个是JVM内存有限,当有大量并发的时候就有可能超过JVM内存上限,二是数据安全问题,JVM的阻塞队列没有持久化机制,当出现宕机或者发生异常时,订单就会丢失。所以使用消息队列解决这两个问题:
队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。
基于List的消息队列有哪些优缺点?
优点:
缺点:
消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
基于PubSub的消息队列有哪些优缺点?
优点:
缺点:
Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。
STREAM类型消息队列的XREAD命令特点:
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
STREAM类型消息队列的XREADGROUP命令特点: