科普文:软件架构数据库系列之【关系型数据库和非关系型数据库的MVCC实现原理方式梳理】-CSDN博客
科普文:软件架构数据库系列之【MySQL引擎Innodb的MVCC特性】-CSDN博客
科普文:软件架构数据库系列之【为啥建议将MySQL 隔离级别从RR改成RC】-CSDN博客
聊完MVCC,再来看看数据库的并发控制。
数据库系统并发控制原理
数据库访问是通过事务完成的,首先我们搞清楚什么是事务?
- 被视为整体的一组工作
- 这组工作要么完全完成,要么全部不完成,不存在部分完成情况
真实生活中以转账说明事务:
- 第一步,从账户A中减去X元金额;
- 第二步,将X元金额存入账户B
这些多步操作必须全部完整完成,不能半途而废。数据库事务的工作方式与此相同。他们能保证,无论发生什么事情,数据的操作处理都被看成是原子的(你永远不会看到“转变一半”的情况)。
原子性是DBMS维护ACID属性的一部分,ACID包括:
- Atomicity原子性: 事务中所有操作要么全部发生,要么全部不。
- Consistency一致性: 数据库从一致状态开始到一致状态结束。
- Isolation隔离性: 一个事务的执行是隔离于其他事务的。
- Durability :如果事务已经确认提交,其效果是持久保存在数据库中。
那么在并发访问数据库的情况下会发生什么问题?并发流程可能会改变隔离性和一致性这两个属性。让我们假设两个用户预订了一架飞机的同一个座位:
- 客户1 发现一个空座位
- 客户2 发现同样的空座位
- 客户1预订了这个座位
- 客户2也同时预订了这个座位
我们将事务的顺序执行称为schedule调度,它表示基于时间先后的一系列事务执行,在这里客户1和客户2分别存在两个事务,这个两个事务同时发生,我们需要通过串行化serializability来保证事务正确执行,也就是说,需要通过一个调度来实现并发控制机制。
一个调度包含下面一些操作:
- read R(X)
- write W(X)
- commit (所有操作完成后,这些操作应该被确认和记录)
- abort (在执行一部分操作后,如果我们退出,所有操作应该没有一个被确认完成或记录保存)
为了有完整的调度,commit 或abort是被强制的。
serial schedule 串行调度是事务执行时间没有交织,所有操作都是顺序执行。
当下面条件满足,Conflicting operation冲突操作会出现:
- 它们属于不同的事务
- 它们得访问同一的对象X
- 至少其中一个操作是W(X) (对X的写操作)
让我们看看下面这些冲突操作:
- 写读冲突 Write-Read Conflict : 读到一个未提交uncommitted的数据
- 读写冲突 Read-Write Conflict : 首次读以后,再读已经被修改的数据。
- 写写冲突 Write-Write Conflict :其中一个写操作丢失
Write-Read Conflict, 也称为reading uncommitted data读未提交的数据或脏都dirty-read,当一个事务T2试图读取数据库对象A,但是其已经被事务T1修改,还没有提交确认,当T1继续其事务时,对象A的数据已经不一致了,如下图:
换句话说,脏读是当一个事务读取了被另外一个事务修改但是还没有提交确认的表记录。
读写冲突Read-Write Conflict, 也称为unrepeatable reads, ,当一个事务T1读两次数据库对象A,第一读以后事务T1等待事务T2完成,T2覆盖重写了对象A,当T1再次读A时,A数据存在两个不同版本,T1被强迫退出,因为这是不可重复的读。
真实案例:Bob和Alice是票务员,他们要预订一个表演票,只剩余一张了,Alice登录进入,发现这周票比较贵,犹豫了一下,而Bob登录进入后,就立即买了这张票,然后退出。Alice决定买这张票时,发现已经没有多余的票了。
写写冲突Write-Write Conflict, 也称为覆盖未提交数据overwriting uncommitted data,它是发生在当有一个丢失修改情况下,试图使这种场景串行只能是下面两者之一:要么是事务T1版本,要么是事务T2的版本:
一旦并发事务应用到数据库上,使用调度确保串行化,也就是执行是顺序的,不会有时间的重叠,除了串行,还有以下几种调度方式:
- ACA : 避免级联中断
- 可恢复性recoverable
- 严格调度strict schedule
如果一个调度是串行化的,最好的验证办法是通过依赖图。为了建立依赖图,我们需要下面过程:
- 1) 每个节点每个事务的代表
- 2)在事务Tx写入后还有另外的事务Ty读吗?如果是,从节点Tx画线到节点Ty
- 3)在事务Tx读取以后,是否有其他事务再进行写入呢?如果是,从Tx画线到Ty。
- 4)在Tx写入后有其他事务再次写入吗?如果是,从Tx画线到Ty。
如果退出事务,不要忘记移除你之前的画线。
为了偶一个串行化的调度,依赖图得是非循环的,也就是不能首尾循环依赖。比如下面的调度不是串行化:
而下面的调度是串行化的:
现在我们知道什么时候一个调度是 strict严格的?
当一个对象被事务T写入,那么一直到事务T确认提交或退出,这个对象就一直不能被再次读取或写入了。
那么一个调度如何避免级联退出呢?
当一个操作只能读取已经被提交确认的数据时,就可以避免级联一个个的事务退出。
那么如何知道一个调度是可恢复的?
对于每个事务,当事务Ty读取一些Tx事务的写入数据时,Tx的Commit提交操作出现在Ty的Commit提交操作之前。
总结:并发控制是通过串行化方式实现,这样能够确保任何非串行化的执行不会发生。当然主要问题是带来性能瓶颈。