缓存数据一致性通常是指在发生数据更新之后缓存数据与数据库数据保持一致,在实际生产中根据操作分为强一致性和最终一致性。强一致性是缓存中的数据和数据库中的数据无论何时都保持一致,也就是说无论以缓存数据为基准或者以数据库数据为基准取到的数据都是一样的,而最终一致性顾名思义就是缓存数据和数据库数据通过某种技术或者某种手段,最终保持一致,允许短暂的数据不一致的情况发生。
首先,我们要了解为什么会出现缓存数据和数据库数据不一致的情况,当我们发起一条请求,首先会先去缓存中查找,如果找到就返回对应数据,如果找不到就会先去数据库查找,然后将查找到的数据更新到缓存。缓存数据不一致的情况一般发生在分布式环境下的修改数据过程中另一台机器的读取操作。当我们修改了一条数据后去操作数据库数据和缓存数据时一般有两个选择:其一,先删除缓存再更新数据库数据;其二,先更新数据库数据后删除缓存。对于先删除缓存再更新数据库数据的操作显然是会有数据不一致的情况发生,我们删除缓存数据成功,且还未操作数据库的时候,假如此时有一个请求进来,我们在缓存中读不到数据,会去数据库里查询,此时数据库数据尚未更新,取到的是变更前的数据,再写入缓存,数据不一致的情况就会发生。那是不是选择先更新数据库是再删除缓存数据就可以避免数据不一致的情况发生了呢?答案依然是否定的,假设数据库更新成功,缓存删除成功,但数据库更新操作尚未提交成功之时一条请求进来了,一样会造成缓存数据不一致。
笔者曾经在实际工作中也遇到过一个经典的数据一致性问题,修改了一条商品信息后显示修改成功,但是加载到页面的商品信息依然是旧数据,排查数据库存储的数据是修改后的数据,查看缓存数据,也在同一时间点删除了缓存,但是存储的数据依然是旧值,百思不得其解,最终排查原因大致如下:为避免数据在同一时间失效给数据库造成较大压力,依据数据时间敏感度梯度设置缓存失效时间,当时商品缓存信息刚失效,请求A进入查询到数据库信息,这时请求B更新数据库数据并删除缓存,最后请求A将请求到的旧数据写入缓存,最终导致了缓存数据和数据库数据不一致。为了避免这种情况的再次发生,我们重构了部分商品信息相关代码,最终的解决方案是引入了mq,更新完数据之后会发一条mq去删除缓存,并通过MQ的重试机制进行补偿来保证数据最终一致性。
重构前的伪代码:
Public void updateProduct(UpdateProductRequest request){
//操作数据库
//删除缓存
//提交事务
}
重构后的伪代码:
Public void updateProduct(UpdateProductRequest request){
//操作数据库
//发收Mq
//提交事务
}
在实际生产中,缓存数据强一致性的业务场景很少出现,可以通过加分布式锁的方式来实现,当数据更新的时候,加上一把粒度为数据级别的锁,阻塞全部读取操作,等到数据库数据更新并提交成功,缓存删除成功之后释放锁。
归根到底,操作缓存数据和操作数据库数据是在分布式环境下,属于分布式事务的典型问题,没有最好的解决办法,我们只能根据业务需要进行抉择,如果我们的业务不能容忍读取短暂的数据误差,那我们只好牺牲性能加锁来保证数据强一直性,事实上要求数据强一致性的业务场景往往并不需要缓存,因为对于读取数据准确性要求苛刻的业务场景一般不会依赖两个数据源;而如果我们的数据对数据的读取没有苛刻的要求,我们通常会选择数据最终一致性。