关于 MySQL 重复读导致的重复插入问题

码农老张 后端 2024-12-02

关于 MySQL 重复读导致的重复插入问题

背景

昨天在写一个业务接口,遇到 MySQL 重复读导致的重复插入问题,下面是一段伪代码:js

代码解读
复制代码
async function createClassOrder(uids, classId){ // 事务开始 await Promise.all(uids.map(uid => { // 将 TBL_CLASS 表进行行锁 await db.execute('SELECT * FROM TBL_CLASS WHERE id=? FOR UPDATE', [classId]) // 查找该用户是否已经预约过 const classOrders = await db.execute('SELECT * FROM TBL_CLASS_ORDER WHERE classId=? AND uid=?', [classId, uid]) // 如果已经预约过则进行报错 if(classOrders.length > 0) { throw new Error('您已经预约') } // 创建预约,涉及到表 TBL_CLASS_ORDER // 更新课程信息,涉及到表 TBL_CLASS })) // 事务结束 } // 接口路由层有限制重复调用问题

可以发现,这段代码其实在最开始已经有数据库锁了,所以如果涉及到对表 TBL_CLASS 相同行数据进行操作时,事务 A 会进行锁定,事务 B 在执行相同行的时候,会进行等待,直到事务 A 结束,事务 B 再继续执行。但为什么仍然导致数据重复插入呢?原因就在 classOrders 里,当事务 A 结束后,事务 B 继续执行时,因为 MySQL 默认隔离级别是重复读,导致事务 B 在读取 classOrders 时仍然为空。

方案

找到原因,方案就比较容易了,目的就是读取最新数据,无论事务是否提交。

1. 使用共享锁

读取 TBL_CLASS_ORDER 行数据时读取最新数据,可以使用共享锁,例如js

代码解读
复制代码
const classOrders = await db.execute('SELECT * FROM TBL_CLASS_ORDER WHERE classId=? AND uid=? LOCK IN SHARE MODE', [classId, uid])

2. 事务并发执行

事务 A 结束后再创建事务 B,可以在控制层进行限制。

3. 强制进行当前读js

代码解读
复制代码
const classOrders = await db.execute('SELECT * FROM TBL_CLASS_ORDER WHERE classId=? AND uid=? FOR UPDATE', [classId, uid])

但可能会导致增加锁竞争

4. 设置隔离级别

设置 READ COMMITTED(读已提交)隔离级别

可以根据业务具体情况进行方案选择。

转载来源:https://juejin.cn/post/7423294364308734004

Apipost 私有化火热进行中

评论