MySQL事务(十一)

什么是事务?

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

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

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

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

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

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

原子性(Atomicity,或称不可分割性)

一致性(Consistency)

隔离性(Isolation,又称独立性)

持久性(Durability)

实际上在数据库中,并没有对一致性做工作,是因为有了原子性,隔离性,持久性,才有了一致性这个结果。

事务的版本支持

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

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

事务提交方式

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

  • 自动提交
  • 手动提交

查看事务提交方式

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

用 SET 来改变 MySQL 的自动提交模式:

cpp 复制代码
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)

事务常见操作方式

cpp 复制代码
-- 将mysql的默认隔离级别设置成读未提交。
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation; --还没生效,需要重新登录mysql
+-------------------------+
| @@transaction_isolation |--MySQL5.x版本中为@@tx_isolation,8.0以上为@@transaction_isolation
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

--重启后
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED        |--读未提交模式
+-------------------------+
1 row in set (0.00 sec)

创建测试表

cpp 复制代码
mysql> 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;
Query OK, 0 rows affected, 1 warning (0.04 sec)

例1:事务的开始与回滚

cpp 复制代码
mysql> show variables like 'autocommit'; -- 查看事务是否自动提交。我们故意设置成自
动提交,看看该选项是否影响begin
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)
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)

例2: 未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)

cpp 复制代码
-- 终端A
mysql> select * from account; -- 当前表内无数据
Empty set (0.00 sec)

mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 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> select * from account; --数据已经存在,但没有commit,此时同时查看终端B
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
+----+--------+---------+
1 row in set (0.00 sec)

mysql> Aborted -- ctrl + \ 异常终止MySQL


--终端B
mysql> select * from account; --终端A崩溃前
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
+----+--------+---------+
1 row in set (0.00 sec)

mysql> select * from account; --终端A崩溃后,数据自动回滚
Empty set (0.00 sec)

例3:commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化

cpp 复制代码
--终端 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; --数据存在了,所以commit的作用是将数据持久化到MySQL中
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
+----+--------+---------+
1 row in set (0.00 sec)

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

cpp 复制代码
-- 终端 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崩溃后,自动回滚,无论开启还是关闭autocommit,结果都是自动回滚
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
+----+--------+---------+
1 row in set (0.00 sec)

结论:只要输入 begin 或者 start transaction 开启手动事务,该事务便必须通过 commit 提交 ,才会将修改持久化 ,与是否设置 set autocommit 无关

例5:单条 SQL 语句与事务的关系

cpp 复制代码
--实验一
-- 终端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    | OFF   |
+---------------+-------+
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); --使用单SQL语句插入记录
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语句需要提交,才能持久化,否则客户端崩溃时,自动回滚。

解决方式:执行单SQL语句后需要commit

cpp 复制代码
-- 实验二
--终端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)

这里说明单SQL语句本质上被打包成一个事务,这里自动提交后,数据持久化。

cpp 复制代码
insert into account values (2, '李四', 10000);
	||
	||隐式打包成事务
	||
begin;
insert into account values (2, '李四', 10000);
commit; -- 取决于autocommit是否开启

总结

  • InnoDB 支持事务,MyISAM 不支持事务。
  • 对于 InnoDB,每一条 SQL 语句都默认封装成事务,自动提交(select 有特殊情况,因为 MySQL 有 MVCC)。
  • 开始事务可以使用 start transaction 或者 begin
  • 只要输入 begin 或者 start transaction 开启手动事务,该事务便必须通过 commit 提交 ,才会将修改持久化 ,与是否设置 set autocommit 无关
  • 从手动事务的操作中,我们能看到事务本身的原子性(回滚)持久性(commit)
  • 如果事务还没有提交,即使没有设置保存点,也可以使用 rollback 回滚 ,只是只能回滚到事务的开始
  • 可以通过设置保存点 ,实现选择回退到指定的保存点(精细化回滚)。
  • 事务可以手动通过 rollback 回滚 ;同时,当事务执行过程中出现操作异常MySQL 会自动回滚该事务
  • 如果一个事务已经被提交了(commit),则不可以再通过 rollback 回退。

事务的隔离级别

初步理解隔离性

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

  • 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶

    段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,

    可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。

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

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

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

总结

在事务场景中,隔离是必要的!(这个隔离是指运行中的事务,进行相互隔离。)

在事务运行中,"不会"出现互相干扰 ,这就是隔离性。

根据影响的程度 不同,隔离级别不同

隔离级别

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

隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。

查看与设置隔离性

cpp 复制代码
-- 查看
mysql> SELECT @@global.tx_isolation; --查看全局隔级别
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ       |
+-----------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@session.tx_isolation; --查看会话(当前)全局隔级别
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@tx_isolation; --默认同上
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

注意:在Mysql8.0中使用transaction_isolation替代tx_isolation

设置隔离级别语法

cpp 复制代码
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

例子:

cpp 复制代码
-- 设置
-- 设置当前会话隔离性,只影响当前当次会话,下次进入会话时,跟全局走
-- 注意这里是会话,不是用户
mysql> set session transaction isolation level serializable; -- 串行化
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@global.tx_isolation; --全局隔离性还是REPEATABLE-READ
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ       |
+-----------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@session.tx_isolation; --会话隔离性成为串行化
+------------------------+
| @@session.tx_isolation |
+------------------------+
| SERIALIZABLE           |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@tx_isolation; --同会话的隔离性,就是当前会话使用的隔离级别
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE   |
+----------------+
1 row in set, 1 warning (0.00 sec)

--设置全局隔离性,另起一个会话,会被影响(但是已经登录的会话不受影响,按照那个会话的隔离级别走)
--相当于永久设置隔离级别
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

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)

总结:全局的隔离级别是永久的,会话的隔离级别是临时的,当前隔离级别看会话的隔离级别,重启后隔离级别重置为全局的隔离级别。

读未提交【Read Uncommitted】例子

几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用

cpp 复制代码
--终端A
-- 设置隔离级别为 读未提交
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> select * from account;
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
|  2 | 李四   |10000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

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)

读提交【Read Committed】例子

cpp 复制代码
-- 终端A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

--重启客户端
mysql> select * from account; --查看当前数据
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  123.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=321.0 where id=1; --更新张三数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

--切换终端到终端B,查看数据。-----------------------------------------转1
mysql> commit; --commit提交!
Query OK, 0 rows affected (0.01 sec)
--切换终端到终端B,再次查看数据。-------------------------------------转2

--终端B
mysql> begin; --手动开启事务,和终端A一前一后
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account; --终端A commit之前,终端B的事务查看不到-----------1
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  123.00 |--老的值
|  2 | 李四   |10000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

--终端A commit之后,终端B的事务看到了!-----------------------------------------2
--但是此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段
(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)
mysql> select *from account;
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  321.00 |--新的值
|  2 | 李四   |10000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

不可重复读

同一事务内多次读取同一数据,在事务尚未结束、且没有自己修改该数据的情况下,读到了不同结果 ,这就是不可重复读(non reapeatable read)

不可重复读是个问题吗?另一个事务已经提交了,我这个事务看到不是很正常吗,这怎么是个问题?

可重复读【Repeatable Read】例子

cpp 复制代码
--终端A
mysql> set global transaction isolation level repeatable read; --设置全局隔离级别RR
Query OK, 0 rows affected (0.01 sec)

--关闭终端重启
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ | --隔离级别RR
+-----------------+
1 row in set, 1 warning (0.00 sec)

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,查看另一个事务是否能看到------------------------------------------------------转1
mysql> commit; --提交事务
--切换终端到终端B,查看数据。---------------------------------------------------------------转2


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

mysql> select * from account; --终端A中事务commit之前,查看当前表中数据,数据未更新------------1
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  321.00 |
|  2 | 李四   |10000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> select * from account; --终端A中事务commit之后,查看当前表中数据,数据未更新-----------2
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  321.00 |
|  2 | 李四   |10000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
--可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!

mysql> commit; --结束事务-----------------------------------------------------------------3
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)

可重复读

可重复读:同一个事务内,多次执行相同的查询语句,得到的结果始终一致,不会受到其他并发事务的增 / 删 / 改操作影响,解决了不可重复读问题。

与不可重复读的区别 :其他事务做出的修改,只有在这个事务commit后,再查询才能看到。(不可重复读:在这个事务commit之前,就可以查询到)

幻读

一般其他的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据 (为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来 ,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读 (phantom read)。

注:MySQL在RR级别的时候,是解决了幻读问题的。(解决的方式是用Next-Key锁(GAP+行锁)解决的)

串行化【serializable】例子

最严格的隔离级别

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

简单地说,事务先到先执行,后到去排队,不允许并发。(事务级别的串行化

cpp 复制代码
--终端A
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE   |
+----------------+
1 row in set, 1 warning (0.00 sec)

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)

mysql> update account set blance=1.00 where id=1; --终端A中有更新或者其他操作,会阻塞,直到终端B事务提交。

Query OK, 1 row affected (18.19 sec)---------------------------------------------------------1
Rows matched: 1 Changed: 1 Warnings: 0

--终端B
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)

mysql> commit; --提交之后,终端A中的update才会提交。-------------------------------------------转1
Query OK, 0 rows affected (0.00 sec)

总结

  • 其中隔离级别越严格安全性越高 ,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
  • 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了
  • 幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样
  • mysql 默认的隔离级别是可重复读,一般情况下不要修改
  • commit是个关键点,commit时机不同,会有不同结果,可以区分不同的隔离级别。
隔离级别 脏读 不可重复读 幻读 加锁读
读未提交(read uncommitted) 不加锁
读已提交(read committed) 不加锁
可重复读(repeatable read) 不加锁
可串行化(serializable) 加锁

事务的一致性

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

深入理解隔离性

数据库并发的场景有三种:
读-读 :不存在任何问题,也不需要并发控制
读-写 :有线程安全问题,可能会造成事务隔离性问题 ,可能遇到脏读,幻读,不可重复读
写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

读-写

多版本并发控制MVCC )是一种用来解决 读-写冲突 的无锁并发控制。

每个事务都要有自己的事务ID ,可以根据事务ID的大小,来决定事务到来的先后顺序

mysqld可能会面临处理多个事务的情况,事务也有自己的生命周期------mysqld要对多个事务进行管理 ------如何管理?先描述,在组织。------事务也要有自己的结构体。(里面有事务ID)

理解 MVCC 需要知道三个前提知识:

  • 3个记录隐藏字段
  • undo 日志
  • Read View

3个记录隐藏字段(列)

DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID ,记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR : 7 byte,回滚指针 ,指向这条记录的上一个版本 (简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键) ,如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引(索引和数据行本身合二为一)

补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

例子:

cpp 复制代码
mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name   | age |
+--------+-----+
| 张三   | 28  |
+--------+-----+
1 row in set (0.00 sec)

只插入了一条记录,里面有两个数据,实际上有三个隐藏列

name age DB_TRX_ID(创建该记录的事务ID) DB_ROW_ID(隐式主键) DB_ROLL_PTR(回滚指针)
张三 28 事务ID 1 null

隐式主键为1,第一条记录也没有其他版本,设置回滚指针为null。

undo 日志(简单了解)

MySQL 将来是以服务进程的方式,在内存中运行。索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。

我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行

模拟 MVCC

现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四)。

  • 事务10,因为要修改,所以要先给该记录加行锁

  • 修改前,现将改行记录拷贝到undo log 中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)

  • 所以现在 MySQL 中有两行同样的记录。现在修改原始记录 中的name,改成'李四'。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前事务10的ID(我们默认从9开始,之后递增)。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。

  • 事务10提交,释放锁。

现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)。

  • 事务11,因为也要修改,所以要先给该记录加行锁
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log
  • 现在修改原始记录 中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
  • 事务11提交,释放锁

上面的一个一个版本,我们可以称之为一个一个的快照

这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。

如果是delete ?同样地,删数据不是清空,而是设置flag为删除即可。也可以形成版本。

如果是insert ?因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。

那么selectselect不会对数据做任何修改,所以,为select维护多版本,没有意义。


一个问题:select读取,是读取最新的版本呢?还是读取历史版本?

快照读

快照读读取历史版本 ,就叫做快照读。通常普通的select语句(不加锁)就是快照读。

串行化中快照读会退化成当前读。

当前读

当前读读取最新的记录 ,就是当前读。增删改,都叫做当前读(可以理解为必须修改实时数据)。select也有可能当前读(select加锁),比如:select lock in share mode(共享锁) 。

隔离性的本质 不是让我们看到不同的数据,而是让我们看到不同的版本
隔离级别本质 就是我们用户看到不同的版本的范围

在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁。

但如果select是快照读,读取历史版本的话,是不受加锁限制的 。也就是可以并行执行!换言之,提高了效率,即MVCC的意义所在,所以有如下的结论。

MVCC的本质

select读取历史版本,增删改修改当前版本 ,他们互不干扰,这就MVCC实现不加锁的读写并发的操作。

那么,是什么决定了,select是当前读,还是快照读呢?隔离级别!

那为什么要有隔离级别呢? 事务都是原子的。所以,无论如何,事务总有先有后。

但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但不管怎么启动多个事务,总是有先有后的。那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的"有先有后",是不是应该让不同的事务看到它该看到的内容 ,这就是所谓的隔离性与隔离级别要解决的问题。

Read View

Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

Read View 在 MySQL 源码中,就是一个类 ,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图 ,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。

总结

  • read view是事务可见性 的一个类,不是事务创建出来就会有read view,而是当这个事务(已经存在),首次进行快照读的时候,就会有read view
  • redo log里面保存着不同版本的数据 ,read view判断当前事务能够看到哪个版本的数据

下面是 ReadView 结构的简化版

cpp 复制代码
class ReadView {
// 省略...
private:
    //核心变量1:活跃事务ID列表(生成Read View时,系统正活跃的事务ID集合)
    ids_t m_ids;  

    //核心变量2:低水位线(最小可见阈值,m_trx_ids中事务ID的最小值)
    trx_id_t up_limit_id;  

    //核心变量3:高水位线(最大不可见阈值,系统尚未分配的下一个事务ID)
    trx_id_t low_limit_id;  

    //核心变量4:创建该Read View的事务ID(当前事务自身的ID)
    trx_id_t creator_trx_id;  

    //非核心变量:配合purge线程,标识该视图不需要小于m_low_limit_no的UNDO LOG
    //如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG
    trx_id_t m_low_limit_no;  

    //非核心变量:标记视图是否被关闭
    bool m_closed;  
// 省略...
};

其中重要的是这四个变量

cpp 复制代码
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
creator_trx_id //创建该ReadView的事务ID

我们在实际读取数据版本链 的时候,是能读取到每一个版本对应的事务ID 的,即:当前记录的DB_TRX_ID 。

那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的DB_TRX_ID 。

所以现在的问题就是,当前快照读,应不应该读到当前版本记录。

以下是上图对应的源码策略,这是 InnoDB 存储引擎中的核心可见性判断函数,用于判断一个事务(id)所做的数据修改,对于当前的一致性视图(Read View)是否可见。

cpp 复制代码
/** Check whether the changes by id are visible.
@param[in]      id      transaction id to check against the view
@param[in]      name    table name
@return whether the view sees the modifications of id. */
bool changes_visible(
    trx_id_t        id,
    const table_name_t&      name) const
    MY_ATTRIBUTE((warn_unused_result))
{
    ut_ad(id > 0);

    if (id < m_up_limit_id || id == m_creator_trx_id) {
        return(true);
    }

    check_trx_id_sanity(id, name);

    if (id >= m_low_limit_id) {
        return(false);
    } else if (m_ids.empty()) {
        return(true);
    }

    const ids_t::value_type*    p = m_ids.data();
    return(!std::binary_search(p, p + m_ids.size(), id));
}

具体例子

当前某表中一条记录

name age DB_TRX_ID(创建该记录的事务ID) DB_ROW_ID(隐式主键) DB_ROLL_PTR(回滚指针)
张三 28 null 1 null

事务操作

事务1 [id=1] 事务2 [id=2] 事务3 [id=3] 事务4 [id=4]
事务开始 事务开始 事务开始 事务开始
... ... ... 修改且已提交
进行中 快照读 进行中 ...
... ... ... ...

事务4:修改name(张三) 变成name(李四)

当 事务2 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图

cpp 复制代码
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2

我们的事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。

cpp 复制代码
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
//事务4提交的记录对应的事务ID
DB_TRX_ID=4
//比较步骤
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。
//结论
故,事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

RR 与 RC的本质区别

SQL语句补充

cpp 复制代码
select * from user; --快照读
select * from user lock in share mode; --以加共享锁方式进行读取,对应的就是当前读。

当前读和快照读在RR级别下的区别

配置工作:

cpp 复制代码
--设置RR模式下测试
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

--重启终端
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (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;

--插入一条记录,用来测试
mysql> insert into user (id, age, name) values (1, 15,'黄蓉');
Query OK, 1 row affected (0.00 sec)

例子1

事务A操作 事务A描述 事务B描述 事务B操作
begin 开启事务 开启事务 begin
select * from user 快照读查询 快照读查询 select * from user
update user set age=18 where id=1; 更新age=18 - -
commit 提交事务 - -
- - select 快照读,没有读到age=18 select * from user
- - select lock in share mode当前读,读到age=18 select * from user lock in share mode

例子2

事务A操作 事务A描述 事务B描述 事务B操作
begin 开启事务 开启事务 begin
select * from user 快照读,查到 age=18 - -
update user set age=28 where id=1; 更新 age=28 - -
commit 提交事务 - -
- - select 快照读 age=28 select * from user
- - select lock in share mode 当前读 age=28 select * from user lock in share mode

这两个例子的差别就是例子1中的事务B在事务A commit前快照读过一次age数据,而例子2的事务B在事务A commit前没有进行过快照读。

结论

事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力。

RR 与 RC的本质区别

正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

RR

在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View , 将当前系统活跃的其他事务记录起来。

此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。

即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。

RC

而在RC级别下的事务中,每次快照读都会新生成一个快照和Read View , 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。

正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。

总结:在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

相关推荐
风流 少年21 小时前
mysql mcp
数据库·mysql·adb
知我Deja_Vu21 小时前
redisCommonHelper.generateCode(“GROUP“),Redis 生成码方法
数据库·redis·缓存
寄存器漫游者21 小时前
Linux 线程间通信
数据库·算法
努力的lpp21 小时前
SQLMap CTF 常用命令全集
数据库·web安全·网络安全·sql注入
IvorySQL1 天前
揭开 PostgreSQL 读取效率问题的真相
数据库·postgresql·开源
努力的lpp1 天前
SQL 报错注入
数据库·sql·web安全·网络安全·sql注入
麦聪聊数据1 天前
统一 Web SQL 平台如何收编企业内部的“野生数据看板”?
数据库·sql·低代码·微服务·架构
山峰哥1 天前
吃透 SQL 优化:告别慢查询,解锁数据库高性能
服务器·数据库·sql·oracle·性能优化·编辑器
TDengine (老段)1 天前
TDengine IDMP 数据可视化——散点图
大数据·数据库·物联网·信息可视化·时序数据库·tdengine·涛思数据
Project_Observer1 天前
工时日志在项目进度管理中扮演着怎样的角色?
数据库·深度学习·机器学习