数据库事务特征ACID
A Atomicity 原子性
事务是一个原子性质的操作单元,事务里面对数据库的操作要么都执行,要么都不执行。
C Consistent 一致性
在事务开始之前和事务结束之后,数据都必须保持一致状态,必须保证数据库的完整性。
I Isolation 隔离性
数据库允许多个并发事务同时对数据进行操作,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别:包括读未提交(Read uncomitted)、读已提交(Read comitted)、可重复读(Repeatable read)和串行化(Serializable)。
D Durable 持久性
一个事务处理结束后,其对数据库的修改就是永久性的,即使系统故障也不会丢失。
脏读、不可重复读、幻读的区别
- 脏读:是指在一个事务中读取到另一个事务还未提交的数据。
- 不可重复读:是指在同一个事务中,前后两次读取到结果集内容不一致。
- 幻读:是指在一个事务中前后两次读取到的结果集数量不一致。
数据库隔离级别
总结如下表: √ 代表可能出现,× 代表不会出现。select @@global.transaction_isolation;
select @@transaction_isolation; # 获取当前会话隔离级别
show variables like 'transaction_isolation'; # 获取当前会话隔离级别
set session transaction isolation level read uncomitted; # 设置当前事务隔离级别为读未提交
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | √ | √ | √ |
Read committed | × | √ | √ |
Repeatable read | × | × | √ (InnoDB除外) |
Serializable | × | × | × |
MySQL默认的隔离级别就是可重复读,并且MySQL5.1以后默认的存储引擎就是InnoDB,因此MySQL默认的隔离级别也能解决幻读问题。
实验
改变当前会话的隔离级别来分别演示不同隔离级别下会出现的并发异常。
实验环境
Mysql,引擎InnoDB
/
+———-+—————+———+——-+————-+————————+
| Field | Type | Null | Key | Default | Extra |
+———-+—————+———+——-+————-+————————+
| id | int(11) | NO | PRI | | auto_increment |
| name | char(20) | NO | | | |
| money | float | NO | | 0 | |
+———-+—————+———+——-+————-+————————+
/
数据:
/
+——+———+————+
| id | name | money |
+——+———+————+
| 1 | 小王 | 1000.0 |
| 2 | 小明 | 0.0 |
+——+———+————+
/
脏读
实验步骤表:
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | 设置事务隔离级别为 read uncommitted | |
T2 | 开始事务 A start transaction; |
|
T3 | 小王转款给小明 500 元 update users set money=money-500 where id = 1; update users set money=money+500 where id = 2; |
|
T4 | 开始事务 B start transaction; |
|
T5 | 查询小明账户余额 select * from users where id = 2; 查询结果为 500 元,余额充足则执行支付逻辑 |
|
T6 | 小明账户扣款 100 元 update users set money=money-100 where id = 2; 本条语句将会阻塞(???) |
|
T7 | 事务 A 回滚 rollback; |
语句执行完毕 |
T8 | 事务 B 提交 commit; |
最后查询结果:
/
+——+———+————+
| id | name | money |
+——+———+————+
| 1 | 小王 | 1000.0 |
| 2 | 小明 | -100.0 |
+——+———+————+
/
发现小明的余额变成了-100元!发现在事务A还未提交之时事务B便读取到了事务A更新后的结果,这直接导致程序判断余额充足从而执行了扣款的逻辑。如果事务A成功提交那么程序结果就是正确的,但是事务A最后没有成功提交而是进行了回滚,这就会导致用户余额被扣款为负数的灾难。
不可重复读
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | 设置事务隔离级别为 read committed | |
T2 | 开始事务 A start transaction; |
|
T3 | 查询小明余额 select * from users where id = 2; 余额为 0 元 |
|
T4 | 开始事务 B start transaction; |
|
T5 | 小明账户充值100元 update users set money=money+100 where id = 2; |
|
T6 | 事务 B 提交 commit; |
|
T7 | 查询小明余额 select * from users where id = 2; 余额为 100 元 |
|
T8 | 事务 A 提交 commit; |
不可重复读表现在于在同一个事务之中,两次相同查询得到的查询结果却不同。这是由于两次查询结果之间出现另一个事务修改了包含之前查询结果的记录,这才导致第二次查询与第一次查询结果不同。它与脏读的区别在于修改记录的事务B必须提交成功,事务A查询时才能读取到修改后的记录,如果事务B回滚的话,事务A的查询结果还是和第一次一样。
幻读
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | 设置事务隔离级别为 repeatable read | |
T2 | 开始事务 A start transaction; |
|
T3 | 开始事务 B start transaction; |
|
T4 | 插入一行 insert into users(id, name, money) values (3, “小红”,1000); |
|
T5 | 事务 B 提交 commit; |
|
T6 | 查询users表 select * from users; 并无 id 为 3 的记录 |
|
T7 | 插入一行 insert into users(id, name, money) values (3, “小红”,1000); |
|
T8 | 出现报错:(1062, u”Duplicate entry ‘3’ for key ‘PRIMARY’”) |
对于事务A来说出现的报错就像出现幻觉一样,因为事务A在查询时并不存在ID为3的用户,而在插入时却出现了ID为3的用户已存在的报错。