在前面的博客中,我有介绍过缓存一致性问题。本篇我打算详细分析各种情况,给出最优的解决方案:
其中保证缓存一致性问题常有以下四种方式:
- 先更新缓存,再更新数据库
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
场景一和场景二很少使用,因为都存在脏数据的可能性。导致原因基本类似:先进行的操作成功,但后进行的操作失败。一般线上更多使用场景三或者场景四
对于场景三,单线程场景下可以保证缓存一致性,但多线程场景下仍有可能出现脏数据情况:
线程a 是写请求,线程 b 是读请求,两个线程并发执行:
- 线程 a 删除缓存,准备执行更新数据库操作
- 线程 b 判断缓存中没数据,去数据库读取到还未更新的数据
- 线程 b 刷新脏数据到缓存中
- 线程 a 接着执行,完成更新数据库操作
对于上述问题,最简单的解决方法就是延时双删除:先删除缓存,再更新数据库,延迟一会再删除缓存
这里延时一会的主要原因在于删的过快,读取的线程可能还没有将脏数据刷新到缓存中,具体延迟的时间可以根据业务自行估算
上面说的是线程交替执行的场景,实际即使线程没有交替执行仍有可能造成脏数据问题:线程 a 删除缓存,并更新数据库成功。线程 b 发现缓存没有数据,从数据库读取到脏数据。既然线程 a 已经更新成功,为什么线程 b 还能读取到脏数据呢,实际道理很简单:数据库主从机制:
一般公司数据库线上一定会做读写分离,涉及到读写分离一定涉及到数据库同步延时问题。数据库压力比较大时,可能得过几秒才能同步到最新的数据,此时从库读取到数据一般就是脏数据。对于该问题解决办法也很简答,对于刷新缓存的读请求,强制在主库执行
除了上面提到的双删法,还有一种比较麻烦的思路:通过内存队列记录任务,依次执行。比如当缓存中没有数据时,将读取缓存操作和更新缓存操作依次打入队列。此时更新数据库操作肯定排再前面,也可以保证读取的总是读取到最新数据。需要注意的一点是,不是每个请求都需要去执行读取和更新,如果队列中已经存在读取操作,直接等待队列执行完从缓存获取即可
对于场景四同样存在脏数据问题:更新数据库成功,删除缓存失败。此时缓存中的数据就和数据库中的数据不一致。解决办法有很多,try-catch 括起来删除缓存操作,如果执行失败打标记,新增其它删除逻辑,比如发 mq 异步再删除或者线程池异步再删除