04. 防止丢失更新
防止丢失更新
到目前为止已经讨论的读已提交和快照隔离级别,主要保证了只读事务在并发写入时可以看到什么。却忽略了两个事务并发写入的问题,我们只讨论了脏写,一种特定类型的写

如果应用从数据库中读取一些值,修改它并写回修改的值(读取
- 增加计数器或更新账户余额(需要读取当前值,计算新值并写回更新后的值)
- 在复杂值中进行本地修改:例如,将元素添加到
JSON 文档中的一个列表(需要解析文档,进行更改并写回修改的文档) - 两个用户同时编辑
wiki 页面,每个用户通过将整个页面内容发送到服务器来保存其更改,覆写数据库中当前的任何内容。
这是一个普遍的问题,所以已经开发了各种解决方案。
原子写
许多数据库提供了原子更新操作,从而消除了在应用程序代码中执行读取
$ UPDATE counters SET value = value + 1 WHERE key = 'foo';
类似地,像
原子操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为游标稳定性(cursor stability
不幸的是,
显式锁定
如果数据库的内置原子操作没有提供必要的功能,防止丢失更新的另一个选择是让应用程序显式地锁定将要更新的对象。然后应用程序可以执行读取
例如,考虑一个多人游戏,其中几个玩家可以同时移动相同的棋子。在这种情况下,一个原子操作可能是不够的,因为应用程序还需要确保玩家的移动符合游戏规则,这可能涉及到一些不能合理地用数据库查询实现的逻辑。但你可以使用锁来防止两名玩家同时移动相同的棋子:
BEGIN TRANSACTION;
SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222
FOR UPDATE;
-- 检查玩家的操作是否有效,然后更新先前SELECT返回棋子的位置。
UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;
自动检测丢失的更新
原子操作和锁是通过强制读取
这种方法的一个优点是,数据库可以结合快照隔离高效地执行此检查。事实上,
丢失更新检测是一个很好的功能,因为它不需要应用代码使用任何特殊的数据库功能,你可能会忘记使用锁或原子操作,从而引入错误;但丢失更新的检测是自动发生的,因此不太容易出错。
比较并设置(CAS)
在不提供事务的数据库中,有时会发现一种原子操作:比较并设置(CAS, Compare And Set
例如,为了防止两个用户同时更新同一个
-- 根据数据库的实现情况,这可能也可能不安全
UPDATE wiki_pages SET content = '新内容'
WHERE id = 1234 AND content = '旧内容';
如果内容已经更改并且不再与“旧内容”相匹配,则此更新将不起作用,因此您需要检查更新是否生效,必要时重试。但是,如果数据库允许
冲突解决和复制
在复制数据库中,防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。锁和
相反,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟