BruceMing

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
学习MySQL之前,我们先了解下什么是事务?
数据库事务通常包含了一个序列的对数据库的读/写操作。这些操作要么全部执行,要么全部不执行,是⼀个不可分割的独⽴的⼯作单元。
 
例⼦:
银⾏应⽤是⼀个经典的例⼦,假设⼀个银⾏的数据库有两张表:⽀票(checking)表和储 蓄
(savings)表。现在要从Bruce的⽀票账户移200美元到她的储蓄账户,那么需要⾄少
三个步骤:
1. 检查⽀票账户的余额⾼于200美元。
2. 从⽀票账户余额中减去200美元。
3. 在储蓄账户余额中增加200美元。
 

支持事务的数据库管理系统transactional DBMS)就是要确保以上操作(整个“事务”)都能完成,或一起取消;否则就会出现200美元平白消失或出现的情况。

但在现实情况下,失败的风险很高。在一个数据库事务的执行过程中,有可能会遇上事务操作失败、数据库系统/操作系统出错,甚至是存储介质出错等情况。这便需要DBMS对一个执行失败的事务执行恢复操作,将其数据库状态恢复到一致状态(数据的一致性得到保证的状态)。为了实现将数据库状态恢复到一致状态的功能,DBMS通常需要维护事务日志以追踪事务中所有影响数据库数据的操作。

(一)、ACID
并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为ACID特性
 
(1)原⼦性(Atomicity)
⼀个事务必须被视为⼀个不可分割的最⼩⼯作单元,整个事务中所有的操作要么全部提交成功,要么全部失败回滚,对于⼀个事务来说,不可能只执⾏其中的⼀部分操作,这就是事务 的原⼦性。
(2)⼀致性(Consistency)
数据库总是从⼀个⼀致性的状态转换到另外⼀个⼀致性的状态,必须满足定义好的规则,包括数据完整性。“一致”是指数据库中的数据是正确的,不存在矛盾。比如前面提到的,支票账户减去200美元,对应储蓄账户就应该增加200美元。
(3)隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行,通常来说,⼀个事务所做的修改在最终提交之前,对其他事务是不可⻅的。
(4)持久性(Durability)
⼀旦事务提交,则其所做的修改就会永久保存到数据库中。这⾥持久性是个有点模糊的概念,实际上持久性也分很多不同的级别。
 
(二)事务并发产生的问题
(1)脏读
脏读说的是事务A在执行过程中读取到了事务B还未提交的数据,如果B事务此时发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。如下图场景:
(2)不可重复读
不可重复读指的是,多个事务并行执行时,事务A两次读取的结果不一样。这是因为两次查询有间隔,期间被其他事务修改并提交,相比于脏读,不可重复读是读取到了事务B已提交的数据。这种现实是正常的,是由于事务隔离级别造成的,但是在某些特殊场景下不被允许。如下图场景:

 

 

(3)幻读
幻读指的是,多事务并行执行时,事务A第一次统计和第二次统计的结果不一致,是因为事务B新增了一条数据。和不可重复读一样,都是读取到了另一个事务已提交的数据,但是不同的是不可重复读查的是同一条数据,而幻读查的是一批数据。或者说,不可重复读是因为A读取了B更新了的数据,幻读是A读取了事务B新增的数据。如下图所示:
 
 
(三)事务隔离级别
(1)Read Uncommitted(未提交读)
在 READ UNCOMMITTED 级别,事务中的修改,即使没有提交,对其他事务也都是可⻅的。事务可以读取到其它事务未提交的数据,在此隔离级别下存在脏读,不可重复读,幻读问题。
 
(2)Read Committed (已提交读)
⼤多数数据库系统默认的隔离级别都是 READ COMMITTED (但MySQL不是),⼀个事务从开始直到提交,所做的任何修改对其他事务都是不可见的。在此隔离级别下,存在不可重复读,幻读问题。
 
(3)Repeatable Read (可重复读)
MySQL的默认隔离级别。该级别保证了在同⼀事务中多次读取同样记录的结果是⼀致的。但⽆法解决幻读(Phontom Read)问题,InnoDB和XtraDB通过多版本并发控制MVCC来解决幻读问题。
 
(4)Serializable (串行化)
Serializable最⾼的隔离级别。它通过强制事务串⾏执⾏,避免了前⾯说的幻读的问题。Serializable 会在读取的每⼀⾏数据上都加锁,所以可能导致⼤量的超时和锁争⽤的问题,实际中基本不会使⽤。
 

 我们通过mysql数据库来模拟一下以上场景:

  1.首先创建一张t_wallet钱包表,初始化一条balance为1000,id为1的数据。

复制代码
mysql> create table t_wallet(
    ->  id int primary key auto_increment comment '主键id',
    ->  account varchar(10) comment '账户',
    ->  balance decimal(6, 2) comment '账户余额'
    -> ) comment = '账户余额表';
Query OK, 0 rows affected (0.03 sec)

mysql> insert into t_wallet(account, balance) values('小明', 1000);
Query OK, 1 row affected (0.01 sec)
复制代码

  2.设置事务自动提交关闭,set autocommit = 0 | OFF;

复制代码
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set, 1 warning (0.01 sec)

mysql> set autocommit = OFF;
Query OK, 0 rows affected (0.00 sec)
复制代码

  3.我们打开两个session会话,修改当前MySQL事务隔离级别为未提交读,分别开启事务A和事务B,事务A执行查询余额操作,获取到账户小明的当前balance=1000.

注意我本地用的是mysql 8,事务隔离级别属性是 transaction_isolation,mysql 8 之前是 tx_isolation.

事务A:

复制代码
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |
+-------------------------+
1 row in set (0.00 sec)

mysql> select * from t_wallet where account = '小明';
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)
复制代码

  5.开启事务B,先查询到小明账户有1000余额,然后事务B执行扣除1000余额操作,此时事务B还未提交。事务A查询余额,此时可以看到余额为0,事务A读取到了事务B还未提交的数据。

事务B:

复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

mysql> update t_wallet set balance = balance - 1000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    |    0.00 |
+----+---------+---------+
1 row in set (0.00 sec)
复制代码

事务A:

获取到当前余额为0。

mysql> select * from t_wallet where account = '小明';
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    |    0.00 |
+----+---------+---------+
1 row in set (0.00 sec)

事务B:
最后再回滚事务B。这就是脏读

复制代码
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)
复制代码

  6.修改事务隔离级别为已提交读。再次执行以上SQL.

设置session事务隔离级别为已提交读:

mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

事务B:

复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

mysql> update t_wallet set balance = balance - 1000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    |    0.00 |
+----+---------+---------+
1 row in set (0.00 sec)
复制代码

事务A:
当前隔离级别为已提交读,再次查看余额,发现仍是1000。脏读问题解决

复制代码
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-COMMITTED          |
+-------------------------+
1 row in set (0.00 sec)

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)
复制代码

   7.当前为已提交读隔离级别,验证不可重复读问题。开始事务A,第一次读取余额为1000。

事务A:

第一次读取余额1000.

mysql> select * from t_wallet where id = 1;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

  8.事务B开启,并扣除余额1000,并提交事务。

事务B:

复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_wallet where id = 1;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

mysql> update t_wallet set balance = balance - 1000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
复制代码

  9.此时,事务A再次读取,发现余额为0。两次读取的结果不一致。

事务A:

mysql> select * from t_wallet where id = 1;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    |    0.00 |
+----+---------+---------+
1 row in set (0.00 sec)

  10.我们再次修改隔离级别为可重复读,重置小明账户余额为1000。再次执行 7 ~ 9 步执行的SQL。

修改事务隔离级别为可重复读:

复制代码
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)
复制代码

事务A:

第一次读取余额1000.

mysql> select * from t_wallet where id = 1;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

事务B:

复制代码
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_wallet where id = 1;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

mysql> update t_wallet set balance = balance - 1000 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
复制代码

事务A:

第二次读取余额仍然是1000。不可重复读问题解决

mysql> select * from t_wallet where id = 1;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

   11.假如此时,我们不结束事务A,事务B新增一条记录 (小王,500)

 事务B:

复制代码
mysql> insert into t_wallet(account, balance) values('小王', 500);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    |    0.00 |
|  2 | 小王    |  500.00 |
+----+---------+---------+
2 rows in set (0.00 sec)
复制代码

 事务A:

查询表数据,发现仍然只查询到余额为1000的小明的账户记录。发现没有出现幻读现象,这个主要是依赖MVCC多版本控制帮我们解决的,后续笔记会详细说明。

mysql> select * from t_wallet;
+----+---------+---------+
| id | account | balance |
+----+---------+---------+
|  1 | 小明    | 1000.00 |
+----+---------+---------+
1 row in set (0.00 sec)

(四)问题思考

(1)为什么MySQL的默认隔离级别是RR? 为什么实际生产环境MySQL隔离级别使用是RC?

至于MySQL默认隔离级别为什么是RR,这个要从MySQL主从复制说起。因为MySQL主从复制基于binlog,binlog的记录顺序是按照事务commit顺序为序的。我们知道binlog有三种格式。分别如下:
(1)statement
(2)row
(3)mixed 前两种结合
statement记录的是sql语句,而row格式记录的是执行的逻辑过程。statement占用空间小,但是会出现主备不一致的问题。row占用空间多,但是不会出现主备不一致的问题。在MySQL5.0之前,binlog只支持statement格式。
因为statement只是记录了sql,重新恢复的时候,在RC隔离级别下,可能有事务提交的顺序问题,以及索引选择不同问题,在一些语句执行上会出现歧义。RR为了实现真正的“可重复读”,引入了间歇锁(Gap lock),不仅锁住了行,而且锁住了行与行之间的间隙,避免出现幻读。所以因为这个主从复制的历史问题,MySQL默认隔离级别是RR。
 
正因为引入了间隙锁,锁多了自然而然地降低了并发性能,并且引发死锁的概率相比RC隔离级别要高很多。所以实际生产环境,我们都一般设置MySQL隔离级别为RC,而且在业务层面上看,RC隔离级别读取到已提交的事务是符合实际场景的。
posted on   BruceMing  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示