事务和并发I
1 导言
在现实中,我们通常不会一次只有一个人访问数据库。许多用户可以一次向数据库发出请求,这可能会导致并发问题。当一个用户写入,然后另一个用户从同一资源读取时会发生什么?如果两个用户都试图写入同一资源,会怎么样?如果不小心,当几个用户同时使用数据库时,我们可能会遇到几个问题:
- 读取不一致:用户仅读取更新内容的一部分。
–用户1更新表1,然后更新表2。
–用户2读取表2(用户1尚未更新),然后读取表1(用户1已更新),因此它以中间状态读取数据库
- 丢失更新:两个用户试图更新同一记录,因此其中一个更新丢失。对于
例子:
–用户1将玩具的价格更新为价格*2。
–用户2将玩具的价格更新为价格+5,取消了用户1的更新
- 脏读:一个用户读取从未提交的更新。
–用户1更新玩具的价格,但这会被中止。
–用户2在回滚之前读取更新。
2 事务
我们解决这些问题的方法是定义一组关于操作的规则和保证。我们将通过使用事务来实现这一点。transaction1是应作为单个逻辑原子单元执行的多个动作的序列。交易保证ACID特性,以避免上述问题:
原子性:事务以两种方式结束:提交或中止。原子性意味着Xact中的所有动作都发生,或者没有发生。
一致性:如果DB开始一致,则在Xact结束时结束一致。
隔离:每个Xact的执行与其他执行隔离。实际上,DBMS将交错许多XACT的动作,而不是按顺序依次执行。DBMS将确保每个Xact的执行就像它自己运行一样。
持续性:如果Xact提交,其影响将持续。承诺的Xact的效果必须在失败后仍然存在。
3并发控制
在本说明中,我们将讨论如何强制执行事务的隔离属性(我们将在关于恢复的说明中了解如何强制执行其他属性)。为此,我们将分析显示操作执行顺序的事务调度。这些操作包括:开始、读取、写入、提交和中止。确保隔离的最简单方法是在开始下一个事务的操作之前,将一个事务中的所有操作运行到完成。这被称为序列计划。例如,以下计划是串行计划,因为T1的操作在T2运行之前完全运行。
然而,这些调度的问题在于,在启动另一个事务之前等待整个事务完成是没有效率的。理想情况下,我们希望获得与串行调度相同的结果(因为我们知道串行调度是正确的),同时也获得同时运行调度的性能优势。基本上,我们正在寻找一个等同于串行计划的计划。对于等效的明细表,它们必须满足以下三条规则:
它们涉及相同的事务
单个事务中的操作顺序相同
它们都使数据库处于相同的状态
如果我们找到一个结果等同于串行调度的调度,我们将该调度称为可串行化。例如,以下计划是可序列化的,因为它与上面的计划等效。您可以执行以下计划,并看到资源A和资源B最终具有与上述串行计划相同的值。
现在的问题是:我们如何确保两个调度使数据库处于相同的最终状态,而不运行整个调度以查看结果?我们可以通过寻找冲突的操作来做到这一点。对于两个冲突的操作,它们必须满足以下三个规则:
操作来自不同的事务
两个事务在同一资源上运行
至少一个操作是写操作
然后我们检查两个调度是否以相同的方式对每对冲突操作排序。如果他们这样做了,我们可以确定数据库最终将处于相同的最终状态。当两个调度以相同的方式排列其冲突操作时,称为冲突等价,这是比等价更强的条件。
现在,我们有了一种方法来确保两个调度以相同的最终状态离开数据库,我们可以在不运行整个调度的情况下检查一个调度是否与串行调度冲突。我们称之为冲突的调度相当于某些串行调度冲突可串行化。注意:如果调度S是冲突可序列化的,则意味着它是可序列化的。
3.1冲突依赖图
现在,我们有了一种检查时间表是否可序列化的方法!我们可以检查调度是否与某个串行调度冲突等价,因为冲突可序列化意味着可序列化。我们可以通过构建依赖关系图来检查冲突序列化性。依赖关系图具有以下结构:
每个Xact一个节点
从$T_i$到$T_j$的边缘,如果:
–$Ti$的操作$O_i$与$T_j$的操作$O_j$冲突
–$O_i$出现在时间表中的时间早于$O_j$
当且仅当其依赖关系图是非循环的时,调度是可冲突序列化的。所以我们所要做的就是检查图是否是非循环的,以确定它是可序列化的!
让我们看两个例子:
- 以下计划是可冲突序列化的,冲突图是非循环的。有两种相互冲突的操作:
–T1读取A,然后T2写入A。因此,从T1到T2将有一个边沿。
–T1向A写入,然后T2从A读取。由于T1到T2之间已经存在一条边,因此我们不必再次添加该边。
- 以下计划不可冲突序列化,且冲突图不是非循环的。一些冲突操作:
–T1读取A,然后T2写入A。因此,从T1到T2将有一个边沿。
–T2写入B,然后T1读取B。因此,T2到T1之间会有一条边沿。
4.结论
在本文中,我们删除了迄今为止的天真假设,即一次只允许一个用户访问数据库。我们讨论了如果我们的数据库不能保证ACID性质,可能出现的潜在异常。我们了解了事务如何是一种强大的机制,用于封装应作为单个逻辑原子单元执行的一系列操作。在下一个注释中,我们将讨论如何为事务调度实际实施冲突序列化。