事务
在执行SQL语句的时候,因为某些业务需求,一些列操作必须执行完毕, 而不能仅仅 执行一部分. 例如一个转账操作. id为1 的用户 给id为2的用户转账100块 . 这种操作是需要全部执行的. 可能出现的情况:
- 第一条执行完毕之后 , 第二条执行失败.
- 第二条执行成功 , 但是第一条执行失败.
这种操作要么就全部执行 , 要么就全部不执行 .
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
此类将多条语句 , 作为一个整体进行操作的功能被称为数据库的 事务 , 数据库事务可以确保该事务范围内的所有操作都可以 全部成功 或 全部失败 , 如果执行失败 , 效果就像没有执行过这些事务一样 , 不会对数据库造成任何改动 .
可见数据库事务具有 ACID 这四个特性 .
- A: Atomic , 原子性,将所有SQL作为原子工作单元执行, 要么全部执行, 要么全部不执行;
- C: Consistent , 一致性, 事务完成之后, 所有数据的状态都是一致的 , 即 A 账户 减去100 , 那么B账户一定增加了 100 ;
- I: Isolation , 隔离性, 如果有多个事务并发执行, 每个事物做出的修改必须和其他事务隔离;
- D: Duration , 持久性,即 事务完成之后, 对数据库的修改被持久化存储.
对于单条SQL语句来说 , 数据库系统自动将其作为一个事务执行 ,这种事务被称为 隐藏事务 .
需要手动将多条SQL语句作为一个事务执行, 使用 BEGIN 开启一个事务 , 使用COMMIT提交一个事务, 这种事务被称为显示事务.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
很显然多条语句需要作为一个事务执行的时候, 就必须使用显示事务 . COMMIT 是提交事务, 即试图将事务内的所有SQL 提交执行并永久保存 , 如果 COMMIT 失败的话 , 整个事务都会失败 且 不会对数据库产生任何影响. 有些时候我们希望事务可以主动失败 ,这个时候可以用ROLLBACK 回滚事务, 整个事务都会失败:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
ROLLBACK;
止于数据库的时候实现, 这个是由数据库系统保证的 , 我们只需要根据业务逻辑使用即可.
对于两个并发执行的事务, 如果牵扯到操作同一条记录的时候 , 可能会发生问题 , 因为并发操作会带来数据的不一致性 , 包括脏读 , 不可重复读, 幻读 等 . 数据库系统提供了隔离级别来让我们有针对性的选择事务的隔离级别, 避免数据的不一致性问题.
SQL提供了四种级别 分别对应可能出现的数据不一致的情况 .
Isolation | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted | YES | YES | YEs |
Read Committed | - | YES | YES |
Repeatable Read | - | - | YES |
Serializable | - | - | - |
Read Uncommitted
Read Uncommitted 是隔离级别最低的一种事务级别, 在这种隔离级别下, 一个事物会读到另一个事务更新后但未提交的数据, 如果另一个事物回滚, 那么当前事务读到的数据就是脏数据, 这就是脏读(Dirty Read).
模拟脏读 , 首先准备好students的数据, 该表仅一条记录 .
id | name |
---|---|
1 | Alice |
开启两个 MySQL 查询 . 按照下列顺序执行.
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED | SET TRANSACTION LEVEL READ UNCOMMITTED |
2 | BEGIN; | BEGIN; |
3 | UPDATE students SET name="Bob" WHERE id=1; | - |
4 | - | SELECT * FROM students WHERE id=1; |
5 | ROLLBACK; | - |
6 | - | SELECT * FROM students WHERE id=1; |
7 | - | COMMIT; |
- 首先设置Read Umcommitted的数据隔离级别.
- 开始事务操作.
- 在事务A 中更新id为1 的记录name字段为 Bob.
- 在事务B 中读取students的所有记录, 因为 数据隔离级别为Read Uncommitted, 所以可以读到 事务A中尚未提交的数据,
- 事务A回滚, 这个时候 第4步 事务B读到的数据就成为脏数据.
- 略.
- 略.
Read Committed
Read Committed 在隔离级别下, 一个事务可能会遇到不可重复读(Non Repeatable Read)的问题. 不可重复读是指, 在一个事务内, 多次读统一数据, 在这个事务还没有结束时, 如果另一个事务恰好修改这个数据, 那么在第一个事务中, 两次读取的数据就可能不一致.
模拟不可重复读 , 首先准备好students的数据, 该表仅一条记录 .
id | name |
---|---|
1 | Alice |
开启两个 MySQL 查询 . 按照下列顺序执行.
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
2 | BEGIN; | BEGIN; |
3 | - | SELECT * FROM students WHERE id=1; |
4 | UPDATE students SET name="Bob" WHERE id=1 | - |
5 | COMMIT; | - |
6 | - | SELECT * FROM students WHERE id=1; |
7 | - | COMMIT; |
- 首先设置Read Committed的数据隔离级别.
- 开始事务操作.
- 在事务B 中查询id=1的所有信息.
- 在事务A 中更新id=1的记录, 因为 数据隔离级别为Read Uncommitted,因为在事务B读取结束之后,我们在事务A修改了原本的信息, 这个时候就遇到了 不可重复读(Non Repeatable Read)的问题.
- 事务A回滚, 这个时候 第4步 事务B读到的数据就成为脏数据.
- 略.
- 略.
Repeatable Read
在Repeatable Read的隔离级别下, 一个事务可能会遇到幻读(Phantom Read)的问题.
幻读: 在一个事务中, 第一次查询某条记录, 发现没有,但是当试图更新这条不存在的记录的时候,竟然可以成功, 并且再次读取同一条记录, 它就神奇的出现了.
id | name |
---|---|
1 | Alice |
开启两个 MySQL 查询 . 按照下列顺序执行.
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ | SET TRANSACTION LEVEL REPEATABLE READ |
2 | BEGIN; | BEGIN; |
3 | - | SELECT * FROM students WHERE id=99 |
4 | INSERT INTO students (id,name) VALUES (99,'Bob') | - |
5 | COMMIT; | - |
6 | - | SELECT * FROM students WHERE id=99; |
7 | - | UPDATE students SET name = 'Alice' WHERE id=99; |
8 | - | SELECT * FROM students WHERE id = 99; |
9 | - | COMMIT; |
- 首先设置Repeatable Read的数据隔离级别.
- 开始事务操作.
- 在事务B 中查询id=99的所有信息 , 显示查询不到.
- 在事务A 中插入一条记录 id=99,name='Bob'.
- 事务A提交信息
- 事务B 发现可以查询到 id=99的信息.
- 事务B, 更新id=99成功.
- 略.
- 略.
Serializable
Serializable是最严格的隔离级别, 在Serializable的隔离级别下, 所有事务按照次序执行, 因此脏读,不可重复读,幻读都不会出现.
在MySQL的 InnoDB,中 默认的隔离级别是Repeatable Read.
小结
Read Uncommitted按照字面意思理解就可以了 , 第一个 是可以读到 没有COMMIT的 信息的 . 因为这个原因 可能会造成脏读 的问题. 同样不可重复读和 幻读 也可以出现 .
Read Committed 按照字面意思 , 只读取已经提交的信息 , 这样可以避免回滚操作 产生的脏数据. 但是 因为 改变某条记录的时候 其他事务无法重复读取 该记录.
Repeatable Read 很明显 意思是解决了 无可重复度 这个问题 .
Serializable 序列化, 顺序执行, 不会再有问题了 ,但是 效率也是低的可怕.