事务系统实现模式很简单?你确定没忽视这些差异?(2)
回到一开始的第一种方案,在一个节点实现了KV、Raft、Lock Table、Transaction Manager,看起来耦合度比较大了,我们能不能对其进行分层,进一步简化呢?例如Google的经典做法,基于GFS实现Bigtable,基于Bigtable实现Percolator,Layered设计易于迭代、易于开发、易于调试。 因此我们可以考虑把KV层单独抽离出来,基于KV去实现Lock Table、Txn Manager:
看过Percolator、TiKV设计的应该会比较熟悉,它们就是基于一个高可用的KV,把事务的状态都下沉到KV中。这种设计很容易拓展到分布式事务的场景,如果KV能够scale,那么上层的事务也能够scale了。 5、基于单机事务引擎实现高可用事务 上面的方案看起来都比较简单,不过有一个细节不容忽视:锁基本都是在复制协议提交之后才会释放,换句话说事务持有的锁会从事务开始直到多个节点写完日志,经历多次网络延迟、IO延迟,并且在拥塞情况下会面临排队延迟的风险。而锁意味着互斥,互斥意味着事务吞吐降低。 翻译一下:
不过这里存在一个问题:
暂且不做回答,我们再看最后一种方案,基于单机事务引擎的高可用事务。 在正常的单机事务流程中,增加一个复制的环节:本地事务提交之后不是立即返回用户,而是写binlog,等待binlog复制到其他节点之后再返回用户。 这种方式的事务延迟,看起来还是本地事务的延迟,加上复制日志的延迟;但相比于之前的方案,本地事务可以先提交,锁可以提交释放,总体的事务吞吐相比之下会有所提升。 看起来甚至比之前的方案更加简单,事务和复制模块得到了完美的分离,但这里忽略了一个复杂的问题:
由于直接复制Journal会引起一系列复杂的耦合问题,大部分数据库都选择单独写一个binlog/oplog来实现复制,不过在实现时可以做优化,因为如果真的写两个log会有原子性的问题(一个写成功了另一个没写成功)以及IO放大的问题。 这里的设计空间比较庞大,不做详细讨论,仅仅考虑在简化的模型下复制顺序的问题。 对于并发执行的事务,为了确定复制顺序,这里维护一个称之为OpTime的自增ID。后续的复制会按照OpTime的顺序,OpTime小的先复制。如果OpTime仅仅是在事务的开始和结束之间分配,会带来问题:
因此,OpTime的分配需要有更强的限制:对于并发且有冲突的事务,OpTime的顺序要和事务的Serialization Order一样: 在S2PL的场景中,我们把OpTime分配放到Lock之后Commit之前,即可满足这个要求。因为按照S2PL的调度,事务的Commit-Point就是Lock完成和Unlock之间。对照上面的例子,事务T2的OpTime被推迟到T1之后,复制的顺序也会相应改变,不会发生先前的异常了。 推广到其他的并发控制方法也是类似,例如上面的Snapshot Isolation。提交之前会检查[begin, end]是否有冲突,有冲突直接重启事务。相当于在[begin, end]区间内分配OpTime即可。 这种方法通过OpTime,保留了Transaction Serialization Order和RSM的Order之间的关系:
不过这里留下了一个问题,留待读者思考: 如何按照OpTime复制,因为有事务Abort的情况,OpTime做不到连续自增,仅仅是单调自增。 二、对比 第一种其实是Spanner,第二种是TiKV、Percolator,第三种是MySQL、MongoDB。 (编辑:ASP站长网) |