如何保证缓存数据的一致性?

文章探讨了在缓存查询和更新中如何处理数据库与缓存的一致性问题。在高并发场景下,先更新数据库再删除缓存可能导致短暂不一致,但能保证最终一致性;而先删除缓存可能引发问题,可通过延迟双删策略优化。先更新数据库的CacheAside模式在某些情况下也可能导致不一致,但概率较低。

缓存查询

  1. 先查询缓存
  2. 若失败,则查询数据库
  3. 重建缓存

缓存更新

缓存更新可以沿着

  • 先更新数据库,再更新缓存
  • 先更新缓存,再更新数据库

在常规场景下,两者均可以,不会出现不一致问题。

不过如果后更新的失败了,那么先更新数据库的情况会是更优的选择。因为只是缓存出现不一致,但是相应的操作逻辑与数据库数据保持了一致,并且缓存可以通过消息队列进行重试或者是订阅mysql binlog同步数据

考虑高并发情况

先更新数据库在高并发的情况下,可能会出现

  1. 线程t1更新数据库x=1
  2. 线程t2更新数据库x=2,并且更新缓存x=2
  3. 线程t1更新缓存x=1

先更新缓存在高并发情况下也是同理

为了应对这种情况,最直接的方法是对于高并发数据x加锁,只允许同一时间一个线程更新数据x;或者设置较短的缓存时间

另外一种方法则是不更新缓存而是删除缓存,而删除缓存也分为两种情况

  • 先更新数据库,后删除缓存
  • 先删除缓存,后更新数据库

如果采用先删除缓存,那么在考虑查询重建缓存的情况下,比如

  1. 线程t1删除缓存x=1
  2. 线程t2无法从缓存查到x,于是便查询数据库x=1后重建缓存
  3. 线程t1更新数据库x=2

此时数据库x值为x,缓存值为1,还是会出现不一致的情况

为了解决这个问题,存在延迟双删的手段来进行优化,具体就是

  1. 删除缓存
  2. 更新数据库
  3. 等待
  4. 再删除缓存

但这仅仅只是降低了出现不一致的概率

而若采用先更新数据库,那么即使

  1. 线程t1更新数据库x=2
  2. 线程t2查询到缓存x后返回
  3. 线程t1删除缓存

也只是线程t2查询到不是最新的缓存,但是缓存和数据库最终会保持一致性

不过先更新数据库,后删除缓存的cache aside模式在

  1. 线程t1读取缓存x没有命中,读取数据库x=1
  2. 线程t2更新x=2,并删除缓存x
  3. 线程t1更新缓存x=1

此时读线程还是会以数据库旧值重建缓存造成不一致

不过这种情况发生的概率不高,需要读线程读缓存恰好失效,写线程速度快于读线程

Ref

  1. https://xiaolincoding.com/redis/architecture/mysql_redis_consistency.html
Caffeine二级缓存保证数据一致性可采用以下方法: #### 缓存更新策略 - **同步更新**:当数据发生变更时,先更新数据库,再同步更新Caffeine本地缓存和Redis分布式缓存。此方式能保证缓存数据库数据实时一致,但在高并发场景下,可能会因同步操作影响系统性能。 ```java public void updateData(Data data) { // 更新数据库 database.update(data); // 更新Caffeine缓存 caffeineCache.put(data.getId(), data); // 更新Redis缓存 redisTemplate.opsForValue().set(data.getId(), data); } ``` - **异步更新**:数据变更时,先更新数据库,然后通过消息队列(如Kafka、RabbitMQ)异步更新Caffeine和Redis缓存。这种方式可减少对系统性能的影响,但存在一定延迟,可能在短时间内出现缓存数据库数据不一致的情况。 ```java public void updateDataAsynchronously(Data data) { // 更新数据库 database.update(data); // 发送消息到消息队列 messageQueue.send(data.getId()); } // 消息队列消费者 @RabbitListener(queues = "cache-update-queue") public void handleCacheUpdateMessage(String dataId) { Data data = database.get(dataId); // 更新Caffeine缓存 caffeineCache.put(dataId, data); // 更新Redis缓存 redisTemplate.opsForValue().set(dataId, data); } ``` #### 缓存失效策略 - **主动失效**:数据更新时,先更新数据库,然后主动删除Caffeine和Redis中的缓存。下次查询时,若缓存不存在,再从数据库中读取数据并更新缓存。 ```java public void updateDataAndInvalidateCache(Data data) { // 更新数据库 database.update(data); // 删除Caffeine缓存 caffeineCache.invalidate(data.getId()); // 删除Redis缓存 redisTemplate.delete(data.getId()); } ``` - **设置合理的过期时间**:为Caffeine和Redis缓存设置合理的过期时间,当缓存过期后,自动从数据库中重新加载数据。这样可在一定程度上保证数据的时效性,但可能会在过期瞬间出现数据不一致的情况。 ```java // Caffeine缓存设置过期时间 Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .build(); // Redis缓存设置过期时间 redisTemplate.opsForValue().set(data.getId(), data, 10, TimeUnit.MINUTES); ``` #### 分布式锁 在更新缓存操作时,使用分布式锁(如Redis分布式锁、ZooKeeper分布式锁)确保同一时间只有一个线程能更新缓存,避免并发更新导致的数据不一致问题。 ```java public void updateDataWithDistributedLock(Data data) { // 获取分布式锁 if (redisLock.tryLock(data.getId())) { try { // 更新数据库 database.update(data); // 更新Caffeine缓存 caffeineCache.put(data.getId(), data); // 更新Redis缓存 redisTemplate.opsForValue().set(data.getId(), data); } finally { // 释放分布式锁 redisLock.unlock(data.getId()); } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值