Day6 Spring事务管理(1)
理解事务之前,先讲一个你日常生活中最常干的事:取钱。
比如你去ATM机取1000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉1000元钱;然后ATM出1000元钱。这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。所以,如果一个步骤成功另一个步骤失败对双方都不是好事,如果不管哪一个步骤失败了以后,整个取钱过程都能回滚,也就是完全取消所有操作的话,这对双方都是极好的。
事务就是用来解决类似问题的。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。
什么是数据库事务
数据库事务四大特性(ACID):
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
A: Atomicity 原子性
是什么?
事务要么执行成功,要么失败,不会只执行一部分。
怎么做到的?
- 事务开始时,设置begin;
- 完成后,执行commit;
- 失败时,执行rollback;
注意:commit和rollback只会执行其一。
rollback(回滚)是如何做到的?
回滚是根据undo_log实现的,在数据库每个修改操作前,都会写undo_log用来记录修改前的值。回滚时,按照undo_log反向执行一次,即可将数据恢复到事务之前的状态;
C: Consistency 一致性
一致性是:
- ACID中最重要的原则,也是最终目的;
- 状态的一致性;
- 转账时,双方个人金额可能有变化,但两人总金额不变;
- 类比:能量守恒定律,能量只能转移,不能凭空增加或消失;
I: Isolation 隔离性
处理什么场景?
并发事务,即有多个事务同时在进行。多个事务同时操作相同的数据库,彼此间可能会有影响。
事务并发执行时,到底会有哪些影响?
- 脏读,读取了其他事务未提交的值,而这些未提交的值可能是无效的值(例如被回滚了)
- 不可重复读,即两次读取的数据发生了变化;
- 幻读,即两次读取数据时,产生了之前不存在的数据;
隔离性是如何来管理上述影响的?
通过设置不同的事务隔离级别。隔离级别见下面小节。
D: Durability 持久性
事务提交之后,数据会被存储到持久化的设备上。通俗讲,就是会写硬盘。
随机IO写效率差,为了提高效率,使用了WAL(Write Ahead Log)技术,参照 HBase的 Hlog,MySQL对应为redo-log。
隔离级别
查找数据当前隔离级别:show variables like "%isolation%";
MySQL 默认的隔离级别是:REPEATABLE-READ
Oracle、SQL Server,它们的默认隔离级别也是REPEATABLE-READ吗?
read-uncommitted
read-uncommitted:未提交读,可以读取没有提交的数据,会造成“脏读”。
设置隔离级别:mysql> set transaction_isolation="read-uncommitted";
脏读示例:
时间 | 事务A | 事务B |
---|---|---|
T1 | begin; | |
T2 | begin; | |
T3 | select money from account where id = 1; // money初始查询为:0 | |
T4 | select money from account where id =1; // money初始查询为:0 | |
T5 | // 将money更新为100 update account set money = 100 where id = 1; | |
T6 | select money from account where id = 1; // 此时可以读取到未提交的值:100 // 读取到未提交的值,称之为“脏读” |
read-committed
read-committed:提交读,没有提交的数据不会被读取,只会读取提交了的数据。
虽然不至于读取未提交的无效值,但有些应用可能要求在当前事务内,数据可重复读取(即多次读取的值是一样的)。提交读没有解决可重复读。
设置隔离级别:mysql> set transaction_isolation="read-committed";
不可重复读示例:
时间 | 事务A | 事务B |
---|---|---|
T1 | begin; | |
T2 | begin; | |
T3 | select money from account where id = 1; // money初始查询为:0 | |
T4 | select money from account where id =1; // money初始查询为:0 | |
T5 | // 将money更新为100 update account set money = 100 where id = 1; | |
T6 | select money from account where id = 1; // 此时读取的值没有变化,仍然是 0 |
|
T7 | commit | |
T8 | select money from account where id = 1; // 此时读取的值因其他事务提交而变化,变为100 // 同一事务内,多次读取数据不同,成为“不可重复读”。 |
repeatable-read
由于历史原因,repeatable-read为MySQL默认的隔离级别。但需要注意,该隔离级别的效率并不高,如Oracle、SQL Server等都是使用read-committed作为默认隔离级别。
在当前事务内,所有读取的数据不会再被更新,重复读取时,数据不会有变化。
注意,MySQL的repeatable-read解决了幻读问题。
Serializable
串行化:所有的操作都被串行化,读也会被加锁。效率非常低,基本不使用。
数据读写
读
读会加锁吗?
读到底发生了什么?
快照读
快照读:
- 数据拥有很多的版本;
- MVCC:多版本并发控制协议;
- 读取了其中的某一个版本,版本跟隔离级别有关;
- 哪些是快照读?比如:
select * from account
; - 好处:快,支持高并发。
MVCC(多版本并发控制协议)
-
repeatable-read隔离级别下
- 记录数据在读取事务开始时刻,已提交的(未删除的)的版本;
- 在整个事务中,只会读取该版本的数据,任何修改更新都不会影响;
-
read-committed隔离级别下
- 数据读取的时刻,已提交的(未删除)的数据;
-
read-uncommitted隔离级别下:
- 没有版本的概念,所有数据都会更新到数据库,只用读取最新的数据即可。
-
serializable
- 没有版本的概念,读也会加锁。
当前读
当前读:只会读取当前最新的值。
示例:
update account set money = money - 50 where id = 1;
以及所有的修改命令,
再加上:
select * from account where ... lock in share mode
:读取数据,并加共享锁。select * from account where ... for update
:读取数据,并加排它锁/互斥锁。
写锁
在事务内,对该行的写操作会加写锁,此时其他事务,无法对该行做修改,除非当前事务提交/回滚,锁被释放。
写锁可以解决:
- 临界资源写冲突;
- 修改丢失;
repeatable-read
-
修改同一行数据;
第二个事务会阻塞; -
对某一范围做修改,会产生间隙锁;
| id | name | password | money |
| ---- | ----- | -------- | ----- |
| 1 | admin | admin | 100 |
| 4 | dd | 123 | 0 |
| 10 | tt | 123 | 2000 |
示例1:
事务A:
- update account set money = 10000 where id >3;
- 加锁:
1)4和10加上行锁;
2)加间隙锁:(1, 4), [4, 10),[10, )
事务B 插入:
- insert into account (id, name, password) values(2, 'a', 'a');
- 插入失败,原因存在一个(1, 4)的间隙锁。id=2,在这个间隙锁中。
示例2:
事务A: - update account set money = 10000 where id >5;
- 加锁:1)行锁:10
2)加间隙锁:(4,10], (10, )
事务B 插入: - insert into account (id, name, password) values(2, 'a', 'a');
- 插入成功。
read-committed
- 没有间隙锁;
- 非索引写,不锁表;
可将隔离级别设置为read-committed,然后使用上述repeatable-read下的两个示例,可以看到不同的加锁行为
如何分析加锁
- 查看事务的隔离级别;
- 如果是RR
- 行锁:按行操作时;
- 间隙锁:
- 唯一索引: 范围操作
- 非索引: 锁表
- 非唯一索引:行锁 + 间隙锁
- 如果是RC,写操作只会加锁对应行;
RR和RC比较:
- RC的效率更高;
- MySQL为什么要选RR?在MySQL5之前,binlog使用的是statement格式,在主从复制时,存在bug(数据不一致)。从MySQL5.1开始,新增了row格式的binlog,解决了此bug。但为了满足之前的老版本,所以当前MySQL默认为RR。
在MySQL下,发生死锁时:
- 发生死锁的事务会马上停止,MySQL会提示发生了死锁;
- 该事务所有的操作都变为无效,所持有的资源(锁)都会被释放;