缓存数据一致性

提出问题:在使用美团Squirrel 作为分布式缓存 ,那么缓存与数据库的一致性如何保证?如何尽可能拿到最新数据?(以单节点为例)

方案1:先更新数据库后更新缓存​不推荐

分析缺点:
(1)AB两个线程写。A更新数据库->B更新数据库->B更新缓存->A更新缓存 显然出现了数据覆盖,此时数据不一致,是旧数据。

(2)另外在更新数据库的时候还需要更新缓存,如果更新逻辑复杂,导致整个更新周期很长。

解决方案:
对于(1)可以增加版本号,版本号比目前大的才更新。

对于(2)可以考虑任务异步化。

方案2:先删除缓存 再更新数据库

分析缺点:
(1) A 先写B后读。那可能存在这样的序列:A删除缓存->B未命中读数据库后,将其加入缓存->A更新数据库 此时数据发生不一致。

解决办法:
“延迟双删”可以解决,在A线程写入完成之后 ,过M时间再去删除缓存,M时间的大小需要根据B线程将数据库数据放入缓存的时间N 来定,比其稍大即可。

方案3:先更新数据库再删除缓存​推荐

分析缺点:
(1)A 先写B后读。假设存在 A先更新数据库->B读->A删除缓存。这样B读到的是过期数据 但是这是可以忍受的,因为这只是短暂的不一致后续的读都是一致数据。

(2)假设某时刻缓存恰好失效,B线程先读,A线程后写。可能存在这样的操作序列B 读数据库->A更新数据库->A 加入缓存->B加入缓存 此时旧值就会覆盖最新值。其实这种情况发生的比较少,因为读操作应该比数据库更新操作快,所以B加入缓存的这一步很大可能是在A 更新数据库之前的操作。

关于删除缓存失败的解决办法?

我们知道不管是延时双删还是一次删除,那么删除失败那么意味着可能发生数据不一致,所以应该循环重试直到成功。

方案1:使用消息队列,在业务代码中应该在更新数据库之后,在将删除任务发到消息队列中,然后正确消费就可以。

方案2:同样适用消息队列,但是利用binlog,我们知道binlog记录改变数据库的操作,所以写操作会被记录,当其变化时就去产生任务放到队列中,减少了业务代码的侵入。比较推荐。

电商系统中保证数据库与缓存数据一致性可从多个方面着手,以下是一些常见的解决方案: ### 最终一致性策略 多数场景可接受短暂不一致,可通过重试、定时任务(如每天全量同步)修复数据不一致问题。同时,设置合理的 TTL(生存时间),自动淘汰旧数据,还需监控缓存命中率、数据库与 Redis 的数据差异,及时发现不一致并处理[^2]。 ### 常见一致性方案 1. **先更新 DB,再删除缓存(Cache - Aside)**:这是最常用的方案,能实现最终一致,性能高且复杂度低。不过在并发、异常、网络延迟下,可能出现时序错乱,例如线程 A 更新数据库成功后,删除缓存失败(网络超时),此时线程 B 查询缓存会命中旧数据,导致数据库已更新,缓存仍是旧值的不一致情况[^3]。 2. **延迟双删(Delayed Double Delete)**:删除两次缓存,能防止并发问题,一致性较好,性能和复杂度处于中等水平[^3]。 3. **先删缓存,再更新 DB(Write - Behind)**:此方案风险高,不推荐使用,一致性差,性能高但复杂度低[^3]。 4. **基于 Binlog 的异步更新(如 Canal)**:异步监听数据库变更,实现最终一致,性能和复杂度都较高[^3]。 5. **分布式事务(如 Seata)**:能实现强一致,但性能差,复杂度高[^3]。 ### 针对不同并发情况的处理 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新。就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),给缓存加上过期时间依然可以解决大部分业务对于缓存的要求[^4]。 ### 推荐方案 推荐采用 Cache - Aside + 主动失效的方案,这是生产级常用方案[^3]。 以下是一个简单的 Java 示例代码,展示先更新 DB,再删除缓存的基本实现: ```java import java.util.HashMap; import java.util.Map; // 模拟数据库 class Database { private Map<String, String> data = new HashMap<>(); public void updateData(String key, String value) { data.put(key, value); } public String getData(String key) { return data.get(key); } } // 模拟缓存 class Cache { private Map<String, String> cacheData = new HashMap<>(); public void deleteCache(String key) { cacheData.remove(key); } public String getCache(String key) { return cacheData.get(key); } public void setCache(String key, String value) { cacheData.put(key, value); } } public class CacheDatabaseConsistency { private Database database = new Database(); private Cache cache = new Cache(); public void updateDataAndDeleteCache(String key, String value) { // 先更新数据库 database.updateData(key, value); // 再删除缓存 cache.deleteCache(key); } public String getData(String key) { String cacheValue = cache.getCache(key); if (cacheValue != null) { return cacheValue; } // 缓存未命中,从数据库获取 String dbValue = database.getData(key); if (dbValue != null) { // 将数据存入缓存 cache.setCache(key, dbValue); } return dbValue; } public static void main(String[] args) { CacheDatabaseConsistency consistency = new CacheDatabaseConsistency(); consistency.updateDataAndDeleteCache("product1", "New Product Name"); String data = consistency.getData("product1"); System.out.println(data); } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值