**高并发、微服务 、性能调优实战案例100讲，所有案例均源于个人工作实战，均配合代码落地**

加我微信：itsoku，所有案例均提供在线答疑。

# 解决超卖的4种方案

## 本文内容

- 了解超卖本质解决方案，也是并发修改数据的本质解决方案
- 掌握4种方案解决超卖问题（原理、源码、测试用例）
- 每个方案都有测试用例，会模拟并发秒杀，带大家看效果
- 学完后，完全可以解决工作中所有并发修改数据出错的问题，太硬核了吧。。。



## 避免超卖，避免并发修改数据出错，有银弹吗？

本质上是需要加锁，不管是什么锁，只要让减库存的操作排队，便可解决超卖问题，核心点就是：**加锁排队**

同理：解决并发修改数据出错问题，最终也是靠锁解决，比如乐观锁、悲观锁，本质上都是要靠锁，让并发问题排队执行，只是这个锁的范围大小的问题。

言归正传，下面咱们上方案、原理、源码、测试用例，一个都不能少。



## 商品表

```sql
-- 商品表
create table if not exists t_goods
(
    goods_id   varchar(32) primary key comment '商品id',
    goods_name varchar(256) not null comment '商品名称',
    num        int          not null comment '库存',
    version    bigint default 0 comment '系统版本号'
) comment = '商品表';
```



## 解决超卖方案1：通过update中携带条件判断解决超卖问题

### 原理

通过下面sql的执行结果，便可确保超卖问题，重点在于需要在update的where条件中加上库存扣减后不能为0，sql会返回影响行数，如果影响行数为0，表示库存不满足要求，扣减失败了，否则，扣减库存成功。

```sql
String goodsId = "商品id";
int num = "本次需要扣减的库存量";

// count表示影响行数
int count = (update t_goods set num = num - #{num} where goods_id = #{goodsId} and num - #{num} >= 0);

// count = 1，表示扣减成功，否则扣减失败
if(count==1){
	//扣减库存成功
}else{
	//扣减库存失败
}
```

### 源码

```java
com.itsoku.lesson004.service.GoodsServiceImpl#placeOrder1
```

### 运行看结果

```java
===========================解决超卖，方案1 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前，商品库存：10
抢购后，商品库存：0
下单成功人数：10
下单失败人数：90
===========================解决超卖，方案1 执行结束=======================================
```



## 解决超卖方案2：使用乐观锁版本号解决这个问题

### 原理

需要在库存表加一个version字段，这个version每次更新的时候需要+1，单调递增的。

**业务逻辑如下**

```java
String goodsId = "商品id";
int num = "本次需要扣减的库存量";
GoodsPo goods = (select * from t_goods where goods_id = #{goodsId});

// 期望数据库中该数据的version值
int expectVersion = goods.getVerion();

//乐观锁更新数据，where条件中必须带 version = #{expectVersion}
int count = update t_goods set num = num - ${num}, version = version + 1 where goods_id = #{goodsId} and version = #{expectVersion}

// count = 1，表示扣减成功，否则扣减失败
if(count==1){
	//扣减库存成功
}else{
	//扣减库存失败
}
```

### 源码

```java
com.itsoku.lesson004.service.GoodsServiceImpl#placeOrder2
```

### 运行结果

```java
===========================解决超卖，方案2 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前，商品库存：10
抢购后，商品库存：0
下单成功人数：10
下单失败人数：90
===========================解决超卖，方案2 执行结束=======================================
```



## 解决超卖方案3：对比数据修改前后是否和期望的一致

### 原理

```java
String goodsId = "商品id";
int num = "本次需要扣减的库存量";

//扣减库存前，查出商品库存数量，丢到变量 beforeGoodsNum 中
GoodsPo beforeGoods = (select * from t_goods where goods_id = #{goodsId});
int beforeGoodsNum = beforeGoods.num;

// 执行扣减库存操作，条件中就只有goodsId，说明这个可能将库存扣成负数，出现超卖，继续向下看，后面的步骤将解决超卖
update t_goods set num = num - ${购买的商品数量} where goods_id = #{goodsId}

//扣减库存后，查出商品库存数量，丢到变量 afterGoodsNum 中
GoodsPo afterGoods = (select * from t_goods where goods_id = #{goodsId});
int afterGoodsNum = afterGoods.num;

// 如下判断，库存扣减前后和期望的结果是不是一致的，扣减前的数据 - 本次需要扣减的库存量 == 扣减后的数量，如果是，说明没有超卖
if(beforeGoodsNum - num == afterGoodsNum){
	//扣减库存成功
}else{
	//扣减库存失败
}
```

这种方案虽然看起来很奇怪，但是有些业务场景中，可以解决一些问题，比如批量去修改数据，想判断批量的过程中，数据是否被修改过，可以通过这种方式判断。

### 源码

```java
com.itsoku.lesson004.service.GoodsServiceImpl#placeOrder3
```

### 运行结果

```
===========================解决超卖，方案3 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前，商品库存：10
抢购后，商品库存：0
下单成功人数：10
下单失败人数：90
===========================解决超卖，方案3 执行结束=======================================
```



## 解决超卖方案4：通过辅助类解决超卖问题

### 原理

需要添加一张辅助表（t_concurrency_safe），如下，这张表需要有版本号字段，通过这张表的乐观锁，**将需要保护的业务方法包起来**，解决超卖问题。

```sql
create table if not exists t_concurrency_safe
(
    id       varchar(32) primary key comment 'id',
    safe_key varchar(256) not null comment '需要保护的数据的唯一的key',
    version  bigint default 0 comment '系统版本号，默认为0，每次更新+1',
    UNIQUE KEY `uq_safe_key` (`safe_key`)
) comment = '并发安全辅助表';
```

逻辑如下

```java
String goodsId = "商品id";
int num = "本次需要扣减的库存量";

// 需要给保护的数据生成一个唯一的：safeKey
String safeKey = "GoodsPO:"+商品id;

// 如下：根据 safe_key 去 t_concurrency_safe 表找这条需要保护的数据
ConcurrencySafePO po = (select * from t_concurrency_safe where safe_key = #{safe_key});

// 这条数据不存在，则创建，然后写到 t_concurrency_safe 表
if(po==null){
	po = new ConcurrencySafePO(#{safe_key});
    // 向 t_concurrency_safe 表写入一条数据
    insert into t_concurrency_safe (safe_key) values (#{safeKey});
}

// 下面执行扣减库存的操作，注意，如果用方案4，那么需要保护的数据的修改，均需要放在这个位置来保护，这块大家细品下
{
    //扣减库存前，查出商品库存
    GoodsPo beforeGoods = (select * from t_goods where goods_id = #{goodsId});
    
    //判断库存是否足够
    if(beforeGoods.num == 0){
        //库存不足，秒杀失败
        return;
    }

    // 执行扣减库存操作，条件中就只有goodsId，说明这个可能将库存扣成负数，出现超卖，继续向下看，后面的步骤将解决超卖
    update t_goods set num = num - ${购买的商品数量} where goods_id = #{goodsId}
}

//对 ConcurrencySafePO 执行乐观锁更新
int update = update t_concurrency_safe set version = version + 1 where id = #{po.id} and version = #{po.version}

// 若update==1，说明被保护的数据，期间没有发生变化
if(update == 1){
    //秒杀成功
}else{
    //说明被保护的数据，期间发生变化了，下面要抛出异常，让事务回滚
    throw new ConcurrencyFailException("系统繁忙，请重试");
}
```

如果是老的业务，涉及到大量代码，改造复杂，那么可以用此方案将业务代码包裹起来，便可防止并发修改导致数据不一致的问题。

### 源码

```
com.itsoku.lesson004.service.GoodsServiceImpl#placeOrder4
```

### 运行结果

```java
===========================解决超卖，方案4 开始执行=======================================
模拟 100 人进行抢购
抢购结束啦............

抢购前，商品库存：10
抢购后，商品库存：0
下单成功人数：10
下单失败人数：90
===========================解决超卖，方案4 执行结束=======================================
```



## 到底选哪种方案？

这几种方案都可以解决超卖的问题，但是方案1最靠谱。

这里说下原因：团队中，涉及到很多人修改代码，那么问题就来了，可能修改库存的地方，有多个口子，那么此时，用其他方案就可能存在风险了，可能会出错。

如果能做到，收敛到一个口子中去修改数据，就是最终修改数据都是一个口子，那么上面的方法都可以，都可确保数据不会出问题，你们觉得呢？



## 总结

本文介绍了4种方案解决超卖问题，每种方案都有其使用场景，可能感觉有些方案很罕见，但是也许在日后某些场景下，你就会用到。

说了这么多方案，也算是开拓下大家解决问题的思路，有些问题，也许有更多方案，每种方案都有其存在的价值，抱着开放的心态，才能不断精进。

一起加油。



## 课程部分大纲，连载中。。。。

以下课程均来源于个人多年的实战，均提供原理讲解 && 源码落地

```java
1. 分片上传实战
2. 通用并发处理工具类实战
3. 实现接口性能压测工具类
4. Semaphore实现接口限流实战
5. 并行查询，优化接口响应速度实战
6. 使用TransactionTemplate优化接口性能
7. 手写线程池管理器，管理&监控所有线程池
8. 使用SpringBoot实现动态Job管理功能
9. 通用的Excel导出功能实战
10. 通用的幂等性工具类实战
11. 通用的防并发处理工具类（解决并发修改db数据出错问题）
12. 接口返回值通用设计
13. 接口太多，各种dto、vo不计其数，如何命名？
14. 一个业务太复杂了，方法太多，如何传参？
15. 如何统计接口耗时？
16. AOP实战接口日志打印功能
17. AOP实现业务操作日志记录功能
18. AOP实现MyBatis分页功能
19. SpringBoot读写分离实战
20. MQ专题：事务消息实战（防止消息丢失）
21. MQ专题：消息幂等消费通用方案实战
22. MQ专题：延迟消息通用方案实战
23. MQ专题：顺序消息通用方案实战
24. 分布式事务：使用事务消息实现事务最终一致性
25. 分布式事务：通用的TCC分布式事务生产级代码落地实战
26. 分布式锁案例实战
27. 微服务中如何传递上下文？实战
28. 微服务链路日志追踪实战（原理&代码落地）
29. SpringBoot实现租户数据隔离
30. MyBatis进阶：封装MyBatis，实现通用的无SQL版CRUD功能，架构师必备
31. MyBatis进阶：自己实现通用分表功能，架构师必备
32. MyBatis进阶：实现多租户隔离ORM框架
33. SpringBoot中实现自动监听PO的变化，自动生成表结构
34. 分布式专题：其他实战课程等
35. 性能调优：如何排查死锁？
36. 性能调优：如何排查内存溢出？
37. 性能调优：CPU被打满，如何排查？
38. 性能调优：生产代码没生效，如何定位？
39. 性能调优：接口太慢，如何定位？
40. 性能调优：如何查看生产上接口的入参和返回值？
41. 性能调优：远程debug
42. 生产上出现了各种故障，如何定位？
43. 其他等各种实战案例。。。
。。。
```

