之前做了用注解的方式管理缓存的功能,但是现在发现可能会发生问题。那就是注解的@CacheEvict和@CachePut行为是立即发生的,而不是事务提交后再调用,而事务回退了缓存也不会回退。
我先测试了@CachePut
@Service
public class MachineRuntimeServiceImpl implements IMachineRuntimeService {
@Override
@Transactional(rollbackFor=Exception.class)
public void saveMachine(String userId, EqMachineInfoModel machine, Long time) throws Exception {
//1.保存当前的机器信息
{
machineDao.save(machine);
if(time != null) {
Thread.sleep(time);
}
if(machine.getMachineName().equals("error"))
throw new Exception("自定义抛出异常");
}
}
}
@Repository
public interface MachineDao extends JpaRepository<EqMachineInfoModel, Integer>{
@Cacheable(value = "machineModel", key = "'machineModel'+#p0")
EqMachineInfoModel findOne(Integer machineId);
@CachePut(value = "machineModel", key = "'machineModel'+#p0.id")
EqMachineInfoModel save(EqMachineInfoModel machine);
}
@Controller
public class TestAction {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
IMachineRuntimeService machineRuntimeService;
@RequestMapping(value="/saveTest")
@ResponseBody
public String saveTest(@RequestParam(value="time",required=false)Long time
, @RequestParam(value="machineId",required=false)int machineId
, @RequestParam(value="machineName",required=false)String machineName) {
String result = "success";
try {
EqMachineInfoModel machine = machineRuntimeService.getMachineById("", machineId);
machine.setMachineName(machineName);
machineRuntimeService.saveMachine("", machine, time);
} catch (Exception e) {
result = "fail";
logger.info(e.getMessage(), e);
// TODO: handle exception
}
return result;
}
@RequestMapping(value="/getTest")
@ResponseBody
public EqMachineInfoModel getTest(@RequestParam(value="machineId",required=false)int machineId) throws Exception {
EqMachineInfoModel machine = machineRuntimeService.getMachineById("", machineId);
return machine;
}
}
发出请求 /saveTest?machineName=error&machineId=10000000&time=0
在save方法后故意抛出异常,结果缓存里的machineName变成了error,而数据库里的machineName并没有变。
@CacheEvict则比较安全,即使修改失败了,缓存也只是被删除而不是变成错误的值。所以我们使用了这种策略,一开始使用的确没有问题,但是后来还是发现了问题。还是事务的问题,save方法时缓存的确被删除了,但是在save方法事务提交之前,如果有另外的接口调用了findOne方法,就会查询到修改前的缓存,这时事务再提交,查询到的还是之前的缓存。当然,概率比较低,但是还是有可能发生。
把@CacheEvict改成@CachePut
@Repository
public interface MachineDao extends JpaRepository<EqMachineInfoModel, Integer>{
@Cacheable(value = "machineModel", key = "'machineModel'+#p0")
EqMachineInfoModel findOne(Integer machineId);
@CacheEvict(value = "machineModel", key = "'machineModel'+#p0.id")
EqMachineInfoModel save(EqMachineInfoModel machine);
}
请求 /saveTest?machineName=mmm2&machineId=10000000&time=10000
在十秒钟之内请求 /getTest?machineId=10000000最后缓存里面还是老的数据,数据库里面是更新后的数据。
要解决这个问题,初步的设想是在事务提交后再更新或删除缓存,不过删除缓存实现会简单很多。
我会尝试用注解切面编程的方式实现。
另一种方式是使用锁,不管是查询还是编辑,跟缓存有关的都加上锁,不过这明显太复杂,并且会影响效率,失去了缓存的初衷。
不过我很好奇,这个问题应该十分常见,难道别人没有碰到过这种问题吗?难道springboot没有考虑到事务的影响吗?所以我还是会找找看有没有现成的方案