记一次悲观锁重试到达上限报错分析
作者: weiyinghua 原文来源:https://tidb.net/blog/30cc3a0b
一、背景
以下是 MySQL 或 TiDB 常见的锁等待超时,熟悉 MySQL 的伙伴应该比较容易理解:
| | | || – | ——————————————————- | ———————————————————————————————————————————————- || | session A | session B || T1 | begin; | begin; || T2 | ```sqlUPDATE testA SET val = ‘666’ WHERE id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
但目前有这么个场景,TiDB 只有两个会话 A 和 B:
会话 A 是短事务,单线程循环执行只读的 SELECT ... FOR UPDATE
语句; 会话 B 是长事务,仅执行一次更新操作,最终并未报 Lock wait timeout
错,而是报了 pessimistic lock retry limit reached
。
只有两个会话时,第一感觉是会话 B 不会达到最大重试次数报错,而是锁等待超时。接下来进行复现并分析原因。
二、问题复现
由于 pessimistic lock retry limit reached 报错和 max-retry-count
配置有关。为更容易复现把配置 max-retry-count
改小,含义是:悲观事务中单个语句最大重试次数,重试次数超过该限制,语句执行将会报错:
通过大模型辅助生成的代码可轻松复现,以下是复现的 SQL。同样只有两个会话 A 和 B,会话 A 是短事务,会话 A 先单线程循环执行只读的 SELECT ... FOR UPDATE
语句:
会话 B 是长事务,跑一次就有报错如下:
三、报错如何产生?
TiDB 的悲观锁是在乐观事务基础上实现的,相比乐观事务的两阶段提交多了一个加锁阶段,pessimistic lock retry limit reached 报错是在加锁阶段出现的。写入时 TiDB 先从 PD 获取当前 tso(分布式全局递增时间戳)作为当前锁的 for_update_ts。悲观事务会用 for_update_ts 检查是否存在写冲突,效果等同于可更新的 start_ts,尝试写入并获取锁时检查 key 的 commit_ts,如果大于 for_update_ts 则出现写冲突,就会用当前冲突的 commit_ts(conflict_commit_ts)更新 for_update_ts 再重试 DML。当重试次数超 max-retry-count
,就会向客户端返回报错 pessimistic lock retry limit reached。示意图:

对于当前的例子:
会话 A 不断随机更新 5000 行数据中的某一行。
会话 B 要一次性 5000 行的锁,会话 B 加锁的 key 比会话 A 多,每次总有某行数据的 commit_ts 大于当前的 for_update_ts,所以只能不断重试,直到重试次数超 max-retry-count
报错 pessimistic lock retry limit reached。在文章有介绍:https://tidb.net/blog/7730ed79 :

四、只读语句也会使版本变大吗?
我们再结合例子看一下,会话 A 不断循环以下:
会话 B 是长事务,只跑一次:
假设最初会话 A 运行事务时,对于 id = 1 这行记录 commit_ts=100。
然后会话 B 获取到 for_update_ts = 101,且会话 B 尝试加锁时,发现这行数据已被会话 A 修改并提交,导致 commit_ts = 102,此时 commit_ts = 102 > for_update_ts = 101,会话 B 会进行悲观锁重试,更新 for_update_ts 为 conflict_commit_ts=102,重新读取数据,再次尝试加锁直到重试上限报错。那问题就来了,只读的 SELECT ... FOR UPDATE
会导致 commit_ts 变大吗?
答:是的,只读语句 SELECT ... FOR UPDATE
的处理和正常的写入处理并无差别,也会留下 MVCC 版本记录。因此,即使是只读语句,只要加了 FOR UPDATE
,也会产生写入行为,从而推进版本号。
五、有时报锁超时有时报超重试次数?
为什么在实际使用过程中,有时报错 Lock wait timeout,有时报错 pessimistic lock retry limit reached?
对于文章开头的锁等待例子,会话 A 一直锁住某行数据不提交,会话 B 尝试更新这行数据。在 TiDB 中其实会话 B 会不断根据 wait-for-lock-timeout
重试,重试最大时间为 innodb_lock_wait_timeout
。这里和重试最大次数 max-retry-count
无关。如果事务能在 innodb_lock_wait_timeout
内成功获取锁,则可以继续执行,否则报错 Lock wait timeout exceeded。innodb_lock_wait_timeout
和 max-retry-limit
两个参数无直接关联,TiDB 中哪个条件先达到,就报哪种错。
六、小结
本文通过对比 MySQL 和 TiDB,在锁等待报错机制上的差异,分析了 Lock wait timeout
与 pessimistic lock retry limit reached
两种常见错误的成因。尽管触发条件不同,但二者本质上都反映了当前事务存在严重的资源竞争或访问冲突。
对于此类问题,仅靠数据库参数调整(如增加 innodb_lock_wait_timeout
或 max-retry-count
)无法根本解决。在实际业务中,更应从业务层面如避免大事务、调整粒度、热点、调整访问顺序、简化事务逻辑等方面入手。只有数据库参数优化与业务访问优化相结合,才能有效降低锁冲突概率,提升事务成功率与系统整体吞吐量。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/5cedb7bec1352c13cba3d655f】。文章转载请联系作者。
评论