数据库原理

该文档来源于https://github.com/CyC2018/CS-Notes/blob/master/notes/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B3%BB%E7%BB%9F%E5%8E%9F%E7%90%86.md#%E4%BA%94%E5%A4%9A%E7%89%88%E6%9C%AC%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6

添加了一部分自身的理解,以及 MVCC无法自洽部分的内容补充

事务

概念

事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。

ACID

Consistency 一致性

概念

数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。

作用

一致性是事务的基石,只有满足一致性,事务的执行结果才是正确的.

可以说AI特性目的都是为了在某种情况下满足一致性的需求而存在的

Atomicity 原子性

概念

事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。

回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

作用

在串行系统下,事务只需要满足原子性就一定满足一致性.

Isolation 隔离性

概念

一个事务所做的修改在最终提交以前,对其它事务是不可见的。

作用

在串行系统下隔离性是一定满足的,而在并发系统下,数据库采用多分支数据的方式来处理并发问题.

在并发系统下,多个事务并行,事务需要在满足原子性的情况下,还需要满足隔离性才能满足一致性

Durability 持久性

概念

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

一般使用redo日志来保证持久性

作用

为了应对系统崩溃情况

一致性问题

一致性问题实际上就是系统在并行环境下的数据损坏问题,按照不同的损坏方式,分为以下几种问题

丢失修改

线程B覆盖了线程A的修改

读脏数据

线程B读取到线程A中操作到一半的数据.

在数据库中体现为事务A读取到事务B中修改了的数据,但事务A实际上将会撤销此次修改.

着重点在于读取到了操作到一半的数据

不可重复读

实际上问题仍然是线程B读取到线程A中操作到一半的数据.

然而这个问题针对的方向是读取的内容,而非操作的结果.

与读脏数据不同的是

不可重复读着重点在于无法读取到相同的数据,也就是至少存在两次数据读取.而读脏数据则只需要一次就可以.

幻影读

实际上和不可重复读问题类似

只不过幻影读针对的是一个范围内的数据,并且着重点在于范围中的数据增/删,而不可重复的读关心的是同一条数据的修改

封锁

封锁实际上是为了满足隔离性的一种实现方式,实际上原理仍然是限制访问

封锁粒度

MySQL 中提供了两种封锁粒度:行级锁以及表级锁。

应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。

但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。

在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。

封锁类型

读写锁

  • 写锁,简写为X,又称排他锁(Exclusive)
  • 读锁,简写为S,又称为共享锁(Shared)

有以下两种规则

  • 事务A对对象a添加了X锁后,可以对a进行任意操作.其他事务不能对A加任何锁
  • 事务A对对象a添加S锁后,仅可以进行读取操作,其他事务可以同时添加S锁,但不能添加X锁

意向锁

在存在行锁和表锁的情况下,若需要添加表锁,则需要检查所有行是否加锁,非常耗时.

引入意向锁(Intention Locks)可以更容易的支持多粒度封锁.

意向锁指的是表级意向锁

新增规则如下

  • 事务在添加对象的S锁之前,必须先添加IS锁或者更强的锁
  • 事务在添加对象的X锁之前,必须先添加IX锁

通过意向锁,在对表进行加锁时,仅需先检测表A是否存在X/IS/S/IS锁.

锁的兼容关系如下

-XIXSIS
X××××
IX××
S××
IS×

可以看到意向锁之间是全部兼容的.

这里的 X/IX/S/IS 锁都是表级锁,IX 锁和行级的 X 锁兼容,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。)

意向锁仅是一个标记,虽然由于读写而存在了权限不同,但其仅是为了减少锁检测代价而存在的一个标记.

封锁协议

三级封锁协议

实际上封锁协议就是使用锁的规则

一级封锁

方式

事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。

作用

可以处理丢失修改的问题,因为保证了仅有一个线程在进行数据的修改

二级封锁

方式

在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。

作用

可以避免读脏数据问题

因为数据如果正在修改,是不能加S锁的,也就是正在修改的数据不能被其他事务读取.

三级封锁

方式

在二级的基础上,读取完不释放锁,事务结束后释放锁.

作用

因为添加了S锁,其他事务无法进行修改,所以可以避免不可重复读问题

注意

为何没有更高的封锁协议来处理幻影读问题,先看幻影读问题的由来

在一个范围中新增/删除的数据,导致了读取范围的不一致.

这里的封锁协议是针对的数据锁,然而幻影读的问题由来是数据的增/删.

因此若需要用封锁协议来处理幻影读问题,那四级封锁协议可能像下面所述

“四级封锁”

方式

在三级的基础上,对数据所在的位置进行添加S锁,事务结束时释放S锁

在数据添加时,必须在其所需要添加的位置上添加X锁.添加后释放X锁.

作用

因为在位置上进行了锁定,所以无法在事务读取一个范围时,在这个范围中添加X锁.

两段锁协议

概念

加锁和解锁分为两个阶段进行

作用

保证可串行化调度

可串行化调度

概念

通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。

注意

两段锁协议可串行化调度的充分不必要条件

例如以下操作不满足两段锁协议,但是它还是可串行化调度。

lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)

隐式和显式锁定

MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。

InnoDB 也可以使用特定的语句进行显示锁定:

SELECT ... LOCK In SHARE MODE;
SELECT ... FOR UPDATE;

隔离级别

隔离级别实际上就对应了并发一致性问题的处理程度,不过写丢失问题是事务必须解决的问题,因此即使最低的隔离级别也处理了丢失修改问题

READ UNCOMMITTED 未提交读

事务中的修改,即使没有提交,对其它事务也是可见的。

也就是说仅处理了丢失修改问题

READ COMMITTED 提交读

一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。

处理了脏读问题

如果一个条件不能通过索引快速过滤,存储引擎就会将所有的记录加锁后返回,由 Server 层进行过滤,在实际使用中,mysql 做出了优化,在 Server 层若发现不满足条件,则会提前解锁,其不满足两段锁协议。这个问题同样会出现在 rr 级别中

REPEATABLE READ 可重复读

保证在同一个事务中多次读取同样数据的结果是一样的。

处理了不可重复读问题

在 mysql 中使用 mvcc 和 next-key locks解决了幻读的问题。

SERIALIZABLE 可串行化

强制事务串行执行。

即消除并发带来的隔离性问题

下面是各隔离级别下的问题情况

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

多版本并发控制

概念

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现RCRR这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

数据封锁不一定需要使用锁来完成,mysql 就使用了版本号来实现。

版本号

  • 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号:事务开始时的系统版本号

隐藏的列

mvcc 在每行记录后都保存着两个隐藏的列,用于存储两个版本号

  • 创建版本号:指示创建一个数据行的快照时的版本号
  • 删除版本号:如果快照的删除版本未定义或删除版本号大于当前事务版本号表示该快照有效

Undo 日志

MVCC 使用到的快照存储在 Undo 日志中,日志通过回滚指针把一个数据行的所有快照链接起来

实现过程

以下实现过程仅针对可重复读隔离级别。

开始一个事务时,事务的版本号肯定大于当前所有数据快照的创建版本。数据行快照的创建版本号是创建数据行快照时的系统版本号,系统版本号随着创建事务而递增,因此新建一个事务时,这个事务的系统版本号比之前的系统版本号都大,也就是比所有数据行快照的创建版本号都大。

这里指的读都是指的快照读

SELECT

row.createVersion<=t.version

row.deleteVersion==undefined || row.deleteVersion>t.version

INSERT

将当前事务版本号作为数据行快照的创建版本号。

DELETE

将当前事务版本号作为数据行快照的删除版本号。

UPDATE

将当前事务版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。

因为旧行的删除版本号t,所以旧行不会被查询出,而新行的创建版本号t,所以会被查询出。

上述的原理描述不能解决以下的问题

问题 1

后开启的事务无法读取到先开启的事务的插入记录,即

  1. 事务 A 开启事务,获得版本号 1
  2. 事务 B 开启事务,获得版本号 2
  3. 事务 A 插入记录 A,创建版本号 1
  4. 事务 B 无法查询到版本号 1 的记录

问题 2

先开启的事务可以读取到后开启的事务的插入提交数据,即

  1. 事务 A 开启事务,获得版本号 1
  2. 事务 B 开启事务,获得版本号 2
  3. 事务 B 插入记录 B,创建版本号 2
  4. 事务 A 查询,获得记录 B

用例 2 还需要进行校验一下

问题 3

  1. 事务 A 开启事务,获得版本号 1
  2. 事务 B 开启事务,获得版本号 2
  3. 事务 B 进行一次查询
  4. 事务 A 进行数据a插入,并提交
  5. 事务 B 进行一次查询,无法查询到数据 a

查询事务 ID

SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();

其他控制逻辑

  1. 事务ID并非在事务begin时就分配,而是在事务首次执行非快照读操作(SELECT … FOR UPDATE/IN SHARE MODE、UPDATE、DELETE)时分配。
注:
如果事务中只有快照读,InnoDB对只有快照读事务有特殊优化,这类事务不会拥有事务ID,因为它们不会在系统中留下任何修改(甚至连锁都不会建),所以也没有留下事务ID的机会。
虽然使用SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
查询此类事务ID时,会输出一个很大的事务ID(比如328855902652352),不过这只是MySQL在输出时临时随机分配的一个用于显示的ID而已。
  1. 每个事务首次执行快照读操作时,会创建一个read_view对象(可以理解为在当前事务中,为数据表建立了一个逻辑快照,read_view对象就是用来控制此逻辑快照的可见范围的)。事务提交后,其创建的read_view对象将被销毁。
read_view对象中有三个关键字段用于判断记录的可见范围。它们分别是trx_ids、low_limit_id、up_limit_id。
1. read_view->trx_ids:创建该read_view时,记录正活跃的其他事务的ID集合。事务ID在集合中降序排列,便于二分查找。
2. read_view->low_limit_id:当前活跃事务中的最大事务ID+1(即系统中最近一个尚未分配出去的事务号)。
3. read_view->up_limit_id:当前活跃事务中的最小事务ID。
  1. 如果记录的版本号比自己事务的read_view->up_limit_id小,则该记录的当前版本一定可见。因为这些版本的内容形成于快照创建之前,且它们的事务也肯定已经commit了。或者如果记录的版本号等于自己事务的事务ID,则该记录的当前版本也一定可见,因为该记录版本就是本事务产生的。

  2. 如果记录的版本号与自己事务的read_view->low_limit_id一样或比它更大,则该版本的记录的当前版本一定不可见。因为这些版本的内容形成于快照创建之后。

不可见有如下两层含义:
1. 如果该记录是新增或修改后形成的新版本记录,则对新增和修改行为不可见,即看不到最新的内容;
2. 如果该记录是标记为已删除形成的新版本记录,则对该删除行为不可见,即可以看到删除前的内容。
  1. 当无法通过4和5快速判断出记录的可见性时,则查找该记录的版本号是否在自己事务的read_view->trx_ids列表中,如果在则该记录的当前版本不可见,否则该记录的当前版本可见。

  2. 当一条记录判断出其当前版本不可见时,通过记录的DB_ROLL_PTR(undo段指针),尝试去当前记录的undo段中提取记录的上一个版本进行4~6中同样的可见性判断,如果可以则该记录的上一个版本可见。

快照读和当前读

快照读

使用 MVCC读取的是快照中的数据,这样可以减少加锁带来的开销。

select * from table...;

当前读

读取的是最新的数据,需要加锁。

select * from table where ? lock in share mode;--S锁
select * from table where ? for update;--X 锁
insert;
update;
delete;

mvcc 无法解决当前读下的幻影读问题

Next-Key Locks

mvcc 仍然无法解决当前读下的幻影读问题,next-key locks 就是为了解决这个问题而存在的。在 RR 级别下,使用 MVCC+Next-Key Locks 可以解决幻影读问题。

Record Locks

锁定记录上的索引,而不是记录本身。

如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 仍然可以使用。

Gap Locks

间隙锁,锁定索引之间的间隙,但是不包含索引本身。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙,是一个前开后闭区间。

假如当前聚簇索引中包含(1,3,5,8,11),其记录的 GAP 区间如下

(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)

需要注意的是 InnoDB 索引还会对辅助索引的下一个键值加上 gap lock。

InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围。但是,当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。

注意:通过主键或则唯一索引来锁定不存在的值,也会产生GAP锁定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值