Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
    • 一. 秒杀优惠卷的核心挑战
    • 二、秒杀优惠券的设计思路
      • 2.1 V1 版本:单机版
      • 2.1.1 设计目标
      • 2.1.2 核心代码
      • 2.1.3 问题分析
      • 2.1.3 核心流程
      • 2.1.4 并发安全保障
      • 2.1.5 方案分析
      • 2.1.6 后续优化方向
      • 2.2 V2 版本:分布式版
      • 2.2.1 设计目标
      • 2.2.2 核心代码
      • 2.2.3 核心流程
      • 2.2.4 并发安全保障
      • 2.2.5 方案分析
      • 2.2.6 后续优化方向
      • 2.3 V2 pro 版本:分布式+异步版
      • 2.3.1 设计目标
      • 2.3.2 核心代码
      • 2.3.3 核心流程
      • 2.3.4 并发安全保障
      • 2.3.5 方案分析
      • 2.3.6 后续优化方向
      • 2.4 v2Pro Max版本:分布式+异步+本地缓存版(狂霸酷炫吊炸天版)
      • 2.4.1 设计目标
      • 2.4.2 核心代码
      • 2.4.3 核心流程
      • 2.4.4 方案分析
      • 2.4.5 后续优化方向
    • 三、总结
  • AOP无侵入式告警
  • 长短链接跳转
  • 双 Token 登录
  • 订单超时取消
  • 实践
Tavio
2025-10-25
目录

高并发秒杀优惠卷

在电商平台中,秒杀活动是一种常见的促销手段,旨在通过限时限量的优惠吸引大量用户参与购买。 但秒杀场景的“高并发、短时间、大流量”特性,对系统的稳定性和性能提出了极高挑战。

# 一. 秒杀优惠卷的核心挑战

在开发前需要先明确秒杀场景下的核心技术难点,后续的设计优化都围绕这些难点展开:

  1. 高并发处理:秒杀活动通常会在短时间内吸引大量用户同时请求,系统需要能够高效处理这些并发请求,避免服务器过载或崩溃。
  2. 库存超卖问题:由于大量用户同时抢购,容易出现库存不足但系统仍然允许购买的情况,导致超卖问题。
  3. 一人多单问题:为了防止用户通过多次请求抢购到多份优惠券,需要确保每个用户在秒杀活动中只能购买一份优惠券。

# 二、秒杀优惠券的设计思路

# 2.1 V1 版本:单机版

# 2.1.1 设计目标

在 V1 版本中,我们采用最简单的设计思路,直接在单机环境下处理秒杀请求,不引入 Redis,仅依赖:

  • JVM 层面的 synchronized 锁
  • 数据库乐观锁(stock > 0)
  • 数据库事务

适用场景:

  • 单机部署或少量机器
  • 日活用户 < 10W
  • 并发请求 < 1000 QPS

# 2.1.2 核心代码

  1. 订单创建逻辑(com.seckill.demo.v1.service.impl.CouponOrderV1Service.java)
@Transactional(rollbackFor = {Exception.class, SeckillException.class})
public String createOrder(Long userId, Long couponId){
    // 1.判断用户是否已抢购过该优惠券
    int count = couponOrderMapper.countByUserIdAndCouponId(userId, couponId);
    if (count > 0) {
        log.warn("用户已抢购过该优惠券, userId={}, couponId={}", userId, couponId);
        throw new SeckillException(3001, "用户已领取过该优惠券");
    }

    // 2.扣减库存
    int rows = couponMapper.decreaseStock(couponId);
    if (rows == 0) {
        log.warn("优惠券库存不足, couponId={}", couponId);
        throw new SeckillException(3002, "优惠券库存不足");
    }

    // 3.创建订单
    String orderNo = "ORDER" + UUID.randomUUID().toString().replace("-", "");
    CouponOrder order = new CouponOrder();
    order.setOrderNo(orderNo);
    order.setUserId(userId);
    order.setCouponId(couponId);
    order.setStatus(1); // 未使用
    order.setCreateTime(LocalDateTime.now());
    order.setUpdateTime(LocalDateTime.now());

    couponOrderMapper.createOrder(order);
    return orderNo;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. 库存扣减 SQL(CouponMapper.xml)**
<!-- 扣减库存(乐观锁:stock > 0 防止超卖) -->
<update id="decreaseStock">
    UPDATE coupon
    SET stock = stock - 1
    WHERE id = #{couponId}
      AND stock > 0
</update>
1
2
3
4
5
6
7

核心机制:AND stock > 0 保证只有库存充足时才能扣减成功,MySQL 的行锁保证并发安全。

# 2.1.3 问题分析

尽管核心代码中已经通过数据库乐观锁解决了库存超卖问题,但由于 SELECT COUNT 和 INSERT 之间没有锁机制,仍然会存在一人多单的问题。

时间线:同一用户的两个并发请求

线程A: SELECT COUNT(*) → 返回 0 → 准备插入
线程B: SELECT COUNT(*) → 返回 0(A还没提交)→ 准备插入
线程A: INSERT 订单 → 提交事务
线程B: INSERT 订单 → 提交事务  ❌ 重复下单!
1
2
3
4
5
6

根本原因:

  • @Transactional 只保证单个事务的 ACID,无法阻止并发事务读到相同的初始状态
  • "查询-判断-插入" 是非原子操作,存在 check-then-act 竞态条件
  1. 解决方案:引入 JVM 层面的 synchronized 锁,确保同一时间只有一个线程能处理同一用户的秒杀请求。
String lockKey = "seckill:" + couponId + ":" + userId;
synchronized (lockKey.intern()) {
    return couponOrderV1Service.createOrder(userId, couponId);
}
1
2
3
4

潜在问题:存在内存泄漏风险:

调用 str.intern() 时,若字符串不在 StringTable 中,JVM 会将该字符串对象的强引用存入 StringTable。

场景模拟:
假设有 100 万用户,10 种优惠券
会产生 1000 万个 不同的 lockKey 字符串进入常量池
这些字符串永远不会被 GC 回收!
1
2
3
4

优化方案:使用 ConcurrentHashMap 管理锁对象

private static class LockWrapper {
    private final AtomicInteger count = new AtomicInteger(1);
}

private final ConcurrentHashMap<String, LockWrapper> lockMap = new ConcurrentHashMap<>();

String lockKey = "seckill:" + couponId + ":" + userId;
// 原子操作:获取或创建锁,并增加引用计数
LockWrapper lock = lockMap.compute(lockKey, (key, value) -> {
    if (value == null) {
        return new LockWrapper();
    }
    value.count.incrementAndGet();
    return value;
});
synchronized (lock) {
    try {
        return couponOrderV1Service.createOrder(userId, couponId);
    } finally {
        // 减少引用计数,若为0则移除锁对象
        lockMap.computeIfPresent(lockKey, (key, value) -> {
            if (value.count.decrementAndGet() == 0) {
                return null; // 移除锁对象
            }
            return value;
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 2.1.3 核心流程

客户端请求
    ↓
[Controller] 接收请求,参数校验
    ↓
[SeckillV1Service] 
    ① 查询优惠券信息
    ② 校验活动时间(begin_time ~ end_time)
    ③ 校验活动状态(status == 1)
    ④ 获取 JVM 锁:synchronized(lockKey)
    ↓
[CouponOrderV1Service]
    ⑤ 判断用户是否已抢购(SELECT COUNT)
    ⑥ 扣减库存(UPDATE ... WHERE stock > 0)【乐观锁】
    ⑦ 创建订单(INSERT)
    ↓
释放 JVM 锁,提交事务
    ↓
返回订单号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.1.4 并发安全保障

┌─────────────────────────────────────────────────────────────────────────────┐
│                           并发安全保障体系                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  第1层:JVM 锁池                                                            │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  lockPool[hash(userId:couponId) % 1024]                             │   │
│  │  作用:同一用户 + 同一优惠券的请求串行执行,防止一人多单               │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    ↓                                        │
│  第2层:数据库查询校验                                                       │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  SELECT COUNT(*) WHERE user_id = ? AND coupon_id = ?                │   │
│  │  作用:二次校验用户是否已领取                                         │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    ↓                                        │
│  第3层:数据库乐观锁                                                         │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  UPDATE ... WHERE stock > 0                                         │   │
│  │  作用:防止库存超卖(多用户并发扣减时只有一个成功)                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    ↓                                        │
│  第4层:数据库唯一索引                                                         │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  UNIQUE KEY uk_user_coupon (user_id, coupon_id)                     │   │
│  │  作用:最后防线,即使代码有 bug 也不会产生重复数据                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 2.1.5 方案分析

  • 优点:
    • 实现简单,易于理解和维护
    • 适用于小规模秒杀活动,部署成本低
  • 缺点:
    • 随着并发量增加,JVM 锁成为瓶颈
    • JVM 锁只能作用在单节点,不适用于分布式部署,无法横向扩展
    • 缺乏限流保护

# 2.1.6 后续优化方向

  • 引入分布式锁(如 Redis 分布式锁)替代 JVM 锁,支持多节点部署
  • 使用 Redis 预减库存,减轻数据库压力
  • 引入消息队列异步处理订单,提升系统吞吐量,提高用户体验
  • 实现限流策略,保障系统稳定性

# 2.2 V2 版本:分布式版

# 2.2.1 设计目标

在V2版本中,我们将引入Redis作为分布式缓存和分布式锁的实现工具,以支持多节点部署和更高的并发处理能力。核心思路包括:

  • 使用Redis预减库存,减少数据库压力
  • 使用Redis分布式锁,确保同一用户的请求在分布式环境下串行执行
  • 实现限流策略,保障系统稳定性

适用场景:

  • 分布式部署环境
  • 日活用户 > 10W
  • 并发请求 > 1000 QPS

# 2.2.2 核心代码

  1. 使用Redis避免优惠卷详情查询的SQL瓶颈(com.seckill.demo.v2.service.impl.CouponV2Service.java)
public Coupon getCouponById(Long couponId) {
    // 1. 优先从Redis缓存获取优惠券信息
    Object couponJsonInfo = redisTemplate.opsForValue().get(RedisKeyConstants.COUPON_INFO_KEY.getKey() + couponId);
    if (couponJsonInfo != null) {
        return (Coupon) couponJsonInfo;
    }
    
    // 2. 缓存未命中,从数据库加载优惠券信息
    Coupon coupon = couponMapper.selectById(couponId);
    if (coupon != null) {
        // 3. 将优惠券信息写入Redis缓存
        redisTemplate.opsForValue().set(RedisKeyConstants.COUPON_INFO_KEY.getKey() + couponId, couponJsonInfo);
    }
    return coupon;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

潜在问题:高并发场景下容易造成缓冲穿透:

优化方案:使用空值缓存(缓存空对象,避免无效查询反复查询数据库) + 参数校验(校验优惠卷ID合法性)

// 参数校验
if (couponId == null || couponId <= 0) {
    throw new SeckillException(400, "无效的优惠券ID");
} 

// 空对象缓存
Object couponJsonInfo = redisTemplate.opsForValue().get(RedisKeyConstants.COUPON_INFO_KEY.getKey() + couponId);
if (couponJsonInfo != null) {
    // 2.1 缓存命中,判断是否为NullValue, 防止无效请求反复查询数据库
    if (couponJsonInfo instanceof NullValue) {
        return null;
    }
    return (Coupon) couponJsonInfo;
}

// 3. 缓存未命中,从数据库加载优惠券信息
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
    // 3.1 将空值写入缓存,防止缓存穿透
    redisTemplate.opsForValue().set(
            RedisKeyConstants.COUPON_INFO_KEY.getKey() + couponId,
            new NullValue(),
            NULL_CACHE_EXPIRE,
            TimeUnit.MINUTES
    );
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

** 潜在问题:缓存失效期间,大量请求涌入造成缓存击穿**

优化方案:使用分布式锁更新缓存

// 3. 缓存未命中,从数据库加载优惠券信息
String lockKey = RedisKeyConstants.COUPON_LOCK_KEY.getKey() + couponId;
// 3.1 获取分布式锁,防止缓存击穿
RLock lock = redissonClient.getLock(lockKey);
try {
    boolean isLocked = lock.tryLock(
            RedisConstants.LOCK_WAIT_TIME,
            TimeUnit.SECONDS
    );
    if (!isLocked) {
        log.error("获取更新优惠券锁失败, couponId={}", couponId);
        throw new SeckillException(4002, "系统繁忙,请稍后重试");
    }

    // 3.2 再次检查缓存,防止线程在获取锁期间,其他线程已更新缓存
    couponObject = redisTemplate.opsForValue().get(cacheKey);
    if (couponObject != null) {
        if (couponObject instanceof NullValue) {
            return null;
        }
        return (Coupon) couponObject;
    }
    
    Coupon coupon = couponMapper.selectById(couponId);
    if (coupon == null) {
        // 3.3 将空值写入缓存,防止缓存穿透
        redisTemplate.opsForValue().set(
                cacheKey,
                new NullValue(),
                RedisConstants.NULL_CACHE_EXPIRE,
                TimeUnit.MINUTES
        );
        return null;
    }

    // 3.4 将优惠券信息写入Redis缓存
    redisTemplate.opsForValue().set(
            cacheKey,
            coupon,
            RedisConstants.NORMAL_CACHE_EXPIRE,
            TimeUnit.MINUTES);
    return coupon;
}   catch (Exception e) {
    log.error("获取更新优惠券锁被中断, couponId={}", couponId);
    throw new SeckillException(4002, "系统繁忙,请稍后重试");
}   finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  1. 使用Redis预减库存(com.seckill.demo.v2.service.impl.SeckillV2ServiceImpl.java)
String stockKey = RedisKeyConstants.SECKILL_STOCK_KEY.getKey() + couponId;
String orderKey = RedisKeyConstants.SECKILL_ORDER_KEY.getKey() + couponId;

Boolean haveOrder = redisTemplate.opsForSet().isMember(orderKey, userId);
if (Boolean.TRUE.equals(haveOrder)) {
    throw new SeckillException(2005, "用户已参与抢购");
}
Object stockObj = redisTemplate.opsForValue().get(stockKey);
if (stockObj == null || (int) stockObj <= 0) {
    throw new SeckillException(2006, "库存不足");
}

redisTemplate.opsForValue().decrement(stockKey);
redisTemplate.opsForSet().add(orderKey, userId.toString());
1
2
3
4
5
6
7
8
9
10
11
12
13
14

潜在问题:分散的Redis操作,无法保证原子性:

优化方案:使用Lua脚本保证预减库存和记录用户抢购操作的原子性

-- 检查用户是否已经下单
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
    -- 用户已下单,返回2
    return 2
end

-- 检查库存是否充足
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock <= 0 then
    -- 库存不足,返回1
    return 1
end

-- 扣减库存
redis.call('DECRBY', KEYS[1], 1)

-- 记录用户已下单
redis.call('SADD', KEYS[2], ARGV[1])


return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 2.执行Lua脚本,完成一人一单校验和库存预扣减
String stockKey = RedisKeyConstants.SECKILL_STOCK_KEY.getKey() + couponId;
String orderKey = RedisKeyConstants.SECKILL_ORDER_KEY.getKey() + couponId;

Long result = (Long) redisTemplate.execute(seckillScript, Arrays.asList(stockKey, orderKey), userId.toString());
if (result == null) {
    log.error("Lua脚本执行异常,返回空结果, userId={}, couponId={}", userId, couponId);
    throw new SeckillException(2005, "系统异常,请稍后重试");
}
int luaResult = result.intValue();
if (luaResult == 1) {
    // 2.1库存不足
    log.info("优惠券已抢完,库存不足, couponId={}", couponId);
    throw new SeckillException(2006, "优惠券已抢完");
}
if (luaResult == 2) {
    // 2.2用户已抢购过
    log.info("用户已抢购过该优惠券, userId={}, couponId={}", userId, couponId);
    throw new SeckillException(2007, "您已经抢购过该优惠券");
}

couponOrderV2Service.createOrder(userId, couponId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

潜在问题:Redis预减库存成功,但后续订单创建失败,导致库存不一致:

优化方案:创建订单失败后,回滚Redis库存和用户抢购记录

-- 检查用户是否在订单集合中
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 0 then
    -- 用户不在订单集合中,说明已经回滚过了或者根本没有下单
    return 0
end

-- 1.恢复库存
redis.call('INCRBY', KEYS[1], 1)

-- 2.移除用户记录
redis.call('SREM', KEYS[2], ARGV[1])

return 1
1
2
3
4
5
6
7
8
9
10
11
12
13
try {
    orderId = couponOrderV2Service.createOrder(userId, couponId);
}   catch (Exception e) {
    log.error("创建订单失败,开始回滚Redis库存和用户记录, userId={}, couponId={}, error={}", userId, couponId, e.getMessage());
    // 回滚Redis库存和用户记录
    rollbackRedis(userId, couponId);
}

// Redis 回滚 + 重试机制
while (retryCount < MAX_RETRY_COUNT) {
    try {
        Long result = redisTemplate.execute(seckillRollbackScript, Arrays.asList(stockKey, orderKey), userId.toString());
        if (result != null && result.intValue() == 1) {
            log.info("Redis回滚成功, couponId={}, userId={}, retryCount={}", couponId, userId, retryCount);
            return;
        } else if (result != null && result.intValue() == 0) {
            // 返回0表示用户不在订单集合中,可能已经回滚过了
            log.warn("Redis回滚失败,用户不在订单集合中, couponId={}, userId={}, result={}", couponId, userId, result);
            return;
        } else {
            // 回滚结果异常,需要重试
            log.error("Redis回滚返回异常结果, couponId={}, userId={}, result={}, retryCount={}", couponId, userId, result, retryCount);
            retryCount++;
            sleepBeforeRetry(retryCount);
        }
    } catch (Exception e) {
        retryCount++;
        log.error("Redis回滚异常, couponId={}, userId={}, retryCount={}, error={}", couponId, userId, retryCount, e.getMessage());
        sleepBeforeRetry(retryCount);
    }
}

// 指数退避,避免无间隙重试导致CPU飙升
private void sleepBeforeRetry(int retryCount) {
    try {
        // 指数退避: 100ms, 200ms, 300ms...
        Thread.sleep(100L * retryCount);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

潜在问题:Redis回滚重试阻塞,影响用户体验:

优化方案:使用异步线程池执行回滚操作,避免阻塞主线程

// 使用线程池异步回滚Redis库存和用户记录,避免阻塞主线程
CompletableFuture.runAsync(() ->
        rollbackRedis(userId, couponId),
        customExecutor
).exceptionally(ex -> {
    // 异步任务执行异常时的处理
    log.error("异步回滚任务执行异常,记录对账日志等待兜底, userId={}, couponId={}, orderId={}", userId, couponId, orderId, ex);
    // 添加对账日志,等待定时对账处理
    reconcileLogService.addReconciliationLog(userId, couponId, 0);
    return null;
});
1
2
3
4
5
6
7
8
9
10
11

潜在问题:Redis回滚失败无后续处理:

优化方案:记录对账日志,定时任务扫描处理(com.seckill.demo.task.ReconcileTask.java),兜底任务处理失败通知人工介入(短信通知)

Long result = redisTemplate.execute(seckillRollbackScript, Arrays.asList(stockKey, orderKey), userId.toString());
// Redis脚本执行成功,表示库存已回滚,更新对账日志状态
if (result != null && result.intValue() == 1) {
    reconcileLog.setStatus(1);
    reconcileLogMapper.updateStatusById(reconcileLog);
    return;
}   else {
    // 短信通知人工介入...
    log.error("对账失败,Redis脚本执行失败,userId={}, couponId={}", userId, couponId);
}
1
2
3
4
5
6
7
8
9
10
  1. Redis 用户级别接口限流(com.seckill.demo.v2.interceptor.RateLimitInterceptor.java)

使用Redis有序集合实现滑动窗口限流 + 降级策略正常放行,避免限流器异常影响正常业务

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 移除窗口外的过期请求,防止redis 大key
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)

if count < limit then
    -- 未超额,允许请求,score=now,member=now-随机数,防止重复
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
    -- 设置过期时间,防止key长期占用内存,多加1秒的缓冲时间
    redis.call('PEXPIRE', key, window + 1000)

    return 1
end

-- 超额,拒绝请求
return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean tryAcquire(String key, int limit, long windows) {
    try {
        long now = System.currentTimeMillis();
        Long result = redisTemplate.execute(
                rateLimiterScript,
                Collections.singletonList(key),
                String.valueOf(limit),
                String.valueOf(windows),
                String.valueOf(now)
        );

        return result != null && result == 1;
    }   catch (Exception e) {
        log.error("分布式限流执行异常, key={}", key, e);
        // 限流器异常时,默认放行,降级策略,避免影响正常业务
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

潜在问题:多节点部署,orderId可能重复:

优化方案:使用雪花算法生成全局唯一订单号

# 2.2.3 核心流程

客户端请求
    ↓
[Controller] 接收请求,参数校验
    ↓
[SeckillV2Service]
    ① Redis限流校验
    ↓
    ② 查询优惠券信息(Redis缓存 + DB回源 + 分布式锁防击穿)
    ③ 校验活动时间(begin_time ~ end_time)
    ④ 校验活动状态(status == 1)
    ⑤ 执行Lua脚本,预减库存并记录用户抢购操作
    ↓
[CouponOrderV2Service]
    创建订单(数据库事务)
    ↓
若订单创建失败,异步回滚Redis库存和用户抢购记录
    ↓
返回订单号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.2.4 并发安全保障

使用Lua脚本代替JVM锁,确保在分布式环境下同一用户的请求串行执行,防止一人多单问题。同时提升系统的并发处理能力和稳定性。

# 2.2.5 方案分析

  • 优点:
    • 支持分布式部署,易于横向扩展
    • 提升系统吞吐量和用户体验
    • 有效防止缓存穿透和击穿问题
  • 缺点:
    • 数据库压力仍然较大,可能成为瓶颈,尤其在高并发场景下,多用户同时创建订单时,数据库写入压力剧增
    • 需要额外维护Redis组件,增加系统复杂度

# 2.2.6 后续优化方向

  • Redis压力过大时,考虑使用本地缓存(如Caffeine)结合Redis,减轻Redis读压力
  • 引入消息队列异步处理订单,进一步减轻数据库压力

# 2.3 V2 pro 版本:分布式+异步版

# 2.3.1 设计目标

在V2版本中,Redis已经有效解决了分布式环境下的高并发处理和一人多单问题,但数据库在高并发场景下仍然可能成为瓶颈。场景分析:

  • 大量用户同时抢购时,虽然Redis预减库存能快速响应请求,但后续的订单创建仍然需要写入数据库,导致数据库写入压力剧增,可能引发性能下降甚至宕机。
  • 用户体验方面,订单创建的同步处理可能导致请求响应时间较长,影响用户满意度。

在V2 pro版本中,我们将引入消息队列(如RabbitMQ或Kafka)异步处理订单创建操作,以进一步提升系统的吞吐量和稳定性。核心思路包括:

  • 使用消息队列异步处理订单创建,减轻数据库压力
  • 优化用户体验,减少请求响应时间

适用场景:

  • 大型秒杀活动,用户量和请求量极高
  • 需要极高的系统稳定性和用户体验

# 2.3.2 核心代码

  1. RabbitMQ 发送消息(com.seckill.demo.v2pro.service.impl.SeckillV2ProServiceImpl.java)

通过MQ异步创建订单,减轻数据库压力,削峰填谷,平缓处理数据库写入压力

SeckillMessage message = new SeckillMessage(
        orderId,
        userId,
        couponId,
        System.currentTimeMillis()
);

// 发送消息到RabbitMQ,异步创建订单
rabbitTemplate.convertAndSend(
        MQKeyConstants.SECKILL_EXCHANGE.getKey(),
        MQKeyConstants.SECKILL_ROUTING_KEY.getKey(),
        message
);
1
2
3
4
5
6
7
8
9
10
11
12
13

潜在问题:消息丢失导致订单未创建:

优化方案:开启RabbitMQ消息持久化 + 消息确认机制

开启生产者确认机制,确保消息成功投递到RabbitMQ服务器 + 开启消息持久化,确保RabbitMQ重启后消息不丢失。

/**
 * 可靠连接工厂,开启发布者确认
 */
@Bean
public CachingConnectionFactory confirmConnectionFactory(RabbitProperties properties) {
    CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
    connectionFactory.setHost(properties.getHost());
    connectionFactory.setPort(properties.getPort());
    connectionFactory.setUsername(properties.getUsername());
    connectionFactory.setPassword(properties.getPassword());
    connectionFactory.setVirtualHost(properties.getVirtualHost());
    // 开启发布者确认
    connectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
    // 开启发布者返回,需要可配置
    connectionFactory.setPublisherReturns(true);

    return connectionFactory;
}

/**
 * 可靠 RabbitTemplate(开启发布者确认)
 * 适用于对消息可靠性要求高的场景,如秒杀订单
 */
@Bean
public RabbitTemplate confirmRabbitTemplate(ConnectionFactory confirmConnectionFactory, MessageConverter messageConverter) {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(confirmConnectionFactory);
    rabbitTemplate.setMessageConverter(messageConverter);

    // 开启消息返回(mandatory=true 表示消息无法路由时会返回给发送者)
    rabbitTemplate.setMandatory(true);

    // 设置返回回调
    rabbitTemplate.setReturnsCallback(returned ->
            log.error("消息被退回: exchange={}, routingKey={}, replyCode={}, replyText={}, message={}",
                    returned.getExchange(),
                    returned.getRoutingKey(),
                    returned.getReplyCode(),
                    returned.getReplyText(),
                    returned.getMessage())
    );
    return rabbitTemplate;
}
// 发送消息时,使用 CorrelationData 进行消息确认
CorrelationData correlationData = new CorrelationData(orderNo);
rabbitTemplate.convertAndSend(
        MQConstants.SECKILL_EXCHANGE,
        MQConstants.SECKILL_ROUTING_KEY,
        message,
        correlationData
);
// 同步等待消息确认
CorrelationData.Confirm confirm = correlationData.getFuture().get(CONFIRM_TIMEOUT_MS, TimeUnit.MILLISECONDS);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

潜在问题:同步等待消息确认,影响请求响应时间:

优化方案:使用异步回调处理消息确认,避免阻塞主线程

correlationData.getFuture().whenComplete((confirmResult, ex) -> {
    if (confirmResult != null && confirmResult.isAck()) {
        log.info("开始创建秒杀订单, userId={}, couponId={}, orderId={}", userId, couponId, orderId);
        return;
    }
    
    String reason = ex != null ? ex.getMessage() :
            (confirmResult != null ? confirmResult.getReason() : "确认超时");
    log.error("消息发送失败: {}, 原因: {} ", correlationData, reason);
    // 消息发送失败,异步回滚Redis库存和用户记录
    rollbackRedis(userId, couponId);
});
1
2
3
4
5
6
7
8
9
10
11
12
  1. RabbitMQ 消费消息(com.seckill.demo.v2pro.mq.SeckillMessageListener.java)
@RabbitListener(queues = "#{MQKeyConstants.SECKILL_QUEUE.getKey()}")
public void handleSeckillOrder(SeckillMessage message) {

    String orderId = message.getOrderNo();
    log.info("收到秒杀消息, orderId={}", orderId);
    couponOrderService.createOrder(message);
}
1
2
3
4
5
6
7

潜在问题: 消费者处理消息失败,导致订单未创建: 优化方案:开启消息确认机制 + 死信队列重试机制

开启消费者确认机制,确保消息成功处理后才从队列中移除 + 配置死信队列,失败消息进入死信队列进行重试处理

// 配置消费者监听容器工厂,开启手动确认模式
@Bean
public SimpleRabbitListenerContainerFactory confirmRabbitListenerFactory(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(messageConverter);
    factory.setAcknowledgeMode(org.springframework.amqp.core.AcknowledgeMode.MANUAL);
    factory.setPrefetchCount(1);
    return factory;
}
1
2
3
4
5
6
7
8
9
10

监听秒杀队列的消费者,手动确认消息 + 消息重试(com.seckill.demo.mq.SeckillMessageListener.java)

// 手动确认消息 + 消息重试
private void handleRetry(SeckillMessage message, Message mqMessage, Channel channel,
                         long deliveryTag, String idempotentKey) throws IOException {
  // 1. 删除幂等性key,允许重试
  redisTemplate.delete(idempotentKey);

  int retryCount = getRetryCount(mqMessage);
  if (retryCount < MAX_RETRY_COUNT) {
    // 2.手动重发消息,并递增重试次数
    log.info("消息重试,retryCount={}, orderId={}", retryCount + 1, message.getOrderNo());
    resendWithRetryCount(message, retryCount + 1);
    channel.basicAck(deliveryTag, false);
    return;
  }

  // 3. 超过重试次数,发送到死信队列
  log.warn("重试次数已达上限,发送到死信队列,orderId={}", message.getOrderNo());
  channel.basicNack(deliveryTag, false, false);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 创建订单,防止mq消息重复消费,造成同一个用户多单(com.seckill.demo.v2pro.service.impl.CouponOrderV2ProServiceImpl.java)

使用分布式锁 + 数据库唯一索引,防止消息重复消费导致一人多单

// 分布式锁,防止同一用户并发创建订单
String lockKey = LOCK_KEY_PREFIX + userId + ":" + couponId;
RLock lock = redissonClient.getLock(lockKey);

try {
    // 尝试获取锁,等待3秒,锁自动释放时间10秒
    boolean acquired = lock.tryLock(3, 10, java.util.concurrent.TimeUnit.SECONDS);
    if (!acquired) {
        log.warn("获取分布式锁失败,用户可能正在处理中, userId={}, couponId={}", userId, couponId);
        throw new SeckillException(3004, "请勿重复提交");
    }

    // 使用编程式事务,避免自调用导致事务失效
    Boolean result = transactionTemplate.execute(status -> {
        try {
            return doCreateOrder(userId, couponId, orderId);
        } catch (SeckillException e) {
            status.setRollbackOnly();
            throw e;
        } catch (Exception e) {
            status.setRollbackOnly();
            throw new RuntimeException(e);
        }
    });
    return Boolean.TRUE.equals(result);
} catch (Exception e) {
    log.error("获取分布式锁被中断, userId={}, couponId={}", userId, couponId, e);
    throw new SeckillException(3005, "系统繁忙,请稍后重试");
} finally {
    // 只有当前线程持有锁时才释放
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2.3.3 核心流程

客户端请求
    ↓
[Controller] 接收请求,参数校验
    ↓
[SeckillV2ProService]
    ① Redis限流校验
    ↓
    ② 查询优惠券信息(Redis缓存 + DB回源 + 分布式锁防击穿)
    ③ 校验活动时间(begin_time ~ end_time)
    ④ 校验活动状态(status == 1)
    ⑤ 执行Lua脚本,预减库存并记录用户抢购操作
    ↓
    ⑥ 发送秒杀消息到RabbitMQ,异步创建订单
    ↓
[SeckillMessageListener]
    ⑦ 消费秒杀消息,创建订单(数据库事务 + 分布式锁防一人多单)
    ↓
秒杀成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.3.4 并发安全保障

使用消息队列异步处理订单创建,进一步提升系统的吞吐量和稳定性,同时结合分布式锁和数据库唯一索引,防止一人多单问题。

# 2.3.5 方案分析

  • 优点:
    • 极大提升系统吞吐量,支持超高并发请求
    • 减轻数据库压力,提升系统稳定性
    • 优化用户体验,减少请求响应时间
  • 缺点:
    • 系统复杂度增加,需维护消息队列组件
    • 需要处理消息重复消费和消息丢失等问题

# 2.3.6 后续优化方向

  • 引入本地缓存(如Caffeine)结合Redis,减轻Redis读压力

# 2.4 v2Pro Max版本:分布式+异步+本地缓存版(狂霸酷炫吊炸天版)

# 2.4.1 设计目标

在V2 Pro版本中,虽然通过引入消息队列异步处理订单创建操作,极大提升了系统的吞吐量和稳定性,但在极端高并发场景下,Redis作为分布式缓存仍然可能成为瓶颈。 为了进一步提升系统的性能和稳定性,我们将在V2 Pro Max版本中引入本地缓存(如Caffeine)结合Redis,减轻Redis的读压力。核心思路包括:

  • 使用本地缓存(Caffeine)缓存热点数据,减少对Redis的访问频率
  • 结合Redis和本地缓存,实现多级缓存架构,提升数据访问速度

适用场景:

  • 超级大型秒杀活动,用户量和请求量极高, qps达到恐怖级别

为什么使用本地缓存?

HashMap 或 ConcurrentHashMap 作为本地缓存,虽然简单易用,但在高并发场景下存在以下问题:

  • 内存泄漏风险:缓存数据无限制增长,可能导致内存溢出
  • 缓存过期策略缺失:无法自动清理过期数据,影响缓存命中率

# 2.4.2 核心代码

  1. 配置Caffeine本地缓存(com.seckill.demo.config.CaffeineConfig.java)
// 配置库存售罄本地缓存
@Bean
public Cache<Long, Boolean> stockSoldOutCache() {
    return Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build();
}
1
2
3
4
5
6
7
8
  1. 使用本地缓存结合Redis(com.seckill.demo.v2promax.service.impl.SeckillV2ProMaxServiceImpl.java)
// 1. 本地缓存检查库存是否售罄
if(Boolean.TRUE.equals(stockSoldOutCache.getIfPresent(couponId))) {;
    log.info("本地缓存命中,优惠券已售罄, couponId={}", couponId);
    throw new SeckillException(2006, "优惠券已抢完");
}

// 库存售完后,标记本地缓存
stockSoldOutCache.put(couponId, Boolean.TRUE);
1
2
3
4
5
6
7
8

潜在问题:Caffeine基于JVM,无法跨节点共享数据:

优化方案:使用Redis Pub/Sub机制,广播库存售罄消息,更新各节点本地缓存

配置Redis消息监听器(com.seckill.demo.config.RedisPubSubConfig.java)

// 配置Redis消息处理内部类
@Slf4j
@AllArgsConstructor
public static class StockSoldOutMessageHandler {

    private Cache<Long, Boolean> stockSoldOutCache;

    /**
     * 处理库存售罄消息
     * 消息格式: "couponId:action"
     *   - "123:soldout" 表示 couponId=123 已售罄
     *   - "123:reset" 表示 couponId=123 库存已恢复
     */
    public void handleMessage(String message) {
        try {
            log.debug("收到库存状态同步消息: {}", message);

            String[] parts = message.split(":");
            if (parts.length != 2) {
                log.warn("无效的消息格式: {}", message);
                return;
            }

            Long couponId = Long.parseLong(parts[0]);
            String action = parts[1];

            switch (action) {
                case "soldout" -> {
                    stockSoldOutCache.put(couponId, Boolean.TRUE);
                    log.info("本地缓存已更新: couponId={} 标记为售罄", couponId);
                }
                case "reset" -> {
                    stockSoldOutCache.invalidate(couponId);
                    log.info("本地缓存已更新: couponId={} 售罄标记已清除", couponId);
                }
            }
        } catch (NumberFormatException e) {
            log.error("解析消息失败, message={}", message, e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

发布库存售罄消息(com.seckill.demo.v2promax.service.impl.SeckillV2ProMaxServiceImpl.java)

// 库存售完后,广播库存售罄消息,更新各节点本地缓存
cacheManager.markSoldOut(couponId);

/**
 * 标记库存已售罄,并广播到其他节点
 * @param couponId
 */
public void markSoldOut(Long couponId) {
    // 1. 更新本地缓存
    stockSoldOutCache.put(couponId, Boolean.TRUE);
    log.info("本地缓存标记库存售罄, couponId={}", couponId);

    // 2. 发布消息到 Redis,同步到其他节点
    // 2.1 Redis Pub/Sub 方式只允许发送字符串消息
    String msg = couponId + ":soldout";
    redisTemplate.convertAndSend(CacheKeyConstants.STOCK_SOLD_OUT_CHANNEL.getKey(),  msg);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 2.4.3 核心流程

在V2Pro的基础上引入本地缓存,防止Redis读压力过大

# 2.4.4 方案分析

  • 优点:
    • 减轻Redis读压力,提升数据访问速度
  • 缺点:
    • 系统复杂度进一步增加
    • 需要处理本地缓存与Redis缓存的一致性问题

# 2.4.5 后续优化方向

拔了一把头发,想也想不出来,那就未完待续...

# 三、总结

勿好高骛远,请勿在QPS不高的场景下盲目追求复杂架构优化,合理评估系统需求和复杂度,选择合适的解决方案,才能真正提升系统性能和用户体验。

技术的迭代是为了更好的解决问题,而非炫技,请务必牢记这一点,与君共勉!

编辑 (opens new window)
#高并发秒杀优惠卷
上次更新: 2026/01/21, 19:29:14
Redis延时双删
AOP无侵入式告警

← Redis延时双删 AOP无侵入式告警→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式