### 问题描述
1. 用户下单过程中为了防止超卖,加入redis锁与mysql事务查询+插入,步骤如下:
1. 开启事务
2. 判断锁,获取到以后加锁
3. 查询(未加lock for update)是否售出,否添加
4. 解锁,结束事务
2. 如上操作还是会出现超卖现象
### 原因分析
1. mysql版本为5.7.37,默认事务隔离级别,可重复读(Repeatable Read)
2. 还原了事务可能出现的流程,不考虑redis锁的情况如下:5.7新版本的幻读问题
| T1 | T2 |
| :————- | :————- |
| 开启 | |
| | 开启 |
| 查询为空并插入 | |
| | 查询为空并插入 |
| | 提交 |
| 提交 | |
3. 还原了事务可能出现的流程,加入redis锁(用cache的put/forget完成的加锁与释放)的情况如下,5.7新版本的幻读问题没有被解决,原因在于 redis 锁其实并不是被本身的进程解锁的,php多进程的原理造成了这个坑爹的问题,本质上其实是加锁与解锁不规范
| T1 | T2 |
| :————- | :————- |
| 开启 | |
| | 开启 |
| | redis 锁 |
| redis 锁成功 | |
| 查询为空并插入 | |
| | 查询为空并插入 |
| | 提交 |
| | redis 解锁 |
| 提交 | |
| redis 解锁 | |
4. 参考资料
1. mysql 事务隔离级别详细说明: https://juejin.cn/post/7136112451959848991
2. MySQL 可重复读隔离级别,解决幻读了吗? https://blog.51cto.com/u_14888059/5685522
3. Redis 分布式锁常见问题: https://www.51cto.com/article/689646.html
4. php解锁 Redis 锁的正确姿势: https://learnku.com/articles/4211/unlock-the-correct-position-of-the-redis-lock
5. 在Spring事务管理下,Synchronized为啥还线程不安全? https://juejin.cn/post/6844904005282332685
### 解决方案
1. 方案一:给Mysql查询语句增加lock for update语句,锁住要插入的行,最好在查询条件加上索引,这就相当于redis锁只是过滤了一部分流量,并没有真正的实现锁的问题。
2. 方案二:使用laravel cache的原子锁,解锁方式正确以后可保证是真正的锁住。
3. 注意一:加锁的时机很重要,参考资料3与laravel手册说明,加锁应该放在try/catch外。
4. 注意二:锁操作要放到Mysql事务外,参考资料5,原因在于事务内加锁会导致多个事务开启,且无法保证事务提交顺序。