背景
昨天在写一个业务接口,遇到 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(读已提交)隔离级别
可以根据业务具体情况进行方案选择。