先说注意事项
-
必须都在事务中才能锁住,如果另一接口没有使用事务,是可以直接find、update 的,此时有事务的接口中的锁是没有用的
-
两个事务处理方式要一样,都设置最高事务隔离级别、排他锁就行了,不再研究了
-
该用事务的时候才要用,不要一股脑儿全用事务,否则影响性能。如更新用户余额需要用事务,但是更新用户头像等信息不要使用
-
如果需要使用事务,那么所有地方都得统一用。如余额更新这种功能,所有的接口都要用相同的事务处理逻辑
问题
多个并发的事务对同一行数据进行更新,且更新的数据是基于这一行数据更新前的数据计算的结果,造成了此行数据更新的问题
事务与锁简述
mysql 本身并不具有事务,事务是 InnoDB 引擎所有的功能,事务的隔离级别分为四种:
1、READ_UNCOMMITTED:脏读,一个事务能读到另一个事务未提交的数据,事务的隔离级别最低。
2、READ_COMMITTED:不可重复读,一个事务对一行数据进行更新的过程中,另一个事务对同一行数据进行读取,会在此行数据更新提交前后读取到不一致的结果。避免了脏读的情况,隔离级别比脏读略高一级。
3、REPEATABLE_READ:幻读,同一个事务内读取的数据是保证相同的,但当事务非独立执行时仍然会造成读取的结果不一致。默认的事务隔离级别,比不可重复读高一级。
4、SERIALIZABLE:序列化,事务的隔离级别最高,避免了上述的问题。
两种锁:
1、共享锁:读锁,获取共享锁的事务只能读,不能修改数据,多个事务可同时获取共享锁。
2、排他锁:写锁,一个事务获取写锁后可对数据进行读写,但其他事务无法再获取到写锁直到上一个事务完成。
解决方案
使用 SERIALIZABLE 事务隔离级别,但这并不够,我们仍然需要保证多个事务并发下读取的原始数据一定是之前事务提交更新之后的数据,因此还需要使用排他锁
真实案例(更新用户余额)
接口1:
const { ctx, app: { Sequelize } } = this;
// 开启事务
transaction = await ctx.model.transaction({
// 设置事务隔离级别最高
// isolationLevel: 'SERIALIZABLE'
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE
});
// 设置排他锁
const modelEmployee = await ctx.model.Employee.findOne({ where: { id: employee_id }, transaction, lock: transaction.LOCK.UPDATE });
let balance = modelEmployee.balance
// 使用 number-precision 实现精确四则运算
balance = NP.plus(balance, total_amount)
console.log('锁住')
// 模拟耗时20秒
await new Promise(done => setTimeout(done, 20 * 1000));
// await modelEmployee.update({ balance }, { transaction })
await ctx.model.Employee.update({ balance }, { where: { id: employee_id }, transaction })
console.log('更新', newModel.balance)
接口2:
// 开启事务
transaction = await ctx.model.transaction({
// 事务隔离级别最高
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE
});
// 不加事务
// const model = await ctx.model.Employee.findOne({ where: { id: '67e376f3-7491-4ea2-b567-db74df9b1af0' } })
// console.log('旧值', model.balance)
// const balance = NP.plus(model.balance, 1)
// const res = await model.update({ balance })
// console.log('更新的旧值', res.balance)
// 加事务
const model = await ctx.model.Employee.findOne({ where: { id: '67e376f3-7491-4ea2-b567-db74df9b1af0' }, transaction, lock: transaction.LOCK.UPDATE })
console.log('新值', model.balance)
const balance = NP.plus(model.balance, 1)
const res = await model.update({ balance }, { transaction })
console.log('更新的新值', res.balance)