并发安全问题
问题
高并发场景下出现超卖、重复扣款、并发数据覆盖等问题,如何解决?
答案
典型并发安全问题
| 问题 | 场景 | 后果 |
|---|---|---|
| 超卖 | 秒杀/抢购 | 库存变负数 |
| 重复扣款 | 支付回调重复 | 多扣用户钱 |
| 数据覆盖 | 并发更新同一行 | 后更新覆盖先更新 |
| 重复注册 | 并发提交注册 | 同一手机号多条记录 |
超卖解决方案
方案 1:数据库乐观锁(CAS)
乐观锁防止超卖
-- 扣减库存时带版本号 / 数量条件
UPDATE inventory SET stock = stock - 1
WHERE product_id = 1001 AND stock > 0;
-- 影响行数 = 0 说明库存不足,返回失败
Java 代码
int affected = inventoryMapper.deductStock(productId, quantity);
if (affected == 0) {
throw new BusinessException("库存不足");
}
方案 2:Redis 预扣减
Redis 原子扣减
public boolean deductStock(Long productId, int quantity) {
String key = "stock:" + productId;
// DECRBY 是原子操作
Long remaining = redisTemplate.opsForValue().decrement(key, quantity);
if (remaining != null && remaining >= 0) {
return true; // 扣减成功
}
// 库存不足,回滚
redisTemplate.opsForValue().increment(key, quantity);
return false;
}
Lua 脚本保证原子性
-- 检查并扣减库存(原子操作)
local stock = tonumber(redis.call('GET', KEYS[1]))
local quantity = tonumber(ARGV[1])
if stock >= quantity then
redis.call('DECRBY', KEYS[1], quantity)
return 1 -- 成功
end
return 0 -- 库存不足
方案 3:分布式锁
Redisson 分布式锁
public boolean deductWithLock(Long productId, int quantity) {
RLock lock = redissonClient.getLock("lock:stock:" + productId);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 获取锁后检查并扣减
int stock = inventoryMapper.getStock(productId);
if (stock >= quantity) {
inventoryMapper.deductStock(productId, quantity);
return true;
}
return false;
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
throw new BusinessException("系统繁忙,请重试");
}
重复扣款解决:幂等设计
基于唯一流水号的幂等
@Transactional
public Result processPayCallback(PayCallbackDTO callback) {
// 1. 查询是否已处理过(幂等检查)
PayRecord record = payRecordMapper.selectByTradeNo(callback.getTradeNo());
if (record != null && "SUCCESS".equals(record.getStatus())) {
return Result.success("已处理"); // 幂等返回
}
// 2. 扣款(带唯一约束)
try {
payRecordMapper.insert(new PayRecord(callback.getTradeNo(), "SUCCESS"));
} catch (DuplicateKeyException e) {
return Result.success("已处理"); // 并发场景下唯一约束兜底
}
// 3. 更新订单状态
orderMapper.updateStatus(callback.getOrderId(), OrderStatus.PAID);
return Result.success();
}
ABA 问题与数据覆盖
版本号防止并发覆盖
// 实体带版本号
@Version
private Integer version;
// MyBatis-Plus 自动处理乐观锁
UPDATE user SET name='新名', version=version+1
WHERE id=1 AND version=1;
常见面试问题
Q1: 秒杀防超卖有哪些方案?
答案:
| 方案 | 性能 | 复杂度 | 适用 |
|---|---|---|---|
DB 乐观锁 stock > 0 | 中 | 低 | 低并发 |
| Redis 原子扣减 | 高 | 中 | 高并发 |
| Redis Lua 脚本 | 高 | 中 | 高并发 + 复杂判断 |
| 分布式锁 | 中 | 中 | 强一致性要求 |
高并发秒杀推荐:Redis 预扣减 → MQ 异步下单 → DB 最终扣减。
Q2: 如何设计幂等接口?
答案:
- Token 机制:下单前获取 Token,提交时校验并删除
- 唯一约束:数据库唯一索引兜底
- 状态机:只有特定状态才能转换,防止重复操作
- 去重表:记录已处理的请求 ID
详见 幂等性设计。
Q3: 分布式锁和数据库乐观锁怎么选?
答案:
- 数据库乐观锁:简单场景、低并发,不需要额外中间件
- 分布式锁:需要保护多步操作的原子性,或涉及多资源
- 高并发推荐 Redis 原子操作(无锁),其次分布式锁