目录
什么是事务
事务(transaction)就是一组在逻辑上存在相关性的DML语句。这一组DML语句要么全部成功,要么全部失败,是一个整体。一个MySQL 数据库可能同时处理多条事务,要处理好事务的并发问题,还要求事务满足如下四个属性(ACID):
- 原子性(Atomicity) :一个事务中的所有DML语句,要么全部完成,要么全部不完成。如果在执行过程中发生错误,就要**回滚(Rollback)**到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency) :一致性的官方定义是:事务必须确保数据库从一个正确的状态转变到另一个正确的状态**。**"正确的状态"指的是:约束性一致,所有表定义的约束在事务结束后必须不变。业务逻辑一致:数据的内在逻辑关系必须成立(比如转账业务)。如果满足了其他三个属性,这个属性自动满足
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务 并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read (S uncommitted )、读提交( read committed)、可重复读( erializable ) repeatable read)和串行化
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
事务的提交方式
事务的提交方式常见的有两种: 自动提交和手动提交
查看事务提交方式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | <= 自动提交已开启
+---------------+-------+
1 row in set (0.41 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.01 sec)
事务的开始、回滚、结束(体现原子性和持久性)
基本操作
下面演示 MySQL 有关事务的基本操作
## 为了便于演示,我们将mysql的默认隔离级别设置成读未提交。
## 具体操作后面会讲,现在以使用为主。
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> quit
Bye
##需要重启终端,进行查看
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
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> show variables like 'autocommit';
-- 查看事务是否自动提交。我们故意设置成自动提交,看看该选项是否影响begin
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
// 开始一个事务
mysql> start transaction;
// 或者
mysql> 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)
mysql> commit; -- 结束事务 -> 持久化
Query OK, 0 rows affected (0.00 sec)
mysql> rollback; -- 事务已经持久化,此时 rollback 失效了
Query OK, 0 rows affected (0.00 sec)
异常情况
下面验证 MySQL 事务在异常情况下的表现
1、事务未 commit,mysql 客户端退出或异常终止
验证方式:启动两个终端作为mysql 客户端,登陆同一个 mysql 服务器,一个终端(终端A)插入数据并故意异常终止 mysql 客户端,另一个终端(终端B)查看表的内容
-- 终端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.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 或者:
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)
结论:事务未 commit,mysql 客户端退出或异常终止,事务会自动回滚,保证了原子性
2、事务已 commit,mysql 客户端退出或异常终止
-- 终端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.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.03 sec)
// 直接关闭终端 -> 退出mysql 或者:
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退出或者崩溃后,数据依然存在
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
结论:如果事务已经 commit,客户端退出或崩溃不影响,保证了持久性
3、单条 SQL 与事务的关系
--实验一
-- 终端A
mysql> select * from account;
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec) --关闭自动提交
mysql> insert into account values (2, '李四', 10000); --插入记录
Query OK, 1 row affected (0.00 sec)
mysql> ^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)
-- 实验二
--终端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 其实被隐式包装为事务
mysql> insert into account values (2, '李四', 10000); --插入记录 Query OK, 1 row affected (0.00 sec) // 被 MySQL 包装成: mysql> begin; Query OK, 1 row affected (0.00 sec) mysql> insert into account values (2, '李四', 10000); --插入记录 Query OK, 1 row affected (0.00 sec) mysql> commit; // 如果 autocommit = 1, 自动提交autocommit 影响单条 SQL 执行后是否自动 commit。如果 autocommit 设置为 0,那么单 SQL 在执行完后都要手动的 commit,才能完成持久化。
总结
- 如果没有设置保存点(savepoint 保存点名),也可以回滚,但只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交) ,如果设置了保存点,可以选择回退到哪个保存点(rollback to 保存点名)。如果一个事务被提交了(commit),就不可以回滚了
- 对于 InnoDB, 如果没有显式开始事务,每一条 SQL 语言都默认封装成事务,自动提交
- 从上面的例子,我们能看到事务的原子性和持久性
隔离性
为什么要有隔离性
MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行,这些事务有可能会出现互相影响的情况,比如:多个事务同时访问同一张表,甚至同一行数据(你说你要删除这行数据,我说我要更新这行数据)。为了保证事务执行过程中尽量不受其他事务干扰,就有了一个重要特征:隔离性。允许事务受不同程度的干扰,就有了隔离级别
如果没有隔离性,多个事务同时并发执行,会出现三种经典的读异常:
| 异常类型 | 表现 | 例子 |
|---|---|---|
| 脏读 | 事务A读到了事务B尚未提交的修改。 | B给账户+100但没提交,A读到了+100后的余额。如果B回滚,A读到的就是无效数据。 |
| 不可重复读 | 事务A在同一个事务内两次读取同一条记录,结果不一样。 | A第一次读到余额100,期间B提交了修改将余额改为200,A第二次读到变成了200。 |
| 幻读 | 事务A在同一个事务内两次执行相同的查询,结果集的行数不一样。 | A第一次查年龄=20的有10人,期间B插入了一条新的年龄=20的数据并提交,A第二次查变成了11人。 |
结论:隔离性是必要的。 隔离性的核心目的,是为了在 "数据一致性" 和 "系统并发性能" 之间找到一个平衡点。隔离得越严,数据越安全,但性能越差;隔离得越松,性能越好,但可能出现数据异常。
SQL标准定义的四个隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式(InnoDB) |
|---|---|---|---|---|
| READ UNCOMMITTED (读"未提交") | ❌ 可能 | ❌ 可能 | ❌ 可能 | 几乎不加锁,所有的事务都可以实时看到其他事务没有提交或已提交的的执行结果,上面所有的实验就是这种隔离级别 |
| READ COMMITTED (读"已提交") | ✅ 避免 | ❌ 可能 | ❌ 可能 | 一个事务只能看到其他的已经提交的事务所做的改变。每条SELECT执行时生成快照,语句级一致性 |
| REPEATABLE READ (可重复读,MySQL默认) | ✅ 避免 | ✅ 避免 | ❌ 可能(标准)/ ✅ InnoDB避免 | 该隔离级别确保同一个事务,在执行中,多次读取数据时,会看到同样的数据,即使该数据已被其他事务修改并 commit。事务开始时生成快照,事务级一致性 + 间隙锁防幻读 |
| SERIALIZABLE(串行化) | ✅ 避免 | ✅ 避免 | ✅ 避免 | 强制事务先后执行,使之不可能相互冲突。可能会导致超时和锁竞争。所有SELECT隐式加锁(LOCK IN SHARE MODE),完全串行执行 |
说明:
- 在 读"已提交" 这一隔离级别下,一个事务A在运行期间看到了其他事务 commit 后的数据,导致事务A在同一个事务内两次读取同一条记录,结果不一样。这合理吗?这不合理,一个事务可以看到其他事务 commit 后的数据,但不应该在运行时看到。
- 读"已提交" 导致不可重复读产生的问题的例子:学生奖品发放
规则:成绩在60,89的学生发放作业本奖品,在90,100的发放毛笔奖品,一个名叫张三的同学成绩是 79,经过校验后的成绩是 95,现在有两个事务:事务A查询成绩在60,89的学生和成绩在90,100的学生(两条 select 语句),事务B:更新张三的成绩为 95(一条 update 语句),不可重复读产生:update 语句在两条 select 语句之间 commit,现在的隔离级别是 读"已提交",事务A 在执行第二条select语句时,又查询到了张三,导致张三既领取了作业本奖品又领取了毛笔奖品。- 幻读问题:一般的数据库在处于可重复读这种隔离级别的时候,无法屏蔽其他事务insert的数据(也就是说即使处于可重复读这种隔离级别,其他事务 insert 数据并 commit 后,当前事务仍然可以看到 insert 的数据,导致不可重复读问题。因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题)。这种情况就称为幻读。可以验证,MySQL在可重复读这种隔离级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁 (GAP+行锁))
查看隔离级别
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)
全局隔离级别与当前会话隔离级别
全局隔离级别是登陆 MySQL 时,MySQL 读取配置文件而设置,当前会话隔离级别是创建 MySQL 会话时,默认等于全局隔离级别。设置当前会话隔离级别不影响全局隔离级别。设置全局隔离级别,新创建的会话的当前会话隔离级别收到影响。
设置隔离级别
语法:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}
// [SESSION | GLOBAL] 建议不要省略
一致性
MySQL事务的"一致性"(Consistency)是唯一一个不能只靠数据库本身保证的ACID特性,它需要应用程序和数据库共同协作完成。
一致性的官方定义是:事务必须确保数据库从一个正确的状态转变到另一个正确的状态。
"正确的状态"指的是:
约束性一致 :所有定义的约束(主键、外键、唯一索引、
CHECK约束、NOT NULL等)在事务结束后必须全部满足。业务逻辑一致:数据的内在逻辑关系必须成立(比如转账业务,转出方扣了100,转入方必须增加100,总金额不变)。
数据库负责哪部分?应用负责哪部分?
数据库(MySQL)负责保证 :约束性一致 。只要你的SQL没有违反索引、外键、触发器或存储引擎层的规则,MySQL的原子性 (靠
undo log)和隔离性 (靠锁和MVCC)会确保事务提交后,物理数据在结构上是合法的。应用程序(你)负责保证 :业务逻辑一致。这是最容易被忽视的点。