缓存最佳实践

目录

前言

一、Cache Aside(旁路缓存)策略

二、不一致解决场景及解决方案

一、数据库主从不一致

二、缓存与数据库不一致

三、问题分析

三、缓存误用

一、多服务共用缓存实例

二、调用方缓存数据

三、缓存作为服务与服务之间传递数据的媒介

四、使用缓存未考虑雪崩

总结


前言

缓存,是互联网分层架构中,非常重要的一个部分,通常用它来降低数据库压力,提升系统整体性能,缩短访问时间。有架构师说“缓存是万金油,哪里有问题,加个缓存,就能优化”,缓存的滥用,可能会导致一些错误用法。

缓存,你真的用对了么?


一、Cache Aside(旁路缓存)策略

旁路缓存策略是最常用的一种缓存读写策略,它适用于读请求比较多,数据更新频率不高的场景。它的基本思想是:应用程序直接访问缓存和数据库,而不通过中间层。当需要读取数据时,先从缓存中查找,如果命中则直接返回;如果未命中,则从数据库中查询,并将结果放入缓存中,然后返回。当需要更新数据时,先更新数据库,然后删除缓存。

Cache Aside 策略(也叫旁路缓存策略),这 个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策 略。

其中读策略的步骤是:

  1. 从缓存中读取数据,如果缓存命中,则直接返回数据;
  2. 如果缓存不命中,则从数据库中查询数据;
  3. 查询到数据后,将数据写入到缓存中,并且返回给用户。

     

写策略的步骤是:
  1. 更新数据库中的记录;
  2. 删除缓存记录。

    

你也许会问了,在写策略中,能否先删除缓存,后更新数据库呢?答案是不行的,因为这样也有可能出现缓存数据不一致的问题,我以用户表的场景为例解释一下。假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读 取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21,这就造成了缓存和数据库的不一致。
 

那么像 Cache Aside 策略这样先更新数据库,后删除缓存就没有问题了吗?其实在理论上还是有缺陷的。假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中,造成缓存和数据库数据不一致。

 不过这种问题出现的几率并不高,原因是缓存的写入通常远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且清空了缓存,请求 A 才更新完缓存的情况。而一旦请求 A 早于请求 B 清空缓存之前更新了缓存,那么接下来的请求就会因为缓存为空而从数据库中重新加载数据,所以不会出现这种不一致的情况。

Cache Aside 策略是我们日常开发中最经常使用的缓存策略,不过我们在使用时也要学会依情况而变。比如说当新注册一个用户,按照这个更新策略,你要写数据库,然后清理缓存(当然缓存中没有数据给你清理)。可当我注册用户后立即读取用户信息,并且数据库主从分离时,会出现因为主从延迟所以读不到用户信息的情况。而解决这个问题的办法恰恰是在插入新数据到数据库之后写入缓存,这样后续的读请求就会从缓存中读到数据了。并且因为是新注册的用户,所以不会出现并发更新用户信息的情况。Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。

二、不一致解决场景及解决方案

发生写请求后(不管是先操作DB,还是先淘汰Cache),在主从数据库同步完成之前,如果有读请求,都可能发生读Cache Miss,读从库把旧数据存入缓存的情况。此时怎么办呢?

一、数据库主从不一致

无缓存时,数据库主从不一致问题

如上图,发生的场景是,写后立刻读:
(1)主库一个写请求(主从没同步完成)
(2)从库接着一个读请求,读到了旧数据
(3)最后,主从同步完成

导致的结果是:主动同步完成之前,会读取到旧数据。可以看到,主从不一致的影响时间很短,在主从同步完成后,就会读到新数据。

二、缓存与数据库不一致

再看,引入缓存后,缓存和数据库不一致问题。

 如上图,发生的场景也是,写后立刻读

导致的结果是:旧数据放入缓存,即使主从同步完成,后续仍然会从缓存一直读取到旧数据。

可以看到,加入缓存后,导致的不一致影响时间会很长,并且最终也不会达到一致。

三、问题分析

可以看到,这里提到的缓存与数据库数据不一致,根本上是由数据库主从不一致引起的。当主库上发生写操作之后,从库binlog同步的时间间隔内,读请求,可能导致有旧数据入缓存。

思路:那能不能写操作记录下来,在主从时延的时间段内,读取修改过的数据的话,强制读主,并且更新缓存,这样子缓存内的数据就是最新。在主从时延过后,这部分数据继续读从库,从而继续利用从库提高读取能力。

选择性读主

可以利用一个缓存记录必须读主的数据。

如上图,当写请求发生时:


(1)写主库
(2)将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”
 

PS:key的格式为“db:table:PK”,假设主从延时为1s,这个key的cache超时时间也为1s。

如上图,当读请求发生时:


这是要读哪个库,哪个表,哪个主键的数据呢,也将这三个信息拼装一个key,到cache里去查询,如果,


(1)cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询。并且把主库的数据set到缓存中,防止下一次cahce miss。
(2)cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询

以此,保证读到的一定不是不一致的脏数据。

PS:如果系统可以接收短时间的不一致,建议定时更新缓存就可以了。避免系统过于复杂。

三、缓存误用

一、多服务共用缓存实例

如上图:服务A和服务B共用一个缓存实例(不是通过这个缓存实例交互数据)

该方案存在的问题是:

1、可能导致key冲突,彼此冲掉对方的数据

可能需要服务A和服务B提前约定好了key,以确保不冲突,常见的约定方式是使用namespace:key的方式来做key。

2、不同服务对应的数据量,吞吐量不一样,共用一个实例容易导致一个服务把另一个服务的热数据挤出去

3、共用一个实例,会导致服务之间的耦合,与微服务架构的“数据库,缓存私有”的设计原则是相悖的

正确的部署方式是


如上图:各个服务私有化自己的数据存储,对上游屏蔽底层的复杂性。

二、调用方缓存数据

如上图,服务提供方缓存,向调用方屏蔽数据获取的复杂性。服务调用方,也缓存一份数据,先读自己的缓存,再决定是否调用服务(这个有问题)

该方案存在的问题是:
1、调用方需要关注数据获取的复杂性(耦合问题)
2、更严重的,服务修改db里的数据,淘汰了服务cache之后,难以通知调用方淘汰其cache里的数据,从而导致数据不一致(带入一致性问题)
3、有人说,服务可以通过MQ通知调用方淘汰数据,额,难道下游的服务要依赖上游的调用方,分层架构设计不是这么玩的(反向依赖问题)

三、缓存作为服务与服务之间传递数据的媒介

如上图:服务A和服务B约定好key和value,通过缓存传递数据服务A将数据写入缓存,服务B从缓存读取数据,达到两个服务通信的目的

多个服务关联同一个缓存实例,会导致服务耦合
(1)大家要彼此协同约定key的格式,ip地址等,耦合

(2)约定好同一个key,可能会产生数据覆盖,导致数据不一致

(3)不同服务业务模式,数据量,并发量不一样,会因为一个cache相互影响,例如service-A数据量大,占用了cache的绝大部分内存,会导致service-B的热数据全部被挤出cache,导致cache失效;又例如service-A并发量高,占用了cache的绝大部分连接,会导致service-B拿不到cache的连接,从而服务异常

四、使用缓存未考虑雪崩

常规的缓存玩法,如上图:
服务先读缓存,缓存命中则返回;缓存不命中,再读数据库

什么时候会产生雪崩?


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

如何应对潜在的雪崩?


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

否则,就要进一步设计。

常见方案一:高可用缓存


如上图:使用高可用缓存集群,一个缓存实例挂掉后,能够自动做故障转移。

常见方案二:缓存水平切分


如上图:使用缓存水平切分(推荐使用一致性哈希算法进行切分),一个缓存实例挂掉后,不至于所有的流量都压到数据库上。


总结

1、服务与服务之间不要通过缓存传递数据

2、如果缓存挂掉,可能导致雪崩,此时要做高可用缓存,或者水平切分

3、调用方不宜再单独使用缓存存储服务底层的数据,容易出现数据不一致,以及反向依赖

4、不同服务,缓存实例要做垂直拆分。

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

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

相关文章

python+tkinter实现多页面多菜单的demo实例

本篇文章主要讲解,python+tkinter多页面多菜单的demo实例,支持一个新窗口弹出、多页面切换,顶部菜单构建及事件绑定。 日期:2023年8月25日 版本:python3.9.6 实际效果 消息菜单-具体效果: 页面菜单具体效果: 事件菜单具体效果: 环境及依赖 python 3.9.6 依赖信息: …

WPF网格拖动自动布局效果

WPF网格拖动自动布局效果 使用Canvas和鼠标相关事件实现如下的效果: XAML代码: <Window x:Class="CanvasTest.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:

Python科研数据可视化

在过去的20 年中&#xff0c;随着社会产生数据的大量增加&#xff0c;对数据的理解、解释与决策的需求也随之增加。而固定不变是人类本身&#xff0c;所以我们的大脑必须学会理解这些日益增加的数据信息。所谓“一图胜千言”&#xff0c;对于数量、规模与复杂性不断增加的数据&…

LLMs对单个任务进行微调Fine-tuning on a single task

虽然LLM因其在单一模型内执行多种不同语言任务的能力而变得出名&#xff0c;但您的应用程序可能只需要执行单一任务。在这种情况下&#xff0c;您可以微调一个预训练的模型&#xff0c;以仅提高您感兴趣的任务的性能。例如&#xff0c;使用该任务的示例数据集进行摘要。有趣的是…

科技资讯|苹果Apple Watch新专利,可根据服装、表带更换表盘颜色

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的清单&#xff0c;苹果公司近日获得了一项 Apple Watch 相关的技术专利&#xff0c;最大的亮点在于配备颜色采样传感器&#xff0c;可以根据表带、服装自动变幻变盘颜色和主题。 Apple Watch 正面配备颜色采样传感器&am…

【Maven教程】(三)基础使用篇:入门使用指南——POM编写、业务代码、测试代码、打包与运行、使用Archetype生成项目骨架~

Maven基础使用篇 1️⃣ 编写 POM2️⃣ 编写业务代码3️⃣ 编写测试代码4️⃣ 打包和运行5️⃣ 使用 Archetype生成项目骨架 1️⃣ 编写 POM 到目前为止&#xff0c;已经大概了解并安装好了Maven环境, 现在&#xff0c;我们开始创建一个最简单的 Hello World 项目。如果你是初次…

数据库第十五课-------------非关系型数据库----------Redis

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

基于 BEM 规范实现简单的全局 scss

该文章是在学习 小满vue3 课程的随堂记录示例均采用 <script setup>&#xff0c;且包含 typescript 的基础用法 前言 BEM 是 css 常用的命名规范BEM &#xff1a;block(块)、 element(元素)、 modify(修饰符)以 namespace-block__element、namespace-block---modify 格式…

如何更高效的写出更健全的代码,一篇文章教会你如何拥有一个良好的代码风格

前言&#xff1a;在平常的写代码的过程中&#xff0c;或多或少的遇到很多奇怪的 bug &#xff0c;尤其是一些大的程序&#xff0c;明明上一部分都是好好的&#xff0c;写下一块的时候突然多几百个 bug 的情况&#xff0c;然后这一块写完了后编译的时候直接傻眼了&#xff0c;看…

(学习笔记-调度算法)进程调度算法

进程调度算法也称 CPU 调度算法&#xff0c;毕竟进程是由 CPU 调度的。 当 CPU 空闲时&#xff0c;操作系统就选择内存中标的某个 [就绪状态] 的进程&#xff0c;将其分配给 CPU。 什么时候会发生CPU调度呢&#xff1f;通常有以下情况&#xff1a; 当进程从运行状态转换到等待…

使用 ChatGPT 创建 PowerPoint 演示文稿

让 ChatGPT 成为您的助手来帮助您编写电子邮件很简单,因为众所周知,它非常能够生成文本。很明显,ChatGPT 无法帮助您做饭。但您可能想知道它是否可以生成文本以外的其他内容。在上一篇文章中,您了解到 ChatGPT 只能通过中间语言为您生成图形。在这篇文章中,您将了解使用中…

【Leetcode】103.二叉树的锯齿形层序遍历

一、题目 1、题目描述 给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 示例1: 输入:root = [3,9,20,null,null,15,7] 输出:[[3],[20,9],[15,7]]示例2: 输入:root = [1] 输…

点亮社交新篇章:探索 WeTalk 新增的头像与群聊功能

目录 引言&#xff1a; 引入头像功能&#xff1a; 头像功能的优势&#xff1a; 引入群聊功能&#xff1a; 群聊功能的优势&#xff1a; 引入头像功能&#xff1a; 查看头像&#xff1a; ​编辑 上传头像&#xff1a; 引入群聊功能&#xff1a; 创建群聊&#xff1a…

【数据结构】回溯算法公式化解题 leetcode经典题目带刷:全排列、组合、子集

目录 回溯算法一、什么是回溯算法1、基本思想&#xff1a;2、一般步骤&#xff1a; 二、题目带练1、全排列2、组合3、子集 三、公式总结 回溯算法 一、什么是回溯算法 回溯算法&#xff08;Backtracking Algorithm&#xff09;是一种解决组合问题、排列问题、选择问题等一类问…

字节跳动 Git 的正确使用姿势与最佳实践

版本控制Git 黑马&尚硅谷 Git的前世今生 方向介绍 为什么要学习Git 1.0 Git是什么 1.1 版本控制 1.1.1 本地版本控制 1.1.2 集中版本控制 1.1.3 分布式版本控制 我们已经把三个不同的版本控制系统介绍完了&#xff0c;Git 作为分布式版本控制工具&#xff0c; 虽然目前来讲…

第一讲使用IDEA创建Java工程——HelloWorld

一、前言导读 为了能够让初学者更快上手Java,不会像其他书籍或者视频一样,介绍一大堆历史背景,默认大家已经知道Java这么编程语言了。本专栏只会讲解干货,直接从HelloWord入手,慢慢由浅入深,讲个各个知识点,这些知识点也是目前工作中项目使用的,而不是讲一些老的知识点…

【算法专题突破】双指针 - 移动零(1)

目录 写在前面 1. 题目解析 2. 算法原理 3. 代码编写 写在最后&#xff1a; 写在前面 在进行了剑指Offer和LeetCode hot100的毒打之后&#xff0c; 我决心系统地学习一些经典算法&#xff0c;增强我的综合算法能力。 1. 题目解析 题目链接&#xff1a;283. 移动零 - 力…

基于51单片机直流电机转速数码管显示控制系统

一、系统方案 本文主要研究了利用MCS-51系列单片机控制PWM信号从而实现对直流电机转速进行控制的方法。本文中采用了三极管组成了PWM信号的驱动系统&#xff0c;并且对PWM信号的原理、产生方法以及如何通过软件编程对PWM信号占空比进行调节&#xff0c;从而控制其输入信号波形等…

mysql -sql触发器

1、创建触发器。 //创建一个触发器在给section关系插入后触发 create trigger timeslot_check1 after insert on sectionreferencing new row as nrow//对每个插入的行都执行for each row//when指定一个条件&#xff0c;仅对于满足条件的元组才会执行触发器剩余的部分when (nr…

实现简单的element-table的拖拽效果

第一步&#xff0c;先随便创建element表格 <el-table ref"dragTable" :data"tableData" style"width: 100%" border fit highlight-current-row><el-table-column label"日期" width"180"><template slot-sc…