高并发下的Mysql事务锁与Redis分布式锁

### 问题描述

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,原因在于事务内加锁会导致多个事务开启,且无法保证事务提交顺序。