浅析数据库事务的隔离性(isolation)
数据库事务ACID
数据库事务可以被定义为一个或者几个数据库允许的操作的集合。这个集合需要支持ACID特性。
在ACID特性中,隔离性(isolation)指的是不同事务在提交的时候,最终呈现出来的效果是串行的,换句话说,既是不同事务,按照提交的先后顺序执行,再换句话说,对于事务本身来说,它所感知的数据库,应该只有它自己在操作。那么最简单的方法,我们按照定义来实现数据库事务的隔离性即可,很明显这在同时并发有多个事务要执行的环境下是没有执行效率的,一个事务的执行,必然会阻塞其他事务的执行。所以SQL的标准制定者对此作出妥协,提出隔离性的四个等级,其中最高级隔离等级才是串行执行。在没有到达串行执行等级的情况下,事务又是并发执行的,总是或多或少会存在问题,这在后面的例子当中会讲到。
隔离性的四个等级
-
未提交读(read uncommitted)这个等级是最低等级,也可以认为,事务之间完全不隔离,事务A开始一个事务,接着事务B开始,事务B对数据C继续update,这时候,A读取了B未提交(commit)的数据,这种情况叫做脏读(dirty read)。这个时候要是事务B遇到错误必须rollback,那么A读取的数据就完全是错的。可以想象这样完全不隔离的状态下,我们相对于数据库的业务方程序员写的一个sql,提交个db的执行引擎,返回的结果是多么不可确定啊。
-
提交读(read committed)既然读取别的事务未提交的数据很不安全,那么在上一个等级完全裸奔的情况下,增加一个要求:事务读取的数据,都是别的事务已经提交了的。但是只要在还没达到串行执行的情况下,总会有问题的,事务A select了一条数据,接着事务B update 这条数据,然后commit,这时候A还未提交,A再回来读这条数据,发现数据居然变了,按照我们之前所说,我们的目标是:对于一个事务本身来说,它所感知的数据库,应该只有它自己在操作,那么A会觉得自己并没有更新数据啊,怎么数据突然变了,这种情况叫做 不可重复读(Non-repeatable reads)
-
可重复读(repeatable read)可重复读,即是在上一个级别的基础上,保证不会在一个事务内两次select同一条数据会出现变化,即是别的事务对你select的对象进行update操作不会影响。但是,如果是insert操作,在这个隔离级别还是会受到影响。事务A开启事务,并select一段有范围的数据,然后事务B开启事务,在先前A事务select的那段有范围的数据中insert一条数据,然后提交事务,接着事务A再select出来这段数据,发现数据多了一条,这种情况叫幻读(Phantom Read)
-
序列化读(serializable)这也就是最高级别,保证事务之间不会有任何踩踏,每个事务都可以认为只有它自己在操作数据库。
隔离性的实现
我们知道,如果要实现数据库事务最高隔离性,也就是最安全的隔离性,有个显而易见的实现就是当一个事务在执行的时候,其他全部事务都阻塞,等待这个事务执行完再执行,这在现代多核CPU环境下显然非常浪费计算资源。为了充分利用资源,必须支持并发,这里就涉及并发控制(Concurrency control)
这是一个非常大的主题,关系到数据库,有两个比较重要的方法,一个是用锁(lock),一个是称为多版本并发控制(MVCC)的方法。
通过锁的方式来实现隔离性
读写锁
读写锁的概念很平常,当你在读取数据的时候,应该先加读锁,读取完之后的某个时间再解开读锁,那么加了读锁的数据,应该需要有什么特性呢,应该只能读,不能写,因为加了读锁,说明有事务准备读取这个数据,如果被别的事务重写这个事务,那数据就不准确了。所以一个事务给这个数据加了读锁,别的事务也可以对这个数据加读锁,因为大家都是只读不写。
写锁则具有排他性(exclusive lock),当一个事务准备对一个数据进行写操作的时候,先要对数据加写锁,那么数据就是可变的,这时候,其他事务就无法对这个数据加读锁了,除非这个写锁释放。
两端式提交锁(Two-phase locking)
两段式提交分为两步:
- 这个阶段只加锁,或者释放锁(读写锁)
- 这个阶段只会释放锁
下面对应于不同隔离级别对加锁方式进一步分析:
-
未提交读(read uncommitted):这个级别加锁,其实并不需要用两端式加锁,每一个具体操作执行完,锁就可以释放了。
-
提交读(read committed):这个阶段其实也可以按照每个操作执行前加锁,执行之后释放锁的方式。
-
可重复读(repeatable read) : 这个级别,就要求读锁必须,到事务结束前最后时刻才能释放,这样才能保证读取到数据是不可变的,可重复读的。但是这样会阻塞其他事务对加锁的数据的写操作。
-
序列化读(serializable):这个级别要求,两段式提交的第一步,要在事务开始的时候,原子性的把需要的锁全部加好(这显然很难估算,除非很大力度的锁),在事务结束前最后时刻,把全部锁一次性释放。这样做的结果就是使很多数据在事务执行期能都被加锁,无法被其他事务所使用。
使用多版本并发控制(MVCC)
加锁的方式处理事务一个比较大的问题就是会造成死锁(dead lock),原因就是一个事务加锁的数据并不止只有一行。事务A对行C加写锁,事务B对行D加写锁,接着事务希望获取行D的锁,事务B希望获取行C的锁,这样很容就死锁了。
使用MVCC就可以避免很多情况下的加锁操作,使用数据冗余的方式来实现事务隔离(这真是个很好的设计啊)
什么是MVCC
MVCC提供的只是一种思路,具体的实现比较多样化。大体的思路是每一行保存冗余数据,读写的时间戳,也可以称为版本号,在对某一行数据继续update或者delete的时候,并不直接操作,而是复制多一份副本进行操作,这个就是所谓多版本(multiversion)
mysql innodb对于MVCC的实现
innodb对每一行保存两个系统版本号,一个更新操作的版本号,一个删除操作的版本号,这两个版本号的来源是事务的ID(transaction id),也就是说,当某个事务对这一行数据进行update,或者删除的时候,相应会把它的事务ID写入这行数据的更新操作的版本号,删除操作的版本号中。
事务ID是随时间推移而增长,而且不可重复的。一个事务打开之后:
- 对于select操作:每次只会select具有比当前事务ID更小的更新操作版本号的数据,而且这些数据要保证删除版本号为空,或者删除版本号大于当前事务ID。
- 对于update操作:对该行数据复制出一份副本,同时在更新操作版本号写入当前事务ID,同时把当前事务ID写入之前的删除操作的版本号中。
- 对于insert操作:写入新行,同时在更新操作版本号写入当前事务ID
- 对于delete操作:在删除操作版本号写入当前事务ID
mysql官方innodb的实现是用MVCC,官方声称默认的innodb的隔离级别是可重复读。但是mysql是保证不会出现幻读的,因为它每次select只会读取在事务开始时候的snapshot,并且忽略在这个时刻之后提交的所有变更。consistent read
mysql官方文档提到串行隔离级别要在原来的基础说对每一个select操作执行SELECT ... LOCK IN SHARE MODE
这样就可以读取的数据加读锁了,那么其他试图写入数据都必须阻塞。那么就可以实现序列化串行了。