避免超卖
避免商品超卖是电商系统,尤其是秒杀场景下的核心挑战。下面我将详细说明几种具体的技术实现方案,从基础到高级层层递进。
1. 数据库层面解决方案
1.1 悲观锁方案
-- 使用 SELECT ... FOR UPDATE 锁定记录
BEGIN TRANSACTION;
SELECT stock FROM products WHERE id = ? FOR UPDATE;
-- 检查库存并扣减
UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0;
COMMIT;
缺点:性能较差,在高并发下容易成为瓶颈。
1.2 乐观锁方案
-- 基于版本号的乐观锁
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = ? AND stock > 0 AND version = ?;
-- 或者直接基于库存值的乐观锁
UPDATE products
SET stock = stock - 1
WHERE id = ? AND stock = current_stock AND stock > 0;
优点:性能较好,适合读多写少的场景。
1.3 直接库存扣减
-- 最简单的库存扣减,利用数据库的行级锁
UPDATE products
SET stock = stock - 1
WHERE id = ? AND stock > 0;
通过返回值判断是否成功:
int affectedRows = productMapper.decreaseStock(productId, quantity);
if (affectedRows > 0) {
// 扣减成功,创建订单
} else {
// 库存不足
}
2. Redis 原子操作方案
2.1 基于 Lua 脚本的原子操作
-- decrease_stock.lua
local key = KEYS[1]
local decrease = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key))
if current == nil then
return -1 -- 商品不存在
end
if current < decrease then
return -2 -- 库存不足
end
redis.call('DECRBY', key, decrease)
return current - decrease -- 返回剩余库存
Java 调用示例:
public boolean decreaseStock(String productId, int quantity) {
String script = "local key = KEYS[1] local decrease = tonumber(ARGV[1]) " +
"local current = tonumber(redis.call('GET', key)) " +
"if current == nil then return -1 end " +
"if current < decrease then return -2 end " +
"redis.call('DECRBY', key, decrease) return current - decrease";
Long result = jedis.eval(script,
Collections.singletonList("stock:" + productId),
Collections.singletonList(String.valueOf(quantity)));
return result >= 0;
}
2.2 Redis 事务方案
public boolean decreaseStockWithWatch(String productId, int quantity) {
String stockKey = "stock:" + productId;
try (Jedis jedis = jedisPool.getResource()) {
// 监视库存key
jedis.watch(stockKey);
int currentStock = Integer.parseInt(jedis.get(stockKey));
if (currentStock < quantity) {
jedis.unwatch();
return false;
}
// 开始事务
Transaction transaction = jedis.multi();
transaction.decrBy(stockKey, quantity);
// 执行事务
List<Object> results = transaction.exec();
// 如果事务执行失败(被其他客户端修改),results为null
return results != null && !results.isEmpty();
}
}
3. 分布式锁方案
3.1 Redisson 分布式锁
@Component
public class InventoryService {
@Autowired
private RedissonClient redisson;
public boolean decreaseStock(String productId, int quantity) {
String lockKey = "lock:stock:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
// 尝试获取锁,等待时间5秒,锁过期时间10秒
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 执行库存扣减
int currentStock = getCurrentStock(productId);
if (currentStock >= quantity) {
updateStock(productId, currentStock - quantity);
return true;
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
4. 令牌桶 + Redis 方案(推荐)
4.1 预扣库存方案
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 初始化秒杀库存
*/
public void initSeckillStock(String productId, int totalStock) {
String stockKey = "seckill:stock:" + productId;
String soldKey = "seckill:sold:" + productId;
redisTemplate.opsForValue().set(stockKey, totalStock);
redisTemplate.opsForValue().set(soldKey, 0);
}
/**
* 尝试预扣库存
*/
public boolean tryPreDeductStock(String productId, String userId) {
String stockKey = "seckill:stock:" + productId;
String soldKey = "seckill:sold:" + productId;
String userOrderKey = "seckill:user_order:" + productId + ":" + userId;
// 使用Lua脚本保证原子性
String luaScript =
"local stockKey = KEYS[1] " +
"local soldKey = KEYS[2] " +
"local userOrderKey = KEYS[3] " +
"local userId = ARGV[1] " +
" " +
"-- 检查用户是否已经下单 " +
"if redis.call('EXISTS', userOrderKey) == 1 then " +
" return -1 " +
"end " +
" " +
"-- 检查库存 " +
"local stock = tonumber(redis.call('GET', stockKey)) " +
"if stock <= 0 then " +
" return -2 " +
"end " +
" " +
"-- 扣减库存 " +
"redis.call('DECR', stockKey) " +
"redis.call('INCR', soldKey) " +
" " +
"-- 标记用户已下单,设置过期时间(如15分钟) " +
"redis.call('SETEX', userOrderKey, 900, '1') " +
" " +
"return 1";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Arrays.asList(stockKey, soldKey, userOrderKey),
userId
);
return result == 1;
}
/**
* 确认扣减库存(创建订单后)
*/
public boolean confirmDeductStock(String productId, String userId) {
// 将预扣库存正式扣减,写入数据库
// 这里可以异步处理,保证最终一致性
return true;
}
/**
* 回滚预扣库存
*/
public boolean rollbackPreDeductStock(String productId, String userId) {
String stockKey = "seckill:stock:" + productId;
String soldKey = "seckill:sold:" + productId;
String userOrderKey = "seckill:user_order:" + productId + ":" + userId;
String luaScript =
"local stockKey = KEYS[1] " +
"local soldKey = KEYS[2] " +
"local userOrderKey = KEYS[3] " +
" " +
"-- 检查用户是否有预扣记录 " +
"if redis.call('EXISTS', userOrderKey) == 0 then " +
" return -1 " +
"end " +
" " +
"-- 恢复库存 " +
"redis.call('INCR', stockKey) " +
"redis.call('DECR', soldKey) " +
"redis.call('DEL', userOrderKey) " +
" " +
"return 1";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Arrays.asList(stockKey, soldKey, userOrderKey)
);
return result == 1;
}
}
5. 消息队列异步方案
5.1 基于 RocketMQ 的事务消息
@Service
public class SeckillOrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送秒杀请求
*/
public boolean sendSeckillRequest(String productId, String userId) {
SeckillMessage message = new SeckillMessage(productId, userId);
// 发送事务消息
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"seckill-topic",
MessageBuilder.withPayload(message).build(),
null
);
return result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE;
}
/**
* 事务消息监听器 - 执行本地事务
*/
@RocketMQTransactionListener
class SeckillTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
SeckillMessage message = (SeckillMessage) ((GenericMessage) msg).getPayload();
// 预扣库存
boolean success = tryPreDeductStock(message.getProductId(), message.getUserId());
if (success) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 检查本地事务状态
SeckillMessage message = (SeckillMessage) ((GenericMessage) msg).getPayload();
// 根据业务逻辑检查订单状态
return RocketMQLocalTransactionState.COMMIT;
}
}
}
6. 完整架构方案
在实际生产环境中,通常采用分层校验 + 多种技术组合的方案:
用户请求 → 限流层 → 缓存校验层 → 队列层 → 服务层 → 数据库层
6.1 分层防护架构
@Component
public class SeckillProtectionLayer {
/**
* 分层校验秒杀请求
*/
public boolean validateSeckillRequest(String productId, String userId) {
// 第一层:基础校验
if (!validateBasicInfo(productId, userId)) {
return false;
}
// 第二层:频率限制
if (!rateLimit(userId, productId)) {
return false;
}
// 第三层:库存预检(快速失败)
if (!preCheckStock(productId)) {
return false;
}
// 第四层:Redis原子扣减
if (!tryPreDeductStock(productId, userId)) {
return false;
}
// 第五层:进入消息队列异步处理
return sendToQueue(productId, userId);
}
private boolean preCheckStock(String productId) {
// 快速库存检查,不保证强一致性
Integer stock = redisTemplate.opsForValue().get("seckill:stock:" + productId);
return stock != null && stock > 0;
}
private boolean rateLimit(String userId, String productId) {
String rateLimitKey = "rate_limit:" + productId + ":" + userId;
Long count = redisTemplate.opsForValue().increment(rateLimitKey, 1);
if (count == 1) {
redisTemplate.expire(rateLimitKey, 1, TimeUnit.SECONDS); // 1秒窗口
}
return count <= 5; // 每秒最多5次请求
}
}
总结
避免超卖的核心技术要点:
- 原子性操作:使用数据库行锁、Redis Lua脚本等保证操作的原子性
- 预扣库存:在Redis中预扣减,异步同步到数据库
- 分层校验:多级防护,快速失败
- 流量削峰:使用消息队列缓冲瞬时高并发
- 最终一致性:通过异步方式保证数据的最终一致性
在实际项目中,推荐使用 Redis Lua脚本 + 消息队列 的组合方案,既保证了性能,又确保了数据的一致性。
浙公网安备 33010602011771号