Redis和Memcache缓存核心及原理对比最全解析

1. 缓存基础知识

在这里插入图片描述

1.1 缓存类型

缓存是高并发场景下提高热点数据访问性能的一个有效手段,在开发项目时会经常使用到。缓存的类型分为:本地缓存、分布式缓存和多级缓存。

本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

分布式缓存可以很好得解决这个问题。分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。

为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。

1.2 淘汰策略

不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。一般的剔除策略有 FIFO 淘汰最早数据、LRU 剔除最近最少使用、和 LFU 剔除最近使用频率最低的数据几种策略。

2. Memcache详解

注意后面会把 Memcache 简称为 MC。

2.1 MC 的特点

MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
MC 功能简单,使用内存存储数据,只支持 K-V 结构,不提供持久化和主从同步功能;
MC 的内存结构以及钙化问题后面会详细介绍;
MC 对缓存的数据可以设置失效期,过期后的数据会被清除;
失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。
另外,使用 MC 有一些限制:key 不能超过 250 个字节;value 不能超过 1M 字节;key 的最大失效时间是 30 天。

2.2 MC内存结构

MC 默认是通过 Slab Allocator 来管理内存,如下图所示。Slab 机制主要是用来解决频繁 malloc/free 会产生内存碎片的问题。
在这里插入图片描述
如图左侧,MC 会把内存分为许多不同类型的 Slab,每种类型 Slab 用来保存不同大小的对象。每个 Slab 由若干的 Page 组成,如图中浅绿色的模块。不同 Slab 的 Page,默认大小是一样的,都是 1M,这也是默认 MC 存储对象不能超过 1M 的原因。每个 Page 内又划分为许多的 Chunk,Chunk 就是实际用来保存对象的空间,就是图中橘色的。不同类型的 Slab 中 Chunk 的大小是不同的,当保存一个对象时,MC 会根据对象的大小来选择最合适的 Chunk 来存储,减少空间浪费。

Slab Allocator 创建 Slab 时的参数有三个,分别是 Chunk 大小的增长因子,Chunk 大小的初始值以及 Page 的大小。在运行时会根据要保存的对象大小来逐渐创建 Slab。

2.3 MC钙化问题

来考虑这样一个场景,使用 MC 来保存用户信息,假设单个对象大约 300 字节。这时会产生大量的 384 字节大小的 Slab。运行一段时间后,用户信息增加了一个属性,单个对象的大小变成了 500 字节,这时再保存对象需要使用 768 字节的 Slab,而 MC 中的容量大部分创建了 384 字节的 Slab,所以 768 的 Slab 非常少。这时虽然 384 Slab 的内存大量空闲,但 768 Slab 还是会根据 LRU 算法频繁剔除缓存,导致 MC 的剔除率升高,命中率降低。这就是所谓的 MC 钙化问题。

解决钙化问题可以开启 MC 的 Automove 机制,每 10s 调整 Slab。也可以分批重启 MC 缓存,不过要注意重启时要进行一定时间的预热,防止雪崩问题。另外,在使用Memcached 时,最好计算一下数据的预期平均长度,调整 growth factor, 以获得最恰当的设置,避免内存的大量浪费。

3. Redis详解

在这里插入图片描述

3.1 Redis的特点

与 MC 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。

3.2 Redis功能。

Bitmap 位图是支持按 bit 位来存储信息,可以用来实现 BloomFilter;HyperLogLog 提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV;Geospatial 可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。这三个其实也可以算作一种数据结构。
pub/sub 功能是订阅发布功能,可以用作简单的消息队列。
Pipeline可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
Redis 支持提交 Lua 脚本来执行一系列的功能。
最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。

3.3 Redis持久化

Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。
RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。
AOF 对日志文件的写入操作使用的追加模式,有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。

3.4 Redis高可用

来看 Redis 的高可用。Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentine l哨兵来监控 Redis 主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof 到新主。选主的策略简单来说有三个:

slave 的 priority 设置的越低,优先级越高;
同等情况下,slave 复制的数据越多优先级越高;
相同的条件下 runid 越小越容易被选中。

在 Redis 集群中,sentinel 也会进行多实例部署,sentinel 之间通过 Raft 协议来保证自身的高可用。
Redis Cluster 使用分片机制,在内部分为 16384 个 slot 插槽,分布在所有 master 节点上,每个 master 节点负责一部分 slot。数据操作时按 key 做 CRC16 来计算在哪个 slot,由哪个 master 进行处理。数据的冗余是通过 slave 节点来保障。

3.5 Redis失效机制和淘汰策略

  • key 失效机制
    Redis 的 key 可以设置过期时间,过期后 Redis 采用主动和被动结合的失效机制,一个是和 MC 一样在访问时触发被动删除,另一种是定期的主动删除。
  • 淘汰策略
    Redis 提供了6种淘汰策略,一类是只针对设置了失效期的 key 做 LRU、最小生存时间和随机剔除;另一类是针对所有 key 做 LRU、随机剔除。当然,也可以设置不剔除,容量满时再存储对象会返回异常,但是已存在的 key 还可以继续读取。(最新版本的还加了两个LFU淘汰策略)
  • 新特性
    可以了解一下 Redis4.0 和 5.0 的新特性,例如 5.0 的 Stream,是一个可以支持多播,也就是一写多读的消息队列。还可以了解一下 4.0 的模块机制等。

3.6 Redis数据结构

Redis 内部使用字典来存储不同类型的数据,如下图中的 dictht,字典由一组 dictEntry 组成,其中包括了指向 key 和 value 的指针以及指向下一个 dictEntry 的指针。
在这里插入图片描述
在 Redis 中,所有的对象都被封装成了 redisObject,如图中浅绿的模块。redisObject 包括了对象的类型,就是 Redis 支持的 string、hash、list、set 和 sorted set 5种类型。另外 redisObject 还包括了具体对象的存储方式,如图最右边的虚线标出的模块内的几种类型。
下面结合类型来介绍具体的数据存储方式。

  • string 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。
  • list 类型,有 ziplist 压缩列表和 linkedlist 双链表实现。ziplist 是存储在一段连续的内存上,存储效率高,但是它不利于修改操作,适用于数据较少的情况;linkedlist 在插入节点上复杂度很低,但它的内存开销很大,每个节点的地址不连续,容易产生内存碎片。此外在 3.2 版本后增加了 quicklist,结合了两者的优点,quicklist 本身是一个双向无环链表,它的每一个节点都是一个 ziplist。
  • hash 类型在 Redis 中有 ziplist 和 hashtable 两种实现。当 Hash 表中所有的 key 和 value 字符串长度都小于 64 字节且键值对数量小于 512 个时,使用压缩表来节省空间;超过时,转为使用 hashtable。
  • set 类型的内部实现可以是 intset 或者 hashtable,当集合中元素小于 512 且所有的数据都是数值类型时,才会使用 intset,否则会使用 hashtable。
  • sorted set 是有序集合,有序集合的实现可以是 ziplist 或者是 skiplist 跳表。有序集合的编码转换条件与 hash 和 list 有些不同,当有序集合中元素数量小于 128 个并且所有元素长度都小于 64 字节时会使用 ziplist,否则会转换成 skiplist。

提示:Redis 的内存分配是使用 jemalloc 进行分配。jemalloc 将内存空间划分为小、大、巨大三个范围,并在范围中划分了小的内存块,当存储数据时,选择大小最合适的内存块进行分配,有利于减小内存碎片。

4. 缓存常见问题

对使用缓存时常遇到几个问题,整理出一个表格,如下图所示。
在这里插入图片描述

4.1 缓存更新方式

第一个问题是缓存更新方式,这是决定在使用缓存时就该考虑的问题。
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。
当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。

但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。

4.2 数据不一致

第二个问题是数据不一致的问题,可以说只要使用缓存,就要考虑如何面对这个问题。缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。
解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。

4.3 缓存穿透

第三个问题是缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。
解决的办法如下:
对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。

4.4 缓存击穿

第四个问题是缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。
解决这个问题有如下办法:
可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。

4.4 缓存雪崩

第五个问题是缓存雪崩。产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。
解决方法:
使用快速失败的熔断策略,减少 DB 瞬间压力;使用主从模式和集群模式来尽量保证缓存服务的高可用。实际场景中,这两种方法会结合使用。

热门文章

暂无图片
编程学习 ·

10.4 引用的本质

10.4 引用的本质 引用的本质其实是一个指针常量。 也就是说:int &b = a;本质上是:int* const b = &a;回想一下引用的一个特性(引用一旦确定了引用关系就不能改变)不难发现,这与指针常量的特性一样。指针常量也是不能再改变指针指向的地址。
暂无图片
编程学习 ·

Java中的反射(1)

Java中的反射 一、概念: 1、反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 2、反射 (Reflection) 是 Java 的特…
暂无图片
编程学习 ·

Unity的学习(二):打砖块

一、新建项目创建成功后,进入了如下界面。二、场景的设计 在Hierarchy中鼠标右键创建Plane(地面)游戏物体,将其Transform组件重置,并将游戏物体重命名为Ground,如下图所示。调整地面的大小。在Assets下创建文件夹Materials,并在其中创建Ground的Material(材质)并在Gro…
暂无图片
编程学习 ·

智能电视良心分享:沉浸式音画享受旗舰级品质做工

上周,我家工作了五年的电视结束了它的“职业生涯”。面对市面上林林总总的智能电视品牌,我在纠结良久之后,最终将目光锁定在荣耀智慧屏X1身上,趁着618打折,入手了一台。不得不说这款电视最近挺火,但是性能咋样,还得亲身体验过才有发言权。买电视我最关心还是音画质。很多…
暂无图片
编程学习 ·

SpringBoot问题集锦

问题一: SpringBoot应用部署在外置Tomcat中没有启动,无任何反应 解决办法:启动类继承SpringBootServletInitializer并实现configure方法;@SpringBootApplication public class PaysApplication extends SpringBootServletInitializer {@Overrideprotected SpringApplicatio…
暂无图片
编程学习 ·

难得一遇的5G大屏手机 荣耀X10 Max配置分析

6月22日,荣耀通过微博证实了荣耀X10 Max的存在,并宣布将会在7月2日正式发布。消息一出可谓是让很多人非常欣喜,尤其是等了多年大屏手机的用户。荣耀X10 Max不仅是荣耀在5G时代发布的首款大屏手机,也是荣耀时隔两年,继荣耀8X后的续作。那么这款5G大屏手机有哪些特点和配置呢…
暂无图片
编程学习 ·

动态任务

1.任务句柄 /* LED任务句柄 */ static TaskHandle_t LED_Task_Handle; 2.任务创建函数 BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //任务函数const char * const pcName, //任务名称const uint16_t usStackDepth, //堆栈大小void * const pvParamet…
暂无图片
编程学习 ·

在 Kudu 中集成 Hive Metastore

在启用 Kudu-HMS 集成之前,要确保 Kudu 和 HMS 现有表的视图一致。这可能需要重命名Kudu表以符合Hive命名约束。在启用与 Hive Metastore 集成之前应升级现有 Kudu 表。准备升级 在升级过程中,Kudu群集仍然可用。Kudu 和 Hive Metastore 中的表可能会更改或重命名。 可以使用…
暂无图片
编程学习 ·

软件测试的基本流程

软件测试的基本流程 1. 测试需求分析阶段阅读需求 理解需求 主要就是对业务的学习 分析需求点 参与需求评审会议2. 测试计划阶段主要任务就是编写测试计划 参考软件需求规格说明书 项目总体计划,内容包括测试范围(来自需求文档),进度安排,人力物力的分配,整体测试策略的制…
暂无图片
编程学习 ·

1.古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子, * 小兔子长到第三个月后每个月又生一对兔子, * 假如兔子都不死,问每个月的兔子对数为多少? * 分析: * 月份:1 2 3

package com.ujiuye.java;/*古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,* 小兔子长到第三个月后每个月又生一对兔子,* 假如兔子都不死,问每个月的兔子对数为多少?* 分析:* 月份:1 2 3 4 5 6 7 8 9...* 对数 :1 1 2 3 5 8 13 21... */ public class Dem…
暂无图片
编程学习 ·

人工智能常用数据预处理

人工智能常用数据预处理一级目录正态化、标准化、归一化、正则化区别和作用 一级目录 1.读数据 2.合并训练和测试 2.填充空白数据 4.改变非数字为数字 5.去除无关数据 6.降为(合并相关数据) 7.正态化数据(碗圆) 正态化、标准化、归一化、正则化区别和作用 1.正态化归一化是…
暂无图片
编程学习 ·

B 1065 单身狗(散列的应用)

题目思路 这题明明写的是200ms要求,但可以暴力破解,这是我万万没想到的- -; 散列记录夫妻,hash【id】=cpid; 再用一个散列记录到场; 之后暴力破解遍历0~100001; 测试点3:注意00000的输出,有格式要求,不能输出成0; AC代码 #include<bits/stdc++.h> using names…
暂无图片
编程学习 ·

matplotlib绘制折线图

绘制了折线图(plt.plot)设置了图片的大小和分辨率(plt.figure)实现了图片的保存(plt.savefig)设置了xy轴上的刻度和字符串(xticks)解决了刻度稀疏和密集的问题(xticks)设置了标题,xy轴的lable(title,xlable,ylable)设置了字体(font_manager. fontProperties,matplotlib.rc)在一…
暂无图片
编程学习 ·

RocketMQ Remoting模块系列之NettyRemotingServer源码浅析

写在前面RocketMQ Remoting模块也是整个代码中比较简单的一个模块,在掌握基本的Netty知识之后就可以尝试对源码进行简单的阅读分析,我也是结合源码分析来进行Netty应用的学习。该模块主要有两个类 NettyRemotingServer 和 NettyRemotingClient 。分别对应服务端和客户端,服务…
暂无图片
编程学习 ·

操作系统-中断

什么是中断?中断是改变处理器执行指令顺序的一种事件。 这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。为什么需要中断?有了中断后,使CPU可以与其他设备并行工作,能有效提高CPU的利用率,改善系统性能,支持系统的异步性。中断的类型 分为 : 同步中断(内部中…
暂无图片
编程学习 ·

杰里之难点播歌曲概率出现卡顿现象篇

其实本身对于杰里的IC性能不是很稳定,客观的评价。 对于TWS经常性的会遇到播歌曲卡顿: 小编总结了以下几个方面: 1 硬件天线首先要调试OK,保证单耳捂住不卡,过8852测试仪器可以通过。 2 软件晶振在合理有效范围内 3 软件主频提升至最高主频 4 提升flash的供电电压 5 保证软…
暂无图片
编程学习 ·

elasticsearch

1. elasticsearch基本操作 1.1. 基本概念 Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的。 对比关系: 索引(indices)----------------------Databases 数据库类型(type)--------------------------Table 数据表文档(Document)--…