电商场景分析
# 电商场景分析
# 秒杀系统设计要点
# 场景分析
秒杀是电商系统中典型的高并发场景,核心特征:
- 瞬时高并发:活动开始瞬间流量激增,可能是平时的数十倍甚至数百倍
- 库存有限:商品数量远小于参与用户数
- 时间窗口短:通常在几秒到几分钟内完成
- 防刷防黄牛:需要识别并拦截恶意请求
# 核心挑战
| 挑战 | 描述 | 危害 |
|---|---|---|
| 超卖问题 | 多个请求同时扣减同一库存,导致卖出数量超过实际库存 | 商家亏损、客诉、信誉受损 |
| 高并发压力 | 瞬时大量请求直接打到数据库 | 数据库宕机、服务雪崩 |
| 接口防刷 | 黄牛通过脚本恶意抢购 | 普通用户无法参与、公平性丧失 |
| 一人一单 | 同一用户重复下单 | 库存被少数人占用 |
# 架构设计思路
秒杀系统的核心原则:层层拦截,将请求逐级递减,最终只有少量请求到达数据库。
- 前端层:按钮防重复提交、页面静态化、CDN 加速
- 网关层:限流(令牌桶 / 漏桶算法)、IP 黑名单、验证码
- 服务层:
- 使用 Redis 预扣库存,利用 Lua 脚本保证原子性
- 使用 Redis 分布式锁保证一人一单
- 异步下单:通过消息队列(如 Redis Stream / RabbitMQ)削峰填谷
- 数据库层:乐观锁(version 字段)兜底防超卖
// Lua 脚本:原子性判断库存 + 一人一单
String luaScript =
"if redis.call('get', KEYS[1]) <= 0 then return -1 end " + // 库存不足
"if redis.call('sismember', KEYS[2], ARGV[1]) == 1 then return -2 end " + // 重复下单
"redis.call('decr', KEYS[1]) " + // 扣减库存
"redis.call('sadd', KEYS[2], ARGV[1]) " + // 记录用户
"return 1";
1
2
3
4
5
6
7
2
3
4
5
6
7
# 缓存策略分析
# 缓存三大问题对比
| 问题 | 描述 | 原因 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询一个不存在的数据,缓存和数据库都没有 | 恶意攻击、业务数据不存在 | 缓存空值(设短 TTL);布隆过滤器拦截 |
| 缓存雪崩 | 大量缓存同时失效,请求全部打到数据库 | 大量 Key 同时过期、Redis 宕机 | 给 TTL 加随机值;Redis 集群高可用;多级缓存 |
| 缓存击穿 | 热点 Key 过期瞬间,大量并发请求打到数据库 | 热点数据过期、高并发访问 | 互斥锁重建缓存;逻辑过期(不设 TTL,后台异步更新) |
# 解决方案选择建议
黑马点评项目中,
CacheClient工具类封装了缓存穿透和缓存击穿的通用解决方案。
| 场景 | 推荐方案 | 适用条件 |
|---|---|---|
| 防缓存穿透 | 缓存空值 | 数据量小、空结果比例低 |
| 防缓存穿透 | 布隆过滤器 | 数据量大、恶意查询多 |
| 防缓存雪崩 | TTL 加随机值 | 简单有效,适用于大多数场景 |
| 防缓存击穿 | 互斥锁 | 对一致性要求高,可接受短暂等待 |
| 防缓存击穿 | 逻辑过期 | 对可用性要求高,可接受短暂不一致 |
# 分布式锁方案对比
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| SETNX | SET key value NX PX timeout | 实现简单、性能高 | 不可重入、无法自动续期、锁误删风险 | 简单场景、学习理解 |
| Redisson | 基于 Redis + Hash 结构 + 看门狗机制 | 可重入、自动续期、支持公平锁 / 读写锁 | 依赖单节点 Redis,存在主从切换丢锁风险 | 生产环境首选 |
| RedLock | 向多个独立 Redis 节点同时加锁,多数成功才算成功 | 解决单点故障问题 | 实现复杂、有性能开销、存在争议 | 对可靠性要求极高的场景 |
黑马点评项目中,
SimpleRedisLock是 SETNX 的简单实现(了解即可),生产环境推荐使用 Redisson。
# SETNX 基本实现
// 简单的 SETNX 分布式锁
String lockKey = "lock:order:" + userId;
String requestId = UUID.randomUUID().toString();
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (BooleanUtil.isFalse(locked)) {
return; // 获取锁失败
}
try {
// 执行业务逻辑
} finally {
// 释放锁:Lua 脚本保证原子性(判断 + 删除)
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";
stringRedisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey), requestId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Feed 流方案对比
Feed 流是社交场景中"关注的人发布了新内容,我需要看到"的核心机制。
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 拉模式 | 用户读时实时拉取关注者的最新内容 | 节省存储空间、数据实时 | 读放大、关注者多时查询慢 | 关注者少、实时性要求高 |
| 推模式 | 作者发布时主动推送到所有粉丝的收件箱 | 读性能高、延迟低 | 写放大、大 V 发布时推送量大 | 粉丝量少、读频繁 |
| 推拉结合 | 普通用户推模式,大 V 拉模式 | 兼顾读写性能 | 实现复杂 | 综合场景(微博等) |
黑马点评项目中,
BlogServiceImpl基于 Redis 的 Sorted Set 结构实现了 Feed 流,使用滚动分页(ZREVRANGEBYSCORE)来获取关注者的最新动态。
# 推模式核心流程
用户发布动态
↓
写入数据库 + 写入作者收件箱(Redis Sorted Set,score = 时间戳)
↓
系统查询所有粉丝 → 逐个推送到粉丝收件箱
↓
粉丝查看 Feed 流 → 从自身收件箱滚动分页读取
1
2
3
4
5
6
7
2
3
4
5
6
7
# 学习参考
上次更新: 2026/10/10 17:43:40