一、什么是不可重复读
在并发的情况下,同一个事务内两次查询同一个数据,可能会得到不同的结果,这就是不可重复读(Unrepeatable Read)。
不可重复读与幻读不同,不可重复读针对的是同一行记录的不同查询结果不一致的情况,而幻读则是在同一条件下的不同查询结果不一致的情况。
二、产生不可重复读的原因
MySQL支持多版本并发控制(MVCC),使用MVCC能够提高数据库的并发能力,但同时也会导致不可重复读的出现。MVCC实现对每个事务开启时间点的快照,若在某个时刻开始的事务能看到之前已提交的快照,但看不到之后或同时提交的快照。
因此,当一个事务访问某行数据时,MySQL会记录当前事务的开启时间点,并将该时间点与该数据行的版本进行比较,若该数据行版本较旧,则该事务可以读取该数据行,否则,该事务将无法读取该数据,从而避免了脏读的情况。但仍有可能出现不可重复读的情况,例如:
-- Session1 BEGIN; SELECT * FROM some_table WHERE some_column = 1; -- Session2 BEGIN; UPDATE some_table SET some_column = 2 WHERE some_key = 1; COMMIT; -- Session1 SELECT * FROM some_table WHERE some_column = 1; COMMIT;
在上面的示例中,Session1执行SELECT查询时,MySQL记录了开启时间点,然后Session2更新了该条记录并提交,随后Session1又执行了一次SELECT查询,由于当前时间点已经大于Session2提交的时间点,因此Session1得到的结果与第一次查询不同。
三、如何避免不可重复读
为了避免不可重复读,我们可以采用如下几种方法:
1、加锁
使用SELECT … FOR UPDATE(悲观锁)或SELECT … LOCK IN SHARE MODE(共享锁)可以保证并发查询结果的一致性。但是,在高并发的情况下,过多的锁将导致性能下降。
2、使用幂等的更新操作
幂等性是指无论进行多少次操作,结果都是相同的操作,可以使用最后一次的操作来代替之前的操作。在更新操作时,使用update … where … and 判断更新条件,从而避免了幻读和不可重复读的情况。
-- Session1 BEGIN; SELECT * FROM some_table WHERE some_column = 1 FOR UPDATE; -- Session2 BEGIN; UPDATE some_table SET some_column = 2 WHERE some_key = 1; -- Session1 UPDATE some_table SET some_column = 3 WHERE some_key = 1; COMMIT; -- Session2 COMMIT;
在上面的示例中,Session1使用SELECT … FOR UPDATE锁定了某行记录,在执行UPDATE操作时,由于该记录已经被锁定,Session1需要等待Session2的事务提交才能继续执行,从而避免了不可重复读的情况。
3、使用MVCC
在使用MVCC时,可以将当前事务的开启时间点与快照版本进行比较,从而避免了不可重复读的情况。同时,为了避免幻读的出现,可以使用间隙锁或一致性非锁定读(consistent nonlocking reads)。
四、总结
不可重复读是在并发的情况下可能出现的问题,可以使用加锁、幂等的更新操作或MVCC来解决。为了保证系统的性能,应该根据实际场景来选择合适的解决方法。