精通Java事务编程(4)-弱隔离级别之防止更新丢失
RC和快照隔离级别主要都是为解决 只读事务遇到并发写时可以看到什么(虽然中间也涉及脏写),还没触及另一种情况:两个写事务并发,而脏写只是写并发的特例。
写事务并发带来最着名的问题就是丢失更新,如图-1的两个并发计数器增量为例。
应用从DB读一些值,修改它并写回修改后的值,则可能导致丢失更新。若两事务同时执行,则其中一个的修改可能丢失,因为第二个写内容并未包括第一个事务的修改(有时会说后面的写入 狠揍(clobber) 了前面的写入)这种模式发生在各种不同场景:
- 增加计数器或更新账户余额(需要读取当前值,计算新值并写回更新后的值)
- 在复杂值中进行本地修改:例如,将元素添加到 JSON 文档中的一个列表(需要解析文档,进行更改并写回修改的文档)
- 两个用户同时编辑 wiki 页面,每个用户通过将整个页面内容发送到服务器来保存其更改,覆写数据库中当前的任何内容。
这是一个普遍的问题,所以已经开发了各种解决方案。
2.3.1 原子写
许多DB支持原子更新,避免了在应用程序代码中执行读取 - 修改 - 写入。用这些操作通常是最好的解决方案。如下指令在大多数关系DB中并发安全:
UPDATE counters SET value = value + 1 WHERE key = 'foo';
类似像:
- MongoDB文档DB提供了对 JSON 文档的一部分进行本地修改的原子操作
- Redis支持修改数据结构(如优先级队列)的原子操作
并不是所有的写操作都可以用原子操作的方式来表达,例如维基页面的更新涉及到任意文本编辑 1,但是在可以使用原子操作的情况下,它们通常是最好的选择。
实现方案
- 一般采用对读取对象加排它锁来实现,以便在更新完成之前没有其他事务可以读它。这种技术有时被称为游标稳定性(cursor stability)
- 另一个实现方案是强制所有的原子操作在单线程执行。
但ORM框架很容易导致执行不安全的读取 - 修改 - 写入,而不是使用数据库提供的原子操作。若你知道自己在做什么,或许这不会引发什么问题,但往往会埋下潜在Bug。
2.3.2 显式加锁
若DB不支持内置原子操作,防止丢失更新的另一个选择是让应用程序显式锁定待更新对象。然后应用程序执行读取 - 修改 - 写入,此时若其他事务尝试同时读取对象,则必须等待,直到第一个 读取 - 修改 - 写入 完成。
如多人游戏,其中几个玩家能同时移动同一个数字。只靠原子操作可能不够,因为应用程序还需确保玩家的移动符合规则,这可能涉及一些应用层逻辑,不可能将其剥离转移给DB层在查询时执行。此时,可使用锁来防止两名玩家同时移动相同棋子,如例-1:
例-1 显式锁定行,以防止丢失更新
BEGIN TRANSACTION;
SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222
# 指示DB对返回的所有结果行要加锁。
FOR UPDATE;
-- 检查玩家的操作是否有效,然后更新先前 SELECT 返回棋子的位置
UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;
这有效,但要做对,需仔细考虑应用层逻辑。忘记在代码某处加锁很容易引入竞争条件。
2.3.3 自动检测更新丢失
原子操作和锁是通过强制 读取 - 修改 - 写入 串行执行来避免丢失更新。
另一种方法是允许它们并发,但若事务管理器检测到丢失更新,则中止当前事务,并强制它们回退到安全的 读取 - 修改 - 写入。
该方案的一个优点是DB能结合快照隔离高效执行检查。PostgreSQL的可重复读,Oracle的可串行化和 SQL Server 的快照隔离级别,都能自动检测到丢失更新,并中止违规的事务。但MySQL/InnoDB的可重复读并不会检测丢失更新。一些作者认为,DB必须防止丢失更新,才称得上是提供了快照隔离,所以在这种定义下,MySQL属于没有安全支持快照级别隔离。
丢失更新检测是个好功能,应用代码因此不依赖某些特殊的DB功能。你可能忘记使用锁或原子操作,但丢失更新的检测会自动生效,就不太容易出错。
2.3.4 CAS
不提供事务的DB有时支持CAS,可避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。若当前值与先前读取的值不匹配,则更新不起作用,就重试读取 - 修改 - 写入。
如为防止两个用户同时更新同一个 wiki,可尝试如下操作,只有当页面从上次读取之后没发生变化时,才会执行当前的更新:
-- 根据数据库的实现情况,这可能安全也可能不安全
UPDATE wiki_pages SET content = 'new content'
WHERE id = 1234 AND content = 'old content';
若内容已更改且不再与 “旧内容” 匹配,则更新失败,需应用层再次检查更新是否生效,必要时重试。若WHERE语句运行在DB的某个旧快照,即使另一个并发写入正在运行,条件可能仍为真,最终可能无法防止更新丢失。所以在使用前,应先仔细检查“比较-设置”操作的安全运行条件。
2.3.5 冲突解决和复制
支持多副本的数据库中,防止丢失更新还需考虑:由于多节点上存在数据副本,不同节点可能并发修改数据,需采取额外措施防止丢失更新。
加锁、CAS前提都要求只有一个最新的数据副本。但多主或无主复制的多副本DB,通常允许多个并发写,并异步复制到副本,所以会出现多个最新的数据副本。此时加锁或CAS将不再适用。
正如系列文章(5)中的【检测并发写入】一节所述,多副本DB通常允许并发写入创建多个冲突版本的值(互称为兄弟),并使用应用层代码或特殊数据结构来解决、合并这些多版本。
若操作可交换(顺序无关,在不同副本上以不同顺序执行时,仍得到相同结果),则原子操作在多副本情况下也能工作。如递增计数器或向集合添加元素都是典型的可交换操作。这是 Riak 2.0 新数据类型思想,当一个值被不同客户端同时更新时, Riak自动将更新合并在一起,避免发生更新丢失。
而最后写入胜利(LWW)的冲突解决方法则容易丢失更新,不幸的是,LWW目前是许多多副本DB的默认配置。
相关文章
- 精通Java事务编程(9)-总结
- 精通Java事务编程(5)-写倾斜与幻读
- 精通Java事务编程(2)-弱隔离级别之已提交读
- 精通Java事务编程(1)-深入理解事务
- JAVA学习实战(九)事务
- java事务的处理
- java操作MySQL数据事务的简单学习
- MessagePack Java Jackson Dataformat 不使用 str8 数据类型的序列化
- Java架构师-分布式(七):数据库读写分离、分库分表【使用MyCat数据库中间件】【MyCat本身不存储数据,数据都是存储在MyCat后面连接的MySql上,数据的可靠性和事务都是MySql保证的】
- Java架构师-分布式(八):数据库表全局唯一主键id设计、分布式事务、数据一致性
- 52类110个主流Java组件和框架
- 面试官:Java 多线程怎么做事务控制?一半人答不上来。。
- Java 中的伪共享详解及解决方案
- Java成神之路技术整理(长期更新)
- java 标准异常
- mongodb-java-driver基本用法
- java基础之java简介(一)
- 金融项目java开发_BigDecimal(解决计算精度问题)
- JAVA下实现二叉树的先序、中序、后序、层序遍历(递归和循环)
- java之Secure communication terminology
- Java中常用的四种线程池
- 一天五道Java面试题----第七天(mysql索引结构,各自的优劣--------->事务的基本特性和隔离级别)
- 【JAVA】力扣-19.删除链表的倒数第 N 个结点
- 【JAVA高级】——myEclipse连接mysql启动数据库服务
- jSparrow 2021Crack,自动 Java 重构
- Java后端实现图片压缩技术
- 使用sonar进行java代码质量管理
- 【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )
- Java或者JAR包获取读取资源文件的路径的问题总结
- 大数据必学Java基础(九十七):事务及回滚点
- 【Java基础系列】tob和toc的区别
- 【丢失的数字(268-java)】