防止丢失更新
如果应⽤从数据库中读取⼀些值,修改它并写回修改的值(读取-修改-写⼊序列),则可能会发⽣丢失更新的问题。如果两个事务同时执⾏,则其中⼀个的修改可能会丢失,因为第⼆个写⼊的内容并没有包括第⼀个事务的修改
原子写
原⼦操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为游标稳定性(cursor stability)【36,37】。另⼀个选择是简单地强制所有的原⼦操作在单⼀线程上执⾏。
显式锁定
如果数据库的内置原⼦操作没有提供必要的功能,防⽌丢失更新的另⼀个选择是让应⽤程序显式地锁定将要更新的对象。然后应⽤程序可以执⾏读取-修改-写⼊序列,如果任何其他事务尝试同时读取同⼀个对象,则强制等待,直到第⼀个读取-修改-写⼊序列完成
自动检测丢失的更新
比较并设置
如果内容已经更改并且不再与“旧内容”相匹配,则此更新将不起作⽤,因此您需要检查更新是否⽣效,必要时重试。但是,如果数据库允许 WHERE ⼦句从旧快照中读取,则此语句可能⽆法防⽌丢失更新,因为即使发⽣了另⼀个并发写⼊, WHERE 条件也可能为真。在依赖数据库的CAS操作前要检查其是否安全。
冲突解决和复制
写⼊偏差与幻读
前⾯的章节中,我们看到了脏写和丢失更新,当不同的事务并发地尝试写⼊相同的对象时,会出现两种竞争条件。为了避免数据损坏,这些竞争条件需要被阻⽌——既可以由数据库⾃动执⾏,也可以通过锁和原⼦写操作这类⼿动安全措施来防⽌。
写偏差的特征
导致写⼊偏差的幻读
⼀个事务中的写⼊改变另⼀个事务的搜索查询的结果
物化冲突
现在,要创建预订的事务可以锁定( SELECT FOR UPDATE )表中与所需房间和时间段对应的⾏。在获得锁定之后,它可以检查重叠的预订并像以前⼀样插⼊新的预订。请注意,这个表并不是⽤来存储预订相关的信息——它完全就是⼀组锁,⽤于防⽌同时修改同⼀房间和时间范围内的预订。这种⽅法被称为物化冲突(materializing conflflicts)
可序列化
可序列化(Serializability)隔离通常被认为是最强的隔离级别。它保证即使事务可以并⾏执⾏,最终的结果也是⼀样的,就好像它们没有任何并发性,连续挨个执⾏⼀样。因此数据库保证,如果事务在单独运⾏时正常运⾏,则它们在并发运⾏时继续保持正确 —— 换句话说,数据库可以防⽌所有可能的竞争条件。
真的串行执行
在存储过程中封装事务
存储过程的优点和缺点
存储过程与内存存储,使得在单个线程上执⾏所有事务变得可⾏。由于不需要等待I/O,且避免了并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。
分区
如果你可以找到⼀种对数据集进⾏分区的⽅法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有⾃⼰独⽴运⾏的事务处理线程。在这种情况下可以为每个分区指派⼀个独⽴的CPU核,事务吞吐量就可以与CPU核数保持线性扩展
两阶段锁定
在2PL中,写⼊不仅会阻塞其他写⼊,也会阻塞读,反之亦然。快照隔离使得读不阻塞写,写也不阻塞读,这是2PL和快照隔离之间的关键区别。另⼀⽅⾯,因为2PL提供了可序列化的性质,它可以防⽌早先讨论的所有竞争条件,包括丢失更新和写⼊偏差。
实现两阶段锁
两阶段锁定的性能
谓词锁
谓词锁甚⾄适⽤于数据库中尚不存在,但将来可能会添加的对象(幻象)。如果两阶段锁定包含谓词锁,则数据库将阻⽌所有形式的写⼊偏差和其他竞争条件,因此其隔离实现了可串⾏化。
索引范围锁
⽆论哪种⽅式,搜索条件的近似值都附加到其中⼀个索引上。现在,如果另⼀个事务想要插⼊,更新或删除同⼀个房间和/或重叠时间段的预订,则它将不得不更新索引的相同部分。在这样做的过程中,它会遇到共享锁,它将被迫等到锁被释放。