在电商系统中,确保库存扣减的原子性是防止超卖的核心问题。以下是基于不同技术方案的加锁实现方案及对比分析:
一、Redis 原子操作方案
1. Redis 事务 + WATCH 命令
-
原理 :通过
WATCH监控库存键,若键被修改则事务回滚。 -
代码示例:
// 伪代码示例 String stockKey = "stock:product_1001"; redisTemplate.watch(stockKey); int currentStock = Integer.parseInt(redisTemplate.opsForValue().get(stockKey)); if (currentStock >= quantity) { redisTemplate.multi(); redisTemplate.opsForValue().decrement(stockKey, quantity); redisTemplate.exec(); } -
优点:简单易用,无需引入额外依赖。
-
缺点 :
WATCH机制在高并发下可能频繁触发重试,性能受限。
2. Lua 脚本原子性操作
-
原理:将库存查询和扣减逻辑封装为 Lua 脚本,在 Redis 单线程中执行。
-
脚本示例:
-- Lua 脚本(扣减库存) local key = KEYS[1] local quantity = tonumber(ARGV[1]) local current = tonumber(redis.call('GET', key)) if current >= quantity then redis.call('DECRBY', key, quantity) return current - quantity else return -1 end -
Java 调用:
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(stockKey), quantity); -
优点:完全原子性,性能高(单次网络请求)。
-
缺点:脚本复杂度高时需优化编译和网络开销。
二、分布式锁方案
1. Redisson 分布式锁
-
原理:通过 Redisson 客户端实现可重入锁,自动续期防止死锁。
-
代码示例:
RLock lock = redissonClient.getLock("lock:product_1001"); try { if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { // 执行库存扣减 int currentStock = inventoryMapper.selectStock(productId); if (currentStock >= quantity) { inventoryMapper.updateStock(productId, quantity); } } } finally { lock.unlock(); } -
优点:支持自动续期、可重入锁,适合复杂业务逻辑。
-
缺点:依赖 Redis 稳定性,网络延迟可能影响锁释放。
2. Redis + SETNX 自定义锁
-
原理 :通过
SETNX命令实现分布式锁,结合过期时间防死锁。 -
代码示例:
String lockKey = "lock:product_1001"; String requestId = UUID.randomUUID().toString(); Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS); if (success != null && success) { try { // 执行库存扣减 } finally { // 仅持有锁的客户端可删除 if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } } -
优点:轻量级,灵活性高。
-
缺点:需手动处理锁续期和释放逻辑。
三、数据库锁方案
1. 悲观锁(SELECT ... FOR UPDATE)
-
原理:通过数据库行锁保证串行化操作。
-
SQL 示例:
BEGIN; SELECT stock FROM products WHERE id = 1001 FOR UPDATE; UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock > 0; COMMIT; -
优点:强一致性,直接保障数据正确性。
-
缺点:高并发下数据库性能瓶颈明显。
2. 乐观锁(版本号机制)
-
原理:通过版本号控制并发更新。
-
SQL 示例:
UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = current_version; -
优点:减少锁竞争,适合读多写少场景。
-
缺点:需处理更新失败的重试逻辑。
四、混合方案(Redis + 数据库)
1. 预扣库存 + 异步持久化
-
流程:
-
Redis 预扣:通过 Lua 脚本快速扣减 Redis 库存。
-
异步落库:通过 MQ 异步更新数据库,若失败则回滚 Redis。
-
-
优点:高吞吐量,数据库压力小。
-
缺点:需实现最终一致性逻辑。
五、方案对比与选型建议
| 方案 | 适用场景 | 性能 | 一致性 | 实现复杂度 |
|---|---|---|---|---|
| Redis Lua 脚本 | 高并发、低延迟扣减 | ★★★★★ | 强 | 中 |
| Redisson 分布式锁 | 需业务逻辑串行化的复杂场景 | ★★★★☆ | 强 | 高 |
| 数据库悲观锁 | 数据强一致性要求的核心交易 | ★★☆☆☆ | 强 | 低 |
| 数据库乐观锁 | 读多写少、可接受少量重试的场景 | ★★★☆☆ | 最终一致 | 中 |
六、最佳实践建议
-
核心库存扣减 :优先使用 Redis Lua 脚本,兼顾原子性与性能。
-
复杂业务逻辑 :结合 Redisson 分布式锁,确保操作串行化。
-
最终一致性 :采用 预扣库存 + MQ 异步落库,提升系统吞吐量。
-
降级策略:Redis 故障时切换至数据库乐观锁,保障基础功能可用。
通过合理选择方案,可有效解决库存超卖问题,同时平衡性能与一致性需求。