事务存在的意义就是保证系统中的数据是正确的,不同数据间不会产生矛盾,也就是保证数据状态的一致性。事务是区别文件系统和数据库系统的主要因素。
提到数据库的事务,必然是耳熟能详的ACID。但是ACID这四种特性并不正交,A、I、D 是手段,C(一致性)是目的。
- 原子性(Atomic):在同一项业务处理过程中,事务保证了多个对数据的修改,要么同时成功,要么一起被撤销。
- 隔离性(Isolation):在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。
- 持久性(Durability):事务应当保证所有被成功提交的数据修改都能够正确地被持久化,不丢失数据。
原子性和持久性
原子性和持久性在事务里是密切相关的两个属性,原子性保证了事务的多个操作要么都生效要么都不生效,不会存在中间状态;持久性保证了一旦事务生效,就不会再因为任何原因而导致其修改的内容被撤销或丢失。
实现原子性和持久性所面临的困难是,“写入磁盘”这个操作不会是原子的,不仅有“写入”与“未写入”,还客观地存在着“正在写”的中间状态。
持久性
事务需要保证持久性,也就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。那么如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘。这么做时机对了,但是方式不太好。我们确实应该在事务提交完成之前进行持久化的操作,但是如果将数据直接进行持久化会有一些问题:
- 太浪费:有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太浪费了。
- 性能不高:一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,倒霉催的是该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的Buffer Pool中的页面刷新到磁盘时,需要进行很多的随机IO,随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。
所以使用怎样的方式做持久化比较好呢?那就是持久化数据的修改过程,而不是修改结果。比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1改成2我们只需要记录一下:将第0号表空间的100号页面的偏移量为1000处的值更新为2。这样当数据库从崩溃中恢复,就可以按照记录的内容进行重放。这就是redo log。redo log的好处:占用空间小、顺序IO。
redo log也不是直接写磁盘的,与引入Buffer Pool同理,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间。向log buffer中写入redo日志的过程是顺序的。
redo日志刷盘时机:
- log buffer空间不足时
- 事务提交时
- 后台线程不停的刷刷刷
- 正常关闭服务器时
原子性
事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚。
因此为了回滚就需要记录日志:undo log。针对不同的操作:INSERT、DELETE、UPDATE,undo log的格式也是不一样的。
隔离性
理论上来说为了保证隔离性,在某个事务对某个数据进行访问时,其他事务应该排队,当该事务提交后,其他事务才能继续访问这个数据。但是,这样做毫无性能可言,所以需要设置一些隔离级别以达到隔离性和性能之间的平衡。
事务并发执行可能遇到的问题
脏写(Dirty Write)
如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写。
脏读(Dirty Read)
如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读。
不可重复读(Non-Repeatable Read)
如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读。
幻读(Phantom)
如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录并提交,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读。幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。
不可重复读的重点是修改:同样的条件, 你读取过的数据, 再次读取出来发现值不一样了。幻读的重点在于新增或者删除:同样的条件, 第1次和第2次读出来的记录数不一样。
当然, 从总的结果来看, 似乎两者都表现为两次读取的结果不一致。但如果你从控制的角度来看, 两者的区别就比较大。对于前者, 只需要锁住满足条件的记录,对于后者, 要锁住满足条件及其相近的记录,这部分在后面讲Gap Lock会讲到。
SQL:1992标准中的四种隔离级别
READ UNCOMMITTED:未提交读
未提交读隔离级别下,除了脏写,其他情况都可能发生,如果连脏写都不能避免,那就等于没隔离…
READ COMMITTED:已提交读
大部分数据库默认的隔离级别。此隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题。
REPEATABLE READ:可重复读
此隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题。可重复读是MySQL默认的隔离级别,但是MySQL的可重复读是可以避免发生幻读的。
SERIALIZABLE:可串行化
各种问题都不可以发生。但是,这种隔离级别还并发啥了…
MySQL主要依靠锁以及一种针对读操作的优化方案MVCC来实现隔离性。同样的,具体内容会在后面文章写。