麻烦不断的分布式缓存。。 分布式缓存操作异常怎么解决
suiw9 2024-12-17 16:13 30 浏览 0 评论
缓存能加快数据的访问速度,几乎每个软件都会使用这一技术。
自1968 年 在 360/85 系统上引入高速缓存(cache)一词以来,缓存技术经历了多次迭代更新, 还出现了许多种缓存框架和工具,以降低其使用门槛和风险。
在分布式技术中, 缓存尤为重要,相关使用方法和介绍文档也相当丰富。
然而,互联网技术历史中不乏因缓存异常导致的重大故障:
- 2012 年,Facebook 的 Memcached 缓存更新异常, 导致用户看到了错误的信息;
- 2013 年,Google 的 Spanner 数据库缓存更新异常, 使得数百万用户无法使用 Google 服务;
- 2016 年,亚马逊 AWS 云服务由于 Elastic Load Balancer 缓存未能正确更新,造成大量网站和应用程序停机。
这些案例引发我们深思:我们是否真正会用缓存?是否所有应用场景都适合引入缓存?在哪些情况下,缓存可能会造成严重损害?
接下来,我们将基于大厂 的实践经验,通过具体案例分析缓存使用中可能遇到的可用性和一致性问题,并 探索解决这些问题的方法,以深化对缓存技术的理解,并确保其更好地服务于我 们的应用。
01
只要使用缓存,就会存在可用性风险
在系统链路上增加一个环节就会增加可用性风险。
尽管缓存的引入提升了数据访问速度,但缓存架构的复杂性也给系统引入了更多的可用性风险,令系统更加脆弱。
因此,在设计缓存系统时,必须充分考虑这些风险,并采取相应的措施来确保系统的稳定性和可靠性。
1. 缓存加载不当导致服务器宕机
缓存通常架设在数据库之前,用于缓存常用数据,以加快访问速度并减轻数据库的负担。为了保持缓存数据与数据库中的数据尽可能一致,需要对缓存数据进行刷新。然而,一旦缓存刷新策略不当,就可能会对数据库造成严重影响。
下面以会员系统缓存刷新为例进行分析。
会员系统存储着用户的基础信息,这类数据的写入和更新频率不高,但读取量大,非常适合放入缓存中。
图1展示了一种利用缓存 JAR 包的方法。服务 提供方将缓存功能封装在一个 JAR 包中,供服务调用方系统集成。这样,服务调 用方可以像访问本地数据一样轻松、迅速地获取远程数据。
图1 缓存 JAR 包的利用过程
在现实中,由于项目时间紧迫或开发者经验不足,缓存刷新方法可能非常简单,例如设定固定的过期时间,一旦缓存数据失效,就立即刷新缓存数据。
这种方法在缓存数据量较小的情况下通常不会出现问题。然而,它存在一个致命的缺点:可能导致大部分数据在同一时刻失效,进而导致所有缓存 JAR 包在同一时刻发起查询请求,将数据更新到缓存中。
一旦大量查询请求集中在同一时间点到达会员系统,就可能使会员系统的数据库过载,导致宕机,从而使整个会员服务不可用。
为了解决这个问题,可以通过调整缓存刷新的频率来减轻数据库的压力。例如,在缓存失效时间上增加随机数,以错开缓存刷新的高峰期,避免集中刷新对服务器造成过大的压力。这种方法可以有效地规避因集中刷新而导致的系统崩溃。
2. 缓存刷新不当导致服务宕机
除了注意缓存刷新的时机,缓存刷新的小细节也同样重要。
如图2所示,这种做法在大多数情况下可能没有问题,但如果远程调用服务 userService.queryAllUsers 时出现网络抖动,缓存就可能会变成空值。
在这种情况下,由于无法从缓存中找到数据,所以系统可能再次触发缓存刷新逻辑,导致远程调用,而远程调用由于网络抖动无法快速返回结果,从而引发服务雪崩, 导致服务调用方和服务提供方全部宕机。
图2 错误的缓存刷新的代码
一个相对更严谨的做法是在远程调用获取数据结果后,再将新的数据结果赋给原缓存变量。这样即使远程调用出现异常,缓存内容也不会为空。
然而,这种全量刷新缓存数据的方法可能会对系统资源造成较大压力。
一个更好的做法是,当服务端数据变化时,通过推送的方式对缓存进行增量刷新。
这样可以更有效地更新缓存,减少对系统资源的消耗。如图 3所示, 代码稍作调整,采用推送方式进行缓存增量刷新。
图3 调整后的缓存刷新的代码
3. 本地缓存不当导致服务宕机
缓存 JAR 包对服务调用方友好,因为它提供了一种便捷的方法来获取缓存数 据。然而,由于缓存 JAR 包寄宿在服务调用方系统中,需要注意以下一些潜在的风险。
缓存刷新的任务量过大:当缓存刷新的任务量过大时,可能会导致服务调用方的负载急剧增加,甚至引发宿主系统崩溃。这是因为缓存刷新通常涉及大量数 据的读取和写入操作,如果这些操作过于频繁或数据量过大,可能就会超出服务 器的处理能力。
缓存 JAR 包中缓存的数据量过大:如果缓存 JAR 包中缓存的数据量过大, 就可能会直接影响宿主系统的稳定性。例如,过大的缓存数据量可能会导致频繁 的垃圾回收(Full GC),这会严重影响系统的响应时间和吞吐量。
4. 分布式缓存穿透击垮数据库
若换成分布式缓存,是不是能够一劳永逸地解决问题呢?会员系统将数据都存储在分布式缓存中的具体情况如图4所示。
图4 分布式缓存示例
当查询的数据已存在于分布式缓存中时,直接返回结果可以提高查询效率。
然而,如果部分数据本来就不存在,直接查询数据库并在返回数据库结果的同时将结果写入缓存中,就可能导致问题。
如果服务调用方在缓存中找不到数据,它就会继续查询数据库;如果数据库也找不到,就可能导致服务调用方不断重试查 询,最终可能引起雪崩效应,击垮数据库。
对于分布式缓存中的数据也需要提前预热,对于不存在的数据需要在缓存中构建特殊空对象以防止缓存被穿透。
02
只要使用缓存,就会存在数据不一致问题
从原理上来说,同一份数据既放到缓存中又存储在数据库中,就一定会带来数据一致性的挑战。
尽管可以通过各种策略和技术手段来减少数据不一致的时间窗口, 例如设置合理的缓存过期时间、使用缓存预读取和后写入机制、实施分布式锁等, 但这些措施并不能从根本上杜绝数据不一致的问题。接下来将分场景论述数据不一致的根源。
1. 数据不一致的本质分析
(1)纯写场景。在正常的业务处理逻辑完成后,可以在本地事务结束之后, 通过回调方法 afterCompletion 将模型写入缓存,如图5所示。
图5 纯写场景
写入缓存的请求可能会失败,导致数据库中有数据而缓存中却没有相应的数据。为了处理这种情况,需要实施一个补偿方案。
具体来说,当缓存中缺少数据时, 系统应该查询数据库,并将查询结果重新写入缓存。在纯写场景中,由于数据库已经包含了最新的数据,因此不会出现数据一致性问题。
在这种情况下,主要关注的是分布式缓存的命中率,即缓存中的数据与数据库中的数据保持一致的频率。如果缓存命中率较低,则意味着系统需要频繁查询数据库来获取缺失的数据,这会增加数据库的负载,从而降低系统的整体性能。
(2)纯删场景。这个场景也是比较简单的,先将缓存中的数据删除,再删 除数据库中的数据,如图6所示。
图6 纯删场景
先删除缓存中的数据再删除数据库中数据的风险在于,最终数据库事务提交可能会失败,这可能导致数据不一致。
为了降低数据不一致的概率,可将删除缓存数据的操作放在最后一步,即在所有业务逻辑处理完毕后再调用删除缓存数据 的方法。
即使缓存数据被删除,但数据库中的数据依然存在,最终读取到的数据库数据不会是脏读。
因此,在纯删场景下,实际上并不存在数据不一致的问题。
(3)纯读并写场景。为了提高缓存命中率并确保数据的最终一致性,常见的做法是首先尝试从缓存中读数据。如果缓存中没有数据(即缓存未命中),则回退到数据库中读数据。一旦从数据库中获取数据,不论是空数据还是有实际内 容的数据,都应该将其更新回缓存中,以便后续的请求能够直接从缓存中获取数 据,减少数据库的访问压力。
这种策略如图7所示。
图7 纯读并写场景
在系统中仅涉及数据读取操作,而不包含数据更新、删除或写入的场景下, 不存在数据不一致的问题。
(4)纯更新场景。在这个场景中,由于数据库和缓存的操作不是原子性的, 无论是先更新数据库还是先更新缓存,都存在数据不一致的风险。
如图8所示, 无论先更新数据库,而缓存更新失败,还是先更新缓存,而数据库更新失败,都会导致数据不一致。这是因为这两个操作不能保证同时成功,所以无法实现强一致性,只能追求最终一致性。
图8 纯更新场景
清晰地认识问题的本质是我们选择解决方案的基础。为了减轻数据库的压力并确保其高可用性,缓存仅是一种手段。
为了维护数据的最终一致性,我们必须优先确保数据库数据的正确性,然后尽最大努力去修正缓存中的数据。
在图8所示的纯更新场景中,应该首先确保数据库更新成功。
然后,可以持久化一个缓存补偿任务,这个任务会在数据库事务提交后执行,用于更新缓存。
最后,通过这个缓存补充任务来检查数据库与缓存的数据一致性。如果发现不一致,应该以数据库的数据为准来修正缓存中的数据。
因此,数据库与缓存之间的数据不一致窗口期取决于缓存写入的成功率,以及定时补偿任务的执行频率。
这种方式,可以最大限度地减少数据不一致的可能 性,并确保系统最终达到一致性状态。
(5)综合场景。以上论述的场景是在仅考虑单一场景的理想情况下进行的 推演(实际上一个系统中不太可能只有数据写入而没有数据更新)。
然而,在现实中,系统通常涉及多种操作,包括数据的读取、写入、更新和删除。
假设需要删除数据,即使缓存和数据库的删除操作都成功执行,仍然存在一种情况:在删除操作之后,并发的读请求可能会将旧数据重新写入缓存, 如图9所示。
这是因为,在多线程或分布式系统中,可能会有多个请求同时进行,其中一些请求可能在删除操作之后但缓存补偿任务执行之前到达。这种情况下,数据的一致性可能会受到影响,因为缓存中可能会短暂地存储过时的数据。
图9 综合场景
2. 减少不一致窗口的方案
虽然数据不一致性在某种程度上是不可避免的,但这并不意味着我们无法对其进行优化。
当优化的效果达到投入与产出比的最佳平衡时,实际上问题也就得到了有效解决。
整个优化思路如图10所示。
图10 减少不一致窗口的方案
具体步骤如下。
(1)本地事务中更新业务数据和持久化缓存补偿任务:在本地事务中,首 先更新数据库的业务数据。同时,在事务中持久化一个缓存补偿任务,这个任务 包含了更新缓存所需的信息。
(2)事务提交后更新分布式缓存:当数据库事务成功提交后,执行之前持 久化的缓存补偿任务。将最新的数据模型存放到分布式缓存中,确保缓存与数据 库的数据一致。
(3)数据版本控制:存入缓存的数据应该包含版本信息,以便检测数据的 新旧。可以选择数据的最新修改时间作为版本号,这样在读取数据时可以比较版 本号,确保使用的是最新数据。
(4)查询请求中的缓存补偿:当查询请求在缓存中找不到数据时,触发缓 存补偿机制。从数据库的主库中捞取最新的数据进行补偿,确保缓存中数据的准确性。
在处理修改和删除场景时,需要特别注意几个容易出错的地方,以确保数据 的一致性和准确性。
(1)使用排他锁:在修改或删除数据时,应该对数据记录加上排他锁 (Exclusive Lock),以防止并发操作导致缓存中出现脏数据。
排他锁可以确保在锁释放之前,其他事务无法读取或修改相同的数据,从而避免了并发问题。
(2)更新缓存前的再次读取:如果系统中没有排他锁的条件或者无法使用排 他锁,那么在更新缓存之前,应该从缓存中再次读取数据。
将这次读取到的数据与新更改的数据合并,然后再次放入缓存。这样做可以在一定程度上避免数据不一致,尽管可能会丢失本次修改的内容,但这是局部的 数据丢失,而不是数据错误。
(3)补偿任务使用主库:在执行缓存补偿任务时,一定要使用主数据库(主库)。
如果系统设计中包含了主库和读库(从库),那么使用读库进行补偿可能会导致数据同步延迟,出现数据不一致的时间窗口。使用主库可以确保补偿任务获 取的是最新的、已经提交的数据,从而提高数据的一致性。
相关推荐
- 看完这一篇数据仓库干货,终于搞懂什么是hive了
-
一、Hive定义Hive最早来源于FaceBook,因为FaceBook网站每天产生海量的结构化日志数据,为了对这些数据进行管理,并且因为机器学习的需求,产生了Hive这们技术,并继续发展成为一个成...
- 真正让你明白Hive参数调优系列1:控制map个数与性能调优参数
-
本系列几章系统地介绍了开发中Hive常见的用户配置属性(有时称为参数,变量或选项),并说明了哪些版本引入了哪些属性,常见有哪些属性的使用,哪些属性可以进行Hive调优,以及如何使用的问题。以及日常Hi...
- HIVE SQL基础语法(hive sql是什么)
-
引言与关系型数据库的SQL略有不同,但支持了绝大多数的语句如DDL、DML以及常见的聚合函数、连接查询、条件查询。HIVE不适合用于联机事务处理,也不提供实时查询功能。它最适合应用在基于大量不可变数据...
- [干货]Hive与Spark sql整合并测试效率
-
在目前的大数据架构中hive是用来做离线数据分析的,而在Spark1.4版本中spark加入了sparksql,我们知道spark的优势是速度快,那么到底sparksql会比hive...
- Hive 常用的函数(hive 数学函数)
-
一、Hive函数概述及分类标准概述Hive内建了不少函数,用于满足用户不同使用需求,提高SQL编写效率:...
- 数仓/数开面试题真题总结(二)(数仓面试时应该讲些什么)
-
二.Hive...
- Tomcat处理HTTP请求流程解析(tomcat 处理请求过程)
-
1、一个简单的HTTP服务器在Web应用中,浏览器请求一个URL,服务器就把生成的HTML网页发送给浏览器,而浏览器和服务器之间的传输协议是HTTP,那么接下来我们看下如何用Java来实现一个简单...
- Python 高级编程之网络编程 Socket(六)
-
一、概述Python网络编程是指使用Python语言编写的网络应用程序。这种编程涉及到网络通信、套接字编程、协议解析等多种方面的知识。...
- [904]ScalersTalk成长会Python小组第20周学习笔记
-
Scalers点评:在2015年,ScalersTalk成长会Python小组完成了《Python核心编程》第1轮的学习。到2016年,我们开始第二轮的学习,并且将重点放在章节的习题上。Python小...
- 「web开发」几款http请求测试工具
-
curl命令CURL(CommandLineUniformResourceLocator),是一个利用URL语法,在命令行终端下使用的网络请求工具,支持HTTP、HTTPS、FTP等协议...
- Mac 基于HTTP方式访问下载共享文件,配置共享服务器
-
方法一:使用Python的SimpleHTTPServer进行局域网文件共享Mac自带Python,所以不需要安装其他软件,一条命令即可...
- 使用curl进行http高并发访问(php curl 大量并发获得结果)
-
本文主要介绍curl异步接口的使用方式,以及获取高性能的一些思路和实践。同时假设读者已经熟悉并且使用过同步接口。1.curl接口基本介绍curl一共有三种接口:EasyInterface...
- Django 中的 HttpResponse理解和用法-基础篇1
-
思路是方向,代码是时间,知识需积累,经验需摸索。希望对大家有用,有错误还望指出。...
你 发表评论:
欢迎- 一周热门
-
-
Linux:Ubuntu22.04上安装python3.11,简单易上手
-
宝马阿布达比分公司推出独特M4升级套件,整套升级约在20万
-
MATLAB中图片保存的五种方法(一)(matlab中保存图片命令)
-
别再傻傻搞不清楚Workstation Player和Workstation Pro的区别了
-
如何提取、修改、强刷A卡bios a卡刷bios工具
-
Linux上使用tinyproxy快速搭建HTTP/HTTPS代理器
-
Element Plus 的 Dialog 组件实现点击遮罩层不关闭对话框
-
日本组合“岚”将于2020年12月31日停止团体活动
-
SpringCloud OpenFeign 使用 okhttp 发送 HTTP 请求与 HTTP/2 探索
-
MacOS + AList + 访达,让各种云盘挂载到本地(建议收藏)
-
- 最近发表
- 标签列表
-
- dialog.js (57)
- importnew (44)
- windows93网页版 (44)
- yii2框架的优缺点 (45)
- tinyeditor (45)
- qt5.5 (60)
- windowsserver2016镜像下载 (52)
- okhttputils (51)
- android-gif-drawable (53)
- 时间轴插件 (56)
- docker systemd (65)
- slider.js (47)
- android webview缓存 (46)
- pagination.js (59)
- loadjs (62)
- openssl1.0.2 (48)
- velocity模板引擎 (48)
- pcre library (47)
- zabbix微信报警脚本 (63)
- jnetpcap (49)
- pdfrenderer (43)
- fastutil (48)
- uinavigationcontroller (53)
- bitbucket.org (44)
- python websocket-client (47)