精通Java事务编程(7)-可串行化隔离级别之两阶段锁定(2PL,two-phase locking)

简介: 近30年,DB只有一种广泛使用的串行化算法:两阶段加锁 12PL不是2PC请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但是完全不同概念

近30年,DB只有一种广泛使用的串行化算法:两阶段加锁 1


2PL不是2PC

请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但是完全不同概念


之前我们知道,加锁可防止脏写:即若两个事务同时尝试写入同一对象,则锁可确保第二个写必须等第一个写完成事务(中止或提交)才能继续。


两阶段锁定类似,但锁的强制性更高。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写,就得加锁独占访问:


若事务 A 已读某对象,此时B想写该对象,则必须等A提交或中止才能继续,这确保 B 不能在 A 执行过程的中间意外改变对象

若事务 A 已写某对象,此时 B 想读该对象,则 B 必须等 A 提交或中止才能继续,像图-1读取旧版本的对象在 2PL 下不可接受

2PL不仅在并发写互斥,读写之间也互斥。快照级别隔离是读写不互斥,这是 2PL 和快照隔离的关键区别。且因 2PL 提供串行化,可防止前文讨论的所有竞争条件,包括丢失更新和写倾斜。


3.2.1 实现原理


2PL已在:


MySQL(InnoDB)和 SQL Server 实现可串行化

DB2 中的可重复读

读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可处于 共享模式或独占模式,使用如下:


若事务要读对象,则须先共享模式获取锁。允许多事务同时持有一个对象的共享锁。但若某事务已持有对象的独占锁,则其它事务必须等待

若事务要写对象,须以独占模式获取锁。禁止其他事务同时持有锁(无论共享模式or独占模式),即若对象上存在锁,则写事务必须等待

若事务先读再写对象,则需将其共享锁升级为独占锁。升级锁的流程和直接获得独占锁相同

事务获得锁后,必须一直持有锁直到事务结束。这就是 “两阶段” 名字来源:第一阶段(当事务正在执行时)获取锁,第二阶段(在事务结束时)释放所有的锁。

由于使用了这么多锁,很容易死锁:如事务A等待B释放锁,而B等A释放锁。DB会自动检测事务之间死锁,并强行中止一个。被中止的事务需由应用层重试。


3.2.2 性能

其巨大缺点及1970s以来没有被广泛使用的原因还是其性能:事务吞吐量和查询响应时间比弱隔离级别下差太多。


部分因为获取、释放锁开销,但更重要是并发性降低。按设计,若两个并发事务试图做任何可能导致竞争条件的事情,则其一必须等待另一完成。


传统关系DB不限制事务的执行时间,因为它们是为等待人类输入的交互式应用而设计。结合2PL,最终结果是,当一个事务还需等待另一事务时,则最终等待时间几乎无上限。即使能保证所有事务都很短,若有多事务同时访问同一对象,会形成一个等待队列,事务需要等待前面事务完成后才能继续。


因此,2PL DB的访问延迟具有极大不确定性,若负载存在严重竞争,以百分比方式观察延迟指标会发现很缓慢。可能某缓慢事务或一个访问大量数据并获取许多锁的事务,就能把系统其他部分拖慢。当需要稳定操作时,这种不稳定性是致命的。


基于锁实现的RX也可能死锁,但 2PL 下取决于事务的访问模式,死锁更频繁。这可能是一个额外的性能问题:当事务由于死锁而被中止并被重试时,应用层就需从头重试。若死锁频繁,则最后性能和效率必然大打折扣。


谓词锁

对加锁,忽略了一个微妙但重要的细节。在写倾斜幻读中的幻读问题,即一个事务改变另一个事务的查询结果。可串行化隔离也必须防止幻读。


会议室预订案例,若事务在查询某时间段内一个房间的预订情况,则另一个事务不能同时插入或更新同一时间段内该房间的预订 (可同时插入其他房间的预订或在不影响另一个预定的条件下预定同一房间的其他时间段)。


要实现就需要谓词锁(predicate lock),类似共享/独占锁,但不属于特定对象(如表的某行),而是作用于所有符合某些搜索条件的对象,如:


SELECT * FROM bookings

WHERE room_id = 123

AND end_time > '2018-01-01 12:00'

AND start_time < '2018-01-01 13:00';


谓词锁会限制如下访问:


若事务A想读取某些满足匹配条件的对象,如SELECT 查询,必须获取查询条件上的 共享谓词锁(shared-mode predicate lock)。若事务B持有任何满足这一查询条件对象的独占锁,则A必须等到B释放锁后才能继续执行查询

若事务A想插入、更新或删除任何对象,须先检查所有旧值或新值是否和现有谓词锁匹配。若B持有匹配的谓词锁,则A须等B完成提交或中止后才能继续

关键在于,谓词锁甚至适用于数据库中尚不存在,但将来可能会添加的对象(幻象)。如果两阶段锁定包含谓词锁,则数据库将阻止所有形式的写入偏差和其他竞争条件,因此其隔离实现了可串行化。


索引范围锁

但谓词锁性能不佳:若活跃事务持有很多锁,则检查匹配的锁很耗时。因此,大多2PL DB实际上实现的是索引范围锁(index-range locking,也称为 next-key locking),本质是对谓词锁的简化或近似。


简化谓词锁的方式是扩大其保护的对象,这肯定是安全的。如若你有12:00~13:00预订 123 号房间的谓词锁,则锁定123号房间的所有时间段或锁定12:00~13:00时间段的所有房间就是安全的近似。这样,任何与原始谓词锁冲突的操作肯定也和近似后的区间锁相冲突。


房间预订DB,一般在:


room_id 列建索引

并/或在 start_time 和 end_time 上有索引

否则前面的查询在大型DB上的速度会很慢。


假设索引位于 room_id 上,并且数据库使用此索引查找 123 号房间的现有预订。现在数据库可以简单地将共享锁附加到这个索引项上,指示事务已搜索 123 号房间用于预订。

或者,若DB使用基于时间的索引来查找预订,则可将共享锁附加到该索引中的一系列值,指示事务已搜索了该时间段内的所有值 (如直到2023年 1 月 1日)

无论哪种,查询条件的近似值都附加到某个索引上。若另一事务想插入、更新或删除同一房间和/或重叠时间段的预订,则须更新这些索引的相同部分,就一定会和共享锁冲突,将被迫等到共享锁被释放。


这有效防止了幻读和写倾斜。索引范围锁并不像谓词锁精确(会锁定更大范围的对象,超出维持可串行化所必需的范围),但由于开销低得多,是很好的折衷方案。


若无可挂载范围锁的索引,则DB可退化到使用整表的共享锁。这对性能不利,会阻止所有其他事务的写,但这是一个安全的回退位置。


有时称为 严格两阶段锁定(SS2PL, strong strict two-phase locking),以便和其他 2PL 变体区分。

目录
相关文章
|
9天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
10天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
7天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
23 2
|
11天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
20天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
7天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
27 9
|
10天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
24 3
|
9天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
10天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
21 1