目录
- 概述
- 为什么有事务?
- 事务操作
- 事务的四大特性(AICD)
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
- 并发事务问题
- 脏读(Dirty Read)
- 不可重复读
- 幻读
- 事务隔离级别
- 查看事务隔离级别
- 查看当前 Session 的事务隔离级别
- 设置全局事务隔离级别
- 证明
- 解决脏读问题
- 模拟场景
- 解决
- 解决不可重复读
- 模拟场景
- 解决
- 幻读
- 模拟场景
- 解决
- Reference
概述
事务是一组操作集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起想系统提交或撤销请求,这么操作要么全部执行成功,要么全部失效。
为什么有事务?
假设有一个银行数据库,张三向李四转账 1000 元。
这个转账操作涉及到两个步骤:
- 从张三的账户中扣除1000元。
- 在李四的账户中增加1000元。
如果没有事务,这两个步骤可能会分开执行,可能会出现以下问题:
- 第一步执行成功,张三 的账户减少了1000 元,但是第二步执行失败,李四 的账户没有增加1000 元。这样会导致数据不一致,张三 的钱消失了。
- 如果第一步执行失败,张三 的账户没有减少1000元,但是第二步执行成功,李四 的账户增加了1000元。这样也会导致数据不一致,李四 无缘无故多了1000 元。
解决办法:
使用事务可以解决这个问题,将上述两个步骤放在一个事务中。
- 如果第一步执行成功而第二步执行失败,那么整个事务会回滚,即第一步的操作也会被撤销,张三 的账户不会减少1000元。
- 同样,如果第一步执行失败,整个事务也会回滚,不会执行第二步。这样就可以确保数据的一致性。
事务操作
- 查看/设置事务提交方式
SHOW VARIABLES LIKE 'autocommit';
Variable_name|Value|
-------------+-----+
autocommit |ON |
-- 设置自动提交事务
SET autocommit = ON;
-- 设置手动提交事务
SET autocommit = OFF;
- 开启事务
-- 开始事务
BEGIN;
-- 执行一系列操作
UPDATE account SET balance = balance - 1000 WHERE account_id = 1;
UPDATE account SET balance = balance + 1000 WHERE account_id = 2;
-- 提交事务
COMMIT;
-- 如果发生错误,可以回滚事务
-- ROLLBACK;
事务的四大特性(AICD)
事务的四大特性,通常被称为ACID特性,是数据库管理系统保证数据一致性和可靠性的基础。这些特性分别是:
原子性(Atomicity)
事务中的所有操作要么全部完成,要么全部不完成,不会处于中间状态。如果事务在执行过程中发生错误,系统会回滚到事务开始前的状态,就像事务从未执行过一样。
- 从张三的账户中扣除1000元。
- 在李四的账户中增加1000元。
两个步骤要么全部完成,要么全部失效。
一致性(Consistency)
事务必须使数据库从一个一致性状态转移到另一个一致性状态。这意味着事务执行结束后,数据库的完整性约束没有被破坏,数据保持一致。
转账后,张三和李四的账户总额 == 转账前,张三和李四的账户总额。
隔离性(Isolation)
事务的隔离性意味着一个事务的执行不应该受到其他事务的干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(Durability)
一旦事务被提交,它对数据库的更改就是永久性的。即使系统发生崩溃,事务的结果也不会丢失,因为它们已经被持久化到数据库中。
这些特性共同保证了数据库事务的可靠性,即使在并发访问和系统故障的情况下,也能保持数据的一致性和完整性。
完成转账后,张三和李四的账户数据被持久存储到磁盘。
并发事务问题
脏读(Dirty Read)
一个事务读取了另一个未提交事务修改过的数据。如果这个未提交的事务最终回滚,那么第一个事务读取的数据就是无效的。
- bilibli 开启事务 A,查看了张三的账户余额还有 2000 元,自动扣款年度大会员 200 元。
- 张三开启事务 B,查看账户余额还有 1800 元。
- 此时,bilibili 发生异常,自动口矿失败,事务回滚。
- 张三之前查看的账户余额数据就是无效的。
假设我们已经解决了脏读的问题。
不可重复读
事务 A 两次读取同一行数据,但在两次读取之间,事务 B 修改了这行数据。因此,事务 A 在两次读取中得到了不同的结果。
主要针对 update
和 delete
操作。
- 张三开启事务 A,第一次查看账户余额 2000 元。
- bilibili 开启事务 B 自动扣款年费大会员 200 元,并执行了
commit
成功提交了事务。 - 张三再次读取余额,发现账户余额只有 1800 元,两次查看发现余额不同。
幻读
事务 A 在数据库中执行查询两次,但在两次查询之间,事务 B 插入了一些满足查询条件的新行。因此,事务 A 在两次查询中得到了不同的结果集。
主要针对 insert
操作。
- bilibli 管理员开启事务 A,查询用户账号信息,发现没有 Boss 的账户。
- 此时,Boss 开启事务 B,并成功注册了账户。
- 此时 bilibli 管理员想新增 Boss 账户时,发现新增失败。
- bilibili 管理员再次查询用户的账号信息,任然没有 Boss 的账户。
bilibli 管理员两次查询的用户数据不同,即幻读。
事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted(未提交读) | ✔ | ✔ | ✔ |
Read committed(已提交读) | ❌ | ✔ | ✔ |
Repeatable Read**(默认)** | ❌ | ❌ | ✔ |
Serializable | ❌ | ❌ | ❌ |
查看事务隔离级别
SELECT @@tx_isolation;
-- 或者(在MySQL 5.7.20及更高版本中)
SELECT @@transaction_isolation;
|@@transaction_isolation|
|-----------------------+
|REPEATABLE-READ |
查看当前 Session 的事务隔离级别
- 将事务隔离级别设置为
READ UNCOMMITTED
。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
- 将事务隔离级别设置为
READ COMMITTED
。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 将事务隔离级别设置为
REPEATABLE READ
。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
- 将事务隔离级别设置为
SERIALIZABLE
。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
设置全局事务隔离级别
- 讲事务隔离级别设置为
READ UNCOMMITED
。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
- 将事务隔离级别设置为
READ COMMITTED
。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 将事务隔离级别设置为
REPEATABLE READ
。
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
- 将事务隔离级别设置为
SERIALIZABLE
。
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
证明
现在有账户余额表:
id|name|salary|
--+----+------+
1|张三 | 2000|
解决脏读问题
模拟场景
-- bilibli开启事务
START TRANSACTION;
-- 1. 查看张三用户余额
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
-- 2. bilibli自动扣款
UPDATE test_table tt
SET tt.salary = tt.salary - 200
WHERE name = '张三';
-- 设置Session隔离级别为READ UNCOMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 张三开启事务
START TRANSACTION;
-- 1. 张三查看账户余额
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三|1800 |
-- 3. 自动扣款发生异常,回滚事务
ROLLBACK;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
- 张三查询的账户余额并不是真实余额,发生了脏读。
解决
- 将 MYSQL 默认的隔离级别设置为
READ COMMITED
。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 执行同样的操作
START TRANSACTION;
SELECT * FROM test_table tt;
COMMIT;
// output
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
解决不可重复读
模拟场景
-- 设置Session隔离级别为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 1. 张三开启事务
START TRANSACTION;
-- 2. 张三查看余额
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
-- 3. bilibli开启事务
START TRANSACTION;
-- 4. bilibli自动扣款
UPDATE test_table tt
SET tt.salary = tt.salary - 200
WHERE name = '张三';
-- 5. 提交事务
COMMIT;
-- 6. 张三再次查看余额
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 1800|
-- 7. 提交事务
COMMIT;
- 张三两次查询的账户余额不同,发生了不可重复读。
解决
- 将 MYSQL 默认的隔离级别设置为
REPEATABLE READ
。
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
- 执行同样的操作
-- 1. 张三开启事务
START TRANSACTION;
-- 2. 张三查看余额
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
-- 3. bilibli开启事务
START TRANSACTION;
-- 4. bilibli自动扣款
UPDATE test_table tt
SET tt.salary = tt.salary - 200
WHERE name = '张三';
-- 5. 提交事务
COMMIT;
-- 6. 张三再次查看余额
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
-- 7. 提交事务
COMMIT;
幻读
模拟场景
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 1. bilibli 管理员开启事务
START TRANSACTION;
-- 2. bilibli 管理员查看账号信息
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 3.Boss 开启事务
START TRANSACTION;
-- 4. Boss 注册bilibli账号
INSERT INTO test_table (id,name,salary)
VALUES(2,'Boss',100000);
-- 5. 提交事务
COMMIT;
-- 6. bilibli 管理员新增 Boss 账号
INSERT INTO test_table (id,name,salary)
VALUES(2,'Boss',100000);
// 发生报错,主键冲突,新增失败
// SQL 错误 [1062] [23000]: Duplicate entry '2' for key 'test_table.PRIMARY'
-- 7. bilibli 管理员再次查看账号信息,还是没有
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
- bilibli 管理员新增失败后,再次查看,还是没有 Boss 的账号。
解决
- 将 MYSQL 默认的隔离级别设置为
SERIALIZABLE
。
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- 再次执行
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 1. bilibli 管理员开启事务
START TRANSACTION;
-- 2. bilibli 管理员查看账号信息
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 2000|
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 3.Boss 开启事务
START TRANSACTION;
-- 4. Boss 注册bilibli账号
INSERT INTO test_table (id,name,salary)
VALUES(2,'Boss',100000);
// 发生阻塞,无法新增数据。。。。。。。。。。
-- 5. bilibli 管理员新增 Boss 账号
INSERT INTO test_table (id,name,salary)
VALUES(2,'Boss',100000);
-- 6. bilibli 管理员再次查看账号
SELECT * FROM test_table tt;
|id|name|salary|
|--+----+------+
| 1|张三 | 1800|
| 2|Boss|100000|
// 结束阻塞,新增报错
Reference
- 黑马程序员 MySQL数据库入门到精通,从mysql安装到mysql高级、mysql优化全囊括