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

GPT摘要

这篇文章介绍了如何利用Redis优化秒杀系统,解决基于MySQL方案的性能瓶颈。作者针对V1版本中数据库压力大、系统吞吐量有限和用户体验差的问题,提出了V2版本的改进方案,主要包含以下关键技术: 1. Redis预减库存:使用Redis原子操作减少数据库访问,将库存信息缓存在Redis中处理扣减,显著提高性能。 2. 库存快速失败检查:通过Redis标记售罄商品,快速拒绝无效请求,避免无谓资源消耗。 3. 异步下单:将耗时的订单创建操作异步化,提高系统响应速度,同时通过线程池控制数据库并发压力。 4. 分布式锁:采用双重检查锁模式保证Redis缓存初始化的线程安全,避免重复初始化。 5. 内存队列:使用内存线程池处理订单创建,减轻数据库压力。 文章详细比较了V1和V2版本的架构差异,V1直接操作MySQL数据库,而V2引入Redis作为缓存层,结合异步处理技术大幅提升性能。测试数据显示V2版本在QPS、响应时间和并发处理能力等方面均有显著提升,特别是QPS从约500-1000提升到10,000-20,000。 文章最后指出V2版本仍面临的挑战,包括服务可靠性、单机瓶颈、峰值处理等问题,并预告将在下一篇文章中通过引入RocketMQ消息队列进一步优化系统。整篇文章通过具体代码示例和架构图,清晰展示了如何构建一个高性能的秒杀系统。

从零开始实现秒杀系统(二):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)

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

V2架构(Redis优化)

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

image-20250307140435122

3. Redis预减库存

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

1
2
3
4
5
6
7
8
9
10
// 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版本使用了分布式锁来保证线程安全:

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
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版本引入了库存快速失败检查机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 快速失败检查:商品是否已标记为售罄
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版本将下单操作异步化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用线程池异步处理订单创建
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版本实现了一个专门的结果查询接口:

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
/**
* 获取秒杀结果
* @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,000 20倍左右
响应时间 平均200-500ms 平均100ms以下 10倍左右
并发连接数 约1,000 约50,000 50倍
数据库压力 所有请求直接访问数据库 只有成功减库存的请求访问数据库 显著降低
系统资源利用 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

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