数据库事务的理解

170 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

事务可以理解为一段代码块,这段代码块要不都执行,要不都不执行。事务需要满足四大特性,分别是原子性、一致性、隔离性和持久性。 原子性和持久性更侧重研究事务本身,MySQL通过undolog实现原子性,通过redolog实现持久性。 而隔离性更侧重不同事务之间的相互影响,类似于多线程产生并发问题,并发事务会产生脏读、幻读、不可重复读和脏写问题,涉及到事务隔离级别,InnoDB默认的RR隔离级别通过MVCC和锁机制来实现隔离性,由于锁机制,事务可能会出现死锁问题;而原子性、持久性和隔离性本质上都是为了保证数据库状态的一致性。这就是我能想起来的有关事务的所有知识点,您想问深入提问哪个一个?

数据库事务

定义

数据库的事务是指一组sql语句组成的数据库逻辑处理单元,在这组的sql操作中,要么全部执行成功,要么全部执行失败。说白了就是一段代码块,这段代码块要不都执行,要不都不执行。

举例:事务A进行转账100操作,那么转出的账户要扣钱,转入的账户要加钱,这两个操作必须同时执行成功,就把他们放到一个事务中,成功了,转入的账户多100,转出的账户少100,;万一失败了,就回滚,不执行这个操作,转入和转出的账户维持原本的数额不变。

分类

1、隐式定义

如果不显示定义事务的边界,SQL Server 会默认把每个单独的语句作为一个事务,即在执行完每个语句之后会自动提交事务

2、显示事务

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

事务四大特性

1、原子性

构成事务的所有操作要么全部成功,要么全部失败

原理 Undolog

undolog又叫回滚日志。在执行数据操作之前,首先将原始数据备份,这就是undo log。之后执行数据修正。 如果执行出现了错误,系统可利用undo log中的备份将数据恢复到事务开始之前的状态,保证事务原子性。

undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。

2、持久性

事务提交完成后,此事务对数据的更改操作会被持久化到数据库,不会被回滚。

原理 Redolog

采取WAL技术(write-Ahead logging)具体来说就是当一条记录需要更新的时候,InnoDB就会先把记录写到redolog中,并更新内存,这时候更新就算完成了。然后再系统比较空闲的时候把这个操作记录更新到磁盘中。

这样做主要能增加I/O速度

如果直接写入磁盘叫脏刷,涉及磁盘的随机I/O访问,涉及磁盘随机I/O访问是非常消耗时间的一个过程,相比之下先写入redo log,后面再找合适的时机批量刷新到磁盘中,是顺序I/O,能提升性能。并且藏刷以page为单位,也就是16KB,而redolog只包含真正需要写入的部分,无效IO大大减少。

redolog大小固定,由write pos为当前记录的位置,一只写一直后移;checkpoint为当前擦除的位置,也是循环后移,擦除记录前要把记录更新到数据文件。当write pos追上checkpoint的时候,就会停止更新,停下擦掉一些记录,然后checkpoint推进一下。

拓展:

redolog和binlog的区别

1)redolog是InnoDB引擎特有的;binglog是MySQL的server层实现的,所有引擎都有

2)redolog是物理日志,记录的是在某个数据页做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如给ID=2这行的c字段加1

3)redolog是循环写的,空间固定会用完,不能持久保存;binlog可以追加写入,具有归档功能,写到一定大小会切换到下一个,并不会覆盖以前的日志

两阶段提交

1)执行器调用存储引擎接口,存储引擎将修改更新到内存中后,将修改操作记录到redolog中,此时redolog处于prepare状态

2)存储引擎告知执行器执行完毕,执行器生成这个操作对应的binlog,并把binlog写入磁盘中

3)执行器调用存储引擎的提交事务接口,引擎把刚刚写入的redo log改成提交commit状态,更新完成。

为什么要两阶段提交?

主要是为了保证redolog和undolog日志逻辑一致。两阶段提交的时候,如果redolog写入成功,这时候crash,binlog写入失败,而redolog并没有commit,所以重启后事务会回滚,从而保证两个日志前后逻辑一致。

如果不用两阶段提交,要么先写redolog在写binlog,要不反一下

比方说update T set c=c+1 where ID =2;

初始c=0

1、先写redolog再写binlog的话,一旦第一个写完第二个没写期间发生crash,由于redolog的存在,会对数据进行恢复,恢复后为1,而binlog没有写入没有这个句子,如果要用binlog恢复临时库,这个临时库少一条更新,如果后面需要恢复到刚刚那个时刻的这一行的话,会是0,这和原库不一致。

2、先写binlog再写redolog的话,binlog写完之后crash,redolog还没洗,恢复后redolog因为没有相关记录,所以c=0,而binlog中有这条记录,恢复的时候c=1,而原库中c=0。

这里面的核心就是redolog记录的内容,即使异常重启了,也会刷新到磁盘中,而binlog记录的,主要用于备份。

crash-safe

InnoDB通过redo log保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。

3、隔离性

定义

与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。 隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

并发事务带来的问题

多个事务并发运行操作同一行数据,事务与事务之间会存在并发冲突,就好比多线程中中多个线程操作同一份数据存在线程间的并发冲突一样

并发事务可能会出现三种情况:读-读、读-写、写-写

1)读-读

因为读取记录并不会对记录造成任何影响,所以不同事务并发读取同一记录也就不存在任何安全问题,所以允许这种操作。

2)写-写

如果允许并发事务都读取同一记录,并相继基于旧值对这一记录做出修改,那么就会出现前一个事务所做的修改被后面事务的修改覆盖,即出现提交覆盖的问题。

另外一种情况,并发事务相继对同一记录做出修改,其中一个事务提交之后之后另一个事务发生回滚,这样就会出现已提交的修改因为回滚而丢失的问题,即回滚覆盖问题。

这两种问题都造成丢失更新,其中回滚覆盖称为第一类丢失更新问题,提交覆盖称为第二类丢失更新问题。

3)读-写

这种情况较为复杂,也最容易出现问题。

如果一个事务读取了另一个事务尚未提交的修改记录,那么就出现了脏读的问题;

如果我们加以控制使得一个事务只能读取其他已提交事务的修改的数据,那么这个事务在另一事物提交修改前后读取到的数据是不一样的,这就意味着发生了不可重复读

如果一个事务根据一些条件查询到一些记录,之后另一事物向表中插入了一些记录,原先的事务以相同条件再次查询时发现得到的结果跟第一次查询得到的结果不一致,这就意味着发生了幻读

会带来脏写、脏读、不可重复读和幻读问题

并发问题描述解决
脏读在读未提交场景下,事务B将数据1改成2,然后事务A读取到数据2并执行操作,但是操作完后事务B回滚了把数据从2改回了1,此时事务A操作的就是脏数据。脏读的本质是读写操作的冲突先写后读,读已提交可以解决脏读问题。
不可重复读在读提交和读未提交场景下,事务A第一次读到一个数据为1,事务B将数据改成2并提交了,事务A第二次读这个数据的时候变成2了,前后数据不一致。不可重复读的本质也是读写操作的冲突先读后写,可重复读可以解决不可重复读的问题
幻读在读提交和读未提交场景下,一个事务两次读取一定范围的数据记录,两次读取到的结果不同。幻读的本质也是读写操作的冲突先读后写,可重复读可以减少幻读的问题
脏写(更新丢失)在读未提交、读提交和可重复读场景下,事务A和事务B给数据100执行加100操作,A先提交,B后提交,最后显示数据为200,事务B覆盖了事务A。脏写的本质是写操作的冲突让每个事务按照串行的方式执行,使用串行化隔离级别

不可重复读和幻读的区别

1、不可重复读的侧重点在于更新和删除操作,幻读的侧重点在于插入操作

2、使用锁机制实现可重复读操作时,SQL 语句第一次读到数据后,会将相应的数据加锁,使得其他事务无法修改和删除数据,此时实现可重复读。但是这种操作无法对新插入的数据加锁,所以无法避免幻读问题

3、幻读无法通过行级锁来避免,需要通过串行化的事务隔离级别,但是这种隔离级别极大地降低了数据库的并发能力。

4、本质上,不可重复读和幻读最大的区别在于如何通过锁机制解决问题

对于以上提到的并发事务执行过程中可能出现的问题,其严重性也是不一样的,我们可以按照问题的严重程度排个序:

丢失更新 > 脏读 > 不可重复读 > 幻读

因此如果我们可以容忍一些严重程度较轻的问题,我们就能获取一些性能上的提升。于是便有了事务的四种隔离级别:\

隔离级别

隔离级别用于决定如何控制并发用户读写数据的操作。可分为读未提交、读提交、可重复读和串行化

修改隔离级别

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;//等级就是上面的几种

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): 事务的修改,即使没有提交,对其他事务也都是可见的。事务能够读取未提交的数据,这种情况称为脏读。
  • READ-COMMITTED(读取已提交): 事务读取已提交的数据,大多数数据库的默认隔离级别。当一个事务在执行过程中,数据被另外一个事务修改,造成本次事务前后读取的信息不一样,这种情况称为不可重复读。
  • REPEATABLE-READ(可重复读): 这个级别是MySQL的默认隔离级别,它解决了脏读的问题,同时也保证了同一个事务多次读取同样的记录是一致的,但这个级别还是会出现幻读的情况。幻读是指当一个事务A读取某一个范围的数据时,另一个事务B在这个范围插入行,A事务再次读取这个范围的数据时,会产生幻读
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

各种隔离级别和数据库异常情况对应情况如下:

隔离级别脏读不可重复 读幻读
READ- UNCOMMITTED读未提交
READ-COMMITTED读提交×
REPEATABLE- READ可重复读××
SERIALIZABLE串行化×××
  • 总结:MySQL 的 InnoDB 引擎,通过MVCC实现(一个事务)写-(另一事务)读并发控制,通过锁机制来实现(一个事务)写-(另一事务)写并发控制

MVCC和锁机制由于体量关系详见下一篇。

4、一致性

事务执行前后,数据始终处于一致状态。原子性、持久性和隔离性本质上都是为了保证数据库状态的一致性,某种意义上,数据一致性是数据库的终极目标。

如果聊得挺好的,你还可以最后总结补充一句:虽然说一致性、隔离性、原子性和持久性是事务的四大特性,但是各大数据库厂商的实现中,能真正满足ACID的事务少之又少,比如例如MySQL的NDB Cluster事务不满足持久性和隔离性;InnoDB默认事务隔离级别是可重复读,不满足隔离性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性…因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。而且在分布式事务时,数据一致性问题会更加复杂。

完结撒花!

参考文献

《凤凰架构》

《深入理解分布式事务》

《 软件架构设计 》

《深入浅出 MySQL》