一文搞懂MySQL事务隔离

文章放置于:https://github.com/zgkaii/CS-Study-Notes,欢迎批评指正!

事务关键属性

提到事务,大家都不陌生,和数据库打交道的时候,我们总是会用到事务。最经典的例子就是转账,你要给朋友转100块钱,转账过程具体到程序里会有一系列的操作,比如查询余额、做加减法、更新余额等,这些操作必须保证是一体的,这就涉及到“事务”这个概念了。

简单来说,事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元(unit),要么全部执行,要么全部不执行,是不可分割的工作单元

MySQL是一个支持多引擎的系统,但并不是所有的引擎都支持事务,事务能否生效取决于数据库引擎是否支持事务。常用的MySQL 数据库默认使用innodb引擎是支持事务的。但是,如果把数据库引擎变为 MYISAM,那么也就不再支持事务了。

提到事务,肯定会想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)。

  • 原子性(atomicity):
    “原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
  • 一致性(consistency):
    “一致”指的是数据的一致,具体是指所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
  • 隔离性(isolation):
    在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
  • 持久性(durability):
    持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

举一个例子:在执行SQL语句的时候,某些业务要求(如一个转账操作),一系列操作必须全部执行,而不能只执行一部分。

假如我们从id=1的A账户给id=2的B账户转账100元

-- 第一步:将id=1的A账户余额(500)减去100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 第二步:将id=2的B账户余额(500)加上100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

从A账户到B账户转账从6个详细操作:
(1)从A账户中把余额读出来(500)。
(2)对A账户做减法操作(500-100)。
(3)把结果写回A账户中(400)。
(4)从B账户中把余额读出来(500)。
(5)对B账户做加法操作(500+100)。
(6)把结果写回B账户中(600)。

事务的ACID特性就体现在:

  • 原子性:保证1-6所有过程要么都执行,要么都不执行。一旦在执行某一步骤的过程中发生问题,就需要执行回滚操作。假如执行到第五步的时候,B账户突然不可用(比如被注销),那么之前的所有操作都应该回滚到执行事务之前的状态。

  • 一致性:在转账之前,A和B的账户中共有500+500=1000元钱。在转账之后,A和B的账户中共有400+600=1000元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态。同时一致性还能保证账户余额不会变成负数等。

  • 隔离性:在A向B转账的整个过程中,只要事务还没有提交(commit),查询A账户和B账户的时候,两个账户里面的钱的数量都不会有变化。如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。

  • 持久性:一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)。

隔离性与隔离级别

这里先重点研究一下隔离性。当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)等并发问题。

假如现在有两个事务——A和B在并发执行修改一个Student表中age值:

  • 脏读(dirty read)
    1)A将某条记录的age值从20修改为30。
    2)B读取了A更新后的值:30。
    3)A回滚,age值恢复到了20。
    4)B读取到的30就是一个无效的值。
  • 不可重复读(non-repeatable read)
    1)A读取了age值为20。
    2)B将age值修改为30。
    3)A再次读取age值为30,和第一次读取不一致。
  • 幻读(phantom read)
    1)A读取了Student表中的一部分数据。
    2)B向Student表中插入了新的行。
    3)A读取了Student表时,多出了一些行或少了一些行。

为了解决上述这些问题,就有了“隔离性”的概念,它要求每个事务都应该与其他事务隔离开来,多个事务在并发执行过程中不会互相干扰。

一个事务与其他事务隔离的程度称为隔离级别。在谈隔离级别之前,首先要知道,隔离级别越高,数据一致性就越好,但效率越弱。因此很多时候,我们都要在二者之间寻找一个平衡点。SQL标准的事务隔离级别包括:读未提交(READ_UNCOMMITTED)、读提交(READ_COMMITTED)、可重复读(REPEATABLE_READ)和串行化(SERIALIZABLE)

隔离级别含义举例
READ_UNCOMMITTED最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。允许A读取B未提交的修改。
READ_COMMITTED允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。要求A只能读取B已提交的修改。
REPEATABLE_READ对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。确保A可以多次从一个字段中读取到相同的值,即A执行期间禁止其它事务对这个字段进行更新。
SERIALIZABLE最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读。确保A可以多次从一个表中读取到相同的行,在A执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

各个隔离级别所能解决的并发问题(x——不能解决,√——能解决):

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED×××
READ_COMMITTED××
REPEATABLE_READ×
SERIALIZABLE

其中“读提交”和“可重复读”比较难理解,下面就用一个例子说明这几种隔离级别。假设数据表T中只有一列,其中一行的值为1,下面是按照时间顺序执行两个事务的行为。

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
在不同的隔离级别下,事务A会有哪些不同的返回结果,也就是图里面V1、V2、V3的返回值分别是什么。
  • 若隔离级别是“读未提交”, 则V1的值就是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此,V2、V3也都是2。
  • 若隔离级别是“读提交”,则V1是1,V2的值是2。事务B的更新在提交后才能被A看到。所以, V3的值也是2。
  • 若隔离级别是“可重复读”,则V1、V2是1,V3是2。之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  • 若隔离级别是“串行化”,则在事务B执行“将1改成2”的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看, V1、V2值是1,V3的值是2。

实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

各种数据库产品对事务隔离级别的支持程度:

隔离级别OracleMySQL
READ_UNCOMMITTED×
READ_COMMITTED√(默认)
REPEATABLE_READ×√(默认)
SERIALIZABLE

Oracle数据库的默认隔离级别其实就是“读提交”,因此对于一些从Oracle迁移到MySQL的应用,为保证数据库隔离级别的一致,你一定要记得将MySQL的隔离级别设置为“读提交”。

配置的方式是,将启动参数transaction-isolation的值设置成READ-COMMITTED。你可以用show variables来查看当前的值。

mysql> show variables like 'transaction_isolation';

+-----------------------+----------------+

| Variable_name | Value |

+-----------------------+----------------+

| transaction_isolation | READ-COMMITTED |

+-----------------------+----------------+

哪个隔离级别都有它自己的使用场景,要根据具体的业务情况来定。比如下面这个数据校对逻辑的案例,如果是 MySQL数据库,该选择隔离级别呢?

假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。

这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。

事务隔离的实现

理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里以“可重复读”为例。

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值从1被按顺序改成了2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是**数据库的多版本并发控制(MVCC)**。对于read-view A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。

同时你会发现,即使现在有另外一个事务正在将4改成5,这个事务跟read-view A、B、C对应的事务是不会冲突的。

回滚日志总不能一直保留,什么时候删除呢?答案是——在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。那什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的read-view的时候。

理解上面知识后,就明白为什么不建议使用长事务了。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。

事务的启动方式

MySQL的事务启动方式有以下几种:

  1. 显式启动事务语句, begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。
START TRANSACTION;
-- 事务代码
commit;
  1. set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行commit 或 rollback 语句,或者断开连接。

有些客户端连接框架会默认连接成功后先执行一个set autocommit=0的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。因此,建议开发中总是使用set autocommit=1, 之后通过显式语句的方式来启动事务。

事务的查看与设置

查看当前会话隔离级别

方式一SHOW VARIABLES LIKE 'transaction_isolation';

mysql> show variables like 'transaction_isolation';
+-----------------------+--------------+
| Variable_name  | Value |
+-----------------------+--------------+
| transaction_isolation | SERIALIZABLE |
+-----------------------+--------------+

方式2SELECT @@transaction_isolation;

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE            |
+-------------------------+

设置隔离级别

方式一:通过set命令——SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

其中level有4种值:

level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}
  • 关键词:GLOBAL

表示只对执行完该语句之后产生的会话起作用,对当前已经存在的会话无效。

  • 关键词:SESSION

表示对当前会话的所有后续的事务有效。该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务;如果在事务之间执行,则对后续的事务有效。

  • 无关键词——SET TRANSACTION ISOLATION LEVEL level;

表示, 只对当前会话中下一个即将开启的事务有效。下一个事务执行完后,后续事务将恢复到之前的隔离级别。

该语句不能在已经开启的事务中间执行,会报错。

方式二:通过服务启动项命令——修改启动参数transaction-isolation的值

比方说启动服务器时指定了transaction-isolation=READ UNCOMMITTED,那么事务的默认隔离级别就从原来的REPEATABLE READ变成了READ UNCOMMITTED。

实际情况演示

下面就使用 2 个命令行,模拟多线程(多事务)对同一份数据的脏读问题。

首先创建一个users_test表:

DROP TABLE IF EXISTS users_test;
CREATE TABLE users_test (
	userid      int(10) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    username    varchar(10) NOT NULL COMMENT '用户名',
    password    varchar(16) NOT NULL COMMENT '密码',
    telephone   varchar(11) NOT NULL COMMENT '电话',
    sex		    varchar(2) NOT NULL COMMENT '性别',
    birthday    datetime NOT NULL COMMENT '生日',
    address     varchar(30) NOT NULL COMMENT '住址',
    PRIMARY KEY (userid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO users_test(userid,username,password,telephone,sex,birthday,address) VALUES 
    (12,'张三','1234','12345678911','男生','1998-12-12','上海')
    , (23,'王五','4321','98765432111','男生','2000-12-12','北京')
    , (1223,'李四','5642','23456789121','女生','1994-12-12','深圳');

查看当前表数据:

mysql> select * from users_test;
+------------+----------+----------+-------------+--------+---------------------+---------+
| userid     | username | password | telephone   | sex    | birthday            | address |
+------------+----------+----------+-------------+--------+---------------------+---------+
| 0000000012 | 张三     | 1234     | 12345678911 | 男生   | 1998-12-12 00:00:00 | 上海    |
| 0000000023 | 王五     | 4321     | 98765432111 | 男生   | 2000-12-12 00:00:00 | 北京    |
| 0000001223 | 李四     | 5642     | 23456789121 | 女生   | 1994-12-12 00:00:00 | 深圳    |
+------------+----------+----------+-------------+--------+---------------------+---------+

MySQL 命令行的默认配置中事务都是自动提交的,即执行SQL语句后就会马上执行 COMMIT 操作。如果要显式地开启一个事务需要使用命令:START TARNSACTION

我们可以通过下面的命令来设置隔离级别。

SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]

我们在下面实际操作中使用到的一些并发控制语句:

  • START TARNSACTION |BEGIN:显式地开启一个事务。
  • COMMIT:提交事务,使得对数据库做的所有修改成为永久性。
  • ROLLBACK:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。

脏读(读未提交)

解读如下:

(1)设置事务隔离级别为READ UNCOMMITTED;

(2)开启事务;

(3)第一次读只是确认数据,查询数据为1234;

(4)第二个事务开启;

(5)第二个事务修改数据,但未提交;

(6)读取到第二事务未提交的数据;

(7)第二个事务回滚;

(8)确认第二个事务进行了回滚,之前读取到的6666是个脏数据。

避免脏读(读已提交)

解读如下:

(1)重新设置事务隔离级别为READ COMMITTED;

(2)开启事务;

(3)第一次读只是确认数据,查询数据为8888;

(4)第二个事务开启;

(5)第二个事务修改数据,但未提交;

(6)因为事务 隔离级别为读已提交,所以不会发生脏读,数据仍为8888;

(7)提交事务;

(8)第二个事务提交后才能读取到更新的数据6666。

不可重复读

还是上面的读已提交的图,虽然避免了读未提交,但是第3、6步与第8步的数据不一样,一个事务还没有结束,就发生了 不可重复读问题。

可重复读

解读如下:

(1)重新设置事务隔离级别为REPEATABLE READ;

(2)开启事务;

(3)第一次读只是确认数据,查询数据为8888;

(4)第二个事务开启;

(5)第二个事务修改数据为9999,但未提交;

(6)第二次读结果不变;

(7)提交事务;

(8)因为事务 隔离级别为可重复读,第三次读结果仍然不变。

防止幻读(可重复读)

解读如下:

(1)第一个事务开启;

(2)第二个事务开始;

(3)第一个事务更改数据;

(4)第二个事务想要更改数据就需要等待。

一个事务对数据库进行操作,这种操作的范围是数据库的全部行,然后第二个事务也在对这个数据库操作,这种操作可以是插入一行记录或删除一行记录,那么第一个是事务就会觉得自己出现了幻觉,怎么还有没有处理的记录呢? 或者 怎么多处理了一行记录呢?

幻读和不可重复读有些相似之处 ,但是不可重复读的重点是修改,幻读的重点在于新增或者删除。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值