TIDB的事务流程
1、客户端发起一个begin到TIDB中
2、TIDB向PD获取一个start_ts,用来记录该事务的开始时间戳
3、进入SQL执行部分,TIDB会向PD获取对应数据的路由信息,region的具体信息都是保存在PD中的。
4、TIDB根据上一步获取的路由信息去对应的TIKV中获取数据,获取数据的时候会根据start_ts去获取对应版本的数据(commit_ts<start_ts)
5、TIKV将结果恢复给客户端
6、客户端对数据进行写入,进行数据校验,比如检查唯一性、检查数据类型等,但是因为是悲观锁,不会进行写写冲突的校验。
7、用户发起commit,进入commit阶段
8、在所有的写入中随机选择一个写入作为primary,其它的作为second。
9、TIDB从PD中获取对应的写入路由,也就是需要知道这些数据都是存储在哪些TIKV中,然后对写入进行分组
10、TIDB发起prewrite,因为prewrite是需要对所有的写入都进行prewrite,也就意味着TIDB需要同涉及到的所有TIKV都进行交互。prewrite的主要工作就是对数据和锁信息的写入,在检查[start_ts,max)这段期间没有数据冲突也就意味这没有写写的冲突,接着对数据进行写入,加锁等操作
11、在所有的prewrite都成功之后就开始进行commit阶段,在commit阶段主要工作就是写入版本号(write列)和清除锁信息。TIDB会再次从PD中获取一个commit_ts,检查锁的状态是否正常有没有被清理,然后写入commit_ts,清除锁信息。在commit_ts被写入之后,这个写入就被其它会话可见了。注意,只需要最primary进行commit,就可以返回成功,其它的都是做异步提交的。
优点缺点分析
问题
根据TIDB的事务流程可以发现在写入过程中会造成内存和网络压力。
从内存上面来说,因为写入都是在内存中的,如果写入量太大就可能会造成内容使用率的上升。
对网络的开销也不小,从图中来看,TIDB每次指向PD、TIKV都意味这一次网络开销,比如TIDB向PD获取版本信息,prewrite时候TIDB向TIKV的交换,甚至因为TIVK底层的raft强一致性策略(必须半数以上的peer完成写入)都会造成网络的开销。
解决
根据上面提到的问题,主要是网络和内存的开销。那么在TIDB的文章就就提到了两种策略,一个是对小的事务做打包处理,这个很容易理解,如果启用autocommit,那么每一条写入都会发生上述的全流程,打包处理也就意味着网络开销可以大大的减少。
另外,大家都知道对对内存还有压力,小事务打包变成大事务减少了网络的开销,但是会增加内存的开销。为了防止内存过大造成的问题,TIDB对事务的大小进行了限制,主要有:
- 单个事务包含的 SQL 语句不超过 5000 条(默认)
- 每个键值对不超过 6MB
- 键值对的总数不超过 300,000、。
- 键值对的总大小不超过 100MB
事务冲突行为
在TiDB的乐观锁机制中是在提交的时候在去检查是否存在事务冲突的,所以必定存在冲突的问题,冲突主要有读写冲突和写写冲突。重点看写写冲突
| 时间 | 事务A | 事务B |
|---|---|---|
| T1 | begin | |
| T2 | begin | |
| T3 | update tab set a=1 where id=1 | |
| T4 | update tab set a=0 where id=1 | |
| T5 | commit | |
| t6 | commit;收到报错 |
这种情况在Mysql是不会出现的,因为Mysql使用的是悲观锁机制。但是在TIDB中的乐观锁机制就可能出现
- 事务A在T1的时候发起,事务B在T2(落后于T1)的时候发起,然后去更新同一条数据
- 在T5的时间点事务B做了提交,也就意味这在这一行的commit_ts写记录的就是T5的时间点
- 在T6的时间点事务A做提交,在prewrite阶段校验检查的时候发现[start_ts,max)之间已经有其它事务对这条记录进行了修改,所以这次提交会报错!
重试
在TIDB中可以通过tidb_disable_txn_auto_retry参数去设置事务重试,重试的过程如下:
- 重新获取start_ts
- 对写入的sql进行重放
- commit
很显然,重试的存在可能会导致更新丢失的问题,而且由于只是对写入的语句进行了重试意味着对于写入需要依赖于读取的业务就不适合开启事务的重试功能。
参考
备注:本文主要是用来记录自己学习Pingcap博客(pingcap.com/blog-cn/)的学习笔记,内容基本上和原文差不多,技术细节建议查看原文