发布时间:2024-05-10 19:01
(1)客户端事务开始,TiDB接到begin后,向PD管理层申请一个time stamp (下面简称ts)做为事务起始编号 start_ts .
(2)执行客户端SQL:
1:读请求,则先向PD获取Region的分散存储记录,找到对应的KV节点查询相关数据,同时把该数据结果保存到TiDB内存中,设置版本号为start_ts, 从此该事务session的数据就有了MVCC版本控制
2:写操作,针对当前版本的数据做修改或者新增,都是先更新TiDB内存中的版本数据,同时可以返回内存中的对应版本数据的更新结果给客户端
(3)客户端事务commit提交
(4)TiDB两阶段提交commit开始(这一步其实是确保更新后的数据准确无误落盘)
1:针对内存中那一堆被修改更新了的KV键值对数据,选择第一个做为primary key
2:通过PD定位每一个key对应的region以及该region所属的KV节点
3:把内存中的键值对数据根据region分区信息分类,也就是把对应key写到对应kv节点group匹配上了,方便下一步一次性发起并发预写操作
4:并发将每一对KV数据和start_ts事务编号一起预写到对应的TiKV中,所谓预写,其实也就是保存在KV内存,还没有提交给RocksDB ,每个KV预写成功后返回响应(官网没说这个响应是什么,可能是前面定义的那个primary key或者事务版本ID) 给TiDB,这是第一阶段提交,在这里可能就会发生数据版本信息是否冲突和事务过期,如果是,则进行CAS,所以是一个loop不断尝试写入,直到预写成功才返回响应。
5:在上一步确保都预写完成后,TiDB再次向PD获取一个事务提交时间戳commit ts(全局唯一且递增)
6:收到commit ts 之后,清理预写阶段加的锁(如果有的话),然后并发把第4步预写的KV真正提交给RocksDB,提交完成后,又返回一个响应(这里还是没说清楚响应是什么,但是我猜是前面定义的那个primary key或者事务版本ID),这一步主要作用就是证明自己已经确定写入成功了,当前事务执行成功!
(5)TiDB收到两阶段提交的响应后,返回执行成功信息给客户端
1:原理简单好理解,功能强大
2:基于单实例事务扩展到了跨节点事务
3:锁的粒度细化,而且不一定会加锁,对于冲突小的场景效率更高
1:两阶段提交的设计使得TiKV与TiDB多次网络通讯
2:需要额外的PD服务管理节点信息和分配时间戳事务ID,增加复杂性和部署成本
3:数据量过大会导致内存暴涨
注意:
自 v3.0.8 开始,新创建的 TiDB 集群默认使用悲观事务模型。但如果从 v3.0.7 版本及之前创建的集群升级到 >= v3.0.8 的版本,则不会改变默认的事务模型,即只有新创建的集群才会默认使用悲观事务模型。
跟Mysql一致,对要操作的数据行加悲观锁
注意悲观事务没有改变两阶段提交的本质,而是在进行TiKV预写数据时候通过Pipelined进行悲观锁加锁,等待其他事务操作完成,相比较起乐观事务模式,性能肯定有所降低
Pipelined 加锁流程(官网原文)
加悲观锁需要向 TiKV 写入数据,要经过 Raft 提交并 apply 后才能返回,相比于乐观事务,不可避免的会增加部分延迟。为了降低加锁的开销,TiKV 实现了 pipelined 加锁流程:当数据满足加锁要求时,TiKV 立刻通知 TiDB 执行后面的请求,并异步写入悲观锁,从而降低大部分延迟,显著提升悲观事务的性能。但当 TiKV 出现网络隔离或者节点宕机时,悲观锁异步写入有可能失败,从而产生以下影响:
无法阻塞修改相同数据的其他事务。如果业务逻辑依赖加锁或等锁机制,业务逻辑的正确性将受到影响。
有较低概率导致事务提交失败,但不会影响事务正确性。
如果业务逻辑依赖加锁或等锁机制,或者即使在集群异常情况下也要尽可能保证事务提交的成功率,应关闭 pipelined 加锁功能。
设想我们之前加的悲观锁,是不是获取不到就一直轮询,轮询一段时间后就挂起等待锁的释放,再唤醒,执行完,再返回
Pipelined的加锁流程就是不等,如果获取不到锁,当前TiKV节点就通知TiDB继续写其它KV节点,不要等它,然后再通过异步的方式写入悲观锁,但是会出现悲观锁异步写入失败,影响到业务逻辑的正确性,所以TiDB建议我们如果业务逻辑需要数据准确性和一致性高的场景,要关闭Pipelined加锁。