前要:在网络服务中,如果对 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)

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)

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

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

可以用 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)

然后我们切换超级用户,使用 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

原本是应该使用 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)

启动两个 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;
# 客户端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:查看 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)

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)
# 客户端2:启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
Empty set (0.00 sec)

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

# 客户端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)
# 客户端2:查看数据
mysql> select * from account;
+----+-------+----------+
| id | name  | blance   |
+----+-------+----------+
|  2 | limou | 10000.00 |
+----+-------+----------+
1 row in set (0.00 sec)

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

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

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

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

补充:使用 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
# 客户端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)

也有一些其他的情况会发生自动回滚(客户端因为关闭 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
# 客户端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)

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
# 客户端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)

而且就算客户端 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
# 客户端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)

也侧面证明了:对于 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)

使用 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:在事务中进行删除记录
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
# 客户端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)

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

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进行修改数据表
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)
# 客户端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.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:更新数据表
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)
# 客户端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)

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

一家公司招聘程序员时,原本规定了一个工作等级,于是开始招聘员工,假设招进了 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较后开启事务
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)
# 客户端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)

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

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

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

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