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);
}