精通Java事务编程(5)-写倾斜与幻读

简介: 多个事务并发写相同对象时,会出现脏写、更新丢失两种竞争条件。为避免数据不一致,可:借助DB内置机制或通过显式加锁

多个事务并发写相同对象时,会出现脏写、更新丢失两种竞争条件。为避免数据不一致,可:


借助DB内置机制

或通过显式加锁

以执行原子写操作。但这还不是并发写可能导致的全部问题。


2.4.1 值班程序


医院通常会同时要求几个医生待命,前提是至少有一位医生在待命。医生可放弃他们的班次(如若自己生病了),只要至少有一个同事在这天的班中继续工作。


Alice、Bob两位值班医生都病了,所以他们都决定请假。但他们恰在同一时刻点击调班按钮

4.png



每笔事务总先检查是否至少有两名医生目前在值班。若是,则有一名医生可安全离开去休班。由于DB使用快照隔离,两次检查都返回2,所以两事务都进入下一阶段:


Alice更新自己的记录为休班

Bob也更新自己的记录

两个事务都成功提交,最后结果是无医生值班,显然违反了至少有一名医生得值班的业务需求。


2.4.2 写倾斜


这种异常即写倾斜,不是脏写、丢失更新。这俩事务更新的是两个不同对象(Alice 和 Bob 各自值班记录)。这里发生的冲突不是那么明显,但显然也是竞态:若两个事务串行,则第二个医生就不能歇班。异常行为只有在事务并发时才可能。


可将写倾斜视为【广义的丢失更新】。即若两事务读取相同一组对象,然后更新其中一部分:


不同事务,更新不同对象,则可能发生写倾斜

不同事务,更新同一对象,则可能脏写或丢失更新

很多方法可防止丢失更新。但对写倾斜,方案更受限:


由于涉及多对象,单对象的原子操作无效

基于快照隔离来实现自动检测丢失更新也有问题:PostgreSQL可重复读,MySQL/InnoDB 可重复读,Oracle可串行化或SQL Server快照隔离级别中,都不支持自动检测写倾斜。自动防止写倾斜要求真正的可串行化隔离

某些DB支持自定义约束,然后由DB强制执行(如唯一性,外键约束或特定值限制)。但为指定至少有一名医生必须在线,涉及多个对象的约束,大多DB都未内置这种约束,但你可使用触发器或物化视图来实现类似约束

若无法使用可串行化,则次优方案可能是显式锁定事务依赖的行:

BEGIN TRANSACTION;


SELECT * FROM doctors

 WHERE on_call = TRUE

 # 告诉DB锁定返回的所有结果行,以用于更新

 AND shift_id = 1234 FOR UPDATE;


UPDATE doctors

 SET on_call = FALSE

 WHERE name = 'Alice'

 AND shift_id = 1234;

 

COMMIT;


2.4.3 写倾斜case


理解到写倾斜的本质后,容易注意到更多case:


会议室预订系统


不能在同一时间对同一会议室进行多次预订。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),若无,则创建会议(参阅示例-2)1


例-2 会议室预订系统,避免重复预订(在快照级别隔离下不安全)


BEGIN TRANSACTION;


-- 检查所有现存的与 12:00~13:00 重叠的预定

SELECT COUNT(*) FROM bookings

WHERE room_id = 123 AND

 end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';

-- 若之前的查询返回 0

INSERT INTO bookings(room_id, start_time, end_time, user_id)

 VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);

COMMIT;


快照级别隔离无法防止并发用户预订同一会议室。为避免预订冲突,需可串行化隔离级别。


多人游戏


例-1中,使用一个锁来防止丢失更新(即两个玩家不能同时移动同一数字)。但锁不妨碍玩家将两个不同数字移动到棋盘的相同位置或其他违反游戏规则的行为。可能需更多约束,否则很容易发生写倾斜。


抢注用户名


在每个用户拥有唯一用户名的网站上,两个用户可能会尝试同时创建具有相同用户名的帐户。可采用事务检查名称是否被抢占,若无,则使用该名称创建账户。但和之前案例类似,快照隔离下不安全。但唯一约束是简单方案(第二个事务在提交时会因为违反用户名唯一约束而被中止)。


防止双重开支


支付或积分服务一般需检查用户的支付数额不超过余额。可通过在用户帐户中插入一个临时支出项目,列出帐户中的所有项目,并检查总和是否为正值。由于写倾斜,可能发生两个支出项目同时插入,两个交易都不超额,但一起会导致余额变为负值。


2.4.4 导致写倾斜的幻读


所有这些案例都遵循类似模式:


首先输入一些匹配条件,即 SELECT 查询所有符合条件的行并检查是否符合一些要求。如至少有两名医生在值班;不存在对该会议室同一时段的预订;棋盘某位置没有出现棋子;用户名还没被抢注;账户里还有余额等


根据查询结果,应用代码决定是否继续


若应用决定继续执行,就发起DB写入(插入、更新或删除),并提交事务


而该写操作会改变步骤2做出决定的前提条件。即若提交写入后,再重复执行步骤1的 SELECT查询,将得到不同结果。因为刚才的写改变了符合搜索条件的行集(现在少了一个医生值班,那时的会议室现已被预订,棋盘上的这个位置已被占,用户名已被抢注,账户余额不够)。


上述步骤可能有不同执行顺序。如可先写,然后SELECT查询,最后根据查询结果决定是放弃还是提交。


医生值班案例,步骤3所修改的行恰好是步骤1查询结果的一部分,所以若通过锁定步骤 1 中的行(SELECT FOR UPDATE)再查询可保证事务安全,避免写倾斜。但其他四个案例不同:它们检查是否 不存在 某些满足条件的行,写入会 添加 一个匹配相同条件的行。若步骤1中的查询没有返回任何行,则 SELECT FOR UPDATE 锁不了任何东西。


这种效应:一个事务中的写入改变另一个事务的搜索查询结果,即幻读。快照隔离避免了只读查询中的幻读,但是在像我们讨论的例子那样的读写事务中,幻读会导致特别棘手的写倾斜。


2.4.5 物化冲突


若幻读的问题是没有对象可以加锁,也许可以考虑人为在DB引入一个锁对象?


如会议室预订案例,想象创建一个关于时间槽和房间的表。此表中的每行对应于特定时间段(如 15min)的特定房间。可提前插入房间和时间的所有可能组合行(例如接下来的六个月)。


现在,要创建预订的事务可以锁定(SELECT FOR UPDATE)表中与所需房间和时间段对应的行。锁定后,它可检查重叠预订并像以前一样插入新预订。该表不是用来存储预订相关信息的,它完全就是一组锁,以防止同时修改同一房间和时间范围内的预订。


这被称为物化冲突(materializing conflicts)方案,因为它将幻读变为DB中一组具体行上的锁冲突。但弄清楚如何物化冲突很难,也很易出错,而让并发控制机制泄漏到应用数据模型是很丑陋的做法。出于这些原因,若无其他办法可以实现,物化冲突应被视为最后手段。大多数情况下,可串行化(Serializable) 隔离级别更可取。


PostgreSQL中,可使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持 ↩︎

目录
相关文章
|
9天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
15天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
8天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
7天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
10天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
16天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
13天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
15天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
39 2
|
16天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
1天前
|
Java API 数据库
Java 反射机制:动态编程的 “魔法钥匙”
Java反射机制是允许程序在运行时访问类、方法和字段信息的强大工具,被誉为动态编程的“魔法钥匙”。通过反射,开发者可以创建更加灵活、可扩展的应用程序。
下一篇
无影云桌面