提到事务首先想到的固然是事务的四个特性:原子性、一致性、隔离性、持久性。事务的实现是由引擎层面来实现的,所以不一样的存储引擎可能对事务有不一样的实现方案。好比 MySQL 的 MyISAM 引擎就没有实现事务,这也是其被 InnoDB 所替代的缘由之一。html
原子性: 事务的全部操做在数据库中要么所有正确的反映出来,要么彻底不反映。mysql
一致性: 事务执行先后数据库的数据保持一致。git
隔离性: 多个事务并发执行时,对于任何一对事务Ti和Tj,在Ti看来,Tj 要么在 Ti 以前已经完成执行,或者在Ti完成以后开始执行。所以,每一个事务都感受不到系统中有其余事务在并发执行。github
持久性: 一个事务成功完成后,它对数据库的改变必须是永久的,即便事务刚提交机器就宕机了数据也不能丢。sql
事务的原子性和持久性比较好理解,可是一致性会更加抽象一些。对于一致性常常有个转帐的例子,A 给 B 转帐,转帐先后 A 和 B 的帐户总和不变就是一致的。这个例子咋一看好像很清楚,但转念一想原子性是否是也能达到这个目的呢?答案是:不能,原子性能够保证 A 帐户扣减和 B 帐户增长同时成功或者同时失败,可是并不能保证 A 扣减的数量等于 B 增长的数量。其实是为了达到一致性因此要同时知足其余三个条件。数据库
还有一个事务的隔离性比较复杂,由于 MySQL 的事务能够有多种隔离级别,接下里一块儿看看。数组
当多个事务并发执行时可能存在脏读(dirty read),不可重复读(non-repeatable read)和幻读(phantom read),为了解决这些问题所以引入了不一样的隔离级别。bash
脏读: 事务 A 和事务 B 并发执行时,事务 B 能够读到事务 A 未提交的数据,就发生了脏读。脏读的本质在于事务 B 读了事务 A 未提交的数据,若是事务 A 发生了回滚,那么事务 B 读到的数据其实是无效的。以下面案例所示:事务 B 查询到 value 的结果为100,可是由于事务 A 发生了回滚,所以 value 的值不必定是 100。session
事务 A | 事务 B |
---|---|
begin | begin |
update t set value = 100 | |
select value from t | |
rollback | |
commit | commit |
不可重复读: 在一个事务中,屡次查询同一个数据会获得不一样的结果,就叫不可重复读。以下面案例所示:事务 B 两次查询 value 的结果不一致。并发
事务 A | 事务 B |
---|---|
begin | begin |
update t set value = 100 | |
select value from t ( value = 100 ) | |
update t set value = 200 | |
select value from t ( value = 200 ) | |
commit | commit |
幻读: 在一个事务中进行范围查询,查询到了必定条数的数据,可是这个时候又有新的数据插入就致使数据库中数据多了一行,这就是幻读。以下面案例所示:事务 B 两次查询到的数据行数不同。
事务 A | 事务 B |
---|---|
begin | begin |
select * from t | |
insert into t ... | |
commit | |
select * from t | |
commit |
MySQL 的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)可重复读(repeatable read)和串行化(serializable)。
未提交读: 一个事务还未提交,其形成的更新就能够被其余事务看到。这就形成了脏读。
读提交: 一个事务提交后,其更改才能被其余事务所看到。读提交解决了脏读的问题。
可重复读: 在一个事务中,屡次读取同一个数据获得的结果老是相同的,即便有其余事务更新了这个数据并提交成功了。可重复读解决了不可重复读的问题。可是仍是会出现幻读。InnoDB 引擎经过多版本并发控制(Multiversion concurrency control,MVCC)解决了幻读的问题。
串行化: 串行话是最严格的隔离级别,在事务中对读操做加读锁,对写操做加写锁,因此可能会出现大量锁争用的场景。
从上到下,隔离级别愈来愈高,效率相应也会随之下降,对于不一样的隔离级别须要根据业务场景进行合理选择。
下面的命令能够查询 InnoDB 引擎全局的隔离级别和当前会话的隔离级别
mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
复制代码
设置innodb的事务级别方法是:
set 做用域 transaction isolation level 事务隔离级别
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
mysql> set global transaction isolation level read committed; // 设定全局的隔离级别为读提交
mysql> set session transaction isolation level read committed; // 设定当前会话的隔离级别为读提交
复制代码
MySQL 里能够经过 begin 命令或 start transaction 来显示启动一个事务。显示开启的事务,须要使用 commit 命令进行提交。
MySQL 里若是没有显示执行命令开启事务,MySQL 也会在执行第一条命令的时候自动开启事务。若是自动提交 autocommit 处于开启状态,那么自动开启的事务也会被自动提交。那么执行一条 select 语句时,MySQL 首先会自动开启一个事务,而且在 select 语句执行完后自动提交。所以,在 MySQL 里执行一条语句时也是一个完整的事务。
在 MySQL 里执行命令 set autocommit=0 能够关闭事务的自动提交。若是 autocommit 处于关闭状态,那么执行一条 select 语句时仍然会开启一个事务,而且在执行完成后不会自动提交。
begin 和 start transaction 命令并非执行后当即开启一个事务,而是在执行第一条语句时才开启事务。start transaction with consistent snapshot 命令才是执行后就当即开启事务。
接下来咱们用一个案例来看不一样隔离级别下会有怎样不一样的结果。
create table t (k int) ENGINE=InnoDB;
insert into t values (1);
复制代码
事务 A | 事务 B |
---|---|
begin | |
1: select k from t | |
begin; update t set k = k + 1 | |
2: select k from t | |
commit | |
3: select k from t | |
commit | |
4: select k from t |
隔离级别为未提交读时:对于事务 A,第1条查询语句的结果是1,第2条查询语句的结果是2,第3条和第4条查询语句的结果也都是2。
隔离级别为读提交时:对于事务 A,第1条查询语句的结果是1,第2条查询语句的结果是1,第3条查询语句的结果是2,第4条查询语句的结果也是2。
隔离级别为可重复读时:对于事务 A,第1条、第2条和第3条查询语句的结果都是1,第4条查询语句的结果是2。
隔离级别为串行化时:对于事务 A,第1条查询语句的结果是1。这时事务 B 执行更新语句时会被阻塞,由于事务 A 在这条数据上加上了读锁,事务 B 要更新这个数据就必须加写锁,因为读锁和写锁冲突,所以事务 B 只能等到事务 A 提交后释放读锁才能进行更新。所以,事务 A 的第2条和第3条查询语句的结果也是1,第4条查询语句的结果是2。
事务的隔离性经过 undo log 日志来实现,对于同一条数据,InnoDB 会存储其多个版本,多个版本则是经过 undo log 日志来实现,将当前值回滚不一样的次数就能够获得不一样低版本的数据,这就是数据库的多版本并发控制(MVCC)。固然只有 undo log 日志还不行,为了支持提交读和可重复读两种隔离级别,一个事务 Ti 如何知道本身应该使用哪一个版本的数据呢?InnoDB 的作法是维护一个一致性视图来现实。
InnoDB 给每个事务维护一个惟一的事务 ID,事务 ID 是严格递增分配的,也就是后开启的事务的事务 ID 必定比先开启的事务的事务 ID 要大。由于经过 undo log 日志能够获得多个版本的数据,能够假想在数据库中每一个数据有多个版本。每一个事务更新一个数据时,就会生成一个新版本数据而且将本身的事务 ID 贴在这个版本的数据上,用来标识这个数据的版本。
当开启一个新的事务时,InnoDB 会为每个事务维护一个数组,这个数组中保存了当前活跃的事务的事务ID,所谓活跃的事务指事务已经开始,可是还未提交的事务。在这个数组中最小的事务 ID 将其称为低水位,最大的事务 ID 加1称为高水位。当某个事务读取某条数据时,从该数据的最高版本开始,若是读得起那么就取这个数据,若是读不起就取更低一个版本的数据,如此循环,直到能读取有效数据。
在判断读得起和读不起时就只有如下几种状况:
数据版本号大于等于事务的高水位,说明是后面的事务建立的,读不起;
数据版本号小于等于低水位,说明是事务开启前就已经提交的,或者是本事务本身修改的,读得起;
数据版本号介于高水位和低水位之间,若是该版本号在数组里,说明是未提交的,读不起。
数据版本号介于高水位和低水位之间,若是该版本号不在数组里,说明是已经提交的,读的起。
提交读和可重复读的区别在于,提交读每次执行语句前更新这个数组,这样已经提交的数据就不在数组里,就会被看到,可重复读就是始终使用事务开启时生成的数组。
InnoDB 给每个事务生成一个惟一事务 ID 的方法称为生成快照,所以这种场景称为快照读。可是对于更新数据不能使用快照读,由于更新数据时若是使用快照读会可能会覆盖其余事务的更改。另外查询时若是加锁也会采用当前读的方式。当前读就是读这个数据最新的提交数据。InnoDB 的多版本并发控制实现了在串行化的隔离级别下读不加锁,提升了并发性能。
下面经过一个例子来理解快照读和当前读:
首先建一个表 t,并插入一条数据。
mysql-> create table t(k int)ENGINE=InnoDB;
mysql-> insert into t(k) values (1);
复制代码
而后将事务的隔离级别设置为 REPEATABLE-READ,接着开启三个事务,并按照下面的顺序进行执行。
事务 A | 事务 B | 事务 C |
---|---|---|
start transaction with consistent snapshot | ||
start transaction with consistent snapshot | ||
select k fromt t; | ||
select k from t; | ||
update t set k = k + 1; | ||
update t set k = k + 1; | ||
select k from t; commit; | ||
select k from t; commit; |
结果是:事务 A 两次读取的结果都是1,事务 B 第一次读取的结果是1,第二次读取的结果是 3。事务 A 两次都是快照读,在可重复读的隔离级别下,所以两次读到的结果相同。事务 B 第一次是快照读,可是 update 语句进行了一次当前读将 k 的值更新为事务 C 已经提交的结果 2,而且在此基础上再加1获得3。执行了 update 操做时会建立一个新版本的数据,而且将本身的事务 ID 做为该数据的版本号,所以在该事务内能够读到本身更新的数据。所以事务 B 最后一次查询的结果是 3。
最近在学习 MySQL 的原理,一篇文章作个笔记。
[1] 数据库系统概念(第6版)
[2] MySQL实战45讲,林晓斌
[3] 高性能MySQL(第3版)