如何保证数据库和缓存双写一致性?

本文讨论了在高并发场景下,如何保证数据库和缓存双写一致性的问题,分析了先写缓存、先写数据库、先删缓存、先写数据库等常见方案的优缺点,提出了在并发情况下可能导致的问题,如数据不一致和资源浪费。作者推荐使用先写数据库,再删缓存的方案,并介绍了删除缓存失败时的重试机制,包括定时任务、消息队列(如RocketMQ)和监听MySQL binlog的方法。

前言

数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。

我很负责的告诉大家,该问题无论在面试,还是工作中遇到的概率非常大,所以非常有必要跟大家一起探讨一下。

今天这篇文章我会从浅入深,跟大家一起聊聊,数据库和缓存双写数据一致性问题常见的解决方案,这些方案中可能存在的坑,以及最优方案是什么。

1. 常见方案

通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,我们是这样使用缓存的:

  1. 用户请求过来之后,先查缓存有没有数据,如果有则直接返回。

  2. 如果缓存没数据,再继续查数据库。

  3. 如果数据库有数据,则将查询出来的数据,放入缓存中,然后返回该数据。

  4. 如果数据库也没数据,则直接返回空。

这是缓存非常常见的用法。一眼看上去,好像没有啥问题。

但你忽略了一个非常重要的细节:如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么该如何更新缓存呢?

不更新缓存行不行?

答:当然不行,如果不更新缓存,在很长的一段时间内(决定于缓存的过期时间),用户请求从缓存中获取到的都可能是旧值,而非数据库的最新值。这不是有数据不一致的问题?

那么,我们该如何更新缓存呢?

目前有以下4种方案:

  1. 先写缓存,再写数据库

  2. 先写数据库,再写缓存

  3. 先删缓存,再写数据库

  4. 先写数据库,再删缓存

接下来,我们详细说说这4种方案。

2. 先写缓存,再写数据库

对于更新缓存的方案,很多人第一个想到的可能是在写操作中直接更新缓存(写缓存),更直接明了。

那么,问题来了:在写操作中,到底是先写缓存,还是先写数据库呢?

我们在这里先聊聊先写缓存,再写数据库的情况,因为它的问题最严重。

某一个用户的每一次写操作,如果刚写完缓存,突然网络出现了异常,导致写数据库失败了。

其结果是缓存更新成了最新数据,但数据库没有,这样缓存中的数据不就变成脏数据了?如果此时该用户的查询请求,正好读取到该数据,就会出现问题,因为该数据在数据库中根本不存在,这个问题非常严重。

我们都知道,缓存的主要目的是把数据库的数据临时保存在内存,便于后续的查询,提升查询速度。

但如果某条数据,在数据库中都不存在,你缓存这种“假数据”又有啥意义呢?

因此,先写缓存,再写数据库的方案是不可取的,在实际工作中用得不多。

3. 先写数据库,再写缓存

既然上面的方案行不通,接下来,聊聊先写数据库,再写缓存的方案,该方案在低并发编程中有人在用(我猜的)。

​用户的写操作,先写数据库,再写缓存,可以避免之前“假数据”的问题。但它却带来了新的问题。

什么问题呢?

3.1 写缓存失败了

如果把写数据库和写缓存操作,放在同一个事务当中,当写缓存失败了,我们可以把写入数据库的数据进行回滚。

如果是并发量比较小,对接口性能要求不太高的系统,可以这么玩。

但如果在高并发的业务场景中,写数据库和写缓存,都属于远程操作。为了防止出现大事务,造成的死锁问题,通常建议写数据库和

缓存数据库场景中,保证数据一致性是一个常见但复杂的挑战。由于缓存数据库是两个独立的存储系统,操作无法通过单一事务保证原子性,因此需要采取一些策略来降低数据不一致的风险。以下是几种常见的解决方案: ### 1. 先更新数据库,再更新缓存(Cache Aside 模式) 这是最常见的一种做法。应用首先更新数据库中的数据,待数据库更新成功后,再更新缓存。这样可以保证缓存中的数据最终与数据库保持一致。 ```python # 伪代码示例 def update_data(key, new_value): # 1. 更新数据库 db.update(key, new_value) # 2. 更新缓存 cache.set(key, new_value) ``` 但这种方式在极端情况下可能出现不一致问题。例如,在更新数据库后、更新缓存前发生异常,缓存中的旧数据仍存在,直到下一次读取触发更新。 ### 2. 先删除缓存,再更新数据库(Read Through + Delete) 在更新数据库之前,先删除缓存中的旧值。当后续读取请求到来时,发现缓存中没有数据,会从数据库中加载最新数据并重新缓存。 ```python def update_data(key, new_value): # 1. 删除缓存 cache.delete(key) # 2. 更新数据库 db.update(key, new_value) ``` 这种方式可以避免缓存中存在过期数据的问题,但可能在并发场景下导致缓存穿透,即多个请求同时访问缓存未命中,进而频繁访问数据库。 ### 3. 使用消息队列异步更新缓存(Eventual Consistency) 通过引入消息队列(如 Kafka、RabbitMQ),将数据库更新操作作为事件发布到队列中,由消费者异步更新缓存,实现最终一致性。 ```python def update_data(key, new_value): # 1. 更新数据库 db.update(key, new_value) # 2. 发送更新事件到消息队列 message_queue.publish("cache_update", {"key": key, "value": new_value}) ``` 消费者端: ```python def consume_message(message): key = message["key"] value = message["value"] cache.set(key, value) ``` 这种方式可以解耦数据库缓存更新操作,适用于对一致性要求不是实时的场景。 ### 4. 使用分布式事务或两阶段提交(2PC) 在某些对数据一致性要求极高的系统中,可以考虑使用分布式事务(如 Seata、XA 协议)来保证数据库缓存一致性。但这类方案通常性能开销较大,适用于金融类等对一致性要求非常高的系统。 ### 5. 使用缓存中间件的穿透机制(Write Through) 某些缓存系统(如 Redis + CacheLib)支持 Write Through 模式,即缓存层在入时自动同步数据库。这种方式将一致性逻辑封装在缓存层内部,简化了应用层的处理。 --- ### 总结 不同场景下可选择不同的策略: - **低一致性要求**:使用消息队列实现最终一致性。 - **中等一致性要求**:采用 Cache Aside 或 Read Through + Delete 模式。 - **高一致性要求**:使用分布式事务或专门的缓存穿透机制。 每种方案都有其适用场景局限性,实际应用中应根据业务需求、性能约束系统复杂度进行权衡选择。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值