只读缓存
操作顺序 | 是否高并发 | 潜在问题 | 现象 | 应对方案 |
---|---|---|---|---|
先删除缓存,再更新数据库 | 否 | 缓存删除成功,数据库更新失败 | 会从数据库读到旧值 | 重试数据库更新 |
先删除缓存,再更新数据库 | 是 | 缓存删除后,尚未更新数据库,有并发读请求 | 并发读请求读到数据库旧值,并更新到缓存,导致之后的读请求读到旧值 | 延迟双删 |
先更新数据库,再删除缓存 | 否 | 数据库更新成功,缓存删除失败 | 会从缓存读到旧值 | 重试缓存删除 |
先更新数据库,再删除缓存 | 是 | 数据库更新成功,尚未删除缓存 | 会从缓存读到旧值 | 不一致的情况短暂存在,对业务影响较小 |
读写缓存
操作顺序 | 是否高并发 | 潜在问题 | 现象 | 应对方案 |
---|---|---|---|---|
先更新缓存,再更新数据库 | 否 | 缓存更新成功,数据库更新失败 | 会从缓存中读到最新值,短期影响不大 | 重试数据库更新 |
先更新数据库,再更新缓存 | 否 | 数据库更新成功,缓存更新失败 | 会从缓存读到旧值 | 重试缓存更新 |
1、先更新数据库,再更新缓存 | 写+读并发 | 线程A先更新数据库,之后线程B读取数据,之后线程A更新缓存 | B会命中缓存,读取到旧值 | A更新缓存前,对业务有短暂影响 |
2、先更新缓存,再更新数据库 | 写+读并发 | 线程A先更新缓存成功,之后线程B读取数据,此时线程B命中缓存,读取到最新值后返回,之后线程A更新数据库成功 | B会命中缓存,读取到最新值 | 业务没影响 |
3、先更新数据库,再更新缓存 | 写+写并发 | 线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致 | 数据库和缓存的不一致 | 写操作加分布式锁 |
4、先更新缓存,再更新数据库 | 写+写并发 | 与场景3类似,线程A和线程B同时更新同一条数据,更新缓存的顺序是先A后B,但是更新数据库的顺序是先B后A,这也会导致数据库和缓存的不一致 | 数据库和缓存的不一致 | 写操作加分布式锁 |
场景1和2对业务影响较小,场景3和4会造成数据库和缓存不一致,影响较大。也就是说,在读写缓存模式下,写+读并发对业务的影响较小,而写+写并发时,会造成数据库和缓存的不一致。
针对场景3和4的解决方案是,对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,这样同一时间只允许一个线程去更新数据库和缓存,没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。
综上,使用读写缓存同时操作数据库和缓存时,因为其中一个操作失败导致不一致的问题,同样可以通过消息队列重试来解决。而在并发的场景下,读+写并发对业务没有影响或者影响较小,而写+写并发时需要配合分布式锁的使用,才能保证缓存和数据库的一致性。
另外,读写缓存模式由于会同时更新数据库和缓存,优点是,缓存中一直会有数据,如果更新操作后会立即再次访问,可以直接命中缓存,能够降低读请求对于数据库的压力(没有了只读缓存的删除缓存导致缓存缺失和再加载的过程)。缺点是,如果更新后的数据,之后很少再被访问到,会导致缓存中保留的不是最热的数据,缓存利用率不高(只读缓存中保留的都是热数据),所以读写缓存比较适合用于读写相当的业务场景。