数据库笔记

术语

  • 外键
    一个表想引用另一个表的的某一列,就在自己的表中建立一个外键并指向被引用表的那一列。

修改表

添加列

ALTER TABLE mytable
ADD col CHAR(20);

删除列

ALTER TABLE mytable
DROP COLUMN col;

删除表

DROP TABLE mytable;

普通插入

INSERT INTO mytable(col1, col2)
VALUES(val1, val2);

插入检索出来的数据

INSERT INTO mytable1(col1, col2)
SELECT col1, col2
FROM mytable2;

将一个表的内容插入到一个新表

CREATE TABLE newtable AS
SELECT * FROM mytable;

更新

UPDATE mytable
SET col = val
WHERE id = 1;

删除

DELETE FROM mytable
WHERE id = 1;

TRUNCATE TABLE 可以清空表,也就是删除所有行。

TRUNCATE TABLE mytable;

连接

默认使用内连接,即不保留没有关联的行。左外链接是是保留左表没有关联的行。
例如customers表
在这里插入图片描述
和orders表
在这里插入图片描述
按cust_id左连接,
在这里插入图片描述
得到
在这里插入图片描述

  • on 和 where 的区别
    on 是连接前生成临时表时的条件,而whee是临时表构建好后,再对其进行过滤的条件。.

  • 连接和删除

    1、delete from t1 where 条件
    2、delete t1 from t1 where 条件
    3、 delete t1 from t1,t2 where 条件
    4、delete t1,t2 from t1,t2 where 条件
    前 3者是可行的,第4者不可行。

  • distinct
    当distinct 作用于两个字段时,实际上是根据两个字段的组合来去重的。

查询处理

  • 关系代数
    在这里插入图片描述
    如果两个关系R,S的列数相同,且R中的第i个属性和S中第i个属性来自相同的域,则说RS相容。交并差针对针对与相容关系。

    举例说明 除法的作用
    关系R有学生,课程两个属性。 关系S有课程这一个属性。
    R除以S表示R中选修了S中所有课程的学生。

  • 查询计划
    先把查询解析成关系代数,对于关系代数中的每一个操作,标注其应该使用的算法和索引就构成了查询计划。
    例如对于查询

    select salary
    from instructor
    where salary < 75000;
    

    对应查询计划:
    在这里插入图片描述
    查询时间主要跟从磁盘中读取一个块的平均时间tT和随机I/O访问操作(寻道)的平均时间tS有关。tT一般为0.1ms,tS一般为4ms。
    对于固态硬盘来说,虽然没有寻道时间但是tS也有90us,tT为10u。

  • 选择操作
    聚簇索引是指数据按照索引的顺序储存在磁盘上。
    如果选择操作在聚簇索引上,并且同时是主键则耗时为(hi+1)*(tS+tT)其中hi是b+树的高度。
    如果是聚簇索引但不是主键,则耗时为hi*(tS+tT)+tS+b*tT。因为 不是主键的话,可能有多个满足条件的元组,所以b是包含了满足条件元组的块数量。
    如果不是聚簇索引,但是是主键和第一种情况一样。
    如果不是聚簇索引,且不是主键,则满足条件的元组可能散落在n个不同的块上,则耗时为(hi+n)*(tS+tT)。

    对于多个条件串联,可以先用一个耗时少的条件检索,然后再根据其他条件过滤。第二种方法是使用复合索引。第三种方法是直接检索每一个条件对应的元组指针,然后求交集。

  • 连接操作
    自然连接是在广义笛卡尔积R×S中选出同名属性上符合相等条件元组,再进行投影,去掉重复的同名属性,组成新的关系
    外部排序:
    外部排序用于待排序的磁盘内容大于内存容量的情况,
    外部排序分为两个阶段,第一个阶段每次从磁盘读取M个块进行排序,并写回磁盘,这M个块被称为一个run。
    第二个阶段,针对第一个阶段得到的N个 run, 每一个run分配一块内存,最后再分配一块用来缓存输出。根据归并排序,每次从N个块中选出一个元组保存在输出块中。
    注意 ,如果N大于M-1,则需要进行多轮外部排序。

    进行针对某个属性x的自然连接,最直接的方法是使用两个循环,外循环操作的关系叫做外关系。在无序的情况下下,以块作为循环单位比以元组作为循环单位效率更高。如果只有其中一个关系有序,则将其作为内关系(indexed nested-loop join)。当两个关系都有序(聚簇索引)的时候,直接按顺序进行匹配属性x相等的元组。
    当关系A为聚簇索引,关系B为非聚簇索引时,可以先按两者都是聚簇索引的情况,把A中的元组和B中的指针组合起来,然后按照指针对结果进行排序,最后再按顺序读取指针。
    hash-join
    对于外关系r和内关系s, 先使用一个hash函数对s和r进行分块(partition)。分块后,每个块保存在磁盘上,使用块时再读取到内存中。使用另外一个哈希函数在每个块si建立内存上的聚簇索引。使用 indexed nested-loop join对ri和si进行连接。 在分块的时候,需要在内存中给每一个块分配一个缓存block,所以当块数量大于内存容量M的时必须进行多趟分块。当第一趟进行完后,单独读取其中一块,并使用不同的hash函数进行下一趟分块。
    pipeline:
    把查询中的各个操作组织成一个树形结构,每个操作实现open,next,close接口。当父节点处理完一条元组之后,主动调用子节点的接口获取数据。 有些操作不能组织成pipeline,比如sort,因为必须它必须等所有数据都处理完成后才能返回。

  • 查询优化
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    查询任务是找music系所有老师的名字及其对应的课程名。
    一个直接地关系代数为 Π n a m e , t i t l e ( σ d e p t n a m e = “ M u s i c ” ( i n s t r u c t o r ⋈ ( t e a c h e s ⋈ Π c o u r s e i d , t i t l e ( c o u r s e ) ) ) ) {Π}_{name,title} (σ_{dept name =“Music”} (instructor ⋈ (teaches ⋈ Π_{course id,title}(course)))) Πname,title(σdeptname=Music(instructor(teachesΠcourseid,title(course))))
    但会产生一个非常大的中间结果 i n s t r u c t o r ⋈ ( t e a c h e s ⋈ Π c o u r s e i d , t i t l e ( c o u r s e ) instructor ⋈ (teaches ⋈ Π_{course id,title}(course) instructor(teachesΠcourseid,title(course),一个解决方法是过滤掉instructor中不在music系的元组然后令其马上与teaches连接,最后再和course连接,即
    Π n a m e , t i t l e ( ( σ d e p t n a m e = “ M u s i c ” ( i n s t r u c t o r ) ⋈ t e a c h e s ) ⋈ Π c o u r s e i d , t i t l e ( c o u r s e ) ) Π_{name,title} ((σ_{dept name =“Music”} (instructor) ⋈ teaches) ⋈ Π_{course id,title}(course)) Πname,title((σdeptname=Music(instructor)teaches)Πcourseid,title(course))

事务

  • ACID
    以从银行账号A转50元到账号B的事务T1为例
  1. 一致性;事务完成后账号A和账号B的钱数总和是一致的
  2. 原子性;A-50和B+50的操作要么一起成功,要么一起失败。
  3. 持久性;一旦事务完成,必须保证修改的数据被持久化。
  4. 隔离性isolation。 并发情况下满足 一致性。
    事务的状态:

在这里插入图片描述

  • 可串行化
    T2是从账户A中转10%的资金到B账户的事务。一个schedule是多个事务的所有指令的一个排列,一个时间只能有一个指令。schedule中的指令只需要满足在其所属事务中的顺序。 如果在schedule中属于同一个事务的指令被排在一起,就称这个schedule是serial的。如果一个schedule的执行结果和某个serial的schedule是相同的,这称其为serializable schedules. 在并发条件下,有些schedule并不满足serializable,例如:
    在这里插入图片描述
    只考虑read和write操作
    conflict serializable是指通过交换read(A)和write(B)或者read(A)和read(B)等能使schedule变为serial schedule.
    判断一个schedule是否为conflict serializable,首先把schedule中的事务作为节点构建有向图,如果schedule中存在事务T1 read(A)在前,T2write(A)在后,或者T1 write(A))在前,T2read(A)在后以及T1 write(A))在前,T2write(A)在后,则画一条由T1指向T2的边,表示T1是T2的前继节点。如果构成的有向图不存在环路,则该schedule 是conflict serializable。

  • Recoverable Schedules
    如果T2读取A之前,A被T1写入,则称T2依赖于T1。一个由T2和T1组成的schedule是可恢复的,当且仅当T1的commit操作在T2的commit之前,因为T1回滚的时候, 必须连同T2一起回滚,而T2已经commit,已经commit的事务不能abort。
    在恢复过中可能造成cascading rollback.
    在这里插入图片描述
    T8的回滚操作需要带动T9的回滚,因为T9依赖于T8,同理T10依赖于T9。解决的方法是如果T2依赖于T1,使得T1的commit在T2的read操作之前。

  • 隔离级别
    可串行化
    可重复读 ,要求读取的数据必须是已提交的,并且一个事务内两次读取的结果要一致
    提交读,只要求读取的数据必须是已提交的
    不提交读。
    phantom phenomenon 幻想读
    假如有一个查询:

    select ID, name
    from instructor
    where salary> 90000;
    

    它的功能是找出工资在90000以上的导师,假如在查询过程中,另外一个用户插入了一条工资超过90000的数据,只包含read和write两种操作的简单模型不能检测出这个冲突。解决方法是在salary > 90000这条谓语上加predicate locking锁。

并发控制

  • 基于锁的协议
    有两种锁,分别是共享锁和排它锁。只有共享锁之间是相容的,其它情况下锁之间都是冲突的,即申请了某个锁,另一个事务就不能申请。
    事务在读取数据之前,必须申请该数据的共享或排它锁。在写数据之前只能申请排他锁。 lock manager授权的顺序应该满足申请锁的顺序。
    锁协议下的前继关系:如果T1先对Q加A锁,然后事务T2对Q加B锁。且AB不相容,则T1是T2的前继。
    二段锁协议是指满足协议的事务分为两个阶段,第一个阶段只申请锁,另一个阶段只释放锁。二段锁协议能保证简单模型下的conflict serializable。例如不可能给以下的一个schedule加二段锁:
    T1: read(A)
    T2: write(A)
    T1: read(A)
    但是二段锁可能引发死锁:
    在这里插入图片描述
    二段锁协议要么使non-serializable不能执行,要么引发死锁。
    二段锁协议中也可能存在cascading rollback:
    在这里插入图片描述
    解决的方法是增加一个限制(strict two-phase locking protocol.),即排它锁必须等到提交前释放。
    在共享锁上还可以加排它锁,这种情况称为upgrade, 同时也可以删除多余的排它锁,称为downgrade。 考虑这样一种情况,某事务在开头读取A,并在很多操作之后更新A。如果直接在开头申请排它锁会降低并发性,所以可以首先申请共享锁然后使用upgrade和downgrade,最后释放共享锁。

  • 锁协议实现
    lock manager可以是一个单独的线程,接收和授权各个事务发来的请求。其内部维护一个hash表
    在这里插入图片描述
    利用数据名进行索引。当事务发来一个针对某个数据的请求时,将这个请求插入数据对应链表的末尾。
    如果请求和在它之前所有已授权的请求是相容的,则授权该请求,否则该请求应该等待。
    当释放一个锁时,从链表中删除对应的请求,并检查其后有没有满足条件能够授权的请求。
    当事务回滚时,删除该事务在hash表中所有未授权的请求,当回滚完成后,再释放已授权的请求。
    找出事务对应请求这一步,需要针对事务建立额外的索引。

  • 基于图的协议
    在这里插入图片描述
    在图协议中,只有排它锁,事务第一次可以申请图中的任何节点,但是后续只能在已经申请父节点的情况下申请子节点,任何节点只能申请一次。图协议可以保证 serializability.

    T1T2
    read(A)
    write(A)
    read(B)
    write(B)

    以上schedule不可能出现在图协议中,因为A和B必须在一个子树中,而且因为一个节点只能申请一次,所以在T1 read(A)和write(B)之间,T1必须持有子树的根节点,导致T2不可能再申请A和B。 同时图协议也能保证不发生死锁。

  • 多粒度锁

    可以将数据库的各个粒度组织成一颗树,比如关系和元组之间就是父子关系。意向锁IS和IX是指当前节点的子节点有S锁或X锁,SIX锁相当于事务先对节点加S锁,然后再加IX锁。
    如果事务要对某个节点加S或IS,必须从上往下把它的所有祖先节点加上IX或IS锁。
    如果事务要对某个节点加X或SIX,必须从上往下把它的所有祖先节点加上IX或SIX锁。
    事务只有在释放完所有子节点的锁后才能释放根节点的锁。
    在这里插入图片描述

  • b+树并发 crab locking
    读操作:
    首先拿到根节点的读锁
    每次迭代过程中尝试获得子节点的读锁,获得后释放父节点的锁。以子节点作为当前节点
    更新操作:
    首先拿到根节点的读锁。
    每次迭代过程中尝试获得子节点的读锁,以子节点作为当前节点。
    如果当前节点安全(在插入操作中,+1后不会造成上溢,在删除操作中。。),则释放其所有祖先的锁。
    到达叶子节点后,将所有剩余的读锁从上至下转换成写锁,再进行操作。
    如果当前节点在插入删除操作时需要修改sibling,则必须对其加写锁。因为此时sibling可能正在进行插入删除操作,造成这种现象的原因是sibling是一个safe节点,其父节点的锁被提早释放了,导致当前节点和sibling节点的插入删除操作并发执行。
    crab locking 在不考虑叶子节点的遍历时不会导致死锁。
    可能出现父节点写 ,子节点中既有节点读也有节点写的情况,这时并不影响只读子节点的一致性,因为父节点最多只会修改只读子节点指向父节点的指针,而只读子节点一般情况下没有访问父节点的操作,除非是在蟹行协议中只读节点会被转换成可写节点。为了防止这种情况,在蟹行协议中规定转换操作只能从上至下执行。

  • b-link
    b+树的不一致性是当子节点分裂后,原本可以通过父节点->当前节点的路径查询到的键值被重新分配到了一个新的节点上,而此时父节点还没有更新,所查询不到。 crab locking可以解决这个问题,但是blink可以在只给当前节点加锁的情况下解决。 它的做法是给每个内部节点增加一个指向右兄弟的指针,当前节点访问不到键值时,可以到右兄弟中搜索。当分裂时,先获取当且节点的锁,然后将新节点连接到当前节点的右节点上,最后更新父节点,每次只对当前调整节点加锁,当子节点调整完毕后再向上回溯调整父节点,直到所有调整完毕。 推论:删除时只能从左兄弟借键值 插入时只能借给右兄弟

  • delete 和 insert 操作
    在时间戳协议下,删除一条item是需要满足当前事务的时间戳大于item的读时间戳和写时间戳。插入一条item之后,把它的读时间戳和写时间戳都设置为当前事务的时间戳。
    幻读出现在对b+树叶子节点的遍历上,为了避免幻读,在索引中查找一个范围时,把范围涉及到的所有节点加上共享锁。在更新item时,同时申请旧索引键值和新索引键值对应叶子节点的排它锁。插入操作申请新键值对应的叶子结点。

  • 死锁预防
    最简单的方法是要求事务的所有锁一起申请,要么全部成功,要么一起失败。 另一种方法是限制事务申请锁的顺序,比如之前的图协议。还可以给所有item一个编号,一旦事务获取了某个item的锁,它就不能再申请编号在这个item之前的所有item的锁。
    在基于抢占的方法中,给事务分配一个时间戳,记录其开始时间,当t1申请一个t2持有的锁时,如果t1时间戳小于t2就等待(wait-die),否则t1就回滚,回滚后不改变timestamp。
    在lock timeouts方法中,如果等待时间超过一个值就回滚事务。

  • 基于时间戳的协议
    在基于锁的协议中,两个并发顺序之间的顺序由第一对产生冲突的锁申请决定。 而在时间戳协议中,事务的顺序在运行前就已经被分配的时间戳决定了。
    进行读操作的事务的时间戳必须大于等于item的写时间戳。进行写操作的事务的时间戳必须同时大于等于item的写时间戳和读时间戳。读操作后如果事务的时间戳大于item的读时间戳,则更新item的读时间戳。
    Thomas’ Write Rule
    在这里插入图片描述
    在时间戳协议下,因为T27的写操作的时间戳小于T28的时间戳,所以T27应该回滚,但实际上T27的write可以直接忽略掉,不需要回滚,因为后面所有时间戳在T28之前读操作都会引发回滚,所有时间戳在T28之后读操作都会使用T28更新的值。 但是,如果T28进行的是一个读操作,T27还是要回滚。图中的例子是一个可串行化的shcedule,但是在其它协议下不可能发生的,Thomas’ Write Rule使其变成可能。

    VIEW SERIALIZABILITY
    假设schedule S和S’是事务T1和T2的两个不同排列,如果S和S’中每个读操作读取到的数据都是一样的,并且每个item最后一个写操作都是一样的,就说S和S’是view equivalent。
    在这里插入图片描述
    图中的shcedule和T27,T28,T30顺序执行的schedule是view equivalent,所以它也是view serializable的。

    基于验证的协议
    为了减少读操作的计算量,把事务分配三个时间戳,startTS是事务开始时间,ValidationTS是事务开始验证的时间,FinishTs是事务结束时间。 在startTS到ValidationTS只进行读操作,ValidationTS之后进行验证和写操作。如果事务只有读操作,就不用进入validation。 用ValidationTS衡量事务之间的先后顺序。每个事务Ti在validation阶段只需要检查在它之前的事务,如果被检查的事务Tk满足FinishTS(Tk) < StartTS(Ti) 或者StartTS(Ti) < FinishTS(Tk) < ValidationTS(Ti),并且Tk的写的元组集合和TI读的元组集合没有交集,则TI可以进行写操作,否则回滚ti对内存中变量的操作。这里ti在validation时应该等待tk的写操作完成。 不用startTS决定事务的顺序,是因为可能出现t1后开始事务,但是先进入validation阶段的情况,这时t1需要同时等待t2读操作和写操作完成。
    基于验证的协议属于乐观并发控制,因为进入事务后,直接完成所有在内存中的操作,然后再检查是写入磁盘还是回滚。

  • 多版本方案
    多版本方案保证serializability。
    每个item维护多个版本,每个版本都有读写两个时间戳。事务ti在读写操作之前要找到写时间戳最大并且小于等于ti的版本Qk。读操作直接读取并更新Qk的读时间戳。写操作要检查QK的读时间戳,如果ti小于读时间戳,则回滚。如果ti等于写时间戳,则直接更新QK的内容,否则创建一个新的版本。
    假设T2在T1之后,T2写Q之后产生新版本Q2 , 这时T1也来写Q,如果T2包含读操作,就会造成T1回滚,否则T1会创建自己的新版本Q1,不影响Q2。
    保留两个旧于当前所有事务的版本,更旧的版本删除

    可以把多版本和两段锁结合起来,事务被分成两种类型,一种是只读类型,一种是更新类型。用一个全局变量ts-counter保存commit的次数,每个只读事务在开始前读取这个变量并作为其时间戳。只读事务不用加锁,只需要找到版本最新的item。 更新事件和二段锁一样,申请共享锁和排它锁,并在提交时创建新版本和更新ts-counter。

  • Snapshot Isolation
    在快照隔离中,每个事务有两个时间戳,时间戳由进入验证阶段的总数表示,startTS到CommitTs之间进行读操作,commitTS后进行验证和写回数据库操作。写回操作会创建item的新版本,并且以commitTS为item的时间戳。满足StartTS(Tj) ≤ StartTS(Ti) ≤ CommitTS(Tj)或者StartTS(Ti) ≤ StartTS(Tj) ≤ CommitTS(Ti)的两个事务被当作是并发的。
    在first committer wins规则下,一个时间段只能有一个事务进行验证和写回,当事务进入验证后,会检查待写的item上是否已经有并发的事务提交的版本,即该版本的时间戳在当前事务的startTS和commitTS之间,如果有就abort当前事务。
    在first updater wins规则下, 在验证阶段申请并待写item的排它锁,如果申请到就和first committer wins规则一样进行检验。否则如果锁的持有者abort, 就继续检查,如果锁的持有者写回成功,就abort当前事务。
    快照隔离并不能保证可串行化.
    在这里插入图片描述
    图中的例子因为读锁和写锁的冲突不可能发生在二段锁协议中,但是可以出现在快照读中,因为两个事务最后更新的item不一样,可以通过检验阶段。

  • Degree-Two Consistency
    degree-two consistency是为了避免级联回滚,读锁可以马上释放,写锁必须在提交或bort后释放。 这可能会导致一个事务中两次读取的值不一样:
    在这里插入图片描述

recovery and atomicity

  • 基本概念
    事务修改item需要经历三个步骤,第一步是计算和修改本地私有变量,第二步修改数据库磁盘在内存中对应的缓冲区内,第三步是把缓冲区内容同步到磁盘。
    把对缓冲区的修改视为对数据库的修改,事务有两种方式来修改数据库,一中是等到commit后一起修改,另一种是当数据库处于active状态时直接修改缓冲区。
    在修改数据库之前必须创建log record。 写操作的record有四个字段,分别标识事务,item,旧值和新值。 其它的record对应start,commit和abort操作。
    进入committed状态之前必须确保事务中所有操作对应的log record都写入了磁盘中,commit操作对应的log record 保存到磁盘中标志着事务真正进入committed状态。
    recovery主要应用在两种情况,第一种是commit后修改只存在于缓冲区内,并没有同步到磁盘中。第二种是事务在active状态进行的修改因为一些原因需要abort。

    redo(T)是指系统崩溃之后重新利用record log更新items, undo(T)操作负责把items的值恢复成record中记录的旧值,在执行undo的时候会产生新的record log (redo-only log records), 但是这些新record中不需要记录旧值。被写入磁盘标志着undo操作的完成。undo发生在正常运行时的回滚和系统崩溃时的回滚操作。每个事务最终都会以 或 结束。当系统崩溃发生时,如果事务的日志中存在 但是不存在 或 ,则触发undo操作。如果日志中存在,并且存在 或 则触发redo操作。

    checkpoint 操作负责首先把所有log record 同步到磁盘中,然后同步磁盘缓冲区,最后把checkpoint操作对应的record 写入到磁盘中, 其中L是处于进入checkoint时处于活跃状态(未创建 或 )的事务的集合。checkpoint操作进行期间,不能进行任何更新操作,例如修改缓冲区内容,或者创建新log record。 在系统崩溃之后检查log,并找到最后一个, 在 之前所有 或 对应的事务所做的修改肯定都已经同步到了磁盘中, 所以只需要检查L中的事务以及在之后start的事务, 如果没日志中不存在对应的 或 ,则触发undo操作,否则触发redo操作。

  • 恢复算法
    恢复算法分为两个阶段,在redo阶段,首先用最后一个中的L初始化一个undo_list,用于后面的undo阶段。然后按顺序扫描后面的record。 如果碰到一个写操作的record,就执行redo操作, 如果碰到一个, 把Ti加入undo_list, 如果碰到 or , 则将Ti从undo_List中移除。在undo阶段,从后往前扫描所有llog record, 只要遇到一条undo_List中事务对应的record,就执行undo操作,当遇到undolist中事务对应的时,写入并把Ti从undo_list中移除。有了redo和undo配合,item的值最终会和committed事务中的值保持一致

    write-ahead logging (WAL) rule: 在把主存中一块数据同步到磁盘之前,必须保证其中数据对应的所有log record都已经写入了磁盘。
    no-force policy 是指允许事务在commit之前,有修改了但还没有同步到磁盘的数据块。steal policy 是指允许一块数据同步到磁盘,即使它被一个处于active状态的事务修改过。 只要满足WAL, 恢复算法在no-force和steal policy下都是正确的。在把某一数据块同步到磁盘时,应该对其加排它锁,防止其他事务写入导致不满足WAL。

    之前的checkpoint是当全部的数据块同步到磁盘后,再写入。但是将全部数据块写入的过程很耗时,并且会阻塞所有的更新操作直到被写入。 在fuzzy checkpoint中,允许先写入,再同步数据块, 但这样的结果是可能产生incomplete的checkpoint, fuzzy checkpoint的做法是在磁盘中的一个固定位置保存最后一个完整地checkpoint的log地址, 用以区分当前的checkpoint是完整地还是不完整的。 当前checkpoint只有在把数据块全部同步之后才会更新这个固定位置保存的值。

    dump 是值为了防止Non-Volatile Storage失效,而周期性进行的数据库整体备份操作。 它的过程和checkpoint类似,但多了一步拷贝数据库,并且在log 中留下一条 。当磁盘中某一块失效后,然后redo所有针对这一块,并且在之后进行的操作。 这里不需要undo,因为这是系统并没有崩溃,内存中未完成的事务可以继续进行。

    高可用:
    若同时存在primary site和 backup site, primary在更新后会把对应的log records发送给backup site. 在backup site上不能进行更新,只能进行通过快照读读取最新的值。

  • logical undo logging
    b+树中的锁时不用遵循二段锁协议的,只要插入或删除操作完成后马上可以释放(Early Lock Release)。
    在这里插入图片描述

    如图,T1的某操作利用新键值对(K5, RID 7)替换叶子节点Index I9上的旧值,会产生两条physical record, 其中X和X+8代表在叶子节点中位置。由于Early Lock Release, T1操作结束后T2可以马上获取B+树中的锁,并插入新的键值对,这回导致K5和RID7的位置发生改变,而两条physical record 应该作废,所以在日志中加入logical record , 表示undo只需要把l5和RID7从节点中删除即可。

    在回滚时,从后往前扫描,如果没有operation-end的日志记录,则进行Physical Undo,因为只有操作结束之后才会释放锁,其它事务不可能修改其锁定的数据。如果没有发现operation-end的日志记录,则进行logical undo, 直到遇到begin operation,中间的physical record 全部跳过。

    在mysql中,当update 的字段有索引时有可能发生就地更新,也有可能先delete再insert

  • ARIES

    在ARIES中,每个record都有一个递增的编号LSN。 每一个page保存一个Pagelsn(磁盘上),表示最新的对这个page更新过的record的LSN。 checkpoint log record中除了原来的undo-list, 还多出来一个DirtyPageTable,用来记录 checkpoint log提交时的所有未同步的脏页。其中每个脏页有一个recLSN, 第一次让该page变dirty的LSN。 DirtyPageTable是在数据库正常运行时生成的,当脏页同步到磁盘后,就将其从DPT中移除。

    ARIES分成三个阶段, 在analysis阶段, 首先把redoLSN设置为DPT中最小的RecLSN, 小于redoLSN的不用进行redo,因为更新的值已经在磁盘上。 然后从checkpoint往后扫描,遇到新的事务就将其加入undo-list,如果该事务对应的page不在DPT,也将该页加入DPT.

    在redo阶段,从redoLSN开始扫描已提交事务对应的record,满足下面一个条件就跳过:
    该page不在DPT中。
    该page在DPT中,但是这条update record的LSN比该page的recLSN小。(说明使该page变dirty的record还没出现,在recLSN之前的数据都已经落盘了)
    该page的pageLSN大于当前record的LSN。(该page是从disk中读上来的,说明pageLSN之前的操作都已经落盘了,不需要在redo)

    在undo阶段,从后往前扫描,遇到一条update log record就进行undo,并产生一条compensation log records (CLRs), 可以通过update log record的PrevLSN找到该事务的下一条应该undo 的record 。 如果遇到一条CLR,用其中的旧值更新后,可以通过UndoNextLSN字段找到该CLR对应update log record的PrevLSN。 下一条待执行record,是所有事务的preLSN或UndoNEXTLSN中最大的一个对应的record。

在这里插入图片描述

并发一致性

  1. 脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

  2. 不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

  3. 幻想读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

两段式锁

在对任何数据进行读、写操作之前,事务首先要申请并获得对该数据的封锁;
在释放一个封锁之后,事务不再申请和获得任何其他封锁。
这样的方式尽管无法避免死锁。可是两段锁协议能够保证事务的并发调度是串行化
(串行化非常重要,尤其是在数据恢复和备份的时候)的。

RC 和 RR

  • 提交读(READ COMMITTED)

    1. 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
  • 可重复读(REPEATABLE READ)

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

表锁

意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。

共享锁(S):

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

排他锁(X):

SELECT * FROM table_name WHERE ... FOR UPDATE

行锁

  • InnoDB有三种行锁的算法:

    1. Record Lock:单个行记录上的锁。

    2. Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

    3. Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

MVCC

mvcc 的意义就是用乐观锁的方式,而不是通过加锁的方式实现了可重复读,但是不能解决幻读,解决幻读只能用串行化或者使用间隙锁

整体流程
我们在了解了隐式字段,undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了
整体的流程是怎么样的呢?我们可以模拟一下

  • 当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设我们称为trx_list
    在这里插入图片描述

  • Read View不仅仅会通过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id(记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1,我更倾向于后者;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下图
    在这里插入图片描述

  • 我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。
    在这里插入图片描述

  • 所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

    也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

MVCC相关问题

  • RR是如何在RC级的基础上解决不可重复读的?
    当前读和快照读在RR级别下的区别:
    表1:
    在这里插入图片描述
    表2:
    在这里插入图片描述
    而在表2这里的顺序中,事务B在事务A提交后的快照读和当前读都是实时的新数据400,这是为什么呢?
    这里与上表的唯一区别仅仅是表1的事务B在事务A修改金额前快照读过一次金额数据,而表2的事务B在事务A修改金额前没有进行过快照读。
    所以我们知道事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力
    我们这里测试的是更新,同时删除和更新也是一样的,如果事务B的快照读是在事务A操作之后进行的,事务B的快照读也是能读取到最新的数据的

  • RC,RR级别下的InnoDB快照读有什么不同?
    正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
    在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
    即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
    而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
    总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View

redo undo

Redo记录以顺序附加的形式记录新值,如某条记录<T,X,V>,表示事物T将新值V存储到数据库元素X,新值可以保证重做;
而Undo记录通常以随机操作的形式记录旧值,如某条记录<T1,Y,9>,表示事物T1对Y进行了修改,修改前Y的值是9,旧值能用于撤销,也能供其他事务读取。
Redo用来保证事务的原子性和持久性,Undo能保证事务的一致性,两者也是系统恢复的基础前提

redo

Innodb执行事务时会拆分为很多小事务,每个小事务产生某条Redo记录。
而通过几个数据库原语能更一般性的描述Redo记录:
Input(X):将X值从存储介质读入缓冲区
Read(X,t):将X值从缓冲区读入事务内的变量t,如果缓冲中不存在,则触发Input
Write(X,t): 将事务内的t写入到缓冲区X块,如果缓冲中X不存在,则触发Input(X),再完成write
Output(X):将缓冲区X写入到存储中

在这里插入图片描述

chekpoint

一旦事务commit日志记录写入磁盘,逻辑上而言本事务的Undo记录在恢复时已经不需要,在commit时可以删除之前的Undo记录。但由于多事务同时执行的原因,有时候不能这样做,尽管本事务已经commit,但其他事务可能在使用Undo中的旧值。为此需要checkpoint来处理这些当前活跃的事务。

检查点技术可分为简单检查点与更优化的非静止检查点。在一个简单检查点中有如下过程:

  1. 停止接受新的事务
  2. 等待当前所有活跃事务完成或中止,并在日志中写入commit或abort记录。
    如果不停止,假设写入ckpt时a事务已经提交,而b事务对X的修改并未提交,那么 b事务时只能从a修改后的X值上进行redo
  3. 将当前位于内存的日志,将缓冲块刷新到磁盘
  4. 写入日志记录,并再次刷新到磁盘
  5. 重新开始接受事务

系统恢复时,可以从日志尾端反向搜索,直到找到第一个标志,而没有必要处理之前的记录。

当使用非静止检查点技术,恢复时的也是从日志尾向前扫描,可能先遇到标志,也可能先遇到标志:
1.先遇到<START CKPT(t1,…tn)>时,说明系统在检查点过程中崩溃,未完成事务包括2部分:(t1,…tn)记录的部分及标志后新进入部分。这部分事务中最早那个事务的开始点就是扫描的截止点,再往前可以不必扫描。

2.先遇到,说明系统完成了上一个周期的检查点,新的检查点还没开始。需要处理2部分事务:标志之后到系统崩溃时这段时间内的事务及上一个,区间内新接受的事务。为此扫描到上一个检查点<START CKPT()>就可以截止。

多说一句,很容易发现,非静止检查点是将一个点扩展为一个处理区间了,类似的设计其他技术也有,如JVM的GC处理,从stop the world到安全区的处理[1]。

undo

在这里插入图片描述

Undo是逻辑日志,并不幂等,在撤销时,根据undo记录进行补偿操作。Undo本身也产生redo记录。通过Undo日志数据库可以实现MVCC。
Undo保证了事务失败或主动abort时的机能,除此之外,系统崩溃恢复时,也确保数据库状态能恢复到一致。

系统恢复时,Undo需要Redo的配合来实现,或者说二者是一套机制的两个方面。因为在Redo日志有commit或abort记录的事务是无需undo的。
假设以静止的检查点为日志类型,以<CKPT (t0,…,tn)>做检查点,期间不接受新事务进入,整个Undo过程可以描述如下:
1.以进行检查点时记录的活跃事务(t0,…,tn)为undo-list
2.在Redo阶段,发现一条<T,START>记录,就将T加入到undo-list
3.在redo阶段,发现一条<T,END>或<T,ABORT>记录,就将T从undo-list删除
4.此时undo-list中的事务都是些未提交也没回滚的事务,系统如同普通的事务回滚样进行具体的undo操作
5.当undo-list中发现<T,START>时,说明完成了具体的回滚操作,系统写入一个<T,ABORT>记录,并从undo-list中删除T。
6.直到undo-list为空,撤销阶段完成

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值