看了一些文档,在乐观事务模型下,读流程大致是这样的:
1.读取lock列,检查目标行在时间戳范围[0, start_ts]是否存在锁。存在,则看是否出现TTL超时,如果TTL超时则发起ROLLBACK回滚掉持有该锁的事务,从而清理掉锁。如果TTL未超时,则说明存在其它事务正占用此行(发生读写冲突),需要等待超时,进行重试或者其它事务主动解锁
2.从write列读取[0, start_ts]范围的最新版本的数据。write列的版本号为commit_ts,因此确保了commit_ts<=读操作的start_ts,然后根据write列的值去data列读取对应版本的数据(TiDB中value小于255字节的数据直接存放在write列,不再需要去data列查询数据)
那么在悲观事务模型中,DML语句执行完就在lock列里面写入锁信息了,而不是在prewrite阶段,所以在事务提交之前,读请求不可能在检查lock列时去等锁,那这里的具体流程是怎么样的?
xfworld
(魔幻之翼)
2022 年6 月 13 日 07:41
2
lock 是针对 insert 和 update,delete,需要对数据判断是否有 lock,否则无法操作
read 则不同,因为 read 会从PD 获取 TSO,假定为 cur_TSO, 通过 region Cache 获取到位置后,直接去region 去读取 ,首先会经过blockCache,如果没读到,则会从Immutable Memtable 中读取
读取的规则也很简单
Same Key : cur_TSO > max(TSO)
希望对你有锁帮助…
没你说的那么简单,读操作也是要判断锁的,它需要确保自己能读到所有commit_ts小于自己start_ts的全部变更,否则就破坏了线性一致性
xfworld
(魔幻之翼)
2022 年6 月 13 日 09:24
4
简单的概念就是这样子了,如果想要更深入,就要看代码拉
多版本并发控制(Multi-Version Concurrency Control,以下简称 MVCC) 以一种优雅的方式来解决这个问题。在 MVCC 中,每当想要更改或者删除某个数据对象时,DBMS 不会在原地去删除或这修改这个已有的数据对象本身,而是创建一个该数据对象的新的版本,这样的话同时并发的读取操作仍旧可以读取老版本的数据,而写操作就可以同时进行。这个模式的好处在于,可以让读取操作不再阻塞,事实上根本就不需要锁。这是一种非常诱人的特型,以至于在很多主流的数据库中都采用了 MVCC 的实现,比如说 PostgreSQL,Oracle,Microsoft SQL Server 等。
参考这个:
在一篇文章中有看到过能保证线性一致性的实现,不确定是否是最新的。
写事务的 ts 必须大于所有与之相交的读事务,在实现中会让读事务推高 key 上的 min_commit_ts = read_ts + 1,在提交时候需要计算 commit_ts = max{commit_ts, min_commit_ts}
https://pingcap.com/zh/blog/transaction-frontiers-research-article-talk4 (从图8开始看起)
这个 min_commit_ts
似乎还同时用于防止大事务阻塞其他读事务
For the problem of blocking other transactions, we must introduce a new concept – the min_commit_ts
, which is the minimum time at which the transaction can be committed. We start a transaction with min_commit_ts = start_ts + 1
. When a transaction is committed, if the commit_ts
is smaller than the min_commit_ts
, then an error is triggered and sent to TiDB. TiDB can then retry committing with a later timestamp.
The clever part happens when another transaction (let’s call it txn B) is blocked from reading by the large transaction (txn A). In this case, rather than txn B being blocked, it will update the min_commit_ts
of txn A’s lock, setting it to the start_ts
of txn B + 1
. (We can’t do this for writes, but that is not too bad since we would always expect writes from one transaction to block writes from another). This is a CheckTxnStatus
request, but TiKV adds it to its work queue, directly rather than sending it. The min_commit_ts
can also be updated by TiDB sending an explicit CheckTxnStatus
request.
https://en.pingcap.com/blog/large-transactions-in-tidb/
感谢回复,min_commit_ts之前在Async Commit 原理介绍丨 TiDB 5.0 新特性 里看到过,主要是为了保证SI隔离级别。你发的有篇文章中提到min_commit_ts能防止大事务阻塞读,确实更clever一些,通过推高写事务的,也能解决一致性问题。但是这种处理方式和读写冲突 中提到的解决方案是冲突的,这篇文档中的意思是,我读就在那里等着,反复backoff,直到自己超时或者写COMMT完成再去读
所以这两篇文档提到的方案到底是什么关系?
不确定我的理解是否正确,感觉读写冲突 指的是当前读(有锁)和写事务的冲突,而快照读无锁,所以应该不会触发Backoff。
我找到了一个相关的Bug,TiDB#21658 ,看起来预期表现是快照读不被写事务阻塞,通过比较min_commit_ts来绕过写事务的锁。
另一方面,悲观事务DML阶段就加锁了,如果悲观事务的快照读都会被写事务的锁阻塞,所有的读写场景就都是串行执行了,会不会很影响性能。
读写冲突 指的应该不是当前读 ,否则在乐观事务的prewrite阶段就锁冲突而cancel了,悲观事务直接在执行SQL语句的时候就阻塞了,到不了commit这里来
这个帖子的提问的原因就是,我如果按照读写冲突 中的过程来理解悲观事务,那读请求是有可能被长时间阻塞的,不符合预期(乐观事务的锁是prewrite阶段写进去的,阻塞一下也不会太久),所以没想明白
嗯, 你说得对,读写冲突 指的应该是快照读。我又胡乱看了好一些文章,大概有一个猜想,不一定对:joy:
以下均为悲观事务:
DML阶段加的锁不会阻塞快照读,因为此时还没有commit_ts
,没必要阻塞 ,原因暂时没想清楚。这个锁只是一个占位符,和Prewrite阶段的锁不一样,出处:
TiDB 的悲观锁实现的原理确实如此,在一个事务执行 DML (UPDATE/DELETE) 的过程中,TiDB 不仅会将需要修改的行在本地缓存,同时还会对这些行直接上悲观锁,这里的悲观锁的格式和乐观事务中的锁几乎一致,但是锁的内容是空的,只是一个占位符,待到 Commit 的时候,直接将这些悲观锁改写成标准的 Percolator 模型的锁,后续流程和原来保持一致即可,唯一的改动是:
对于读请求,遇到这类悲观锁的时候,不用像乐观事务那样等待解锁,可以直接返回最新的数据即可(至于为什么,读者可以仔细想想)。
TiDB 新特性漫谈:悲观事务
Prewrite 阶段,
2.1. 写事务会根据现有的 max_ts
,计算 min_commit_ts
2.2. 写事务会加个自己的 start_ts
的锁
2.3. 快照读分两种情况
2.3.1. 快照读在 Prewrite 该 Key 前就读取了该 Key,会把该 Key 的 max_ts
推高至快照读的 start_ts
,如果下一次继续读这个 Key,判断 min_commit_ts
是否大于快照读的 start_ts
,若是,则锁 Bypass
2.3.2. 快照读在 Prewrite 该 Key 前没有读取该 Key,且 start_ts
大于 min_commit_ts
,会被锁住,这应该就是 读写冲突 里的 Backoff 重试
考虑以下三个事务:
T1
T2
T3
Lock
Begin(Start TS = 1)
Prewrite(x)
Begin(Start TS = 5)
Lock(x, Start TS = 1)
Read(y) => Max TS(y) = 5
Prewrite(y) => Min Commit TS(y) = 6
Lock(y, Start TS = 1)
Read(y) => Ok
Begin(Start TS = 7)
Min Commit TS(y) > Start TS(T2) => 锁Bypass
Read(y) => Block
Min Commit TS(y) < Start TS(T3) => 锁阻塞
COMMIT
参考:Async Commit 原理介绍丨 TiDB 5.0 新特性
你的意思是,写事务确定好min_commit_ts
后,如果读事务是在该时间戳前Begin的,就不会被阻塞,否则就走Backoff?
system
(system)
关闭
2022 年10 月 31 日 19:19
12
此话题已在最后回复的 1 分钟后被自动关闭。不再允许新回复。