MySQL 如何实现事务
mysql如何实现事务
原子性:
想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚而在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。
这个过程其实非常好理解,为了能够在发生错误时撤销之前的全部操作,肯定是需要将之前的操作都记录下来的,这样在发生错误时才可以回滚。
回滚日志除了能够在发生错误或者用户执行 ROLLBACK
时提供回滚相关的信息,它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。
回滚日志并不能将数据库物理地恢复到执行语句或者事务之前的样子;它是逻辑日志
持久性
既然是数据库,那么一定对数据的持久存储有着非常强烈的需求,如果数据被写入到数据库中,那么数据一定能够被安全存储在磁盘上;而事务的持久性就体现在,一旦事务被提交,那么数据一定会被写入到数据库中并持久存储起来。
当事务已经被提交之后,就无法再次回滚了,唯一能够撤回已经提交的事务的方式就是创建一个相反的事务对原操作进行『补偿』,这也是事务持久性的体现之一。
与原子性一样,事务的持久性也是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文件,它是持久的。
当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上,图中的第 4、5 步就是在事务提交时执行的。
回滚日志和重做日志
到现在为止我们了解了 MySQL 中的两种日志,回滚日志(undo log)和重做日志(redo log);在数据库系统中,事务的原子性和持久性是由事务日志(transaction log)保证的,在实现时也就是上面提到的两种日志,前者用于对事务的影响进行撤销,后者在错误处理时对已经提交的事务进行重做,它们能保证两点:
- 发生错误或者需要回滚的事务能够成功回滚(原子性);
- 在事务提交后,数据没来得及写会磁盘就宕机时,在下次重新启动后能够成功恢复数据(持久性);
在数据库中,这两种日志经常都是一起工作的,我们可以将它们整体看做一条事务日志,其中包含了事务的 ID、修改的行元素以及修改前后的值。
隔离性
RAED UNCOMMITED
:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read);READ COMMITED
:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read);REPEATABLE READ
:多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read);
SERIALIZABLE
:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题;
1.使用查询语句不会加锁,读到未提交的数据。
2.只对记录加记录锁,而不会在记录之间间隙锁。所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果。
3.多次查询同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读。
4.Innodb隐式地将全部的查询语句加上共享锁,解决了幻读的问题。
以上的所有的事务隔离级别都不允许脏写入(Dirty Write),也就是当前事务更新了另一个事务已经更新但是还未提交的数据,大部分的数据库中都使用了 READ COMMITED 作为默认的事务隔离级别,但是 MySQL 使用了 REPEATABLE READ 作为默认配置;从 RAED UNCOMMITED 到 SERIALIZABLE,随着事务隔离级别变得越来越严格,数据库对于并发执行事务的性能也逐渐下降。
隔离级别的实现
数据库对于隔离级别的实现就是使用并发控制机制对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新,而最重要也最常见的并发控制机制,在这里我们将简单介绍三种最重要的并发控制器机制的工作原理。
锁
锁是一种最为常见的并发控制机制,在一个事务中,我们并不会将整个数据库都加锁,而是只会锁住那些需要访问的数据项, MySQL 和常见数据库中的锁都分为两种,共享锁(Shared)和互斥锁(Exclusive),前者也叫读锁,后者叫写锁。
时间戳
除了锁,另一种实现事务的隔离性的方式就是通过时间戳,使用这种方式实现事务的数据库,例如 PostgreSQL 会为每一条记录保留两个字段;读时间戳中包括了所有访问该记录的事务中的最大时间戳,而记录行的写时间戳中保存了将记录改到当前值的事务的时间戳。
使用时间戳实现事务的隔离性时,往往都会使用乐观锁,先对数据进行修改,在写回时再去判断当前值,也就是时间戳是否改变过,如果没有改变过,就写入,否则,生成一个新的时间戳并再次更新数据,乐观锁其实并不是真正的锁机制,它只是一种思想,在这里并不会对它进行展开介绍。
多版本和快照隔离
通过维护多个版本的数据,数据库可以允许事务在数据被其他事务更新时对旧版本的数据进行读取,很多数据库都对这一机制进行了实现;因为所有的读操作不再需要等待写锁的释放,所以能够显著地提升读的性能,MySQL 和 PostgreSQL 都对这一机制进行自己的实现,也就是 MVCC,虽然各自实现的方式有所不同,MySQL 就通过文章中提到的回滚日志实现了 MVCC,保证事务并行执行时能够不等待互斥锁的释放直接获取数据。
如果一个事务原子地在一个一致地数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。