极客时间-读多写少型缓存设计

news2024/11/20 10:22:42

背景

内容是极客时间-徐长龙老师的高并发系统实战课的个人学习笔记,欢迎大家学习!https://time.geekbang.org/column/article/596644

总览内容如下:
在这里插入图片描述

缓存性价比

一般来说,只有热点数据放到缓存才更有价值

  • 数据量
  • 查询频率
  • 命中率

临时缓存

把目标放到会被高频查询的信息,也就是用户信息,在用户信息第一次被使用的时候,同时将数据放到缓存中,短期内如果再次有类似的查询酒可以快速从缓存中获取,伪代码如下。

userInfo, err := Redis.Get("user_info_9527")
if err != nil {
	return nil, err
}

if userInfo != nil {
	return userInfo, nil
}

userInfo, err := userInfoModel.GetUserInfoById(9527)
if err != nil {
	//这里的err是数据库链接时的错误,期望是通知获取方系统错误
	return nil, err
}

if userInfo != nil {
	Redis.set("user_info_9527", userinfo, 60)
	return userInfo, nil
}

//可以未找到,放一个空数据进入,短期内不再访问数据库
Redis.set("user_info_9527", "")
return nil, nil

缓存更新不及时问题

临时缓存有TTL,如果60秒内修改了用户的昵称,缓存不会马上更新

单条实体数据缓存刷新

  1. 先更新数据库
  2. 然后清理缓存,让下次读取时刷新缓存,防止并发修改导致临时数据进入缓存

可以给队列发更新消息让子系统更新,还可以开发中间件把数据操作发给子系统,自行决定更新的数据范围

  1. 中间件可以失败重试,保证其可以更新成功
  2. 队列形式可以保证并发的执行顺序

问题:但条件批量更新的操作无法知道具体有多少个ID可能有修改(更新操作是基于条件进行的,因此在更新之前无法确定有多少个ID可能已经被修改)

解法:先用同样的条件把所有涉及的ID都取出来,然后update,这时用所有相关ID更新具体缓存即可。

关系型和统计型数据缓存刷新

这类数据缓存刷新存在一定难度,核心在于统计是由多条数据计算而成的。很难识别出需要刷新哪些关联缓存

人工维护缓存方式

刷新缓存很多,那么缓存更新会比较慢,并且存在延迟

订阅数据库来找到ID数据变化

maxwell 或 canal,对MySQL的更新进行监控

缺点:复杂的关联关系刷新,仍旧需要通过人工写逻辑来实现

版本号缓存设计

一旦有任何更新,整个表内所有数据缓存一起过期

user_info表设置一个key,更新这个表数据时,直接对key+1,在缓存中也保留version的值。

当业务要读取user_info某个用户的信息的时候,业务会同时获取当前表的version。如果发现缓存数据内的版本和当前表的版本不一致,那么就会更新这条数据。但如果 version 更新很频繁,就会严重降低缓存命中率,所以这种方案适合更新很少的表

当然,我们还可以对这个表做一个范围拆分,比如按 ID 范围分块拆分出多个 version,通过这样的方式来减少缓存刷新的范围和频率。

识别主要实体ID来刷新缓存

这要保证其他缓存保存的key也是主要实体ID

异步脚本遍历数据库刷新所有相关缓存

这个方式适用于两个系统之间同步数据,能够减少系统间的接口交互;缺点是删除数据后,还需要人工删除对应的缓存,所以更新会有延迟。但如果能配合订阅更新消息广播的话,可以做到准同步

长期热数据缓存

长期缓存要求业务几乎不走数据库,并且服务运转期间所需的数据都要能在缓存中找到,同时还要保证使用期间缓存不丢。

下面伪代码使用了singleflight方式预防临时缓存被大量请求穿透

singleflight实现可以参考我的另一个博客https://editor.csdn.net/md/?articleId=135174867

// 尝试从缓存中直接获取用户信息
userinfo, err := Redis.Get("user_info_9527")
if err != nil {
  return nil, err
}

//缓存命中找到,直接返回用户信息
if userinfo != nil {
  return userinfo, nil
}

//set 检测当前是否是热数据
//之所以没有使用Bloom Filter是因为有概率碰撞不准
//如果key数量超过千个,建议还是用Bloom Filter
//这个判断也可以放在业务逻辑代码中,用配置同步做
isHotKey, err := Redis.SISMEMBER("hot_key", "user_info_9527")
if err != nil {
  return nil, err
}

//如果是热key
if isHotKey {
  //没有找到就认为数据不存在
  //可能是被删除了
  return "", nil
}

//没有命中缓存,并且没被标注是热点,被认为是临时缓存,那么从数据库中获取
//设置更新锁set user_info_9527_lock nx ex 5
//防止多个线程同时并发查询数据库导致数据库压力过大
lock, err := Redis.Set("user_info_9527_lock", "1", "nx", 5)
if !lock {
  //没抢到锁的直接等待1秒 然后再拿一次结果,类似singleflight实现
  //行业常见缓存服务,读并发能力很强,但写并发能力并不好
  //过高的并行刷新会刷沉缓存
  time.sleep( time.second)
  //等1秒后拿数据,这个数据是抢到锁的请求填入的
  //通过这个方式降低数据库压力
  userinfo, err := Redis.Get("user_info_9527")
  if err != nil {
    return nil, err
  }
  return userinfo,nil
}

//拿到锁的查数据库,然后填入缓存
userinfo, err := userInfoModel.GetUserInfoById(9527)
if err != nil {
  return nil, err
}

//查找到用户信息
if userinfo != nil {
  //将用户信息缓存,并设置TTL超时时间让其60秒后失效
  Redis.Set("user_info_9527", userinfo, 60)
  return userinfo, nil
}

// 没有找到,放一个空数据进去,短期内不再问数据库
Redis.Set("user_info_9527", "", 30)
return nil, nil

查询某个用户信息时:

如果缓存中没有数据,长期缓存会直接返回没有找到,临时缓存则直接走更新流程。

如果数据属于热点key,并且在缓存中找不到的话,就直接返回不存在。

这些热缓存 key,来自于统计一段时间内数据访问流量,计算得出的热点数据。

TTL过期刷新

那长期缓存的更新会异步脚本去定期扫描热缓存列表,通过这个方式来主动推送缓存,同时把 TTL 设置成更长的时间,来保证新的热数据缓存不会过期,同时需要将热度过的key从当前set移除。

在每个业务服务器上部署一个小容量的Redis来保存热点缓存数据

小容量Redis查不到,再去集群中查

问题

使用 Bloom Filter 识别热点 key 时,有时会识别失误,进而导致数据没有找到,那么如何避免这种情况呢?

使用 Bloom Filter 只能添加新 key,不能删除某一个 key,如果想更好地更新维护,有什么其他方式吗?

https://github.com/MGunlogson/CuckooFilter4J

总结

在这里插入图片描述

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

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

相关文章

67.网游逆向分析与插件开发-角色数据的获取-分析角色数据基址

内容参考于:易道云信息技术研究院VIP课 上一个内容:角色类的数据分析与C还原-CSDN博客 基址这个东西说好找也好找,说不好找是真找不着,但就根据一个原则,就是确认这个东西有基址还是没基址,为什么会有没基…

STM32--基于STM32F103的MAX30102心率血氧测量

本文介绍基于STM32F103ZET6MAX30102心率血氧测量0.96寸OLED(7针)显示(完整程序代码见文末链接) 一、简介 MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块。它集成了一个红光LED和一个红外光LED、光电检测器、光器…

移动通信原理与关键技术学习之信道编解码(5)

先回顾调制的过程:调制就是对信号源的信息进行处理加到载波上,使其变为适合于信道传输的形式的过程,就是使载波随信号而改变的技术。 1.什么是IQ调制? 答:将数据分为两路,分别进行载波调制,两…

Camunda Sub Process

一:内嵌子流程 repositoryService.createDeployment().name("内嵌子流程").addClasspathResource("bpmn/embed_sub_process.bpmn").deploy(); identityService.setAuthenticatedUserId("huihui"); ProcessInstance processInstance …

教你三招该如何制作家具样本册

​随着生活品质的提高,人们对家居装饰的要求也越来越高。家具作为家居装饰的重要组成部分,其选择和搭配也显得尤为重要。为了更好地展示家具的特点和风格,制作家具样本册成为了许多人的选择。那么,如何制作一份精美的家具样本册呢…

Day31 贪心算法 part01 理论基础 455.分发饼干 376.摆动序列 53.最大子序和

贪心算法 part01 理论基础 455.分发饼干 376.摆动序列 53.最大子序和 理论基础(转载自代码随想录) 什么是贪心 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。 这么说有点抽象,来举一个例子: 例如&#…

CMake入门教程【实战篇】使用Boost库

文章目录 安装 Boost 库CMake中使用Boost库时安装 Boost 库 下载地址 https://www.boost.org/users/download/ 下载版本 选择二进制版本或者源码 二进制版本直接安装即可 源码版本编译->安装 解压到本地目录,如e:/dev/。运行E:\dev\boost_1_83_0\bootstrap.bat,会在同目录下…

激活/注册navicat15

一、获取软件 链接:https://pan.baidu.com/s/1F_tiLuLvVFMEz8pDfIvDjw?pwdjjfj 提取码:jjfj 二、安装 安装的过程我就不放了,重点如下 安装完不要打开软件! 安装完不要打开软件! 安装完不要打开软件!…

Spark SQL进阶

DataFrame详解 清洗相关API 去重API 删除空缺值的API 替换缺失值的API from pyspark import SparkConf, SparkContext import os from pyspark.sql import SparkSession# 绑定指定的Python解释器 os.environ[SPARK_HOME] /export/server/spark os.environ[PYSPARK_PYTHON]…

前端面试题集合六(高频)

1、vue实现双向数据绑定原理是什么&#xff1f; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>…

RT-Thread入门笔记3-线程的创建

线程 RT-Thread 中&#xff0c;线程由三部分组成&#xff1a;线程代码&#xff08;入口函数&#xff09;、线程控制块、线程堆栈. 线程代码: 线程控制块 : 线程控制块是操作系统用于管理线程的一个数据结构&#xff0c; 它会存放线程的一些信息&#xff0c; 例如优先级、 线程…

从学习投研流程的角度学习Qlib

许多同学只是把Qlib当做一个简单的工具来学习。其实Qlib隐含了一套正规的投研流程&#xff0c;从投研流程的视角去学习Qlib,则不仅能加深对Qlib的理解&#xff0c;而且能够掌握正确的投研流程&#xff0c;哪怕以后不使用Qlib而是使用其他系统了&#xff0c;这套流程还是适用的。…

ADOV路由和DSR路由matlab对比仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 ADOV路由&#xff08;Ad hoc On-demand Distance Vector Routing&#xff09; 4.2 DSR路由&#xff08;Dynamic Source Routing&#xff09; 5.完整程序 1.程序功能描述 ADOV路由和DSR…

创建EasyCodeMybatisCodeHelperPro模板文件用于将数据库表生成前端json文件

在intellij idea中&#xff0c;通过插件EasyCodeMybatisCodeHelperPro&#xff0c;从现有的模板文件中选择一个复制粘贴&#xff0c;然后稍为修改&#xff0c;即可得到一个合适的模板文件。 现在的前端&#xff0c;越来越像后端。TypeScript替代了JavaScript&#xff0c;引入了…

基于Flask的高并发部署方案

文章目录 Flask方案简介服务端代码客户端代码 Gevent Flask方案简介安装示例 gunicornFlask 部署服务简介安装示例 在AI部署方案中&#xff0c;Flask是最常用的方案&#xff01;本文列举几种最常用基于Flask的部署方案。 Flask方案 简介 Flask 是一个轻量级的 Python Web 框架…

微信小程序swiper实现层叠轮播图

在微信小程序中,需要实现展示5个&#xff0c;横向层叠的轮播图效果&#xff0c;轮播图由中间到2侧的依次缩小.如下图 使用原生小程序进行开发,没有使用Skyline模式&#xff0c;所以layout-type配置项也无效。所以基于swiper组件进行调整。 主要思路就是设置不同的样式&#xff…

Mysql 分割字符串,一行变多行,@rownum,mysql.help_topic

1 前言 朋友最近遇到一个比较棘手的 sql 问题&#xff0c;让我帮忙看看&#xff1a; 他有两张表 testa 和 testb &#xff0c;一个表存的日期&#xff0c;另一个表存字符串例如 2023-11-01,2023-11-02&#xff0c;如何将这两张表关联起来&#xff0c;只查 testa 表的数据&#…

MES数据采集在制造业的应用

MES设备数据采集的流程包括以下几个步骤&#xff1a; 1. 设备接入&#xff1a;将设备接入MES系统&#xff0c;建立设备与MES系统之间的连接。 2. 数据采集&#xff1a;通过传感器和采集器等设备&#xff0c;采集设备运行数据和状态信息。 3. 数据存储&#xff1a;将采集到的设…

如何给字符串字段添加索引

MySQL是支持前缀索引的&#xff0c;可以定义字符串的一部分作为索引&#xff0c;如果创建索引的语句不指定前缀长度&#xff0c;那么索引就会包含整个字符串。 alter table SUser add index index1(email);alter table SUser add index index2(email(6)); 如上两个创建索引的语…