事务概述
事务(transaction)是区别数据库和文件系统的重要特征,事务是访问并更新数据库中各数据项的一个执行单元。在事务的操作中,要么都执行修改要么都不执行,不能存在事务中有些操作被执行,有些操作没被执行的状态。不同的存储引擎对事务的支持粒度是不一样的,有的存储引擎甚至不支持事务。innodb是支持事务的存储引擎,所以我们主要看看innodb的机制。
理论上讲,事务有非常严格的定义,它需要满足4个特性,也就是ACID特性:
(1)原子性: 事务中的操作要么全做,要么全部不做,是一个不可分割的整体。只有事务中的所有操作被执行成功,整个事务才执行成功,如果有一个操作执行失败,那么事务中的其他操作也必须撤回。
(2)一致性: 指事务将数据库从一种状态转变为另一种一致的状态。在事务开始之前和事务结束以后,数据库完整性约束没有被破坏。
(3)隔离性: 要求每个读写事务的对象与其他事务的操作对象相互分离,即该事物提交前对其他事务都不可见,通常使用锁来实现隔离性。
(4)持久性: 事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。
事务的隔离级别
在ANSI SQL标准定义的四个隔离级别为:READ UNCOMMITTED(读未提交)、READ COMMITTED(读提交)、REPEATABLE READ(可重复读)、SERIALIZABLE(可串行化)。读未提交就是事务中的修改提交前对其他事务可见,这个级别最低,没有起到隔离的作用;读已提交指一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读;可重复读确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过这个级别又会导致幻读,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。可串行化就是完全的隔离,是最高级别的隔离,虽然能保证数据的安全,但是由于完全的隔离会导致锁的竞争影响并发程度。需要注意一点就是可重复读和幻读的差别,它们的区别在于读取的粒度,可重复读强调的是某一条记录的数据修改是可重复读的,它的粒度是记录,幻读重点强调了读取到了之前读取没有获取到的记录,它的粒度是某些记录但是对于这些记录中的数据是可重复读的。
在innodb中默认的隔离级别是REPEATABLE READ,但是与标准SQL不同的是,innodb在这种隔离级别下,通过使用next-key lock解决了幻读问题。一般来讲,事务的隔离级别越低,事务请求的锁越少或者请求的时间越短,系统的开销就越小,并发程度及越高,同时数据的安全性就越低;同理,事务隔离级别越高,事务请求锁越多或者请求锁的时间越长,系统开销就越大,并发程度越低,同时数据的安全性越高。
innodb中的事务隔离级别
在innodb中的默认隔离级别是REPEATABLE READ,就是可重复读级别,但是innodb中的这个级别和标准SQL的隔离级别不太一样,因为innodb通过mvcc解决了幻读的问题,它实际上是通过next-Key Lock算法来避免幻读的,因此在这种隔离级别上已近接近了可串行化的隔离级别。而且在性能上也不会比可重复读差多少,因此在inndb中默认采用这种隔离级别。
隔离级别的实现原理
事务的隔离性主要是通过给数据对象加锁来实现的,锁有很多类型,所得粒度也不一样,我们来看一下。
读未提交
这个隔离级别的性能是最好的,因为它并没有加任何锁,因此也没有什么隔离可言,它连基本的脏读都没办法解决。因此它的并发程度很高,但是同时数据的安全性也最差。
串行化
由于其他两种的过程会比较复杂所以先说说串行化,读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
可重复读
MySQL通过多版本并发控制来实现可重复读,在数据库表中一行记录可能有多个版本,每个版本除了记录本身外还有一个记录版本的字段,这个字段记录了让它产生的事务的 id,事务id记为transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。每一个版本都可以称为一个快照(snapshot),这个快照是关键。在读已提交和可重复读的隔离级别中,就是根据快照生成的时间来实现。例如在读已提交的级别中,每次事务执行一次更新语句就会生成一个快照;可重复读是在事务开始的时候生成一个当前事务全局性的快照。
对于一个快照而言,它需要根据一定的规则决定读哪些版本的数据。当前事务内的更新是可以读到的;版本未提交,不能读到;版本已提交,但是却在快照创建后提交的,不能读到;版本已提交,且是在快照创建前提交的,可以读到。
在这种级别下如何解决并发写的情况呢?当有两个事务T1, T2如果T1对某行记录中的数据进行更新修改操作,那么它会对这行记录加上一个行锁,如果在这个时候事务T2也想修改这行记录,T2就会申请锁,但是由于此时的锁被T1占有,因此T2必须等到T1释放锁它才能拥有。
在有索引和没有索引的情况下加锁的情况是不一样的,假如通过id索引更新某条记录,MySQL直接通过索引找到这条记录然后加上一个行锁。假如更新时的条件中字段不是索引行,这种情况下,MySQL会先对表中的所有记录加上行锁,然后再将那些不符合条件的记录上的行锁释放。因此可以知道,在这种情况下,由于加锁和释放锁对系统的开销是很大的,因此需要合理设计索引。
幻读
在可重复读的级别下,MySQL通过Next-Key Lock解决幻读问题。假如现在我们数据库的某张表中为某个字段建立了一个索引,这个字段被分为多个区间,对于某一行记录是可以加上行锁的,但是对于这条记录的其他区间,就可以加上间隙锁。举个例子来说明一下,假如现在有一个 id 字段设置它为索引,其值有3,8,12,20,那么该索引可能会被划分为几个区间:(负无穷,3),[3,8),[8,12),[12,20),[20,正无穷)。当某个事务执行查询操作时,例如select * from xxx where id <= 12,那么这个区间的记录以及中间行记录就都会被加上锁,当另一个事务试图在这个区间插入数据时,就会被阻塞,从而解决幻读。当事务的查询条件是例如 where id = 12的时候,innodb会对其进行优化,就会退化为行锁,仅仅锁住索引本身而不再是一个范围。
因此next-key lock锁在可重复读的级别中与在读已提交中就是锁的范围,在读已提交中,例如执行 where id > 8,只会锁住满足这个条件的记录,如果此时在这个范围内插入某些记录,再次查询就会产生幻读。这就是这两种隔离级别在幻读的问题的方式。