从零开始实现秒杀系统(二):Redis优化篇

从零开始实现秒杀系统(二):Redis优化篇

引言

上一篇文章中,我们探讨了基于MySQL实现秒杀系统的几种方案,包括无锁实现、悲观锁、乐观锁以及最优的单SQL原子更新方案。这些方案都直接操作数据库,在高并发场景下仍然存在性能瓶颈。

本文作为系列的第二篇,将介绍如何利用Redis来优化秒杀系统,大幅提升系统的并发处理能力。在V2版本中,我们引入了Redis作为缓存和预减库存的工具,并结合异步处理技术,实现了一个更高性能的秒杀系统。

1. 高并发秒杀系统的挑战

回顾V1版本,我们虽然解决了超卖问题,但仍然面临以下挑战:

  1. 数据库压力大:所有请求都直接访问数据库,导致数据库成为瓶颈
  2. 系统吞吐量有限:MySQL的行锁机制虽然保证了数据一致性,但限制了并发处理能力
  3. 用户体验差:高并发下,大量请求等待数据库锁释放,导致响应时间长

V2版本就是为了解决这些问题而设计的, 于引入了以下关键技术:

  1. Redis预减库存:使用Redis原子操作减少数据库访问
  2. 库存快速失败检查:通过Redis标记售罄商品,避免无效请求
  3. 异步下单:将下单操作异步化,提高系统吞吐量
  4. 分布式锁:保证Redis缓存初始化的线程安全
  5. 内存队列:使用内存线程池处理订单创建,减轻数据库压力

这些创新点共同构成了一个高性能、高可用的秒杀系统。下面我们将详细介绍每个创新点的实现。

2. V1V2架构对比

V1架构(基于MySQL)

用户请求 -> 应用服务器 -> MySQL(减库存+创建订单)
image-20250307140357358

V2架构(Redis优化)

用户请求 -> 应用服务器 -> Redis预减库存 -> 快速返回结果给用户
                      -> 异步队列 -> MySQL(最终减库存+创建订单)
                      -> 用户查询结果 -> Redis查询状态

image-20250307140435122

3. Redis预减库存

在V1版本中,每个秒杀请求都需要访问数据库查询和更新库存,这是系统的主要瓶颈。V2版本引入了Redis预减库存机制,将库存信息缓存在Redis中,使用Redis的原子操作来处理库存扣减。

// Redis预减库存,减少对数据库的访问
Long stock = redisService.decr(SeckillKey.goodsStock, "" + goodsVo.getId());

// 判断库存是否充足
if (stock < 0) {
    // 库存不足,回滚Redis库存,设置商品已售罄标识
    redisService.incr(SeckillKey.goodsStock, "" + goodsVo.getId());
    setGoodsOver(goodsVo.getId());
    return null;
}

这种方式的优势在于:

  1. 高性能:Redis的内存操作速度远快于数据库磁盘操作
  2. 原子性:Redis的incr/decr操作是原子的,无需额外加锁
  3. 减轻数据库压力:只有通过Redis预减库存的请求才会访问数据库

3.1 库存初始化的优化

为了避免每次启动服务或处理请求时重复初始化Redis库存,V2版本使用了分布式锁来保证线程安全:

private void initStockIfNeeded(GoodsVo goodsVo) {
    Long goodsId = goodsVo.getId();
    
    // 检查库存是否已在Redis中初始化
    if (!redisService.exists(SeckillKey.goodsStock, "" + goodsId)) {
        // 创建分布式锁,设置过期时间和唯一标识符
        String lockKey = LOCK_PREFIX + goodsId;
        RedisDistributedLock lock = new RedisDistributedLock(redisService, lockKey, LOCK_EXPIRE_SECONDS);
        
        boolean lockAcquired = lock.tryLock(LOCK_TIMEOUT_MS);
        if (lockAcquired) {
            try {
                // 双重检查避免重复初始化
                if (!redisService.exists(SeckillKey.goodsStock, "" + goodsId)) {
                    log.info("Initializing stock for goods: {}", goodsId);
                    // 从数据库获取最新库存
                    GoodsVo freshGoodsInfo = goodsService.getGoodsVoByGoodsId(goodsId);
                    if (freshGoodsInfo != null) {
                        redisService.set(SeckillKey.goodsStock, "" + goodsId, freshGoodsInfo.getStockCount());
                        log.info("Stock initialized for goods {}: {}", goodsId, freshGoodsInfo.getStockCount());
                    }
                }
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    }
}

这段代码使用了双重检查锁(DCL)模式和Redis分布式锁来确保在多实例、多线程环境下,库存只被初始化一次,避免了缓存重复初始化的问题。

4. 库存快速失败检查

在高并发秒杀场景中,大部分请求都是在商品已售罄后到达的。为了避免这些请求无谓地消耗系统资源,V2版本引入了库存快速失败检查机制:

// 快速失败检查:商品是否已标记为售罄
if (isGoodsOver(goodsVo.getId())) {
    return null;
}

// 标记商品已售罄
private void setGoodsOver(Long goodsId) {
    redisService.set(SeckillKey.isGoodsOver, "" + goodsId, true);
}

// 判断商品是否售罄
private boolean isGoodsOver(Long goodsId) {
    return redisService.exists(SeckillKey.isGoodsOver, "" + goodsId);
}

这一优化使得系统能够快速拒绝无效请求,将宝贵的系统资源用于处理有效请求,极大地提高了系统的吞吐能力。

5. 异步下单处理

秒杀过程中最耗时的操作是创建订单和更新数据库,为了进一步提升系统性能,V2版本将下单操作异步化:

// 使用线程池异步处理订单创建
private static final ExecutorService orderExecutor = Executors.newFixedThreadPool(5000);

// 标记订单为处理中状态
redisService.set(SeckillKey.seckillPending, finalUserId + "_" + finalGoodsVo.getId(), System.currentTimeMillis());

// 异步下单
orderExecutor.submit(() -> {
    try {
        SeckillOrder order = createSeckillOrder(finalUserId, finalGoodsVo);
    } catch (Exception e) {
        log.error("Create order async error: ", e);
        // 出错时回滚Redis库存
        redisService.incr(SeckillKey.goodsStock, "" + finalGoodsVo.getId());
    } finally {
        // 清理处理中状态
        redisService.delete(SeckillKey.seckillPending, finalUserId + "_" + finalGoodsVo.getId());
    }
});

异步下单的优势:

  1. 提高响应速度:用户无需等待下单完成即可获得响应
  2. 控制数据库并发:通过线程池限制并发数量,避免数据库压力过大
  3. 失败自动回滚:异常情况下自动回滚Redis库存,保证数据一致性

6. 订单状态查询

由于订单处理是异步的,用户需要一种机制来查询秒杀结果。V2版本实现了一个专门的结果查询接口:

/**
 * 获取秒杀结果
 * @return orderId: 成功, -1: 秒杀失败, 0: 排队中, -2: 处理超时
 */
@Override
public Long getSeckillResult(Long userId, Long goodsId) {
    String pendingKey = userId + "_" + goodsId;

    // 1. 优先检查处理中状态
    Long startTime = redisService.get(SeckillKey.seckillPending, pendingKey);
    if (startTime != null) {
        long diff = System.currentTimeMillis() - startTime;
        if (diff > 30000) { // 超时30秒
            redisService.delete(SeckillKey.seckillPending, pendingKey);
            return -2L; // 处理超时
        }
        return 0L; // 正在处理
    }

    // 2. 查询订单信息
    SeckillOrder order = orderService.getOrderByUserIdGoodsId(userId, goodsId);
    if (order != null) {
        return order.getId(); // 秒杀成功
    }
    
    // 3. 查询商品是否已售罄
    boolean isOver = isGoodsOver(goodsId);
    if (isOver) {
        return -1L; // 已售罄,秒杀失败
    }
    
    return -1L; // 秒杀失败
}

这个接口设计考虑了多种状态:

  • 订单处理中(排队)
  • 处理超时
  • 秒杀成功
  • 秒杀失败(商品售罄)

这种设计极大地提升了用户体验,让用户能够及时了解秒杀结果。

7. V2版本的完整秒杀流程

结合上述优化,V2版本的秒杀流程如下:

  1. 判断商品是否售罄(快速失败)
  2. 初始化Redis库存(如果需要)
  3. Redis预减库存
  4. 异步创建订单
  5. 返回临时结果
  6. 用户查询秒杀结果

image-20250307140652054

这一流程不仅保证了系统的高性能,还确保了数据的一致性和良好的用户体验。

8. V1与V2性能对比

单个 MySQL 的每秒写入在 4000 QPS,读取如果记录超过千万级别效率会大大降低。而Redis单分片写入瓶颈在 2w 左右,读瓶颈在 10w 左右

性能指标V1(MySQL实现)V2(Redis优化)提升
QPS约500-1000约10,000-20,00020倍左右
响应时间平均200-500ms平均100ms以下10倍左右
并发连接数约1,000约50,00050倍
数据库压力所有请求直接访问数据库只有成功减库存的请求访问数据库显著降低
系统资源利用CPU和数据库IO高负荷内存使用率高,CPU和IO负载分散更均衡
可扩展性受限于数据库性能,难以水平扩展可通过增加Redis和应用节点水平扩展大幅提升

9. 潜在问题

尽管V2版本在性能上有了显著提升,但我们仍然面临一些挑战:

  1. 服务可靠性与一致性:内存线程池处理订单可能因服务重启或宕机导致任务丢失,同时异步处理增加了保证数据一致性的难度
  2. 单机瓶颈:线程池在单机内存中运行,难以实现真正的分布式横向扩展
  3. 峰值处理:在极端高并发下,内存队列可能迅速堆积,导致系统内存压力增大
  4. 监控与重试:缺乏完善的监控和失败任务重试机制
  5. 性能问题:在库存扣减和订单入库依旧是一个数据库事务处理的,库存扣减依旧是行锁导致系统性能不佳,而事务也加剧了性能的下降。如果秒杀场景持续进行会导致待处理的请求挤压

这些挑战将在系列的第三篇文章中通过引入RocketMQ等技术得到解决。

10. 总结

V2版本秒杀系统通过引入Redis预减库存、快速失败检查、异步下单等创新技术,成功解决了V1版本中的性能瓶颈,实现了一个高性能、高可靠的秒杀系统。

Redis作为高性能内存数据库,与MySQL行锁机制相比,具有显著的性能优势。通过将热点数据(库存)放在Redis中,系统能够承受更高的并发压力,同时保证数据的一致性。

在系列的下一篇文章《从零开始实现秒杀系统(三):RocketMQ消息队列篇》中,我们将引入RocketMQ消息队列,进一步优化秒杀系统,解决V2版本中存在的剩余问题


本文源码已开源在GitHub:Goinggoinggoing/seckill

如有疑问或建议,欢迎在评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值