精通Java事务编程(8)-可串行化隔离级别之可串行化的快照隔离

简介: 本系列文章描述了DB并发控制的黯淡:2PL虽保证了串行化,但性能和扩展不好性能良好的弱隔离级别,但易出现各种竞争条件(丢失更新,写倾斜,幻读

本系列文章描述了DB并发控制的黯淡:


2PL虽保证了串行化,但性能和扩展不好

性能良好的弱隔离级别,但易出现各种竞争条件(丢失更新,写倾斜,幻读

串行化的隔离级别和高性能就是相互矛盾的吗?也许不是,一个称为可串行化快照隔离(SSI, serializable snapshot isolation)算法很有前途。提供完整的可串行化保证,而性能与快照隔离相比只有很小性能损失。 SSI在 2008 年首次被提出,如今既用于单节点DB(PostgreSQL9.1后的可串行化)和分布式DB(FoundationDB)。由于 SSI 与其他并发控制机制相比还很年轻,还在实践中证明自己。


3.3.1 悲观锁、乐观锁

两阶段锁是一种 悲观锁机制(pessimistic) ,其设计原则:若有操作可能出错(如与其他事务发生锁冲突),则直接放弃,等待直到绝对安全。和多线程编程中的互斥锁一致。


某种意义上,串行执行是很悲观的:事务期间,每个事务对整个DB(或DB的一个分区)持有互斥锁,我们只能假定每笔事务执行够快、短时持锁,来稍微弥补悲观色彩


相比之下,串行化快照隔离 是一种 乐观锁。如若存在潜在冲突,也不阻止事务,而是继续执行事务,寄希望于一切平安。而当事务想提交时(只有可串行化的事务才被允许提交。),DB会检查是否冲突(即违反隔离性原则):若是,则中止事务并重试。


乐观锁是古老的想法,其优缺点争论已久。若存在很多冲突,则性能不佳,大量事务需中止。若系统已接近最大吞吐量,重试的额外负载会使系统性能更差。


但若系统有足够性能提升空间,且事务之间争用不大,乐观锁比悲观锁更高效。可交换的原子操作能减少争用:如若多个事务同时增加某计数器,则应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要,所以并发增量可全部应用且无需冲突。


SSI基于快照隔离,即事务中的所有读取都基于DB的一致性快照(参阅本文的快照隔离、可重复读),这和早期乐观锁的主要区别。在快照隔离基础上,SSI新增一种算法检测写入之间的串行化冲突,并确定要中止哪些事务。


3.3.2 基于过期条件来决策

讨论写倾斜时,有一种场景:事务先从DB读一些数据,根据查询结果决定采取后续操作,如修改数据。但快照隔离下,数据可能在查询期间就已被其他事务修改,导致原事务在提交时决策的依据信息已变。


即事务基于某些前提而行动,事务开始时条件成立,如目前有两名医生正在值班,当事务提交时,数据可能已改变,前提已不再成立。


当应用执行查询时(如当前有多少医生在值班),DB本身不知道应用会如何使用该查询结果。为了安全,DB假定对该结果集的变更都可能会使该事务中的写无效。 即事务中的查询与写可能存在因果依赖关系。为提供可串行化隔离,DB必须检测事务是否会修改其它事务的查询结果,并在此情况下中止写事务。


DB如何知道查询结果是否已变?可分为如下case:


读取是否作用于一个(即将)过期的MVCC对象(读取之前已经有未提交的写入)

检查写是否影响即将完成的读取(读取后,又有新写入)

3.3.3 检测旧MVCC读取

快照隔离通常采用MVCC实现。当事务从 MVCC DB的一致性快照读时,会忽略创建快照时还没提交的事务写入。如图-10:


事务43认为 Aliceon_call = true ,因事务 42(修改 Alice 值班状态)还没提交

然而,事务43提交时,事务42已提交

即从快照读取时,被忽略的写已生效,直接导致事务43做决定的前提不再成立。

7.png



为防止这种异常,DB需跟踪一个事务由于MVCC可见性规则而被忽略的其它事务写。当事务提交时,DB会检查是否存在被忽略的写现在已被提交的,若是,则当前事务必须中止。


为何要等到提交?当检测到读旧值,为何不立即中止事务43,考虑如下场景:


若事务43是只读事务,则无需中止,因为无写倾斜风险


当事务43读DB 时,DB还不知道事务是否要稍后执行写操作


此外,事务42可能在事务43提交时,被中止或仍处于未被提交,因此读取的并非旧值


通过避免不必要的中止,SSI可高效支持那些需在一致性快照中运行很长时间的读事务。

3.3.4 检测写是否影响之前的读

读取数据后,另一个事务修改了数据:

6.png



2PL下讨论了索引范围锁,允许DB锁定和某查询匹配的所有行,如WHERE shift_id = 1234。可在此使用类似技术,只有一点差异:SSI锁不阻塞其他事务。


图-11中,事务42、43 都在班次1234查找值班医生。若 shift_id 有索引,则DB能使用索引项1234记录事务42、43读取这个数据的事实。若无索引,可在表级别跟踪此信息。该信息只需保留很小一段时间:当所有并发事务完成后,就能丢弃。


当另一事务写时,先检查索引,从而确定是否在最近存在一些读目标数据的其它事务。这过程类似在受影响字段范围上获取写锁,但锁不会阻塞其它事务读取,而是直到读事务提交时才进一步通知它们:所读到的数据已变化。


图-11中,事务43通知事务42其先前读已过时,反之亦然。事务42先提交并成功,尽管事务 43写影响了42 ,但因43没提交,所以写还没生效。当43提交时,来自42的冲突写入已被提交,所以43必须中止。


3.3.5 性能

许多工程细节会影响算法实际效果。如一个需权衡考虑的是跟踪事务的读、写的粒度:


若DB详细跟踪每个事务的操作(细粒度),确实能准确确定哪些事务需中止,但记录元数据的开销可能也很大

而跟踪速度更快时(粗粒度),可能导致更多不必要的事务中止

有的case读过期数据不会造成太大影响:这还是完全取决于具体场景,有时可确信执行结果都是可串行化的,PostgreSQL 使用该理论减少不必要的中止。


相比于2PL,可串行化快照隔离最大优点:事务无需阻塞等待其它事务所持有的锁。这和快照隔离一样,读写不互相阻塞。这使查询延迟更稳定、可预测。尤其是只读查询可运行在一致快照,无需任何锁,对读密集系统友好。


相比于串行执行,可串行化快照隔可突破单CPU核吞吐量限制:FoundationDB将检测到的串行化冲突分布在多台机器,从而提高吞吐量。即使数据可能跨多台机器分区,事务也能在保证可串行化隔离等级同时,读写多个分区中的数据。


事务中止率会显著影响SSI性能。如长时间读、写数据的事务很可能会发生冲突并中止,因此SSI要求读写型事务尽量短(但只读的长事务则没问题)。总体上,对慢事务,SSI比2PL或串行执行更能容忍。

目录
相关文章
|
9天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
8天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
7天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1天前
|
Java API 数据库
Java 反射机制:动态编程的 “魔法钥匙”
Java反射机制是允许程序在运行时访问类、方法和字段信息的强大工具,被誉为动态编程的“魔法钥匙”。通过反射,开发者可以创建更加灵活、可扩展的应用程序。
|
17天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
8天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
26天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
13天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
39 9
|
10天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
16天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
下一篇
无影云桌面