项目开发中MySQL应该选择什么事务隔离级别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gu_wen_jie/article/details/88540918

读了一篇讲述MySQL事务隔离级别的帖子,加上自己的一点理解写了这篇文章(文中部分RC举例为引用)
先借用一个例子,下面这个场景你可能遇到过。
A:“MySQL有几个事务隔离级别?”
B:“读未提交,读已提交,可重复读,串行化四个!默认是可重复读”
A:“为什么mysql选可重复读作为默认的隔离级别?”
B:"…"
A:“你们项目中选了哪个隔离级别?为什么?”
B:“默认的可重复读,原因…”
那么这个到底如何作答呢?

先来介绍一下MySQL的事务隔离级别。

MySQL的四种事务隔离级别

首先,如何查询MySQL的事务隔离级别:

select @@tx_isolation;

如下:

+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
一、事务的基本要素(ACID)

1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

二、事务的并发问题

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

三、MySQL事务隔离级别
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

MySQL的默认事务隔离级别确实如例子中所说的:可重复读(repeatable-read)RR,不过我们日常在项目开发中是否使用的就是默认隔离级别呢?
答案是:否。因为在项目中我们一般将MySQL的事务隔离级别设置为不可重复读(read-committed)RC,也就是读已提交
有人会说了,这个隔离级别不是存在不可重复读幻读吗?这个问题不需要注意吗?

例子说明

用过SqlServer的同学知道在SqlServer中事务隔离级别选择的是读已提交 RC作为默认隔离级别。那么为什么MySQL 却选择可重复读 RR作为默认的隔离级别?

1.先来说下主从复制。

主从复制,是基于什么复制的?
binlog有几种格式?
先看几个名词:

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,因此从机执行的顺序和主机不一致!就会出现主从不一致!

2.如何解决
  • 隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住!

  • 将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!

那么,当我们了解完mysql选可重复读(Repeatable Read)作为默认隔离级别的原因后,接下来我们将其和读已提交(Read Commited)进行对比,来说明为什么在项目开发中为什么将隔离级别设为读已提交RC

3.对比

首先我们要明白:项目中是不用读未提交(Read UnCommitted)串行化(Serializable)两个隔离级别,原因有二

  • 采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据,这个不用多说吧,从逻辑上都说不过去!
  • 采用串行化(Serializable),每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别!(笔者从未用过mysql自带的这个功能,因为这是XA事务,是强一致性事务,性能不佳!互联网的分布式方案,多采用最终一致性的事务解决方案!)

现在我们的问题就是:隔离级别是用RC呢还是RR
下面的例子将讲解下我们为什么选用读已提交(Read Commited) RC

以下说明采用简称:可重复读(Repeatable Read),简称为RR; 读已提交(Read Commited),简称为RC;

假设表结构如下:

 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 |
+----+-------+

:原因1:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!
执行:

select * from test where id <3 for update;

在RR隔离级别下,存在间隙锁,可以锁住(2,5)这个间隙,防止其他事务插入数据!
而在RC隔离级别下,不存在间隙锁,其他事务是可以插入数据!
注意:在RC隔离级别下并不是不会出现死锁,只是出现几率比RR低而已!

:原因2:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行
执行:

update test set color = 'blue' where color = 'red'; 

在RC隔离级别下,其先走聚簇索引,进行全部扫描。加锁如下:
在这里插入图片描述
但在实际中,MySQL做了优化,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁。

实际加锁如下:
在这里插入图片描述
然而,在RR隔离级别下,走聚簇索引,进行全部扫描,最后会将整个表锁上,如下所示:
在这里插入图片描述
:原因3:在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!
在5.1.15的时候,innodb引入了一个概念叫做“semi-consistent”,减少了更新同一行记录时的冲突,减少锁等待。

所谓半一致性读就是,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!

具体表现如下:

此时有两个Session,Session1Session2

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只能等待!

4.问题

1.在RC级别下,不可重复读问题需要解决么?
不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!

2.在RC级别下,主从复制用什么binlog格式?
在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!

可重复读(repeatable-read)隔离级别举例

事实上,我们很多同学在刚做开发的时候,如果是在自己搭建数据库的环境下去做开发,并不会去修改MySQL事务隔离级别,那么这时候使用的就是RR隔离级别。
现在假设一个抽奖活动项目,如果两个用户同时抽奖,而且同时中奖。两者都进入了中奖的事务。A事务扣减了奖品数量,B也执行了扣减数量。假设奖品数量是N,如果是可重复读,那么,如果两个事务并行进行,那么不论A有没有提交,B读到的数量都是N,执行后为N-1,而事务A也是N-1,这样不就有问题了吗?我们期望的是N-2。
那么实际上会出现这样的问题吗?
可以了解一下:快照读当前读
在事务中,执行普通select查询之后,会创建快照,后面再执行相同的select语句时,查询的其实是前面生成的快照。这也就是为什么会有可重复读。
如果这时候执行:

select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (); 
update table set ? where ?; 
delete from table where ?;

会执行当前读,获取最新数据。回到前面的问题,如果事务B执行N-1操作,会触发当前读,读取事务A提交后的数据,也就是N-1,在此基础上执行-1操作,最终N变成N-2。
这种场景下会遇到另一个问题:并发更新
上面解决了事务A已经提交的额情况。但如果事务A更新奖品数量后但还未提交呢?此时事务B执行当前读拿到的也是N啊。了解数据库锁机制的话,就不会有这种困惑了。事务A提交前,会一直持有排他锁(具体是行锁还是表锁,要看查询条件有没有走索引),此时事务B更新是会阻塞的。也就是说,只有事务A提交,或回滚之后,事务B才能获得排它锁,从而进行更新奖品的操作。

总结

虽然对RR隔离级别进行了一个举例,但是还是建议在项目开发中MySQL的事务隔离级别请采用不可重复读(read-committed)= 读已提交(read-committed) = RC
开头说了如何查看事务隔离级别,最后附上如何修改:
如何设置事务隔离级别(例如设置为读已提交 RC):

set session transaction isolation level read committed;
展开阅读全文

没有更多推荐了,返回首页