一、超卖问题的本质:高并发下的原子性缺失
在秒杀场景中,超卖(库存扣减至负数)和超买(用户重复购买)的核心问题源于非原子性操作。例如,当多个线程同时查询库存并执行扣减时,传统数据库的“查询-判断-扣减”三步操作可能被并发请求打乱,导致库存逻辑错误(如剩余库存为1时被多个线程同时判定为可购买)。
案例模拟:某电商平台推出100件商品秒杀,瞬间涌入1万请求。假设库存存储于MySQL,若未加锁,多个线程可能同时查询到剩余库存为1,均执行扣减,最终库存变为-99,导致超卖。
二、Redis分布式锁:秒杀系统的核心防线
1. 分布式锁的核心要求
- 互斥性:同一时刻仅一个线程持有锁。
- 防死锁:锁需设置超时时间,避免线程崩溃导致锁永久占用。
- 原子性:锁的获取和释放必须是原子操作(如Lua脚本实现)。
2. Redis实现分布式锁的三种方案
方案一:SETNX + EXPIRE(基础版)
// 加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:product_001", "uuid", 10, TimeUnit.SECONDS);
if (lock) {
try {
// 业务逻辑(扣减库存)
} finally {
// 释放锁时需校验UUID,防止误删其他线程的锁
if ("uuid".equals(redisTemplate.opsForValue().get("lock:product_001"))) {
redisTemplate.delete("lock:product_001");
}
}
}
缺陷:SETNX和EXPIRE非原子操作,可能因线程崩溃导致死锁。
方案二:Lua脚本(原子操作版)
-- 扣减库存的Lua脚本
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
end
通过Redis单线程特性,保证操作的原子性,避免超卖。
方案三:RedLock(高可用版)
在Redis主从架构下,为避免主节点宕机导致锁失效,使用RedLock算法(需部署多个独立Redis实例):
// Redisson实现RedLock
RLock lock1 = redisson.getLock("lock:product_001");
RLock lock2 = redisson.getLock("lock:product_001");
RLock lock3 = redisson.getLock("lock:product_001");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();
try {
// 扣减库存
} finally {
redLock.unlock();
}
RedLock通过半数以上节点加锁成功来保证高可用性。
三、实战:Redis秒杀系统设计(吞吐量提升10倍的关键)
1. 架构设计
- 流量分层:前端用Nginx+Lua限流,拦截90%无效请求。
- 库存预热:活动开始前将库存加载至Redis(如stock:product_001=100)。
- 异步下单:扣减库存后发送MQ消息,由消费者异步处理订单和数据库更新,缓解数据库压力。
2. 核心代码实现(Spring Boot + RedisTemplate)
@RestController
public class SeckillController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/seckill")
public String seckill(@RequestParam String productId) {
String stockKey = "stock:" + productId;
String lockKey = "lock:" + productId;
String uuid = UUID.randomUUID().toString();
// 获取分布式锁(设置10秒超时)
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS);
if (locked != null && locked) {
try {
// Lua脚本原子扣减库存
String script = "local stock = tonumber(redis.call('GET', KEYS[1])) " +
"if stock > 0 then redis.call('DECR', KEYS[1]) return 1 else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(stockKey)
);
if (result == 1) {
// 发送MQ消息,异步创建订单
return "秒杀成功!";
} else {
return "库存不足!";
}
} finally {
// 释放锁时校验UUID
if (uuid.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
return "系统繁忙,请重试!";
}
}
四、性能优化:从千级到万级并发的跨越
1. 分段锁:提升并发粒度
将库存拆分为多个Key(如stock:product_001_1到stock:product_001_10),每个Key对应10件库存。线程随机选择分段扣减,并发能力提升10倍。
2. 主从架构下的锁失效问题
Redis主从同步存在延迟,若主节点宕机,可能导致锁丢失。解决方案:
- 使用RedLock(需部署多实例)。
- ZooKeeper替代(CP模型,但性能较低)。
3. 熔断降级
通过Sentinel或Hystrix监控系统负载,当库存耗尽或Redis异常时,直接返回“已售罄”,避免无效请求冲击。
五、总结:秒杀系统的核心公式
高性能秒杀 = 流量分层 + 原子操作 + 异步处理 + 熔断降级
通过Redis分布式锁和Lua脚本,结合分段锁与RedLock,可实现万级TPS的秒杀系统。实际落地中需根据业务规模选择方案:小规模场景用Lua脚本+单Redis,大规模高可用场景用RedLock+分段锁。
实战建议:在阿里云等生产环境中,建议使用Tair(阿里云版Redis)或Redisson封装分布式锁,避免重复造轮子。若追求极致性能,可结合本地缓存(如Guava)减少Redis访问频率。