事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。当事务被提交给了DBMS(数据库管理系统),则DBMS(数据库管理系统)需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
数据库事务正确事项的四个基本要素(事务的锁个属性):ACID
事务中包含的各项操作在一次执行过程中,只允许出现以下两种状态之一:要么全做,要么不做,没有中间状态。
对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
在事务开始和完成时,数据库的数据都保持一致的状态,即事务的执行使数据库从一种正确状态转到另一种正确状态
比如A向B转账100,首先A的余额减少100元,然后B的余额增加100元,这个是数据库从一种正确状态转到另外一种正确状态。不能A向B转账,A的余额减少100元,B的余额没有变化。这就不满足数据库事务的一致性
在并发环境中,一个事务的执行不能被其他事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。
一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的。
隔离性分4个级别,下面会介绍。
一旦事务提交,那么它对数据库中的数据的改变就是永久性的,并不会被回滚。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。
提交事务时断电,如何恢复,保证数据一致性
以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:
时间 | 取款事务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写,发生了提交覆盖。
一个事务读到了另一个未提交的事务写的数据。另一事务撤销事务。
时间 | 转账事务A | 事务B |
T1 | 开始事务 | |
T2 | 查询余额为1000 | |
T3 | 转账100,账户余额为1100 | |
T4 | 开始事务 | |
T5 | 查询余额为1100 | |
T6 | 撤销转账,余额为100 | |
T7 | 提交事务 |
一个事务中两次读同一行数据,可是这两次读到的数据不一样。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询余额为1000 | |
T4 | 查询余额为1000 | |
T5 | 取出1000,余额变为0 | |
T6 | 提交事务,余额为0 | |
T7 | 查询余额为0 | |
T8 | 提交事务 |
事务A其实除了查询两次以外,其它什么事情都没做,结果钱就从1000编程0了,这就是不可重复读的问题
幻读就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。
一个事务中两次查询,但第二次查询比第一次查询多了或少了几行或几列数据。
时间 | 新增事务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引擎的加锁机制,来理解,在事务 处理中数据库到底做了什么。
随着数据库隔离级别的提高,数据的并发能力也会有所下降。所以,如何在并发性和隔离性之间做一个很好的权衡就成了一个至关重要的问题。
因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
每行数据其实在数据库都是多个版本的,可能同一时间有很多事务在更新一条数据,事务在开始的时候会申请一个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 ,中间有一个新的事务执行了修改操作,对于可重复读,两次查询结果都是一样的,对于读已提交,两次结果就不一样了。
Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能
一句话讲,MVCC就是用 这里留意到 MVCC 关键的两个点:
|
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数据文件中(表空间)。
快照读:读取的是快照版本,也就是历史版本,MVCC读取的是快照中的数据,可以减少加锁带来的开销。
当前读:读取的是最新版本,读的是最新数据,需要加锁。
普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。
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有下面几个特点:
MVCC参考:MySQL事务隔离级别的实现原理 - 废物大师兄 - 博客园