redis 缓存更新一致性

本文深入探讨了数据库和缓存同步中的更新策略,包括先更新数据库还是缓存、删除缓存或更新缓存的不同组合,分析了每种策略下可能的并发错误及解决方案,最后介绍了异步更新方案。

当执行写操作后,需要保证从缓存读取到的数据与数据库中持久化的数据是一致的,因此需要对缓存进行更新。

因为涉及到数据库和缓存两步操作,难以保证更新的原子性。

在设计更新策略时,我们需要考虑多个方面的问题:

  • 对系统吞吐量的影响:比如更新缓存策略产生的数据库负载小于删除缓存策略的负载
  • 并发安全性:并发读写时某些异常操作顺序可能造成数据不一致,如缓存中长期保存过时数据
  • 更新失败的影响:若某个操作失败,如何对业务影响降到最小
  • 检测和修复故障的难度: 操作失败导致的错误会在日志留下详细的记录容易检测和修复。并发问题导致的数据错误没有明显的痕迹难以发现,且在流量高峰期更容易产生并发错误产生的业务风险较大。

更新缓存有两种方式:

  • 删除失效缓存: 读取时会因为未命中缓存而从数据库中读取新的数据并更新到缓存中
  • 更新缓存: 直接将新的数据写入缓存覆盖过期数据

更新缓存和更新数据库有两种顺序:

  • 先数据库后缓存
  • 先缓存后数据库

两两组合共有四种更新策略,现在我们逐一进行分析。

并发问题通常由于后开始的线程却先完成操作导致,我们把这种现象称为“抢跑”。下面我们逐一分析四种策略中“抢跑”带来的错误。

先更新数据库,再删除缓存

若数据库更新成功,删除缓存操作失败,则此后读到的都是缓存中过期的数据,造成不一致问题。

可能发生的并发错误:

时间线程A线程B数据库缓存
1缓存失效v1null
2从数据库读取v1v1null
3更新数据库v2null
4删除缓存v2null
5写入缓存v2v1

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

同删除缓存策略一样,若数据库更新成功缓存更新失败则会造成数据不一致问题。

可能发生的并发错误:

时间线程A线程B数据库缓存
0v0v0
1更新数据库为v1v1v0
2更新数据库为v2v2v0
3更新缓存为v2v2v2
4更新缓存为v1v2v1

当两个写线程发生冲突时,可以通过比较数据版本方式避免线程A写入旧的数据。

先删除缓存,再更新数据库

可能发生的并发错误:

时间线程A线程B数据库缓存
1删除缓存v1null
2缓存失效v1null
3从数据库读取v1v1null
4更新数据库为v2v2null
5将v1写入缓存v2v1

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

若缓存更新成功数据库更新失败, 则此后读到的都是未持久化的数据。因为缓存中的数据是易失的,这种状态非常危险。

因为数据库因为键约束导致写入失败的可能性较高,所以这种策略风险较大。

可能发生的并发错误:

时间线程A线程B数据库缓存
0v0v0
1更新缓存为v1v0v1
2更新缓存为v2v0v2
3更新数据库为v2v2v2
4更新数据库为v1v1v2

异步更新

双写更新的逻辑复杂,一致性问题较多。现在我们可以采用订阅数据库更新的方式来更新缓存。

阿里巴巴开源了mysql数据库binlog的增量订阅和消费组件 - canal。

我们可以采用API服务器只写入数据库,而另一个线程订阅数据库 binlog 增量进行缓存更新的策略。

这种策略存在和先更新数据库后删除缓存类似的并发问题:

时间读线程写线程异步线程数据库缓存
1缓存失败v1null
2从数据库读取v1v1null
3更新数据库为v2v2null
4删除缓存/更新缓存v2null
5写入缓存v2v1

这个问题同样可以采用异步线程更新缓存,且写入缓存时比较数据版本的方法来解决。

### 产生原因 Redis 缓存一致性问题产生的主要原因在于数据在数据库(如 MySQL)和 Redis 缓存之间的更新操作存在时间差和并发问题。 - **更新操作顺序问题**:在更新数据时,若先更新数据库再更新缓存,在更新数据库后、更新缓存前,有其他请求读取缓存,就会读到旧数据;若先更新缓存更新数据库,在更新缓存后、更新数据库前,数据库更新失败,就会造成缓存和数据库数据不一致。 - **并发读写问题**:多个客户端同时对同一数据进行读写操作时,可能会出现数据不一致。例如,一个客户端正在更新数据库,另一个客户端同时读取缓存,此时可能读到旧数据。 ### 解决方案及应对策略 #### 缓存失效策略 - **Cache-Aside Pattern(旁路缓存模式)**:这是最常用的模式。读操作时,先从缓存中读取数据,若缓存中不存在,则从数据库中读取,并将数据写入缓存;写操作时,先更新数据库,再删除缓存。这样下次读取时会从数据库加载最新数据到缓存。 ```python import redis import pymysql # 连接 Redis 和 MySQL r = redis.Redis(host='localhost', port=6379, db=0) conn = pymysql.connect(host='localhost', user='root', password='password', database='test') def get_data(key): data = r.get(key) if data is None: cursor = conn.cursor() cursor.execute(f"SELECT * FROM table WHERE key = '{key}'") data = cursor.fetchone() if data: r.set(key, str(data)) return data def update_data(key, new_data): cursor = conn.cursor() cursor.execute(f"UPDATE table SET data = '{new_data}' WHERE key = '{key}'") conn.commit() r.delete(key) ``` - **Read-Through Pattern(读穿模式)**:应用程序只与缓存交互,缓存负责从数据库中加载数据。当缓存中不存在数据时,缓存会自动从数据库中读取数据并更新缓存中。 - **Write-Through Pattern(写穿模式)**:写操作时,应用程序先将数据写入缓存缓存立即将数据写入数据库,保证缓存和数据库数据一致。但这种模式会增加写操作的延迟。 #### 延迟双删策略 在更新数据库后,先删除缓存,然后在一段时间后再次删除缓存。这样可以确保在第一次删除缓存后,有其他请求将旧数据写入缓存的情况下,第二次删除缓存能保证数据一致性。 ```python import time def update_data_with_delay(key, new_data): cursor = conn.cursor() cursor.execute(f"UPDATE table SET data = '{new_data}' WHERE key = '{key}'") conn.commit() r.delete(key) time.sleep(1) # 延迟 1 秒 r.delete(key) ``` #### 消息队列 使用消息队列(如 Kafka、RabbitMQ)来保证数据一致性。在更新数据库后,发送一条消息到消息队列,消费者从消息队列中读取消息并删除缓存。这样可以确保在数据库更新成功后,缓存一定会被删除。 ```python from kafka import KafkaProducer producer = KafkaProducer(bootstrap_servers='localhost:9092') def update_data_with_queue(key, new_data): cursor = conn.cursor() cursor.execute(f"UPDATE table SET data = '{new_data}' WHERE key = '{key}'") conn.commit() producer.send('cache-delete', key.encode()) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值