Redis笔记

Redis 是一个开源(BSD许可)的内存存储系统,它可以用作数据库、缓存和消息中间件。支持多种类型的数据结构,如 [字符串(strings)]、 [散列(hashes)], [列表(lists)], [集合(sets)], [有序集合(sorted sets)]。Redis 内置了 [复制(replication)],[LUA脚本(Lua scripting)],[事务(transactions)]和不同级别的 [磁盘持久化(persistence)], 并通过 [Redis哨兵(Sentinel)]和自动 [分区(Cluster)]提供高可用性(high availability)。本文从存储、基础数据结构、基础理论、使用以及实际应用场景对Redis进行解析。

1 Redis底层存储?

1.1 SDS

Simple Dynamic String

1.1.1 内部结构

struct SDS<T> {
    T capacity; // 数组容量
    T len; // 数组长度
    byte flags; // 特殊标识位,不理睬它
    byte[] content; // 数组内容
}

        上面的 SDS 结构使用了范型 T,为什么不直接用 int 呢,这是因为当字符串比较短时,len 和 capacity 可以使用 byte 和 short 来表示,Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。

        字符串是可以修改的字符串,它要支持 append 操作。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再 append 新内容。如果字符串的长度非常长,这样的内存分配和复制开销就会非常大

  • SDS除了保存数据库的之外,还被用来做缓冲区使用!!!

  • 与c字符串不同,SDS完全杜绝了内存溢出的问题。

  • 减少字符串扩充带来的内存分配次数,(SDS扩容的时候可能会多分配一些空间,即空间预分配,另外在释放内存的时候采用惰性释放);

1.1.2 embstr vs raw

        Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储。首先来了解一下 Redis 对象头结构体,所有的 Redis 对象都有下面的这个结构头:

struct RedisObject {
    int4 type; // 4bits
    int4 encoding; // 4bits
    int24 lru; // 24bits
    int32 refcount; // 4bytes
    void *ptr; // 8bytes,64-bit system
} robj;

        不同的对象具有不同的类型 type(4bit),同一个类型的 type 会有不同的存储形式 encoding(4bit),为了记录对象的 LRU 信息,使用了 24 个 bit 来记录 LRU 信息。每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对象内容 (body) 的具体存储位置。这样一个 RedisObject 对象头需要占据 16 字节的存储空间。

        embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。

1.1.3 embstr最大容纳的长度

        RedisObject对象头占据16个字节, SDS对象头至少3个字节, 内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64。为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。留给 content 的长度最多只有 45(64-19) 字节了。字符串又是以\0结尾,所以 embstr 最大能容纳的字符串长度就是 44。

1.1.4 扩容

        字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。

1.2 dict

        除了hash结构使用了字典(dict)外,整个Redis数据库中所有的key和value也组成了一个全局的字典

图片

1.2.1 内部结构

        dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。

1.2.1.1 Rehash步骤
  • 计算负载因子

    负载因子=哈希表已经保存的节点数量/hash表的大小

    load_factor =ht[0].used / ht[0].size

  • 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量,也就是ht[0].used属性的值:

    如果当前执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2^n.

    如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n.

  • 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的hash值和索引值,然后键键值对放置到ht[1]哈希表的指定位置上

  • 当ht[0]包含的所有键值对都迁移到ht[1]之后,ht[0]变成空表,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白的哈希表,为下一次rehash做准备。

1.2.1.2 渐进式hash

        大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个O(n)级别的操作,作为单线程的Redis表示很难承受这样耗时的过程。所以Redis使用渐进式rehash小步搬迁。虽然慢一点,但是肯定可以搬完。搬迁操作埋伏在当前字典的后续指令中(来自客户端的hset/hdel指令等),但是有可能客户端闲下来了,没有了后续指令来触发这个搬迁,那么Redis就置之不理了么?当然不会,优雅的Redis怎么可能设计的这样潦草。Redis还会在定时任务中对字典进行主动搬迁。

1.2.2 查找过程

        hashtable的元素在第二维的链表上,首先需要确定元素在哪一个链表上。

1.2.3 扩容条件

        正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize)。但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。

1.2.4 缩容条件

        当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考虑 Redis 是否正在做 bgsave。

1.3 ziplist

图片

       Redis 为了节约内存空间使用,zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。

struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

        压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一个元素,然后倒着遍历。因为 ziplist 都是紧凑存储,没有冗余空间 (对比一下 Redis 的字符串结构)。意味着插入一个新的元素就需要调用 realloc 扩展内存。取决于内存分配器算法和当前的 ziplist 内存大小,realloc 可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。所以 ziplist 不适合存储大型字符串,存储的元素也不宜过多。

1.4 intset

        当set集合容纳的元素都是整数并且元素个数较少的时候,Redis会使用intset来存储结合元素。intset 是紧凑的数组结构,同时支持 16 位、32 位和 64 位整数。​​​​​​​

struct intset<T> {
    int32 encoding; // 决定整数位宽是 16 位、32 位还是 64 位
    int32 length; // 元素个数
    int<T> contents; // 整数数组,可以是 16 位、32 位和 64 位
}

1.5 linkedlist

        链表在Redis中应用广泛,比如列表键的底层实现之一就是链表。其中每个链表节点使用一个adlist.h/listNode结构来表示:​​​​​​​

typedef struct listNode{
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点d额值
    void *value;
}listNode;

        并且使用adlist.h/list来持有链表,便于操作:​​​​​​​

typedef struct list{
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 链表中所包含的节点数量
    unsigned long len;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void *(*free)(void *ptr);
    // 节点值对比函数
    void *(*match)(void *ptr, void *key);
}list;

1.6 quicklist

图片

        考虑链表的附加空间相对太高,prev和next指针就要占据16个字节(64位系统为8个字节),另外每一个节点的内存是单独存储的,加剧了内存的碎片化,影响内存的效率,在后续的版本中,使用quicklist代替了ziplist和linkedlist。

        quicklist是ziplist和linkedlist的混合体,它将linkedlist按段切分,每一段使用ziplist来紧凑存储,多个ziplist之间使用双向指针连接。

struct ziplist {
}
struct ziplist_compressed {
    int32 size;
    byte[] compressed_data;
}
struct quicklistNode {
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向压缩列表
    int32 size; // ziplist 的字节总数
    int16 count; // ziplist 中的元素数量
    int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
    // ...
}
struct quicklist {
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素总数
    int nodes; // ziplist 节点的个数
    int compressDepth; // LZF 算法压缩深度
    // ...
}

1.7 skiplist

        Redis 的 zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获取 value 列表的功能,这就需要另外一个结构「跳跃列表」。zset 的内部实现是一个 hash 字典加一个跳跃列表 (skiplist)。hash 结构类似于 Java 语言中的 HashMap 结构。

图片

typedef struct zskiplistNode {
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;
typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

​​​​​​​

1.7.1 插入

  • 随机层数:对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数。直观上期望的目标是 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层2^-63,因为这里每一层的晋升概率是 50%。不过 Redis 标准源码中的晋升概率只有 25%,也就是代码中的 ZSKIPLIST_P 的值。所以官方的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一点。也正是因为层数一般不高,所以遍历的时候从顶层开始往下遍历会非常浪费。跳跃列表会记录一下当前的最高层数maxLevel,遍历时从这个 maxLevel 开始遍历性能就会提高很多。

  • 找出搜索路径

  • 创建新的节点(随机分配层数)  

  • 将搜索路径上的节点和这个新的节点串联起来

  • 可能需要更新最大层数

  • 以下为插入80和45到skiplist中演示。

图片

1.7.2 删除

  • 找出搜索路径

  • 对每一层相关的节点重排前向后向指针

  • 更新最大层数maxlevel

1.7.3 修改

        Redis中对分数的修改就很粗暴,直接删除了重新插入,根本就不会判断值到底有没有发生改变!无脑删除无脑重新插入。

1.7.4 查找

        跳跃列表有了多层结构之后,这个定位的算法复杂度将会降到 O(lg(n))。

1.8 listpack

        Redis5.0后引入了新的数据结构listpack,它是对ziplist的改进,在存储空间上更加节省,结构上比ziplist更加精简。​​​​​​​

struct listpack<T> {
    int32 total_bytes; // 占用的总字节数
    int16 size; // 元素个数
    T[] entries; // 紧凑排列的元素列表
    int8 end; // 同 zlend 一样,恒为 0xFF
}


 

1.9 Rax

        Rax 是 Redis 内部比较特殊的一个数据结构,它是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。我们可以将一本英语字典看成一棵 radix tree,它所有的单词都是按照字典序进行排列,每个词汇都会附带一个解释,这个解释就是 key 对应的 value。有了这棵树,你就可以快速地检索单词,还可以查询以某个前缀开头的单词有哪些。Redis 五大基础数据结构里面,能作为字典使用的有 hash 和 zset。hash 不具备排序功能,zset 则是按照 score 进行排序的。rax 跟 zset 的不同在于它是按照 key 进行排序的。Redis 作者认为 rax 的结构非常易于理解,但是实现却有相当的复杂度,需要考虑很多的边界条件,需要处理节点的分裂、合并,一不小心就会出错。

2 对象 

        Redis并没有直接使用上述介绍的数据结构来直接实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象,每一种对象至少使用了一种上述的数据结构。

2.1 string

        一个key存储一个字符串。如果你要存数据呢?转换成Json或者其他的字符串序列化。不过需要注意企业在使用的时候,一般会限制存储的字符串的长度。

数据命令

1)赋值:SET key value。如 set hello world

2)取值:GET key。如 get hello。返回是 world

3)自增:INCR key。就是 Mysql的AUTO_INCREMENT。每次执行 INCR key时,该key的值都会+1.若key不存在,则先建立一个0,然后+1,返回 1。如果值不是整数则报错。该操作是原子操作。

4)自减:DECR key。将指定 key 的值减少 1。 如 DECR num,就是 num-1

5)自增 N:INCRBY key increment 用来给指定 key 的值加 increment。如 INCRBY num 5 就是 num+5

6)自减 N:DECRBY key increment 用来给指定 key 的值减 increment。如 DECRBY num 5 就是 num-5

7)增加浮点数:INCRBYFLOAT key increment。

8)向尾部追加:APPEND key value。如set test:key 123、append test:key 456、get test:key 就是 123456

9)获取长度:STRLEN key。

10)同时给多个 key 赋值:MSET title 这是标题 description 这是描述 content 这是内容。

11)同时获取多个 key 的值:MGET title description content

12)位操作之获取:GETBIT key offset。如字符 a 在 redis 中的存储为 01100001(ASCII为98),那么 GETBIT key 2 就是 1,GET key 0 就是 0。

13)位操作之设置:SETBIT key offset value。如字符 a 在 redis 中的存储为 01100001(ASCII为98),那么 SETBIT key 6 0,SETBIT key 5 1 那么 get key 得到的是 b。因为取出的二进制为 01100010。

14)位操作之统计:BITCOUNT key [start] [end]:BITCOUNT key 用来获取 key 的值中二进制是 1 的个数。而 BITCOUNT key start end 则是用来统计key的值中在第 start 和 end 之间的子字符串的二进制是 1 的个数。

15)位操作之位运算:BITOP operation resultKey key1 key2。operation 是位运算的操作,有 AND,OR,XOR,NOT。resultKey 是把运算结构存储在这个 key 中,key1 和 key2 是参与运算的 key,参与运算的 key 可以指定多个。

2.2 list

        列表类型存储了一个有序的字符串列表。常用的操作是向两端插入新的元素。时间复杂度为 O(1)。结构为一个链表。记录头和尾的地址。看到这里,Redis 数据类型的列表类型一个重大的作用呼之欲出,那就是队列。新来的请求插入到尾部,新处理过的从头部删除。另外,比如微博的新鲜事。比如日志。列表类型就是一个下标从 0 开始的数组。由于是链表存储,那么越靠近头和尾的元素操作越快,越靠近中间则越慢。

数据命令

1)向头部插入:LPUSH key value1 value2…。返回增加后的列表长度。

2)向尾部插入:RPUSH key value1 value2…。返回增加后的列表长度。

3)从头部弹出:LPOP key。返回被弹出的元素值。该操作先删除key列表的第一个元素,再将它返回。

4)从尾部弹出:RPOP key。返回被弹出的元素值。

5)列表元素个数:LLEN key。key 不存在返回 0。

6)获取列表的子列表:LRANGE start end。返回第 start 个到第 end 个元素的列表。包含 start 和 end。支持负数索引。-1 表示最后一个元素,-2 表示倒数第二个元素。

7)删除列表中指定值:LREM key count value。删除 key 这个列表中,所有值为 value 的元素,只删除 count。如果有 count+1 个,那么就保留最后一个。count 不存在或者为 0,则删除所有的。如果 count 大于 0,则删除从头到尾的 count 个,如果 count 小于 0,则删除从尾到头的 count 个。

8)获取指定索引值:LINDEX key index。如LINDEX key 0就是列表的第一个元素。index可以是负数。

9)设置索引和值:LSET key index value。这个操作只是修改指定 key 且指定 index 的值。如果 index 不存在,则报错。

10)保留片段,删除其它:LTRIM key start end。保留 start 到 end 之间的所有元素,含 start 和 end。其他全部删除。

11)向列表插入元素:LINSERT key BEFORE/AFTER value1 value2。从列表头开始遍历,发现值为 value1 时停止,将 value2 插入,根据 BEFORE 或者 AFTER 插入到 value1 的前面还是后面。

12)把一个列表的一个元素转到另一个列表:RPOPLPUSH list1 list2。将列表 list1 的右边元素删除,并把该与元素插入到列表 list2 的左边。原子操作。

2.3 set

        集合类型是为了方便对多个集合进行操作和运算。集合中每个元素不同且没有顺序的概念,每个元素都是且只能是一个字符串。常用操作是对集合插入、删除、判断等操作。时间复杂度尾 O(1)。可以进行交集、并集、差集运算。例如文章 1 的有 3 个标签,是一个 Redis 数据类型集合类型存储。文章 2 有 3 个标签,有一个 Redis 数据类型集合类型存储。文章是 1 是 mysql,文章 2 是讲 redis。那么交集是不是就交出了一个数据库?(假设数据库这个tag在两篇文字都有)。集合类型在 redis 中的存储是一个值为空的散列表。

数据命令

1)增加:SADD key value。

2)删除:SREM key value。

3)获取指定集合的所有元素:SMEMBERS key。

4)判断某个元素是否存在:SISMEMBER key value。

5)差集运算:SDIFF key1 key2…。对多个集合进行差集运算。

6)交集运算:SINNER key1 key2…。对多个集合进行交集运算。

7)并集运算:SUNION key1 key2…。对多个集合进行并集运算。

8)获取集合中元素个数:SCARD key。返回集合中元素的总个数。

9)对差集、交集、并集运算的结果存放在一个指定的 key 中:SDIFFSTORE storekey key1 key2。对 key1 和 key2 求差集,结果存放在 key 为 storekey 的集合中。SINNERSTORE 和 SUNIONSTORE 类似。

10)获取集合中的随即元素:SRANDMEMBER key [count]。参数 count 可选,如果 count 不存在,则随即一个。count 大于 0,则是不重复的 count 个元素。count 小于 0,则是一共 |count|个 元素,可以重复。

11)随即弹出一个元素:SPOP key。随即从集合中弹出一个元素并删除,将该元素的值返回。

2.4 hash

        Redis 是以字典(关联数组)的形式存储的,一个 key 对应一个 value。在字符串类型中,value 只能是一个字符串。那么在散列类型,也叫哈希类型中,value 对应的也是一个字典(关联数组)。那么就可以理解,Redis 的哈希类型/散列类型中,key 对应的 value 是一个二维数组。但是字段的值只可以是字符串。也就是说只能是二维数组,不能有更多的维度。

数据命令

1)赋值:HSET key field value。如 hset user name lane。hset user age 23

2)取值:HGET key field。如 hget user name,得到的是 lane。

3)同一个key多个字段赋值:HMSET key field1 value1 field2 value2…

4)同一个KEY多个字段取值:HMGET key field1 fields2…

5)获取KEY的所有字段和所有值:HGETALL key。如 HGETALL user 得到的是 name lane age 23。每个返回都是独立的一行。

6)字段是否存在:HEXISTS key field。存在返回 1,不存在返回 0

7)当字段不存在时赋值:HSETNX key field value。如果 key 下面的字段 field 不存在,则建立 field 字段,且值为 value。如果 field 字段存在,则不执行任何操作。它的效果等于 HEXISTS + HSET。但是这个命令的优点是原子操作。再高的并发也不会怕怕。

8)自增 N:HINCREBY key field increment。同字符串的自增类型,不再阐述。

9)删除字段:DEL key field1 field2… 删除指定KEY的一个或多个字段。

10)只获取字段名:HKEYS key。与 HGETALL 类似,但是只获取字段名,不获取字段值。

11)只获取字段值:HVALS key。与 HGETALL 类似,但是只获取字段值,不获取字段名。

12)获取字段数量:HLEN key。

2.5 zset

        集合类型是无序的,每个元素是唯一的。那么有序集合就是有序的,每个元素是唯一的。有序集合类型和集合类型的差别是,有序集合为每个元素配备了一个属性:分数。有序集合就是根据分数来排序的。有序集合是使用散列表和跳跃表实现的。所以和列表相比,操作中间元素的速度也很快。时间复杂度尾 O(log(N))。Redis 数据类型中的有序集合类型比 Redis 数据类型中的列表类型更加耗费资源。

数据命令

1)增加:ZADD key sorce1 value1 sorce2 value2…。

2)获取分数:ZSCORE key value。获取key的有序集合中值为 value 的元素的分数。

3)获取排名在某个范围内的元素列表:ZRANFGE key start stop [WITHSCORE]。获取排名在 start 和 end 之间的元素列表,包含 start 和 end2 个元素。每个元素一行。如果有WITHSCORE参数,则一行元素值,一行分数。时间复杂度为O(LOGn+m)。如果分数相同,则 0<0</a

4)获取指定分数范围的元素:ZRANGEBYSCORE key min max [WITHSCORE] [LIMIT offset count]。获取分数在 min 和 max 之间的元素列表。含两头。每个元素一行。如果有 WITHSCORE 参数,则一行元素值,一行分数。如果 min 大于 max 则顺序反转。

5)为某个元素增加分数:ZINCRBY key increment value。指定的有序集合的值为 value 的元素的分数 +increment。返回值后更改后的分数。

6)获取集合中元素的数量:ZCARD key。

7)获取指定分数范围内的元素个数:ZCOUNT key min max。

8)删除一个或多个元素:ZREM key value1 value2…

9)根据排名范围删除元素:ZREMRANGEBYRANK key start end。删除排名在 start 和 end 中的元素。

10)按照分数范围删除元素:ZREMRANGEBYSCORE key min max。

11)获得元素排名(正序):ZRANK key value。获取 value 在该集合中的从小到大的排名。

12)获得元素排名(倒序):ZREVRANK key value。获取 value 在该集合中从大到小的排名。

13)有序集合的交集:ZINTERSTORE storekey key1 key2…[WEIGHTS weight [weight..]] [AGGREGATE SUM|MIN|MAX]。用来计算多个集合的交集,结果存储在 storekey中。返回值是 storekey 的元素个数。AGGREGATE 为 SUM 则 storekey 集合的每个元素的分数是参与计算的集合分数和。MIN 是参与计算的分数最小值。MAX 是参与计算分数最大值。WEIGHTS 设置每个集合的权重,如 WEIGHTS 1 0.1。那么集合A的每个元素分数 1,集合B的每个元素分数 0.1

14)有序集合的并集:ZUNIONSTORE storekey key1 kye2…[WEIGHTS weight [weight..]] [AGGREGATE SUM|MIN|MAX]

2.6 总结

图片

3 Redis原理 

3.1 事件

图片

3.1.1 文件事件

  • ae.c/aeCreateFileEvent

  • ae.c/aeDeleteFileEvent

  • ae.c/aeGetFileEvents

  • ae.c/aeWait 函数接受(一个套接字描述符、一个事件类型和一个毫秒参数),在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生或者等待超时,函数返回!

  • ae.c/aeApiPoll函数接受一个sys/time.h/struct timeval结构为参数,并在指定的时间内,阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生的文件事件,但至少有一个事件产生或者超时,则结束,和aeWait相比,这个函数并没有指定文件事件的类型。

  • ae.c/aeProcessEvents函数是文件事件分派器,(1)它首先调用aeApiPoll函数等待事件产生,(2)然后遍历所有已经产生的事件,(3)并调用相应的事件处理器来处理这些事件。(文件事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器、复制处理器)

  • ae.c/aeGetApiName函数返回I/O多路复用程序底层所使用的的I/O多路复用函数库的名称,epoll或者select。

3.1.2 时间事件

        Redis的时间事件分为两类---(1)定时事件:让程序在指定的时间之后执行一次;(2)周期性事件:让程序每隔固定的时间执行。

        一个时间事件主要由三个属性构成---(1)id:服务器为时间事件创建的全局唯一ID,ID号从小到大进行递增。(2)when:毫秒级的UNIX时间戳,记录事件的到达时间。(3)timeProc:时间事件处理器,是一个函数,当时间事件到达时,服务器就会调用相应的处理器处理事件。

        服务器将所有的时间事件存放在一个无序的链表中,每当时间事件执行器运行的时候,遍历整个链表,查询所有已经到达的时间事件,并调用相应的事件处理器。

3.2 事件轮询、多路复用

        Redis 基于Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler):文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。文件事件处理器的四个组成部分, 它们分别是套接字、 I/O 多路复用程序、 文件事件分派器(dispatcher)、 以及事件处理器。

图片

        Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select、 epoll 、 evport 和 kqueue这些 I/O 多路复用函数库来实现的, 每个 I/O 多路复用函数库在 Redis 源码中都对应一个单独的文件, 比如 `ae_select.c` 、 `ae_epoll.c` 、 `ae_kqueue.c` , 诸如此类。因为 Redis 为每个 I/O 多路复用函数库都实现了相同的 API , 所以 I/O 多路复用程序的底层实现是可以互换的。

图片

        I/O 多路复用程序总是会将所有产生事件的套接字都入队到一个队列里面, 然后通过这个队列, 以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字:当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字。

epoll 是任务驱动模型,其在高并发场景下保证效率的关键点如下:

  • socket 不再阻塞,服务器线程只需要抛出一个线程,避免大量线程的系统开销(BIO缺陷);

  • 使用 select 多路复用,轮询发生在内核空间而不是在用户空间,不需要进行频繁的内核调用,避免大量的用户态到内核态转换带来的开销(NIO缺陷);

  • 使用mmap 共享内存,不需要进行多次的文件描述符数据的复制过程,节省系统资源(select 多路复用缺陷)

  • 使用 wait 系统调用,即事件驱动模型,且链表天然有序(先后顺序),使事件任务能够顺序处理

3.3 管道

        Redis本身是基于Request/Response协议(停等机制)的,正常情况下,客户端发送一个命令,等待Redis返回结果,Redis接收到命令,处理后响应。在这种情况下,如果同时需要执行大量的命令,那就是等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁调用系统IO,发送网络请求。为了提升效率,这时候pipeline出现了,它允许客户端可以一次发送多条命令,而不等待上一条命令执行的结果,这和网络的Nagel算法有点像(TCP_NODELAY选项)。pipeline不仅减少了RTT,同时也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)

        应用举例:Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作

3.4 持久化

3.4.1 AOF(Append Only File) 

图片

        AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,AOF持久化保存数据库状态的方法是将服务器执行SET、SADD、RPUSH三个命令保存到AOF文件中,如下图:

图片

        Redis 的服务器进程就是一个事件循环(loop), 这个循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复, 而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到 aof_buf 缓冲区里面, 所以在服务器每次结束一个事件循环之前, 它都会调用 flushAppendOnlyFile 函数, 考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面, 这个过程可以用以下伪代码表示:​​​​​​​

def eventLoop():
    while True:
        # 处理文件事件,接收命令请求以及发送命令回复
        # 处理命令请求时可能会有新内容被追加到 aof_buf 缓冲区中
        processFileEvents()
        # 处理时间事件
        processTimeEvents()
        # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面
        flushAppendOnlyFile()
3.4.1.1 AOF步骤
  • 命令追加(append)

  • 文件写入

  • 文件同步(sync)

3.4.1.2 AOF重写步骤
  • 执行客户端发送来的命令。

  • 将执行后的命令追加到AOF缓冲区。

  • 将执行后的写命令追加到AOF重写缓冲区。

3.4.2 RDB

        RDB持久化保存数据的方法是将msg、fruits、numbers三个键的键值对保存到RDB文件中。

图片

        有两个Redis命令可以用来生成RDB文件,一个是SAVE,另外一个是BGSAVE。SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令。BGSAVE命令会派生一个子进程,由子进程负责创建RDB文件。伪代码如下:​​​​​​​

def SAVE():
    # 创建RDB文件
    rdbSave()
def BGSAVE():
    # 创建子进程
    pid = fork()
    if pip == 0:
        # 子进程负责创建RDB文件
        rdbSave()
        # 完成之后向父进程发送信号
        signal_parent()
    elif pid > 0:
        # 父进程继续处理命令请求,并通过轮询等待子进程的信号
        handle_request_and_wait_signal()
    else:
        # 处理出错的情况
        handle_fork_error()

3.4.3 混合持久化

        Redis的两种持久化方式,它们都有各自的优点和缺点:(1)RDB持久化可以生成紧凑的RDB文件,并且使用RDB文件进行数据恢复的速度也非常快,但是RDB的全量持久化模式可能会让服务器在停机时丢失大量数据。(2)与RDB持久化相比,AOF持久化可以将丢失数据的时间窗口限制在1s之内,但是协议文本格式的AOF文件的体积将比RDB文件要大得多,并且数据恢复过程也会相对较慢。

        Redis从4.0版本开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化模式构建。如果打开了服务器的AOF持久化功能,并且将aof-use-rdb-preamble <value>选项的值设置成了yes,则Redis服务器执行AOF重写操作时,就会像执行BGSAVE命令那样,根据数据库当前的状态生成出相应的RDB数据,并将这些数据写入新建的AOF文件中,至于那些在AOF重写开始之后执行的Redis命令,则会继续以协议文本的方式追加到新AOF文件的末尾,即已有的RDB数据的后面。

3.5 通信协议

        RESP是REdis Serialization Protocol的简称,也就是专门为redis设计的一套序列化协议

3.6 事务

3.6.1 事务的基本作用

        每个事务的操作都有 begin、commit 和 rollback,begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化———当前执行的事务有着不被其它事务打断的权利。

begin();
try {
    command1();
    command2();
    ....
    commit();
} catch(Exception e) {
    rollback();
}

Redis 在形式上看起来也差不多,分别是multi/exec/discard。

  • multi 指示事务的开始;

  • exec 指示事务的执行;

  • discard 指示事务的丢弃。

3.6.2 watch

        一种乐观锁机制,它可以在EXEC命令执行前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果被修改过,服务器拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

3.7 内存分配

3.7.1 内存管理基础

[jemalloc](http://tinylab.org/memory-allocation-mystery-·-jemalloc-a)

3.7.2 淘汰策略

        当Redis内存超过物理内存限制的时候,内存数据会开始和磁盘数据产生频繁的交换(swap),交换会让Redis性能急剧下降。当实际内存超过maxmemory的时候,Redis提供了几种可选的策略,让用户自己决定如何腾出新的空间以继续提供读写服务。

  • noviction(不继续写请求)

  • volatile-lru(尝试淘汰设置了过期时间的key,最少使用的key优先被淘汰,没有设置过期时间的key不会被淘汰)

  • volatile-ttl(同上,但是淘汰的策略不是LRU,而是key剩余寿命ttl的值,ttl越小越优先淘汰)

  • volatile-random(淘汰的key是过期集合中随机的key)

  • allkeys-lru(淘汰的key对象是全体key的集合)

  • allkeys-random(淘汰的key对象是全体key的集合)

  • 总结:带有volatile的会对设置了过期时间的key进行淘汰,带有allkeys的会堆所有的key进行淘汰。

  • LRU算法

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

LRU,即:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。

LFU,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。

3.8 Rehash

        当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1

  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5

        其中哈希表的负载因子可以通过公式:​​​​​​​

# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

3.9 定时任务

        Redis 的定时任务会记录在一个称为最小堆的数据结构中。

3.10 多机数据库

3.10.1 复制

图片

        Redis2.8以前的复制功能不能高校处理断线重连后的复制情况,Redis2.8之后新添加的部分重同步功能可以解决这个问题。部分重同步通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分实现。在复制操作刚开始的时候,从服务器会成为主服务器的客户端,通过向主服务器发送命令请求执行复制步骤,而在复制操作后期,主从服务器会相互成为对方的客户端。主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致,而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测。

3.10.2 哨兵

3.10.2.1 故障转移
  • Sentinel只是一个运行在特殊模式下的Redis服务器,它使用了和普通模式不同的命令表,所以Sentinel模式能够使用的命令和普通Redis服务器能够使用的命令不同。

  • Sentinel会读入用户指定的配置文件,为每一个要被监视的主服务器创建相应的实例结构,并创建连向主服务器的命令连接和订阅连接,其中命令连接用于向主服务器发送命令请求,而订阅连接则用于接收指定频道的消息。

  • Sentinel通过向主服务器发送INFO命令来获得主服务器属下所有从服务器的地址信息,并为这些从服务器创建相应的实例结构,以及连向这些从服务器的命令连接和订阅连接。

  • 在一般的情况下,Sentinel以每10秒1次的频率向被监视的主服务器和从服务器发送INFO命令,当主服务器处于下线状态,或者Sentinel正在对主服务器进行故障转移操作的时候,Sentinel向从服务器发送INFO命令的频率改为1秒1次。

  • 对于监视同一个主服务器和从服务器的多个Sentinel来说,它们以两秒一次的频率,通过向被监视服务器的__sentinel__:hello频道发送消息来向其他的Sentinel宣告自己的存在。

  • 每一个Sentinel也会从__sentinel__:hello频道中接收其他sentinel发来的消息,并根据这些消息为其他的Sentinel创建相应的实例结构,以及命令连接。

  • Sentinel只会与主服务器和从服务器创建命令连接和订连接,sentinel和sentinel之间只会创建命令连接。

  • Sentinel会以每秒一次的频率向实例(包括主服务器、从服务器、其他Sentinel)发送PING命令,并根据实例对PING命令的回复来判断实例是否在线,当一个实例在指定的时长中连续向Sentinel发送无效回复时,Sentinel将这个实例判断为主观下线。

  • 当Sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他Sentinel进行询问,看它是否同意这个主服务器已经进入主观下线状态。

  • 当Sentinel收集到足够多的主观下线投票后,它将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。

3.10.2.2 客户端实现

        实现一个哨兵客户端的基本步骤如下:

图片

3.10.3  集群

        Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据库的共享,并提供复制和故障转移功能。

3.10.3.1 节点

        一个集群由多个节点构成,使用CLUSTER MEET命令将独立的节点连接,构成一个包含多个节点的集群。节点通过握手将其他节点添加到自己所处的集群中。

3.10.3.2 槽指派

        Redis集群通过分片的方式保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每一个键属于这个16384个槽的其中一个,集群中每一个节点处理[0,17384]个槽。每个节点都会记录哪些槽位指派给了自己,而哪些槽位指配给了其他节点。

3.10.3.3 命令执行

        节点收到一条命令请求的时候,会首先检查这个命令请求要处理的键所在的槽位是否是自己负责。如果不是的话将向客户端返回一个MOVED错误,MOVED错误携带的信息将指引客户端转向至正在负责相关槽位的节点。

3.10.3.4 重新分片

        Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关的槽所属的键值对也会从源节点被移动到目标节点。对Redis集群的重新分片工作是由redis-trib负责执行的,重新分片的关键在于将属于某一个槽位的所有键值从一个节点转移到另外一个节点。

3.10.3.5 ASK错误

        在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。如果节点A正在迁移i至节点B,那么当节点A没能够在自己的数据库中找到命令指定的数据库键时,节点A将向客户端返回一个ASK错误,指引客户端到节点B继续查找指定的数据库键。MOVED错误表示槽的负责权已经从一个节点转移到了另外一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时的措施。

图片

3.10.3.6 故障转移

        当一个从节点发现自己正在复制的主节点进入已经下线的状态时,从节点将开始对下线主节点进行故障转移,步骤如下:

  • 下线的主节点的所有从节点里面,会有一个从节点被选中。

  • 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。

  • 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。

  • 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变为主节点,并且这个节点已经接管了已经下线的节点负责处理的槽指派。

  • 新的主节点开始接受和自己负责处理的槽的有关命令请求,故障转移完成。

3.10.3.7 消息

        集群中的节点通过发送和接受消息来进行通信,常见的消息包括MEET、PING、PONG、PUBLISH、FAIL五种。

3.11 codis

        Codis是一个分布式Redis解决方案,对于上层的应用来说,连接到Codis Proxy和连接原生的RedisServer没有明显的区别,有部分命令不支持。Codis底层会处理请求的转发,不停机的数据迁移等工作,所有后边的一切事情,对于前面的客户端来说是透明的,可以简单的认为后边连接的是一个内存无限大的Redis服务.

  • Codis-proxy实现redis协议,由于本身是无状态的,因此可以部署很多个节点

  • Codis-config是codis的管理工具,包括添加/删除redis节点添加/删除proxy节点,发起数据迁移等操作,自带httpserver,支持管理后台方式管理配置

  • Codis-server是codis维护的redis分支,基于2.8.21分支,加入了slot的支持和原子的数据迁移指令; codis-proxy和codis-config只能和这个版本的redis交互才能正常运行

  • Zookeeper用于codis集群元数据的存储,维护codis集群节点

4 应用场景  

4.1 分布式锁

        Redis中分布式锁的原理其实就是在Redis当中设置一个值,这个值只能由一个线程来存放,当其他线程(或者不同机器上的进程)也来存放时,发现这个值已经存在了,就说明此时已经有人在用这把锁了,这时候要么进行重试等待,要么进行放弃。设置一般使用 SETNX (set if not exists) 指令,如果该值没有,则进行设置,有了则不设置,这就是拿锁的关键了,当拿到锁的人执行处理完毕后,再调用DEL执行进行锁的释放。

4.2 Hyperloglog

        Hyperloglog提供不精确的去重逻辑,当然这个不精确也不是说非常不精确,标准误差为0.81%。HyperLogLog提供了两个指令pfadd和pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和set集合的sadd是一样的,来一个用户ID,就将用户ID塞进去就是。 pfcount 和 scard 用法是一样的,直接获取计数。

4.3 布隆过滤器

        布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率(假正例False positives,即Bloom Filter报告某一元素存在于某集合中,但是实际上该元素并不在集合中)和删除困难,但是没有识别错误的情形(即假反例False negatives,如果某个元素确实没有在该集合中,那么Bloom Filter 是不会报告该元素存在于集合中的,所以不会漏报)。

图片

4.4 限流

4.4.1  基于zset实现时间窗口限流

        使用一个zset来记录请求,全局key就一个,表示xxx接口。value就是当前这个接口的请求,每一个请求的score是当前的时间戳,作为时间窗口,每一次请求到达的时候就是求这个窗口内的value的数量。​​​​​​​

public Response limitRequest(){
    Long currentTime = new Date().getTime();
    if(redisTemplate.hasKey(LIMIT_KEY)) {
        // intervalTime 限流的时间(滑动窗口)
        Integer count = redisTemplate.opsForZSet().rangeByScore(LIMIT_KEY, currentTime -  intervalTime, currentTime).size();         
        if (count != null && count > 10) {
            return Response.ok("每分钟最多只能访问10次");
        }
    }
    redisTemplate.opsForZSet().add(LIMIT_KEY, getID(), currentTime);
    return Response.ok("成功访问");
}

private String getID() {
    return UUID.randomUUID().toString();
}

4.4.2 基于漏斗算法

        Redis 4.0 提供了一个限流 Redis 模块,它叫 redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就非常简单了。

5. 常见问题 

5.1  缓存雪崩

        数据库服务器崩溃:系统平稳运行过程中,忽然数据库连接量激增。应用服务器无法及时处理请求。大量 408,500 错误页面出现。客户反复刷新页面获取数据。数据库崩溃。应用服务器崩溃。重启应用服务器无效。Redis 服务器崩溃。Redis 集群崩溃。重启数据库后再次被瞬间流量放倒

        问题排查:在一个较短的时间内,缓存中较多的 key 集中过期。此周期内请求访问过期的数据,Redis 未命中,Redis 向数据库获取数据。数据库同时接收到大量的请求无法及时处理。Redis 大量请求被积压,开始出现超时现象。数据库流量激增,数据库崩溃。重启后仍然面对缓存中无数据可用。Redis 服务 器资源被严重占用,Redis 服务器崩溃。Redis 集群呈现崩塌,集群瓦解。应用服务器无法及时得到数据响应请求,来自客户端的请求数量越来越多,应用服务器崩溃。应用服务器、Redis、数据库全部重启,效果不理想。一些解决方案如下:

(1)更多的页面静态化处理

(2)构建多级缓存架构:Nginx 缓存 + Redis 缓存 + Ehcache 缓存

(3)检测Mysq|严重耗时业务进行优化:对数据库的瓶颈排查(例如超时查询、耗时较高事务等)

(4)灾难预警机制:监控 Redis 服务器性能指标(CPU占用、CPU使用率、内存容量、查询平均响应时间、线程数)

(5)限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务 低速运转后再逐步放开访问

(6)LRU 与 LFU 切换

(7)数据有效期策略调整:

根据业务数据有效期进行分类错峰,A 类 90 分钟、B 类 80 分钟、C 类 70 分钟

(8)过期时间使用固定时间 + 随机值的形式,稀释集中到期的 key 的数量

(9)超热数据使用永久 key

(10)定期维护(自动 + 人工):对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时

(11)加锁,慎用!

        缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。

5.2  缓存击穿

        数据库服务器崩溃:系统平稳运行过程中。数据库连接量瞬间激增。Redis 服务 器无大量 key 过期。Redis 内存平稳,无波动。Redis 服务器 CPU 正常,数据库崩溃。

  • 问题排查:Redis 中某个 key 过期,该 key 访问巨大。多个数据请求从服务器直接压到 Redis 后,均未命中。Redis 在短时间内发起了大量对数据库中同一数据的访问。   

  • 问题分析:单个 key 高热数据、key 过期。一些解决方案如下:

(1)预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长。注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势

(2)现场调整:监控访问量,对自然流量激增的数据延长过期时间或设为久性 key

(3)后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失

(4)二级缓存:设置不同的失效时间,保障不会被同时淘汰就行

(5)加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重!

        缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可。

5.3  缓存穿透

        数据库服务器崩溃:系统平稳运行过程中。应用服务器流量随时间增量较大。Redis 服务器命中率随时间逐步降低。Redis 内存平稳,内存无压力。Redis 服务器 CPU 占用激增。数据库服务器压力激增。数据库崩溃

  • 问题排查:Redis 中大面积出现未命中。出现非正常 URL 访问

  • 问题分析:获取的数据在数据库中也不存在,数据库查询未得到对应数据。Redis 获取到 null 数据未进行持久化,直接返回。下次此类数据到达重复上述过程。出现黑客攻击服务器。一些解决方案如下:

(1)缓存 null:对查询结果为 null 的数据进行缓存(长期使用,定期清理),设定短时限,例如30-60秒,最高 5 分钟

(2)白名单策略:

    • 提前预热各种分类数据 id 对应的 bitmaps,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时,放行,加载异常数据时直接拦截(效率偏低)

    • 使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略)、

(3)实施监控:实时监控 Redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比。

    • 非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象。

    • 活动时段波动:通常检测 10-50 倍,超过 50 倍纳入重点排查对象。

    • 根据倍数不同,启动不同的排查流程。然后使用名单进行防控(运营)

(4)key 加密:问题出现后,临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验。例如每天随机分配 60 个加密串,挑选 2-3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问

        缓存击穿访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除。

6 参考资料  

  1. 《Redis设计与实现》---黄健宏 著

  2. 《Redis深度历险:核心原理和应用实践》--- 钱文品 著

封面故事

2021年上半年在杭州,在钱塘江边拍摄过往船只。

相关推荐

  1. Redis 笔记

    2024-03-20 00:16:05       36 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-20 00:16:05       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-20 00:16:05       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-20 00:16:05       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-20 00:16:05       18 阅读

热门阅读

  1. 聚合函数和GROUP BY

    2024-03-20 00:16:05       19 阅读
  2. LeetCode第389场周赛个人题解

    2024-03-20 00:16:05       20 阅读
  3. ocp考试通过率如何?ocp考试内容有哪些?

    2024-03-20 00:16:05       32 阅读
  4. ocp考试是中文还是英文?ocp认证好考吗

    2024-03-20 00:16:05       26 阅读
  5. 【LeetCode周赛】第 389 场周赛

    2024-03-20 00:16:05       21 阅读
  6. LeetCode 76 最小覆盖子串

    2024-03-20 00:16:05       18 阅读
  7. Git 的基本概念和使用方式。

    2024-03-20 00:16:05       16 阅读
  8. VirtualBox 无法打开终端肿么办

    2024-03-20 00:16:05       17 阅读
  9. MySQL数据库中的锁机制(通俗易懂)

    2024-03-20 00:16:05       18 阅读