背景描述
现在有两个事务,事务A和事务B,他们都需要修改同一行数据,这行数据原始值为100,事务A的操作是数据增加100,事务B的操作也是增加100,预期的最终结果是300,现在如何保证最终的数据是300的?什么时候会出现200的情况?
执行结果
最终数据是300的情况(可重复读) :
在可重复读(REPEATABLE READ)隔离级别下,这种情况较为容易实现。此隔离级别是MySQL默认级别,它可以有效避免脏读、不可重复读和幻读问题。
-
执行流程:
- 事务A开始,执行查询操作(
SELECT
),获取值100。 - 事务A给这行数据加上读锁(共享锁)或排他锁(根据是否使用
SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
)。 - 事务A在查询结果的基础上增加100,更新这行数据,然后提交事务,释放任何持有的锁。
- 接着,事务B开始,执行查询操作,获取到更新后的值200(因为事务A已经提交)。
- 事务B给这行数据加上读锁或排他锁。
- 事务B在查询结果的基础上增加100,更新这行数据,然后提交事务,释放任何持有的锁。
- 事务A开始,执行查询操作(
-
使用的锁:
- 根据是否声明
FOR UPDATE
或LOCK IN SHARE MODE
,可能会使用记录锁(行锁)或next-key锁(next-key锁是行锁和gap锁的组合,防止幻读)。 - 如果没有使用上述子句,那么InnoDB存储引擎默认会在更新操作时自动给涉及的行加上排他锁(X锁) 。
- 根据是否声明
最终数据是300的情况(读未提交) :
在读未提交(READ UNCOMMITTED)隔离级别下,事务可以读取到其他事务未提交的更改,也就是这个隔离级别允许发生“脏读”(Dirty Reads)
-
执行流程:
- 事务A在读未提交的隔离级别下启动。
- 事务A执行 SELECT 查询操作,读取数据值为100。
- 然后事务A增加这个值100,执行 UPDATE 操作,更新该数据行为200。此时,事务A还没有提交,所以更改是未提交的。
- 在读未提交隔离级别下,事务B也启动。
- 事务B执行 SELECT 操作,在这个隔离级别下,它可以读取到事务A未提交的更改,也就是它可能会读取到数据值为200。
- 事务A提交事务后,事务A对数据行的更改变成持久的,这时候数据是200。
- 基于事务B读到的200的数据值,事务B决定将这个数据增加100,更新操作将这个值更改为300。
- 然后事务B提交,这时数据行的更新已经持久化为300。
出现200的情况(低级别的隔离级别) :
在较低级别的隔离级别,如读未提交(READ UNCOMMITTED)或读提交(READ COMMITTED),可能会出现最终结果是200的情况。
-
执行流程:
- 事务A开始,执行查询操作,获取值100。
- 事务B几乎同时间开始,并执行查询操作,也读取到值100(尤其在READ UNCOMMITTED级别,或事务B在READ COMMITTED级别下且在事务A提交之前读取数据)。
- 事务A增加100,更新数据库行为200,然后提交。
- 事务B没有得到事务A的更新(因为它已经读取了数据),它也增加100,在其本地的值基础上(100),并更新数据库行为200,然后提交。
-
使用的锁:
- 在READ UNCOMMITTED级别下,不使用行级锁定,事务可以读取未提交的数据。
- 在READ COMMITTED级别下,每次查询都会读取最新的已提交数据,但如果事务B在事务A提交之前读取数据,它不会再次查询数据库。
- 在REPEATABLE READ级别下,第一次读取后,事务内的读操作会在提交前看到同样的数据,即事务B第一次查询后再次查询依然会是100,直到事务提交才会读到新的值。
出现200的情况(并发导致更新丢失) :
-
执行流程
- 事务A在可重复读的隔离级别下启动。
- 事务B也在可重复读的隔离级别下启动。
- 事务A执行SELECT查询,并且读取到这行数据的初始值100。
- 事务A在查询结果的基础上增加100,并执行UPDATE操作,将该行的值变为200。
- 在UPDATE操作过程中,事务A为这行数据加上排他锁。
- 事务A提交事务,行数据变为200,并释放排他锁。
- 因为事务B也在可重复读隔离级别下,所以即便事务A已经提交更新,事务B的SELECT操作依然会看到事务B自己快照里的数据,即历史值100。这意味着事务B在这个阶段读取到的是100,而非事务A更新后的200。
- 此时,事务B尝试基于它自己读取到的那个100的值增加100并且执行UPDATE。
- 由于事务A已经提交,并释放了排他锁,事务B现在确实能对这行数据进行更新操作,但是基于自己快照里的原始值100,它更新行数为200,而非基于事务A的更新结果。
- 事务B提交事务,数据的最新状态是200。
更新丢失问题
当两个或多个事务读取同一数据,并基于此数据进行更新时,其中一个事务的更新可能会由于另一个事务的更新而被覆盖,导致第一个事务的更改丢失。什么场景下会出现这种问题呢?
假设一个场景,在事务中涉及到先查询后更新的操作,那么使用排他锁时,是在事务开始时加锁,还是说在更新数据时加锁?
这有两种常见的策略,各有不同的问题:
1、 查询时不加锁,更新时加锁:
在这种策略中,事务在查询数据时不会对数据行加锁。只有当事务执行更新操作时,才会对相关数据行加上排他锁。这样做的好处是可以减少锁的持有时间,从而提高并发性能。但是,这种策略可能会导致“丢失更新”(Lost Update)问题,即两个事务可能同时读取相同的数据,然后几乎同时更新,导致一个事务的更新覆盖另一个事务的更新。
2、查询时就加锁,保持到事务结束:
在这种策略中,事务在查询数据时就会对数据行加上排他锁,并且保持锁直到事务结束(提交或回滚)。这样可以确保在事务执行期间,其他事务无法修改被锁定的数据行,从而避免了“丢失更新”问题。然而,这种策略会增加锁的持有时间,可能会降低系统的并发性能。
在MySQL的InnoDB存储引擎中,默认的隔离级别是可重复读(REPEATABLE READ)。在这个隔离级别下,InnoDB会使用以下策略,查询时不加锁,更新时加锁:
- 当执行普通的SELECT查询时,InnoDB不会加排他锁。
- 当进行写操作,如UPDATE、DELETE、INSERT时,会对涉及的数据行加上排他锁。
这里是两种不同SELECT语句的例子:
-
普通查询(不加锁) :
SELECT * FROM your_table WHERE id = 1;
-
查询并锁定(加排他锁) :
如果你希望在读取数据时立即加锁以确保数据不会被其他事务修改,你可以使用SELECT ... FOR UPDATE语句。这会对选中的行加上排他锁,直到当前事务结束。
SELECT * FROM your_table WHERE id = 1 FOR UPDATE;
那么MySQL是不是存在“更新丢失”问题呢?它是怎么解决的呢?
如何解决“更新丢失”问题呢?
我们先来看看解决“更新丢失”有哪些办法:
SELECT ... FOR UPDATE
,以获取对相关数据行的排他锁,防止其他事务并发修改。下面是如何在MySQL中使用乐观锁定的方法和步骤:
ALTER TABLE your_table ADD COLUMN version INT DEFAULT 0;
SELECT value, version FROM your_table WHERE id = 1;
UPDATE your_table
SET value = value + 100, version = version + 1
WHERE id = 1 AND version = read_version;
int affectedRows = preparedStatement.executeUpdate();
if (affectedRows == 0) {
// Handle the optimistic lock failure (e.g., throw an exception or retry)
}
在Java代码中,可以通过检查数据库操作返回的受影响行数来实现乐观锁定逻辑。如果受影响行数为0,则说明更新并没有成功,很可能是因为版本号不一致导致的更新冲突。应用程序可以根据业务场景,选择是重新尝试、放弃操作,还是抛出异常来通知用户。
乐观锁定在并发不是特别高,冲突概率较低的系统中是非常有用的,并且相对于悲观锁,它提供了更好的性能和用户体验。但在高冲突环境下,乐观锁可能会导致大量的冲突和重试,从而可能影响系统的整体性能。
MySQL怎么解决“更新丢失”问题?
在MySQL默认的事务隔离级别(REPEATABLE READ)下,解决并发事务中的“更新丢失”的关键流程主要包括:
排他锁
排他锁(Exclusive Lock),通常简称为X锁,是数据库中用于控制并发访问的一种锁机制。当一个事务对数据行加上排他锁时,它可以确保在这个事务完成并释放锁之前,其他事务不能对该数据行进行任何读取或修改操作。
排他锁的主要特点如下:
在MySQL中,当执行INSERT、UPDATE或DELETE操作时,InnoDB存储引擎会自动为涉及的数据行加上排他锁。这些锁在事务提交或回滚后自动释放。排他锁是实现事务隔离的关键机制之一,它有助于维护事务的原子性和一致性。
需要注意的是,排他锁可能会导致性能问题,特别是在高并发的场景下。如果事务长时间持有锁,可能会导致其他事务等待,从而降低系统的并发性能。因此,在设计数据库操作时,应该尽量减少锁的持有时间,合理地使用锁,以及考虑使用其他并发控制机制,如乐观锁等,来平衡性能和数据一致性的需求。