TiDB Write Conflict问题分析
本篇博客的视频教程首发于 Youtube:科技小飞哥,加入 电报粉丝群 获得最新视频更新和问题解答。
背景
我们线上使用TiDB来储存数据,最近DBA报告TiDB发生了write conflict.
[2020/08/25 06:01:44.781 +08:00] [WARN] [session.go:623] [retrying] [conn=26799778] [schemaVersion=194] [retryCnt=0] [queryNum=0] [sql="UPDATE user_tab SET `region` = ?, `status` = ?, `update_time` = ? WHERE `from_userid` = ? AND `to_userid` = ? AND `type` = ? [arguments: (ID, 0, 1598306504, 118049824, 8572364, 1)]"]
[2020/08/25 06:01:44.782 +08:00] [WARN] [session.go:644] ["transaction association"] [conn=26799778] ["retrying txnStartTS"=418986460378563450] ["original txnStartTS"=418986460352348716]
[2020/08/25 06:01:44.783 +08:00] [INFO] [2pc.go:1104] ["2PC clean up done"] [conn=26799778] [txnStartTS=418986460352348716]
[2020/08/25 06:01:45.612 +08:00] [WARN] [client_batch.go:577] ["send request is cancelled"] [to=10.65.234.201:20160] [cause="context canceled"]
[2020/08/25 06:01:45.612 +08:00] [WARN] [session.go:425] [sql] [conn=26799680] [label=general] [error="[kv:9007]Write conflict, txnStartTS=418986459434844923, conflictStartTS=418986459434844925, conflictCommitTS=418986460457205920, key={tableID=89, handle=8646911292299313507} primary={tableID=89, handle=1729382264526793474} [try again later]"] [txn="Txn{state=invalid}"]
以下就针对以上log进行分析。
TiDB乐观锁
在 v3.0.8 版本之前,TiDB 默认采用乐观事务模型,在事务执行过程中并不会做冲突检测,而是在事务最终 COMMIT 提交时触发两阶段提交,并检测是否存在写写冲突。当出现写写冲突,并且开启了事务重试机制,则 TiDB 会在限定次数内进行重试,最终重试成功或者达到重试次数上限后,会给客户端返回结果。因此,如果 TiDB 集群中存在大量的写写冲突情况,容易导致集群的 Duration 比较高。
出现锁冲突的原因
TiDB 中使用 Percolator 事务模型来实现 TiDB 中的事务。Percolator 总体上就是一个二阶段提交的实现。具体的二阶段提交过程可参考乐观事务文档。
当客户端发起 COMMIT 请求的时候,TiDB 开始两阶段提交:
- TiDB 从所有要写入的 Key 中选择一个作为当前事务的 Primary Key
- TiDB 向所有的本次提交涉及到的 TiKV 发起 prewrite 请求,TiKV 判断是否所有 Key 都可以 prewrite成功
- TiDB 收到所有 Key 都 prewrite 成功的消息
- TiDB 向 PD 请求 commit_ts
- TiDB 向 Primary Key 发起第二阶段提交。Primary Key 所在的 TiKV 收到 commit 操作后,检查数据合法性,清理 prewrite 阶段留下的锁
- TiDB 收到两阶段提交成功的信息
- 写写冲突发生在 prewrite 阶段,当发现有其他的事务在写当前 Key (data.commit_ts >txn.start_ts),则会发生写写冲突。
TiDB 会根据 tidb_disable_txn_auto_retry 和 tidb_retry_limit 参数设置的情况决定是否进行重试,如果设置了不重试,或者重试次数达到上限后还是没有 prewrite 成功,则向 TiDB 返回 Write Conflict 错误。
txnlock 表示集群中存在写写冲突,txnLockFast 表示集群中存在读写冲突。
也可以通过 TiDB 日志查看是否有 [kv:9007]Write conflict 关键字,如果搜索到对应关键字,则可以表明集群中存在写写冲突。
关于日志的解释如下:
[kv:9007]Write conflict:表示出现了写写冲突 txnStartTS=416617006551793665:表示当前事务的 start_ts 时间戳,可以通过 pd-ctl 工具将时间戳转换为具体时间 conflictStartTS=416617018650001409:表示冲突事务的 start_ts 时间戳,可以通过 pd-ctl 工具将时间戳转换为具体时间 conflictCommitTS=416617023093080065:表示冲突事务的 commit_ts 时间戳,可以通过 pd-ctl 工具将时间戳转换为具体时间 key={tableID=47, indexID=1, indexValues={string, }}:表示当前事务中冲突的数据,tableID 表示发生冲突的表的 ID,indexID 表示是索引数据发生了冲突。如果是数据发生了冲突,会打印 handle=x 表示对应哪行数据发生了冲突,indexValues 表示发生冲突的索引数据 primary={tableID=47, indexID=1, indexValues={string, }}:表示当前事务中的 Primary Key 信息
通过 pd-ctl 将时间戳转换为可读时间:
./pd-ctl -u https://127.0.0.1:2379 tso {TIMESTAMP}
通过 tableID 查找具体的表名:
curl http://{TiDBIP}:10080/db-table/{tableID}
通过 indexID 查找具体的索引名:
SELECT * FROM INFORMATION_SCHEMA.TIDB_INDEXES WHERE TABLE_SCHEMA='{table_name}' AND TABLE_NAME='{table_name}' AND INDEX_ID={indexID};
另外在 v3.0.8 及之后版本默认使用悲观事务模式,从而避免在事务提交的时候因为冲突而导致失败,无需修改应用程序。悲观事务模式下会在每个 DML 语句执行的时候,加上悲观锁,用于防止其他事务修改相同 Key,从而保证在最后提交的 prewrite 阶段不会出现写写冲突的情况。
总结
TiDB的乐观锁,会导致使用普通的SQL语句更新的时候。
UPDATE user_tab set name = `***` where userid = `a`
TiDB中当有并发请求对同一个unique key进行update操作的时候,由于乐观锁的机制,会出现Write Conflict. 而MySQL使用的是悲观锁,不会出现Write Conflict。
<全文完>