记录分布式锁双重检测失效问题处理

1、问题现象

已经使用了分布式锁,控制数据库只会插入一条用户数据,结果出现重复数据,创建时间相同,推测肯定是分布式锁未生效。
 

2、问题分析

源码如下,加锁方式没问题,并发情况下,两个线程都完成首次检查,然后依次进入加锁代码块。问题肯定出在二次检查,后进入线程仍然查出account不存在。通过查看日志,发现其中一个线程的上层调用加了@Transactional注解。在添加了@Transactional注解的Spring方法中,MyBatis-Plus查询确实会只查询一遍,这是由于在事务内,MyBatis的一级缓存(也称为Session缓存)默认会持续到事务结束。这意味着在同一个事务内对相同查询条件的多次查询,通常会直接从一级缓存中获取结果,而不会重新查询数据库。所以二次检测走的是缓存,并没有真正查询数据库。

public Account init(Integer userId) {
        // 首次检查
        Account account = accountMapper.selectOne(
                new QueryWrapper<Account>().lambda()
                        .eq(Account::getUserId, userId)
                        .eq(Account::getDelFlag, 0));
        if (account == null) {
            RLock lock = null;
            try {
                lock = redissonClient.getLock("account_init_lock");
                boolean tryLock = lock.tryLock(10, 10, TimeUnit.SECONDS);
                if (!tryLock) {
                    log.error("获取锁失败");
                    throw new Servicexception(ResultCode.SYSTEM_INNER_ERROR);
                }
                 // 二次检查
                account = accountMapper.selectOne(
                        new QueryWrapper<Account>().lambda()
                                .eq(Account::getUserId, userId)
                                .eq(Account::getDelFlag, 0));

                if (account == null) {

                    Account accountNew = new Account();
                    accountNew.setUserId(userId);
                    
                    accountMapper.insert(accountNew);
                    log.info("用户初始化账户完成 userId:{}", userId);
                    account = accountNew;
                }

            } catch (Exception e) {
                log.error("init异常", e);
                throw new Servicexception(ResultCode.SYSTEM_INNER_ERROR);
            } finally {
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                     lock.unlock();
                }
            }

        }
        return account;
    }
3、问题解决

1、只要让二次检查仍然真正查询即可。默认情况下,二级缓存的刷新策略是“false for select statement; true for insert/update/delete statement.”,使用@Option注解,关闭缓存。

mybaties底层核心代码见org.apache.ibatis.executor.BaseExecutor#query方法

2、设置init方法使用非事务方式运行
 @Transactional(propagation = Propagation.NOT_SUPPORTED)

3、一定要注意,必须使用代理对象调用init()方法。如果是被类中调用,需要注入本身再调用,否则@Transactional不生效


修改后代码如下

    /**
     * 注入本身,使用代理对象调用init方法
     */
    @Autowired
    AccountService accountService;


    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public Account init(Integer userId) {
        // 首次检查,不走缓存
        Account account = accountMapper.selectByUserIdWithoutCache(userId);
        if (account == null) {
            RLock lock = null;
            try {
                lock = redissonClient.getLock("account_init_lock");
                boolean tryLock = lock.tryLock(10, 10, TimeUnit.SECONDS);
                if (!tryLock) {
                    log.error("获取锁失败");
                    throw new Servicexception(ResultCode.SYSTEM_INNER_ERROR);
                }
                 // 二次检查,不走缓存
                account = accountMapper.selectByUserIdWithoutCache(userId);
                if (account == null) {
                    Account accountNew = new Account();
                    accountNew.setUserId(userId);
                    accountMapper.insert(accountNew);
                    log.info("用户初始化账户完成 userId:{}", userId);
                    account = accountNew;
                }

            } catch (Exception e) {
                log.error("init异常", e);
                throw new Servicexception(ResultCode.SYSTEM_INNER_ERROR);
            } finally {
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                     lock.unlock();
                }
            }

        }
        return account;
    }

    @Override
    public boolean 本类中上层调用() {
        // 使用accountService代理对象调用init,否则init的事务注解不生效
        Account account = accountService.init(userId);
        // 其他操作....
        return this.updateById(account);
    }

 mapper中增加方法
 

import xxxx.xxx.xxx.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface AccountMapper extends BaseMapper<Account> {

    // 禁用缓存,并在查询的时候也刷新缓存
    @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE)
	@Select("select * from account where user_id = #{userId} and del_flag = 0")
	Account selectByUserIdWithoutCache(@Param("userId") Integer userId);

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值