Redis - 缓存的双写一致性

文章探讨了在修改数据库时如何保持缓存与数据库的一致性问题,分析了先删除缓存再修改数据库导致的不一致情况,并提出了双删策略和延迟双删策略。然而,这些方法都有其局限性。对于高一致性要求,文章建议使用分布式锁或读写锁来保证线程安全;而对于允许短暂不一致的场景,推荐采用异步通知,通过消息队列实现最终一致性。

概念: 当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

那为什么会有不一致的情况呢?

如果不追求一致性,正常有两种做法

  1. 先修改数据库 后删除旧的缓存
  2. 先删除旧的缓存 再修改数据库

我们以先删除旧的缓存,再修改数据库为例:

  1. 当 线程1 要对数据库做更新操作的时候,先将Redis中旧的缓存删掉
  2. 不巧此时线程之间发生切换,线程2读取缓存,因为被线程1删掉了,所以缓存未命中
  3. 线程2就直接查询数据库,并重建缓存(将此时的数据库数据写回Redis)
  4. 接着又切换回线程1,线程1将数据库中的数据修改为新的值

此时就出现了数据库和缓存中的数据不一致的问题

因此我们不能只进行一次缓存删除操作,要使用双删的方法

  1. 比如先删除旧的缓存,修改完数据库后,再删除一次缓存

但是单纯双删不能解决问题,比如

  1. 当 线程1 要对数据库做更新操作的时候,先将Redis中旧的缓存删掉
  2. 不巧此时线程之间发生切换,线程2读取缓存,因为被线程1删掉了,所以缓存未命中
  3. 线程2就直接查询数据库,获取当前数据库的值,但未重建缓存
  4. 接着又切换回线程1,线程1将数据库中的数据修改为新的值,并再次删除缓存
  5. 此时又切换为线程2,线程2将当时读取到的值写回Redis,又造成了数据不一致

因此我们可以采取 延迟双删策略

还是上面那个例子:

  1. 当 线程1 要对数据库做更新操作的时候,先将Redis中旧的缓存删掉
  2. 不巧此时线程之间发生切换,线程2读取缓存,因为被线程1删掉了,所以缓存未命中
  3. 线程2就直接查询数据库,获取当前数据库的值,但未重建缓存
  4. 接着又切换回线程1,线程1将数据库中的数据修改为新的值,但不马上删除缓存,而是等待一段时间
  5. 切换为线程2,线程2将当时读取到的值写回Redis
  6. 最后切换回线程1,线程1再将Redis中的数据删除

可以看到 延迟双删策略 确实能解决数据一致性的问题,但延迟的时间很难确定,短了怕上面的例子中,第6步先于第5步执行,长了怕在第5步和第6步之间的数据不一致状态持续时间太长

因此我们需要另外的解决方案

针对双写一致性有两种场景: 一致性要求高允许短暂不一致

这两种场景的解决方案不同

一致性要求高

可以使用如下的分布式锁方案

在这里插入图片描述

但是我们可以看到该方案让并发变为了串行,极大降低了性能

因此我们可以使用读写锁

读锁 readLock: 加了读锁之后,其他线程还能继续加读锁和读数据,但是不能写,也不能加写锁

写锁 writeLock:写锁是排他锁,加锁之后,其他线程阻塞,不能进行读写操作

Redission 以及实现了读写锁

代码实例

读锁

在这里插入图片描述

写锁

在这里插入图片描述

其中 redissonClient.getReadWriteLock()中传入的值必须是一样的

允许短暂不一致

实际上的开发过程中,这种场景才是主流

这种场景的解决方法很多,比较常用的方法是 异步通知保持数据的最终一致性

流程图如下:

在这里插入图片描述

修改数据库时,需要发送修改记录给MQ,缓存服务需要监听MQ,根据MQ中的修改记录更新缓存

### 三级标题:Redis 缓存数据库一致性解决方案 在高并发系统中,为了提高性能,通常会使用 Redis 作为缓存来减少对数据库的直接访问。然而,引入缓存后,如何保证缓存数据库之间的数据一致性成为了一个关键问题。以下是几种常见的解决方案,用于解决 Redis 缓存数据库一致性问题。 #### 三级标题:更新数据库 + 更新缓存 在更新数据库的同时更新缓存,这种方法看似简单直接,但在并发情况下无法保证缓存数据库一致性。此外,如果缓存更新失败,会导致缓存中的数据与数据库中的数据不一致。因此,这种方法并不推荐在高并发场景下使用。 #### 三级标题:更新数据库 + 删除缓存 更新数据库后删除缓存,这种方法可以确保在下次读取时从数据库中获取最新的数据并重新填充缓存。然而,在并发情况下,如果在删除缓存和更新数据库之间有其他请求读取缓存,可能会导致读取到旧数据。为了解决这个问题,可以采用延迟删策略,即在更新数据库后立即删除缓存,并在一段时间后再删除一次,以确保所有可能的旧数据都被清除。 #### 三级标题:先更新数据库,再删除缓存 为了保证两步都成功执行,可以配合消息队列或订阅变更日志的方案来做。通过消息队列将更新数据库和删除缓存的操作异步化,并通过重试机制保证数据一致性。这种方法的本质是通过重试的方式保证数据一致性。 #### 三级标题:使用读锁 对于并发几率很小的数据,可以通过加读锁来保证并发读的时候按顺序排好队。读读的时候相当于无锁。这种方法可以有效避免数据不一致问题,但会降低系统性能。 #### 三级标题:监听数据库变更日志 通过监听数据库的 binlog 日志及时去修改缓存,例如使用阿里开源的 Cana,这种方法可以实时更新缓存,但引入了新的中间件,增加了系统的复杂度。 ### 三级标题:代码示例 以下是一个简单的 Python 示例,展示如何使用 Redis数据库进行操作: ```python import redis import mysql.connector # 连接数据库 db = mysql.connector.connect( host="localhost", user="yourusername", password="yourpassword", database="yourdatabase" ) # 连接 Redis r = redis.Redis(host='localhost', port=6379, db=0) def update_data(data_id, new_value): cursor = db.cursor() try: # 更新数据库 cursor.execute("UPDATE yourtable SET value = %s WHERE id = %s", (new_value, data_id)) db.commit() # 删除缓存 r.delete(f"data:{data_id}") except Exception as e: db.rollback() print(f"Error: {e}") finally: cursor.close() def get_data(data_id): # 从缓存中获取数据 cached_data = r.get(f"data:{data_id}") if cached_data: return cached_data else: # 从数据库中获取数据 cursor = db.cursor() cursor.execute("SELECT value FROM yourtable WHERE id = %s", (data_id,)) result = cursor.fetchone() cursor.close() if result: # 将数据缓存 r.setex(f"data:{data_id}", 3600, result[0]) return result[0] else: return None ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值