建设网站500错误,建设工程信息网站,aso应用优化,怎么用ps做网站首页图片Redis与MySQL的双写一致性问题更新缓存#xff1f; 删除缓存#xff1f;先更新缓存再更新数据库先更新数据库#xff0c;再更新缓存先删除缓存再更新数据库先更新数据库#xff0c;再删除缓存解决方案1. 重试2. 异步重试2.1 使用消息队列实现重试2.2 Binlog实现异步重试删除…
Redis与MySQL的双写一致性问题更新缓存 删除缓存先更新缓存再更新数据库先更新数据库再更新缓存先删除缓存再更新数据库先更新数据库再删除缓存解决方案1. 重试2. 异步重试2.1 使用消息队列实现重试2.2 Binlog实现异步重试删除3. 延时双删总结参考文章Redis与MySQL双写一致性是指在使用缓存和数据库同时存储数据的场景下( 主要是存在高并发的情况)如何保证两者的数据一致性内容相同或者尽可能接近。 正常业务流程
读没什么问题关键就在于写更新操作这就会出现几个问题了这里是先更新数据库然后对缓存操作。但对于缓存操作是更新缓存还是删除缓存呢或者为什么不是先操作删除、更新缓存在更新数据库呢
总结一下就是到底先操作缓存再操作数据库还是先操作数据库再操作缓存
带着这几个问题接着往下讲。
首先讲一下操作缓存包括两种更新缓存和删除缓存如何选择 更新缓存 删除缓存 假设都先更新数据库因为先操作缓存再操作数据库问题较大后面会讲 更新缓存 先更新数据库再更新缓存。 如果两个请求同时对同一条数据进行修改那么可能出现先后顺序颠倒导致缓存中的数据是旧的。之后的读请求读到的都是旧数据只有当缓存失效后才能从数据库中得到正确的值。 删除缓存 先更新数据库再删除缓存。 会有这样一种情况缓存刚好失效请求B从数据库中查询数据得到旧值。此时请求A更新数据库将新值写入数据库并删除缓存。而请求B又将旧值写入缓存中导致脏数据 从上面看出现脏数据的要求要比更新缓存的要求更多必须满足以下几个条件
缓存失效读请求 写请求并发更新数据库 删除缓存的时间要比读数据库 写缓存时间短
前面两个很好满足我们再看看第三点这个真的会出现吗
数据库在更新时一般是加锁的读操作的速度远快于写操作的所以第三点发生概率极低当然也可能发生 注这里我其实不是很理解单纯看确实发生概率低但如果出现网络延迟等情况呢不也会发生吗希望好心人解惑我反正没理解。 因此在选择删除缓存时还需要结合其他技术来优化性能和一致性。例如
使用消息队列来异步地删除或更新缓存避免阻塞主线程或者丢失消息。使用延时双删来增加删除成功率和减少不一致时间窗口。即在更新数据库后立即删除一次缓存并在一定时间间隔后再次删除一次。 对比 在更新缓存中 每次去更新缓存但是缓存中的数据不一定会被马上读取这就会导致缓存中可能存放了很多不常访问的数据浪费缓存资源。而且很多情况下写到缓存中的值并不是与数据库中的值一一对应的很有可能是先查询数据库再经过一系列「计算」得出一个值才把这个值才写到缓存中。
由此可见这种更新缓存的方案不仅缓存利用率不高还会造成机器性能的浪费。所以我们一般考虑删除缓存 先更新缓存再更新数据库 在更新数据时先将新数据写入缓存Redis再将新数据写入数据库MySQL 但其存在一下问题 缓存更新成功但数据库更新失败导致数据不一致 例用户修改了自己的昵称系统先将新的昵称写入缓存然后再更新数据库。但是在更新数据库的过程中发生了网络故障或者数据库宕机等异常情况导致数据库中的昵称没有被修改。这样就会出现缓存中的昵称和数据库中的昵称不一致的情况。 缓存更新成功但数据库更新延迟导致其他请求读取到旧的数据 例用户下单了一个商品系统先将订单状态写入缓存然后再更新数据库。但是在更新数据库的过程中由于并发量大或者其他原因导致数据库的写入速度慢于缓存的写入速度。这样就会出现其他请求从缓存中读取到订单状态为已支付而从数据库中读取到订单状态为未支付的情况。 缓存更新成功但在数据库更新之前有其他请求查询了缓存和数据库并将旧的数据写回缓存覆盖了新的数据 例用户A修改了自己的头像并上传到服务器上。系统先将新的头像地址写入缓存并返回给用户A显示。然后再将新的头像地址更新到数据库中。但是在这个过程中用户B访问了用户A的个人主页并从缓存中读取到了新的头像地址。由于缓存失效策略或者其他原因比如重启导致缓存被清空或者过期。这时候用户B再次访问用户A 的个人主页并从数据库中读取到了旧的头像地址并将其写回缓存中。这样就会出现缓存中 的头像地址和 数据库 中 的头像地址不一致 的情况。 上面说了一堆其实总结就是缓存更新成功了数据库没更新更新失败导致缓存存的是最新值数据库存的是旧值。如果缓存失效了就会拿到数据库中的旧值。 后面我自己也搞疑惑了既然是因为数据库更新失败导致的问题那我是不是只要保证数据库更新成功就可以解决数据不一致的问题当数据库更新失败时不停的重试更新数据库直到数据库更新完成。
后面发现自己太天真其中存在很多问题比如
如果数据库更新失败的原因是数据库宕机或者网络故障那么你不停地重试更新数据库可能会造成更大的压力和延迟甚至导致数据库恢复困难。如果数据库更新失败的原因是数据冲突或者业务逻辑错误那么你不停地重试更新数据库可能会导致数据丢失或者数据错乱甚至影响其他用户的数据。如果你不停地重试更新数据库那么你需要考虑如何保证重试的幂等性和顺序性以及如何处理重试过程中发生的异常情况。
所以这种方法并不是一个很好的解决方案。 先更新数据库再更新缓存 当有一个更新操作时先更新数据库数据然后再更新对应的缓存数据 但是这种方案也有一些问题和风险比如
如果更新数据库成功了但是更新缓存失败了那么就会导致缓存中就会保留旧的数据而数据库中已经是新的数据即脏数据。如果在更新数据库和更新缓存之间有其他请求查询了同一个数据并且发现缓存存在那么就会从缓存中读取旧的数据。这样也会造成缓存和数据库之间的不一致性。
因此在使用更新缓存操作时无论谁先谁后但凡后者发生异常就会对业务造成影响。还是上面那张图 那么如何处理异常情况来保证数据一致性呢 这些问题的源头都是多线程并发所导致的所以最简单的方法就是加锁分布式锁。两个线程要修改同一条数据每个线程在改之前先去申请分布式锁拿到锁的线程才允许更新数据库和缓存拿不到锁的线程返回失败等待下次重试。这么做的目的就是为了只允许一个线程去操作数据和缓存避免并发问题。
但加锁费时费力肯定不推荐。并且每次去更新缓存但是缓存中的数据不一定会被马上读取这就会导致缓存中可能存放了很多不常访问的数据浪费缓存资源。而且很多情况下写到缓存中的值并不是与数据库中的值一一对应的很有可能是先查询数据库再经过一系列「计算」得出一个值才把这个值才写到缓存中。
由此可见这种更新数据库 更新缓存的方案不仅缓存利用率不高还会造成机器性能的浪费。 所以此时我们需要考虑另外一种方案删除缓存 先删除缓存再更新数据库 当有一个更新操作时先删除对应的缓存数据然后再更新数据库数据 但是这种方案也有一些问题和风险比如
如果删除缓存后更新数据库失败了那么就会导致缓存丢失下次查询时需要重新从数据库加载数据增加了数据库压力和响应时间。如果在删除缓存和更新数据库之间有其他请求查询了同一个数据并且发现缓存不存在那么就会从数据库中读取旧的数据并写入到缓存中。这样就会造成缓存和数据库之间的不一致性。 先更新数据库再删除缓存 当有一个更新操作时先更新数据库数据,再删除缓存 上面其实讲过了我再重复一遍吧
会有这样一种情况缓存刚好失效请求B从数据库中查询数据得到旧值。此时请求A更新数据库将新值写入数据库并删除缓存。而请求B又将旧值写入缓存中导致脏数据 从上面看出现脏数据的要求要比更新缓存的要求更多必须满足以下几个条件
缓存失效读请求 写请求并发更新数据库 删除缓存的时间要比读数据库 写缓存时间短
前面两个很好满足我们再看看第三点这个真的会出现吗
数据库在更新时一般是加锁的读操作的速度远快于写操作的所以第三点发生概率极低
所以解决双写问题更适合的方法是先更新数据库再删除缓存当然具体场景具体分析不定说一定就是这个。
讲解了这些操作后会出现的问题那么为了避免这些问题如何做呢
先删除缓存再更新数据库然后使用异步线程或消息队列来重建缓存。先更新数据库再删除缓存并设置一个合理的过期时间来保证缓存的有效性。使用分布式锁或乐观锁来控制并发访问并保证每次只有一个请求能够操作缓存和数据库
…… 下面讲几种常见的方法以保证双写一致性 解决方案
1. 重试
上面也提到过当第二步操作失败时我就重试嘛尽可能地补救但重试的成本太大上面讲过就不重复了。
2. 异步重试
既然重试方法占用资源那我就做异步。在删除或更新缓存时如果操作失败不立即返回错误而是通过一些机制如消息队列、定时任务、订阅binlog等来触发缓存的重试操作。这样可以避免同步重试缓存时的性能损耗和阻塞问题但也可能导致缓存和数据库的数据不一致的时间较长。
2.1 使用消息队列实现重试
消息队列保证可靠性写到队列中的消息成功消费之前不会丢失重启项目也不担心消息队列保证消息成功投递下游从队列拉取消息成功消费后才会删除消息否则还会继续投递消息给消费者符合我们重试的需求 使用消息队列异步重试缓存的情况是指当信息发生变化时先更新数据库然后删缓存如果删除成功就皆大欢喜如果删除失败则将需要删除的key发送到消息队列。另外有一个消费者线程从消息队列中获取要删除的key并根据key删除或更新Redis中的缓存。如果操作失败则重新发送到消息队列中进行重试。 注也可以不先尝试删除直接发送给消息队列让消息队列 举例来说假设有一个用户信息表需要将用户信息缓存在Redis中。如果采用使用消息队列异步重试缓存的方案可以有以下几个步骤
当用户信息发生变化时先更新数据库并返回成功结果给前端。尝试去删除缓存成功则结束操作失败则将要删除或更新缓存的操作生成一个消息比如包含key和操作类型并发送到消息队列中比如使用Kafka或RabbitMQ。另外有一个消费者线程从消息队列中订阅并获取这些消息并根据消息内容删除或更新Redis中的对应信息。如果删除或更新缓存成功则把这个消息从消息队列中移除丢弃以免重复操作。如果删除或更新缓存失败则执行失败策略比如设置一个延迟时间或者一个重试次数限制然后重新发送这个消息到消息队列中进行重试。如果重试超过一定次数仍然失败则向业务层发送报错信息并记录日志。
2.2 Binlog实现异步重试删除
使用binlog实现一致性的基本思路是利用binlog日志来记录数据库的变更操作然后通过主从复制或者增量备份的方式来同步或者恢复数据。
举例来说如果我们有一个主数据库和一个从数据库我们可以在主数据库上开启binlog日志并设置从数据库作为它的复制节点。这样当主数据库上发生任何变更操作时它会将对应的binlog日志发送给从数据库从数据库则会根据binlog日志来执行相同的操作从而保证数据一致性。
另外如果我们需要恢复某个时间点之前的数据我们也可以利用binlog日志来实现。首先我们需要找到对应时间点之前的最近一个全量备份文件并将其恢复到目标数据库。然后我们需要找到对应时间点之前的所有增量备份文件即binlog日志文件并按照顺序将其应用到目标数据库。这样我们就可以恢复出目标时间点之前的数据状态了。 使用 Binlog 实时更新/删除 Redis 缓存。利用 Canal即将负责更新缓存的服务伪装成一个 MySQL 的从节点从 MySQL 接收 Binlog解析 Binlog 之后得到实时的数据变更信息然后根据变更信息去更新/删除 Redis 缓存MQCanal 策略将 Canal Server 接收到的 Binlog 数据直接投递到 MQ 进行解耦使用 MQ 异步消费 Binlog 日志以此进行数据同步 注binlog日志是MySQL的二进制日志它记录了对数据库的变更操作比如插入、更新、删除等。 binlog日志有两个主要作用一个是主从复制另一个是增量备份。 主从复制是指在一个主数据库和一个或多个从数据库之间实现数据的同步。主数据库会将自己的binlog日志发送给从数据库从数据库则会根据binlog日志来执行相同的操作从而保证数据一致性。这样可以提高数据的可用性和可靠性也可以实现负载均衡和故障转移。 增量备份是指在全量备份的基础上定期备份数据库的变更操作。全量备份是指将整个数据库的数据完整地备份到一个文件中。增量备份则是指将每次变更操作对应的binlog日志文件备份到另一个文件中。这样可以减少备份所占用的空间和时间也可以实现灵活地恢复数据到任意时间点。 至此我们可以得出结论想要保证数据库和缓存一致性推荐采用「先更新数据库再删除缓存」方案并配合「消息队列」或「订阅变更日志」的方式来做。
3. 延时双删
我们重点在将先更新数据库在删除缓存。那如果我要先删除缓存再更新数据库呢
回顾之前讲的先删除缓存再更新数据库它会出现旧值覆盖缓存的问题那好办我们直接把这个旧值给删了不就完了吗延时双删就是这个原理它的基本思路是
先删除缓存再更新数据库休眠一段时间根据系统情况确定再次删除缓存
这样做的目的是为了防止在更新数据库后有其他线程读取到旧的缓存数据并将其写回缓存导致数据不一致。
举个例子假设有一个用户信息表其中有一个字段是用户积分。现在有两个线程A和B同时对用户积分进行操作
线程A要给用户增加100积分线程B要给用户减少50积分
如果使用延时双删策略那么线程A和B的执行过程可能如下 线程A先删除缓存中的用户信息 线程A再从数据库中读取用户信息发现用户积分为1000 线程A将用户积分加上100变为1100并更新到数据库中 线程A休眠5秒假设这个时间足够让数据库同步 线程A再次删除缓存中的用户信息 线程B先删除缓存中的用户信息 线程B再从数据库中读取用户信息发现用户积分为1100因为线程A已经更新了 线程B将用户积分减去50变为1050并更新到数据库中 线程B休眠5秒假设这个时间足够让数据库同步 线程B再次删除缓存中的用户信息
这样最终结果就是数据库中的用户积分为1050缓存中没有该用户信息。当下次有请求查询该用户信息时就会从数据库中读取并写入到缓存中。这样就保证了数据一致性。
延时双删适用于高并发场景特别是对数据的修改操作比较频繁而查询操作比较少的情况。这样可以减轻数据库的压力提高性能同时保证数据的最终一致性。延时双删也适用于数据库有主从同步延迟的场景因为它可以避免在更新数据库后从库还没有同步完成时读取到旧的缓存数据并将其写回缓存。 注: 这个休眠时间 读业务逻辑数据的耗时 几百毫秒。 为了确保读请求结束写请求可以删除读请求可能带来的缓存脏数据。 总结
好了总结一下这篇文章的重点。
Redis与MySQL的双写一致性问题是指在使用缓存和数据库同时存储数据的场景下如何保证两者的数据一致性。这个问题主要涉及到以下几个方面 缓存更新策略缓存更新策略有三种分别是先更新缓存再更新数据库先更新数据库再更新缓存先删除缓存再更新数据库 和先更新数据库再删除缓存。每种策略都有可能导致数据不一致的情况。 数据库主从同步延迟如果使用了主从复制模式来提高数据库的可用性和读写分离能力那么就可能存在主从同步延迟的问题。也就是说在主库上执行了写操作后并不会立即同步到从库上。这样在读取数据时如果从主库读取则可能获取到最新的数据而如果从从库读取则可能获取到旧的数据。这样也会导致与缓存中的数据不一致。
为了解决这些问题 可以采用以下几种方法
采用先删除缓存再更新数据库方案在并发场景下依旧有不一致问题解决方案是延迟双删但这个延迟时间很难评估。采用先更新数据库再删除缓存方案为了保证两步都成功执行需配合消息队列或订阅变更日志的方案来做本质是通过重试的方式保证数据最终一致性。采用先更新数据库再删除缓存方案读写分离 主从库延迟也会导致缓存和数据库不一致缓解此问题的方案是延迟双删凭借经验发送延迟消息到队列中延迟删除缓存同时也要控制主从库延迟尽可能降低不一致发生的概率。 总之根据场景选择适合自己的方案 参考文章
Redis与MySQL双写一致性方案解析 - 知乎 (zhihu.com)
美团二面Redis与MySQL双写一致性如何保证 - 掘金 (juejin.cn)
缓存和数据库一致性问题看这篇就够了 - 知乎 (zhihu.com) 终于水完了这篇文章得去背面经了