事务理解

Published: by Creative Commons Licence

  • Tags:

单机事务

实现方案

假设我们实现了一个存储服务,其仅存储最简单的字符串键值对。该服务仅支持get和put操作,前者读取关键字对应的值,而后者将键和值绑定并存储在我们的服务中。

现在我们考虑在我们的存储服务上实现事务功能。

为了简单起见,我们假设该存储服务仅一条服务线程。

在实现事务之前,我们先讨论如何在宕机后能恢复数据。只需要每次写操作我们都将写命令写入到日志文件中(命令模式),每次写出都必须确保确实成功(务必将写出数据入盘)。为了避免一些操作系统没有提供这样的原语保障原子性写入(即可能仅写入一部分命令),我们可以在每条命令的开始处插入一条命令的强摘要(MD5,SHA-1等)。之后我们每次读取命令后都需要比较是否和摘要一致,如果不一致,则表示数据有误,可以丢弃该条命令。

为了避免坏盘或者灾害,可以通过多地区副本的机制实现,这里不做探讨。

这样我们就实现了宕机恢复的方式,下面我们实现单机事务。

当我们开启事务时,首先修改内存,之后将修改作为命令写入到日志中。宕机恢复时,我们只需要确定一个事务是否有对应的commit命令,如果有就执行整个命令,否则,跳过事务中的命令。

这样事务也实现了。

优化日志文件

使用上面的方式实现存储服务,很容易发现,日志文件会越来越大。我们可以用一些策略来减小日志文件,比如日志文件中会包含很多对相同键的写入,很显然只有最后一次写入是有效的,我们只需要在(日志文件大小/键数目)较大时,选择整理日志文件即可。

优化读取性能

为了优化读取性能,可以使用缓存技术,但是缓存应该在客户端实现。

优化读写性能

使用类似B树,LSM等数据结构,可以将读写性能优化到对数级别。此时我们就可以不用再将数据存储在内存中,可以放在硬盘上了。

此时我们也不用维护冗长的日志了,仅未处理完成的事务需要记录日志,而执行完成的事务会直接记录在这些数据机构中。

此时恢复的机制也要有所改变,因为事务回滚之前宕机的话,事务的影响已经写入到数据结构中了,因此我们在记日志时不再记录具体的命令,而是记录命令修改的记录的原始值,而回滚则是从下至上撤销操作。这样我们就可以实现回滚。

优化并发

使用高性能的数据结构后,我们可以开始选择支持并发。每次写操作,我们将其分解为两个操作,对记录加锁操作,以及修改操作。锁会在事务提交回滚或宕机时自动释放。

而锁可以通过在内存中维护。由于我们锁定的是文件的一段连续区域,因此,我们可以用平衡二叉树结构维护锁信息,判断是否重复加锁。这样每次操作都是在内存中的O(log2n)时间复杂度,按照现代计算机,一秒钟足够支持数百万次加锁解锁操作,这是足够的。

在并发事务时,我们可以将日志写入到同一个文件中,但是每条日志都需要有自己的事务ID。

分布式事务

假设我们在两台不同的服务器上同时启动了两个我们的存储服务。但是两个服务存储着不同业务的数据。

现在我们希望让两台服务器上的两个事务同时提交或同时回滚。

XA事务

XA事务是两阶段提交服务,现在我们首先实现prepare原语。一个prepare命令。由于我们在写时已经自动加了锁,因此我们选择prepare什么都不做。

之后我们增加一个协调者服务,我们在提交或回滚时,将命令发送给协调者。协调者先向两个存储服务发送prepare命令,之后发送commit命令。如果在这个过程中协调者重启,协调者在重启后会进入恢复阶段,此时它会根据日志来确认是否向服务发起过commit命令,如果有,那么久确保每个存储服务都收到commit命令。否则,就直接回滚事务即可。

而如果存储服务宕机,它会根据自己是否有未完成的事务,如果有,就会询问协调者事务是否应该被提交。

因此数据始终都是一致的。

分布式事务实践

上面说的方式,都有些不够使用,因此我们使用一些开源的服务实现我们的分布式事务。

分布式事务队列

RocketMQ是一款支持分布式事务的队列,即它能保证队列的事务和本地事务同时提交或同时回滚。

对于一些后置事件,我们可以将事件存储进RocketMQ中,就可以保证消息投递的成功。

单机跨服务事务

如果MySQL只有一台,那么跨服务事务也很容易实现,只需要在MySQL之前增加一个MySQL代理。而需要跨服务事务的服务需要通过传递transaction-id来保证服务之间的使用的是同一个连接,而transaction-id可以作为注释放在SQL中一起传送给代理,让代理进行解析。

单机数据库消息

要实现消息的传递,我们不一定非要选择消息队列。数据库表也可以作为消息的载体。而由于事务的存在,我们可以保证仅在业务成功时,消息被投递。同样消费消息时,也可以保证业务成功时,消息也被消费成功。