事务的四大特性(ACID)
- 原子性(Atomicity): 事务是最⼩的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
- 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务带来的问题
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插⼊了一些数据时。在随后的查询中,第一个事务(T1) 就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
MySQL底层实现事务的四大特性的方法
- 原子性(隔离性)是undo log(回滚日志)实现的。
- 一致性是由代码逻辑层面和其他三个特性保证的。
- 隔离性是由mvcc实现的。
- 持久性是基于redo log (重做日志)实现的。
-
持久性的实现:
基本的修改数据的过程为:
这样做有严重的性能问题:InnoDB在磁盘中存储的基本单元是页,可能本次修改只变更页一中几个字节,但是需要刷新整页的数据。一个事务可能修改了多页中的数据,页之间又是不连续的,就会产生随机IO。
为了解决上述问题,InnoDB提供了缓存解决这一问题(Buffer Pool),BP中包含了部分数据页的映射,作为访问数据库的缓冲;当从数据库读取数据时,会首先写入BP,BP中修改的数据会定期刷新到磁盘中(这一过程称之为刷脏)
但如果MySQL宕机,而此时BP中修改的数据还没有刷新的磁盘,就会导致数据的丢失,事务的持久性无法保证。
引入redo log(重做日志)是为了解决上述问题。 Redo Log记录的是物理日志,也就是磁盘数据的修改。当数据被修改时,除了修改BP中的数据,还会在redo log中记录这次操作,当事务提交时,会调用fsync接口对 redo log进行刷盘,如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复,redo log采用的是WAL(Write-ahead logging,预写式日志), 所有修改先写入日志,再更新到磁盘。日志从redo log buffer 先write到文件系统的page cache,在fsync到磁盘。保证了数据不会因为MySQL宕机而丢失,从而满足了持久性的要求。引入重做日志后从磁盘加载数据到内存过程如下:
- 在内存中修改数据
- 把新数据写到Redo Log Buffer中
- 把Redo Log Buffer中数据持久化到Redo Log文件中
- 把Redo Log文件中数据持久化到数据库磁盘中
原子性的实现
实现原子性的核心就在于如何实现回滚,当事务回滚时能够撤销所有已经执行成功的SQL语句。InnoDB实现回滚,靠的是undo log。undo log属于逻辑日志,它记录的是SQL执行相关的信息。当事务对数据库进行修改时,InnoDB会生成对应的回滚日志。每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上,undo log记录了数据被修改前的信息以及新增和被删除的数据信息。以update操作为例,当事务执行update时,其生成的undo log中会包含被修改行的主键(一遍知道修改了哪些行),修改了哪些列、这些列在修改前后的值的信息,回滚时便可以使用这些信息将数据还原到update之前的状态。回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。如果事务执行失败或者调用了rollback,导致事务需要回滚,根据undo log生成回滚语句回滚到修改之前的样子。
完整过程的顺序如下:
隔离性的实现
SQL 标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化): 最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
级别越低的隔离级别可以执行越高的并发,但同时实现复杂度以及开销也越大。
原子性,隔离性,持久性的目的都是为了要做到一致性。原子性和持久性是为了要实现数据的可性保障靠,隔离性是要管理多个并发读写请求的访问顺序。
READ UNCOMMITTED
READ UNCOMMITTED隔离级别下,事务中的修改即使还没提交,对其他事务是可见的。事务可以读取未提交的数据,造成脏读。因为读不会加任何锁,所以写操作在读的过程中修改数据,所以会造成脏读。好处是可以提升并发处理性能,能做到读写并行。换句话说,读的操作不能排斥写请求。
优点:读写并行,性能高
缺点:造成脏读
READ COMMITTED
一个事务的修改在他提交之前的所有修改,对其他事务都是不可见的。其他事务能读到已提交的修改变化。InnoDB在 READ COMMITTED中,使用排它锁,读取数据不加锁而是使用了MVCC机制。或者换句话说他采用了读写分离机制。但是该级别会产生不可重读以及幻读问题。这跟 READ COMMITTED 级别下的MVCC机制有关系,在该隔离级别下每次 select的时候新生成一个版本号,所以每次select的时候读的不是一个副本而是不同的副本。
在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读。
REPEATABLE READ(Mysql默认隔离级别)
在一个事务内的多次读取的结果是一样的。这种级别下可以避免,脏读,不可重复读等查询问题。mysql 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC。只要没释放读锁,在次读的时候还是可以读到第一次读的数据。
优点:实现起来简单
缺点:无法做到读写并行
- 采用读写锁实现:只要没释放读锁,在次读的时候还是可以读到第一次读的数据。优点:实现起来简单。缺点:无法做到读写并行
- 采用MVCC实现:多次读取只生成一个版本,读到的是相同数据。优点:读写并行缺点:实现的复杂度高。但是在该隔离级别下仍会存在幻读的问题。
SERIALIZABLE
该隔离级别理解起来最简单,实现也最单。在隔离级别下除了不会造成数据不一致问题,没其他优点。
一致性的实现
- 原子性,隔离性,持久性的目的都是为了要做到一致性。
- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
- 应用层面(代码层面)进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致