我的应用程序中有一些逻辑,我认为这些逻辑会导致以下 MySQL 调用,但是当我在几毫秒内运行其中两个时,我会得到两个不兼容的子行。
- 使用可重复读隔离启动事务。
- 获取并锁定 Account 对象
SELECT ... FROM Account WHERE id = ? FOR UPDATE
- 读取链接到帐户的现有收件人
SELECT ... FROM Address WHERE account_id = ?
- 如果现有地址被标记为主要地址,请更新它。
UPDATE Addresses SET primary = false WHERE id = ?
- 插入primary = true 的新地址。
- 完成交易。
如果这个过程快速运行两次,我会得到两个地址行,其中primary = true,并且account_id设置为帐户,这让我感到惊讶,因为我认为在步骤2中锁定帐户会阻止多个事务同时运行。我不想以过多限制我的吞吐量的方式改变行为。
我想知道是否需要将隔离级别切换为“可序列化”[原文如此],但我不清楚第 3 步中的“FOR SHARE”读取是否能真正解决问题。
我的数据结构从上面可能很明显,但看起来像这样:
Account
:
id
PK
其他不相关的账户数据
Address
:
id
PK,
account_id
- 不是实际的外键,只是与帐户 PK 匹配的 id。
primary
- 布尔值,每个帐户最多应有一个主地址,但这在 MySQL 中并不强制执行,因为使用生成的列来启用此功能会使数据库变得有点复杂,因为 MySQL 不支持部分索引。
其他不相关的地址数据
发生的情况是,由于可重复读取,第二个事务看不到第一个事务执行的更新。它只能看到 Address 表中行的版本,就像事务 2 启动时(事务 1 提交更新之前)一样。
这称为“丢失更新”。事务 1 更新了一些行,但事务 2 看不到它们,并且认为可以自由地再次更新这些行。因此它进行更新,并且永远不会看到事务 1 的更改。
为了解决这个问题,您可以使用 READ COMMITTED 隔离级别,因此事务 2 对 Address 表执行 SELECT 操作并查看最近提交的更改。
或者,您可以将事务 2 对地址表的 SELECT 设为锁定查询。SELECT FOR UPDATE 或 SELECT FOR SHARE 都可以解决此问题。InnoDB 实现锁定读取的方式,它们始终查看行的最近提交版本,就好像您使用了 READ COMMITTED 隔离级别(即使事务以 REPEATABLE READ 启动)。