事务,就是用来保证数据操作符合业务逻辑要求而实现的一系列功能。换句话说,如果数据库不支持事务,上层业务系统的程序员就需要自己写代码,以保证相关数据处理逻辑的正确性。举个例子,数据库最开始普及就是在金融业,银行的存取款场景就是一个最典型的 OLTP 数据库场景,而事务就是用来保证类似场景的业务逻辑正确性的。
(图片来自网络,侵删)
原子性: 如果你要给家人转账,必须在你的账户里扣掉 100 块,在家人账户里加上 100 块,这两笔操作需要一起完成,业务逻辑才是正确的。但是程序在做修改时,肯定会有先后顺序,试想一下程序扣了你的钱,这个时候程序崩溃了,家人账户的钱没有加上,那这 100 块是不是消失了?你是不是要发疯?那么,就把这两笔操作放进一个事务里,通过原子性保证,这两笔操作要么都成功,要么都失败。这样才能保证业务逻辑的正确性。
一致性: 有很多文章讲过一致性,但是很多人会把一致性跟原子性混在一起说。事务的一致性指的是,每一个事务必须保证执行之后所有库内的规则依旧成立,比如内外键、constraint、触发器等。举例来说,你在储蓄卡里有 100 元,理财账户里有 100 元,基金账户有 100 元,那么你在资产总和里会看到 300 元,这 300 元必须是三个账户余额加在一起得到的。你从储蓄卡里转出去了 100 元给家人,那么可以在数据库上创建触发器,当储蓄卡余额账户减 100 元的同时,把资产总和也同步减去 100 元,不然就会出现逻辑上的错误。你已经转走了 100 元储蓄卡余额,实际资产总和应该是 200 元,若还是 300 元,数据库状态就不一致了。因此实现事务的时候,必须要保证相关联的触发器以及其他内部规则都执行成功,事务才算执行成功。如果在减去资产总额时出错,数据库就会进入不一致的状态 , 那么这笔转帐交易也不能成功。
那么一致性跟原子性的区别到底在哪里呢?原子性是指多个用户指令之间必须作为一个整体完成或失败,而一致性更多是数据库内的相关数据规则同时完成或失败。
持久性: 事务只要提交了,对数据库的修改就会保存下来不会丢了。简单来说,只要提交了,数据库就算崩溃了,重启之后你刚存的 100 块依然在你的账户里。
隔离性: 每个事务相对于其他的事务有一定独立性、不能互相影响,因为数据库需要支持并发的操作来提高效率。在并发操作时,一定要通过操作之间的隔离来保证业务逻辑的正确性。比如,你转帐 100 块给家人,一系列操作的最后一步可能是输入验证码,这个时候转帐还没有完成,但是在数据库里,你的账户对应的记录中已经减去 100 块,家人账户也加了 100 块,就等着验证码输入以后,事务提交,完成操作。那么,这个时候,家人通过手机银行能够查到这 100 块么?你的答案可能是不能,因为你的转帐操作还没有提交,事务还没有完成,那么数据库就应该保证这两个并发操作之间具有一定的隔离性,这样才符合业务逻辑。
到底应该隔离到什么程度呢?隔离性又分为 4 个等级:由低到高依次为 Read uncommitted(读未提交)、Read committed(读提交)、Repeatable read(可重复读取)、Serializable(序列化),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。
怎么理解不同的隔离等级呢?首先要理解并发操作,并发操作就是指有不同的用户同时对一个数据进行读、写操作,那么在这个过程中,每个用户应该看到什么数据才能保证业务逻辑的正确性呢?
如果是前面存取款的场景,我看到的是已经存进来的钱,也就是必须是已经提交的事务。而 12306 刷火车票,你可以看到有 10 张余票,但是在下单的时候告诉你票卖完了,因为同时有 10 个用户把票买掉了,你需要重新刷余票。这个也是可以接受的。也就是说我可以读到一些虚假的余票,在业务上也没有什么问题。那么在设计这两个不同系统时,就可以选择不同的事务隔离级别来实现不同的并发效果。不同的隔离等级就是在系统的并发性和数据逻辑的严谨性之间做出的平衡。
数据库实现事务会有多种不同的方式,但基本的原理类似,比如都需要对事务进行统一的编号处理,都需要记录事务的状态(是成功了还是失败了),都需要在数据存储的层面对事务进行支持,以明确哪些数据是被哪些事务插入、修改和删除的。同时还会记录事务日志等,对事务进行系统化的管理以实现数据的原子性、一致性和持久性。
要实现事务的隔离性,最基础的就是通过加锁机制,把并发操作适当串行化来保证数据操作的正确逻辑。但是为了要保证系统具有良好的并发性能,必须要在实现事务隔离性时找到合理的平衡点。
大部分数据库(包括 Oracle、MySQL、Postgres 在内)在做并发控制的时候,都会采用 MVCC(多版本并发控制)的机制来保证系统具有较高的并发性。不同数据库实现 MVCC 的具体方案不尽相同,但其基本原理类似。
所谓 MVCC,就是数据库中的同一查询根据相关事务执行的先后顺序以及隔离级别的不同,可能会存在不同版本的结果,通过这样的手段来保证大部分查询操作不会被修改操作阻塞并保证数据逻辑的正确性。简单来说就是,用存储空间来交换并发能力。
下面以 Postgres 为例介绍一下 MVCC 的一种实现方式,下图用以解释 Posrgres 里最基本的数据可见性是如何实现多版本控制的。
(图片来自网络,侵删)
首先,Postgres 里的每一个事务都有编号,这里可以简单理解为时间顺序编号,编号越大的事务发生越晚。然后,数据库里的每一行记录都会保存创建这条记录的事务号(Cre),也会在记录删除时保存删除这条记录的事务号(Exp),换句话说,只要 Exp 这里一列里记录了事务编号,就说明这条记录被删除了。那么一个事务应该能看见哪些记录呢?Postgres 里每一个事务都会保存一个当前系统的事务快照(Snapshot),这个快照里会保存事务创建时当前系统的最高(最晚)事务编号,以及目前还在进行中的事务编号。在如上图所示的一个事务的快照里,最高事务编号为 100,目前正在进行的事务有 25、50 和 75。对应左边数据记录,这 6 行数据的可见性就如同标注的一般:
第一行,Cre 30,没有删除,在 100 这个时间点,应该能看到。
第二行,Cre 50,没有删除,但是 50 这个事务还没有提交,正在进行中,所以看不见。
第三行,Cre 110,没有删除,但是 100 这个时间点 110 事务还没有发生,所以看不见。
第四行,Cre 30,Exp 80,在 80 的时候数据被删掉了,所以看不见。
第五行,Cre 30,Exp 75,在 30 的时候被创建,75 时候被删掉了,但是 75 这个事务在 100 的时候还没有提交,这条记录在 100 的时候还没有删掉,所以看得见。
第六行,Cre30,Exp 110,在 30 的时被创建,110 时候被删掉,但是在 100 时候,110 还没有发生,所以看得见。
综上就是这个事务对这六条记录的可见性,也就是一个数据版本。大家可以看一下,如果另一个事务的快照里存的是最高事务编号为 110,正在进行的事务为 50,那么它能看到的数据应该是哪几行呢?
同时大家也看到,Postgres 里删除一行数据就是在这一行的 Exp 这个列记录一个删除事务的编号。相当于做了一个删除标记,而数据没有真正被删除,因此 Postgres 数据库需要定期做数据清理操作(Vacuum)。我们这里假定所有的事务最终都是正确提交了,Postgre 在现实场景里的可见性比介绍的要复杂,会存在某些事务没有提交的情况,这里不再展开。
你也「在看」吗?👇