【进阶之路】详解数据库事物与隔离级别

664 阅读11分钟

这段时间忙于上线和重构、写文章的是减少了很多,更新不得不变得迟缓起来~

一、事务的特性

事务是指作为单个逻辑工作单元执行的一系列操作,要么都执行成功,要么都执行失败。数据库事物有四种特征:即原子性、一致性、隔离性和持久性,也就是我们俗称的 ACID 特性。事务处理可以确保除只有本事务单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。事务是数据库运行中的一个逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。 事务主要用于复杂度高、重要且会出现并发的任务。比如银行扣款、生成订单、转账汇款、装备强化(大雾)等任务的的过程需要封装成一个事务。

  • 原子性(Atomicity) 原子性是指事务包含的所有操作,要么全部成功,要么全部失败,不会出现部分提交部分未提交的情况。

  • 一致性(Consistency) 一致性是指事务必须使数据库从一个一致性的状态变到另一个一致性的状态,也就是事务执行前和执行后,数据库内容都处于一致性状态

  • 隔离线(Isolation) 隔离线是指当有多个用户并发访问数据库时,数据库为每个用户打开一个事务,事务间不能互相影响,具体的隔离效果,需要根据事务的隔离级别来确定(Mysql中InnoDB 支持事务,MyISAM 不支持事务)。

  • 持久性(Durability) 持久性是指一个事务一旦提交,那么事务对数据修改的效果是永久性的,不会由于数据库故障而丢失

二、事物的实现原理

原子性、一致性、持久性通过数据库的redo log和undo log来完成。

redo log是InnoDB存储引擎层的日志,又称重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。在一条更新语句进行执行的时候,InnoDB引擎会把更新记录写到redo log日志中,然后更新内存,此时算是语句执行完了,然后在空闲的时候或者是按照设定的更新策略将redo log中的内容更新到磁盘中

undo log一般是逻辑日志,根据每行记录进行记录,主要存储的是数据的逻辑变化日志,比如说我们要insert一条数据,那么undo log就会生成一条对应的delete日志。简单点说,undo log记录的是数据修改之前的数据,因为需要支持回滚。

那么当需要回滚时,只需要利用undo log的日志就可以恢复到修改前的数据。

undo log另一个作用是实现多版本控制(MVCC),undo记录中包含了记录更改前的镜像,如果更改数据的事务未提交,对于隔离级别大于等于read commit的事务而言,不应该返回更改后数据,而应该返回老版本的数据。

处理流程大概如下:

image.png

bin log是存储所有数据变更的情况,理论上只要记录在bin log上的数据,都可以恢复。而redo log不会存储历史所有的数据的变更,当内存数据刷新到磁盘中,redo log的数据就失效

事务的隔离性通过锁机制来实现

三、数据库的隔离级别

在我们实际的工作中,数据库中的数据经常被多个用户共同访问的,在多个用户同时操作相同的数据时,可能就会出现一些事务的并发问题:丢失更新、不可重复读、脏读和幻读。

  1. 读未提交(Read Uncommitted):一个事务在执行过程中,既可以访问其他事务未提交的新插入的数据,又可以访问未提交的修改数据。如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据。此隔离级别可防止丢失更新。

  2. 读已提交(Read Committed):一个事务在执行过程中,既可以访问其他事务成功提交的新插入的数据,又可以访问成功修改的数据。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。此隔离级别可有效防止脏读。

脏读是指事务T1读取了事务T2未提交的数据,事务T2回滚后导致T1读到的数据是脏数据库。脏读的粒度是表行。解决脏读的方法是修改隔离级别为“未读提交”以上。

  1. 可重复读(Repeatable Read):一个事务在执行过程中,可以访问其他事务成功提交的新插入的数据,但不可以访问成功修改的数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。此隔离级别可有效防止不可重复读和脏读。这是Mysql数据库的默认隔离级别。

不可重复读是指事务T1多次查询同一条记录,返回了不同的数据,这是由于在两次查询间隔过程中,数据库记录被事务T2修改了导致的。不可重复读的粒度是表行。

  1. 串行化(Serializable):事务执行的时候不允许别的事务并发执行,而是完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。这是事务隔离的最高级别,虽然最安全,但是效率太低,一般不会用。

幻读是指事务T1读取了表中的一类数据进行操作时,在操作过程中,事务T2修改(新增、修改或删除)了批量中的某条记录,导致T1发现数据不一致。不可重复读和幻读的主要区别是:不可重复读侧重于数据被修改,幻读侧重于数据被新增或删除。

具体情况如下表格:

脏读不可重复读幻读
读未提交
读已提交×
可重复读××
串行化×××

3.1 间隙锁(Gap Lock)

间隙锁是Innodb在可重复度的级别中为了解决幻读问题时引入的锁机制。幻读的问题存在是因为新增或者更新操作,这时如果进行范围查询的时候(加锁查询),会出现不一致的问题,这时使用不同的行锁已经没有办法满足要求,需要对一定范围内的数据进行加锁,间隙锁就是解决这类问题的。在可重复读隔离级别下,数据库是通过行锁和间隙锁共同组成的(next-key lock),来实现的。

间隙锁是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。

间隙锁产生的条件:

  • 1.使用普通索引锁定;
  • 2.使用多列唯一索引;
  • 3.使用唯一索引锁定多行记录。

间隙锁特性:

  • 1.加锁的基本单位是(next-key lock),他是左开右闭原则。
  • 2.插叙过程中访问的对象会增加锁。
  • 3.索引上的等值查询--给唯一索引加锁的时候,next-key lock升级为行锁。
  • 4.索引上的等值查询--向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。
  • 5.唯一索引上的范围查询会访问到不满足条件的第一个值为止。

四、MVCC多版本并发控制

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

MVCC的使用特点:

  • 应对高并发事务, MVCC比单纯的加锁更高效。
  • MVCC只在读已提交(Read Committed) 和可重复读(Repeatable Read) 两个隔离级别下工作。
  • MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现。

在并发操作数据库的过程中,读操作可能会读取到不一致的数据(脏读),为了避免这种情况,就要对数据库的并发访问进行控制,比如加锁处理(如 for upudate,这些都被称为当前读,对应之后的快照读)。但是,加锁会导致读写操作变为串行化,导致读操作会被写操作阻塞,大幅降低读性能。

在java的concurrent包中,有copyonwrite系列的类,它的作用就是用于优化读操作远大于写操作的情况。在进行写操作时,将会将数据copy一份,不会影响原有数据,然后进行修改,修改完成后原子替换掉旧的数据,而读操作只会读取原有数据。通过这种方式实现写操作不会阻塞读操作,从而优化读效率。

MVCC的原理与copyonwrite类似。在MVCC协议下,每个读操作会看到一个一致性的快照(snapshot),并且可以实现非阻塞的读。MVCC允许数据具有多个版本,这个版本可以是时间戳或者是全局递增的事务ID,在同一个时间点,不同的事务看到的数据是不同的。

4.1 MVCC的实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。

数据库的隐式字段:

  • DB_TRX_ID(当前事务ID):记录创建这条记录/最后一次修改该记录的事务ID。
  • DB_ROLL_PTR(回滚指针):指向这条记录的上一个版本,回滚指针指向写入回滚段的undo log记录,读取记录的时候会根据指针去读取undo log中的记录。

因为MySQL中undo log中会维护一个历史数据记录,所以我们应该养成定期提交事务的习惯,否则回滚段会越来越大,甚至占满了表空间。

  • DB_ROW_ID (隐藏主键):如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。

Read View是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR(回滚指针)去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。

那么MVCC机制到底如何查询的呢?

根据网上的资料,可以得出这样的结论:

  • 1、查询DB_TRX_ID小于等于当前事务ID的数据。(这里要等于是因为假如自己的事务插入了一条数据,会生成一条当前事务ID的数据,所以必须包含本事务自己插入的数据)
  • 2、查询未删除(回滚指针为空)或者回滚指针大于当前事务ID的数据。(这里不能等于是因为假如自己的事务删除了一条数据,会生成数据的回滚指针为当前事务ID,所以必须排除掉自己删除的数据)

大家好,我是练习java两年半时间的南橘,下面是我的微信,需要之前的导图或者想互相交流经验的小伙伴可以一起互相交流哦。

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:cloud.tencent.com/developer/s…