关于并发编程常见业务问题及解决方案

一、库存超卖问题 🔴

1.1 问题现象

商品库存仅剩1件,多个用户同时下单,最终下单成功多件,库存变为负数,引发超卖。

1.2 根本原因

典型并发竞争:查询库存、判断库存、扣减库存属于三步复合操作,不具备原子性。多线程同时查询到库存充足,全部通过判断,执行扣减,导致超卖。

1.3 五种解决方案(由低到高)

方案1:数据库悲观锁 for Update

操作:查询库存时加行级排他锁,事务提交释放锁,串行执行扣减。

优点:数据强一致、绝对不会超卖、逻辑简单。

缺点:串行执行、并发性能极差、容易产生锁等待、死锁。

适用:低并发、内部系统、数据一致性要求极高场景。

方案2:数据库乐观锁(版本号机制)

数据表新增字段 data_version

扣减SQL:

update goods set stock=stock-1,data_version=data_version+1 where id=100 and stock > 0 and data_version=1

核心本质:业务乐观思想,DB底层行锁悲观机制。查询无锁,更新校验版本,冲突直接失败不阻塞。

优点:并发性能高、无阻塞、不死锁。

缺点:高并发下大量请求失败,吞吐量上不去,容易出现下单失败扎堆

方案3:数据库乐观锁(库存CAS方案,无版本号)

update goods set stock=stock-1 where id=100 and stock > 0

直接依靠库存字段做CAS校验,无需版本号,最简防超卖。

特点:业务最简单、生产最常用、足够防超卖。

方案4:Redis原子扣减(Lua 无锁方案)

扣减成功执行后续业务,扣减失败直接返回库存不足。

方案原理

Redis 单线程执行 Lua 脚本 = 原子性操作

查询 + 判断 + 扣减 三步变成一个原子命令,

不加锁、不等待、不阻塞、高并发、绝对不超卖

LUA脚本内容:

  1. 获取当前库存
  2. 判断库存是否 > 0
  3. 扣减库存
  4. 返回成功 / 失败

整个过程原子性、无锁、高并发、绝对不会超卖。

这是秒杀、库存、抢购的标准工业级方案。

Lua脚本扣减库存示例
1
2
3
4
5
6
7
local stock = tonumber(redis.call("get", KEYS[1]))
if stock <= 0 then
    return 0  -- 库存不足
end
local after = stock - 1
redis.call("set", KEYS[1], after)
return 1  -- 扣减成功

方案5:本地预扣减库存 + Redis原子扣减

适用场景:大促秒杀,超大并发,请求量远大于库存,支持用户单次多件购买。

核心设计: 本地预扣减小批量库存,减少redis访问次数,减少网络IO。

执行流程

  1. 商品总库存初始化存入 Redis
  2. 优先扣减本地 AtomicInteger 库存,扣减成功则库存扣减完成,进入后续业务流程
  3. 扣减失败,校验本地售罄标记,如果售罄,返回库存不足。
  4. 本地库存不足,调用 Redis Lua 脚本,从 Redis 扣减库存。
  5. 如果脚本返回 0:库存不足,下单失败,添加秒级售罄标记。
  6. 如果脚本返回扣减数量 (10~20):本地 AtomicInteger 原子新增【扣减数量 - 本次购买数量】,库存扣减完成,进入后续业务流程
  7. 如果脚本返回用户购买数量:库存扣减完成,进入后续业务流程,不更新本地库存
  8. 如果Redis 库存耗尽,节点本地添加秒级售罄标记

注意事项

其中校验本地售罄标记,如果在一个标记失效且标记时间距离当前时间在一个标记周期内,则将 4-5 步骤使用DCL双重检查锁包裹

这样做的目的是极致的性能优化,且尽可能避免对Redis的无效访问。

另外,实际业务中,库存是按照商品-sku来的,所以无论是本地 DCL 锁粒度,还是本地库存、本地售罄标记,一定是在并发容器中的。

这种方案,会存在尾部少量库存卖不出去的情况,如果要解决这种问题,就要加短期本地库存回流到redis的逻辑,每次redis lua扣减库存前加本地锁,代码复杂度会高很多。

Lua脚本扣减库存示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
local stock = tonumber(redis.call("GET", KEYS[1]))
local preDeductNum = tonumber(ARGV[1])
local buyNum = tonumber(ARGV[2])

if stock < buyNum then
    return 0
end

if stock >= preDeductNum then
    redis.call("DECRBY", KEYS[1], preDeductNum)
    return preDeductNum
end

redis.call("DECRBY", KEYS[1], buyNum)
return buyNum

方案6:Redis预减库存 + MQ异步削峰落地(大厂标准方案)

  1. 预热库存放入Redis;2. 前端请求先扣Redis库存;3. 扣减成功发送MQ消息;4. 消费者异步落地数据库。

优点:支撑秒杀几十万并发、抗峰值、数据库压力极小。

缺点:架构复杂、存在短暂数据不一致,需要补偿机制。

二、接口幂等性问题(业务必遇)

2.1 问题现象

网络超时、前端重复点击、重试机制导致:重复下单、重复扣款、重复充值、重复回调

2.2 核心定义

同一个请求多次提交,最终业务结果一致,不会产生多条数据

2.3 五大解决方案

  1. 数据库唯一索引:订单号、交易号设唯一索引,重复插入直接报错拦截。最简单、最稳定。

  2. 数据库状态机控制:通过订单状态判断,已支付/已处理任务直接忽略,不重复执行。适合状态流转业务。

  3. Token令牌机制:前端获取唯一Token,下单携带Token,后端Redis校验Token且一次性失效,防止重复提交。

  4. 全局唯一ID幂等:基于雪花算法唯一业务ID,Redis记录已处理ID,做请求去重。

  5. MQ消费幂等:消息ID去重、业务唯一标识去重。

三、缓存并发问题(缓存穿透/击穿/雪崩)

3.1 缓存穿透

问题:查询数据库不存在的数据,缓存永远不命中,请求直接打满DB。

解决方案

  • 缓存空值(短期过期)

  • 布隆过滤器拦截不存在key

  • 接口参数校验

3.2 缓存击穿(热点key失效)

问题:热点缓存key瞬间过期,大量并发请求瞬间打入数据库。

解决方案

  • 互斥锁(本地锁/分布式锁,只放行一个线程更新缓存)

  • 热点key永不过期

  • 缓存过期时间加随机值

3.3 缓存雪崩(大量key同时失效)

问题:海量缓存同时过期,或Redis宕机,所有请求压入数据库,导致DB宕机。

解决方案

  • 过期时间加随机偏移量,错开失效时间

  • Redis集群高可用

  • 服务熔断、降级、限流

四、线程池业务坑点(线上高频故障)

4.1 任务丢失问题

使用 DiscardPolicy 静默丢弃任务,业务无报错,导致数据丢失、业务不执行。

避坑:业务系统禁止使用丢弃策略,优先使用 CallerRunsPolicy 限流或自定义告警策略。

4.2 线程池异常丢失

线程池内部任务异常如果不try-catch,直接消失,无日志、无报错,业务无声失败。

解决:线程任务内部必须try-catch全部异常,自定义线程工厂统一异常捕获。

4.3 线程池参数配置错误

IO密集型设置过少线程,导致接口阻塞、超时、堆积;CPU密集型线程过多,导致上下文切换频繁、CPU打满。

五、ThreadLocal 业务坑点

5.1 内存泄漏

线程池线程复用,ThreadLocal使用完毕不remove,value常驻内存,导致内存泄漏。

铁律finally中必须remove()

5.2 数据串号(极其严重)

线程池复用线程,上一个用户的线程变量残留,导致下一个用户读到别人的数据,造成用户数据错乱、权限漏洞

解决:使用前set、使用后强制清除。

六、复合操作线程不安全(最普遍BUG)

6.1 问题

所有JUC并发容器、锁,只能保证单一操作原子性

例如 ConcurrentHashMap、ReentrantLock 都无法保证:先查询、再修改复合操作原子性。

6.2 场景

积分增减、余额修改、计数统计、状态更新。

6.3 解决方案

  • 业务层加锁(分布式锁)

  • 数据库CAS更新

  • 事务+乐观锁兜底

七、死锁业务问题与规避

7.1 常见场景

转账业务、多资源锁定、嵌套锁、多表更新顺序不一致。

7.2 规避方案

  • 统一锁获取顺序

  • 使用tryLock超时抢锁

  • 尽量减少锁嵌套

  • 缩小锁范围,只锁核心代码

八、高并发限流、削峰、降级(业务架构)

8.1 限流

防止请求量过大打垮服务:Semaphore单机限流、Redis令牌桶/漏桶限流、网关限流。

8.2 削峰

瞬时高并发:MQ异步队列削峰,将瞬时流量转为平稳流量。

8.3 降级

服务压力过大时,关闭非核心业务,保证核心业务可用(比如关闭商品推荐,保证下单支付)。

九、避坑总结

  1. 单机并发问题本质:复合操作不原子、共享变量竞争

  2. 分布式并发问题本质:多节点无法共享本地锁、数据不同步

  3. 能用数据库CAS乐观锁解决,不加代码锁;能Redis分布式锁解决,不用数据库锁。

  4. 高并发场景优先:无锁设计、异步削峰、缓存兜底,尽量避免加锁串行。

  5. 所有并发工具只保证单一操作安全,业务复合操作必须自己保证原子性

  6. 极端单热点key,核心思路是去中心化,将流量尽可能在服务层拦截。