TiDB 并发更新同一行频繁报 Write Conflict 写入冲突故障

业务系统高并发场景下,多个请求同时更新同一条用户余额记录,频繁抛出 Write Conflict 报错,更新操作失败,单线程串行执行更新则无任何异常,集群硬件资源充足、无 TiKV 热点、GC 参数为默认配置。

乐观事务还是悲观事务?

并行执行依赖Region级并发扫描。

事务并发更新同一个key导致的,如果是乐观锁模式,可以改成悲观事务观察看看。

乐观事务默认在提交时才检测冲突,而非执行时上锁

TiDB 默认乐观事务,提交时才校验同一行 key 的写入时序;多请求同时更新同一行,后提交的事务检测到数据已被修改,直接抛出 Write Conflict。串行执行无竞争所以正常。

我们测试环境是V8.5 默认悲观锁,每天也会有很多的write conflict。
官网有这个说明哦,可以详细看下
在 v3.0.8 之前,TiDB 默认使用的乐观事务模式会导致事务提交时因为冲突而失败。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。
Prewrite 阶段

在悲观锁模式下,在事务的提交阶段沿用的仍然是乐观锁模式,所以在 Prewrite 阶段乐观锁遇到的锁相关的一些报错,在悲观锁模式同样会遇到。

读写冲突

报错信息以及处理建议同乐观锁模式。

TiDB 锁冲突问题处理 - v5.2 | TiDB 归档文档站

看看是否是autocommit,自动提交默认乐观锁。

在事务中,是悲观锁。但是SQL自动提交的话,默认是乐观锁,然后就会产生很多写入冲突?
是这个原因导致的吗?

如果手工开启事务了,应该就不是这个原因。

pessimistic lock retry limit reached

在冲突非常严重的场景下,或者当发生 write conflict 时,乐观事务会直接终止,而悲观事务会尝试用最新数据重试该语句直到没有 write conflict。因为 TiDB 的加锁操作是一个写入操作,且操作过程是先读后写,需要 2 次 RPC。如果在这中间发生了 write conflict,那么会重试。每次重试都会打印日志,不用特别关注。重试次数由 pessimistic-txn.max-retry-count 定义。

可通过查看 TiDB 日志查看报错信息:

悲观事务模式下,如果发生 write conflict,并且重试的次数达到了上限,那么在 TiDB 的日志中会出现含有下述关键字的报错信息。如下:

err="pessimistic lock retry limit reached"

处理建议:

  • 如果上述报错出现的比较频繁,建议从业务的角度进行调整。
  • 如果业务中包含对同一行(同一个 key)的高并发上锁而频繁冲突,可以尝试启用系统变量 tidb_pessimistic_txn_fair_locking。需要注意启用该选项可能对存在锁冲突的事务带来一定程度的吞吐下降(平均延迟上升)的代价。对于新部署的集群,该选项默认启用 (ON) 。

看了下日志,悲观锁机制,因为有try again later。
[session.go:819] [sql] [label=internal] [error=“[kv:9007]Write conflict, txnStartTS=466925186248605706, conflictStartTS=466925186248605702, conflictCommitTS=466925186248605707, key={tableID=54, tableName=mysql.column_stats_usage, handle={24411, 1}}, originalKey=7480000000000000365f72038000000000005f5b038000000000000001, primary={tableID=54, tableName=mysql.column_stats_usage, handle={24411, 1}}, originalPrimaryKey=7480000000000000365f72038000000000005f5b038000000000000001, reason=Optimistic [try again later]”] [txn=“Txn{state=invalid}”]

改为悲观事务试试

更新余额这类强一致性场景改用悲观模式,修改前先加行锁,从底层减少冲突报错。

应该是乐观事务。

这就把我搞糊涂了呀,这是是系统表的重试机制,系统表采用乐观锁?业务表是悲观锁吗?

autocommit 设置为off再观察下看看。

建表用SHARD_ROW_ID_BITS打散写入热点。