精通Java事务编程(2)-弱隔离级别之已提交读

简介: 若两个事务不触及相同数据,即无数据依赖关系,则它们能安全并行运行。只有当:某事务读取由另一个事务同时修改的数据时或两个事务同时修改相同数据

若两个事务不触及相同数据,即无数据依赖关系,则它们能安全并行运行。只有当:


某事务读取由另一个事务同时修改的数据时

或两个事务同时修改相同数据

才会出现并发问题。


并发 BUG 很难通过测试找到,因为这样的错误只有在特殊时序下才会触发。这样的时序问题可能非常少发生,通常很难重现 1。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问DB。只有一个用户访问数据时,应用开发就够麻烦了,多用户并发更困难,每个数据都可能被多个用户修改。


因此,DB一直试图通过【事务隔离】来隐藏内部的各种并发问题。理论上,隔离是假装没有并发发生,让程序员生活不再加班。而可串行化隔离级别就是DB保证事务最终效果如同串行执行。但可串行化会有极大性能损失,许多DB不愿意牺牲性能,所以倾向较弱隔离级别,防止某些而非全部并发问题。


弱隔离导致的并发性错误不仅是理论问题,它们已造成很多资损,审计调查和客户数据破坏。比起盲目依赖工具,不如对各种并发问题及如何防止有深入理解,构建可靠、正确的应用。


2.1 读已提交(Read Committed)


最基本的事务隔离级别2,提供如下保证:


读DB时,只能看到已成功提交的数据(防止脏读)

写DB时,只会覆盖已成功写入的数据(防止脏写)

2.1.1 防止脏读

某事务已完成部分数据写,但事务尚未提交或中止。另一个事务可以看到尚未提交的数据吗?是,则为脏读。


读已提交的事务必须防止脏读,即事务的任何写只有在事务成功提交后才能被其他人看到。如图-4,用户1设置x=3,但用户2get x 仍旧返回旧值2(用户1还未提交)。

2.png



防止脏读的意义

若事务需更新多个对象,脏读代表另一个事务可能只看到部分更新。如图-2,用户看到新的未读邮件,但看不到更新的计数器。这就是电邮脏读。看到部分更新的数据会让用户困惑

若事务中止,则所有写都得回滚(如图-3)。若发生脏读,意味着一个事务可能看到稍后需回滚的数据,即从未实际提交给DB的数据。

2.1.2 防止脏写

若两个事务同时尝试更新DB的相同对象,不知道写的顺序如何,但通常认为后写入会覆盖前写入。


但若先前写入是尚未提交事务的一部分,是否还被覆盖?是,则为脏写。RC下的事务可以防止脏写,一般就是延迟后写,直到前写事务完成提交或中止。


防止脏写可避免如下并发问题:


若事务需更新多个对象,如图-5的二手车销售网站,Alice 和 Bob 同时购买同一辆车。购买汽车需两次DB写入:网站上的商品列表需更新,以反映买家购买,销售发票需发给买家。图-5的销售属于 Bob(因为他成功更新车辆列表),但发票却寄给了爱丽丝(因为她成功地先更新了发票表)。RC就能避免此类事故。

但RC不能防止图-1的计数器增量竞争。它的第二次写入确实发生在第一个事务提交后,所以不是脏写,但结果仍不正确。防止更新丢失中将讨论如何修正

1.png


2.1.3 实现原理

互联网主流隔离级别,Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL和其他许多DB的默认设置。


2.1.3.1 防脏写

DB一般通过 行锁(row-level lock)防脏写:当事务想修改某对象(如行或文档),必须首先获得该对象的锁。然后一直持有直到事务提交(或中止)。一次只有一个事务可持有特定对象的锁;若另一事务要更新同一对象,则必须等到前面事务提交或中止后,才能获取锁并继续。这是RC模式(或更高隔离级别)的DB自动完成的锁定。


2.1.3.2 防脏读

① 方案一

使用相同的锁,所有想读取该对象的事务必须先申请锁,事务完成后释放锁。确保不会发生读取脏的、未提交的值(因为锁在此期间,一直由一个事务持有)。


但要求读锁,实践中效果不好。因为耗时长的写事务会迫使许多只读事务等到这个慢写入事务完成。这会损失只读事务的响应延迟,且可操作性差:由于读锁,应用的局部性能问题可能由于连锁效应,扩散导致其他部分都受影响。


② 方案二

因此,大多DB 3 使用图-4方案防脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。


轶事:偶然出现的瞬时错误有时称为 Heisenbug,而确定性的问题对应地称为 Bohrbugs ↩︎


某些数据库支持甚至更弱的隔离级别,称为 读未提交(Read uncommitted)。它可以防止脏写,但不防止脏读。 ↩︎


唯一在读已提交隔离级别使用读锁的主流数据库是使用 read_committed_snapshot = off 配置的 IBM DB2 和 Microsoft SQL Server。 ↩︎

目录
相关文章
|
4天前
|
缓存 Java UED
Java中的多线程编程:从基础到实践
【10月更文挑战第13天】 Java作为一门跨平台的编程语言,其强大的多线程能力一直是其核心优势之一。本文将从最基础的概念讲起,逐步深入探讨Java多线程的实现方式及其应用场景,通过实例讲解帮助读者更好地理解和应用这一技术。
22 3
|
4天前
|
Java 开发者
在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。
【10月更文挑战第13天】在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。本文将带你深入了解Java命名规则,包括标识符的基本规则、变量和方法的命名方式、常量的命名习惯以及如何避免关键字冲突,通过实例解析,助你写出更规范、优雅的代码。
26 3
|
4天前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
12 3
|
4天前
|
Java 程序员 编译器
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。本文通过示例详细解析了保留字的定义、作用及与自定义标识符的区别,帮助开发者避免因误用保留字而导致的编译错误,确保代码的正确性和可读性。
16 3
|
3天前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
在Java并发编程中,`final`关键字不仅用于修饰变量、方法和类,还在多线程环境中确保对象状态的可见性和不变性。本文深入探讨了`final`关键字的作用,特别是其在final域重排序规则中的应用,以及如何防止对象的“部分创建”问题,确保线程安全。通过具体示例,文章详细解析了final域的写入和读取操作的重排序规则,以及这些规则在不同处理器上的实现差异。
了解final关键字在Java并发编程领域的作用吗?
|
4天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。
|
8天前
|
Java 调度 UED
深入理解Java中的多线程与并发机制
本文将详细探讨Java中多线程的概念、实现方式及并发机制,包括线程的生命周期、同步与锁机制以及高级并发工具。通过实例代码演示,帮助读者理解如何在Java中有效地处理多线程和并发问题,提高程序的性能和响应能力。
|
6天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
13 2
|
7天前
|
存储 安全 Java
Java-如何保证线程安全?
【10月更文挑战第10天】
|
8天前
|
Java