使用Redis的原子操作decr命令实现库存自减,代码如下:
if (redisClient.decr("stock:" + productId) < 0) {
redisClient.incr("stock:" + productId); // 回滚
return false;
}
return true;
高并发下用Lua脚本保证原子性:
local stock = redis.call('get', KEYS[1])
if tonumber(stock) > 0 then
redis.call('decr', KEYS[1])
return 1
else
return 0
end
方案一:Lua脚本原子扣减
在高并发场景下,为了保证库存扣减的原子性,我们可以使用Redis的Lua脚本。Lua脚本在Redis中是原子执行的,可以避免并发问题。
-- Lua脚本内容
local stock_key = KEYS[1]
local num = tonumber(ARGV[1])
local stock = tonumber(redis.call('get', stock_key) or 0)
if stock >= num then
redis.call('decrby', stock_key, num)
return 1
end
return 0
分布式锁保障一致性
使用Redisson分布式锁:
RLock lock = redisson.getLock("lock:" + productId);
try {
lock.lock(10, TimeUnit.SECONDS);
Long stock = redisTemplate.opsForValue().decrement("stock:" + productId);
if (stock != null && stock >= 0) {
// 下单成功
} else {
redisTemplate.opsForValue().increment("stock:" + productId);
}
} finally {
lock.unlock();
}
方案二:乐观锁 + WATCH
Redis的WATCH命令实现乐观锁:
MULTI
WATCH stock:123
DECR stock:123
EXEC
在高并发下,如果EXEC失败(因为WATCH的key被修改),则重试。这样保证了扣减的原子性和一致性。
超卖解决方案
为了防止超卖,除了原子扣减,还需要结合数据库事务:
1. Redis预减库存
2. 数据库扣减库存 + 记录订单
3. 如果Redis扣减失败,回滚数据库
4. 异步补偿:定时任务检查Redis和DB库存差异,进行修正。
真实案例代码
完整的SpringBoot实现:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean deductStock(String productId, int quantity) {
String key = "stock:" + productId;
Long result = redisTemplate.opsForValue().decrement(key, quantity);
if (result == null || result < 0) {
if (result != null) {
redisTemplate.opsForValue().increment(key, quantity);
}
return false;
}
return true;
}
性能优化
高并发场景下,单机Redis可能成为瓶颈,建议使用Redis Cluster集群,并设置合理的过期时间,避免内存爆炸。结合MQ异步化非核心扣减逻辑。
FAQ
Q: Redis dectr扣减后小于0怎么办?
A: 立即incr回滚,并返回扣减失败。
Q: Lua脚本为什么比分布式锁快?
A: Lua脚本单线程原子执行,无网络往返,性能更高。
Q: 怎么处理Redis宕机?
A: 双写Redis和DB,Redis只做缓存,DB做最终一致性。
Q: 秒杀场景怎么防刷?
A: 加验证码、限流、黑名单,用Lua脚本判断用户是否重复下单。