0
2
2
0
专栏/.../

TiDB锁行为分析

 大鱼海棠  发表于  2024-06-17

TiDB乐观事务实现原理

参考官网文档:https://docs.pingcap.com/zh/tidb/stable/optimistic-transaction

  1. TiDB加锁非原子操作,先读取,后加锁,可能存在写写冲突

其他数据库一般是在读的时候直接加锁,然后再修改。TiDB 在执行完 DML 后才能得到会修改的 key,这时才会加锁

TiDB悲观事务实现原理

参考官网文档: https://docs.pingcap.com/zh/tidb/stable/pessimistic-transaction

  1. 部分场景下,更新前后未变化的key不进行加锁,系统变量
  2. TiDB尽量按照FIFO方式对解锁等待的事务唤醒,MySQL 8.0 按照SATS(Contention-Aware Transaction Scheduling)模式唤醒
悲观锁不阻塞读,因为遇到悲观锁,说明事务一定没有获取到commit_ts

TiDB的加锁、清锁、锁续期

等锁

悲观事务遇到锁时要等待锁释放,为了高效的实现锁等待,每个 TiKV 都有 Waiter Manager。当 Acquire Pessimistic Lock 遇到 KeyIsLocked 时,会在 Waiter Manager 里等锁释放(commit/rollback)。

单次等锁有超时时间:

  1. 为了支持 innodb_lock_wait_timeout,TiDB 会传入 wait timeout。

  2. TiKV 有 wait-for-lock-timeout 配置,为默认和最大的单次等锁超时时间,原因是:

    1. 若是其他事务遗留下来的锁,需要 TiDB Resolve Lock 才能清理。
    2. 若 lock 所在的 region leader 切换到其他 TiKV 上,在之前 leader 等锁的事务不会被唤醒。

v7.0开始使用加强悲观锁唤醒模型 tidb_pessimistic_txn_fair_locking

加锁

在prewrite 阶段的 primary key 加锁(悲观事务此时唤醒 ttl manager 开始续期,乐观事务大于32M此时开始唤醒 ttl manager 开始续期)

悲观锁会写到tikv,要走raft消息同步流程

悲观事务TiDB 收到来自客户端的更新数据的请求会加锁,此时的锁为悲观锁,与prewrite阶段加的锁类型不一样

唤醒

若多个事务等待相同锁,会先唤醒 start ts 最小的事务,其他事务会在 wake-up-delay-duration 后同时唤醒;若该锁再被释放,会唤醒 start ts 第二小的,其他事务再往后推 wake-up-delay-duration,以此类推。为了防止一直被推导致饥饿或其他问题,单次等锁时间最大仍是 wait-for-lock-timeout。

清锁

为了防止挂掉的事务写下的或者 Commit 没清理掉的 lock 一直阻塞后面事务的执行,每个 lock 都有 TTL,当事务遇到锁时会等待锁被清理或者等锁过期执行 Resolve Lock。

Resolve Lock 的流程是:查看 Primary Lock 的状态,若是已提交则把遇到的 lock 提交;若是未提交或者不存在则写入 Rollback 记录,再把遇到的 lock Rollback。

清锁只有tidb Resolve Lock 才能清理,异常状态下,比如tidb_server断连,前序事务会遗留之前加的锁,在等待 wait-for-lock-timeout 后tidb 发起 resolve lock 清理残留的锁(因为前序事务已经不会自己处理 commit 或者 rollback)

TTL manager

加锁后,TTL manager对锁的生命周期进行管理,主要以txn heartbeat的方式

  1. lockTTL最小3s,最大20s,其余情况根据参数设置及事务大小进行计算,最终值会加上事务查询时间作为浮动
ManagedLockTTL     uint64 = 20000               // 20s
MaxPipelinedTxnTTL uint64 = 24 * 60 * 60 * 1000 // 24h
const bytesPerMiB = 1024 * 1024
// ttl = ttlFactor * sqrt(writeSizeInMiB)
var ttlFactor = 6000
var defaultLockTTL uint64 = 3000
const DefTxnCommitBatchSize uint64 = 16 * 1024

func txnLockTTL(startTime time.Time, txnSize int) uint64 {
        // Increase lockTTL for large transactions.
        // The formula is `ttl = ttlFactor * sqrt(sizeInMiB)`.
        // When writeSize is less than 256KB, the base ttl is defaultTTL (3s);
        // When writeSize is 1MiB, 4MiB, or 10MiB, ttl is 6s, 12s, 20s correspondingly;
        lockTTL := defaultLockTTL
        if txnSize >= int(kv.TxnCommitBatchSize.Load()) {
                sizeMiB := float64(txnSize) / bytesPerMiB
                lockTTL = uint64(float64(ttlFactor) * math.Sqrt(sizeMiB))
                if lockTTL < defaultLockTTL {
                        lockTTL = defaultLockTTL
                }
                if lockTTL > ManagedLockTTL {
                        lockTTL = ManagedLockTTL
                }
        }

        // Increase lockTTL by the transaction's read time.
        // When resolving a lock, we compare current ts and startTS+lockTTL to decide whether to clean up. If a txn
        // takes a long time to read, increasing its TTL will help to prevent it from been aborted soon after prewrite.
        elapsed := time.Since(startTime) / time.Millisecond
        return lockTTL + uint64(elapsed)
}
  1. 每10s进行一次txn heartbeat,按照lockttl对事务进行续期,并打印newttl
const keepAliveMaxBackoff = 20000
const pessimisticLockMaxBackoff = 20000
const maxConsecutiveFailure = 10
# ttl-refreshed-txn-size decides whether a transaction should update its lock TTL.
# If the size(in byte) of a transaction is large than `ttl-refreshed-txn-size`, it update the lock TTL during the 2PC.
ttl-refreshed-txn-size = 33554432

func keepAlive(
...
        // Ticker is set to 1/2 of the ManagedLockTTL.
        ticker := time.NewTicker(time.Duration(atomic.LoadUint64(&ManagedLockTTL)) * time.Millisecond / 2)
uptime := uint64(oracle.ExtractPhysical(now) - oracle.ExtractPhysical(c.startTS))
                        maxTtl := config.GetGlobalConfig().MaxTxnTTL
                        if isPipelinedTxn {
                                maxTtl = max(maxTtl, MaxPipelinedTxnTTL)
                        }
 newTTL := uptime + atomic.LoadUint64(&ManagedLockTTL)                       

对于悲观事务,加悲观锁的时候就会启动 ttl 续约直到 txn 结束,和事务大小无关。

乐观事务较小不会启动 ttl,由ttl-refreshed-txn-size配置控制,默认 32MB,以该大小作为下限启动TTL

0
2
2
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论