数据库的事务管理

事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。当事务被提交给了DBMS(数据库管理系统),则DBMS(数据库管理系统)需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。

事务的思维图:

一. 事物的ACID 特性

数据库事务正确事项的四个基本要素(事务的锁个属性):ACID

1   A:atomicity 原子性

事务中包含的各项操作在一次执行过程中,只允许出现以下两种状态之一:要么全做,要么不做,没有中间状态。

对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。

2   C:consistency 一致性

在事务开始和完成时,数据库的数据都保持一致的状态,即事务的执行使数据库从一种正确状态转到另一种正确状态

比如A向B转账100,首先A的余额减少100元,然后B的余额增加100元,这个是数据库从一种正确状态转到另外一种正确状态。不能A向B转账,A的余额减少100元,B的余额没有变化。这就不满足数据库事务的一致性

3   I:isolation 隔离性

在并发环境中,一个事务的执行不能被其他事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。

一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的。

隔离性分4个级别,下面会介绍。

4   D:durability 持久性

一旦事务提交,那么它对数据库中的数据的改变就是永久性的,并不会被回滚。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。

提交事务时断电,如何恢复,保证数据一致性

以Sql Server举例,SQL数据库由两个文件组成,数据库文件和日志文件。利用日志文件,保证数据的一致性,undo:回滚,redo:前滚

mysql开启与提交事务

用户U1 向用户 U2转账1000。U1金额减少1000,U2余额增加1000(增加了2000,增加错了,回滚余额重新增加1000)
SELECT * from  account_tbl;# U1-5000 ,U2-2000
BEGIN; #开始事务
UPDATE  account_tbl SET  money=money-1000 WHERE user_id='U1';
SELECT * from  account_tbl; # U1-4000 ,U2-2000
SAVEPOINT s1; #保存点是事务过程中的一个逻辑点,用于取消部分事务,当结束事务时,会自动的删除该事务中所定义的所有保存点。当执行rollback时,通过指定保存点可以回退到指定的点
UPDATE  account_tbl SET  money=money+2000 WHERE user_id='U2';
SELECT * from  account_tbl;  # U1-4000 ,U2-4000
ROLLBACK TO s1; #回滚到保存点s1
SELECT * from  account_tbl; # U1-4000 ,U2-2000
UPDATE  account_tbl SET  money=money+1000 WHERE user_id='U2';
SELECT * from  account_tbl; # U1-4000 ,U2-3000
COMMIT; #提交事务

二. 并发事务带来的问题

并发事务带来的问题:更新丢失、脏读、不可重复读、幻读

事务问题原因事务顺序(绿色和橙黄色是两个事务)
更新丢失修改覆盖改    同时进行
脏读撤销改 读 撤销
不可重复读修改读 改 
幻读新增/删除读 新增/删除 -

1. 更新丢失

第一类更新丢失,回滚覆盖:撤消一个事务时,在该事务内的写操作要回滚,把其它已提交的事务写入的数据覆盖了。
 第二类更新丢失,提交覆盖:提交一个事务时,写操作依赖于事务内读到的数据,读发生在其他事务提交前,写发生在其他事务提交后,把其他已提交的事务写入的数据覆盖了。这是不可重复读的特例。

更新丢失举例1:

时间取款事务A转账事务B
T1开始事务
T2开始事务
T3读余额为1000
T4取出200,余额为800
T4读余额为1000
T6转账100,余额为1100
T7提交事务,余额为1100
T8撤销事务,余额为1000
T9最终余额为1000,更新丢失

写操作没加“持续-X锁”,没能阻止事务B写,发生了回滚覆盖。

更新丢失举例2:

时间取款事务A转账事务B
T1开始事务
T2开始事务
T3读余额为1000
T4取出500,余额为500
T5读余额为1000
T6转账100,余额为1100
T7提交事务,余额为1100
T8提交事务,最终余额为500
T9最终余额为500,更新丢失

写操作加了“持续-X锁”,读操作加了“临时-S锁”,没能阻止事务B写,发生了提交覆盖。

2. 脏读

一个事务读到了另一个未提交的事务写的数据。另一事务撤销事务。

时间转账事务A事务B
T1开始事务
T2查询余额为1000
T3转账100,账户余额为1100
T4开始事务
T5查询余额为1100
T6撤销转账,余额为100
T7提交事务

3. 不可重复读

一个事务中两次读同一行数据,可是这两次读到的数据不一样。

时间转账事务A取款事务B
T1开始事务
T2开始事务
T3查询余额为1000
T4查询余额为1000
T5取出1000,余额变为0
T6提交事务,余额为0
T7查询余额为0
T8提交事务

事务A其实除了查询两次以外,其它什么事情都没做,结果钱就从1000编程0了,这就是不可重复读的问题

4. 幻读

幻读就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。

一个事务中两次查询,但第二次查询比第一次查询多了或少了几行或几列数据。

时间新增事务A查询事务B
T1开始事务
T2开始事务
T3查询 name为memory的记录,不存在
T4新增name为memory的记录
T5提交事务
T6查询name为 memory的记录,存在
T7提交事务

三. 数据库事务的隔离级别

在MySql中,支持事务的只有InnoDB。这里说的隔离级别只针对InnoDB,MyISAM支持表级锁,InnoDB表级锁和行级锁,默认行级锁

数据库是的隔离级别有四种,读未提交、读已提交、可重复读、可串行化。

1. 读未提交 (Read uncommitted):最低级别,任何情况都无法保证。

2.  读已提交 (Read committed):可避免脏读的发生。

3.  可重复读 (Repeatable read):可避免脏读、不可重复读的发生。

4. 可串行化 (Serializable):可避免脏读、不可重复读、幻读的发生。

四种隔离级别,从上往下,级别越来越高,并发性越来越差,安全性越来越高。

事务隔离级别脏读不可重复读幻读
读未提交
读已提交×
可重复读××
可串行化×××

分别解释了四种事务隔离级别下,事务 B 能够读取到的结果。

那我们再举个例子。

A,B 两个事务,分别做了一些操作,操作过程中,在不同隔离级别下查看变量的值:

读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。
读已提交:别人改数据的事务已经提交,我在我的事务中才能读到。
可重复读:别人改数据的事务已经提交,我在我的事务中也不去读。
串行:我的事务尚未提交,别人就别想改数据。

这 4 种隔离级别,并行性能依次降低,安全性依次提高。

总的来说,事务隔离级别越高,越能保证数据的完整性和一致性,但是付出的代价却是并发执行效率的低下。

四. 数据库事务的隔离级别的原理

数据库为了维护数据ACID特性,尤其是一致性与隔离性,一般使用加锁这种方式。同时数据库是一个高并发的系统,同一时间会有多个请求访问数据库,,如果加锁过度,会降低并发处理能力,所以对于加锁的处理,正是数据库对于事务处理的精髓所在。这里通过分析MySql对于InnoDB引擎的加锁机制,来理解,在事务 处理中数据库到底做了什么。

随着数据库隔离级别的提高,数据的并发能力也会有所下降。所以,如何在并发性和隔离性之间做一个很好的权衡就成了一个至关重要的问题。

1. 一次封锁还是两段锁?


因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前申请并获得S锁( shared lock 共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前申请并获得X锁(exclusive lock  排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

2.事务的隔离级别是怎么实现的?


每行数据其实在数据库都是多个版本的,可能同一时间有很多事务在更新一条数据,事务在开始的时候会申请一个id,这个id是严格随着时间递增的,先开始的事务id总是小的,数据库的版本就是事务id的版本。

      1.读未提交

每次读的都是最新版本,这样速度是最快的,使用中的业务场景基本上没有

     2.读已提交

保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。

可避免脏读的发生,但是可能会造成不可重复读。

 如果当前数据版本的号(最新事务对这条数据的操作)比事务的id大,就会根据版本的id查看事务是否提交了,如果提交了,就会承认这条数据,如果查到这个事务还没有提交,就会查看上个版本,直到找到已提交的版本,获取那个版本的数据,那有没有读到的版本是已提交的,上个版本还没提交呢,当然是不会的,更新的时候会加上一个行锁,上个事务如果没有提交,这个事务是不可能提交的。

  3.可重复读

可重读读在事务启动的时候获取一个数组,记录未提交的事务,可重复读取数据的时候多了一个验证,如果事务提交了但是数据的版本号(操作这个数据事务的id)比当前事务高,说明这个事务是在当前事务启动后启动并且提交的,这条数据是不会被承认的,如果当数据的版本号比当前事务id低的话,说明操作是在当前事务开启之前就开启了,这条数据是被当前事务承认的。

可以发现InoDB引擎是通过MVCC解决了幻读的问题。原理可以往下看

但如果这个事务(T1)在读取某个范围内的记录时,其他事务(T0)又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,这就是幻读。

可避免脏读、不可重复读的发生。但是可能会出现幻读。

4.串行化-花费最高代价但最可靠的事务隔离级别。

用加锁的方式来避免并行访问

“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

事务 100% 隔离,可避免脏读、不可重复读、幻读的发生

5.视图

读已提交和可重复读都有视图概念的,读已提交获取的是最新提交的视图,可重复读在事务启动的时候就开启,保证事务内读到的数据是一样的,比如一个事务 执行了两次 select city from tb_user where id = 100 ,中间有一个新的事务执行了修改操作,对于可重复读,两次查询结果都是一样的,对于读已提交,两次结果就不一样了。

五、 MVCC


Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能

一句话讲,MVCC就是用 同一份数据临时保留多版本的方式 的方式,实现并发控制。

这里留意到 MVCC 关键的两个点:

  1. 在读写并发的过程中如何实现多版本;
  2. 在读写并发之后,如何实现旧版本的删除(毕竟很多时候只需要一份最新版的数据就够了);

1. 概念

  1. 系统版本号:每开启一个事务,系统版本号就会自增
  2. 事务版本号:事务创建时的系统版本号
  3. 创建版本号:创建一个数据行快照时的系统版本号
  4. 删除版本号:如果快照的删除版本号大于事务版本号,说明该快照有效;否则表示该快照已经被删除。
  5. innodb存储的最基本row中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT
  6. 6字节的DATA_TRX_ID 标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
  7. 7字节的DATA_ROLL_PTR 指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
  8. 6字节的DB_ROW_ID,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值.,这个用于索引当中
  9. DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候

2. Undo日志

MVCC使用的快照保存在日志中,该日志通过回滚指针把一个数据行的所有快照连接起来。

●  redo log

    MySQL在开启事务时,会将执行的SQL保存到指定的log文件,即redo log。当MySQL执行recovery时执行redo log里的SQL操作即可。redo log不会被立即写入磁盘,会先写入redo buffer;当客户端执行commit时,redo buffer的内容会视情况存入磁盘。

●  undo log

    与redo log相反,undo log是为了回滚事务而写的日志,具体内容就是copy事务开始前的数据(行)到undo buffer。

    与redo buffer一样,undo buffer也是环形缓冲,当缓冲满的时候buffer内容会被刷新到磁盘。

    与redo log不同的是,undo log没有独立的磁盘文件,所有的undo log均被存在主ibd数据文件中(表空间)。

3. 快照读和当前读

快照读:读取的是快照版本,也就是历史版本,MVCC读取的是快照中的数据,可以减少加锁带来的开销。

当前读:读取的是最新版本,读的是最新数据,需要加锁。

普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。

4. 实现过程

innodb MVCC主要是为Repeatable-Read事务隔离级别做的。在此隔离级别下,A、B客户端所示的数据相互隔离,互相更新不可见

了解innodb的行结构、Read-View的结构对于理解innodb mvcc的实现由重要意义具体的执行过程

begin->用排他锁锁定该行->记录redo log->记录undo log->修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行

上述过程确切地说是描述了UPDATE的事务过程,其实undo log分insert和update undo log,因为insert时,原始的数据并不存在,所以回滚时把insert undo log丢弃即可,而update undo log则必须遵守上述过程

下面分别以select、delete、 insert、 update语句来说明

关键:开启一个事务时,该事务版本号肯定大于当前所有数据行的创建版本号

SELECT

Innodb检查每行数据,确保他们符合两个标准:

1、InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行

2、行的删除操作的版本一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前,行没有被删除

符合了以上两点则返回查询结果。

INSERT

InnoDB为每个新增行记录当前系统版本号作为创建ID。

DELETE

InnoDB为每个删除行的记录当前系统版本号作为行的删除ID。

UPDATE

新插入一行(复制了要删除的记录),并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号

说明

insert操作时 “创建时间”=DB_ROW_ID,这时,“删除时间 ”是未定义的;

update时,复制新增行的“创建时间”=DB_ROW_ID,删除时间未定义,旧数据行“创建时间”不变,删除时间=该事务的DB_ROW_ID;

delete操作,相应数据行的“创建时间”不变,删除时间=该事务的DB_ROW_ID;

select操作对两者都不修改,只读相应的数据

5. 对于MVCC的总结

上述更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,这个可能与我们所理解的MVCC有较大的出入,一般我们认为MVCC有下面几个特点:

  • 每行数据都存在一个版本,每次数据更新时都更新该版本
  • 修改时Copy出当前版本随意修改,各个事务之间无干扰
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
  • 事务以排他锁的形式修改原始数据
  • 把修改前的数据存放于undo log,通过回滚指针与主数据关联
  • 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)

MVCC参考:MySQL事务隔离级别的实现原理 - 废物大师兄 - 博客园

  • 3
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值