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(多版本并发控制协议)

  1. repeatable-read隔离级别下

    • 记录数据在读取事务开始时刻,已提交的(未删除的)的版本;
    • 在整个事务中,只会读取该版本的数据,任何修改更新都不会影响;
  2. read-committed隔离级别下

    • 数据读取的时刻,已提交的(未删除)的数据;
  3. read-uncommitted隔离级别下:

    • 没有版本的概念,所有数据都会更新到数据库,只用读取最新的数据即可。
  4. 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

  1. 修改同一行数据;
    第二个事务会阻塞;

  2. 对某一范围做修改,会产生间隙锁;
    | 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

  1. 没有间隙锁;
  2. 非索引写,不锁表;

可将隔离级别设置为read-committed,然后使用上述repeatable-read下的两个示例,可以看到不同的加锁行为

如何分析加锁

  1. 查看事务的隔离级别;
  2. 如果是RR
    • 行锁:按行操作时;
    • 间隙锁:
      • 唯一索引: 范围操作
      • 非索引: 锁表
      • 非唯一索引:行锁 + 间隙锁
  3. 如果是RC,写操作只会加锁对应行;

RR和RC比较:

  • RC的效率更高;
  • MySQL为什么要选RR?在MySQL5之前,binlog使用的是statement格式,在主从复制时,存在bug(数据不一致)。从MySQL5.1开始,新增了row格式的binlog,解决了此bug。但为了满足之前的老版本,所以当前MySQL默认为RR。

在MySQL下,发生死锁时:

  • 发生死锁的事务会马上停止,MySQL会提示发生了死锁;
  • 该事务所有的操作都变为无效,所持有的资源(锁)都会被释放;
posted @ 2019-12-26 21:44  cheng_18  阅读(225)  评论(0编辑  收藏  举报