0
点赞
收藏
分享

微信扫一扫

mysql四大事务特性原理拆解——MVCC版本控制..

杨小羊_ba17 2022-02-21 阅读 36

什么是事务?

 ✦ 解析:A给B转账200元,第一步执行:A -200。 第二步执行:B +200,如果第一步执行完毕,还没来得及执行第二步,此时银行系统故障,则结果未  A 少了200元,但是B没有收到钱,所以就需要事务,将扣除你的100元再返回给你,就是这么简单。

事务控制语句

a. 开关事务

-- 1.关闭事务
set autocommit = 0;

-- 2.开启事务(mysql默认,即执行sql语句后就会马上执行 commit 操作)
set autocommit = 1;

-- 3.查看当前库是否开启了事务自动提交   ON:开启(默认)    OFF:关闭中
show variables like 'autocommit%';

注意:autocommit参数是针对连接的,在一个连接中修改了该参数,不会对其他连接产生影响。

b. 手动提交事务

-- 1.开启一个事务
start transaction; 或者 begin;

-- 2.提交事务,持久化(成功)
commit; 或 commit work;

-- 3.回滚事务:回到事务前的数据状态(失败)
rollback; 或 rollback work;

c. 事务块的保存点

-- 1.允许在事务中创建一个保存点,一个事务中可以有多个保存点
savepoint [保存点的名称];

-- 2.把事务回滚到标记点
rollback to savepoint [保存点的名称];

-- 3.删除一个事务的保存点
release savepoint [保存点的名称]; 

事务场景演示

第一步:关闭自动提交      由于mysql 默认是开启事务自动提交的,先关闭自动提交

第二步:开启事务       使用begin start transaction 语句开启事务,并执行转账语句

第三步:回滚事务    使用 rollback 命令撤销正在进行的所有未commit提交的修改;

第四步:提交事务    一旦执行commit,数据将被永久保存到数据库中

结论

  • 从 begin 到  commit  命令之间的一系列操作为一个完整的事务周期。
  • 手动开启一个事务,若不执行commit 命令,终止 MySQL 会话后,数据库将会自动执行回滚
  • 只有当执行 commit 命令后,数据的改变才会持久化的保存到数据库中
  • 如自动提交未关闭,begin手动开启一个事务后,自动提交将被暂时禁用,直到使用commit 或 rollback 结束事务。自动提交会恢复开启状态。

扩展:理解和使用 savepoint 保存点

第一步:执行第一次转账成功后(A有900元,B有100元),设置一个保存点,如下

第二步:继续执行第二次转账成功(A有800元,B有200元)

 

第三步:回滚数据,撤销第二次的转账


 

衡量事务四大特性
 

▎  原子性 (Atomicity)     

定义:如果事务中某条语句执行失败了,前面已经执行成功的语句,会回滚到未执行前的状态,就像这个事务从来没有执行过一样。

实现原理:undo log(回滚日志)

逻辑格式的日志,在执行undo时,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。

解析

每对一条记录做一次改动,就会产生1条或者2条 undo log。一个事务中可能有多个增删改SQL语句,一个SQL语句可能会产生多条 undo log,一个事务中的这些 undo log 会被从 0 开始递增编号,这个编号称为 undo no。

回滚原理

undo log是逻辑日志,记录的是sql执行相关的信息,当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:

  • 对于insert,回滚时会执行delete;
  • 对于delete,回滚时会执行insert;
  • 对于update,回滚时会执行一个相反的update;

InnoDB存储引擎中,undo log类型

  • insert undo log
  • update undo log

insert undo log是指在insert 操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。

update undo log记录的是对delete 和update操作产生的undo log,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。

undo log什么时候释放?

答:当事务提交后,undo log并不能立马被删除,提交时放入undo log链表,由purge线程判断是否有其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可清理undo log

▎  一致性 (Consistency)  

定义:指事务执行结束后,数据库的完整性约束未被破坏,数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的总和应该不变),保证数据的正确和一致性
 

实现一致性的措施包括

  • 数据库层面:必须要实现A(原子性)、I(隔离性)、D(持久性) 三大特性,才有可能实现一致性
  • 应用层面:例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

结论:ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段


 

▎  隔离性  (Isolation) 

定义:当多个用户同时访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务具有隔离性,不会被互相干扰,各搞各的互不影响!

问题一:事务并发,带来什么问题?(脏读、不可重复读、幻读)

1. 脏读 (dirty read):当前事务A中可以读到事务B未提交的数据

  • 张三余额100,事务A、B 同时开启事务
  • 事务B执行修改,将张三的余额从100修改至200
  • 在B未提交事务前,A此时查询张三余额,得到的结果是:200

时间顺序

事务A事务B
开始事务

开始事务

 修改张三余额 由原100 修改为 200元

查询张三余额,结果为:200【脏读

提交事务

提交事务

结论:事务A、B交替执行,事务A被事务B干扰到了,事务A读取到事务B未提交的数据!

2. 不可重复读 (unrepeatable read):事务A中先后两次读取同一个数据,两次读取的结果不一样

  • 事务A先查询张三余额,结果为100
  • 事务B执行修改,将张三的余额从100修改至200,并提交了事务!
  • 事务A再次查询张三余额,发现变为了200

时间顺序

事务A事务B
开始事务

开始事务

查询张三余额,结果为:100元(初始值)

修改张三余额,由原100修改为200元

提交事务
查询张三余额,结果为:200元【不可重复读
提交事务

结论:事务A再次被事务B干扰到了,相同的查询语句,却返回了不同的数据。

3. 幻读 :select某记录是否存在,不存在则插入记录,执行 insert 时发现记录已存在,无法插入

  • 事务A:检测表中是否有 id 为 1 的记录,没有则插入,这是我们期望的正常业务逻辑
  • 此时事务B 先插入一条数据:{ id = 1 , name ="王五" },并提交了事务。
  • 接着,事务A执行插入语句{ id = 1 , name ="张三" },提示报错:表的主键冲突
  • 事务A再次查询user表记录,发现表里还是没有数据,但插入又提示报错

时间顺序

事务A事务B
开始事务

开始事务

查询user表记录,结果:空(没有数据)

insert 一条数据(id = 1, name="王五")

执行 insert 语句(id = 1, name="张三")

提交事务

插入失败,报错:

error 1062:Duplicate entry “1” for key ‘primary’

查询user表记录,结果:空(没有数据)
提交事务

结论:幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

!! 注意:目前网上的众多解释幻读的概念,都是说在一个事务内,执行两次select查询的结果不一样,即 select 1 得到 10 条记录,select 2 得到 11 条记录,这其实并不是幻读,这是不可重复读的一种,而且只会在 读未提交、读已提交 级别下出现,而在 mysql 默认的 可重复读 隔离级别是不会出现的!

问题二:如何解决事务并发的问题?

隔离性最简单最直接的实现方式就是串行执行。让事务一个一个执行,前面的事务还没执行完,后面的事务就都等待。但这样的实现方式很明显并发效率不高,并不适合在实际环境中使用。

——那有没有更好的方式呢?

☛ 实现隔离机制

  • 读写锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
  • MVCC多版本并发控制 :一次性快照读。不加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot), 并用该快照提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。

一、事务隔离级别

隔离级别脏读不可重复读幻读

Read Uncommitted

(读未提交)

存在存在存在

Read Committed

(读已提交)

-存在存在

Repeatable Read

(可重复读)默认

--存在

Serializable

(可串行化)

---

二、数据库隔离级别配置相关命令

***************************** 查看隔离级别 *****************************

-- 方式一:输入以下命令查看事务隔离级别,(其中transaction_isolation就是隔离级别)
mysql> show variables like 'transaction%';
+----------------------------------+-----------------+
| Variable_name                    | Value           |
+----------------------------------+-----------------+
| transaction_alloc_block_size     | 8192            |
| transaction_allow_batching       | OFF             |
| transaction_isolation            | REPEATABLE-READ |
| transaction_prealloc_size        | 4096            |
| transaction_read_only            | OFF             |
| transaction_write_set_extraction | XXHASH64        |
+----------------------------------+-----------------+

-- 方式二:使用sql查看(该语句只能使用命令窗口)
--  SELECT @@global.transaction_isolation;  查看全局
--  SELECT @@session.transaction_isolation;  查看当前会话
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+





***************************** 设置隔离级别 *****************************

-- 语法:set [会话 | 全局] transaction isolation level [隔离级别]

-- 设置当前会话,隔离级别为:读未提交 read uncommitted
set session transaction isolation level read uncommitted ;

-- 设置全局会话,隔离级别为:读未提交 read uncommitted
set global transaction isolation level read uncommitted ;

三、命令演示:各个隔离级别事务之间的数据变化

读未提交(Read Uncommitted)

结论:读未提交,一个事务会读到其他事务未提交的数据,即存在脏读问题。事务B都没执行commit 提交到数据库,就被事务A就读到了其修改的数据。

读已提交(Read Committed)     解决:脏读

➳ 结论:读已提交。当前事务A只能读取到事务B已经commit 提交的数据。
 

可重复读(Repeatable Read)     解决:脏读、不可重复读

 ➳ 结论:可重复读。只要当前事务A未结束,则A在事务期间每次读的数据都跟第一次读到的结果一致,即便其他事务已经修改了数据,并commit提交到了数据库。

RR级别存在幻读

场景一:

流程解析:

  1. 事务A,检测表中是否有记录,没有则插入id为1的纪录,这是我们期望的正常业务逻辑。
  2. 事务B,在A插入前,先插入一条数据,并提交事务,目的在于扰乱A的正常的事务执行。
  3. 在 RR 隔离级别下,step1、step2 是会正常执行的,step3 则会报错主键冲突!

场景二:RR级别防止幻读:加排他锁或共享锁

流程解析:

  1. 事务A,查询user表所有数据。(step1)
  2. 事务B,插入一条数据,并提交事务。(step2)
  3. 事务A,再次查询user表数据,结果与第一次查询一致,实际库中记录已改变(step3)
  4. 利用for update,对该记录加排他锁,并转为当前读,期间其他用户阻塞等待(step4)

场景三:RR级别防止幻读:不存在的记录是否会加锁?

假设现在 user表数据为空,没有任何记录,如下:

☛ 演示 id = 1 的记录不存在的场景:

流程解析:

  1. 事务A,查询 id =1 的记录,利用for update,对该 “记录” 加锁
  2. 事务B,插入一条数据失败,原因:等待 step1中事务A释放该“记录”的锁,等待超时

扩展:加锁读

select .. for update   

对读取的行加 X 排它锁,其他事务不能对该行再加任何锁。(串行化隔离级别下会隐式做此操作)

select .. lock in share mode 

对读取的行加 S 共享锁。其他事务可对该行继续加 S 锁,但是不允许添加 X 锁,否则会被阻塞住


 

串行化(Serializable)     解决:脏读、不可重复读、幻读

 ➳ 结论:串行化。当前A不结束事务,则在A的事务期间,事务B对数据执行写操作,会进入阻塞状态,直到A结束事务。事务B对表的写操作,在等事务A的读操作。读写都不允许并发。

 

四、保证隔离性:锁机制

★ 读未提交(Read Uncommitted)    采取的是读不加锁原理

  • 事务读不加锁,不阻塞其他事务的读和写
  • 事务写阻塞其他事务写,但不阻塞其他事务读;

 串行化(Serializable)      读加共享锁,写加排他锁, 读写互斥

  • 所有SELECT语句会隐式转化为 select...for share,即加共享锁。
  • 读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有select这些行的语句都会阻塞。

五、保证隔离性:MVCC一次性快照读

在了解mvcc多版本并发控制前,我们先了解两个概念,什么是一致性锁定读 和 一致性锁定读

☛ 一致性锁定读

所谓一致性锁定读,就是加锁读,对读取的行进行加锁。读期间其他事务无法对该数据行进行修改;保证读取最新和数据逻辑的一致性!

InnoDB 引擎对读操作支持两种加锁方式: select .. for update  和 select .. lock in share mode ,加 X排它锁 或加 S共享锁,保证读取到的行是最新的。! 注意:加锁读时不会采用MVCC!

☛ 一致性锁定读

一致性非锁定读就是读快照。快照:当前行数据的历史版本,每行记录可能存在多个历史版本,也就是多份快照数据——行多版本技术

由于一个行记录可能存在多个快照(历史版本),从而引发一系列的并发问题,如何解决这些并发问题,就是所谓的多版本并发控制(MVCC)。非锁定读机制极大地提高了数据库的并发性。

✎ 一致性锁定读:RC表现形式

  • 在 读已提交 事务隔离级别下,总是读取行的最新版本;如果行被锁定了,非一致性读不会因此去等待行上锁的释放,而是去读取该行版本的最新的一个快照。

 ✎ 一致性锁定读:RR表现形式

  • 在 可重复读 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时,第一次读到的行数据版本

通俗理解:假设事务A第一次读到的是 快照数据1 ,接着事务B修改数据并提交事务,产生了新的快照数据,事务A第二次读的时候,还是读取快照数据1,而不是去读最新的快照数据

5.1 undo log 版本链

B+ 索引树上对应的记录只会有一个最新版本,只不过 InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。

 

 

5.1.1 实现原理 

  •  trx_id :最近更新这条行记录的事务id
  •  roll_pointer :指向之前生成的 undo log

步骤一:假设 id=100 的事务 A 插入一条记录,则 trx_id = 100roll_pointer 指向空的undo log,因为这是一条新的数据,不存在历史版本

步骤二:接着,id = 200 的事务 B 修改了这条行记录,把 age 从18修改为20,于是这条行记录的 trx_id 就变成了 200, roll_pointer 就指向事务 A 生成的 undo log 

步骤三:最后,id = 300 的事务 C 也来修改这条行记录,把age从20修改为30,如下:

 结论 :每次修改记录都会更新 trx_id roll_pointer 字段的值,该条数据的历史版本对应的undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链

5.1.2 回滚日志(undo log) 类型

在 InnoDB 存储引擎中,undo log 只分为两种:

  • insert undo log:在 insert 操作中产生的 undo log
  • update undo log:对 delete 和 update 操作产生的 undo log

MVCC 机制,主要靠 update undo log 实现,对于insert操作产生的insert undo log, 只对事务本身可见。所以也不存在并发的问题。故该undo log可以在事务提交后直接删除。

update undo log记录的是对delete 和update产生的undo log,该undo log可能需提供MVCC机制,故事务提交后不会立马删除。而是放入undo log链表中,等待purge线程判断是否可删除

5.2 ReadView (读视图) 机制

主要由以下部分组成:

5.2.1 ReadView生成时间

在InnoDB中,begin/start一个事务并不会立即分配事务id,而是真正执行了操作才会分配事务id。例如事务A和事务B根据下图时间线执行:

✦ 解析 :虽然事务A先begin,但它执行select时能看到事务B提交的数据,因为事务在begin的时候没有真正开始一个事务,事务A的 readview 是在select 时生成的,此时事务B对数据修改的版本快照,按规则来说:trx_id < min_trx_id,属于已提交事务生成,所以对于事务A来说可见。

流程示例:事务并发操作时读取的ReadView变化

  1. 事务A 插入一条 id=1的数据 (已提交事务)
  2. 事务B第一次查询 id =1  (查询 1中事务A已提交的数据)
  3. 事务C修改 id =1的数据
  4. 事务B第二次查询 id =1  (查询 3中事务C修改了但未提交的数据)
  5. 事务C第一次查询 id =1  (查询 3中自己修改的数据)

注意:事务A已提交事务,事务B、C同时并发执行,但不提交

步骤一: 数据插入并提交数据

假设表中已被事务 A(id = 100)插入了一条行记录,并完成commit提交


 

步骤二:生成ReadView

事务B开始执行select, 此时会生成一个 ReadView,数据如下:(因为事务B、C并发begin开始事务,所以两者都存在活跃事务ids中)

步骤三:事务B第一次查询   查询事务A已提交的数据

事务B执行第一次查询,会把这行数据的 trx_id 和快照中的 min_trx_id 进行判断,发现 trx_id=100,而在当前快照里,活跃的最小事务id=200,这说明在事务B (id=200) 开始之前,该条数据就已经存在并完成事务的提交,属于历史记录。所以事务B可以查询到该条数据!

步骤四:事务C修改,更新隐藏字段 trx_id、roll_pointer

接着事务C执行修改这条记录,把 age=18 修改为20,故这行记录的 trx_id 更新为300, 同时 roll_pointer 指向了事务 C 修改之前生成的 undo log,如图:

步骤五:事务B第二次查询    查询事务C修改了但未提交的数据

事务B执行第二次查询,发现该行数据的 trx_id=300,大于ReadView 快照中的最小活跃事务min_trx_id (200),且小于下一个事务自增id  max_trx_id (301)

故事务 B 会去判断所有活跃事务m_ids 里是否存在 trx_id=300 的事务,存在说明 id = 300 的事务是跟自己(事务 B)在同一时间并发执行的,则该 age=20的数据不能被事务B所查询到:

——那么该如何查询? 

事务B会根据undo log 版本链排查,根据该行数据的 roll_pointer指针往下找,就会找到最近的一条 trx_id=100 的 undo log,而自己的 id 是 200,说明 trx_id = 100 的 undo log 版本是在事务 B 开启之前就已经提交了的。故事务 B 的这次查询读到的就是该版本undo log的数据,即 age = 18。

步骤六:事务C第一次查询    查询自己修改的数据

事务C查询 id=1的记录,会生成一份ReadView,发现 trx_id=300 与 ReadView 里的 creator_trx_id=300 一样,说明这是事务C刚刚自己修改的,是可以被查询到的,即age=20

思考:如新开一个事务D (id=400),将age修改为30并提交事务,事务C能读到吗?

答:RC级别下可以读到,RR级别不可以(加锁读除外)

一、图解演示:RC级别

解析:事务D修改数据提交事务后,事务C执行select查询,重新生成了一份ReadView快照,发现最近更新了id=1这条记录的 trx_id =400,大于最小活跃事务min_trx_id (200) ,且小于下一个事务自增的max_trx_id (401) 

说明可以通过当前快照得到是否可见,继续判断:trx_id =400 是否存在活跃事务m_ids

  • 存在:说明该条记录是已开始但没提交的事务修改的,当前事务C不可见
  • 不存在:说明这条记录对于事务C来说,是已提交的事务生成的,当前事务C可见

结论:根据RC级别机制,事务C可以读取到事务D所做的修改 age=30的记录

二、图解演示:RR级别

解析:事务D修改数据提交事务后,事务C查询这行记录时,发现最近更新了id=1这条记录的 trx_id =400,大于下一个事务自增的max_trx_id (301) ,这说明事务C执行期间,有另外一个事务更新了数据(可理解为 "将来" 的事务操作的),所以该条数据对于事务C不可见。

结论:根据RR级别机制,事务C不可以读取到事务D所做的修改 age=30的记录

5.3  通过 undo log 版本链和 ReadView 机制

  • 可以保证一个事务不会读到并发执行的另一个事务的更新
  • 可以保证一个事务只可以读到该事务自己修改的数据或该事务开始之前的数据

六、隔离性总结

1、RR 级别作为 mysql 事务默认隔离级别,是事务安全与性能的折中,可能也符合二八定律(20%的事务存在幻读的可能,80%的事务没有幻读的风险)。

2、serializable 级别悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,故事务是安全的,但需要认真考虑性能。

3、InnoDB 的行锁锁定的是索引,而不是记录本身,故某索引相同的记录都会被加锁,会造成索引竞争,建议尽可能的使用主键或唯一索引对记录加锁。索引映射的记录如果存在,加行锁,如果不存在,则会加 next-key lock / gap 锁 / 间隙锁


 

▎  持久性 (Durability)  

定义:事务一旦被提交,对数据库中的改变就是永久性的,即便是在数据库发生了故障,也不会对其数据有任何影响。

实现原理:redo log(重做日志)

☛ 缘起:InnoDB作为MySQL的存储引擎,数据存放磁盘,每次读写数据都需磁盘IO,效率很低。

结果:InnoDB提供了缓存(Buffer Pool),作为访问数据库的缓冲

  • 读:先从Buffer Pool中读取,如Buffer Pool中没有,则从磁盘读取后再放入Buffer Pool
  • 写:先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(此过程称为刷脏)

结论:Buffer Pool的存在提高了读写数据的效率,但也存在新的问题:如果MySQL宕机,而此时缓存中修改的数据还没刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

解决:redo log,当数据修改时,既修改Buffer Pool中的数据,还会记录到redo log;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log日志,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

思考 :既然redo log也需在事务提交时将日志写入磁盘,为何比直接将Buffer Pool中修改的数据写入磁盘 (即刷脏) 要快呢?

主要有以下两方面的原因

(1)刷脏是随机IO,每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

1、什么时候产生?

事务开始后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。

2、什么时候释放?

对应事务的脏页写入到磁盘后,重做日志占用的空间就可以重用(被覆盖)

MySQL 事务使用注意

1)  事务尽可能简短

事务的开启到结束会在数据库系统中保留大量资源,以保证事务的ACID特性。在多用户系统中,较大的事务会占用系统的大量资源,从而影响软件的运行性能,甚至导致系统崩溃。

2)  事务中访问的数据量尽量最少

当并发执行事务处理时,事务操作的数据量越少,事务之间对相同数据的操作就越少。

3)  查询数据时尽量不要使用事务

查询操作并不会更新数据库的数据,应尽量不使用事务查询数据,避免占用过量的系统资源。

4)  在事务处理过程中尽量不要出现等待用户输入的操作

在处理事务的过程中,如需等待用户输入数据,则事务会长时间地占用资源,可能造成系统阻塞。

举报

相关推荐

0 条评论