Sequelize 事务大并发下造成的死锁问题
Author: bugall
Wechat: bugallF
Email: [email protected]
Github: https://github.com/bugall
环境
Mysql 5.6 InnoDB
一.声明
这并不是sequelize的bug,在涉及到connection pool的时候都有可能出现这个问题
二.起因
我们有个需要事物的业务场景,上线之初一直运行正常,可是在晚上高峰的时候一直会有逻辑错误的问题,刚开始觉得是逻辑有问题。
后来查看innodb才发现出现大量的锁,主表的某些数据行持有锁不释放,其它的sql一直等待,直到业务服务器报deadlock。因为主表其它业务模块也会用到,所以是一个比较紧急的情况
三.错误的代码
代码可以简写为:
DBSequelize.transaction({autocommit:false,isolationLevel:'SERIALIZABLE'}).then(function(t){ return db.Tuser.create({...},{transaction:t}); }).then(function(trans){ return db.TuserRelation.update({...},{where:{...},transaction:t}) }).then(function(trans){ return db.Twork.update({...},{where:{...}}) }).then(function(result){ t.commit(res.json(result)); })
大概意思就是如果用户注册了一个账号,就在账号关系里增加一个记录这两个操作是原子的。没并发的情况下,这个逻辑执行是没有问题的,但是一旦有并发就会出问题。
四.分析错误
首先在创建事务的时候我们指定了两个参数”不自动提交“,”一致性读“。(我们这里希望对一条数据读的时候加X锁,事务级别但是问题不大,最多是影响数据库的并发性能,
对于主表来说加X锁是一个很愚蠢的做法),问题关键是出现在不自动提交设置这里。
刚开始我对事务理解就是:只有我们主动执行commit的时候才会把buffer cache的脏数据写入磁盘并释放事务里持有的锁。
但是真是情况不是这样的。在mysql官方文档中找到了关于autocommit transaction相关描述
By default, MySQL runs with autocommit mode enabled. This means that as soon as you execute a statement that updates (modifies) a table, MySQL stores the update on disk to make it permanent. The change cannot be rolled back. To disable autocommit mode implicitly for a single series of statements, use the START TRANSACTION statement: START TRANSACTION; SELECT @A:=SUM(salary) FROM table1 WHERE type=1; UPDATE table2 SET summary=@A WHERE type=1; COMMIT; With START TRANSACTION, autocommit remains disabled until you end the transaction with COMMIT or ROLLBACK. The autocommit mode then reverts to its previous state.
这里有几个概念可以明确:
1. autocommit的设定是针对session的(一个数据库连接),如果你对这个这个连接设定了autocommit=1那么这个连接 执行的所有sql都会自动commit,反之不会自动提交必须手动commit提交 2. 无论连接设置的autocommit是什么状态,在执行事务时候事务里的autocommit状态都会被隐式的设置为0,当事物执行 完成后autocommit会被设置为原先的状态(执行事务前)
所以我们的问题出现在创建事务时候的{tautocommit=false}这个参数其实是对连接设置的,而不是事务。
在并发的情况下,sequelize连接池的连接是会复用的,假如一个执行了事务的连接去执行其它的DML语句,这些语句是不会提交的
也就是说数据行的锁不释放,其它请求设计到该条数据的时候将会被阻塞,直到超时。
五.解决
我们只需在事务参数中设定autocommit=true就行了。需要注意的时候在Isolation的级别是一致性读的时候,因为该级别违反了
MVCC规范,所以这时候的autocommit=true/false是要分情况讨论的。这里不是sequelize的问题,在用到connection pool
对innodb进行操作的时候应该注意这个情况。