数据库基本原理

本文详细解析了ACID原则在事务中的作用,包括原子性、一致性、隔离性和持久性。探讨了并发情况下可能出现的丢失修改、脏读、不可重复读和幻读问题,以及如何通过锁机制(如行级锁、读写锁和意向锁)和封锁协议(如一级、二级和三级)来解决。此外,讲解了InnoDB的MVCC在实现隔离级别中的关键角色,尤其是如何配合next-keylocks避免幻读。
摘要由CSDN通过智能技术生成

一、ACID原则

事务需要满足ACID原则。

A–原子性

事务要么就成功提交,要么就全部失败,然后回滚。

例如,银行扣款200,微信增加200,只进行扣款是不行滴。

C–一致性

数据库在事务执行前后都保持一致的状态。只有一致性得到保证,数据库才是正确的。

I–隔离性

在一个事务完成以前,它对于其他事务是不可见的。

比如A事务银行扣款了200元,提交事务之前其他事务无法知道。

D–持久性

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

例如:

操作前A:800,B:200
操作后A:600,B:400
如果在操作前(事务还没有提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:800,B:200
如果在操作后(事务已经提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:600,B:400

二、并发一致性问题

在并发情况下,数据库会出现种种问题

1、丢失修改

两个事务对同一个数据进行修改,那么其中一个事务修改的结果就会被替换,从而造成丢失。

就比如说上面的图中,-1张这个修改就被丢失了,从而导致了错误的结果(正确应该是15,13)

2、脏读

指一个事务读取了另外一个事务未提交的数据。注意,主体是外部的另一个事务,而不是正在进行的这个未提交的事务。

3、不可重复读

在一个事务内读取某一数据,多次读取结果不同。主体是没有提交的这个事务

4、幻读

幻读本质上也属于不可重复读的情况

是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。(一般是行影响,多了一行)

突然多了个东西,让人感觉出现了幻觉

三、锁

1、粒度

“行级锁”:行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。

“表级锁”:表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁。

“页级锁”:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。

一般来说,粒度越小,发生锁争用的可能就越小,并发程度就越高;但同时,系统开销就越大

2、读写锁
  • 独占锁—X锁,写锁

    对象A只能被一个事务获取,获得X锁的事务即能读取又能修改数据。

    其它事务不能对 A 加任何锁

  • 共享锁–S锁,读锁

    对象A可以被多个事务读,但是谁也不能修改它。

    其它事务能对 A 加 S 锁,但是不能加 X 锁。

3、意向锁

在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。

因此,在原来的 X/S 锁之上引入了意向锁 IX/IS,IX/IS 都是表锁,用来表示一个事务有意向在表中的某个数据行上加 X 锁或 S 锁

规定:

  • 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
  • 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。

它们之间的兼容性矩阵是酱紫的:

  • 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁
  • IX和X的关系等同于X和X之间的关系,为什么呢?因为事务获得了IX锁,接下来就有权利获取X锁,这样就会出现两个事务都获取X锁的情况,这和我们已知的X锁和X锁之间互斥是矛盾的。S和IS、X和IS、IX和IS也可以由此推导出来
  • 注意,这里兼容关系针对的是表级锁

有趣的问题:

IX 与 X冲突,那岂不是任意两个写操作,即使写不同行也会造成死锁

  1. Session A 请求 IX–成功,Session B请求 IX–成功
  2. Session A 请求 X,发现已经有其他session有IX,因此冲突
  3. 同理SessionB请求X也会是这种情况。那这row lock还有什么用?

答案:

  • IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突
  • 行级别的X和S按照普通的共享、排他规则即可。所以之前的示例中第2步不会冲突,只要写操作不是同一行,就不会发生冲突。
4、封锁协议
  • 一级封锁协议

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

    可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。

    但是不能解决脏读问题等。一个事务正在修改着,另一个进来读,照样脏锁

  • 二级封锁协议

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

    可以解决脏读问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,这在存在 S 锁的情况下是不允许的

    但是不能解决不可重复读问题等。一个事务读完了释放S锁,然后另一个事务修改了值,那再读就会出现不可重复读。

  • 三级封锁协议

    在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。

    可以解决不可重复读的问题,因为读 A 时直到事务结束,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。

  • 两段锁协议(2PL)

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

    1.所有的读写操作之前均需加锁;

    2.解锁操作后不允许再出现加锁操作;

    可以证明,若并发执行的所有事务均遵守两段锁协议,则对这些事务的任何并发调度策略都是可串行化的。

四、隔离级别

1、未提交读

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

不能防脏读。

2、提交读

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

可防脏读,不能防不可重复读。

3、可重复读

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

可防不可重复读,不可防幻读。

4、可串行化

强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。

该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。

可防幻读。

五、多版本并发控制(MVCC)

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。

主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

MVCC没有用锁,除非在select语句中强制加锁

而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。

可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

1、当前读,快照读与MVCC
  • 当前读:读取的是记录的最新版本,会对读取的记录进行加锁

    当前读包含:update、insert、delete情况,

    以及select * from table where ? lock in share mode; (加共享锁)

    和select * from table where ? for update; (加排它锁)这两种特殊情况

  • 快照读:读到的并不一定是数据的最新版本,有可能是之前的历史版本,不加锁

    快照读包含:select的一般情况

MVCC的理念是维持一个数据的多个版本,使得读写操作没有冲突。快照读就是MySQL为我们实现MVCC的理想模型。MVCC模型在MySQL中的具体实现则是由 3个隐式字段undo日志Read View 等去完成的。

2、MVCC的实现原理
1、隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID
    最近修改事务ID

  • DB_ROLL_PTR
    这条记录的上一个版本

  • DB_ROW_ID
    隐含的自增ID(隐藏主键)

    实际还有一个删除flag隐藏字段, 即记录被更新或删除并不代表真的删除,而是删除flag变了

2、undo日志

undo log是一条记录版本线性表,即链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录

undo log的节点可能会被purge线程清除掉。

purge线程:

为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。

3、Read View

Read View主要是用来做可见性判断的。

当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据

  • trx_list
    一个列表,维护Read View生成时刻系统正活跃的事务ID
  • up_limit_id
    记录trx_list列表中最小的事务ID(最早的)
  • low_limit_id
    ReadView生成时系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(最近的)

然后,用该条记录的DB_TRX_ID和这些值做比较

  1. 首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断

  2. 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断

  3. 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的

我想吐槽一下up和low的命名,对应最小和最大,让人感觉弄反了,但源码里就是这么写的

3、RC和RR级别下的快照读

RC:提交读

RR:可重复读

InnoDB默认的隔离级别是RR(可重复读)

但是呢,InnoDB默认还禁用了innodb_locks_unsafe_for_binlog系统变量。 在这种情况下,InnoDB使用next-key locks进行搜索和索引扫描,从而避免幻读。

意思是说,隔离级别是RR的,但是由于禁用系统变量的缘故,导致达成了串行化的效果。

在RC隔离级别下,同一个事务中每个快照读都会生成并获取最新的Read View;

而在RR隔离级别下,同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View

4、MVCC+next-key locks解决幻读

Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。

MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

1、Record Locks

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

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

2、Gap Locks

锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
3、Next-Key Locks

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:

(-, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +)

参考资料:

CS-Notes

数据库事务丢失修改,不可重复读,读"脏"数据的区别

事务ACID理解

脏读、不可重复读、幻读
正确的理解MySQL的MVCC及实现原理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值