[面试] InnoDB中如何解决幻读?

幻读是通过 MVCC 机制来解决的, MVCC 类似于一种乐观锁的机制,通过版本的方式来区分不同的并发事务,避免幻读 问题!

什么是幻读?

事务A前后两次读取同一个范围的数据,在事务A两次读取的过程之间,事务B新增了数据,导致事务A后一次读取到前一次查询没有看到的行(数据)。幻读强调的是集合的增减,而不是单条数据的更新。如下图示例:
在这里插入图片描述

所以幻读会带来数据一致性的问题!

如何解决幻读问题?

  • 调整事务隔离级别:将事务隔离级别设置为可重复读串行化,这可以防止幻读。在这些隔离级别下,事务会锁定读取的数据,直到事务结束。

  • 使用行级锁:行级锁可以在读取或修改数据时锁定单独的行,而不是整个表。这有助于避免幻读。可以使用SELECT ... FOR UPDATE语句来获取行级锁

  • 使用间隙锁:间隙锁是一种特殊类型的锁,用于防止幻读。它会锁定一个范围内的数据,而不仅仅是单个行, 在InnoDB存储引擎中,使用SELECT ... LOCK IN SHARE MODE语句可以获取间隙锁。(MyISAM引擎不支持间隙锁)

  • 使用乐观锁:乐观锁不会阻塞其他事务,而是在更新数据时检查是否有其他事务已经修改了相同的数据。通常,乐观锁使用版本号或时间戳来实现。

乐观锁解决幻读代码案例

CREATE TABLE account (
    id INT AUTO_INCREMENT PRIMARY KEY,
    deposit DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    version INT NOT NULL DEFAULT 0
);

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.AccountMapper">

    <select id="selectById" resultType="com.example.model.Account">
        SELECT * FROM account WHERE id = #{id}
    </select>

    <update id="updateDeposit" keyProperty="id" parameterType="com.example.model.Account">
        UPDATE account SET deposit = #{deposit}, version = version + 1 WHERE id = #{id} AND version = #{version}
    </update>

</mapper>

@Data
public class Account {
    private int id;
    private BigDecimal deposit;
    private int version;
}

@Slf4j
@Service
public class AccountService {

    @Resource
    private AccountMapper accountMapper;

    // ...其他属性和方法...

    // 使用乐观锁实现转账操作
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public Result transferOptimistic(int fromId, int toId, BigDecimal value) {
        Account from = accountMapper.selectById(fromId);
        Account to = accountMapper.selectById(toId);
      //检查账户中的余额是否足够,如果账户余额不足,回滚当前的事务,即撤销之前的操作。
        if (!isDepositEnough.test(from.getDeposit(), value)) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return Result.DEPOSIT_NOT_ENOUGH;
        }

        from.setDeposit(from.getDeposit().subtract(value));
        to.setDeposit(to.getDeposit().add(value));

        int r1 = accountMapper.updateDepositWithVersion(from);
        int r2 = accountMapper.updateDepositWithVersion(to);

        if (r1 < 1 || r2 < 1) {
            throw new RetryException("Transfer failed, retry.");
        } else {
            return Result.SUCCESS;
        }
    }
}

这里的 updateDepositWithVersion 方法会更新账户的存款金额和版本号,同时检查版本号是否匹配,以实现乐观锁。

虽然这些方法能解决幻读问题, 但是会一定程度上减少并发性能的, 如果性能要求较高, 可以将事务隔离级别设置为RC(Read Committed)

更多精彩内容请关注☄公众号: Coder无霸哥
不要让我们追赶知识, 要让知识追赶我们~

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值