目录
一、问题现象
核心系统进行交易时,偶现交易日期与日切日期不一致的情况。
二、初步分析
1、日切日期统一由日切服务维护,每次交易前,会先调用日切服务获取当前日切日期,因此,问题源头应在日切服务;
2、因为日切服务只做简单的更新和查询操作,而且不一致的情况属于偶现,所以不应该是日切的功能出现异常;
3、日切服务维护了日切日期的缓存,因此,数据不一致的问题可能出在缓存上;
三、问题定位
1、通过梳理代码,分析日切缓存的运作机制
(1)调用日切服务进行日切时,会首先查询得到当前日切日期。
(2)然后,查询当前日切信息;操作数据库,在当前日切日期上加1天;将之前获取的日切信息插入操作历史中;查询更新后的日切日期,并返回。
(3)在进行日切日期更新前,会先清空缓存(注:CacheEvict的beforeInvocation参数设置为true,表示在方法执行前清空缓存,默认为false)。
2、至此,得到完整的日切操作流程如下:
获取当前日期-->清空缓存-->更新数据库-->查询更新后的结果并返回。
单独执行此流程不会存在任何问题,缓存数据与数据库保持一致。
但是,清空缓存到数据写入数据库之间存在时间差(当数据库分主从时,主从同步的时间差也应被考虑进来),在此时间差内,如果有查询操作,会将当前未被修改的日切日期写入缓存。
如此一来,当数据被写入数据库后,缓存与数据库的数据即不一致,查询得到的结果都是缓存中的脏数据,真相大白。
四、解决方案
将缓存清空的时间,调整到更新数据库之后进行
五、问题发酵
解决了上一个问题之后,在梳理代码的过程中,发现了另外一个设计问题,即:在事务中使用缓存
在事务中使用缓存是一个典型的设计漏洞,现以本场景为例进行说明
日切时,在事务中进行的数据操作为(@Transactional方法内):
获取当前日切信息-->更新数据库-->清空缓存-->将之前获取的日切信息插入日切历史表-->查询当前最新日切日期(先查缓存,缓存为空,然后查数据库)–>写入缓存
分析可知,清空缓存到插入日切历史表之间存在时间差,在此时间差内,如果有查询操作,则最新的数据在事务完成前被写入缓存中,如果插入日切历史表失败,导致事务回滚,则数据库中日切日期与缓存不一致。
对于事务而言,其中使用缓存会存在的最大风险便是:事务回滚后,缓存与数据库中数据不一致。
六、最终方案
进行日切操作时,去除缓存。
七、总结
1、使用缓存时,一定要考虑缓存的更新时机,对于高并发场景,很容易造成数据不一致的问题;
2、切记:事务中不要使用缓存,否则剁手;