前要:在网络服务中,如果对
CURD
不加限制,会出现什么问题?在多个客户端同时向一个卖票服务器买票时(假设票数只剩一张),
A
客户端让数据库内的票数count
减1
,但是还没来得及更新数据,又有B
服务器查询数据库,此时B
服务器看到票数还是1
,又进行了减1
操作,导致一张票被同时卖了两次。
CURD
的过程需要满足什么条件,才能解决上述问题呢?
- 买票的过程得应该是原子操作(要么抢到,要么没抢)
- 买票的过程中不能被相互影响(不能被互相影响,要割裂/独立开来)
- 买完票后应该要永久有效(必须要做持久化,不能只是在内存中操作)
- 买前和买后都是确定的状态(不能出现中间状态,要保证一致性)
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
原本是应该使用 Windows11
的 cmd
来远程访问 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
),就不会再进行回滚(包括因为崩溃造成的自动回滚)。
但是我们之前设置的自动提交又是什么鬼?不是事务会自动提交吗?从现象来看,无论是设置 autocommit
为 OFF
还是 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
语句单独看作一个事务,都会被自动 BEGIN
和 COMMIT
。因此如果没设置 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
有点特殊,您先记住就行,因为 MySQL
有 MVCC
)。
补充:
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
,程序员B
和HR
谈拢后,确定了和程序员A
一样的等级,但是薪资却更高。而对于公司(黑心的)来说,不会直接告诉
A
你这个等级的薪资提高了,所以我要给你提薪…A
需要知道吗?不需要,你继续做你的工作就行了,在A
的认知中,自己这个等级的薪资就是原先谈好的那么高。而一旦那一天工资项目出现问题,要进行裁员,就把
A
给“优化”(裁员)了,但是最后公司发现,自己貌似开走了一个“大动脉”(你之前写的代码逻辑非常重要,没A
不行的那种),最后又把你找回来,按照B
的薪资招聘你。这就是“可重复读”,
A
始终用的是旧的薪资等级来判断薪资,但是后来的B
却是更新后的第二套薪资体系,可是在A
就职期间(被重新招聘前),没必要知道更新后的薪资等级(不然A
闹起来怎么办,笑)。员工
A
:客户端 1员工
B
:客户端 2
A
就职期间:客户端 1 启动事务期间
B
就职期间:客户端 2 启动事务期间
A
离职时:客户端 1 结束事务进行提交
但是,在一些数据库中,在可重复隔离下,insert
的 sql
可能无法被屏蔽(因为隔离性是通过加锁和其他策略来实现的,但是混则很难通过加锁来实现),在多次查询的过程中,就有可能会查出更新的数据(也就是“幻读”)。但是 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
是果。
注意:这关于事务这块还是比较复杂的,建议深入了解,反复学习。