Transactions (4) - Concurrent Write

Preventing Lost Update

昨天讲的快照隔离优雅的解决了 read-skew 的问题,除了 read-skew ,今天要来聊聊另一个常发生的状况:2 个 transaction 同时做写入该怎麽办!?其中最为人知的写入冲突就是 更新遗失 (lost update),如前几天有看过的图 7-1 就是一个典型的并发计数器更新遗失问题。

这最常发生在应用程序需要进行 读取-修改-写入 (read-modify-wite) 的 transaction 回圈中,就像上图 7-1 ,势必有一个 transaction 的更新将会遗失;因为这是一个很常见的问题,所以我们有以下几种解决方式可用:

原子写入操作 (Atomic wrtie operations)

许多资料库提供原子写入操作 (Atomic write operations),这就代表了我们不用在应用程序端进行 读取-修改-写入(read-modify-write) 回圈操作,举例来说,下面这段 SQL 就是执行绪安全 (concurrency-safe) 的更新:

UPDATE conuters SET value = value + 1 WHERE key = 'foo';

不只 RDB,document 类型的资料库和 Redis 都有提供类似的原子写入操作。

原子操作通常使用全域唯一物件锁来实作,当物件被读取时没有其他 transaction 可读取,除非它的写入被 commit,这技术也称 cursor stability;另一个方式是强迫所有的原子操作只被执行在单一执行绪上。

外部锁 (Explicit locking)

如果资料库内建的原子写入操作无法满足需求,另一个避免更新遗失的方式就是应用程序来指定我该锁哪里。

例如下面这段 SQL 就是来检查机器人跟玩家不能同时走到同一个地图点,(1) 的 FOR UPDATE 语法就是指示资料库要取得所有被查询结果的物件锁。

BEGIAN TRANSACTION;

SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222
FOR UPDATE; --(1)

UPDATE figures SET position = 'c1' WHERE id = 12345;

COMMIT;

这个方法很需要厘清资料更新的逻辑,若在哪边忘记加锁,又会重蹈竞争写入的覆辙了。

比较并交换 (Compare-and-set)

在一些没有提供 transaction 的资料库中,你有时会找到有支持原子操作的 比较并交换 (compare-and-set),这种操作只允许当资料从你读取後从未被变更,才能更新,否则会更新无效并重试,用个 SQL 来举例可能比较好懂:

UPDATE wiki_page SET content = 'new content' WHERE id = 1234 AND content = 'old content';

当 wiki 页面不是你以为的旧资料时,该更新会无效。

请留意各资料库针对 比较并交换 (compare-and-set) 是以什麽来实做的!例如这个 wiki 页面的例子,若资料库允许在 update 时的 where 语句能读取旧的快照资料,此做法还是不能避免 更新遗失 (lost update)

解决冲突和副本 (Confict resoution and replication)

在副本型资料库中 (2020 Day 21~26),更新遗失的处理需要多一些额外步骤才能避免,因为同一份资料会被复制到多台节点上,所以资料很有可能会并发的在不同节点上一起被更新。

最常见的方法是允许并发写入建立多个版本的资料 (也被称为 siblings),然後依靠应用程序或其他特殊资料结构来解决冲突,细节可回头看 2020 Day 26 - Capturing the happens-before relationship 小节

原子写入操作同样也可在副本型资料库里运作良好,尤其是累加型的操作(如计数器或对个 list 加元素),此概念是来自 Riak 分散式资料库 2.0 的资料型态,它能避免跨节点的 更新遗失 (lost update),Riak 能自动合并并发写入且不需要建立 siblings 资料。

最後一个方法,依旧是 2020 Day 26 Detecting Concurrent Writes 中提过的 最後写的最大 (last write wins - LWW) ,这也是很多副本型资料库的预设解冲突方法。

Write Skew and Phantoms

除了 Day 3 有讲到的 dirty writes 和今天的 更新遗失 这 2 种 竞争条件 (race condition) 写入外(多个 transaction 操作同一个资料物件),今天要讲讲多个资料物件版本的竞争条件写入。

假设一下这个场景:你正在写一个应用系统管理医生的排班,医生可同时值班,但最少一定要留一个医生 oncall,医生们可以选择不值班,想像一下有 2 位医生 Alice 和 Bob 都有同一段时间的值班 (shift_id=1234),2 位都很不舒服想 翘班 请假,所以上系统操作,很不幸的,他们几乎同时点了按钮,此时会发生如下图的事情:

现在好了,每一个 transaction 都是符合系统业务逻辑规则,但现在 shift_id=1234 的班表上没有医生值班了。

Write skew

这种异常情况称为 write skew,因为他们是同时修改 2 个不同物件,所以不归类在 dirty writes 或 更新遗失 里,这个解法看起来很简单对吧!?让某个 transaction 慢一点执行就好了,但现实世界总会 Surprise 妈的发科 一下,就是有某个时间点会并发一下。

怎麽解决?我们来看一下:

  • 使用资料库的 constrains,例如 uniqueness, foreign key constrains 或者 完整限制 (Integrity Constraints)

  • 使用明天会讲的最强隔离等级 序列化隔离 (serializability isolation)

  • 使用 显示锁定 (Explicit Locking),如下方 SQL,FOR UPDATE 语句会告诉资料库要锁住查询结果(不作用在要新增资料的场景上,因为没资料可锁定)。

    BEGIN TRANSACTION;
    	SELECT * FROM doctors
    		WHERE on_call = true
    		AND shift_id = 1234 FOR UPDATE;
    
    	UPDATE doctors
    		SET on_call = false 
    		WHERE name = 'Alice' AND shift_id = 1234;
    
    COMMIT;
    

Phantoms 导致 write skew

write skew 通常符合以下模式:

  1. 查询资料。
  2. 判断是否要继续执行。
  3. 写入资料(insert, update 或 delete)。

有些业务埸景可能会有不同的顺序,举例来说你可以先写入,然後查询,最後在决定要 commit 或中止。

当一个 transaction 的写入改变另一个 transaction 的查询结果的效果,称为 幻影 (phantom)Day 4 讲的快照隔离能避免 read-only 查询,但 read-write 就无法了,phantom 会让你遇到一些很吊诡的 write skew。


明天就来介绍解决 write-skew 的最完美方法,序列化隔离 (serializability isolation) 啦。


<<:  Day 5 - 手机连线和New Project

>>:  Day 0x4 UVa10041 Vito's Family

[D04] 取样与量化(2)

接着来更深入的了解数位影像的取样与量化吧! 取样简单来说就是我们要以多少个方格来表示这张图片,方格愈...

ui li 列出清单标签-基础语法

<ul> <li>1</li> <li>2<...

.NET Core第28天_ValidationMessageTagHelper和ValidationSummaryTagHelper的使用

在.net core MVC当中针对model验证是有支援双重验证的机制,也就是在Client端跟S...

Day 28 - Clean Coder 时程与承诺

前言 今天会接续着昨天的主题,来聊聊 The Clean Coder 的另一个主题。 在我过去的工作...

【Day12】系列终止

因为某种天外飞仙,天上掉下来的炸弹等的理由,所以这个项目就会终止,在报 名截止前让我想想要参加甚麽项...