分布式缓存

news/2024/4/25 2:00:14/文章来源:https://blog.csdn.net/jx_ZhangZhaoxuan/article/details/127247198

本文介绍关于缓存的常用设计模式。以及如何保证缓存的一致性进行分类讨论。
还会介绍关于缓存失效的常见问题,以及针对缓存失效的解决方法。

在高并发的环境下,比如春节抢票大战,一到放票的时间节点,分分钟大量用户以及黄牛的各种抢票软件流量进入12306,这时候如果每个用户的访问都去数据库实时查询票的库存,大量读的请求涌入到数据库,瞬间Db就会被打爆,cpu直接上升100%,服务马上就要宕机或者假死。即使进行了分库分表也是无法避免的。为了减轻db的压力以及提高系统的响应速度。一般都会在数据库前面加上一层缓存,甚至可能还会有多级缓存。

想要在压力测试中提高接口的吞吐量,就不得不说到缓存这一优化方案。

缓存又分进程内缓存和分布式缓存两种:

  • 本地(进程内)缓存如ehcache、GuavaCache、Caffeine等。
    • 可以简单的在代码中使用诸如Map一类的数据结构,存储数据
  • 分布式缓存如redis、memcached等。
    • 分布式则需要在一个所有节点均能访问到的位置存储数据

那么那些数据适合放入缓存?

  • 及时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)

常用技巧

  • 设置过期时间:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题。

缓存一致性问题

首先,我们得清楚“数据的一致性”具体是啥意思。其实,这里的“一致性”包含了两种情况:

  • 缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;
  • 缓存中本身没有数据,那么,数据库中的值必须是最新值。

常见缓存使用模式

  • Cache-Aside pattern
  • Read-Through
  • Write-Through
  • Write Behind Caching Pattern

Cache-Aside pattern

参考:Microsoft Design Patterns: Cache-Aside pattern

一般我们更新缓存会使用旁路缓存(Cache Aside Pattern)的方式,按需将数据存入缓存,缓存中并不存储所有数据。具体逻辑如下:

  1. 确定数据是否存在于缓存中,存在则直接返回
  2. 如果不在缓存中,则从数据库中读取数据
  3. 将从数据中读取的数据存入缓存

整体流程图

伪代码

data = cache.load(id);  //从缓存加载数据
if (data == null) {data = db.load(id);  //从数据库加载数据cache.put(id, data);  //保存到 cache 中
}
return data;

Java Spring代码

在Spring中可以使用框架中的缓存抽象,可使用@Cacheable注解,如下实现,当getRecordForSearch()方法被调用的时候,如果缓存中存在对应key的数据,那就会自动的从缓存中获取(此时方法体不会被执行),当缓存中不存在key对应数据的时候,会执行方法体从数据库中查询数据并设置到缓存中去。

@Cacheable("default", key="#search.keyword")
public Record getRecordForSearch(Search search)

default 为分区名,key支持spEL表达式,普通字符串必须加单引号,为redis中的键。

这个注解默认不开启锁,使用sync可以开启锁,但是锁的实现方式是使用 synchronized代码块实现的单机锁,在分布式下是锁不住所有节点的。

@Cacheable("default", key="#search.keyword", sync=true)

数据更新

如果数据被更新,我们还需要使用其他策略来修改缓存区的数据。流程一定都是先修改数据库中的数据,之后再来操作缓存里的数据。这里有三种常见的方式。

  • 失效模式,让缓存失效
  • 双写模式,让缓存更新
  • 订阅模式,订阅数据库binlog日志

失效模式,让缓存失效

该情况下,当请求需要更新数据库数据的时候,缓存中的值需要被删除掉(删除掉就表示旧值不可用了),当下次该key被再次查询到就去数据库中查出最新的数据。

顺序问题:那我们应该先删除缓存,再修改数据库呢,还是应该先修改数据库,再删除缓存呢?

▶ 假如我们先删除缓存,再修改数据库。

试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放入缓存中,然后更新操作更新了数据库。于是缓存中的数据还是老数据,导致缓存中的数据是脏的,而且之后缓存中一直是脏数据。

▶ 假如我们先修改数据库,再删除缓存。

比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

但,这个情况理论上会出现,不过,实际上出现的概率可能非常低。

因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

失效模式,在Spring中可以使用@CacheEvict注解,实现如下:

@CacheEvict("default", key="#search.keyword")
public Record updateRecordForSearch(Search search)

双写模式,让缓存更新

缓存数据也可以在数据库更新的时候被更新,从而在一次操作中让之后的查询有更快的查询体验和更好的数据一致性。

顺序问题:那我们应该先更新缓存,再修改数据库呢,还是应该先修改数据库,再更新缓存呢?

▶ 假如我们先更新缓存,再修改数据库。

写+写并发:线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致。

▶ 假如我们先修改数据库,再更新缓存。

写+写并发:与上一条类似,线程A和线程B同时更新同一条数据,更新缓存的顺序是先A后B,但是更新数据库的顺序是先B后A,这也会导致数据库和缓存的不一致。

在Spring中可以使用@CachePut注解,注意函数返回值一定要是存入缓存中的对象。实现如下:

@CachePut("default", key="#search.keyword")
public Record updateRecordForSearch(Search search)

订阅模式,订阅数据库binlog日志

canal

canal [kə'næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

canal 工作原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

Flink CDC

Flink CDC也是阿里的开源技术,这篇官方样例分别使用MySQL和Postgres中的两张表,在其表数据变动后,实时通过流的方式将最新数据写入ES中。

过程中只需要用到Flink SQL,无需一行Java代码,即可实现。

方案总结

上述无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。

  1. 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  2. 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  3. 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  4. 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略)
  5. 使用读写缓存同时操作数据库和缓存时,因为其中一个操作失败导致不一致的问题,可以通过消息队列重试来解决。

总结

  • 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
  • 遇到强一致性的且一定要加缓存的需求,可以使用读写锁来让操作排序。可以通过消息队列重试来解决,缓存或数据库其中一个操作失败的问题。
  • 要想做到强一致,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。

Read/Write Through

我们可以看到,在上面的Cache Aside套路中,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。

所以,应用程序比较啰嗦。而Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。

核心思想:应用需要操作数据时只与缓存组件进行交互;缓存里的数据不会过期。

Read-Through

Read-Through和Cache-Aside很相似,不同点在于程序不需要再去管理从哪去读数据(缓存还是数据库)。

相反它会直接从缓存中读数据,该场景下是缓存去决定从哪查询数据。当我们比较两者的时候这是一个优势因为它会让程序代码变得更简洁。

Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

Write-Through

Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)

下图自来Wikipedia的Cache词条。其中的Memory你可以理解为就是我们例子里的数据库。

Write-Behind

适用场景:读少写多
存在的问题:异步或间隔一定时间的批量回写会导致数据延迟或数据丢失的情形出现。

Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。软件设计从来都是取舍Trade-Off。

另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

在wikipedia上有一张write back的流程图,基本逻辑如下:

缓存失效问题

大并发读下,可能会产生以下几个缓存失效问题。

缓存雪崩

指的是我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

风险 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃。

解决 null结果缓存,并加入短暂过期时间。

缓存击穿

  • 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发的访问,是一种非常“热点”的数据。
  • 如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决 加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查询缓存,就会有数据,不用去db。

使用分布式锁来解决

参考文章

  • 脸书的论文Scaling Memcache at Facebook
  • 阿里的Flink连接器Flink CDC
  • 微软文档Cache-Aside pattern
  • 知乎讲解缓存模式(Cache Aside、Read Through、Write Through)
  • 耗子叔的缓存更新的套路
  • Daniel Wu的博客Cache Consistency with Database

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_398891.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

魔改xxl-job,彻底告别手动配置任务!

xxl-job是一款非常优秀的任务调度中间件,轻量级、使用简单,但是苦于手动注册任务久矣,今天就来魔改一下,实现任务的自动注册!原创:微信公众号 码农参上,欢迎分享,转载请保留出处。哈喽大家好啊,我是Hydra。 xxl-job是一款非常优秀的任务调度中间件,轻量级、使用简单、…

12个小细节让普源示波器使用更加高效(上)

俗话说细节决定成败,示波器作为电子测量的第一工具,虽然使用简单,但并不是每个人都能注意到细节。运用好细节,可以使你的示波器使用更加的便捷。以下由安泰测试带来普源示波器测量相关的12个小细节可作为示波器常识快速自检的小文…

Spring Boot(4):@Import注解和@Conditional注解

说明:基于atguigu学习笔记。 在了解spring boot自动配置原理前,再来了解下两个注解Import注解和Conditional注解。 Import Import注解主要用于导入某些特殊的Bean,这些特殊的Bean和Bean Definitaion 有关。 主要用于导入Configuration 类…

Python实现桌面挂件,做一只可爱的桌面宠物~

文章目录嗨嗨,大家好 ~ 我是小圆相关文件开发工具相关模块:环境搭建安装原理简介1.初始化一个窗口组件:效果2.设置一下窗口的属性:随机导入一张图片,看效果随机导入一个宠物的所有图片的函数代码3.宠物随机出现在桌面上…

服务端渲染的探索与实践

服务端渲染(SSR)近两年炒得很火热,相信各位同学对这个名词多少有所耳闻。本节我们将围绕“是什么”(服务端渲染的运行机制)、“为什么”(服务端渲染解决了什么性能问题 )、“怎么做”(服务端渲染的应用实例与使用场景)这三个点,对服务端渲染进行探索。 服务端渲染是一…

GOM引擎登录器的研究,逆向技术在这款GOM20151108引擎上是一个大舞台

最近遇到一个逆向类课题,是关于GOM20151108版本对应登录器研究。刚接触传奇的时候是2002年,那时候网吧玩SF,都是手动用IP直接连接,当时的一款 联创传奇 很好玩,有传送戒子,木域戒指,土域戒指&am…

不会 Vue,但不影响我学 diff 算法

前言 现在社会各行各业大都面临着寒冬,互联网行业最近还出现了裁员潮,导致前端是越来越卷,普通学校的应届生不仅要跟985、211毕业的学生以及研究生进行竞争,甚至还需要和最近刚被裁的、有了几年工作经验的程序员竞争,…

page.json

uni-app需要给page.json文件需要进行配置路由,否则会不报错,也跳转不过去

【数模/启发式算法】蚁群算法

文章目录简介符号说明核心思想流程图文章使用到的测试函数基本步骤蚁群算法代码简介 蚁群算法是一种用来寻找优化路径的概率型算法。它由Marco Dorigo于1992年在他的博士论文中提出,其灵感来源于蚂蚁在寻找食物过程中发现路径的行为。 这种算法具有分布计算、信息正…

Arduino播放声音

玩软件有点虚无,没有实际东西,所以接下来要体验下硬件与软件结合。 1 Arduino Arduino是一种包含硬件(各种型号的Arduino板)和软件(Arduino IDE)的开源电子平台。硬件部分是可以用来做电路连接的Arduino电…

小白学习Java第四十三天

Git概述 (一)什么是Git Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。版本控制是指对软件开发过程中各种程序代码、配置文件及说明文档等文件变更的管理,是软件配置管理的核心思想之一…

设计模式学习笔记(五) - 观察者模式 Observer

目录 观察者模式 Observer 一、背景描述 Version 1 (面向过程) Version 2 (面向对象) Version 3 (单个观察者) Version 4 (多个观察者) Version 5 (分离观察者与被观察者) 二、不同事件下的观察者模式 三、事件本身也可以形成继承体系 四、观察者常用场景 观察者模式…

Selenium基础 — 鼠标操作

1、鼠标事件介绍 前面例子中我们已经学习到可以用click()来模拟鼠标的单击操作,而我们在实际的web产品测试中发现,有关鼠标的操作,不单单只有单击,有时候还要用到右击,双击,拖动等操作,这些操作…

【Nginx】认识与基本使用 Nginx 实现反向代理、配置负载均衡

文章目录1. Nginx 概述1.1 Nginx 介绍1.2 Nginx 下载和安装1.3 Nginx 目录结构2. Nginx 命令3. Nginx 配置文件结构4. Nginx 具体应用4.1 部署静态资源4.2 反向代理4.2.1 介绍4.2.2 配置反向代理4.3 负载均衡4.3.1 介绍4.3.2 配置负载均衡4.3.3 负载均衡策略1. Nginx 概述 1.1…

Ubuntu开机界面出现“error found when loading /root/.profile”

原因 今天一开始按照一篇文章,想把普通用户的权限提高到最高权限,修改了**/etc/passwd**文件,然后重启,发现之前的用户进不去了,一开机就出现如下信息 解决方法 1、重启虚拟机进入recovery模式(长按shi…

计算机网络-第一章 | 王道考研

目录 一、基本介绍 定义 功能 组成 分类 标准化工作 标准的分类 标准化工作相关组织 二、性能指标 ※ 速率 带宽 ※吞吐量 时延 时延带宽积 往返时延RTT 利用率 三、分层结构 ※ 分层基本规则 正式认识分层 7层OSI参考模型 怎么来的 怎么分的 怎么传的…

<特殊类设计与单例模式>——《C++高阶》

目录 1.请设计一个类,不能被拷贝 2. 请设计一个类,只能在堆上创建对象 3. 请设计一个类,只能在栈上创建对象 4. 请设计一个类,不能被继承 5. 请设计一个类,只能创建一个对象(单例模式) 后记:●由于…

GD32F307VC+WIN10+VSCODE+GCC+JLINK环境build

为了构建Cortex M系列单片机免费开源的开发环境,网络上了解来看VSCODEGCCJLINK是一套比较高效的组合方式,下面记录环境搭建的流程。 我这边的PC环境为 WIN10专业版64bit。 工具准备 1. arm-none-eabi-gcc下载及安装 官网下载链接:Downloa…

c++数据结构:数组和向量

线性表: 在数据元素的非空有限集中 存在唯一的一个被叫做“第一个”的数据元素存在唯一的一个被叫做“最后一个”的数据元素除第一个之外,集合中的每个数据元素均只有一个前驱除最后一个之外,每个集合元素均只有一个后继数据结构中线性结构指…

文字识别检测入门(1)

CTPN 优点:对水平文字检测效果超级好 缺点:对扭曲的文字不好 RRPN 在faster的基础上改进 RPN改为RRPN ROI pooling改进为RROI pooling 能解决旋转,但是解决不了弯曲的曲面问题 EAST Anchor free 特征合并,检测不同尺度文本 检测各…