关系数据库(事务:一致性+隔离级别)
数据库的学习,主要分为几个部分(按时间顺序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
3. 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
前缀索引和索引选择性
使用唯一索引
使用组合索引代替多个列索引
注意重复/冗余的索引、不使用的索引