分布式缓存

article/2025/9/21 22:37:53

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

在高并发的环境下,比如春节抢票大战,一到放票的时间节点,分分钟大量用户以及黄牛的各种抢票软件流量进入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://chatgpt.dhexx.cn/article/2Zncmhfi.shtml

相关文章

详解分布式系统的缓存设计

作者:vivo互联网服务器团队-Zhang Peng ​ 一、缓存简介 1.1 什么是缓存 缓存就是数据交换的缓冲区。缓存的本质是一个内存 Hash。缓存是一种利用空间换时间的设计,其目标就是更快、更近:极大的提高。 将数据写入/读取速度更快的存储&#xf…

今天带你了解-分布式缓存(一)

在网站架构的衍化历程中,当网站遇到性能瓶颈时,首先想到的解决方案就是使用缓存。 缓存指将数据存储在较高访问速度的存储介质中,以供系统处理。一方面缓存访问速度快,可以减少数据的访问时间,另一方面如果缓存的数据…

深入浅出分布式系统中的缓存架构

缓存,已经是一个老生常谈的技术了,在高并发读的情况下对于读服务来说可谓是抗流量的银弹。 高并发三大利器:缓存、限流、降级。 今天我们就来谈谈缓存。对于缓存,我的理解是让数据更接近于用户,目的是让用户的访问速…

分布式缓存灵魂十连,你能坚持几个?

点击上方蓝色“方志朋”,选择“设为星标” 回复“666”获取独家整理的学习资料! 目录 前言 目前工作中用到的分布式缓存技术有redis和memcached两种,缓存的目的是为了在高并发系统中有效降低DB的压力,但是在使用的时候可能会因为缓…

Webform 常用控件

Webform 常用控件 一,简单控件 1,Lable——标签:在网页中呈现出来的时候会变成span标签 属性:Text——标签上的文字 BackColor,ForeColor——背景色,前景色 Font——字体 Bold-加粗 Italic-倾斜 Under…

Web窗体(WebForm)

一.简介 0. 页面的生命周期。 1. WebForm后台页面类继承于Page类,Page类实现了IHttpHandler接口。 2. 前台页面类继承于后台页面类。 3. 先调用PageLoad方法,再调用Render方法生成html代码。 二. 加密安全 互联网没有绝对的安全,登…

ASP.NET Web Form学习

ASP.NET Web Form学习 0.aspx与html 它如何工作? 从根本上讲,ASP.NET 页面与 HTML 完全相同。 HTML 页面的扩展名是 .htm 或 .html。假如浏览器从服务器请求某张 HTML 页面,服务器不进行任何修改,就会把该页面发往浏览器。 A…

forms.Form和forms.ModelForm

forms.ModelForm是forms.Form的升级版 forms.Form验证规则 2.1 forms.py 2.2 view.py 把我们写的UserResetForm导入到view.py 2.3 模板 forms.ModelForm验证规则 3.1 models.py 3.2 forms.py就用上面模型类里面的验证规则 3.3 view.py 3.4 模板看你实际的情况 forms.…

WebForm与MVC混用

在现有的WebForm项目中加入MVC,可以吗? 西蒙说,可以。 怎么加呢? 我的开发环境是:WIN7 IIS7.5 VS2012 一、WebForm项目添加引用: 我都是选了最高的版本。 二、将MVC项目的部分文件拷贝到WEBFORM项目 …

ASP.NET WebForm+Vue.js

QQ:285679784 欢迎加入博主CSDN资源QQ群799473954(附加信息:CSDN博客)一起学习 ! 参考原文:https://blog.csdn.net/myppbird/article/details/85598154 Vue.js教程:http://www.runoob.com/vue2/vue-tutorial.html Vue.js Ajax…

解析ASP.NET WebForm和Mvc开发的区别

因为以前主要是做WebFrom开发,对MVC开发并没有太深入的了解。自从来到创新工场的新团队后,用的技术都是自己以前没有接触过的,比如:MVC 和EF还有就是WCF,压力一直很大。在很多问题都是不清楚的情况下,问周围的人,别人也只是给自己讲一个大概。而且前两天因为问了一个比较…

C#-WebForm-WebForm开发基础

1、C/S  客户端应用程序 WinForm  WPF  平级 数据是存放在其他的电脑上或服务器上 需要从服务器上下载相应的数据,在本地电脑上的客户端里进行加工 数据的加工是在用户的电脑上执行的,会对用户的电脑配置有所要求 2、B/S  网页端应用程序 ASP.NE…

ASP.NET--WebForm框架

WebForm框架 WebForm是微软推出的一款为了吸引更多的其他开发者能够快速入门到.NET技术中的一个框架,让开发人员感觉使用.NET技术进行BS开发和使用.NET技术进行CS开发一样简单快速。 微软的想法:以后程序员开发网站项目不需要先学习HTML+CSS就能够直接入门BS,因此将HTML标…

ASP.NET(二)--WebForm框架

WebForm框架 WebForm框架WebForm介绍WebForm窗体框架事件驱动开发简单应用ASP.NET程序开发过程网页生成过程的分析案例 WebForm框架 WebForm是微软推出的一款为了吸引更多的其他开发者能够快速入门到.NET技术中的一个框架,让开发人员感觉使用.NET技术进行BS开发和使…

winform webform 简单高效的UI界面框架

一、winform的界面框架设计( james_lx) 一直以来,我都在寻找WINFORM程序,简单高效的UI界面框架,终于,我领悟了。在此分享给和我一样使用C#而苦于界面设计的人。我的发现中,并没有加入什么其它的…

WebForm 基础学习

C/S 客户端应用程序(Client/Server) 客户端——服务器端 两种技术 WinForm WPF -- WPF开发于WinForm之后, --GUI 图形用户界面(采用图形方式显示的计…

c#webform制作网页应用的思路login登录

一方面是有实际工作需要,一方面是想学点东西,制作一个webform的信息科工单登记查询网页,虽然目前还是很多bug,虽然代码基本都是复制的,但是慢慢的搭建成型。自己记录下思路,纯小白,可以说基本都…

<form>表单

1.form表单 <form>标签是表单是一个框架&#xff0c;其中主要包含<input>,<textarea>,<select>标签 1.1<input>标签 <input>标签首先是一个单标签 <input>标签的格式是<input type"" value"" name"&…

表单<form>

创建表单 <form> 标签用于创建供用户输入的 HTML 表单。 <form>标签的action属性的值指定了表单提交到服务器的地址。 <form> 元素包含一个或多个如下的表单元素&#xff1a; <input> <textarea> <button> <select> <option&g…

ASP.NET WebForm--简介

文章目录 ASP.NETASP.NET 模型HTTP请求处理过程HttpHandler ASP.NET ASP.NET基于.NET Framework的Web开发平台&#xff0c;不但吸收了ASP以前版本的最大优点并参照Java、VB语言的开发优势加入了许多新的特色&#xff0c;同时也修正了以前的ASP版本的运行错误。 ASP.NET就是属于…