缓存一致性的常见方案

本文深入探讨了四种常见的缓存策略,包括旁路缓存模式、读穿透模式、直写模式和异步回写模式,详细分析了每种模式的优缺点及适用场景,并提出了针对数据一致性的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Cache Aside Pattern - 旁路缓存模式
  • 读请求:如果未命中缓存则查询数据库并更新至缓存,否则返回缓存中数据
  • 写请求:先更新数据库,再删除缓存(非延迟双删)

在这里插入图片描述

写请求为啥不更完DB直接更缓存?

Cache Aside 模式的读请求处理流程应该很好理解,但对于写请求大家或许会有疑问,为何写完库不直接更缓存?从直觉上而言直觉更缓存似乎更容易被理解,但实际上要从两个方面考虑:性能与安全。

  • 从性能方面考虑,当写请求较多时,可能出现一个线程刚更完缓存就被重新更新了(此现象业界称之为缓存扰动),那么机器的性能会被白白浪费,缓存利用率也不高,而等到读请求读完再更新缓存,也符合懒加载的思想。
  • 从安全性上考虑,当出现写写线程并发执行时,缓存容易出现脏数据,下面会逐步踩坑。

开始踩坑

1、先更数据库,再更缓存?(写写情况下易出问题)

假如执行过程为:

1、线程1更新数据库

2、线程2更新数据库

3、线程2更新缓存

4、线程1更新缓存

很明显执行完毕后,缓存中的数据为脏数据,出现数据不一致问题。

而之所以大概率出现这个问题,是因为过程3与4的执行均为操作缓存,速度差不多,所以缓存出现脏数据的概率很大。

在这里插入图片描述

2、先删缓存,再更新数据库?(写读情况下易出问题)

假设执行过程为:

1、写请求先删除缓存

2、读请求查询缓存,发现不存在,则查询数据库

3、读请求写入缓存

3、写请求更新数据库

在这里插入图片描述

3、先更数据库,再删缓存(推荐)

假设执行过程为:

1、读请求先查询缓存,未击中,查询数据库返回18

2、写请求更新数据库,删除缓存(删了个寂寞)

3、读请求才回写缓存

同理也有可能出现数据不一致问题,但实际上出现概率会很小,因为数据库的更新操作要比内存操作慢几个数量级,所以理论上过程3回写入缓存速度会快于过程2更新数据库的操作。如何避免这种问题?通常我们会有一个兜底方案,就是设置缓存过期时间,允许一定时间内的数据库与缓存的数据不一致。

但加上过期时间并非就是完美的,假设过程2写完库后,删除缓存失败,那么也会导致数据不一致问题的出现,而考虑到这种场景,第一种方案是直接对更新数据库与删除缓存的操作加一把分布式锁,但加锁必然带来吞吐量的下降,需综合考虑,另外一种则是对删除操作做一些补偿的措施,下面谈及延迟删除策略会对删除缓存的补偿方案做进一步的解析。

在这里插入图片描述

4、结合2和3,做延迟双删(推荐)

1、先删除缓存
2、更新数据库
3、休眠一会儿(比如1s),之后再删一次缓存数据
在这里插入图片描述
上述方案在极端情况下,如果第三步删除失败仍然可能导致数据一致性问题,解决方案有:

1、引入MQ重试机制。假设更完库后删除失败,则把失败的key丢到MQ中,由mq消费端拉出来进行删除重试。这种方案的弊端是对于删除失败的处理逻辑需要基于业务代码的 trigger 来触发,对业务代码侵入性较为严重。

2、基于数据库binlog的方式增量解析、订阅和消费。为了保证删除成功,可以利用阿里巴巴开源中间件canal订阅binlog发送到MQ中,再利用MQ的ACK机制来保证删除成功,最终保证数据缓存一致性(比如更新了uid=2这个用户信息,那么可以读取binlog中uid=2的log,然后删除缓存中key={user:2}这个key)。

在这里插入图片描述

延伸1:Alibaba开源组件canal
  • canal的原理:
    模仿MySQL Slave发送dump请求到MySQL服务,MySQL服务接受到该请求后,会将binary log推送给canal server,由Canal Server解析binary log对象(byte流),也就是我们常说的binlog。再由canal client拉取进行消费,且canal server也支持投递到如Kafka、RocketMQ这样的MQ系统中,让其他系统可以消费到,在其ACK机制的加持下,无论推送或拉取都可以保证数据按预期被消费到。
  • canal依赖于zookeeper来实现HA,并且为了减轻MySQL dump的压力,canal同一时刻只允许一个server instance处于运行状态,其他instance则为standby状态。处理之外,为了保证消费的有序性,对于一个instance同一时刻只能由一个canal client进行get/ack等操作。
延伸2:MySQL主从部署,如何解决主从同步延迟?

**监听从库的binlog,**保证最终删除的操作一定发生在更新数据库之后。

如下图,就是A先删除缓存(不管成功或失败都不影响,因为失败了最终通过binlog会删除的),再更新DB,而此时因为主从可能存在延迟,所以B在cache miss之后从从库可能读取到旧数据写入缓存(脏数据)或者还有一种情况就是A删除缓存失败并且同步有延迟,那么B读取旧缓存,但是不影响,因为最后主从同步成功之后通过canal将binlog数据写入MQ,消费者可以根据更新的log数据删除缓存,并且通过ACK机制确认处理这条更新log,以保证数据缓存一致性。
在这里插入图片描述

Read Though Pattern - 读穿透模式

简单来说,区别于Cache Aside的是应用程序不需再管理缓存和数据库,只需从独立的缓存提供程序Cache Provider中获取即可。

好处:是独立管理可以减少数据源上的负载,也让应用端的容错性更佳(如果缓存服务挂了,Cache Provider仍然可以通过直接转到数据源上进行操作,不影响应用端的使用)

适用场景:多次请求相同数据的场景。(guavacache采用该模式)
在这里插入图片描述

Write Trough Pattern - 直写模式

与Read Aside类似,增加了一个Cache Provider代理层来处理更新底层数据源和缓存。与Cache Aside不同的是,在写请求更完库之后Write Through是直接写入缓存,而不是删除缓存。

适用场景:写操作较多,且一致性要求高的场景,并且为了避免前面提到并发双写导致的一致性问题,需要给更新数据库和更新缓存的操作加一把分布式锁,牺牲掉一部分的性能。
在这里插入图片描述

Write Behind Pattern - 异步回写模式

简单来说就是先写缓存,再由Cache Provider定时在数据库负载较低的时候写入数据库。可以看出,此方案的缓存与数据库为弱一致性,且有丢数据的风险,需做好缓存的高可用,此方案对于一致性要求高的系统应慎用。

适用场景:写入速度快,适用于大量写入的场景(实际上在大型互联网应用中大多都是读多写少的场景),电商秒杀场景中库存的扣减常用该模式。

在这里插入图片描述

Write Around

对于一些一致性要求较低的业务,也可以选择Wrtie Aound模式。在该模式下,读请求对于缓存的写入都需要设一个expired time,而写请求在更新数据库后不会对缓存有任何操作。这种方案的优点很明显,实现简单,缺点是数据的弱一致性。

总结

四种策略:
  • Cache Aside Pattern 首选先写入数据库后删除缓存的策略,再增加缓存时间兜底。对于一致性要求更高的场景,考虑用订阅MySQL binlog+MQ的方式做延迟双删。
  • Read/Write through Pattern 增加一个Cache Provider 对外提供读写操作,解耦且避免缓存服务挂掉给应用系统带来的影响。
  • Write Behind Pattern 应用只写缓存,再由Cache Provider定时入库,数据一致性差
  • Write Around 由读请求写缓存并设置一个过期时间,写请求只负责写库

参考文献
https://github.com/CoderLeixiaoshuai/java-eight-part/blob/master/docs/redis/高并发场景下,到底先更新缓存还是先更新数据库?.md#cache-aside

https://www.modb.pro/db/237472

https://xie.infoq.cn/article/1322475e05c11bd2aacd8bc73

https://cdmana.com/2022/01/202201191744387996.html

<think>好的,用户正在寻找关于数据库缓存一致性的解决方案。我需要先理解数据库缓存一致性是什么,然后结合现有的引用资料来给出解决方案。 首先,数据库缓存一致性指的是在使用缓存(如Redis、Memcached)时,如何确保缓存中的数据与底层数据库的数据保持一致。当数据在数据库中被修改后,如果缓存没有相应更新,就会出现不一致的情况,导致应用程序读取到过期的数据。 根据用户提供的引用资料,引用[4]提到要确保数据库在系统故障和事务失败时保持一致性,并发控制管理器需要控制并发事务的交互。这可能涉及到事务管理和锁机制,保证在更新数据库缓存时的原子性。比如,使用事务来同时更新数据库缓存,或者采用两阶段提交协议。 引用[1]指出,涉及关键业务数据如金钱、密钥等不应放在缓存中,这可能影响到缓存策略的设计,比如对于关键数据,可能需要更严格的一致性保证,或者避免缓存这类数据。但这和缓存一致性解决方案也有关系,比如哪些数据需要缓存,如何保证它们的同步。 引用[3]提到内存数据库缓存的高性能优势,但高性能的同时需要一致性,可能涉及到缓存失效策略,如写穿透(Write-Through)或延迟写入(Write-Behind)。例如,写穿透会在更新数据库的同时更新缓存,确保数据一致性,但可能增加延迟。 引用[2]讨论了MySQL在线备份的并发处理、缓冲池效率等,虽然没有直接讲缓存一致性,但处理并发和缓冲池效率的方法可能对缓存策略有参考价值,比如如何在高并发下保证缓存数据库的数据同步。 接下来需要考虑常见的解决方案,比如缓存失效策略、双写策略、使用消息队列异步更新、分布式锁等。此外,还有基于日志的解决方案,如MySQL的binlog监听,当数据库变更时,通过解析binlog来更新缓存,这样可以保证缓存的及时更新。 另外,需要考虑在分布式系统中,缓存一致性可能面临的挑战,比如网络延迟、分区容忍性等,这时候可能需要权衡一致性和可用性,采用最终一致性模型。例如,设置缓存的过期时间,确保即使出现不一致,也能在一定时间后自动刷新。 还要注意在更新数据库缓存时的顺序问题,比如先更新数据库再删除缓存Cache-Aside模式),或者先删除缓存再更新数据库,不同的顺序可能带来不同的并发问题,需要根据场景选择合适的方法。 最后,结合用户提供的引用,需要将解决方案与这些资料关联起来,比如事务控制、并发管理、缓存策略的选择等,确保回答有依据,并且引用正确的来源。</think>### 数据库缓存一致性解决方案 数据库缓存一致性是分布式系统中常见的核心问题,主要解决缓存与底层数据库的数据同步和一致性保障。以下是常见的解决方案及其技术原理: --- #### 1. **缓存失效策略(Cache Invalidation)** - **原理**:通过主动或被动方式使缓存数据失效,强制下次读取时从数据库加载最新数据。 - **方法**: - **写穿透(Write-Through)**:更新数据库时同步更新缓存。例如,若更新数据库字段$A$,则同时更新缓存中的$A$[^3]。 - **延迟写入(Write-Behind)**:先更新缓存,再异步批量更新数据库。适用于高吞吐场景,但需容忍短暂不一致。 - **缓存删除(Cache-Aside)**:更新数据库后直接删除缓存,下次查询时重新加载。例如: ```sql UPDATE users SET name = 'Alice' WHERE id = 1; -- 更新数据库 DELETE user:1 FROM cache; -- 删除缓存 ``` --- #### 2. **基于事务的原子操作** - **原理**:利用数据库事务的原子性,确保缓存数据库操作的原子提交。 - **方法**: - **两阶段提交(2PC)**:将缓存更新和数据库修改绑定为事务,任一失败则回滚。例如: ```python with db.transaction(): db.execute("UPDATE accounts SET balance = 100 WHERE id = 1") cache.set("account:1", 100) # 事务提交时同步执行 ``` - **缺点**:性能开销较大,适用于强一致性要求的场景[^4]。 --- #### 3. **基于日志的增量同步** - **原理**:通过数据库日志(如MySQL的binlog)捕获数据变更,触发缓存更新。 - **流程**: 1. 监听数据库的变更日志。 2. 解析日志中的操作(如INSERT/UPDATE/DELETE)。 3. 根据操作更新或删除缓存。 - **优势**:解耦数据库缓存,支持最终一致性[^2]。 --- #### 4. **版本控制与乐观锁** - **原理**:为数据添加版本号,确保仅当缓存版本与数据库一致时才更新。 - **实现**: - 数据库表中增加`version`字段,例如: $$ \text{表结构: } (id, data, version) $$ - 更新时检查版本号: ```sql UPDATE table SET data = 'new_data', version = version + 1 WHERE id = 1 AND version = 2; ``` - 若更新成功,则同步更新缓存中的数据和版本号。 --- #### 5. **分布式锁控制并发** - **原理**:在并发写操作时,通过锁机制避免缓存数据库的竞争。 - **示例**: ```python lock = acquire_lock("user:1") # 获取分布式锁(如Redis RedLock) try: data = db.query("SELECT * FROM users WHERE id = 1") cache.set("user:1", data) finally: release_lock(lock) ``` - **适用场景**:高并发写操作(如秒杀系统)。 --- #### 6. **最终一致性模型** - **原理**:允许短暂的不一致,通过异步机制最终达到一致状态。 - **方法**: - 设置缓存过期时间(TTL),例如:`cache.set(key, value, ttl=60)`。 - 结合消息队列(如Kafka)异步处理缓存更新。 --- ### 关键挑战与权衡 - **性能 vs 一致性**:强一致性(如2PC)牺牲性能,最终一致性(如TTL)牺牲实时性。 - **关键数据限制**:涉及财务、密钥等敏感数据时,需避免缓存或使用强一致性方案[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值