死锁的概念
死锁是由于多个事务之间互相持有对方事务所需的锁,结果导致事务无法继续执行,进而触发死锁。死锁检测会在事务申请锁超时时触发。
从mysql5.7.15开始,新增了选项 innodb_deadlock_detect,默认开启,innodb会检测死锁,在高并发场景下可以提高事务的并发性能。再触发死锁检测时,innodb的最大检测深度是200(检测深度这里后面解释),innodb的行锁申请等待超时时间默认为50秒,当然有时长时间的行锁等待不一定就是死锁。
那么如果此参数默认关闭了,又遇到了死锁,mysql又会如何处理?
- 这时就涉及到了另一个参数,锁超时:innodb_lock_wait_timeout,该参数指定了锁申请时的最长等待时间 那么是否可以认为是超过50秒的时间,申请不到锁,就自动回滚了呢?
- 其实不是。这里又涉及了另一个参数innodb_rollback_on_timeout,该参数决定了当请求锁超时,是回滚整个事务,还是回滚当前语句。默认是关的, 只回滚当前语句。
其实除此之外除此之外,我们还应思考的问题有很多?
- 如果在高并发的场景下,发生了如上的设置下的死锁,那么占用的连接数就会上来,上游业务就会被拖累,导致整体雪崩
- 申请锁超时时间肯定时需要设置为一个比较小的时间, 那么设置多少合适?
- 面对申请锁超时情况,应用端如何合理的处理锁超时时间,重试还是放弃?
- 当申请锁超时之后,我们时设置回滚整个事务还是设置回滚当前sql?
旧版的死锁检测机制
简述
上面我们说innodb在触发死锁时的最大200的检测深度。在8.0.18版本之前,innodb的死锁检测机制是最常见的深度优先遍历(DFS)算法,等待关系图如下:
- 等待关系中的节点是一种等待对象(object) 例如行锁; 另外一个是事务
- 等待关系中每一个对象都被事务锁持有,用虚线箭头表示
- 等待关系中每一个事务都在等待一个对象(例如行锁),用实箭头表示
在死锁检测的时候,mysql server会持有lock_sys->mutex,然后对整个等待关系图进行DFS遍历,这里遍历的最大深度是200,当发现成环时,就认为发生了死锁,msyql server层会根据事务优先级/undo大小/锁数量等因素选择一个事务回滚或者当前sql回滚。
问题
老的死锁检测主要存在的问题是性能问题,在死锁检测的时候,mysql server会持有lock_sys->mutex这个大锁然后在做DFS检测,在lock_sys->mutex持有期间所有的新加行锁和释放锁全部会被阻塞。当出现大量锁等待的时候(电商的热点场景)等待关系图会变的特别大,导致每一次加锁DFSb遍历整个等待关系图的时间变长,引发大量线程等待lock_sys->mutex,从而导致数据库在此场景下雪崩。
新版死锁检测
从上文我们知道在申请锁失败后,会触发死锁检测,这里的死锁检测会对整个等待关系图就行DFS扫描,从而导致大量的锁等待。新版的死锁检测理念是尽量在较短的时间内检测出死锁,而不保证每次死锁检测都能找出已存在的死锁。因此新版死锁检测对等待关系图进行了裁剪,裁剪之后的图称之为稀疏等待关系图。
稀疏等待关系图
在稀疏等待关系图中:
- 等待关系中的节点都是一个事务,等待关系是事务和事务的直接等待
- 等待关系中一个事务只等待一个被等待事务
- 实际上一个事务可能被很多事务等待,但是稀释等待关系图中进行了裁剪,只显示最早被等待事务
- 等待关系并非是一个全局一致的等待关系
-
只是一个乐观的快照,获取快照时并不持有lock_sys->mutex ,仅持有lock_sys->wait_mutex
-
稀疏等待关系图的构造
- 稀疏等待关系图保存在trx_t::blocking_trx这个原子指针中,通过版本号来保证其有效性
- 等待关系图分别在等待事务加锁和等待事务释放锁的时候被更新
- 特别的如果被等待事务释放锁,那么等待事务如果需要继续等待,则blocking_trx会被跟新成新的被等待事务
死锁检测流程
新的死锁检测机制变的比较轻量:
1. 在持有lock_sys->wait_mutex的情况下,构造稀疏等待关系图
2. 对稀疏等待关系图进行DFS扫描,得到成环的子图
3. 对成环的子图进行有效性检测;确保其版本号是一致的;确保其还在继续等待
4. 选择牺牲事务,并进行回滚
源码解读
storage/innobase/lock/lock0lock.cc
dberr_t RecLock::add_to_waitq(const lock_t *wait_for, const lock_prdt_t *prdt) {
// m_trx : 当前事务
// lock_t :当前事务申请的锁
ut_ad(locksys::owns_page_shard(m_rec_id.get_page_id()));
// 确保同时持有 lock_sys->mutex
ut_ad(m_trx == thr_get_trx(m_thr));
/* It is not that the body of this function requires trx->mutex, but some of
the functions it calls require it and it so happens that we always posses it
so it makes reasoning about code easier if we simply assert this fact. */
// 确保当前事务持有trx->mutex
ut_ad(trx_mutex_own(m_trx));
// 尝试先将该事务加入等待队列
DEBUG_SYNC_C("rec_lock_add_to_waitq");
// 假如当前事务设置了TRX_FORCE_ROLLBACK, 既不允许该事务等待锁锁,而造成可能的死锁,我们直接将该事务回滚
if (m_trx->in_innodb & TRX_FORCE_ROLLBACK) {
return (DB_DEADLOCK);
}
m_mode |= LOCK_WAIT;
// 进行初步检测, 并设置查询线程
prepare();
// 如果是高优先级事务,不将其排在哈希表中
lock_t *lock = create(m_trx, prdt);
lock_create_wait_for_edge(lock, wait_for);
ut_ad(lock_get_wait(lock));
set_wait_state(lock);
MONITOR_INC(MONITOR_LOCKREC_WAIT);
return (DB_LOCK_WAIT);
}