首页 > 技术文章 > 关系数据库(事务:一致性+隔离级别)

qyf2199 2020-05-18 23:19 原文

数据库的学习,主要分为几个部分(按时间顺序123进行):

1. 关系数据库基础知识

CyC2018/CS-Notes/blob/master/notes/数据库系统原理

2. MySQL的语法

《MySQL必知必会》

CyC2018/CS-Notes/blob/master/notes/SQL

CyC2018/CS-Notes/blob/master/notes/MySQL

牛客MySQL入门教程

3. MySQL上机实战训练&选择题

MySQL上机实战训练

选择题专练 

-------------------------------------------------------------------------

 下面是正文:

 

 

第一步:关系数据库基础知识     

参考 CyC2018/CS-Notes/blob/master/notes/数据库系统原理

 

事务 Transaction

 

 

ACID

A:atomic原子性

C:consistency一致性

I:isolation隔离性

D:durability持久性

 

A:事务不可分割的最小单元,事务的操作(c、r、u、d)要么全成功,要么失败全部回滚(Undo Log)   "Ctrl+Z"

C:多个事务,对同一个数据的:读/写,获取相同结果

I:  在Commit(提交)之前,做的修改其他人(事务)看不见

D: 数据安全持久保存:崩溃后恢复,重做日志(Redo Log)

 

 

 

 

并发一致性问题

1)修改丢失 (覆盖) 

2)脏读 (撤销修改,中间状态被别的捕获)(两个事务的读)

3)不可重复读(连读2次不一样,因为中间有别的修改了)(同一个事务两次读)

4)幻影读(类似上条,不同的是获取:间接数据)

T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。

 

 

 

T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了修改,那么 T2 读取的数据是脏数据。

 

 

T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

 

 

T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。

 

并发性 <==> 隔离性

在并发环境下,事务的隔离性很难保证因此会出现很多并发一致性问题。

解决方法:通过 并发控制 来保证 隔离性。

并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。

 

 

 

锁粒度

  • 粒度越大,锁争用越大,并发度越小
  • 粒度越小,系统开销越大

MySQL两种粒度:行级锁、表级锁

 

读写锁(S/X)

  • 读锁(共享锁Share)S锁
  • 写锁(互斥锁)X锁

 

 

意向锁

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

IS/IX 对行加锁;S/X对整个表加锁

 红框是原来2X2的读写锁(S/X)就有的,

绿框新增的兼容性,包括:IS与IX,IX与IX

 

行锁、表锁

行锁or表锁都可以用:读锁or写锁

行锁:细粒度,消耗数据库资源大,但并发性好;

表锁:组粒度;

 

乐观锁、悲观锁

乐观锁:  CAS(达到预期就改,否则自旋等待)(AtomicInteger)  =》 ABA问题(版本号解决,用getStamp)

乐观锁本身不加锁(不用syn同步锁),只是在提交时检查

悲观锁:  假定发生冲突,用锁(1.MySQL的X锁-排它锁)(2.代码块,用Synchronized同步锁)

 

 

封锁协议(三级封锁协议、两段锁)

三级封锁协议(层层递进,解决前三个并发一致性问题)

1)一级封锁协议   修改时,X锁|整个事务T  阻止其他修改(读时不阻止)==》 防止"修改丢失"

2)二级封锁协议   读时 ,S锁|读完释放  阻止其他修改  ==》 增加:防止"脏读"

3)三级封锁协议   读时 ,S锁|整个事务T  阻止其他修改  ==》 增加:防止"不可重复读"

一级封锁:防止同时修改;

二级封锁:防止读的时候,有其他修改(但两次读之间,没有保护,所以存在"不可重复读")

三级封锁:整个事务都封锁,保证多次读取数据之中,没有其他线程进行修改

 

 

两段锁协议 (两个阶段):加锁、解锁

第一阶段全部是加锁,第二阶段全部解锁(开始解锁后就不能开始任何加锁了):

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

 可串行化调度:通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。串行执行的事务互不干扰,不会出现并发一致性问题。

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

(不是特别能领悟,后续再补充。参考阅读:https://blog.csdn.net/qq4165498/article/details/76855139)

 

MySQL 默认隐式锁定(使用两段锁协议) / 也可手动切换显式锁定

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

 

 

隔离级别

1)未提交读:  未提交的修改,对其他事务可见

2)提交读:  未提交的修改,其他事务不可见【解决:脏读(两个事务读)】

3)可重复读:  读数据时,其他事务不能改 => 多次读取结果一样【解决:不可重复读(同一个事务读两次)】

4)可串行化:  加锁保证(同一时间仅一事务),事务串行互不干扰 => (所有的)并发一致性问题不出现

 

 

 

MVCC: 版本快照, 适用于2/3(使用undolog回退)

  • 未提交读:无须MVCC 
  • 提交读+可重复读:适合MVCC
  • 可串行化:MVCC无法实现读写行都加锁

基本思想

MVCC修改操作(增删改)会创建版本快照

脏读不可重复读最根本的原因是事务读取到其它事务未提交的修改

在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照

版本号

  • 系统版本号  SYS_ID   每个新事务自动递增
  • 事务版本号  TRX_ID

Undo日志

MVCC的多版本快照,存储在Undo日志中,通过回滚指针连接快照。

例如:

 

 

ReadView(MVCC) (??这里不是很懂)

在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:

  • TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。

  • TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。

  • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:

    • 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
    • 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。
  • 快照读 读不加锁

  MVCC的 SELECT 操作是快照中的数据,不需要进行加锁操作。

  • 当前读 改加锁

   MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。

  在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁:

SELECT * FROM table WHERE ? lock in share mode;
SELECT * FROM table WHERE ? for update;

 

Next-Key Locks(锁定索引+索引间隙) =>用于解决幻影读问题(3升级到4)    (InnoDB)

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

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

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

  • Next-Key Locks: 双管齐下Record Locks + Gap Locks. 不仅锁定一个记录上的索引,也锁定索引之间的间隙。

  

 

关系数据库设计理论

函数依赖

  • 依赖:记 A->B 表示 A 函数决定 B         其中A={A1,A2...An}  B={B1,B2...Bm}也就是A/B都有若干属性
  • 部分依赖:A存在子集A'  这时候A'就可以决定B
  • 完全依赖:A->B,其中A为最小集

  对于 A->B,B->C,则 A->C 是一个传递函数依赖。

异常

以下的学生课程关系的函数依赖为 {Sno, Cname} -> {Sname, Sdept, Mname, Grade},键码为 {Sno, Cname}。

也就是说,确定学生Sno和课程Cname之后,就能确定其它信息: 

不符合范式的关系,会产生很多异常,主要有以下四种异常:

  • 冗余数据:例如 学生-2 出现了两次。
  • 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
  • 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 课程-1 需要删除第一行和第三行,那么 学生-1 的信息就会丢失。
  • 插入异常:例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。

 

范式 NF (作用是消除冗余;时间 =》换空间 + 一致性)

范式理论是为了解决以上提到四种异常。

高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。

  • 1NF  属性不可分
  • 2NF  消除“部分依赖”
  • 3NF  消除“传递依赖”
  • BCNF 

 

例子:

(1)符合1NF:  属性不可分(上图)

函数依赖:

  • Sno, Cname-> Grade    //Grade 完全依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
  • Sno -> Sname, Sdept    //部分依赖(因为有2个主键)
  • Sdept -> Mname          //Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。

拆分:部分依赖的全部拆出来建新表,这样减少冗余:

(2)符合2NF:  无部分依赖(上图)

上面的 关系-1 中存在以下传递函数依赖:

  • Sno -> Sdept -> Mname

于是,将关系1分解消除传递;关系2不变:

 (3)符合3NF:  无传递(非主属性)(上图)

一般数据库到3NF就行了:

没有冗余的数据库设计可以做到。但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。具体做法是:在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,允许冗余,目的是达到:时间换空间

 

 

ER图 

三个组成部分:实体、属性、关系:

实体的三种关系

1对1, 1对n, n对n

  • 如果 A 到 B 是1对n关系,那么画个带箭头的线段指向 B;
  • 如果是1对1,画两个带箭头的线段;
  • 如果是n对n,画两个不带箭头的线段。

下图的 Course 和 Student 是1对n的关系:

??

多重关系图

一个实体在联系出现几次,就要用几条线连接。

下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系:

表示子类

用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。

 

 

 

索引

B+树(连续性、局部性)   hash索引(高频)

 

主要索引类型:

  • PRIMARY KEY(主键索引)

  • UNIQUE(唯一索引)

  • INDEX(普通索引)

  • FULLTEXT(全文索引)

  • 组合索引

 

索引设计:

  • 索引字段尽量使用数字型(简单的数据类型)

  • 尽量不要让字段的默认值为 NULL

  • 前缀索引和索引选择性

  • 使用唯一索引

  • 使用组合索引代替多个列索引

  • 注意重复/冗余的索引、不使用的索引

 

推荐阅读