SQL数据库核心构成

目录

一.SQL数据库的索引设计

1.B+树的结构

2.B+树的查找过程

1.单个查找ID=30的行数据

2.范围查找18<=ID<=49

3.B+树的特点优点

4.B+数3层大约可以存两千多万条数据

二.SQL数据库的事务与并发

1.事务自动提交模式

2.事务并发的四个问题

1)丢失更新

2)脏读

 3)不可重复读

 4)幻读

三.封锁

1.共享锁和排他锁

2.表锁

1)分类有三种

2)引擎默认用表锁

3)特点

4)什么时候用到表锁

3.行锁

1)分类

2)引擎默认用行锁

3)特点

4.页锁

5.如何上锁

四.三种级别的封锁协议

1. 一级封锁协议

2.二级封锁协议

3.三级封锁协议

4.两段锁协议(解决幻读问题)

5.可串行化调度(解决幻读问题)

五.事务的隔离级别​编辑

1.读未提交(read uncommitted)

2.读已提交(read committed)

3.可重复读取(Repeatable Read)

4.可串行化(SERIALIZABLE)

​编辑

六.MVCC

1.不属于mvcc的内容,但是需要知道的知识

2.必须知道的mvcc的特点

3.简单描述mvcc怎么做的

4.快照读

5.事务的ID

6.ReadView

7.快照

8.版本链

9.行隐藏的字段

10.mvcc实现RC和RR的唯一区别

11.寻找可读数据的规则

12.Undo log 

13.记录行修改的具体流程

14.MVCC扩展细节

细节1:

细节2:

细节3:

细节4:delete行数据的删除

细节5:在可重复读的隔离级别下,mvcc是真的防止幻读了吗

1.快照读是怎么避免幻读的

2.当前读是怎么避免幻读的

3.幻读被彻底解决了吗?

七.mysql的innodb引擎是怎么实现四个隔离级别的


注意:

1.索引结构b+树, 2.事务,3.并发的四个问题,4.封锁和封锁协议,5.事务的隔离级别,6.mvcc,

以上6个数据库基础的知识是mysql和oracle共有的, 除了mvcc和事务的隔离级别的实现方式有不同之外,其他的都大致一样.

一.SQL数据库的索引设计

SQL数据库的索引结构有很多,下面介绍一种常用的索引结构--B+树

注意:mysql的索引默认是B+树结构,oracle的索引结构可以选择B+树,也可以选其他结构.

1.B+树的结构

B+树,叫平衡多叉树,由根节点,非叶子节点,叶子节点,组成

根节点:   只有一个
非叶子节点:除了根结点和叶子节点都是非叶子节点
叶子节点:存的是行数据,所有的叶子节点串起来组成双向链表.

2.B+树的查找过程

索引原本是和表一起在磁盘中的,但是为了查找效率,在用到表的时候,会把此表的索引(除了叶子节点层)先加载到内存.

先说索引是简单的主键id情况

1.单个查找ID=30的行数据

1)把索引(除了叶子节点层)加载到内存.

2).找到根节点大于等于15,小于56的区间,(在内存)

3).再找到非叶子节点大于等于20,小于49的区间,(在内存)

5).去磁盘中遍历[20,30]这个子叶节点,找到id=30这行的数据.(在磁盘)

2.范围查找18<=ID<=49

1)把索引(除了叶子节点层)加载到内存.

2).找到根节点大于等于15,小于56的区间,(在内存)

3).再找到非叶子节点大于等于15,小于20的区间,(在内存)

4).遍历[15,18]这个子叶节点,找到id=18这行的数据,(在磁盘)

5).从id=18这行数据开始顺着双向链表往后遍历查找,直到找到id=49这行数据的位置,然后把id=18和id=49和它俩之间的记录全部读出来.(在磁盘)

再说索引是字符串或者主键id和字符串组成联合索引的情况,这个我放到mysql专栏了.

3.B+树的特点优点

1.更少的查找次数

平衡树查找操作的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。

红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,检索的次数也就更多。

2.利用计算机预读特性

为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。

操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。

数据库系统将索引的一个节点的大小设置为页的大小的倍数,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。

因此,以mysql为例,会把一个B+数节点的大小默认设为16KB.

4.B+数3层大约可以存两千多万条数据

对于 B+树而言,树的高度一般不超过 4 层,就 MySQL 的 InnoDB 存储引擎而言,一个结点默认的存储空间为 16Kb ( 可以通过这个命令查看SHOW GLOBAL STATUS like 'Innodb_page_size'; ), MySQL 的 InnoDB 存储引擎的索引一般用 bigint 存储,占用 8 个 byte,一个索引又会关联一个指向孩子结点的指针,这个指针占用 6 个 byte,也就是说结点中的一个关键字大概要用 14 byte 的空间,而一个结点的默认大小为 16kb ,那么一个结点可以存储关键的个数最多为 16384/14=1170 , 就相当于阶数 �=1170 ,那么对于一颗高度为 3 的 B+树而言保守估计可以存储 1170×1170×16=21902400 个关键字,也就是两千多万条记录,其中的 16 为假定每一个叶子结点包含的关键字的个数(由于包含 Data 指针,所以叶子结点可以容纳的关键字的个数会少一些),就这样我想你也看到了 B+树的强大了。3层的 B+树就可以存储两千多万的数据,牛逼不?

上面的B+数知识是oracle和mysql共用的,剩余的细节知识两者也是大差不差.下面B+树往下延伸的知识我放到oracle和mysql的文章专栏了.
 

二.SQL数据库的事务与并发

1.事务自动提交模式

MySQL 默认采用事务自动提交模式。也就是说,如果不显式使用START TRANSACTION语句来开始一个事务,那么一条单独的(查询,增加,删除,修改)的sql都会被当做一个事务自动提交。

不手动显示开启事务

 手动显示开启事务

2.事务并发的四个问题

1)丢失更新

A   改--查  ,   B在A 的改--查   中间  改.

2)脏读

A   改--回滚  ,   B在A 的改--回滚  中间  读.

 3)不可重复读

A   读--读  ,   B在A 的读--读  中间  改.

 4)幻读

A   count(*)读--count(*)读  ,   B在A 的count(*)读--count(*)读  中间  增.

上面四个问题是事务在并发情况下产生了不一致性问题,主要原因是破坏了事务的隔离性,

解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。

因此,数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题

三.封锁

封锁协议是数据库自带的

是为了解决数据库的事务的并发出现的四种问题(丢失更新,脏读,不可重复读,幻读)

锁从性质上来说可以分为两类:共享锁和排他锁

锁从封锁的颗粒度来说可以分为三类:表锁,行锁,页锁

需要知道的是,共享锁和排他锁只是锁的性质不同,它们加在表上就是表锁,加在行上就是行锁

1.共享锁和排他锁

共享锁(S)---就是读锁,得到数据的共享锁可以读这个数据.

排他锁(X)---就是写锁,得到数据的排他锁可以读和写这个数据.

锁的兼容关系如下:

一个事务拿到了共享锁(S),其他事务可以拿到共享锁(S),不能拿到排他锁(X).

一个事务拿到了排他锁(X),其他事务不能拿到共享锁(S),不能拿到排他锁(X).

2.表锁

1)分类有三种

1.共享锁(S):当表上有共享锁(S)时,其他的事务可以拿到表的共享锁(S),可以对表中的数据进行读操作,但是不能拿到表的排他锁(X),不能对表中数据进行修改.

2.排他锁(X):当表上有排他锁(X)时,其他的事务不能拿到表的共享锁(S)和排他锁(X).也就不能对表的数据进行读和写.

3.AUTO-INC Locks 锁:自增锁

Auto-Inc Lock 是一个特殊的表级锁,用于自增列插入数据时使用。 在插入一条数据的时候,需要在表上加个 Auto-Inc Lock,然后为自增列分配递增的值,在语句插入结束之后,再释放 Auto-Inc Lock。

在 MySQL 5.1.22 版本之后,又弄了个互斥量来进行自增减的累加。互斥量的性能高于 Auto-Inc Lock,因为 Auto-Inc Lock是语句插入完毕之后才释放锁,而互斥量是在语句插入的时候,获得递增值之后,就可以释放锁,所以性能更好。

但是我们还需要考虑主从的情况,由于并发插入的情况,基于 statement -based binlog 复制时,自增的值顺序无法把控,可能会导致主从数据不一致。

所以 MySQL 有个 innodb_autoinc_lock_mode 配置,一共有三个值:

  • 0,只用 Auto-Inc Lock。
  • 1,默认值,对于插入前已知插入行数的插入,用互斥量,对于插入前不知道具体插入数的插入,用 Auto-Inc Lock,这样即使基于 statement -based binlog 复制也是安全的。
  • 2,只用互斥量。
2)引擎默认用表锁

Myisam

3)特点

1.对整张表加锁

2.开销小,加锁,解锁都需要性能开销

3.加锁快

4.无死锁

5.锁颗粒度大,发生锁冲突概率极高,并发性极低

4)什么时候用到表锁

平日的 update 、select 要用也是用行锁了,不可能用粒度粗的表锁。唯一能想到用上表锁的就是 DDL 语句了,比如 ALTER TABLE 的时候,应该锁定整个表,防止查询和修改,但是这个 server 已经提供了一个叫 MDL 的东西,即 Metadata Locks,所以已经用 MDL 来阻塞了,表锁也就排不上用场了。

真要用表锁,估计也就是数据恢复的时候,手动锁表还原数据了

3.行锁

1)分类

分类有下面几种:

共享锁(S),排他锁(X),

意向共享锁(IS)和意向排他锁(IX),

Record Lock锁(记录锁),

Gap Lock锁(范围锁,间隙锁),

Insert Intention Locks(插入意向锁)

next-key Lock锁(临键锁)

1.共享锁(S)和排他锁(X)上面提过就不说了

2.意向共享锁(IS)和意向排他锁(IX)

使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。

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

意向锁在原来的 X/S 锁之上引入了 IX/IS,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:

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

通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。

各种锁的兼容关系如下:

3. Record Lock锁(记录锁)

只锁住一行记录,叫记录锁,其实就是共享锁(S)和排他锁(X)只锁住了一行数据记录.

注意,我们说的锁一行数据记录,它的原理其实是去锁这行数据的索引.

下面以两条 SQL 的执行为例,讲解一下 InnoDB 对于单行数据的加锁原理。

update user set age = 10 where id = 49;update user set age = 10 where name = 'Tom';

第一条 SQL 使用主键索引来查询,则只需要在 id = 49 这个主键索引上加上写锁;第二条 SQL 则使用二级索引来查询,则首先在 name = Tom 这个索引上加写锁,然后由于使用 InnoDB 二级索引还需再次根据主键索引查询,所以还需要在 id = 49 这个主键索引上加写锁,如上图所示。

也就是说使用主键索引需要加一把锁,使用二级索引需要在二级索引和主键索引上各加一把锁。

4.Gap Lock锁(范围锁,间隙锁)

锁住一个范围.比如select * from stu where id>1 and id < 5 for update,锁住id>1和id<5(不包括1和5本身)这个区间,保证这个区间不会有其他数据插入.

不同的间隙锁锁住的区域重叠了也不会冲突,因为间隙锁的唯一目的就是防止其他事务插入数据到间隙中 ,所以即使两个间隙锁要锁住相同的间隙也没有关系,因为它们的目的是一致的,所以不冲突。

5.插入意向锁(Insert Intention Locks)

它也是一类间隙锁,但是它不是锁定间隙,而是等待某个间隙。比如上面举例的 id = 4 的那个事务 C ,由于被间隙锁给阻塞了,所以事务 C 会生成一个插入意向锁,表明等待这个间隙锁的释放。

并且插入意向锁之间不会阻塞,因为它们的目的也是只等待这个间隙被释放,所以插入意向锁之间没有冲突。

插入意向锁并没有什么用,只是排个队等待别人释放间隙锁.

6.Next-key Lock锁 (临键锁)

相当于记录锁+范围锁,同时锁住记录本身+间隙,锁的是一个左开又闭的范围,

比如select * from stu where id>2 for update  ,锁住了表中 id 范围为 (2, +∞] (包括2)的范围,

比如select * from stu where id>1 and id < 5 for update ,锁住id>1和id<5(包括1和5本身)这个区间,保证这个区间不会有其他数据插入.

2)引擎默认用行锁

innoDB

3)特点

1.对行数据加锁

2.性能开销大

3.加锁慢

4.有几率出现死锁

5.锁颗粒度最小,发生锁冲突概率最低,并发度最高

4.页锁

开销,加锁时间和锁粒度介于表锁和行锁之间,会出现死锁,并发处理能力一般(此锁不做介绍)

5.如何上锁

说明:正确的应该是说成拿到表或行的锁, 而不是说成给表和行加锁.虽然两种说法最后的结果都是一样的.

下面讲的还有其他锁,太深奥了,看不懂,下面就不举例介绍了.

四.三种级别的封锁协议

1. 一级封锁协议

解决事务并发的(丢失更新)问题

2.二级封锁协议

解决事务并发的(丢失更新)和(脏读)问题  

 

3.三级封锁协议

解决事务并发的(丢失更新)和(脏读)和(不可重复读)问题 

4.两段锁协议(解决幻读问题)

解决事务并发的(丢失更新)和(脏读)和(不可重复读)和(幻读)问题 

两段锁协议是对三级封锁协议的优化,它在三级封锁协议的基础上,允许事务没有结束也可以释放锁.

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

就是事务运行分两个阶段

加锁阶段:事务执行开始,然后不断的加锁,但是途中不能释放锁.

解锁阶段:事务不断的释放锁,但是途中不能再加锁,直到事务结束.

可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。

事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。

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

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

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

5.可串行化调度(解决幻读问题)

解决事务并发的(丢失更新)和(脏读)和(不可重复读)和(幻读)问题 

什么是串行化,就是只有当A事务完全执行完毕,成功提交后,B事务才能执行,就是事务一个一个的执行.

可串行化调度就是允许在A事务执行的时候B事务也可以执行,但是要经过合理的调度,保证最终A事务和B事务的结果都是跟串行化一样是正确无误的.

百度解释:多个事务的并发执行是正确的,当且仅当其结果与按某一次序串行化执行这些事务时的结果相同。

优点:可串行化调度能保证并发的事务调度方式既能满足数据一致性需求,又能提高并发事务的执行效率。

五.事务的隔离级别

1.读未提交(read uncommitted)

允许一个事务可以读取另一个未提交事务的数据.

只能解决(丢失更新)

2.读已提交(read committed)

允许一个事务可以读取另一个已经提交事务的数据

只能解决(丢失更新)和(脏读)问题

3.可重复读取(Repeatable Read)

在一个事务内部多次读取同一个数据对象,结果一致

只能解决(丢失更新)和(脏读)和(不可重复读)问题

4.可串行化(SERIALIZABLE)

强制事务串行执行,可以解决(丢失更新)和(脏读)和(不可重复读)和(幻读)问题。

数据库针对于事务的

封锁协议

事务隔离级别丢失更新脏读不可重复读幻读
一级封锁协议读未提交×
二级封锁协议读已提交××
三级封锁协议可重复读×××
两段锁协议+可串行化调度可串行化××××

六.MVCC

MVCC是多版本并发控制 Multi-Version Concurrent Contrl。

SQL数据库都有MVCC,只不过它们的实现方式略有不同,但是总体思路和下面讲的内容是一样的.

它是SQL数据库中的提高并发性能的一种方式,配合Undo log 和版本链,替代锁,去实现提交读和可重复读这两种隔离级别。

下面以mysql的InnoDB引擎的mvcc为例

1.必须知道的mvcc的特点

1.mysql和oracle都默认开启使用了mvcc.是自带的, 这个关不掉,不用也得用.

2.mysql和oracle两者实现mvcc的方式虽然有点差别,但是核心设计方式都是一样的.

3.mvcc是不加锁的,所有加锁的都不是mvcc的内容.

4.mvcc就一个作用,就是只能实现快照读的事务并发隔离

5.因为只有(脏读/不可重复读)是快照读的问题,所以mvcc只能解决这两种问题.所以mvcc只能实现RC和RR这两种隔离级别.

2.简单描述mvcc怎么做的

每次select操作会访问数据库中的版本链记录,其他事务可以修改这条记录,而select根据当前隔离级别去版本链中找到对应的版本记录,实现了读-写,写-读的并发执行,提升了系统的性能。

3.当前读

当前读:    特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。 

 select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
-- 所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加共享锁外,其他的操作,都加的是排它锁。

 

4.快照读

简单的select操作,属于快照读,不加锁。 不加读锁 读历史版本

5.事务的ID

事务的id是数据库自动给事务的.是一个自增数字,每开启一个新事物,这个数字就+1,然后给这个新的事务,当做这个新事务的id.

6.ReadView

RC级别下,事务每次快照读都会自动生成一个ReadView.

RR级别下,事务只在第一次快照读自动生成一个ReadView,后面再快照读会复用此ReadView.

快照读的一种形式,MySQL执行MVCC提取数据的依据;本质是一个数据结构,其中包括几下几个部分:
1. m_ids:当前活跃的事务ID集合(开启但未提交的事务ID);
2. min_trx_ids:最小活跃事务ID,小于它说明事务已经提交;
3. max_trx_ids:预分配的事务ID(下一个事务开启分配的事务ID),即当前最大事务ID+1;大于它说明该事物是ReadView生成之后才开启;
4. creator_trx_id:ReadView创建者的事务编号;

7.快照

一条行数据当被修改(insert,update,delete)时,都会把原来的数据记录下来生成一个快照,记录到undo log 日志的版本链中.

8.版本链

MVCC 将使用到的版本快照存储在 Undo 日志中,该日志通过回滚指针(db_roll_pointer)把一个数据行(Record)的所有版本快照连接起来,称为版本链。

9.行隐藏的字段

MySQL数据库中,每条数据除肉眼可见的数据之外,还有几个隐藏字段:
1.db_trx_id:表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。
2.db_roll_pointer:回滚指针,指向指向这条数据的上一个版本记录;
3.db_row_id:随着新行插入而单调递增的行ID。理解:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC没关系

10.mvcc实现RC和RR的唯一区别

实际上,RC和RR两个隔离级别,实现的主要区别在于ReadView的时机不一样,RC隔离级别下,每次快照读都会生成一个ReadView;而RR级别下,只有在第一次快照读时生成ReadView,后面会延续使用;事务根据生成的ReadView去版本链中寻找自己能够访问的数据,最后得到所读的数据;

11.寻找可读数据的规则

事务是如何根据生成的ReadView去版本链中寻找自己能够访问的数据的
判断规则如下:
1.当前记录修改操作事务ID 等于 ReadView创建事务ID ? 等于说明是自身事务修改的数据,可以访问;
2.判断 trx_id < min_trx_id(2) ? 如果小于说明事务提交了,可以访问;
3. 判断trx_id >= max_trx_id(5) ? 成立说明该事物是在ReadView生成之后才开启的,不允许访问,不成立继续向下判断;
4. 判断min_trx_id <= trx_id < max_trx_id,成立则在m_ids中对比,如果不存在说明事务已经提交,可以访问;
5. 不满足以上条件,根据回滚指针依次往前一个版本数据查找,直到找到满足条件的数据;

12.Undo log 

Undo log中存储的是老版本数据。
        大多数对数据的变更操作包括 insert/update/delete,在InnoDB里,undo log分为如下两类:
        ①insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
        ②update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

如下图:事务A的改动的这行数据的版本快照记录在undo log 中,现在事务回滚用不到它,而且也没有readview用到它了,因此这个事物A的版本快照就会被purge线程删除.

Purge线程

为了实现InnoDB的MVCC机制,删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。
        为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

13.记录行修改的具体流程

注意:(删除,新增,修改)都被看做是修改

 假设有一条记录行如下,字段有Name和Honor,值分别为"curry"和"mvp",最新修改这条记录的事务ID为1。

(1)现在事务A(事务ID为2)对该记录的Honor做出了修改,将Honor改为"fmvp":

                ①事务A先对该行加排它锁
                ②然后把该行数据拷贝到undo log中,作为旧版本
                ③拷贝完毕后,修改该行的Honor为"fmvp",并且修改DB_TRX_ID为2(事务A的ID), 回滚指针指向拷贝到undo log的旧版本。(然后还会将修改后的最新数据写入redo log)
                ④事务提交,释放排他锁

        (2) 接着事务B(事务ID为3)修改同一个记录行,将Name修改为"iguodala":

                ①事务B先对该行加排它锁
                ②然后把该行数据拷贝到undo log中,作为旧版本
                ③拷贝完毕后,修改该行Name为"iguodala",并且修改DB_TRX_ID为3(事务B的ID), 回滚指针指向拷贝到undo log最新的旧版本。
                ④事务提交,释放排他锁

14.MVCC扩展细节

细节1:

一行数据有很多版本快照,但是只有最新版的快照(事务已经成功提交的)是真实存在的,之前的版本快照都不是真实存在的,用到的话是从最新版的快照根据undo log的操作记录倒推出来的,也就是说undo log主要记录了数据的逻辑变化,比如一条INSERT语句,对应一条DELETE的undo log,对于每个UPDATE语句,对应一条相反的UPDATE的undo log,这样在发生错误时,就能回滚到事务之前的数据状态。

细节2:

mysql的事务已提交的一行的数据存在磁盘中,    而这行数据的历史记录数据存在undo log日志中

细节3:

我通过代码测试验证得知,事务没有提交之前,它对行数据所做的任何增删改都是对外不可见的,都是在事务自己的事务空间里.

细节4:delete行数据的删除

先说结论:不会物理删除,而是会把这行数据的删除位标记为delete.

delete删除一行数据A,发生的4个过程

1.复制一份这行数据A存进undo log日志中,以便后面回滚或者恢复数据.

2.在这行数据A的删除位标记为delete.

3.有一个垃圾链表,专门记录被删除数据的空间位置,在垃圾链表中记录这行数据A的空间位置,把这行数据A占用的空间标记为可重用空间,

4.之后有新插入行数据的时候,比如,子叶节点有4个数据,id分别是3,5,6,9,那么这个时候你把id=5的这行数据A删除了,插入进来一个id=4的,那么就会把这行数据A进行物理删除,把空间腾出来给新插入的id=4的行数据. 

为什么数据库要这么设计

因为如果直接物理删除的话,那么这行数据的空间就被空出来了,那么,数据库的索引结构B+数强大的平衡算法检测到这个空间没有数据后,为了数的平衡性,就会把其他的数据往这个空间移动,那么就会造成整个索引结构的变化,会产生大量的磁盘IO,这是很耗费时间并且影响性能的.

细节5:在可重复读的隔离级别下,mvcc是真的防止幻读了吗

先说结论:

在可重复读的隔离级别下,mvcc只能很大程度上避免幻读,但是不能完全解决幻读.

下面说的都是在在可重复读的隔离级别下的.

1.快照读是怎么避免幻读的

mvcc使用Read View+undo log解决了快照读的幻读问题.

2.当前读是怎么避免幻读的

我们假设select ... for update当前读是不会加锁的(实际上是会加锁的),在做一遍实验。

这时候,事务 B 插入的记录,就会被事务 A 的第二条查询语句查询到(因为是当前读),这样就会出现前后两次查询的结果集合不一样,这就出现了幻读。

所以,Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了Next-key Lock锁 (临键锁=记录锁+间隙锁)。(额外提一句,读提交隔离级别,是没有临键锁的,只有记录锁)

假设,表中有一个范围 id 为(3,5)临键锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

比如select * from stu where id>2 for update  ,锁住了表中 id 范围为 (2, +∞] (包括2)的范围

举个具体例子,场景如下:

事务 A 执行了这条当前读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。

然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了  next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象。

3.幻读被彻底解决了吗?

可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读

我举例两个可重复读隔离级别发生幻读现象的场景。

第一个发生幻读现象的场景

在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A  再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。

第二个发生幻读现象的场景

 除了上面这一种场景会发生幻读现象之外,还有下面这个场景也会发生幻读现象。

T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。

T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;

T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

再说一下结论:MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。

七.mysql的innodb引擎是怎么实现四个隔离级别的

1.未提交读

innoDB采用一级封锁协议,就是通过给增删改sql加写锁(普通select不加锁),事务结束才释放写锁,来实现未提交读.

2.提交读

innoDB通过一级封锁协议+MVCC来实现提交读.

3.可重复读(mysql默认的隔离级别)

innoDB通过一级封锁协议+MVCC来实现提交读.

3.可串行化

先说明:innoDB通过一级封锁协议+MVCC+临键锁 很大程度上防止了幻读,相当于很大程度上可以保证可串行化,但是并不能100%保证串行化.

说法一:innoDB通过两段锁协议和可串行化调度来实现事务的串行化执行.

说法二:一个事务开始直接给表加锁,等这个事物完成后,另一个事物才能对表的数据进行操作,这样就实现了绝对的串行化.

其实可串行化是一个实现目标,是多方努力的结果,并不一定说是只加表锁,或者只加行锁,是多方协同结合着去实现可串行化的.

扩展:

1.一条普通的select有没有事务

1.mysql给每条sql都加了一个隐形事务,然后自动帮你提交.因此select也是有事务的,也会被分配一个事务id

2.但是由于select并不会改变数据,因此,select一执行完同时就瞬间commit提交事务了

2.一条普通的select有没有加锁

1.在mysql前三个隔离级别的实现下,普通的select都是什么锁都没加的,都是快照读.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值