事务是并发控制的基本单位。所谓事务就是一个操作序列,在序列里面的操作,要么全部执行,要么都不执行(原子性),是一个不可分割的工作单位,每个事务结束时,都能保持数据的一致性。
事务的基本特性 ACID:
- 原子性(Atomic):事务中的操作要么全部成功,要么全部失败。
- 一致性(Consistency): 从一个正确的状态到另一个正确的状态,只有合法的才可以被写入,所有不正确的,不合法的都会被回滚
- 隔离性(Isolation):事务允许多个用户对同一个数据并发操作,而不破坏数据的正确性和完整性。同时,并行的事务修改必须与其他并行的事务的修改互相独立
- 持久性 (Durability):事务完成后,对数据的影响的永久性的。
事务的隔离级别
试想一下,当我们在并发对同一个数据执行事务时,A提交了update,B提交了delete ,如果不进行控制,那么势必会破坏数据的一致性。所以我们必须让事务与事务直接是隔离的,彼此之间没有干扰或者保证一致性情况下,能承受的,不照成破坏性的干扰。于是就有了一个规范 :事务隔离级别
Mysql的事务隔离级别
MySQL定义了4种事务隔离级别:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
READ_UNCOMMITTED(读未提交) | 允许 | 允许 | 允许 |
READ_COMMITTED(读提交) | 禁止 | 允许 | 允许 |
REPEATABLE_READ(可重复读) | 禁止 | 禁止 | 允许 |
SERIALIZABLE(串行化) | 禁止 | 禁止 | 禁止 |
隔离级别从上到下,从低到高,隔离级别越低,对并发处理的支持越高,并拥有更低的系统开销,但是同时也破坏了事务的一致性。隔离级别的主要目的就是保证数据的一致性,说到保证一致性,那就要讲到事务处理中,会遇到的三个主要问题。脏读、不可重复读、幻读
脏读
比如有2个事务并发执行竞争,一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”。
时间 | 事务A | 事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询余额(1000)元 | |
T4 | 取出(1000)元 (余额0) | |
T5 | 查询余额(0) | |
T6 | 撤销事务(余额恢复1000元) | |
T7 | 存入500元(余额500) | |
T8 | 提交事务(最终余额500) |
余额应该是1500元才对,但是由于读到了事务操作的脏数据,导致了脏读问题。
不可重复读
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
时间 | 事务A | 事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询余额(1000) | |
T4 | 查询余额(1000) | |
T5 | 取出(500) | |
T6 | 提交事务 | |
T7 | 查询余额(500) |
事务A还没有操作,只是重复查询了2次,但是结果从1000变成了500,重复查询的结果不一致,这就是不可重复读。但是这种现象也是合理的,因为事务B提交了事务,事务B的操作结果是一致性的。事务A出现了不可重复读
幻读
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
时间 | 事务A | 事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计总存款数(1000) | |
T4 | 存入100 | |
T5 | 提交事务 | |
T6 | 统计总存款数(10100) |
幻读就是每次读到的数据可能不一样,可能会读到了其他事务新插入的数据。
简单来说:
- 脏读:事务A读取了事务B未提交的数据,并在这个脏数据上执行了操作
- 不可重复读:事务A读取了事务B已提交的更新数据。
- 幻读:事务A的查询条件读取到了事务B已经提交的新增插入数据。
那么在我们的应用中,为了应对这种情况,就要使用到我们的事务隔离了,那么应该怎么样去选择事务隔离的级别呢?
首先,脏读是不可容忍的,为了保持一致性,我们必须禁止出现脏读现象。而不可重复读和幻读,他们最终的结果是一样的,没有影响一致性。
所以说,最起码要做到不可脏读,也就是只有一下3个等级可以选择:
READ_COMMITTED(读提交) 、REPEATABLE_READ(可重复读)、SERIALIZABLE(串行化)
SERIALIZABLE(串行化) : 这是最高的隔离级别,禁止了上面出现的3个问题,也就是通过串行阻塞来保证事务的强一致性,但是会明显降低并发性能,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。。
REPEATABLE_READ(可重复读) : MySql的默认级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。InnoDB的间隙锁。
READ_COMMITTED(读提交):这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
MySQL为什么默认REPEATABLE_READ(可重复读)隔离级别?
https://www.cnblogs.com/rjzheng/p/10510174.html 这篇文章解释的很清楚。以下是文章内容
Mysql默认的事务隔离级别是可重复读(Repeatable Read),那互联网项目中Mysql也是用默认隔离级别,不做修改么?
OK,不是的,我们在项目中一般用读已提交(Read Commited)这个隔离级别!
what!居然是读已提交,网上不是说这个隔离级别存在不可重复读
和幻读
问题么?不用管么?好,带着我们的疑问开始本文!
我们先来思考一个问题,在Oracle,SqlServer中都是选择读已提交(Read Commited)作为默认的隔离级别,为什么Mysql不选择读已提交(Read Commited)作为默认隔离级别,而选择可重复读(Repeatable Read)作为默认的隔离级别呢?
这个是有历史原因的,当然要从我们的主从复制开始讲起了!
主从复制,是基于什么复制的?
是基于binlog复制的!这里不想去搬binlog的概念了,就简单理解为binlog是一个记录数据库更改的文件吧~
binlog有几种格式?
OK,三种,分别是
- statement:记录的是修改SQL语句
- row:记录的是每行实际数据的变更
- mixed:statement和row模式的混合
那Mysql在5.0这个版本以前,binlog只支持STATEMENT
这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!
接下来,就要说说当binlog为STATEMENT
格式,且隔离级别为读已提交(Read Commited)时,有什么bug呢?如下图所示,在主(master)上执行如下事务
此时在主(master)上执行下列语句
select * from test;
输出如下
+---+
| b |
+---+
| 3 |
+---+
1 row in set
但是,你在此时在从(slave)上执行该语句,得出输出如下
Empty set
这样,你就出现了主从不一致性的问题!原因其实很简单,就是在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删!从(slave)同步的是binglog,因此从机执行的顺序和主机不一致!就会出现主从不一致!
如何解决?
解决方案有两种!
(1)隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1
执行delete语句时,会锁住间隙。那么,Ssession 2
执行插入语句就会阻塞住!
(2)将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!
那么,当我们了解完mysql选可重复读(Repeatable Read)作为默认隔离级别的原因后,接下来我们将其和读已提交(Read Commited)进行对比,来说明为什么在互联网项目为什么将隔离级别设为读已提交(Read Commited)!
对比
ok,我们先明白一点!项目中是不用读未提交(Read UnCommitted)和串行化(Serializable)两个隔离级别,原因有二
- 采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据,这个不用多说吧,从逻辑上都说不过去!
- 采用串行化(Serializable),每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别!(笔者从未用过mysql自带的这个功能,因为这是XA事务,是强一致性事务,性能不佳!互联网的分布式方案,多采用最终一致性的事务解决方案!)
也就是说,我们该纠结都只有一个问题,究竟隔离级别是用读已经提交呢还是可重复读?
接下来对这两种级别进行对比,讲讲我们为什么选读已提交(Read Commited)作为事务隔离级别!
假设表结构如下
CREATE TABLE `test` (
`id` int(11) NOT NULL,
`color` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
数据如下
+----+-------+
| id | color |
+----+-------+
| 1 | red |
| 2 | white |
| 5 | red |
| 7 | white |
+----+-------+
为了便于描述,下面将
- 可重复读(Repeatable Read),简称为RR;
- 读已提交(Read Commited),简称为RC;
缘由一:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!
此时执行语句
select * from test where id <3 for update;
在RR隔离级别下,存在间隙锁,可以锁住(2,5)这个间隙,防止其他事务插入数据!
而在RC隔离级别下,不存在间隙锁,其他事务是可以插入数据!
ps
:在RC隔离级别下并不是不会出现死锁,只是出现几率比RR低而已!
缘由二:
在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行
此时执行语句
update test set color = 'blue' where color = 'white';
在RC隔离级别下,其先走聚簇索引,进行全部扫描。加锁如下:

但在实际中,MySQL做了优化,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁。
实际加锁如下

然而,在RR隔离级别下,走聚簇索引,进行全部扫描,最后会将整个表锁上,如下所示

缘由三:
在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!
在5.1.15的时候,innodb引入了一个概念叫做“semi-consistent”,减少了更新同一行记录时的冲突,减少锁等待。
所谓半一致性读就是,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!
具体表现如下:
此时有两个Session,Session1和Session2!
Session1执行
update test set color = 'blue' where color = 'red';
先不Commit事务!
与此同时Ssession2执行
update test set color = 'blue' where color = 'white';
session 2尝试加锁的时候,发现行上已经存在锁,InnoDB会开启semi-consistent read,返回最新的committed版本(1,red),(2,white),(5,red),(7,white)。MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!
而在RR隔离级别下,Session2只能等待!
两个疑问
在RC级别下,不可重复读问题需要解决么?
不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle的默认隔离级别就是RC,你们改过Oracle的默认隔离级别么?
在RC级别下,主从复制用什么binlog格式?
OK,在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!
总结
本文啰里八嗦了一篇文章只是为了说明一件事,互联网项目请用:读已提交(Read Commited)这个隔离级别!