• MySQL的事务基础


    前要:在网络服务中,如果对 CURD 不加限制,会出现什么问题?

    在多个客户端同时向一个卖票服务器买票时(假设票数只剩一张),A 客户端让数据库内的票数 count1,但是还没来得及更新数据,又有 B 服务器查询数据库,此时 B 服务器看到票数还是 1,又进行了减 1 操作,导致一张票被同时卖了两次。

    CURD 的过程需要满足什么条件,才能解决上述问题呢?

    1. 买票的过程得应该是原子操作(要么抢到,要么没抢)
    2. 买票的过程中不能被相互影响(不能被互相影响,要割裂/独立开来)
    3. 买完票后应该要永久有效(必须要做持久化,不能只是在内存中操作)
    4. 买前和买后都是确定的状态(不能出现中间状态,要保证一致性)

    1.事务的理解

    1.1.事务的概念

    事务简单来理解就是 一组 DML 语句,也就是一组用于数据操作(插入数据、删除数据…)的语句。

    事务主要处理操作量大,复杂度高的数据,这些数据往往需要多条 SQL 语句来构成。

    但是事务还不仅仅是多条语句的集合,还必须保证四个属性(ACID)才能保证安全运行事务:

    • 原子性(Atomicity):一组 DML 语句/一个事务,要么全部成功,要么全部失败,这是由 MySQL 提供的机制来保障的。

    • 一致性(Consistency):在事务开始和结束以后,数据库的完整性没有被破坏,写入的数据必须完全符合所有的预设规则(在 MySQL 中,一致性时被其他三个属性来保证的,但是需要数据库和用户来配合,才能得到一致性)

    • 隔离性(Isolation):防止多个事务并发执行时由于交叉执行导致的数据不一致问题,事务隔离有不同的等级

      (1)读未提交(Read Uncommitted

      (2)读提交(Read Committed

      (3)可重复读(Repeatable Read

      (4)串行化(Serialzable

    • 持久性(Durability):事务处理完后,对表中数据的影响是永久的(哪怕系统挂了也不会丢失),也就是持久化

    注意:学习事务一定要在使用数据库的用户视角来理解。

    事务在 MySQL 内也一定是一个具体对象,这就需要先描述再组织。

    另外,事务不是 MySQl 天然就存在,而是为了在应用程序访问数据库的时候,能够简化编程模型,不需要考虑各种潜在并发问题、网络问题,用户只需要提交和回滚。因此,事务的本质是为了应用层服务的,而不是面向数据库内部。

    1.2.事务的版本

    MySQL 中只有使用了 Innodb 数据库引擎的数据库或数据表才可以支持事务,其他基本都不支持。

    # 查看 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
    *************************** 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
    *************************** 5. row ***************************
          Engine: MyISAM
         Support: YES
         Comment: MyISAM storage engine
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    2.事务的操作

    2.1.做一些准备工作

    开始之前,先设置一下隔离性(后面提及)。

    # 设置隔离性
    mysql> set global transaction isolation level READ UNCOMMITTED;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select @@tx_isolation;
    +------------------+
    | @@tx_isolation   |
    +------------------+
    | READ-UNCOMMITTED |
    +------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    首先,事务有两种提交方式:

    • 自动提交
    • 手动提交
    # 查看事务提交方式
    mysql> show variables like 'autocommit';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit    | ON    |
    +---------------+-------+
    1 row in set (0.01 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以用 SET 来改变 MySQL 的自动提交方式。

    # 取消事务自动提交
    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> set autocommit=1;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> show variables like 'autocommit';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit    | ON    |
    +---------------+-------+
    1 row in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    然后我们切换超级用户,使用 netstat -nltp 是一个用于显示网络连接、路由表和网络接口信息的命令行工具查看 MySQL 是否成功运行且占用端口号。

    # 查看网络状态
    # netstat -nltp
    Active Internet connections (only servers)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
     tcp6       0      0 :::3306                 :::*                    LISTEN      7404/mysqld         
    
    • 1
    • 2
    • 3
    • 4
    • 5

    原本是应该使用 Windows11cmd 来远程访问 Centos7 云服务的 MySQL 服务,不过需要提前在 Windows11 中提前下载好 MySQL,所以我们先用本地的两个客户端来模拟,后续再来详细了解。

    然后修改事务等级,不然有些现象会看不到。

    # 客户端中设置事务隔离级别并且重启检查
    mysql> set global transaction isolation level read uncommitted;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> quit
    Bye
    
    $ mysql -uroot -p
    Enter password: 
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 32
    Server version: 5.7.44 MySQL Community Server (GPL)
    
    Copyright (c) 2000, 2023, Oracle and/or its affiliates.
    
    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.
    
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    
    mysql> select @@tx_isolation;
    +------------------+
    | @@tx_isolation   |
    +------------------+
    | READ-UNCOMMITTED |
    +------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    启动两个 MySQL 客户端(模拟并发场景),然后其中一个客户端建立一个如下的表结构:

    # 客户端1:创建表结构
    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    # 客户端2:查看是否能看到 account 数据表
    mysql> desc account;
    +--------+---------------+------+-----+---------+-------+
    | Field  | Type          | Null | Key | Default | Extra |
    +--------+---------------+------+-----+---------+-------+
    | id     | int(11)       | NO   | PRI | NULL    |       |
    | name   | varchar(50)   | NO   |     |         |       |
    | blance | decimal(10,2) | NO   |     | 0.00    |       |
    +--------+---------------+------+-----+---------+-------+
    3 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以在一个客户端中查看连接情况,可以看到确实存在两个本地客户端。

    # 客户端1:查看 MySQL 的连接情况
    mysql> show processlist\G
    *************************** 1. row ***************************
         Id: 33
       User: ljp
       Host: localhost
         db: limou_database
    Command: Query
       Time: 0
      State: starting
       Info: show processlist
    *************************** 2. row ***************************
         Id: 34
       User: ljp
       Host: localhost
         db: limou_database
    Command: Sleep
       Time: 355
      State: 
       Info: NULL
    2 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.2.正常情况的事务

    启动事务有两种方法:(1)start transaction; (2)begin

    一旦开启事务,后续的 SQL 操作都属于同一个事务的部分。并且,我还设置了一个保存点(可选)

    # 客户端1:启动事务
    mysql> start transaction;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> savepoint s1;
    Query OK, 0 rows affected (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    # 客户端2:启动事务
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account;
    Empty set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后进行插入工作,设置第二个保存点,并且查看插入的数据是否同步到另外一个客户端。

    # 客户端1:插入数据
    mysql> insert into account values (2, 'limou', 10000);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> savepoint s2;
    Query OK, 0 rows affected (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    # 客户端2:查看数据
    mysql> select * from account;
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  2 | limou | 10000.00 |
    +----+-------+----------+
    1 row in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    尝试插入更多的插入和设置保存点的操作,同时没插入一个记录,就检查客户端是否数据同步。

    # 客户端1:插入数据和保存
    mysql> savepoint s2;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into account values (1, 'dimou', 11030);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> savepoint s3;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into account values (3, 'iimou', 10431);
    Query OK, 1 row affected (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    # 客户端2:每次插入,就查看一次数据表
    mysql> select * from account;
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  2 | limou | 10000.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account;
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  2 | limou | 10000.00 |
    |  3 | iimou | 10431.00 |
    +----+-------+----------+
    3 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    如果我们突然后悔了,可以回滚事务。

    # 客户端1:回滚事务
    mysql> rollback to s3;
    Query OK, 0 rows affected (0.00 sec)
    
    • 1
    • 2
    • 3
    # 客户端2:查看回滚后的数据表
    mysql> select * from account;
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  2 | limou | 10000.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到,数据表确实发生了回退,如果我们这个时候使用 COMMIT 就会提交本次事务。

    # 客户端1:提交事务
    mysql> commit;
    Query OK, 0 rows affected (0.01 sec)
    
    • 1
    • 2
    • 3

    补充:使用 roolback 还可以将所有操作全部取消,但是一般很少这么做。

    而事务只有在启动的时候才可以进行回滚操作,而一旦提交了就无法进行回滚。

    2.3.异常情况的事务

    如果其中一个服务端在事务状态下奔溃了会怎么样?其他客户端会自动回滚,也就是保证原子性,要么不做,要么就操作完。

    # 客户端1:不断插入数据最后因为异常导致奔溃,插入一次就在客户端2中查看一次
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into account values (3, 'eimou', 20431);
    Query OK, 1 row affected (0.01 sec)
    
    mysql> insert into account values (4, 'eimou', 30434);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> ^C
    mysql> Aborted
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    # 客户端2:客户端1插入后不断查找数据表,最后检查奔溃后的数据表
    mysql> select * from account;
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  2 | limou | 10000.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account;
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  2 | limou | 10000.00 |
    |  3 | eimou | 20431.00 |
    |  4 | eimou | 30434.00 |
    +----+-------+----------+
    4 rows in set (0.00 sec)
    
    mysql> select * from account; # 奔溃后,这里自动发生了回滚,回到开头
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  2 | limou | 10000.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    也有一些其他的情况会发生自动回滚(客户端因为关闭 shell 而导致退出),但是如果事务已经进行了提交(也就是使用了 COMMIT),就不会再进行回滚(包括因为崩溃造成的自动回滚)。

    但是我们之前设置的自动提交又是什么鬼?不是事务会自动提交吗?从现象来看,无论是设置 autocommitOFF 还是 ON 现象都是一样的结果(这个您可以自己实验一下),那这个 autocommit 究竟有什么用呢?您先知道一个点,只要是手动开启启动事务,就必须手动提交,和是否设置自动提交无关即可。

    而对比有无设置 autocommit 的两种情况。

    2.3.1.设置为 OFF

    # 客户端1:设置 autocommit 为 OFF,然后在事务中进行删除记录
    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> delete from account where id=3;
    Query OK, 1 row affected (0.01 sec)
    
    mysql> Aborted
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    # 客户端2:不断查看数据表
    mysql> select * from account; # 客户端1没删除之前
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  3 | timou | 50434.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account; # 客户端1删除后
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    +----+-------+----------+
    1 row in set (0.00 sec)
    
    mysql> select * from account; # 客户端1奔溃后,发现记录恢复
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  3 | timou | 50434.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2.3.2.设置为 ON

    # 客户端1:进行删除操作,但是不在事务中删除后崩溃
    mysql> show variables like 'autocommit';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit    | ON    |
    +---------------+-------+
    1 row in set (0.00 sec)
    
    mysql> delete from account where id=3;
    Query OK, 1 row affected (0.01 sec)
    
    mysql> Aborted
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    # 客户端2:不断查看数据表
    mysql> select * from account; # 客户端1没删除之前
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  3 | timou | 50434.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account; # 客户端1删除后
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    +----+-------+----------+
    1 row in set (0.00 sec)
    
    mysql> select * from account; # 客户端1奔溃后,发现记录没有恢复
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    +----+-------+----------+
    1 row in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    而且就算客户端 2 之前有做 BEGIN,再 COMMIT 后也不会恢复数据。

    以上测试说明,只要设置了 autocommit,就会把一条单纯的 SQL 语句单独看作一个事务,都会被自动 BEGINCOMMIT。因此如果没设置 autocommit 就会因为没有 COMMIT 而导致数据回滚。

    复习:再理一理,BEGIN 是开启事务处理,COMMIT 是事务提交,防止回滚。

    但是如果我们手动进行回滚呢?

    # 客户端1:删除数据表,但是先 commit 再崩溃
    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.01 sec)
    
    mysql> delete from account where id=1;
    Query OK, 1 row affected (0.00 sec)
    
    mysql> commit;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> Aborted
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    # 客户端2:不断查看数据表
    mysql> select * from account; # 客户端1删除记录前
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    +----+-------+----------+
    1 row in set (0.00 sec)
    
    mysql> select * from account; # 客户端1删除记录后
    Empty set (0.00 sec)
    
    mysql> select * from account; # 客户端1崩溃后,没有发生回滚
    Empty set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    也侧面证明了:对于 InnoDB 里一条单纯的 SQL 就是一个完整的事务,最后都会因为 autocommit=ON 而自动提交(但是 SELECT 有点特殊,您先记住就行,因为 MySQLMVCC)。

    补充:COMMIT 操作实际上就是数据持久化的一种手段。

    到这里就可以看到事务本身的 原子性(回滚)持久性(提交)

    3.事务的隔离

    3.1.隔离和隔离级别

    但是事务的隔离性体现在哪里呢?为何需要隔离呢?不隔离可以吗?

    一个 MySQL 服务可能会被多个客户端进程访问(一般都是程序直接连接数据库,命令行操作很少用到),访问的方式是以事务进行的。

    而事务在上述小节中实现了原子性和持久性,事务注定有执行前、执行中、执行后,一旦异常就会回滚。为了保证事务在执行中,为了保证事务尽量不受干扰,就产生的隔离性,根据不同的干扰级别可以设置不同的隔离级别。

    实际上,正常人的认知是:无论别的客户端 A 怎么对数据表怎么增删查改,另外一个客户端 B 要拿到最新的数据,不管两进程的先后顺序如何,都必须要先等待进程 A 完成一个事务。这个观点有问题吗?有点,举一个不恰当的例子,一个婴儿在出生后,不需要知道父母之前的经历,也能和父母一起创造新的经历,每个人只需要在自己生命周期内看到的世界也都是不一样的。这代表着:不一定需要获取最新的消息,只需要在自己的生命周期内看到该看到的就行,这就是隔离性

    MySQL 中,原子性不仅是在操作上原子,还体现在时间上。一个改表的 A 客户端先执行事务,一个查表的 B 客户端后执行事务,两者在执行中交叉进行。而 B 客户端执行得较慢,A 客户端执行得较快,那么 B 客户端是否应该立刻获取最新的数据呢?不应该!因为要保证隔离性,隔离运行中的事务。

    因此隔离体现在运行事务中,而隔离性要隔离的程度就是隔离级别(根据内容重要度的不同,有些信息是可以不被隔离的,因此就有隔离级别),在交叉事务处理过程中,不同的事务级别分为:

    • 读未提交(READ UNCOMMITTED):所有事务都可以看到其他事务没有提交的执行结果(相当于没有任何隔离性,实际生产中不太可能用到这个级别),我们之前的书写的事务代码,就是读未提交,没有进行 COMMIT 也会看到事务中的操作后结果,而对方客户端一旦崩溃就和没有存在过一样,则本客户端发送回滚
    • 读已提交(READ COMMITTED):只有对事务进行了 COMMIT,才能看到更新的数据表(一条单纯的 SQL 语句本身就被包装成一个事务,因此使用单纯的 SQL 语句会让所有客户端都读取到)
    • 可重复读(REPEATABLE READ):即便对方客户端提交了,哪怕是退出了,本客户端都无法实时知道更新结果,只有在本客户端退出了再次启动,才能看到更新后的新数据(这是 MySQL 默认的隔离等级),但是可能会有幻读的问题。
    • 串行化(SERIALIZABLE):事务隔离的最高级别,强制事务进行排序,使之不会相互冲突,确实解决了幻读的问题,但是会导致超时和锁竞争(太极端了,实际生产中很少用)

    隔离级别,基本都是通过锁来实现的,不同的隔离级别使用的锁不一样,常见的有:表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key 锁,简单认识一下就行。

    3.2.查看和设置隔离级别

    # 查看隔离级别
    mysql> select @@global.tx_isolation; # 全局设置
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | READ-UNCOMMITTED      |
    +-----------------------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> select @@session.tx_isolation; # 当前会话设置
    +------------------------+
    | @@session.tx_isolation |
    +------------------------+
    | READ-UNCOMMITTED       |
    +------------------------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> select @@tx_isolation; # 默认显示会话设置(就是当前会话的设置)
    +------------------+
    | @@tx_isolation   |
    +------------------+
    | READ-UNCOMMITTED |
    +------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    使用 SET {SESSION/GLOBAL} TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE}; 就可以设置隔离级别。

    而如果设置了全局设置,只会在下一次重启客户端时才会更新设置,一般一开始设置了隔离级别就不要修改了,最好保证隔离级别是一致的。

    3.3.不同隔离级别带来的影响

    再次强调,隔离主要是为了避免交叉事务出现问题。

    3.3.1.读未提交

    复习:读未提交,所有事务都可以看到其他事务没有提交的执行结果(相当于没有任何隔离性,实际生产中不太可能用到这个级别),我们之前的书写的事务代码,就是读未提交,没有进行 COMMIT 也会看到事务中的操作后结果,而对方客户端一旦崩溃就和没有存在过一样,则本客户端发送回滚。

    实际上之前已经做过了,最上面从设置隔离级别为 READ-UNCOMMITTED,后续的操作都是读未提交情景下的操作。

    # 设置隔离级别
    mysql> set global transaction isolation level READ UNCOMMITTED;
    Query OK, 0 rows affected (0.00 sec)
    
    # 然后重启两个客户端,分别查看隔离等级
    mysql> select @@tx_isolation;
    +------------------+
    | @@tx_isolation   |
    +------------------+
    | READ-UNCOMMITTED |
    +------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    # 客户端1:在事务中进行删除记录
    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> delete from account where id=3;
    Query OK, 1 row affected (0.01 sec)
    
    mysql> Aborted # 这里奔溃后,就相当于没有进行 COMMIT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    # 客户端2:不断查看数据表
    mysql> select * from account; # 客户端1没删除之前
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  3 | timou | 50434.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    mysql> select * from account; # 客户端1删除后,还没提交就被读取到了,也就是“读未提交”
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    +----+-------+----------+
    1 row in set (0.00 sec)
    
    mysql> select * from account; # 客户端1奔溃后,发现记录恢复
    +----+-------+----------+
    | id | name  | blance   |
    +----+-------+----------+
    |  1 | dimou | 11030.00 |
    |  3 | timou | 50434.00 |
    +----+-------+----------+
    2 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这种未提交就可以被其他客户端读取的现象,也叫做 脏读 现象。

    3.3.2.读已提交

    复习:读已提交,只有对事务进行了 COMMIT,才能看到更新的数据表(一条单纯的 SQL 语句本身就被包装成一个事务,因此使用单纯的 SQL 语句会让所有客户端都读取到)。

    # 设置隔离级别
    mysql> set global transaction isolation level READ COMMITTED;
    Query OK, 0 rows affected (0.00 sec)
    
    # 然后重启两个客户端,分别查看隔离等级
    mysql> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | READ-COMMITTED        |
    +-----------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    # 客户端1进行修改数据表
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update account set name='rimou' where id=2;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> commit;
    Query OK, 0 rows affected (0.01 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    # 客户端2不断进行查看
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; # 没修改之前
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | dimou | 1023.00 |
    |  3 | eimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> select * from account; # 客户端1修改数据,但是没有进行 commit,继续在客户端2查看,发现数据仍旧没有被修改
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | dimou | 1023.00 |
    |  3 | eimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> select * from account; # 客户端1使用 commit,此时客户端2就会发现数据被修改了
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | eimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这种提交才可以被其他客户端读取的现象,在提交之前只能获得旧数据,这要看具体场景,才能决定是对是错的。

    读提交有一个问题,一旦服务端 1 提交了,哪怕客户端 2 还处于事务中,也会读取到更新的数据,这在某些场景是错误的。理论上插入后更新数据表难道有错吗?有可能有错,提交确实是要让所有事务看到,但有时候不应该让运行中的事务看到(后面有个例子)。

    3.3.3.可重复读

    复习:可重复读,即便对方客户端提交了,哪怕是退出了,本客户端都无法实时知道更新结果,只有在本客户端退出了再次启动,才能看到更新后的新数据(这是 MySQL 默认的隔离等级),但是会有幻读的问题。

    # 设置隔离等级
    mysql> show variables like 'autocommit';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit    | ON    |
    +---------------+-------+
    1 row in set (0.01 sec)
    
    mysql> set global transaction isolation level REPEATABLE READ;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | REPEATABLE-READ       |
    +-----------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    # 客户端1:更新数据表
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update account set name='iimou' where id=3;
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    # 客户端2:不断查看,奔溃后再次查看
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; # 在没有更新数据表内记录之前查询
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | eimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> select * from account; # 更新数据表内记录后查询
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | eimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> select * from account; # 在客户端1 commit 后再次查询
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | eimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> Aborted 3 # 客户端2奔溃了...
    
    $ mysql
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 54
    Server version: 5.7.44 MySQL Community Server (GPL)
    
    Copyright (c) 2000, 2023, Oracle and/or its affiliates.
    
    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.
    
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    
    mysql> use limou_database;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from account; # 在客户端2奔溃后重新启动
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | iimou | 2034.00 |  # 发现这里终于被修改了
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    例子:举一个超级生动具体的例子

    一家公司招聘程序员时,原本规定了一个工作等级,于是开始招聘员工,假设招进了 A 程序员,A 程序员和 HR 敲定了自己的工资等级,最后成功入职。问题是,假设公司新出现了一个大型项目,需要招聘大量程序员,于是提高了薪资等级中的工资量。果然成功吸引了程序员 B,程序员 BHR 谈拢后,确定了和程序员 A 一样的等级,但是薪资却更高。

    而对于公司(黑心的)来说,不会直接告诉 A 你这个等级的薪资提高了,所以我要给你提薪…A 需要知道吗?不需要,你继续做你的工作就行了,在 A 的认知中,自己这个等级的薪资就是原先谈好的那么高。

    而一旦那一天工资项目出现问题,要进行裁员,就把 A 给“优化”(裁员)了,但是最后公司发现,自己貌似开走了一个“大动脉”(你之前写的代码逻辑非常重要,没 A 不行的那种),最后又把你找回来,按照 B 的薪资招聘你。

    这就是“可重复读”,A 始终用的是旧的薪资等级来判断薪资,但是后来的 B 却是更新后的第二套薪资体系,可是在 A 就职期间(被重新招聘前),没必要知道更新后的薪资等级(不然 A 闹起来怎么办,笑)。

    员工 A:客户端 1

    员工 B:客户端 2

    A 就职期间:客户端 1 启动事务期间

    B 就职期间:客户端 2 启动事务期间

    A 离职时:客户端 1 结束事务进行提交

    但是,在一些数据库中,在可重复隔离下,insertsql 可能无法被屏蔽(因为隔离性是通过加锁和其他策略来实现的,但是混则很难通过加锁来实现),在多次查询的过程中,就有可能会查出更新的数据(也就是“幻读”)。但是 MySQL 中,在 RR 级别中修复了这个问题(使用 GAP+行锁),我们不在这里深入了解,这也是为什么该级别是默认级别的原因。

    3.3.4.串行化

    复习:串行化,事务隔离的最高级别,强制事务进行排序,使之不会相互冲突,确实解决了幻读的问题,但是会导致超时和锁竞争(太极端了,实际生产中很少用)

    # 设置隔离级别
    mysql> set global transaction isolation level SERIALIZABLE;
    Query OK, 0 rows affected (0.01 sec)
    
    mysql> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | SERIALIZABLE          |
    +-----------------------+
    1 row in set, 1 warning (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    # 客户端1较后开启事务
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update account set name='eimou' where id=2;
    
    # 陷入阻塞状态,知道客户端2进行 commit 后才执行这条 sql
    
    Query OK, 1 row affected (14.32 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> commit;
    Query OK, 0 rows affected (0.01 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    # 客户端2较先开启事务
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; # 客户端1还没修改前
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | iimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> select * from account; # 客户端1修改后
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | iimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> commit;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from account; # 客户端2提交后查看
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | iimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> Aborted # 客户端2崩溃、
    
    $ mysql
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 61
    Server version: 5.7.44 MySQL Community Server (GPL)
    
    Copyright (c) 2000, 2023, Oracle and/or its affiliates.
    
    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.
    
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    
    mysql> use limou_database;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    Database changed
    
    mysql> select * from account; # 重启后再次查看就发现依旧没有被修改
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | rimou | 1023.00 |
    |  3 | iimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    mysql> select * from account; # 只有客户端1提交了,才能看到修改
    +----+-------+---------+
    | id | name  | blance  |
    +----+-------+---------+
    |  1 | limou | 1001.00 |
    |  2 | eimou | 1023.00 |
    |  3 | iimou | 2034.00 |
    +----+-------+---------+
    3 rows in set (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    注意,是事务之间串行化,不是进程之间串行化!

    以上隔离等级就是 隔离性 的体现,而怎么体现一致性呢?

    事务执行的结果,必须使数据库的一个一致性状态,转移到另外一个一致性状态。如果系统发生中断,某个事务未完成而被迫中止,而为改完的事务对数据库所做的修改已经写入数据库,此时数据库处于不正确、不一致的状态。因此是通过原子性(回滚)来保证一致的(而一致性和用户的逻辑强相关,由用户决定)。而其他三属性实际上都是围绕一致性来展开的,可以说 AID 是因,用户配合,C 是果。

    注意:这关于事务这块还是比较复杂的,建议深入了解,反复学习。

  • 相关阅读:
    lectin
    MyBatis是如何为Dao接口创建实现类的
    Python接入企业微信 - 推送信息到内部群里
    Wampserver使用问题记录
    【智慧燃气】智慧燃气解决方案总体概述--终端层、网络层
    VR 在未来也许是一个好风口
    Java Web概述
    STL- 函数对象
    云计算模式的区域LIS系统源码,基于ASP.NET+JQuery、EasyUI+MVC技术架构开发
    【AI视野·今日NLP 自然语言处理论文速览 第四十二期】Wed, 27 Sep 2023
  • 原文地址:https://blog.csdn.net/m0_73168361/article/details/136512190