目录

1. 如果没有事务

2. 什么是事务

3. 为什么会出现事务

4. 事务的版本支持

5. 事务的提交方法

5.1 查看事务提交方式

5.2 用 set 改变 MySQL 的自动提交模式

6. 事务常见操作方式

6.1 查看/修改事务隔离级别

6.2 事务的开始与回滚

6.3 事务结束后,客户端崩溃,MySQL 数据不受影响,已经持久化

6.4 begin 不受 autocommit 的影响

6.5 单条 SQL 与事务的关系

6.6 结论

7. 事务隔离级别

7.1 如何理解隔离性

7.2 隔离级别

7.3 查看与设置隔离性

7.4 读未提交【Read Uncommitted】

7.5 读提交【Read Committed】

不可重复读可能造成的问题

7.6 可重复读【Repeatable Read】

insert 对可重复读的影响

7.8 串行化【serializable】

7.9 总结

7.10 一致性


1. 如果没有事务

1. CURD 不加控制,会有什么问题?

如果没有事务,在两件事并发的时候就会出现问题,比如只剩最后一张票了,这时候多人并发购票就会造成最后一张票被重复买走的情况

2. CURD 满足什么属性,能解决上述问题?

  • 买票的过程得是原子
  • 买票互相应该不能影响
  • 买完票应该要永久有效
  • 买前,和买后都要是确定的状态

2. 什么是事务

事务就是一组 DML(数据库操作语言) 语句组成,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。MySQL 提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。

事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:你毕业了,学校的教务系统后台 MySQL 中,不在需要你的数据,要删除你的所有信息,那么要删除你的基本信息(姓名,电话,籍贯等)的同时,也删除和你有关的其他信息,比如:你的各科成绩,你在校表现,甚至你在论坛发过的文章等。这样,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。

正如我们上面所说,一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?

所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

上面四个属性,可以简称为 ACID。

  • 原子性(Atomicity,或称不可分割性)
  • 一致性(Consistency)
  • 隔离性(Isolation,又称独立性)
  • 持久性(Durability)

3. 为什么会出现事务

事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。

(我们后面把 MySQL 中的一行信息,称为一行记录)

4. 事务的版本支持

在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务,MyISAM 不支持。

mysql> show engines \G         
*************************** 1. row ***************************
     Engine: InnoDB    -- 引擎名称
     Support: DEFAULT   -- 默认引擎
     Comment: Supports transactions, row-level locking, and foreign keys -- 描述
Transactions: YES       -- 支持事务
         XA: YES
 Savepoints: YES       -- 支持事务保存点
*************************** 5. row ***************************
     Engine: MyISAM    
     Support: YES
     Comment: MyISAM storage engine
Transactions: NO           -- MyISAM不支持事务
         XA: NO
 Savepoints: NO
*************************** 2. row ***************************
     Engine: MRG_MYISAM
     Support: YES
     Comment: Collection of identical MyISAM tables
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 3. row ***************************
     Engine: MEMORY    --内存引擎
     Support: YES
     Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 4. row ***************************
     Engine: BLACKHOLE
     Support: YES
     Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 6. row ***************************
     Engine: CSV
     Support: YES
     Comment: CSV storage engine
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 7. row ***************************
     Engine: ARCHIVE
     Support: YES
     Comment: Archive storage engine
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 8. row ***************************
     Engine: PERFORMANCE_SCHEMA
     Support: YES
     Comment: Performance Schema
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 9. row ***************************
     Engine: FEDERATED
     Support: NO
     Comment: Federated MySQL storage engine
Transactions: NULL
         XA: NULL
 Savepoints: NULL
9 rows in set (0.00 sec)

5. 事务的提交方法

事务的提交方式常见的有两种:

  • 自动提交
  • 手动提交

5.1 查看事务提交方式

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |  <=事务提交方式为默自动提交
+---------------+-------+
1 row in set (0.06 sec)

5.2 用 set 改变 MySQL 的自动提交模式

mysql> set autocommit=0;            #SET AUTOCOMMIT=0 禁止自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | OFF   |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=1;            #SET AUTOCOMMIT=1 开启自动提交
Query OK, 0 rows affected (0.01 sec)

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

6. 事务常见操作方式

6.1 查看/修改事务隔离级别

1. 查看事务隔离级别

select @@transaction_isolation;        ——MySQL 8.0 以后

select @@tx_isolation;                       ——MySQL 5.7之前

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

2. 修改事务隔离级别为读未提交

  • 修改后需要重启 MySQL 才能生效
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)


## 需要重启 MySQL 才能生效
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |
+-------------------------+
1 row in set (0.00 sec)

创建测试表

CREATE TABLE IF NOT EXISTS account (
  id INT PRIMARY KEY,
  name VARCHAR(50) NOT NULL DEFAULT '',
  blance DECIMAL(10,2) NOT NULL DEFAULT 0.0
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

6.2 事务的开始与回滚

  • 开始事务:start transaction; 或者 begin;
  • 结束事务:commit;
  • 保存节点:savepoint point_name;
  • 事务回滚到指定节点:rollback to point_name;
  • 事务回滚到最开始:rollback;
mysql> show variables like 'autocommit';  -- 事务设置为自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> start transaction;               -- 开始一个事务begin也可以,推荐begin
Query OK, 0 rows affected (0.00 sec)

mysql> savepoint save1;                -- 创建一个保存点save1
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100);   -- 插入一条记录
Query OK, 1 row affected (0.05 sec)

mysql> savepoint save2;                 -- 创建一个保存点save2
Query OK, 0 rows affected (0.01 sec)

mysql> insert into account values (2, '李四', 10000);  -- 在插入一条记录
Query OK, 1 row affected (0.00 sec)

mysql> select * from account;             -- 两条记录都在了
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> rollback to save2;                 -- 回滚到保存点save2
Query OK, 0 rows affected (0.03 sec)

mysql> select * from account;             -- 一条记录没有了
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> rollback;                          -- 直接rollback,回滚在最开始
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;             -- 所有刚刚的记录没有了
Empty set (0.00 sec)

6.3 事务结束后,客户端崩溃,MySQL 数据不受影响,已经持久化

事务 commit 结束后,客户端崩溃,MySQL 数据不会受影响,已经持久化在数据库中存储了。

--终端 A
mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> select * from account;   -- 当前表内无数据
Empty set (0.00 sec)

mysql> begin; -- 开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100); -- 插入记录
Query OK, 1 row affected (0.00 sec)

mysql> commit; --提交事务
Query OK, 0 rows affected (0.04 sec)

mysql> Aborted -- ctrl + \ 异常终止MySQL 
--终端 B
mysql> select * from account; 
化到MySQL中
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |--数据存在了,所以commit的作用是将数据持久
+----+--------+--------+
1 row in set (0.00 sec)

6.4 begin 不受 autocommit 的影响

begin操作会自动更改提交方式,不会受MySQL是否自动提交影响

-- 终端 A
mysql> select *from account; --查看历史数据
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> show variables like 'autocommit'; --查看事务提交方式
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> set autocommit=0; --关闭自动提交
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'autocommit'; --查看关闭之后结果
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (2, '李四', 10000); --插入记录
Query OK, 1 row affected (0.00 sec)

mysql> select *from account; --查看插入记录,同时查看终端B
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> Aborted --再次异常终止
-- 终端B
mysql> select * from account;       --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> select * from account;      --终端A崩溃后,自动回滚
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

6.5 单条 SQL 与事务的关系

    实验一:关闭事务自动提交,单条 SQL 不 commit 手动提交会有什么影响

    先说结论:关闭事务自动提交,单条 SQL 不 commit 手动提交,客户端崩溃后,之前未提交的这些 SQL 被回滚了,操作无效了

    -- 终端A
    mysql> select * from account;
    +----+--------+--------+
    | id | name   | blance |
    +----+--------+--------+
    |  1 | 张三   | 100.00 |
    +----+--------+--------+
    1 row in set (0.00 sec)
    
    mysql> show variables like 'autocommit';  
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit   | ON   |
    +---------------+-------+
    1 row in set (0.00 sec)
    
    mysql> set autocommit=0; --关闭自动提交
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into account values (2, '李四', 10000); --插入记录
    Query OK, 1 row affected (0.00 sec)
    
    mysql> select *from account; --查看结果,已经插入。此时可以再查看终端B
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   100.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    mysql> ^DBye --ctrl + \ or ctrl + d,终止终端
    
    --终端B
    mysql> select * from account;                    --终端A崩溃前
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   100.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account;                   --终端A崩溃后
    +----+--------+--------+
    | id | name   | blance |
    +----+--------+--------+
    |  1 | 张三   | 100.00 |
    +----+--------+--------+
    1 row in set (0.00 sec)
    

    实验二:开启事务自动提交,单条 SQL 不 commit 手动提交会有什么影响

    先说结论:开启事务自动提交,单条 SQL 不 commit 手动提交,客户端崩溃后,之前未手动提交的这些 SQL 不会被回滚,操作是有效的。

    --终端A
    mysql> show variables like 'autocommit'; --开启默认提交
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit   | ON   |
    +---------------+-------+
    1 row in set (0.00 sec)
    
    mysql> select * from account;
    +----+--------+--------+
    | id | name   | blance |
    +----+--------+--------+
    |  1 | 张三   | 100.00 |
    +----+--------+--------+
    1 row in set (0.00 sec)
    
    mysql> insert into account values (2, '李四', 10000);
    Query OK, 1 row affected (0.01 sec)
    
    mysql> select *from account; --数据已经插入
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   100.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    
    mysql> Aborted --异常终止
    --终端B
    mysql> select * from account; --终端A崩溃前
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   100.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account; --终端A崩溃后,并不影响,已经持久化。autocommit
    起作用
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   100.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)

    6.6 结论

    • 在开启事务自动提交的情况下,单条 sql 就是一个事务,无需手动提交(commit)
    • 在关闭事务自动提交的情况下,单条 sql 也需要手动提交(commit)
    • 只要输入 begin 或者 start transaction,事务便必须要通过 commit 提交,才会持久化,与是否设置 set autocommit 无关
    • 事务可以手动回滚,同时,当操作异常,MySQL 会自动回滚
    • 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select 有特殊情况,因为MySQL有 MVCC)

    从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)。

    事务操作注意事项

    • 如果没有设置保存点,也可以回滚,但只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)
    • 如果一个事务被提交了(commit),则不可以回退(rollback)
    • 可以选择回退到哪个保存点
    • InnoDB 支持事务,MyISAM 不支持事务
    • 开始事务可以使 start transaction 或者 begin

    7. 事务隔离级别

    7.1 如何理解隔离性

    MySQL 服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行。

    一个事务可能由多条 SQL 构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。

    但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个 SQL 的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。

    就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。

    数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。

    数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别

    7.2 隔离级别

    • 读未提交【Read Uncommitted】:在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
    • 读提交【Read Committed】:该隔离级别是大多数数据库的默认的隔离级别(MySQL默认是 Repeatable Read)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读(即一个事务执行时,如果多次 select,可能得到不同的结果)
    • 可重复读【Repeatable Read】:这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行(即别的事务对数据最终产生的影响,当前事务只有在结束后再次读才能看见,事务起降就算其他事务对数据产生影响,也读不到)但是会有幻读问题。
    • 串行化【Serializable】:这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。

    隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key 锁(GAP + 行锁)等。不过,我们目前现有这个认识就行,先关注上层使用。

    7.3 查看与设置隔离性

    1. 查看隔离性

    查看全局隔离级别:select @@global.transaction_isolation;

    查看当前会话隔离级别:select @@session.transaction_isolation; 或者

    select @@transaction_isolation; 

    上面的操作是 MySQL 8.0 之后的,如果是 MySQL 5.7 之前的,需要把 transaction 换成 tx,比如:

    select @@global.tx_isolation;

    ## 查看全局隔离级别
    mysql> select @@global.transaction_isolation;
    +--------------------------------+
    | @@global.transaction_isolation |
    +--------------------------------+
    | READ-UNCOMMITTED               |
    +--------------------------------+
    1 row in set (0.00 sec)
    
    ## 查看当前会话隔离级别
    mysql> select @@session.transaction_isolation;
    +---------------------------------+
    | @@session.transaction_isolation |
    +---------------------------------+
    | READ-UNCOMMITTED                |
    +---------------------------------+
    1 row in set (0.01 sec)
    
    ## 查看当前会话隔离级别
    mysql> select @@transaction_isolation;
    +-------------------------+
    | @@transaction_isolation |
    +-------------------------+
    | READ-UNCOMMITTED        |
    +-------------------------+
    1 row in set (0.00 sec)
    

    2. 设置隔离性

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

    set [session / global] transaction isolation level {read uncommitted / read committed / repeatable read / serializable};

    新启会话的隔离级在创建之初受全局隔离级别的影响

    7.4 读未提交【Read Uncommitted】

    -- 终端A
    mysql> begin; --开启事务
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update account set blance=123.0 where id=1; --更新指定行
    Query OK, 1 row affected (0.05 sec)
    
    Rows matched: 1 Changed: 1 Warnings: 0 --没有commit哦!!!
    --终端B
    mysql> begin;
    
    mysql> select * from account;
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   123.00 | --读到终端A更新但是未commit的数据[insert,delete同样]
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    

    一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未 commit 的数据,这种现象叫做脏读(dirty read)

    7.5 读提交【Read Committed】

    比如现在同时启动两个事务,终端A 负责更新数据,终端B 需要在终端A 更新数据后查看表的内容,看看终端A 这边的事务提交前后,终端B 这边查表的内容是否有变化

    --终端A
    mysql> begin; --手动开启事务,同步的开始终端B事务
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update account set blance=321.0 where id=1; --更新张三数据
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1 Changed: 1 Warnings: 0
    
    mysql> commit; --commit提交!
    Query OK, 0 rows affected (0.01 sec)
    

    可以看到,在终端A 提交事务后,终端B 此时还在当前事务中,并未commit,它看到了表的变化,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!

    --终端B
    mysql> begin; --手动开启事务,和终端A一前一后
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; --终端A commit之前,查看不到
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   123.00 | --老的值
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    
    --终端A commit之后,看到了!
    mysql> select *from account;              
    +----+--------+----------+ 
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   321.00 | --新的值
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)

    不可重复读可能造成的问题

    《年终奖事件·查表篇》

    年底了,老板把财务叫到办公室:"去查工资表,按表发年终奖。3 万以上的发金条,3 万以下的发银条。"

    财务使用 MySQL 开了个事务在查表准备挨个核对。

    他先查 3 万以下的,查到了小王月薪2万5,记下来:银条。

    刚记完,小王去找老板涨工资,老板同意后,数据库管理员就给小王的工资更新成 3万5了。

    财务还搁那里查表呢,结果因为事务是读提交的,是不可重复读的,小王的工资更新财务这边立马就能看见,于是财务查 3 万以上的人的时候把小王又记下来了。

    然后财务把统计结果给了老板,老板看到了以下两条数据:

    小王 月薪2万5 --发银条
    小王 月薪3万2 --发金条

    老板知道后气得拍桌子:"我让你查一次表按一次标准发,你怎么查出了两个小王?同一笔奖金,你读了两次工资,两次不一样,多花了一根金条!"


    这就是不可重复读:同一个事务(老李查工资表发奖金)里,两次读取同一行数据(小王的工资),中间被另一个事务(涨工资并更新表)修改了,导致两次读到的值不一致,最终结果出错。


    7.6 可重复读【Repeatable Read】

    两个事务同时进行,事务A 提交后的结果,事务B 在事务期间无法看到,只有结束事务B 后再次查询才能看到,这样可以保证事务B 在事务期间每次读到的信息都不会变(不会多出没进行过的操作),这就是可重复读。

    --终端A
    mysql> select *from account; --查看当前数据
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   321.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    
    mysql> begin; --开启事务,同步的,终端B也开始事务
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update account set blance=4321.0 where id=1; --更新数据
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1 Changed: 1 Warnings: 0
    
    --切换到终端B,查看另一个事务是否能看到
    mysql> commit;   --提交事务
    
    --终端B
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account;  --终端A中事务 commit之前,查看当前表中数据,数据未更新
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   321.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account; --终端A中事务 commit 之后,查看当前表中数据,数据未更新
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |   321.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    --可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
    
    mysql> commit; --结束事务
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; --再次查看,看到最新的更新数据
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |  4321.00 |
    |  2 | 李四   | 10000.00 |
    +----+--------+----------+
    2 rows in set (0.00 sec)
    

    insert 对可重复读的影响

    一般的数据库在可重复读情况的时候,无法屏蔽其他事务 insert 的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是 insert 的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,而 MySQL 在 RR 级别的时候,是解决了幻读问题的(解决的方式是用 Next‑Key 锁(GAP+行锁)解决的。这块比较难,有兴趣可以自行了解一下)。

    7.8 串行化【serializable】

    对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用

    --终端A
    mysql> begin; --开启事务,终端B同步开启
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; --两个读取不会串行化,共享锁
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |  4321.00 |
    |  2 | 李四   | 10000.00 |
    |  3 | 王五   |  5432.00 |
    +----+--------+----------+
    3 rows in set (0.00 sec)
    
    --终端A中有更新或者其他操作,会阻塞。直到终端B事务提交。
    mysql> update account set blance=1.00 where id=1; 
    Query OK, 1 row affected (18.19 sec)
    Rows matched: 1 Changed: 1 Warnings: 0
    
    --终端B
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    mysql> select * from account;    --两个读取不会串行化
    +----+--------+----------+
    | id | name   | blance   |
    +----+--------+----------+
    |  1 | 张三   |  4321.00 |
    |  2 | 李四   | 10000.00 |
    |  3 | 王五   |  5432.00 |
    +----+--------+----------+
    3 rows in set (0.00 sec)
    
    mysql> commit; --提交之后,终端A中的update才会提交。
    Query OK, 0 rows affected (0.00 sec)
    

    7.9 总结

    • 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
    • 不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了,幻读的重点在于新增:同样的条件,第 1 次和第 2 次读出来的记录数不一样。
    • mysql 默认的隔离级别是可重复读,一般情况下不要修改。
    • 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有 commit 的时候,影响会比较大。

    7.10 一致性

    • 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
    • 其实一致性和用户的业务逻辑强相关,一般 MySQL 提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。
    • 而技术上,ACID 是通过 AID 来保证 C 的。

    更多推荐