悲观锁 Fair Locking 客户端的流程原理大概是什么呢

下面是我通过文档大概收集的关于 Fair Locking 的资料与原理:

悲观公平锁优化 Fair Locking

Enhanced pessimistic lock queueing RFC:https://github.com/tikv/rfcs/pull/100/files?short_path=b1bee83#diff-b1bee83cbc9b96a0f2b6ddcd382ac9d1b97f41d2c8aa840bf9043520af3d86bb

如果业务场景存在单点悲观锁冲突频繁的情况,原有的唤醒机制无法保证事务获取锁的时间,造成长尾延迟高,甚至获取锁超时。我们可以想象一下这个场景:

  • 假如目前 TIKV 存在很多悲观事务(t1t2、…)对相同一行数据进行加锁,他们都在一个队列里面等待当前事务 t 的提交

  • 这个时候,t 提交了事务,TIKV 唤醒了其中一个事务 t1 来继续处理,t1 由于发现需要加锁的数据已经被更新,会向客户端 (TIDB) 返回 WriteConflict 来进行重试加锁流程

  • 恰好这个时候,t2 超过了 wake-up-delay-duration 时间被唤醒,也会尝试进行加锁流程

  • t1 由于某些原因,请求到达 TIKV 的时候,t2 已经加锁完毕,因此 t1 整个流程相当于空转

为了解决这个问题,TIKV 对悲观事务进行了一系列优化,我们再重复上述场景:

  • 目前 TIKV 存在很多悲观事务(t1t2、…)对相同一行数据进行加锁,他们都在一个队列里面等待当前事务 t 的提交

  • 这个时候,t 提交了事务,TIKV 会唤醒了***最早请求的 ***事务 t1 来继续处理。与以往不同的是 ,t1 的加锁流程将会 成功,即使发现加锁的数据已经被更新,也不会返回 WriteConflict 错误。但是该成功的请求会携带最新 write 记录的 commit_ts ,用来通知客户端,虽然加锁成功,但是数据其实是有冲突的

  • wake-up-delay-duration 时间将不会起效,因此不会有其他事务突然唤醒来与 t2 事务并发

  • t1 事务加锁成功的结果到达客户端后,由于 commit 数据有变动,客户端可能依旧会使用最新的 ts 进行再次重试

但是对客户端的相关流程不太了解,例如:

客户端在收到 fair locking 成功后,接下来的流程是如何运作的呢?还有就是客户端即使收到 fair locking 成功后依旧会进行重试,这个有没有具体的样例场景来描述一下呢?

1 个赞

建议查阅官方相关的文档,然后结合代码再来理解会好一点…

分布式的锁,本身就很复杂…

以下可以参考:

你好,这些文档我都有参考的,但是的的确确没有 fair locking 的资料。RFC 的资料其实蛮详细的,但是可能缺乏一些样例,所以理解上面有点困难。相对来说 TIKV 上面的代码逻辑比较容易理解,但是客户端方面关于重试和带锁重试方面介绍比较少,tidb 的代码注释不太好理解

嗯,从数据库的角度来说,锁就两种:乐观锁和悲观锁,

你要从系统角度来看这个问题,哪这个锁的种类就多了

  • 公平锁和非公平锁
  • 自旋锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

视角上不一样,所以你想获取的是关于 tidb 的分布式锁机制,还是系统机制?

Enhanced pessimistic lock queueing RFC:https://github.com/tikv/rfcs/pull/100/files?short_path=b1bee83#diff-b1bee83cbc9b96a0f2b6ddcd382ac9d1b97f41d2c8aa840bf9043520af3d86bb

我指的是这个悲观锁的优化

Enhanced Pessimistic Lock Queueing

这标题写的很清楚了,采用队列的方式 ,和公平锁没啥关系…

The basic idea of the optimization is: when a pessimistic lock request is woken up after being blocked by another lock, try to grant it the lock immediately, instead of returning to the client and let the client retry. By this way, we avoids a transaction being woken up failed to get the lock due to the lock being preempted by another transaction.

When a lock is released and a queueing pessimistic lock request is woken up, allow the latter request to resume and continue acquiring the lock (it’s very likely to succeed since we allow locking with conflict).

就是为了减少锁冲突,但是一样会有隐患,好像还没解决办法…

TiDB 悲观锁 Fair Locking 是 TiDB 在实现悲观锁时采用的一种锁策略,通过 Fair Locking 确保了在并发情况下对资源的公平竞争。在客户端的流程原理大致如下:

  1. 客户端请求悲观锁:当客户端需要获取某个资源的悲观锁时,会向 TiDB 发起请求。
  2. TiDB 接收请求:TiDB 接收到客户端的请求后,会根据 Fair Locking 的策略进行处理。
  3. Fair Locking 策略:Fair Locking 确保了不同事务之间对资源的公平竞争。当多个事务请求同一资源的悲观锁时,TiDB 会根据一定的规则来决定哪个事务可以获取到锁,避免某个事务长时间占用锁导致其他事务无法执行的情况。
  4. 获取锁:最终根据 Fair Locking 策略,TiDB 会将锁分配给某个事务,并通知客户端获取到了锁。
  5. 执行操作:客户端在获取到悲观锁之后,可以执行对资源的操作。
  6. 释放锁:当客户端完成对资源的操作后,会释放悲观锁,让其他事务可以继续竞争该资源的锁。

总的来说,TiDB 的 Fair Locking 机制能够确保在并发情况下对资源的公平竞争,避免悲观锁导致的性能问题和竞争不公平的情况。

1 个赞

这个有没有例子呢,例如增删改查涉及的客户端操作:
例如
客户端收到 fair locking 后一定需要重试吗?什么时候需要重试?
还有 fair locking 后什么情况下重试发现可以不需要加锁了而是需要清锁?
这方面的代码比较难看懂,资料还比较少

额 这个就是公平锁,官方后面改了名字,该功能优化叫做 Fair Locking。目标是获取锁类似先入先出,防止大量锁冲突。存储层其实还比较好理解,但是结合 select/update/insert/delete 等语句,客户端如何配合整体流程这里,还缺乏比较具体的认识

分享给你的文档描述【对于分布式锁的机制】得十分详细了…

对于资源的管理,下面这篇文档会更清晰

感谢回复,也谢谢你分享的分布式锁文档。但是目前我并没有看到关于 Fair Locking 比较细节的文档,特别是关于Fair Locking 的设计文档中


这个逻辑的描述

Enhanced pessimistic lock queueing RFC:https://github.com/tikv/rfcs/pull/100/files?short_path=b1bee83#diff-b1bee83cbc9b96a0f2b6ddcd382ac9d1b97f41d2c8aa840bf9043520af3d86bb

我从这个连接里面没有搜到fair,所以我感觉即使有优化,大概率并不叫fair locking。
从这个RFC找到对应issue连接是:

https://github.com/tikv/tikv/issues/13298

还是open的,感觉是大工程,现在都没搞完。

从这个里面又能找到一些client-go相关的issue链接。

https://github.com/tikv/client-go/pull/528/files#diff-47a169599d0edd907d33f40b6bc32efa1615bbb0784d0260ee45b64c29cfa3ad

从这个修改记录里面,能看到这样一段

//
// Aggressive locking refers to the behavior that when a DML in a pessimistic transaction encounters write conflict,
// do not pessimistic-rollback them immediately; instead, keep the already-acquired locks and retry the statement.
// In this way, during retry, if it needs to acquire the same locks that was acquired in the previous execution, the
// lock RPC can be skipped. After finishing the execution, if some of the locks that were acquired in the previous
// execution but not needed in the current retried execution, they will be released.
//
// In aggressive locking state, keys locked by LockKeys will be recorded to a separated buffer. For LockKeys
// invocations that involves only one key, the pessimistic lock request will be performed in ForceLock mode
// (kvrpcpb.PessimisticLockWakeUpMode_WakeUpModeForceLock).
func (txn *KVTxn) StartAggressiveLocking() {
if txn.aggressiveLockingContext != nil {
panic(“Trying to start aggressive locking while it’s already started”)
}
txn.aggressiveLockingContext = &aggressiveLockingContext{
lastRetryUnnecessaryLocks: nil,
currentLockedKeys: make(map[string]tempLockBufferEntry),
startTime: time.Now(),
}
}

所以我感觉这个RFC在tidb的说法可能不叫Fair Locking 而是叫Aggressive locking。
至于你图片里面的那段逻辑我确实没找到。可否告知这个图片的来源是哪里?

1 个赞

https://github.com/tikv/tikv/issues/13298
这个 issue 链接的标题就是 Fair locking,其实就是这个
https://github.com/pingcap/tidb/issues/42107
这个是重命名的 issue
https://docs.pingcap.com/zh/tidb/stable/system-variables#tidb_pessimistic_txn_fair_locking-从-v700-版本开始引入
这个是系统变量的配置,已经改了名字叫 Fair Locking

那个图片是 tidb 配套的设计:
https://github.com/pingcap/tidb/pull/37518/files

1 个赞

我又看了一遍 tidb 配套的设计:
https://github.com/pingcap/tidb/pull/37518/files
感觉大部分看懂了。

但是对这个流程好像还是不太了解:

When a key is locked with conflict, the current statement becomes executing at a different snapshot. However, the statement may have already read data in the expired snapshot (as specified by the for_update_ts of the statement). In this case, we have no choice but retry the current statement with a new for_update_ts .

这里说,由于 sql 语句很可能已经读了旧版本 snapshot 的数据,因此我们需要重试读取最新的 for_update_ts 数据。

可能我对 tidb 的悲观锁加锁的场景了解有限,我理解的场景例如:
select * from t where id=1 for update; (表中不存在唯一索引)
悲观事务中很可能由于其他事务原因导致这个 select 请求阻塞,如果使用 fair locking 的话,tikv 在其他事务 commit 的时候,顺利拿到了 id=1 这个行锁,并且向 tidb 返回了 id=1 的最新数据以及其 commit_ts.
这个流程中好像并不需要重试 select for update 语句了。

insert 语句大概率应该不需要,大部分场景下 insert 应该不需要查询。

那么什么样的 sql 语句,在悲观事务中,即使 fair locking 成功了,也需要再次重试呢?

对于 delete 语句、update 语句,例如 delete from t where k=2; update t set a=3 where k=2;
可能由于阻塞过程中,其他事务提交了 k=2 的新数据,所以 tidb 需要重试,重新查询新的 snapshot 下所有 k=2 的 rowid,然后一一加锁。

对于存在唯一索引的表数据,
select * from t where indexKey=1 for update; 或
select * from t where id=1 for update;
不太清楚是先 seek 行数据再加 index 锁和 rowid 锁,还是先加 index 锁和 rowid 锁再 seek 行数据。
如果是前者的话,那的确需要再重试 seek 一下
如果是后者的话,好像也没必要重试

不知道我自己猜测的场景对不对

1 个赞

你把官方文档关于加锁部分的细节流程图能理解清楚,就能明白了,别猜了

分布式锁的难点,不在于单个节点,是维持多个节点之间的关联状态,
写一条数据,和写一批差距的差异
更新一条数据 ,和更新一批数据的差异(这里还有更多的场景,A 和 B 先后执行,A 和 B 同时执行)
delete数据同时在查询,还有可能同时插入了一条新的数据… 还有可能多人一起在操作…

请指教一下,有没有官方文档写 select for update、delete、update、insert 等语句更新一条数据、更新多条数据的加锁流程呢,最好带有举例的那种。
多谢多谢

实践出真知,搭个环境模拟下就知道了

https://github.com/pingcap/tidb/blob/9ee46d42e5f54d832317c7b0b746be00d9ba540d/pkg/store/driver/txn/txn_driver.go#L389

// StartFairLocking adapts the method signature of KVTxn to satisfy kv.FairLockingController.
// TODO: Update the methods’ signatures in client-go to avoid this adaptor functions.
// TODO: Rename aggressive locking in client-go to fair locking.

现在破案了,tidb里面管这个叫fair locking,但在client-go,也就是负责和kv通信的客户端模块里面,fair locking就是aggressive locking。

是的 后期估计也会改成 Fair Locking 的名字,目前还没改

不奇怪,之前的版本同一个配置名称都会有差异,习惯了… :see_no_evil:

1 个赞