【技术解决方案】(多级)缓存架构最佳实践

news2024/9/29 3:23:00

凌晨三点半了,太困了,还差一些,明天补上…

因为自己最近做的项目涉及到了缓存,所以水一篇缓存相关的文章,供大家作为参考,若发现文章有纰漏,希望大家多指正。

缓存涉及到的范围颇广,从CPU缓存,到进程内缓存,到进程外缓存。再加上已经凌晨一点了,我得保住我的几丝残发,本文不会将每一处的细枝末节都写到,见谅。

文章目录

    • 关于CPU缓存
    • 关于多级缓存
    • 关于二级缓存
    • 进程内缓存
      • 进程内缓存有什么好处?
      • 进程内缓存有什么缺点?
      • 如何保证进程内缓存的数据一致性?
      • 为什么不能频繁使用进程内缓存?
      • 什么时候可以使用进程内缓存?
    • 服务之间通过缓存传递数据的错误性
    • 使用缓存未考虑雪崩的错误性
    • 多服务共用缓存实例的错误性
    • 缓存与数据库不一致的解决方案
    • 先操作缓存,还是数据库
    • Cache Aside Pattern方案
    • 缓存为什么总是淘汰,不是修改
    • 缓存相关的清除策略
    • 为什么选Caffeine
    • 为什么选Redis
    • Redis最佳应用实践

关于CPU缓存

这里提一句CPU缓存,因为缓存的核心思想都是那点事,命中、淘汰、一致性等。
以前着重写过CPU的一些东西,这里只附一张图。

ps:听说最近有哪个厂商的CPU把三级缓存架构和总线锁改了,有相关资源的小伙伴快发给我,我观摩一下,hhh~

在这里插入图片描述

关于多级缓存

本文重点不在多级缓存,因为以前我也专门写过一篇关于多级缓存的详细设计。
简要步骤:

  1. 浏览器缓存
  2. Nginx反向代理,负载OpenResty集群
  3. OpenResty基于Nginx和Lua,可实现Lua业务编码,缓存性能很好,京东技术做过压测对比。
  4. 如果OpenResty缓存未命中,则查询Redis
  5. 若Redis缓存未命中,则查询进程缓存
  6. 为了保证缓存和DB的数据一致性,还可以用Canal和DTS做数据同步(基于Mysql的Binlog,和主从一个原理,伪装成slave)

在这里插入图片描述

关于二级缓存

二级缓存最佳实践:Caffeine + Redis

  1. 先走Caffeine,如果未命中,走Redis
  2. 为了保证数据一致性,可以用Canal / DTS做数据同步
  3. 进程缓存Caffeine的话,设置个定时同步就可以了

性能优化:

  1. 进程缓存应用Caffeine是因为其底层ConcurrentHashMap的结构,支持并发(后面会出各个进程缓存性能对比报告)
  2. 进程外缓存,我通常会无脑选Redis,基于其容错性,多数据结构等。(后面会出和memcache等对比分析)

市面上也有二级缓存框架,比如J2Cache,该框架本身并没有做额外工作,主要是集成了常见的进程内缓存和进程外缓存。

如果基于Spring开发,基于AOP设计的Spring Cache框架适配常用的缓存,自身的注解和策略天然和业务解耦,很不错,但是,如何集成Redis,这里需要特别注意!!!

因为集成Redis时,Spring Cache的清除策略,在从Redis中删除缓存时使用的是 keys指令,keys指令时间复杂度是O(N),如果缓存数量较大会产生明显的阻,因此在生产环境中Redis会禁用这个指令,导致报错。

//keys 指令
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
         .toArray(new byte[0][]);

 if (keys.length > 0) {
     statistics.incDeletesBy(name, keys.length);
     connection.del(keys);
 }

所以,我们可以重写DefaultRedisCacheWriter(spring cache提供的默认的Redis缓存写出器,其内部封装了缓存增删改查等逻辑)

使用scan命令代替keys命令

//使用scan命令代替keys命令
Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(new String(pattern)).count(1000).build());
Set<byte[]> byteSet = new HashSet<>();
while (cursor.hasNext()) {
    byteSet.add(cursor.next());
}

byte[][] keys = byteSet.toArray(new byte[0][]);

讲真的,多级缓存和二级缓存这东西,不要为了炫技乱用,可能会增加没必要的开发成本和未知问题,而且还要做好数据量的评估,别搞了缓存,造成雪崩,那就真的血本无归了。

至理名言:不结合业务的技术都是耍流氓

进程内缓存

进程内缓存有什么好处?

与没有缓存相比,进程内缓存的好处是,数据读取不再需要访问后端,例如数据库。

与进程外缓存相比(例如redis/memcache),进程内缓存省去了网络开销,所以一来节省了内网带宽,二来响应时延会更低。

进程内缓存有什么缺点?

如果数据缓存在站点和服务的多个节点内,数据存了多份,一致性比较难保障。

如何保证进程内缓存的数据一致性?

  1. 可以通过单节点通知其他节点。
  2. 可以通过MQ通知其他节点。
  3. 为了避免耦合,降低复杂性,干脆放弃了“实时一致性”,每个节点启动一个timer,定时从后端拉取最新的数据,更新内存缓存。在有节点更新后端数据,而其他节点通过timer更新数据之间,会读到脏数据。

为什么不能频繁使用进程内缓存?

站点与服务的进程内缓存,实际上违背了分层架构设计的无状态准则

什么时候可以使用进程内缓存?

  1. 只读数据,可以考虑在进程启动时加载到内存。(实现InitializingBean)
  2. 极其高并发的,如果透传后端压力极大的场景,可以考虑使用进程内缓存。(秒杀)
  3. 一定程度上允许数据不一致业务。

服务之间通过缓存传递数据的错误性

  1. 数据管道场景,MQ比cache更合适;
  2. 多个服务不应该公用一个cache实例,应该垂直拆分解耦;
  3. 服务化架构,不应该绕过service读取其后端的cache/db,而应该通过RPC接口访问。

使用缓存未考虑雪崩的错误性

如果缓存挂掉,所有的请求会压到数据库,如果未提前做容量预估,可能会把数据库压垮(在缓存恢复之前,数据库可能一直都起不来),导致系统整体不可服务。

应提前做容量预估,如果缓存挂掉,数据库仍能扛住,才能执行上述方案。

否则,就要进一步设计:
使用高可用缓存集群(例如主备),一个缓存实例挂掉后,能够自动做故障转移。
使用缓存水平切分,一个缓存实例挂掉后,不至于所有的流量都压到数据库上。

多服务共用缓存实例的错误性

  1. 可能导致key冲突,彼此冲掉对方的数据;(可做namespace:key的方式来做key,隔离)
  2. 不同服务对应的数据量,吞吐量不一样,共用一个实例容易导致一个服务把另一个服务的热数据挤出去;
  3. 共用一个实例,会导致服务之间的耦合,与微服务架构的“数据库,缓存私有”的设计原则是相悖的;

例如,我做过的一个单体架构项目,缓存用Caffeine,每个业务都会有一个Caffeine实例。

缓存与数据库不一致的解决方案

  1. 主从同步;
  2. 通过工具(DTS/cannal)订阅从库的binlog,这里能够最准确的知道,从库数据同步完成的时间;
  3. 从库执行完写操作,向缓存再次发起删除,淘汰这段时间内可能写入缓存的旧数据;

先操作缓存,还是数据库

  1. 读请求,先读缓存,如果没有命中,读数据库,再set回缓存
  2. 写请求
    • 先缓存,再数据库
    • 缓存,使用delete,而不是set

Cache Aside Pattern方案

对于读请求:

(1)先读cache,再读db;

(2)如果,cache hit,则直接返回数据;

(3)如果,cache miss,则访问db,并将数据set回缓存;

对于写请求:

(1)淘汰缓存,而不是更新缓存;

(2)先操作数据库,再淘汰缓存;

缓存为什么总是淘汰,不是修改

修改成本太大了,无脑选淘汰,问题不大

缓存相关的清除策略

FIFO(first in first out)
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。

LFU(less frequently used)
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。

LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。

除此之外,还有一些简单策略比如:

根据过期时间判断,清理过期时间最长的元素;
根据过期时间判断,清理最近要过期的元素;
随机清理;
根据关键字(或元素内容)长短清理等。

为什么选Caffeine

底层数据结构,W-TinyLFU算法,当然还有权威给出个各个组件性能对比图,谁不愿意用好的呢,对吧。(关于Caffeine源码,改天单写一篇)

为什么选Redis

没有为什么,无脑选就完了,下周我写一篇Redis7的源码文章,你就懂了。

Redis最佳应用实践

  1. 在主页中显示最新的项目列表:Redis使用的是常驻内存的缓存,速度非常快。LPUSH用来插入一个内容ID,作为关键字存储在列表头部。LTRIM用来限制列表中的项目数最多为5000。如果用户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。
  2. 删除和过滤:如果一篇文章被删除,可以使用LREM从缓存中彻底清除掉。
  3. 排行榜及相关问题:排行榜(leader board)按照得分进行排序。ZADD命令可以直接实现这个功能,而ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
  4. 按照用户投票和时间排序:排行榜,得分会随着时间变化。LPUSH和LTRIM命令结合运用,把文章添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。
  5. 过期项目处理:使用Unix时间作为关键字,用来保持列表能够按时间排序。对current_time和time_to_live进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用ZRANGE…WITHSCORES进行查询,删除过期的条目。
  6. 计数:进行各种数据统计的用途是非常广泛的,比如想知道什么时候封锁一个IP地址。INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
  7. 特定时间内的特定项目:这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决。SADD不会将已经存在的成员添加到一个集合。
  8. Pub/Sub:在更新中保持用户对数据的映射是系统中的一个普遍任务。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,让这个变得更加容易。
  9. 队列:在当前的编程中队列随处可见。除了push和pop类型的命令之外,Redis还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

spring boot整合Swagger2(2.9.2版本)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

计算机组成原理-中央处理器-指令流水线和多处理器

目录 一、指令流水线基本概念 1.1影响流水线的因素 1.1.1结构相关(资源冲突) 1.1.2 数据相关(数据冲突) 1.1.3 控制相关(控制冲突) 1. 2 流水线分类 二、流水线的多发技术 2.1 超标量技术 2.2 超流水技术 2.3 超长指令字 三、五段式指令流水线 四、多处理器系统基本概念…

I.MX RT1170加密启动详解(1):Encrypted Boot image组成

使用RT1170芯片构建的所有平台一般都是高端场合&#xff0c;我们需要考虑软件的安全需求。该芯片集成了一系列安全功能。这些特性中的大多数提供针对特定类型攻击的保护&#xff0c;并且可以根据所需的保护程度配置为不同的级别。这些特性可以协同工作&#xff0c;也可以独立工…

chatgpt赋能python:Python中的区间:什么是区间(Interval),如何使用区间

Python 中的区间&#xff1a;什么是 区间&#xff08;Interval&#xff09;&#xff0c;如何使用区间 在Python中&#xff0c;区间&#xff08;Interval&#xff09;是一个广泛使用的数据结构&#xff0c;用于表示一段连续的数据范围。使用区间可以更方便地处理各种数据类型&a…

本地搭建CFimagehost私人图床【公网远程访问】

文章目录 1.前言2. CFImagehost网站搭建2.1 CFImagehost下载和安装2.2 CFImagehost网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar临时数据隧道3.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;3.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 4.公网访问测…

代码随想录算法训练营第四十一天 | 力扣 343. 整数拆分, 96.不同的二叉搜索树

343. 整数拆分 题目 343. 整数拆分 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 解析 1.确定dp数组&#xff08;dp table&#xff09;以及下标的…

史上最详细的使用Claude和接入Claude-api教程

是什么&#xff08;What&#xff09; Claude 是最近新开放的一款 AI 聊天机器人&#xff0c;是世界上最大的语言模型之一&#xff0c;比之前的一些模型如 GPT-3 要强大得多&#xff0c;因此 Claude 被认为是 ChatGPT 最有力的竞争对手。Claude 的研发公司是专注人工智能安全和研…

Java制作520表白代码——爱一个人需要理由吗?

✨博主&#xff1a;命运之光 ✨专栏&#xff1a;Java经典程序设计 520表白日&#xff0c;每个人都期待着浪漫的表白&#xff0c;而作为一名热爱编程的程序员&#xff0c;我决定用程序员的方式来向你表达我的爱意。 在2023年5月20日这个特殊的日子里&#xff0c;我要用一段特别的…

BM1684X-onnx模型转化为bmodel

1&#xff1a;在tpu-mlir目录下进入docker docker run --privileged --name tpu-mlir -v $PWD:/workspace -it sophgo/tpuc_dev:v2.2 原因&#xff1a;该镜像已创建&#xff0c;要么重新创建一个新进程&#xff0c;要么杀死老进程&#xff1b; 解决办法如下&#xff1a; 2:接着…

夜深人静学32系列17——OLED

夜深人静学32系列17——OLED OLED简介接口定义OLED驱动原理驱动函数OLED.COLED.HCubeMX配置 实战部分效果展示驱动代码 OLED简介 LED&#xff0c;即有机发光二极管&#xff08;Organic Light-Emitting Diode&#xff09;&#xff0c;又称为有机激光显示&#xff08;Organic El…

基于YOLOV5的道路损伤(GRDDC‘2020)检测

1. GRDDC2020 数据集介绍 GRDDC2020 数据集是从印度、日本和捷克收集的道路图像。包括三个部分&#xff1a;Train, Test1, Test2。训练集包括带有 PASCAL VOC 格式 XML 文件标注的道路图像。 缺陷类型&#xff1a;D00、D01、D11、D10、D20、D40、D43、D44、D50、D0w0…

Otsu阈值法原理及实现

文章目录 Otsu算法简介Otsu 算法的逻辑源码实现 欢迎访问个人网络日志&#x1f339;&#x1f339;知行空间&#x1f339;&#x1f339; Otsu算法简介 Otsu阈值法发表于1979年&#xff0c;论文为A threshold selection method from gray level histograms,作者是日本东京大学的…

序列模型基础概念

一、公式定义 在时间 t t t观察到 x t x_{t} xt​&#xff0c;那么得到 T T T个不独立的随机变量 ( x 1 , . . . , x T ) − p ( X ) (x_{1},...,x_{T})-p(X) (x1​,...,xT​)−p(X) 由条件概率公式&#xff1a; p ( a , b ) p ( a ) p ( b ∣ a ) p ( b ) p ( a ∣ b ) p(a,…

chatgpt赋能python:Python中局部变量的介绍

Python中局部变量的介绍 在Python中&#xff0c;局部变量是在函数中定义的变量&#xff0c;其范围限制在该函数内部。每当函数被调用时&#xff0c;局部变量将被创建并且只在函数的执行期间存在。当函数执行结束时&#xff0c;局部变量将被销毁。 局部变量是在函数内部定义的…

代码随想录算法训练营第四十六天 | 力扣 139.单词拆分

139.单词拆分 题目 139. 单词拆分 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意&#xff1a;不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 解析 1.确定dp数组以及下标的含义 …

Windows上GIT配置文件的位置

Git作为常见的版本控制系统。在Windows上&#xff0c;我偶尔在CLI上使用官方的版本&#xff1a; Git for Windows 。本文简单介绍Windows下的git配置文件。 系统和全局的gitconfig 配置文件因环境而异&#xff08;Windows 原生的cmd、Windows shell 或 MSYS2 shell&#xff09;…

chatgpt赋能python:Python中如何取消列表

Python中如何取消列表 在Python中使用列表是一种非常常见的数据结构&#xff0c;它允许我们在其中存储任意数量的元素&#xff0c;并且可以非常容易地进行遍历和操作。但是&#xff0c;有时候我们需要从列表中删除元素。这个过程并不难&#xff0c;但是有些细节需要注意。本文…

写最好的Docker安装最新版MySQL8(mysql-8.0.31)教程(参考Docker Hub和MySQL官方文档)

一、前言 MySQL官方安装包下载地址&#xff1a;   https://dev.mysql.com/downloads/mysql/   Docker Hub官方网址&#xff1a;   https://hub.docker.com/ 如果需要了解Centos7下MySQL5.7最新版的安装部署&#xff0c;可参考教程【最新MySQL-5.7.40在云服务器Centos7.…

《深入理解计算机系统(CSAPP)》第9章虚拟内存 - 学习笔记

写在前面的话&#xff1a;此系列文章为笔者学习CSAPP时的个人笔记&#xff0c;分享出来与大家学习交流&#xff0c;目录大体与《深入理解计算机系统》书本一致。因是初次预习时写的笔记&#xff0c;在复习回看时发现部分内容存在一些小问题&#xff0c;因时间紧张来不及再次整理…

chatgpt赋能python:Python中对列表求和-一篇全面介绍和使用建议的SEO文章

Python中对列表求和 - 一篇全面介绍和使用建议的SEO文章 什么是Python中的列表&#xff1f; 在Python中&#xff0c;列表&#xff08;List&#xff09;是一种非常有用的数据结构&#xff0c;它是一组有序的元素集合。列表能够存储多个元素&#xff0c;每个元素都可以是不同的…