面试官:说一下数据库事务的实现原理

yumo6663个月前 (04-05)技术文章20

#数据库事务的实现原理是什么样的?#

  1. 什么是数据库事务?
  2. 数据库事务的四大特性?
  3. 数据库事务隔离级别?
  4. 数据库事务的实现原理?

一、什么是数据库事务?

数据库事务是指作为一个不可分割的操作单元执行的一系列数据库操作。一个事务可以包含一个或多个数据库操作,例如插入、更新或删除数据。在分布式系统和并发编程中,事务可以确保数据库操作的完整性和可靠性,并避免数据损坏和不一致的情况。

@Transactional
public void doService() {
	// 增加一条数据
	addUser();
	// 更新一条数据
	updateUser();
	// 删除一条数据
	deleteUser();
}

事务里可能是一个或者多个增删改查的SQL语句。要么一起成功就提交了,只要有一个失败,那么事务就回滚了,所有的修改都撤销了!

二、数据库事务四大特性

事务通常具有以下特征:

  1. 原子性(Atomicity):事务作为一个整体要么完全执行,要么完全不执行,不允许部分执行。如果在事务执行过程中发生错误,系统会进行回滚操作,将数据恢复到事务开始之前的状态。
  2. 一致性(Consistency):事务的执行不会破坏系统的完整性约束和规则,保证系统处于一致的状态。事务在执行前后,数据的完整性和约束条件应保持一致。
  3. 隔离性(Isolation):并发执行的事务之间应该相互隔离,不会互相干扰。每个事务应该感知不到其他事务的存在,以确保数据的一致性和正确性。
  4. 持久性(Durability):一旦事务提交,其结果应该永久保存,即使在系统故障或重启后仍然有效。系统应该通过持久化机制,如日志记录,来确保事务的持久性。

三、数据库事务隔离级别

常见的数据库隔离级别包括以下几种:

  1. 读未提交(Read Uncommitted):最低级别的隔离级别,事务可以读取其他事务尚未提交的数据。这种隔离级别容易导致脏读(Dirty Read)问题,即读取到其他事务未提交的数据。
  2. 读已提交(Read Committed):在这个隔离级别下,事务只能读取已经提交的数据。这种隔离级别可以避免脏读问题,但可能会导致不可重复读(Non-repeatable Read)问题,即同一个事务内多次读取同一数据时,可能会得到不同的结果。
  3. 可重复读(Repeatable Read):在可重复读隔离级别下,事务读取数据时会创建一个一致的快照,事务开始后其他事务对数据的修改不可见。这种隔离级别可以避免不可重复读问题,但可能会导致幻读(Phantom Read)问题,即同一个查询在事务中多次执行时,结果集的行数可能不一样。
  4. 串行化(Serializable):串行化隔离级别是最高级别的隔离级别,它通过对事务进行串行执行来避免并发访问问题。每个事务按顺序依次执行,不会出现并发冲突的情况。这种隔离级别可以避免脏读、不可重复读和幻读问题,但可能会导致性能下降,因为事务需要顺序执行。

举例:假设有两个用户,Alice和Bob,他们同时操作一个银行账户的转账功能。现在我们来看看不同的事务隔离级别对转账过程中的并发访问会有什么影响。

假设Alice的账户有1000元,Bob的账户有500元。他们同时执行以下转账操作:

  1. Alice从她的账户中转出200元给Bob。
  2. Bob从他的账户中转出300元给Alice。

不同的事务级别所产生的不同结果:

  1. 读未提交(Read Uncommitted):Alice开始转账后,Bob可以读取到她的未提交的转账记录。如果Bob同时执行转账操作,他可能会读取到Alice未提交的扣款记录,导致Bob将金额从错误的余额中进行扣除。
  2. 读已提交(Read Committed):Alice开始转账后,Bob只能读取到已提交的转账记录。这意味着Bob只能在Alice转账完成后,才能获取到她的扣款记录并执行自己的转账操作。这样可以避免脏读问题。
  3. 可重复读(Repeatable Read):Alice开始转账后,Bob无论执行多少次查询,只能读取到他开始执行转账时的账户余额。即使Alice完成转账操作后,Bob再次查询,他也只能看到自己开始时的余额。这可以避免不可重复读问题。
  4. 串行化(Serializable):Alice和Bob的转账操作会按照顺序进行执行,无论谁先谁后。这意味着他们之间不会发生并发冲突,保证了数据的一致性。但是串行化会限制并发性能,因为他们无法并行执行转账操作。

其实脏写、脏读、不可重复读、幻读这些问题的本质,都是数据库的多事务并发问题,那么为了解决多事务并发问题,数据库才设计了事务隔离级别、MVCC多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题。

默认数据库事务级别是可重复读,也就是说脏写、脏读、不可重复读、幻读都不会发生,每个事务执行的时候,跟别的事务没关系,不管别的事务怎么更新和插入,我查到的值都是不变的,是一致的!

四、数据库事务实现原理

这到底是怎么做到的呢?这就得了解数据库的的MVCC多版本并发控制机制,了解MVCCMVCC机制之前,先了解undo log版本链这个机制,这样才能更好的理解MVCC机制。

undo log 版本链

简单来说呢,我们每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向你这个事务更新数据之前生成的undo log。

举个例子,现在假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示,插入的这条数据的值是值A,因为事务A的id是50,所以这条数据的trx_id 就是50,roll_pointer 指向一个空的undo log,因为之前这条数据是没有的。

接着假设有一个事务B跑来修改了一下这条数据,把值改成了B值,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示:

事务B修改了值为值B,此时表里的那行数据的值就是值B了,那行数据的trx_id就是事务B的id,也就是58,roll_pointer指向了undo log,这个undo log就记录你更新之前的那条数据的值,在这里就记录了值A。

假设事务C又来修改了一下这个值为值C,它的事务id是69,此时会把数据行里的trx_id改成69,然后生成一条undo log,记录之前事务B修改的那个值,此时如下图所示:

在上图可以清晰看到,数据行里的值变成了值C,trx_id是事务C的id,也就是69,然后roll_pointer 指向了本次修改之前生成的undo log,也就是记录了事务B修改的那个值,包括事务B的id,同时事务B修改的那个undo log还串联了最早事务A插入的那个undo log。

不管多个事务并发执行时如何执行的,先搞清楚就是多个事务串行执行的时候,每个修改了一行数据都会更新隐藏字段trx_id和roll_pointer,同时之前多个数据快照对应的undo log,会通过roll_pointer指针串联起来,形成一个重要的版本链!

ReadView机制

基于undo log多版本链条 实现的 ReadView机制,简单来说,就是执行一个事务的时候,就会生成一个ReadView,比较关键的有4点:

  1. m_ids:这个就是说此时有哪些事务在MySQL里执行还没提交的;
  2. min_trx_id:就是m_ids里最小的值;
  3. max_trx_id:就是说MySQL下一个要生成的事务id,就是最大事务id;
  4. creator_trx_id:就是这个事务的id;

假设原来数据库里就有一行数据,很早以前就有事务插入过了,事务id是32,它的值就是初始值,如下图所示:

此时两个事务并发过来执行了,一个是事务A(id=45),一个是事务B(id=59),事务B是要去更新这行数据的,事务A是要去读取这行数据的值的,此时两个事务如下图所示:


事务A直接开启一个ReadView,这个ReadView里的m_ids就包含了事务A和事务B的两个id,45和59,然后min_trx_id就是45,max_trx_id就是60,creator_trx_id就是45,是事务A自己。

这个时候事务A第一次查询这行数据,会走一个判断,判断一下当前这行数据的trx_id是否小于ReadView中的min_trx_id,如果发现trx_id=32,是小于ReadView里的min_trx_id的45的,说明事务开启之前,修改这行数据的事务早就提交了,所以此时可以查到这行数据,如下图所示:

事务B开始执行,把这行数据的值修改为了值B,然后这行数据的trx_id设置为自己的id,也就是59,同时roll_pointer指向了修改之前生成的undo log,接着这个事务B就提交了,如下图所示:

此时事务A再次查询,此时数据行里的trx_id=59,那么这个trx_id是大于ReadView里的min_trx_id(45),同时小于ReadView里的max_trx_id(60)的,说明更新这条数据的事务,很可能就跟自己差不多同时开启,于是就会看一下这个trx_id=59,是否在m_ids列表里?如在ReadView的m_ids列表里,有45和59两个事务id,直接证实了,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以对这行数据是不能查询的!

此时查不到数据,就会顺着这条数据的roll_pointer顺着undo log日志链条往下找,就会找到最近的一条undo log,trx_id是32,此时发现trx_id=32是小于ReadView里的min_trx_id(45)的,说明这个undo log版本必然是在事务A开启之前就执行且提交的。那么就查询最近的那个undo log里的值好了,这就是undo log多版本链条的作用,它可以保存一个快照链条,让你可以读到之前的快照值,如下图:

多个事务并发执行的时候,事务B更新的值,通过这套ReadView + undo log版本链的机制,就可以保证事务A不会读到并发执行的事务B更新的值,只会读到之前更早的值。通过这套机制就可以实现多个事务并发执行时候的数据隔离。

相关文章

数据库加密技术原理与实践

1、数据库加密概述数据库加密是指对存储在数据库中的敏感数据进行编码处理的过程,目的是防止未经授权的访问和数据泄露。加密后的数据即使被未授权的第三方获取,也无法理解其原始含义,从而保护数据的机密性数据库...

【数据库原理】(7)关系数据库的完整性约束

关系模型的完整性规则是为了确保数据的唯一性和数据之间的关系的准确性。有三类完整性约束:实体完整性、参照完整性和用户定义完整性。其中实体完整性和参照完整性是必须满足的完整性约束条件,应该由关系系统自动支...

数据库系统原理:模式的定义与删除

SQL命令包括数据定义、查询、操纵和控制四大类,其中SQL的数据定义用于创建数据库中的各种数据对象。SQL的数据定义包括对SQL数据库、模式、基本表、视图和索引的创建和撤销操作。学习用SOL语言定义数...