清淨
  • Java

    • Java 基础
  • 框架

    • Flyway
    • MapStruct
    • Spring Cloud
  • 中间件

    • Elasticsearch
    • Redis
    • RabbitMQ
    • Kafka
  • 三剑客

    • HTML
    • CSS
    • JavaScript
  • 进阶

    • jQuery
    • ECMAScript6
    • TypeScript
  • Vue

    • Vue2
    • Vue3
    • VuePress
  • Linux
  • Jenkins
  • Maven
  • MySQL
  • 插件工具
  • 代码集成
  • 其它

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • 设计模式
迁移日志
订阅号
微信订阅号
微信订阅号
GitHub
  • Java

    • Java 基础
  • 框架

    • Flyway
    • MapStruct
    • Spring Cloud
  • 中间件

    • Elasticsearch
    • Redis
    • RabbitMQ
    • Kafka
  • 三剑客

    • HTML
    • CSS
    • JavaScript
  • 进阶

    • jQuery
    • ECMAScript6
    • TypeScript
  • Vue

    • Vue2
    • Vue3
    • VuePress
  • Linux
  • Jenkins
  • Maven
  • MySQL
  • 插件工具
  • 代码集成
  • 其它

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • 设计模式
迁移日志
订阅号
微信订阅号
微信订阅号
GitHub
  • Redis

    • 简介
    • 安装
    • Redis 配置
    • 数据类型
    • Lua 脚本
    • Java 客户端
    • 实战

      • 短信登录
      • 查询缓存
      • 缓存穿透
      • 缓存击穿
      • 缓存雪崩
      • 全局唯一 ID
      • 超卖问题
      • 一人一单
      • 分布式锁
      • Redisson
      • 异步秒杀
      • 消息队列
      • 签到(BitMap)
      • UV 统计
    • 持久化
    • 集群

      • 主从集群
      • 哨兵集群
      • 分片(Cluster)集群
    • 最佳实践

      • Key-Value 设计
      • 服务端配置
    • 底层数据结构

      • 动态字符串 SDS
      • IntSet
      • Dict
      • ZipList
      • QuickList
      • SkipList
      • RedisObject
    • 数据类型底层实现

      • String
      • List
      • Set
      • ZSet
      • Hash
    • 内存管理策略

异步秒杀

在前面,我们实现了一个秒杀业务,它的流程如下:

  1. 根据优惠券 ID 查询数据库,判断优惠券是否存在。
  2. 判断优惠券的状态,例如开始抢购时间、结束抢购时间、优惠券库存。
  3. 根据用户 ID 和优惠券 ID 查询数据库,判断用户是否已经抢购过该优惠券。
  4. 更新数据库,修改优惠券库存。
  5. 插入数据库,生成用户订单。

上面的逻辑还有优化空间,流程是串行的,而且需要多次访问数据库,并且还在 3、4、5 这几个步骤加了分布式锁,所以接口的性能其实不高。

通过上面的步骤我们可以发现,抢券其实就分为两个步骤,判断用户资格以及生成订单。我们可以采用异步的方式进行处理,如果用户有资格,则生成一个订单号返回给用户,后台开启异步线程根据订单号创建订单。

判断用户是否有资格:

  1. 先把优惠券的库存同步到 Redis,采用 String 进行存储。例如 coupon:stock:id -> stock。
  2. 判断用户是否已经抢购过,可以借助 Redis 的 Set 实现。例如 coupon:order:id -> 用户1 的 ID, 用户2 的 ID。
  3. 如果用户有资格,则扣减库存,并把用户 ID 添加到 Set 中,表示用户已经抢过券。
  4. 步骤 1、2、3 需要使用 Lua 脚本保证原子性。

创建订单:

  • 把订单号、优惠券 ID、用户 ID 放入阻塞队列

couponOrder.lua:

local couponId = ARGV[1]
local userId = ARGV[2]

-- lua 脚本使用 .. 拼接字符串
local couponKey = 'coupon:stock:' .. couponId
local orderKey = 'coupon:order:' .. couponId

-- 1. 判断优惠券库存是否充足
if (tonumber(redis.call('get', couponKey) <= 0) then
    return 1
end

-- 2. 判断用户是否已经领取过该优惠券
if (redis.call('sismember', orderKey, userId) == 1) then
    return 2
end

-- 3. 扣减优惠券库存
redis.call('incrby', couponKey, -1)
-- 4. 将用户 id 添加到已领取优惠券的集合中
redis.call('sadd', orderKey, userId)

return 0

CouponOrderServiceImpl.java:

@Service
public class CouponOrderServiceImpl implements CouponOrderService {

    private static final DefaultRedisScript<Long> COUPON_ORDER_SCRIPT;

    private final BlockingQueue<CouponOrderTask> couponOrderTaskQueue = new LinkedBlockingDeque<>();

    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    static {
        COUPON_ORDER_SCRIPT = new DefaultRedisScript<>();
        COUPON_ORDER_SCRIPT.setLocation(new ClassPathResource("couponOrder.lua"));
        COUPON_ORDER_SCRIPT.setResultType(Long.class);
    }

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisIdWoker redisIdWoker;

    @Resource
    private CouponService couponService;

    @Resource
    private CouponOrderMapper couponOrderMapper;

    @PostConstruct
    public void init() {
        executorService.execute(() -> {
            while (true) {
                try {
                    // 从阻塞队列中获取任务
                    CouponOrderTask task = couponOrderTaskQueue.take();
                    // 处理任务
                    task.getCouponOrderService().createOrder(task);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
    }

    @Override
    public Long addCouponOrder(int userId, int couponId) throws Exception {
        Long result = stringRedisTemplate.execute(COUPON_ORDER_SCRIPT, Collections.emptyList(), String.valueOf(couponId), String.valueOf(userId));

        if (result == 1) {
            throw new Exception("优惠券已经抢购完");
        }
        if (result == 2) {
            throw new Exception("用户已经领取过该优惠券");
        }

        // 生成订单 ID
        long orderId = redisIdWoker.generateId("order");

        // 放入阻塞队列
        couponOrderTaskQueue.add(new CouponOrderTask(couponId, userId, orderId, AopContext.currentProxy()));

        return orderId;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public long createOrder(CouponOrderTask task) throws Exception {

        // 扣减优惠券库存
        int update = couponService.updateStock(task.getCouponId());
        if (update < 1) {
            throw new Exception("活动太火爆了,请稍后再试");
        }

        // 生成订单
        CouponOrder couponOrder = createOrder0(task.getUserId(), task.getCouponId(), task.getOrderId());
        couponOrderMapper.addCouponOrder(couponOrder);
        return couponOrder.getId();
    }

    private CouponOrder createOrder0(int userId, long couponId, long orderId) {
        CouponOrder couponOrder = new CouponOrder();
        couponOrder.setId(orderId);
        couponOrder.setUserId(userId);
        couponOrder.setCouponId(couponId);
        return couponOrder;
    }
}
想要编辑此页?
上次更新: 2025/4/26 23:45
贡献者: Junfeng Dai
←Redisson消息队列→
[ 纸上得来终觉浅绝知此事要躬行 ]
Copyright © 2021-present Junfeng Dai
蜀ICP备2021009537号