Redis知识最全整理

Redis数据结构

String

String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。

这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。

使用场景
  • 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  • 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。
Hash

这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。

List

List 是有序列表,可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。

使用场景

分页:可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。

消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

Set

Set 是无序集合,会自动去重。如果需要对一些数据进行快速的全局去重,本地可以基于 JVM 内存里的 HashSet 进行去重,但是如果系统部署在多台机器上,可以使用Redis进行全局的 Set 去重。

使用场景

可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁。

SortedSet

Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。

有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。

使用场景

排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
微博热搜榜,就是有个后面的热度值,前面就是名称。

HyperLogLog
简介

Redis 在 2.8.9 版本添加了 HyperLogLog 结构。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

原理

基数基数就是统计集合中不重复的元素的个数。最简单的算法是,建立一个集合将元素添加进去,新增元素之前先判断元素是否存在,不存在就不添加。这样的问题是:
1、这个集合占用的空间非常大。
2、集合大了之后判断一个元素是否存在变得困难。

基数技术演进

基数计数方法
1、hashset或者B+树:插入和查找效率很高,但是不节省存储空间,数据量一大就挂掉了。
2、bitmap:

  • 用位数组来表示各元素是否出现,每个元素对应一位,所需的总内存为n bit。能大大减少内存占用且位操作迅速。
  • 如果要统计1亿个数据的基数值,大约需要内存100000000/8/1024/1024 ≈
    12M,内存减少占用的效果显著。然而统计一个对象的基数值需要12M,如果统计10000个对象,就需要将近120G,同样不能广泛用于大数据场景。

3、概率算法
概率算法不操作数据,而是根据概率算法估算出大约多少个基数,由于是基于概率的,所以基数值可能有偏差。

  • Linear
    Counting(LC):早期的基数估计算法,LC在空间复杂度方面并不算优秀,实际上LC的空间复杂度与上文中简单bitmap方法是一样的(但是有个常数项级别的降低),都是O(Nmax);
  • LogLog Counting(LLC):LogLog
    Counting相比于LC更加节省内存,空间复杂度只有O(log2(log2(N​max)));
  • HyperLogLog Counting(HLL):HyperLogLog
    Counting是基于LLC的优化和改进,在同样空间复杂度情况下,能够比LLC的基数估计误差更小。HLL在空间复杂度和错误率方面最优。一亿的数据HLL需要内存
    不到1k就能做到,效率惊人。
HLL实现原理

先看下网上大神的总结:我们丢硬币的场景,第一次出现正面,之前都是反面的概率与实验次数有一个关系 n = 2^k,那么我们将 key 映射成 二进制hashcode,0001010, 该过程完全随机,那么 n 就等于 2的4次方,也就是说试验次数达到 8,但误差较大,所以我们采用多个映射取平均,这样误差就会变小。
通俗点说明: 假设我们为一个数据集合生成一个8位的哈希串,那么我们得到00000111的概率是很低的,也就是说,我们生成大量连续的0的概率是很低的。生成连续5个0的概率是1/32,那么我们得到这个串时,可以估算,这个数据集的基数是32。
HyperLogLog算法是用来计算基数的,这个抛硬币的序列和基数有什么关系呢?比如在数据库中,我只要在每次插入一条新的记录时,计算这条记录的hash,并且转换成二进制,就可以将其看成一个硬币序列了。大致原理的伪代码如下:

输入:一个集合
输出:集合的基数
算法:
     max = 0
     对于集合中的每个元素:
               hashCode = hash(元素)
               num = hashCode二进制表示中最前面连续的0的数量
               if num > max:
                   max = num
     最后的结果是2的(max + 1)次幂  
redis中HLL的使用

pfadd 添加
影响基数估值则返回1否则返回0.若key不存在则创建
时间复杂度O(1)

127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1

pfcount 获得基数值

  • 得到基数值,白话就叫做去重值(1,1,2,2,3)的插入pfcount得到的是3
  • 可一次统计多个key
  • 时间复杂度为O(N),N为key的个数
  • 返回值是一个带有 0.81% 标准错误(standard error)的近似值.
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
127.0.0.1:6379> pfcount m1
(integer) 4

pfmerge 合并多个key

  • 取多个key的并集
  • 命令只会返回 OK.
  • 时间复杂度为O(N)
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
127.0.0.1:6379> pfcount m1
(integer) 4
127.0.0.1:6379> pfadd m2 3 3 3 4 4 4 5 5 5 6 6 6 1
(integer) 1
127.0.0.1:6379> pfcount m2
(integer) 5
127.0.0.1:6379> pfmerge mergeDes m1 m2
OK
127.0.0.1:6379> pfcount mergeDes
(integer) 6
应用场景
  • 使用的场景都是一个大集合中,找出不重复的基数数量。
  • 基数不大,数据量不大就用不上,会有点大材小用浪费空间。
  • 有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么 和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog
    去重比 bitmap 方便很多。
  • 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数。
具体使用:
  • 获取每天独立IP的访问量
  • 获取每天某个页面user的独立访问量。这样的的场景不能考虑使用set去做,因为涉及大量的存储,占用很大的空间,可以考虑采用
  • HyerLogLog去做,当然数值不是很精确。
  • 统计注册 IP 数
  • 统计每日访问 IP 数
  • 统计页面实时 UV 数
  • 统计在线用户数
  • 统计用户每天搜索不同词条的个数
Geo - 地理位置

谷歌也出了一个精确度更高的算法,交Google s2,具体文章可以看:高效的多维空间点索引算法 — Geohash 和 Google S2

Geo相关命令
  • GEOADD:将给定的位置对象(纬度、经度、名字)添加到指定的 Key。
  • GEOPOS:从 Key 里面返回所有给定位置对象的位置(经度和纬度)。
  • GEODIST:返回两个给定位置之间的距离。
  • GEOHASH:返回一个或多个位置对象的 GeoHASH 表示。一般6位能确定相同的城市。
  • GEORADIUS:以给定的经纬度为中心,返回目标集合中与中心的距离不超过给定最大距离的所有位置对象。
  • GEORADIUSBYMEMBER:以给定的位置对象为中心,返回与其距离不超过给定最大距离的所有位置对象。
    一般6位能确定相同的城市

具体使用请参考:用 Redis 高效实现GEO地理位置相关功能

Pub/Sub
什么是pub/sub

Pub/Sub功能(means Publish, Subscribe)即发布及订阅功能。基于事件的系统中,Pub/Sub是目前广泛使用的通信模型,它采用事件作为基本的通信机制,提供大规模系统所要求的松散耦合的交互模式:订阅者(如客户端)以事件订阅的方式表达出它有兴趣接收的一个事件或一类事件;发布者(如服务器)可将订阅者感兴趣的事件随时通知相关订阅者。熟悉设计模式的朋友应该了解这与23种设计模式中的观察者模式极为相似。

同样,Redis的pub/sub是一种消息通信模式,主要的目的是解除消息发布者和消息订阅者之间的耦合,Redis作为一个pub/sub的server,在订阅者和发布者之间起到了消息路由的功能。

实现

Redis通过publish和subscribe命令实现订阅和发布的功能。订阅者可以通过subscribe向redis server订阅自己感兴趣的消息类型。redis将信息类型称为通道(channel)。当发布者通过publish命令向redis server发送特定类型的信息时,订阅该消息类型的全部订阅者都会收到此消息。

使用场景

一般不使用,直接使用队列就好

Redis Module
BloomFilter
BloomFilter算法

简介:布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
原理
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
优点:
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(O(k))。而且它不存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点:
一定的误识别率和删除困难。
结合以上几点及去重需求(容忍误判,会误判在,在则丢,无妨),决定使用BlomFilter。

思想

位数组和k个散列函数

位数组
初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。
在这里插入图片描述
添加元素(k个独立的hash函数)
添加元素时,对x使用k个哈希函数得到k个哈希值,对m取余,对应的bit位设置为1。

在这里插入图片描述判断元素是否存在
判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。
图中y1不是集合中的元素,y2属于这个集合或者是一个false positive。

在这里插入图片描述BloomFilter有以下参数:

  • m 位数组的长度
  • n 加入其中元素的数量
  • k 哈希函数的个数
  • f False Positive
RedisSearch

基于redis的搜索引擎

Redis-ML

机器学习模型服务器

一个简单的基于Redis-ML的机器学习生命周期

在这里插入图片描述

Redis为什么那么快

在这里插入图片描述
单线程实现

  • Redis采用的是基于内存的采用的是单进程单线程模型的 KV
    数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 使用多路I/O复用模型,非阻塞IO;
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
    总结:虽然是单线程,对于多核机器,可以开多个实例,不会造成cpu的浪费。单机有瓶颈,这时候就要使用redis集群。

Redis集群

三种集群方式:主从复制,哨兵模式和集群

A.主从复制
主从复制原理

1.从服务器连接主服务器,发送SYNC命令;
2.主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3.主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4.从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5.主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6.从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;(从服务器初始化完成)
7.主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)

主从复制优缺点
优点:

1.支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
2.为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
3.Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
4.Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
5.Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据

缺点:

1.Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
2.主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
3.Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

B.哨兵模式

当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。 为此,Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。

哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。
(1)监控主服务器和从服务器是否正常运行。
(2)主服务器出现故障时自动将从服务器转换为主服务器。

哨兵的工作方式

1.每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
2.如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
3.如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
4.当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)
5.在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
6.当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
7.若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

哨兵模式的优缺点
优点

哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
主从可以自动切换,系统更健壮,可用性更高。

缺点

Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

C.Redis-Cluster集群

redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容。

Redis-Cluster采用无中心结构,它的特点如下:
1.所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
2.节点的fail是通过集群中超过半数的节点检测失效时才生效。
3.客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

工作方式

在redis的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。

Redis持久化以及原理

RDB快照

Redis支持将当前数据的快照存成一个数据文件的持久化机制。而一个持续写入的数据库如何生成快照呢。Redis借助了fork命令的copy on write机制。在生成快照时,将当前进程fork出一个子进程,然后在子进程中循环所有的数据,将数据写成为RDB文件。

我们可以通过Redis的save指令来配置RDB快照生成的时机,比如你可以配置当10分钟以内有100次写入就生成快照,也可以配置当1小时内有1000次写入就生成快照,也可以多个规则一起实施。这些规则的定义就在Redis的配置文件中,你也可以通过Redis的CONFIG SET命令在Redis运行时设置规则,不需要重启Redis。

Redis的RDB文件不会坏掉,因为其写操作是在一个新进程中进行的,当生成一个新的RDB文件时,Redis生成的子进程会先将数据写到一个临时文件中,然后通过原子性rename系统调用将临时文件重命名为RDB文件,这样在任何时候出现故障,Redis的RDB文件都总是可用的。

同时,Redis的RDB文件也是Redis主从同步内部实现中的一环。

但是,我们可以很明显的看到,RDB有它的不足,就是一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的,从上次RDB文件生成到 Redis停机这段时间的数据全部丢掉了。在某些业务下,这是可以忍受的,我们也推荐这些业务使用RDB的方式进行持久化,因为开启RDB的代价并不高。 但是对于另外一些对数据安全性要求极高的应用,无法容忍数据丢失的应用,RDB就无能为力了,所以Redis引入了另一个重要的持久化机制:AOF日志。

AOF日志

AOF日志的全称是Append Only File,从名字上我们就能看出来,它是一个追加写入的日志文件。与一般数据库不同的是,AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令。他会记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据。

其生成过程和RDB类似,也是fork一个进程,直接遍历数据,写入新的AOF临时文件。在写入新文件的过程中,所有的写操作日志还是会写到原来老的 AOF文件中,同时还会记录在内存缓冲区中。当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。然后调用原子性的rename命令用新的 AOF文件取代老的AOF文件。

AOF与RDB的区别

rdb的特性如下

RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。

fork一个进程,遍历hash table,利用copy on write,把整个db dump保存下来。

save, shutdown, slave 命令会触发这个操作。

粒度比较大,如果save, shutdown, slave 之前crash了,则中间的操作没办法恢复。

aof的特性如下

AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存在。

把写操作指令,持续的写到一个类似日志文件里。(类似于从postgresql等数据库导出sql一样,只记录写操作)

粒度较小,crash之后,只有crash之前没有来得及做日志的操作没办法恢复。

区别

两种区别就是,aof是持续的用日志记录写操作,crash后利用日志恢复;rdb是平时写操作的时候不触发写,只有手动提交save命令,或者是关闭命令时,才触发备份操作。

选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

RDB 的优点:RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。

RDB 的缺点:如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。

AOF 的优点:AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

AOF 的缺点:对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。

一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。

Redis内存模型

Redis作为内存数据库,在内存中存储的内容主要是数据(键值对);通过前面的叙述可以知道,除了数据以外,Redis的其他部分也会占用内存。

Redis的内存占用主要可以划分为以下几个部分:

数据

作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。

Redis使用键值对存储数据,其中的值(对象)包括5种类型,即字符串、哈希、列表、集合、有序集合。这5种类型是Redis对外提供的,实际上,在Redis内部,每种类型可能有2种或更多的内部编码实现;此外,Redis在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如redisObject、SDS等;这篇文章后面将重点介绍Redis中数据存储的细节。

进程本身运行需要的内存

Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。

补充说明:除了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。当然,这部分内存不属于Redis进程,也不会统计在used_memory和used_memory_rss中。

缓冲内存

缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由jemalloc分配,因此会统计在used_memory中。

内存碎片

内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。

内存碎片的产生与对数据进行的操作、数据的特点等都有关;此外,与使用的内存分配器也有关系:如果内存分配器设计合理,可以尽可能的减少内存碎片的产生。后面将要说到的jemalloc便在控制内存碎片方面做的很好。

如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。
细节请参考该文章:https://www.cnblogs.com/kismetv/p/8654978.html

Redis渐进式哈希

渐进式哈希的原理:
将原哈希表中的数据以少量多次的方式,rehash到新的哈希表中,避免一次性数据迁移导致堵塞问题,通过rehashidx记录rehash的进度,在rehash结束后,新的哈希表将替代原哈希表。
具体细节请参考该文章:https://baijiahao.baidu.com/s?id=1663870339713597715&wfr=spider&for=pc

Redis过期策略以及内存淘汰机制

过期键删除策略

定时删除

设置键的过期时间时,创建一个 Timer ,当过期时间到临时,立刻删除键。
内存友好型策略,一旦键过期,就会被删除,并释放所占用的内存,Cpu 不友好,当一批数量比较多的键过期时,正好遇上Cpu 紧张的时段,这时候需要的是Cpu处理能力,而不是内存,显然 Cpu 时间用在删除过期键上,会对服务器的响应时间和吞吐量造成影响。另外当前 Redis 时间事件(无序链表O(N))无法高效处理大量时间事件,所以定时删除并不是一种好的定时删除策略。

惰性删除

不管过期的键,在这种策略下,当键在键空间中被取出时,首先检查取出的键是否过期,若过期删除该键,否则,返回该键。
很明显,惰性删除依赖过期键的被动访问,对于内存不友好,如果一些键长期没有被访问,会造成内存泄露(垃圾数据占用内存)。我们知道,Redis是依赖内存的,所以惰性删除也不是一个好的策略。

定期删除

由定时删除算法,定期的去检查一定的数据库,删除一定的过期键。
通过合理的删除操作执行的时长和频率,达到合理的删除过期键。

内存淘汰机制

如果redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)

allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)

volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

Redis分布式锁

实现

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

异常场景

如果在setnx之后执行expire之前进程意外crash或者要重启维护了怎么办?
【答】set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的。#从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。

多节点 Redis 分布式锁:Redlock 算法
  • 获取当前时间(start)。
  • 依次向 N 个 Redis节点请求锁。请求锁的方式与从单节点 Redis获取锁的方式一致。为了保证在某个
    Redis节点不可用时该算法能够继续运行,获取锁的操作都需要设置超时时间,需要保证该超时时间远小于锁的有效时间。这样才能保证客户端在向某个Redis节点获取锁失败之后,可以立刻尝试下一个节点。
  • 计算获取锁的过程总共消耗多长时间(consumeTime = end - start)。如果客户端从大多数 Redis节点(>= N/2+1) 成功获取锁,并且获取锁总时长没有超过锁的有效时间,这种情况下,客户端会认为获取锁成功,否则,获取锁失败。
  • 如果最终获取锁成功,锁的有效时间应该重新设置为锁最初的有效时间减去 consumeTime。
  • 如果最终获取锁失败,客户端应该立刻向所有 Redis节点发起释放锁的请求。
Redis 中设置过期时间主要通过以下四种方式
  • expire key seconds:设置 key 在 n 秒后过期;
  • pexpire key milliseconds:设置 key 在 n 毫秒后过期;
  • expireat key timestamp:设置 key 在某个时间戳(精确到秒)之后过期;
  • pexpireat key millisecondsTimestamp:设置 key 在某个时间戳(精确到毫秒)之后过期;

Redis缓存雪崩/穿透/击穿

缓存穿透:

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

解决方案

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存击穿:

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案
使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

缓存雪崩:

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方案

大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一种被称为“二级缓存”的解决方法。

Redis使用场景

Redis实现异步队列

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

Redis实现延时队列

使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
不过,增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。

参考链接:
Redis:HyperLogLog使用与应用场景
Redis高级数据结构HyperLogLog原理详解和应用场景
用 Redis 高效实现GEO地理位置相关功能
高效的多维空间点索引算法 — Geohash 和 Google S2
BloomFilter(大数据去重)+Redis(持久化)策略
用Redis-ML模块实现实时机器学习

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值