目前大部分数据库的隔离级别都是通过 snapshot Isolation 的方式来完成的,主要解决的问题
1 读写能并发,一份数据多个版本,不在单纯用锁来控制数据
2 每个事务对行的操作都是一个快照
3 写操作不能并发,读操作可以并发
4 写操作以获得锁先来先得的方式,遵循先来后到,如遇到冲突后面的事务要么等待,要么取消。
snapshot Isolation的方式对性能是有利的,同时产生了另外一个问题写偏序。采用这样方式的分布式数据库除了会产生与传统数据库在不同隔离级别中同样的 ,脏读,不可重复读,幻读等问题
还会产生 丢失更新, 读倾斜,写倾斜,那什么是丢失更新, 读倾斜,写倾斜
1 丢失更新
在多进程访问中,进程读取基数后,每个进程对于数据进行处理如果没有锁的介入,则会导致在每个进程处理同一行数据中产生写丢失的问题,如上图进程1 和 2 都读取了基数 100 然后,每个进程在不同的时间对数据进行处理,最后一个提交的进程的数据忽略了中间其他进程对数据的修改,造成数据的不一致,也就是写丢失,所以snapshot isolation 也是必须要配合锁的使用,避免 lost update 的发生。
2 读偏序和写偏序
读偏序和写偏序的问题主要集中在下面的图中, 由于不同进程在读取数据的同时,对其他数据进行了修改,而导致最终计算的结果与读取和修改数据的顺序产生了问题。
所以 snapshot isolation 中的事务操作会引出commited时是需要时间戳的。
1 事务的读操作时进行snapshot 读取的数据时的时间戳,是最近一次committed,之后的操作时间记录为 StartTimeStamp
2 在事务提交时或许一个 commitTimeStamp, 在准备提交时,进行事务的冲突检查,将 startTimestamp 和 commitTimestamp 作为一个时间段,此时如果在这个时间段没有与自己有交集的写操作,则事务可以提交,否则事务不能提交,这就解决了最上面的问题 lost update 更新丢失的问题。
所以单机基于SI 方式的事务控制机制,都是使用时间戳来控制事务的写或阻塞,或者committed. 目前大部分数据库都是基于SI 隔离 + 锁的方式来完成事务的MVCC 以及事务的并发的。
在明白单机模式下的SI 隔离界别的控制后, 分布式数据库本身的核心也是事务的控制,解决方案目前有两种
1 全局发号器 ,类似于POSTGRES-XL 中的GTM
2 时钟,无论是使用物理还是逻辑时钟,TIDB 的事务控制是基于时钟的
但这里面都包含一个统一的规则就是递增,无论在单机还是分布式数据库,最终的提交都会牵扯到顺序,没有顺序的则就会引起上面的所描述的事务提交的问题。所以分布式事务核心可以用两个字来表达 顺序。
为了满足顺序的需求,这里主要两个方案
1 全局发号器
全局发好奇方案,通过全局授时服务器来生成严格单调递增的时间戳,所有的分布式事务的时间戳都来自于一个全局的授时服务器, 所有的分布式事务都有严格的先后顺序,根据这样的顺序,基于 SI 隔离级别的事务就可以在分布式数据库中进行工作了。 大部分的分布式系统采用了 TSO 方式 timestamp oracle 的方式来进行整体事务的严格的快照隔离级别的实现。
此方案的问题也很明显,热点的问题,所有的事务都要获得数据必须从TSO 节点来获取时间戳,这样的解决方案的除了热点的问题,还有关于单点故障的问题,大部分分布式数据库都采用了基于 raft 或者 paxos 协议的TSO方式。产生的另一个问题是,如果基于 TSO 的 节点进行切换的过程中,整体业务是无法继续的,同时对于跨机房分布式数据库的工作也是有难度的,对于网络的延迟会非常的敏感。
另外获取时间需要对TSO 节点进行频繁的交互,相关的网络延迟以及性能问题是很难别逾越的问题。其中频繁交互的问题主要是基于每个事务至少需要和TSO 两次互访,获取开始的startTs 与 CommitTs ,基于这个问题部分的TSO 节点都是分配一组的连续型的时间戳,而为了提高性能在获取时间戳的时候也会批量的获取减少TSO的压力。
另一种的方案就是统一时钟的方式,也就是所有的节点的时间都是一致的情况下,来保证事务的有序性,Google 的 truetime就是基于这样的思想提出的方案,通过原子时钟让所有的节点都能保持同样的时间,最低的不一致在 7ms。但由于整体的消耗和关于方案的实现难度, 目前大部分方式还是基于TSO的方式来为事务进行时间戳的分配。
基于上面的文字,分布式数据库,或者数据库基于SI 隔离级别的实现,必然会牵扯到事务的顺序性,通过保证事务的顺序性来完成基于数据库最基本的ACID的需求,所以时间或者说顺序是分布式数据库中完成数据库事务的核心之一。