闽公网安备 35020302035485号
最近线上的一个状态修改功能出现了问题,一开始是运营找了过来,运营告知某条数据的状态已经开启了的,但是实际使用起来还是没有生效,于是拿到这个问题后,首先就去数据库查了这条数据,发现确实如他所说,状态数据是已经更改过的。但是为什么没有生效呢?于是再次查看了获取数据的方法,发现是优先获取的缓存,于是查询缓存里的数据,发现了问题所在,缓存里并没有将这条数据的状态更改过来,也就是缓存数据不一致问题。
继续追查状态修改的方法,发现采用的更新方式是先更新数据库,然后删除缓存。这是我们常用的双写一致性的处理方法,但也正是这样的方式出现了问题。下面我们来详细讲解。
public R save(UserVO userVO) {
User user = new User();
BeanUtils.copyProperties(userVO, user);
saveUser(user);
redisTemplate.delete("userInfo:" + user.getUserName());
return R.success("操作成功");
}
同时在获取数据时,采用的是先从缓存获取,如果缓存没有,就从数据库查询,并同时存放到缓存上,这样保证了下次访问时数据能直接从缓存获取,减少了数据库压力。 public User getByUserName(String userName) {
// 堆代码 duidaima.com
User user = (User) redisTemplate.opsForValue().get(userName);
if (user != null) {
return user;
}
user = this.getOne(Wrappers.<User>lambdaQuery().eq(User::getUserName, userName));
redisTemplate.opsForValue().set("userInfo:" + userName, user);
return user;
}
数据库的数据已经更新成功了,说明数据库是没有报错了,那么哪个地方报错了呢?当然是redis,在执行redis的删除操作时,如果发现错误,比如因为网络问题,或者redis本身服务问题,导致没有正常执行而产生的报错,所以为了捕获这个报错,我们需要添加上数据库事务 @Transactional(rollbackFor = Exception.class)。 @Transactional(rollbackFor = Exception.class)
public R save(UserVO userVO) {
User user = new User();
BeanUtils.copyProperties(userVO, user);
saveUser(user);
redisTemplate.delete("userInfo:" + user.getUserName());
return R.success("操作成功");
}
但是这样你以为就结束了吗,这样的操作确实可以实现事务的回滚,但是上述的模式仅仅只是我们用到的Cache Aside模式(这里大家要了解redis缓存的四种模式),而对于另外一种同样经典的Write Through模式就不再适用于这个操作了。
Write Through模式采用的是读缓存,写时先更新缓存,再更新数据库,一般缓存不设置过期时间,适用于频繁查询缓存数据的场景。因为是先更新缓存,再更新数据库,且在查询操作时不做更新缓存,所以保证了数据的一致性,也防止了缓存击穿。
@Transactional(rollbackFor = Exception.class)
public R save3(UserVO userVO) {
User user = new User();
// 拷贝属性
BeanUtils.copyProperties(userVO, user);
redisTemplate.opsForValue().set(user.getUserName(), user);
userMapper.addUser(user);
return R.success("操作成功");
}
但是这样的操作,肯定是有问题的,因为一旦数据库报错,@Transactional能够保证数据库回滚,但并不能保证redis的事务性,于是我们需要让redis也能保证事务 @Transactional(rollbackFor = Exception.class)
public R save2(UserVO userVO) {
// 本地事务+redis事务 = 双写一致性/缓存强一致性/redis.mysql事务回滚
User user = new User();
// 拷贝属性
BeanUtils.copyProperties(userVO, user);
Object execute = redisTemplate.execute(new SessionCallback<List<Boolean>>() {
@Override
public List<Boolean> execute(RedisOperations operations) throws DataAccessException {
// 事务开启
operations.multi();
// 执行的操作,redis先操作
operations.opsForValue().set(user.getUserName(), user);
try {
saveUser(user);
} catch (Exception e) {
// 事务取消
operations.discard();
e.printStackTrace();
return null;
}
// 事务的提交
return operations.exec();
}
});
return R.status(execute != null);
}
方案二: setEnableTransactionSupport实现redis事务@Configuration
public class RedisConfig {
/**
* 创建RedisTemplate
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
// 创建对象
RedisTemplate redisTemplate = new RedisTemplate();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置redis生成的key的序列化器,对key编码进行处理
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
// 设置redis支持数据库事务
redisTemplate.setEnableTransactionSupport(true);
return redisTemplate;
}
}
开启之后,我们的更新代码就变成了如下所示,非常的简洁清爽 @Transactional(rollbackFor = Exception.class)
public R save3(UserVO userVO) {
User user = new User();
// 拷贝属性
BeanUtils.copyProperties(userVO, user);
redisTemplate.opsForValue().set(user.getUserName(), user);
userMapper.addUser(user);
return R.success("操作成功");
}
测试



