content
- 并发控制理论基础
- 基于锁的并发控制
- 基于时间戳的并发控制
并发控制理论基础
▍知识点
- 并发设计的目标是,既要考虑事务的可串行性,又要考虑并发度
- 并发控制中的典型问题是数据的一致性
▍读集 / 写集
事务T: x = x + 1; y = y - 1;
表示为: R(x)W(x)R(y)W(y)
读集R(T) = {x, y}
写集W(T) = {x, y}
▍三种错误
- 脏读——读取为提交的数据——A事务不知道B事务即将回滚,读取了脏数据
- 不可重复读——前后两次读取数据本身不一致——A事务比较大,在前后两次读取之间,数据被B事务修改
- 幻读——前后两次读取数据总量不一致——A事务比较大,在前后两次统计数据之间,B事务增加了几条数据,出现了“凭空多出几条数据”的幻觉
▍分布式中的并发控制
- 集中式数据库系统中,冲突的事务串行执行,不冲突的事务并行执行
- 分布式数据库系统中,对于一个场地,冲突的事务串行执行,不冲突的事务并行执行
基于锁的并发控制
▍核心思想
- 事务在对某一数据项操作之前,必须申请先对该数据项加锁
- 如果此时数据项已经加了与此次申请不相容的锁,则该事务必须等待,知道数据项解锁
▍锁的相容性
- 共享锁(shared):读锁之间是共享的
- 排他锁(exclusive):写锁与任何一个其他锁是排斥的
▍锁的粒度/并发度
- 被封锁的数据对象的单位称为锁的粒度
- 锁的粒度可以是属性(即字段)、元组(即记录)、关系(即文件)、甚至整个数据库
- 并发度与粒度成反比——很好理解
▍两段封锁协议(2PL)
- 就是上面讲过的,加锁操作与解锁操作分两阶段完成
- 分类:基本的两段封锁协议 / 严格的两段封锁协议
基于时间戳的并发控制
▍核心思想
- 每个事务被赋予一个唯一时间戳(可以理解为优先级),事务的执行顺序按时间戳顺序串行执行
- 如果发生冲突,撤销并重启其中一个事务,并为其赋予新的时间戳
▍时间戳(TimeStamp)
- 是由系统赋予该事务的全局唯一标识
- 时间戳是单调递增的,即是一个增量
- 时间戳并不与现实中的日期时间相关,时间是数据库内部流动的时间
- 分布式系统中,维护一个全局的时间戳比较困难,因此每个场地维护一个时间戳
- 分布式系统中,时间戳是一个<本场地时间戳,场地>的二元组——这样才能全局唯一
▍TO排序规则
设数据项x
设读时间戳read_ts(x)
,写时间戳write_ts(x)
设预写时间戳pre_ts(x)
设读队列queue_read(x)
,写队列queue_write(x)
,预写队列queue_pre(x)
设读队列最小时间戳min_read_ts(x)
,预写队列最小时间戳min_pre_ts(x)
▶ 读操作
- 若【读时间戳 < 写时间戳】,废弃
- 若【读时间戳 > 预写队列最小时间戳】,缓存到读队列
- 否则,直接读
▶ 预写操作
- 若【预写时间戳 < 读/写时间戳】,废弃
- 否则,缓存到预写队列
▶ 写操作
- 若【写时间戳 > 读队列最小时间戳/预写队列最小时间戳】,缓存到写队列
- 否则,直接写
// 读操作
if(read_ts < write_ts) {
abort();
} else if(read_ts > min_pre_ts) {
queue_read.add();
} else {
read();
}
// 预写操作
if(pre_ts < read_ts || pre_ts < write_ts) {
abort();
} else {
queue_pre.add();
}
// 写操作
if(write_ts > min_read_ts || write_ts > min_pre_ts) {
queue_write.add();
} else {
write();
}
M o r e More More