跳到主要内容

并发安全问题

问题

高并发场景下出现超卖、重复扣款、并发数据覆盖等问题,如何解决?

答案

典型并发安全问题

问题场景后果
超卖秒杀/抢购库存变负数
重复扣款支付回调重复多扣用户钱
数据覆盖并发更新同一行后更新覆盖先更新
重复注册并发提交注册同一手机号多条记录

超卖解决方案

方案 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: 如何设计幂等接口?

答案

  1. Token 机制:下单前获取 Token,提交时校验并删除
  2. 唯一约束:数据库唯一索引兜底
  3. 状态机:只有特定状态才能转换,防止重复操作
  4. 去重表:记录已处理的请求 ID

详见 幂等性设计

Q3: 分布式锁和数据库乐观锁怎么选?

答案

  • 数据库乐观锁:简单场景、低并发,不需要额外中间件
  • 分布式锁:需要保护多步操作的原子性,或涉及多资源
  • 高并发推荐 Redis 原子操作(无锁),其次分布式锁

相关链接