当前位置: 首页 > 科技观察

浅谈TiDB的分布式事务模型

时间:2023-03-18 23:49:52 科技观察

本文转载自微信公众号《程序员jinjunzhu》,作者jinjunzhu。转载本文请联系程序员jinjunzhu公众号。在传统关系型数据库领域,我们往往通过配置事务的隔离级别来解决脏读、幻读、不可重复读等问题。不同的事务隔离级别对应解决问题的不同强度。下表展示了不同事务隔离级别对脏读、幻读和不可重复读的容忍度。一起来看看:注意:Repeatableread的读锁会一直到事务结束才会释放;Readcommitted读锁不会等到事务结束,而是在读完成后立即释放。当然,传统数据库中解决并发控制的手段还有mvcc,这里不展开。上面我提到了读锁、写锁和GAP锁。事实上,锁的种类远不止这些。对于我们开发者来说,经常会谈到乐观锁和悲观锁。乐观锁其实是不加锁的,悲观锁是需要真正加锁的。在分布式数据库领域,同样需要并发控制,也有乐观事务和悲观事务之分。就TiDB而言,v3.0版本开始支持悲观事务。从v3.0.8开始,新建的TiDB集群默认使用悲观事务。传统数据库加锁传统数据库的乐观锁主要是在表中增加一个版本号字段,更新时根据更新结果判断是否成功。比如我们有一个表table_a,我们在里面添加了一个version字段,下面是table_a表的一条记录tableidnameversion1jinjunzhu4我们用id=1更新这条记录,SQL如下:updatetable_asetname='xiaoming',version=version+1whereid=1andversion=4此时,如果SQL执行结果返回的更新行数为0,说明其他事务更新了version字段,发生写冲突,业务代码必须处理这种冲突。在高并发下,如果对同一条记录有很多修改操作,势必会造成大量的写失败。所以乐观锁更适合读多写少的场景。传统数据库的悲观锁使用的是物理锁,还是上面的表,不需要version字段。如果有2条记录:idname1jinjunzhu2xiaoming这时候我们需要更新id=1的记录,如果id=2的记录在一个事务中完成,加锁sql如下:select*fromtable_awhereidin(1,2)forupdate;updatetable_asetname='zhangsan'whereid=1;updatetable_asetname='lisi'whereid=2;悲观锁的问题在长事务的情况下,其他事务需要很长时间等待锁,所以Oracle提供了如下优化,即在发现要修改的数据被锁定后立即返回失败:select*fromtable_aforupdatenowaitPercolator模型Percolator模型是由Google提出并建立在BigTable上的分布式事务解决方案。谷歌的论文如下,文章链接可以在扩展阅读[1]中找到:《Large-scaleIncrementalProcessingUsingDistributedTransactionsandNotifications》我们以经典的电商系统为例。如果系统中有订单、账户、库存三张表,用户需要为一次购买添加一条订单记录。account表需要扣金额,inventory表需要扣库存,这三个表要操作的记录分别在分布式数据库的三个分片上。这时候就需要处理分布式事务了。我们来看看Percolator算法模型:初始阶段初始阶段,我们假设订单表记录订单数量为0,账户表记录账户金额为1000,库存表记录产品数量为100.客户下单后,order表加1一个订单,account表减100,inventory表减1。各表的初始数据如下:上表中,“:”前面是时间戳所代表的数据版本,后面是数据值。第一列是表名,第二列低版本保存数据,第三列保存事务操作给数据加的锁。第四列高版本存放的是指向所存数据版本的指针,例如版本6存放的是指向版本5的指针,6:data@5。Prewrite阶段在Prewrite阶段,协调节点向每个分片发送Prewrite命令。Percolator定义了primarylock的概念,即主锁。在Prewrite阶段,每个分布式事务中只有一个待修改的数据行可以获得主锁。这种情况下,如果order表获得了主锁,其他表的锁都指向这个主锁的指针称为从锁,如下表所示:在Prewrite阶段,每个数据行要modified会被写入日志,交易的私密版本会根据时间戳记录。这里的私有版本是7,这样其他交易就无法操作这三条数据。注意,在获取主锁时,如果出现以下情况,则锁失败:1.其他事务已经被加锁;2、事务开始后,待更新的数据被其他数据更新。提交阶段在提交阶段,协调节点只需要和拥有主锁的分片通信,所以在这种情况下,它只需要和订单表所在的分片通信。此时数据如下:我们注意到订单表的锁没有了,增加版本8指向版本7,说明订单表已经提交成功,没有私有版本,但帐户表和库存表的私有版本仍然存在。这是因为Percolator模型并没有同步commitaccount表和inventory表,而是启动了一个异步线程来commit这两张表并清除锁。如果订单表提交失败,account表和inventory表也需要回滚。提交成功后,最终数据如下:commit阶段,因为协调节点只需要和有主锁的分片通信(这里是订单表所在的分片),保证了原子性,从而避免所有节点在提交时无法成功。数据不一致问题。Prewrite阶段记录日志和私有版本。如果account表和inventory表所在分片commit失败,可以根据log重新commit,保证了数据的最终一致性。这里有两点需要注意:1、主锁的选择是随机的。比如本例中不一定要选择order表;2、协调节点发送commit后,订单表先提交成功。这时候如果其他事务要读取accountservice和inventoryservice的两条数据,虽然这两条数据还是有锁的,但是查找primary@order.bal后发现,已提交,可以阅读。但是在阅读的时候需要做一些二次锁的清理。TiDB乐观事务模型上面我们分析了Percolator模型,TiDB的乐观事务使用的是Percolator模型。TiDB支持MVCC。当事务启动时,它将使用一个时间戳start_ts作为当前事务ID和MVCC的快照版本。后续的读请求会读取当前快照版本下的数据。数据校验成功后,客户端会进行两个commit阶段,我们看下面的时序图:第一阶段,TiDB收到客户端请求后,会先从缓存的key中找到第一个预写请求待修改,给该key加主锁后返回成功。然后TiDB会向本次事务中的所有其他key发送预写请求,这些key加二级锁后会返回成功。第二阶段,预写成功后,TiDB首先从PD获取时间戳作为当前事务的commit_ts,然后向主锁键发送提交请求。主锁key提交数据成功后,清除主锁,返回success。TiDB收到主锁key的成功信息后,返回success给客户端。乐观事务的冲突检测主要在预写阶段。如果检测到当前键已被锁定,则会有等待时间。如果过了这个时间还没有获取到锁,就会返回失败。因此,当多个事务修改同一个key时,必然会导致大量的锁冲突。注意:TiDB也有重试机制,默认关闭。TiDB的retry会重新获取start_ts,但不会重新读取数据,因此无法保证可重复读的隔离级别。具体可以参考TiDB官方文档。TiDB悲观事务模型TiDB从v3.0开始就引入了悲观事务。注意:v3.0.7及之前版本创建的集群升级到更高版本后,仍然默认使用乐观事务,只有新创建的集群默认使用悲观事务。我们也可以使用以下命令来启动悲观事务。下面第一条语句会修改TiDB系统参数,后面两条语句会忽略系统参数,优先级更高:SETGLOBALtidb_txn_mode='pessimistic';开始悲观;开始乐观;为了兼容mysql,TiDB的悲观事务与mysql非常相似。相似的。悲观事务支持两种隔离级别,repeatableread和readcommitted,默认使用repeatableread。乐观事务和悲观事务在TiDB中是可以共存的,会优先使用乐观事务,只有在发生锁冲突时才会使用悲观事务。使用悲观事务的语句如下:UPDATE、DELETE、INSERT、SELECTFORUPDATETiDB中的悲观事务需要注意几点:SELECTFORUPDATE语句会对最新提交的数据而不是修改的行加悲观锁TiDB不支持GAPlocks,所以当FORUPDATE语句的WHERE条件使用范围条件时,还是可以插入的。比如下面的sql中id不冲突,还是可以插入成功的:SELECT*FROMt1WHEREidBETWEEN1AND10FORUPDATE;可以通过innodb_lock_wait_timeout变量设置等待锁超时时间,默认为50s不支持FORUPDATENOWAIT语法。如果PointGet和BatchPointGet运算符不读取数据,它们仍将锁定给定的主键或唯一键,阻止其他事务锁定或写入同一主键。Pessimistic在事务执行过程中,如果执行了DDL操作,可以成功,但之后事务会失败。悲观事务的执行时间是有上限的,默认是10分钟,业务场景的复杂程度可以通过参数配置来概括,这必然会导致乐观事务的冲突。这也是TiDB后续版本转向悲观事务的重要原因。乐观事务和悲观事务在TiDB中是可以共存的。进一步阅读:[1].https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdf[2].https://docs.pingcap.com/zh/tidb/stable/pessimistic-transaction[3].https://docs.pingcap.com/zh/tidb/stable/optimistic-transaction[4].https://pingcap.com/blog-cn/percolator-and-TX/