前言
数据库事务的 ACID 四大特性是事务的基础,了解了 ACID 是如何实现的,我们也就清楚了事务的实现,接下来我们将依次介绍数据库是如何实现这四个特性的。
四大特性
原子性
原子性就是,保证事务就是一系列的操作,要么全部都执行,要么都不执行。
回滚日志
如果要保证原子性的话,就需要在异常发生时,对已经执行的操作进行回滚。在MySQL中,回滚的实现是通过回滚日志(undo log)去实现的。
解释一下,因为原子性是要么都执行,要么都不执行,所以如果发生异常的话,要保证操作都不执行,那么就需要对已执行的操作进行回滚。
回滚日志除了能够在发生错误或者用户执行 ROLLBACK
时提供回滚相关的信息,它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。
注意:要先写日志!!!!要先写日志!!!!
我们需要了解的是,回滚日志并不是像redis中的RDB快照一样,记录着之前的状态,回滚日志是撤销我们已经执行的操作。简单理解为,事务中一条 insert 操作,在回滚日志中就对应着一条 delete 操作。
持久性
既然是数据库,那么一定对数据的持久存储有着非常强烈的需求,如果数据被写入到数据库中,那么数据一定能够被安全存储在磁盘上;而事务的持久性就体现在,一旦事务被提交,那么数据一定会被写入到数据库中并持久存储起来。
所以我们如果想要保证持久性的实现,就一定需要保证事务被提交之后,数据一定会被写入到数据库中并持久存储起来,或者被提交之后,遇到错误,一定重新提交数据,并保存到磁盘之中。
重做日志
上文提到,我们需要保证提交后一定被写入数据库之中,并持久存储,或遇到错误重新提交,然后持久存储。
与原子性一样,事务的持久性也是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文件,它是持久的。
有一张图片,我认为特别好:图片来自 javabetter.cn
当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上,图中的第 4、5 步就是在事务提交时执行的。
在 InnoDB 中,重做日志都是以 512 字节的块的形式进行存储的,同时因为块的大小与磁盘扇区大小相同,所以重做日志的写入可以保证原子性,不会由于机器断电导致重做日志仅写入一半并留下脏数据。
除了所有对数据库的修改会产生重做日志,因为回滚日志也是需要持久存储的,它们也会创建对应的重做日志,在发生错误后,数据库重启时会从重做日志中找出未被更新到数据库磁盘中的日志重新执行以满足事务的持久性。
回滚日志和重做日志
到现在为止我们了解了 MySQL 中的两种日志,回滚日志(undo log)和重做日志(redo log);在数据库系统中,事务的原子性和持久性是由事务日志(transaction log)保证的,在实现时也就是上面提到的两种日志,前者用于对事务的影响进行撤销,后者在错误处理时对已经提交的事务进行重做,它们能保证两点:
- 发生错误或者需要回滚的事务能够成功回滚(原子性);
- 在事务提交后,数据没来得及写会磁盘就宕机时,在下次重新启动后能够成功恢复数据(持久性);
隔离性
事务的隔离性是数据库处理数据的几大基础之一,如果没有数据库的事务之间没有隔离性,就会发生级联回滚等问题,造成性能上的巨大损失。
级联回滚是并行事务可能会发生的问题,介绍如下。
并行事务
当 Transaction1 在执行的过程中对
id = 1
的用户进行了读写,但是没有将修改的内容进行提交或者回滚,在这时 Transaction2 对同样的数据进行了读操作并提交了事务;也就是说 Transaction2 是依赖于 Transaction1 的,当 Transaction1 由于一些错误需要回滚时,因为要保证事务的原子性,需要对 Transaction2 进行回滚,但是由于我们已经提交了 Transaction2,所以我们已经没有办法进行回滚操作,在这种问题下我们就发生了问题,Database System Conceptsopen in new window 一书中将这种现象称为不可恢复安排(Nonrecoverable Schedule),那什么情况下是可以恢复的呢?引用于 javabetter.cn
如果想恢复,并且Transaction2 依然依赖于 Transaction1 ,这样我们需要让Transaction1 先提交。
而实际情况之中,有多个事务并行执行,那么就会出现更复杂的情况。例如
Transaction2 依赖于 Transaction1,而 Transaction3 又依赖于 Transaction1,当 Transaction1 由于执行出现问题发生回滚时,为了保证事务的原子性,就会将 Transaction2 和 Transaction3 中的工作全部回滚,这种情况也叫做级联回滚。
我们现在重新将内容来回到隔离性。
可以知道,如果事务没有隔离性,那么就会出现上文提到的级联回滚等问题。串行虽然能够允许开发者忽略并行造成的影响,能够很好地维护数据库的一致性,但是却会影响事务执行的性能。
事务隔离的级别
在 SQL 标准中定义了四种数据库的事务的隔离级别:READ UNCOMMITED
、READ COMMITED
、REPEATABLE READ
和 SERIALIZABLE
;每个事务的隔离级别其实都比上一级多解决了一个问题:
-
READ UNCOMMITED:使用查询语句不会加锁。但是可能会读到未提交的数据
(Dirty Read); READ COMMITED:只对记录加锁,但是对记录之间的间隙不加锁,所以两次查询可能会查到不一样的数据。
(Non-Repeatable Read)REPEATABLE READ:多次查询会返回第一次查询的快照,不会返回不同的数据。但是这样可能会导致幻读。
SERIALIZABLE:InnoDB将全部的查询语句隐式的加上了共享锁,解决了幻读的问题。
以上的所有的事务隔离级别都不允许脏写入(Dirty Write),也就是当前事务更新了另一个事务已经更新但是还未提交的数据,大部分的数据库中都使用了 READ COMMITED 作为默认的事务隔离级别,但是 MySQL 使用了 REPEATABLE READ 作为默认配置;从 RAED UNCOMMITED 到 SERIALIZABLE,随着事务隔离级别变得越来越严格,数据库对于并发执行事务的性能也逐渐下降。
幻读
在一个事务中,同一个范围内的记录被读取时,其他事务向这个范围添加了新的记录。
重新开启了两个会话 SESSION 1
和 SESSION 2
,在 SESSION 1
中我们查询全表的信息,没有得到任何记录;在 SESSION 2
中向表中插入一条数据并提交;由于 REPEATABLE READ
的原因,再次查询全表的数据时,我们获得到的仍然是空集,但是在向表中插入同样的数据却出现了错误。
一致性
数据库对于 ACID 中的一致性的定义是这样的:如果一个事务原子地在一个一致地数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。
它的第一层意思就是对于数据完整性的约束,包括主键约束、引用约束以及一些约束检查等等,在事务的执行的前后以及过程中不会违背对数据完整性的约束,所有对数据库写入的操作都应该是合法的,并不能产生不合法的数据状态。
而第二层意思其实是指逻辑上的对于开发者的要求,我们要在代码中写出正确的事务逻辑,比如银行转账,事务中的逻辑不可能只扣钱或者只加钱,这是应用层面上对于数据库一致性的要求。
总结
本文讲述了事务的四大特性的实现,原子性与持久性是通过日志去实现的,比较好理解。关于隔离性,我们需要根据实际情况需要的隔离级别去调整,不可以直接使用最高级别,这样会导致效率过低。至于隔离性,本文讲的是ACID的隔离性,实际问答中,要确认是ACID还是CAP中的一致性,两者有根本性的不同,如果要看CAP隔离性,可以去看 javabetter.cn 大佬的网站。