1. 基础

1.1 基本操作

  1. 查看服务连接状态

ping
—pong

  1. 查看当前库key的个数

dbsize

  1. 切换数据库

select db

  1. 删除当前库的数据

flushdb db

1.2 key的操作

  1. 查询key

keys

  • 语法:keys pattern
  • 作用:查找所有符合模式pattern的key。pattern可以是通配符
  • 通配符
      • 表示0个或多个字符,例如keys *查询所有的key
    • ?表示单个字符,例如wo?d,匹配word wood
    • [] 表示匹配其中某一个字符
      1. 删除key

del

  • 语法:del key[key,…]
  • 作用:删除存在的key,不存在的keyhul
  • 返回值:数字,删除key的数量
    1. 判断key是否存在

exists

  • 语法:exists key [key, …]
  • 作用:判断可以是否存在
  • 返回值:整数,存在返回1,其他返回0。使用多个key,返回存在key的数量

注:只返回存在的个数,但不返回哪一个存在/不存在

  1. 设置key的存活时间

expire

  • 语法:expire key [存活时间]
  • 作用:设置key的生存时间,超过时间,key自动删除。单位是秒
  • 返回值:设置成功返回数字1,其他情况0
    1. 查看key的存活时间

ttl

  • 语法:ttl key
  • 作用:已秒为单位,返回key的剩余生存时间
  • 返回值
    • -1:没有设置生存时间,key永不过期
    • -2:key不存在
    • 数字:key的剩余时间,秒为单位
      1. 清除过期时间

persist key

  1. 查看key的数据类型

type

  • 语法:type key
  • 作用:查看key所存储值的数据类型
  • 返回值:字符串表示的数据类型
    • none(key不存在)
    • string
    • list
    • set
    • zset
    • hash

      2. 常见数据类型

      2.1 string字符串类型

      常见命令

  1. set
  • 语法:SET key value [EX seconds] [PX milliseconds] [NX|XX] EX
    • seconds:设置过期时间,单位为秒
    • PX millionseconds:设置过期时间,单位为毫秒
    • NX:key值不存在的时候才创建
    • XX:key值已存在的时候才更新
  1. get
  • 作用:获取key中设置的字符串值
  • 语法:get key
  1. append
  • 语法:append key value
  • 作用:
    • 如果key存在,则将value追加到key原来旧值的末尾
    • 如果key不存在,则将key设置为value
  • 返回值:追加字符串之后字符串的总长度
  1. strlen
  • 语法:strlen key
  • 作用:返回key所存储的字符串值的长度
  • 返回值
    • 如果key存在,返回字符串值的长度
    • key不存在,返回0
  1. getrange
  • 语法:getrange key start end
  • 作用:获取key中字符串值从start开始到end结束的字符串,包括start和end,负数表示从字符串末尾开始,-1表示最后一个字符
  • 返回值:截取的字符串
  1. setrange
  • 语法:setrange key offset value
  • 作用:用value覆盖(替换)key的值offset开始,不存在的key做空白字符串
  • 返回值:修改后的字符串的长度
  1. mset
  • 语法:mset key value [ key value]
  • 作用:同时设置一个或多个key-value
  • 返回值:OK
  1. mget
  • 语法:mget key [key, …]
  • 作用:获取所有(一个或多个)给定key的值
  • 返回值:包含所有key的列表

    2.2 hash类型

    常见命令

  1. hset
  • 语法:hset hash表的key field value
  • 作用:将哈希表key中的field的值设置为value,如果key不存在,则新建hash表,执行赋值,如果有field,则覆盖值
  • 返回值
    • 如果field是表中新field,且设置值成功,返回1
    • 如果field已经存在,旧值覆盖新值,返回0
  1. hget
  • 语法:hget key field
  • 作用:获取哈希表key中给定域field的值
  • 返回值:field域的值,如果key不存在或者field不存在返回nil
  1. hmset
  • 语法:hset key filed value [field value, …]
  • 作用:同时将多个field-value设置到哈希表key中,此命令会覆盖已经存在的field,hash表key不存在,创建空的hash表,执行hmset
  • 返回值:设置成功返回OK,如果失败返回一个错误
  1. hmget
  • 语法:hmget key field [field, …]
  • 作用:获取哈希表key中一个或多个给定域的值
  • 返回值:返回和field顺序对应的值,如果field不存在,返回nil
  1. hgetall
  • 语法:hgetall key
  • 作用:获取哈希表key中所有的域和值
  • 返回值:已列表的形式返回hash中域和域的值,key不存在,返回空hash
  1. hdel
  • 语法:hdel key field [field, …]
  • 作用:删除哈希表key中的一个或多个指定域field,不存在field直接忽略
  • 返回值:成功删除的field的数量
  1. hkeys
  • 语法:hkeys key
  • 作用:查看哈希表key中所有的field域
  • 返回值:包含所有field的列表,key不存在返回空列表
  1. hvals
  • 语法:hvals key
  • 作用:返回哈希表中所有域的值
  • 返回值:包含哈希表所有域值的列表,key不存在返回空列表
  1. hexists
  • 语法:hexists key field
  • 作用:查看哈希表key中,给定域field是否存在
  • 返回值:如果field存在,返回1,其他返回0

    3. list类型

    常见命令

  1. lpush
  • 语法:lpush key value [value, …]
  • 作用:将一个活多个值插入到列表key的最左边(表头),从左边开始加入值,从左到右的顺序依次插入到表头
  • 返回值:数字,新列表的长度
  1. rpush
  • 语法:rpush key value [value, …]
  • 作用:将一个活多个值插入到列表key的最右边(表尾),从左边开始加入值,从左到右的顺序依次插入到表尾
  • 返回值:数字,新列表的长度
  1. lrange
  • 语法:lrange key start stop
  • 作用:获取列表key中指定区间的元素,0表示列表的第一个元素,以1表示列表的第二个元素,start,stop是列表的下标值,也可以负数的下标,-1表示列表的最后一个元素,-2表示倒数第二个元素,以此类推。start,stop超出列表的返回不会出现错误。
  • 返回值:指定区间的列表
  1. lindex
  • 语法:lindex key index
  • 作用:获取列表key中下标为指定index的元素,列表元素不删除,只是查询。
  • 返回值:指定下标的元素;index不在范围列表返回,返回nil
  1. llen
  • 语法:llen key
  • 作用:获取列表key的长度
  • 返回值:列表的长度;key不存在返回0
  1. lrem
  • 语法:lrem key count value
  • 作用:根据参数count的值,移除列表中与参数value相等的元素。
    • count>0,从列表的左侧向右开始移除
    • count<0,从列表尾部开始移除
    • count=0,移除表中所有与value相等的值
  • 返回值:数值,移除的元素个数
  1. lset
  • 语法:lset key index value
  • 作用:将列表key下标为index的元素的值设置为value
  • 返回值:设置成功返回OK,key不存在或者index超出返回返回错误信息
  1. linsert
  • 语法:linsert key BEFORE | AFTER pivot value
  • 作用:将value插入到列表key当中位于值pivot之前(之后)的位置,key不存在,pivot不在列表中,不执行任何操作。
  • 返回值:命令执行成功,返回新列表的长度。没有找pivot返回-1,key不存在返回0.
  1. rpop
  • 作用:移除列表的最后一个元素
  • 返回值:移除的元素
  1. rpoplpush source destination
  • 作用:移除列表的最后一个元素,并将该元素添加到另一个列表并返回
  1. lpop key
  • 作用:移除列表的第一个元素
  • 返回值:移除的元素

    4. set类型

    常见命令

  1. sadd
  • 语法:sadd key member [member…]
  • 作用:将一个活多个member加入到集合key当中,已经存在于集合的member元素将被忽略,不会再加入
  • 返回值:加入到集合的新元素个数。不包括被忽略的元素。
  1. smembers
  • 语法:smembers key
  • 作用:获取集合key中的所有成员元素,不存在的key视为空集合
  1. sismember
  • 语法:sismember key member
  • 作用:判断member元素是否是集合key的成员
  • 返回值:member是集合元素返回1,其他返回0
  1. scard
  • 语法:scard key
  • 作用:获取集合元素里面元素的个数
  • 返回值:数字,key的元素的个数。其他情况返回0.
  1. srem
  • 语法:srem key member [member, …]
  • 作用:删除集合key中的一个或多个member元素,不存在的元素被忽略
  • 返回值:数字,成功删除的元素个数,不包括忽略的元素
  1. srandmember
  • 语法:srandmember key [count]
  • 作用:只提供key,随机返回集合中一个元素,元素不删除,依然在集合中。
    • 提供count时,count为正数,返回包含count个数元素的集合,集合元素各不相同
    • count是负数,返回一个count绝对值的长度的集合,集合中元素可能会重复出现多次
  • 返回值:一个元素;多个元素的集合
  1. spop
  • 语法:spop key [count]
  • 作用:随机从集合中删除一个元素,count是删除的元素的个数
  • 返回值:被删除的元素,key不存在或空集合返回nil

    5. Zset有序集合

    命令

    zadd
    语法:zadd key [NX|XX] [CH] [INCR] score member [score member…]
    ZADD 参数(options) (>= Redis 3.0.2)
    ZADD 命令在key后面分数/成员(score/member)对前面支持一些参数,他们是:
    XX: 仅仅更新存在的成员,不添加新成员。
    NX: 不更新存在的成员。只添加新成员。
    CH: 修改返回值为发生变化的成员总数,原始是返回新添加成员的总数 (CH 是 changed 的意 思)。
    更改的元素是新添加的成员,已经存在的成员更新分数。 所以在命令中指定的成员有相同的分 数将不被计算在内。
    注:在通常情况下,ZADD返回值只计算新添加成员的数量。
    INCR: 当ZADD指定这个选项时,成员的操作就等同ZINCRBY命令,对成员的分数进行递增操作。

zincrby
语法:ZINCRBY key increment member
作用:对有序集合中指定成员的分数加上增量 increment
可以通过传递一个负数值 increment ,让分数减去相应的值,比如 ZINCRBY key -5 member ,就是让 member 的 score 值减去 5 。
当 key 不存在,或分数不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member 。
分数值可以是整数值或双精度浮点数。

zrange
语法:zrange key start stop [WITHSCORES]
作用:查询有序集合,指定区间的内的元素。集合成员按 score 值从小到大来排序。
start,stop 都是 从 0 开始。0 是第一个元素,1 是第二个元素,依次类推。
以 -1 表示最后一个成员,-2 表示倒数第二 个成员。WITHSCORES 选项让 score 和 value 一同返回。
返回值:自定区间的成员集合

zrevrange
语法:zrevrange key start stop [WITHSCORES]
作用:返回有序集 key 中,指定区间内的成员。
其中成员的位置按 score 值递减(从大到小)来排列。 其它同 zrange 命令。
返回值:自定区间的成员集合

zrem
语法:zrem key member [member…]
作用:删除有序集合 key 中的一个或多个成员,不存在的成员被忽略
返回值:被成功删除的成员数量,不包括被忽略的成员。

zcard
语法:zcard key
作用:获取有序集 key 的元素成员的个数
返回值:key 存在返回集合元素的个数, key 不存在,返回 0

zrangebyscore
语法:zrangebyscore key min max [WITHSCORES ] [LIMIT offset count]
作用:获取有序集 key 中,所有 score 值介于 min 和 max 之间(包括 min 和 max)的成员,有序 成员是按递增(从小到大)排序。
min ,max 是包括在内 , 使用符号 ( 表示不包括。
min , max 可以使用 -inf ,+inf 表示 最小和最大 limit 用来限制返回结果的数量和区间。
withscores 显示 score 和 value
返回值:指定区间的集合数据

zrevrangebyscore
语法:zrevrangebyscore key max min [WITHSCORES ] [LIMIT offset count]
作用:返回有序集 key 中, score 值介于 max 和 min 之间(默认包括等于 max 或 min )的所有的成 员。
有序集成员按 score 值递减(从大到小)的次序排列。其他同 zrangebyscore

zcount
语法:zcount key min max
作用:返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max ) 的成员的数量

  1. zcard key
    1. 获取有序集合的成员数
  2. zcount key min max
    1. 计算在有序集合指定区间分数的成员数。
  3. zincrby key increment member
    1. 有序集合中对指定成员的分数加上增量increment
  4. zrange key start end
    1. 查看指定范围的成员,withscores为输出结果带分数
  5. zrevrange key start end [withscores]
    1. 返回有序集中指定区间内的成员,通过索引,分数从高到低
  6. zrank key member
    1. 返回有序集合中指定成员的索引
  7. zrem key member1 [member2]
    1. 删除指定集合的一个或多个成员
  8. zscore key member
    1. 获取指定值的分数
  9. ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
    1. 通过分数返回有序集合指定区间内的成员
  10. zunionstore dest A B
    1. 将A与B的并集添到dest中
  11. zinterstore dest A B
    1. 将A与B的交集添加到dest中

使用场景

  1. 交集并集
  2. 热搜

    1. zadd
    2. zrevrange key idx1 idx2 [withscores] 返回指定索引区间的成员,分数从高到低

      3. Key命名规范

  3. 建议全部大写

  4. key不能太长也不能太短,键名越长越占资源,太短可读性太差
  5. key 单词与单词之间以 : 分开
  6. 按照“业务类型:id:字段”的方式进行命名

示例: 如下
数据: 给手机号 16800000001 发送了验证码 6666
在Redis中可以这样命名存储:
Key: PHONE:16800000001:CODE
Value: 6666

参照整理https://www.cnblogs.com/dyd520/category/1541704.html

4. 数据结构

redis底层数据结构图如下:
Redis - 图1

4.1 SDS

Redis使用C语言实现,但是他没有使用C语言的char *字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS)的数据结构表示字符串。

C语言字符数组(char *)数据结构
Redis - 图2
字符数组使用‘\0’表示字符串的结束。

4.1.1 char *字符数组缺陷

  1. 获取字符串长度的时间复杂度为O(N)
  2. 字符串的结尾是以‘\0’字符标识,而且字符必须符合某种编码(ASCII),只能保存文本数据,不能保存二进制数据
  3. 字符串操作函数不高效且不安全,比如可能会发生缓冲区溢出,从而造成程序运行终止。

    4.1.2 SDS结构

    下图就是Redis5.0的SDS的数据结构:
    Redis - 图3
  • len,SDS所保存的字符串长度。这样获取字符串长度时只需返回这个变量就行,时间复杂度O(1)。
  • alloc分配给字符数组空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算 出剩余的空间大小,然后用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区益处的问题。
  • flags,SDS类型,用来表示不同类型的SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
  • buf [],字节数组,用来保存实际数据。不需要用 “\0” 字符来标识字符串结尾了,而是直接将其作为二进制数据处理,可以用来保存图片等二进制数据。它即可以保存文本数据,也可以保存二进制数据,所以叫字节数组会更好点。

    4.2 链表

    链表数据结构:

    1. typedef struct listNode {
    2. //前置节点
    3. struct listNode *prev;
    4. //后置节点
    5. struct listNode *next;
    6. //节点的值
    7. void *value;
    8. } listNode;

    list数据结构:

    1. typedef struct list {
    2. //链表头节点
    3. listNode *head;
    4. //链表尾节点
    5. listNode *tail;
    6. //节点值复制函数
    7. void *(*dup)(void *ptr);
    8. //节点值释放函数
    9. void (*free)(void *ptr);
    10. //节点值比较函数
    11. int (*match)(void *ptr, void *key);
    12. //链表节点数量
    13. unsigned long len;
    14. } list;

    list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 dup、free、match 函数。
    举个例子,下面是由 list 结构和 3 个 listNode 结构组成的链表。
    Redis - 图4
    Redis的链表实现优点如下:

  • listNode 链表节点带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表

  • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)
  • list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1)
  • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;

注:链表每个节点间的内存都不是连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。因此,Redis 的 list 数据类型在数据量比较少的情况下,会采用「压缩列表」作为底层数据结构的实现,压缩列表就是由数组实现的,下面我们会细说压缩列表。

4.3 压缩列表

压缩列表是Redis数据类型为list和hash的底层实现之一。

  • 当一个列表键(list)只包含少量的列表项,并且每个列表项都是小整数值,或者长度比较短的字符串,那么 Redis 就会使用压缩列表作为列表键(list)的底层实现。
  • 当一个哈希键(hash)只包含少量键值对,并且每个键值对的键和值都是小整数值,或者长度比较短的字符串,那么 Redis 就会使用压缩列表作为哈希键(hash)的底层实现。

    4.3.1 压缩列表结构设计

    压缩列表是Redis为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。
    Redis - 图5
    压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数组

  • ztail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,特殊值 OxFF(十进制255)。

压缩列表节点(entry)的构成如下:
Redis - 图6

  • prevlen,记录了前一个节点的长度;
  • encoding,记录了当前节点实际数据的类型以及长度;
  • data,记录了当前节点的实际数据;

连锁更新

4.4 哈希表

哈希表是一种保存键值对(key-value)的数据结构。哈希表中的每一个 key 都是独一无二的,程序可以根据 key 查找到与之关联的 value,或者通过 key 来更新 value,又或者根据 key 来删除整个 key-value等等。
hash类型什么时候选用哈希表作为底层实现?

  • 当一个哈希键包含的 key-value 比较多,或者 key-value 中元素都是比较长多字符串时,Redis 就会使用哈希表作为哈希键的底层实现。

优点:它能以O(1)的复杂度快速查询数据。
缺点:在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高。
解决哈希冲突的方式,有很多种。Redis 采用了链式哈希,在不扩容哈希表的前提下,将具有相同哈希值的数据链接起来,以便这些数据在表中仍然可以被查询到。

4.4.1 哈希冲突

当有两个以上数量的 key 被分配到了哈希表数组的同一个哈希桶上时,此时称这些 key 发生了冲突。
链式哈希
Redis 采用了「链式哈希」的方法来解决哈希冲突。实现的方式就是每个哈希表节点都有一个 next 指针,多个哈希表节点可以用 next 指针构成一个单项链表,被分配到同一个哈希桶上的多个节点可以用这个单项链表连接起来,这样就解决了哈希冲突。
缺点:随着链表长度的增加,在查询这一位置上的数据的耗时就会增加,毕竟链表的查询的时间复杂度是 O(n)。
要想解决这一问题,就需要进行 rehash,就是对哈希表的大小进行扩展。

4.4.2 rehash

渐进式hash
rehash触发条件
介绍了 rehash 那么多,还没说什么时情况下会触发 rehash 操作呢?
rehash 的触发条件跟负载因子(load factor)有关系。
负载因子可以通过下面这个公式计算:
Redis - 图7
触发 rehash 操作的条件,主要有两个:

  • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。


4.5 整数集合

整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合不会出现重复元素。

4.5.1 实现

整数集合结构体定义:

  1. typedef struct intset {
  2. // 编码方式
  3. uint32_t encoding;
  4. // 集合包含的元素数量
  5. uint32_t length;
  6. // 保存元素的数组
  7. int8_t contents[];
  8. } intset;
  1. contents 数组是整数集合的底层实现: 整数集合的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。
  2. length 属性记录了整数集合包含的元素数量, 也即是 contents 数组的长度。
  3. 虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组并不保存任何 int8_t 类型的值 —— contents 数组的真正类型取决于 encoding 属性的值:

    1. 如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t 类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )。
    2. 如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t 类型的整数值 (最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。
    3. 如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t 类型的整数值 (最小值为 -9,223,372,036,854,775,808 ,最大值为 9,223,372,036,854,775,807 )。

      4.5.2 升级

      每当我们要将一个新元素添加到整数集合里面, 并且新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级(upgrade), 然后才能将新元素添加到整数集合里面。
      升级整数集合并添加新元素共分为三步进行:
  4. 根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。

  5. 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
  6. 将新元素添加到底层数组里面。

因为每次向整数集合添加新元素都可能会引起升级, 而每次升级都需要对底层数组中已有的所有元素进行类型转换, 所以向整数集合添加新元素的时间复杂度为 O(N) 。
升级之后新元素的摆放位置
因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大, 所以这个新元素的值要么就大于所有现有元素, 要么就小于所有现有元素:

  • 在新元素小于所有现有元素的情况下, 新元素会被放置在底层数组的最开头(索引 0 );
  • 在新元素大于所有现有元素的情况下, 新元素会被放置在底层数组的最末尾(索引 length-1 )。

好处

  1. 提升灵活性。因为 C 语言是静态类型语言, 为了避免类型错误, 我们通常不会将两种不同类型的值放在同一个数据结构里面。比如说, 我们一般只使用 int16_t 类型的数组来保存 int16_t 类型的值, 只使用 int32_t 类型的数组来保存 int32_t 类型的值, 诸如此类。但是, 因为整数集合可以通过自动升级底层数组来适应新元素, 所以我们可以随意地将 int16_t 、 int32_t 或者 int64_t 类型的整数添加到集合中, 而不必担心出现类型错误, 这种做法非常灵活。
  2. 节约内存。要让一个数组可以同时保存 int16_t 、 int32_t 、 int64_t 三种类型的值, 最简单的做法就是直接使用 int64_t 类型的数组作为整数集合的底层实现。 不过这样一来, 即使添加到整数集合里面的都是 int16_t 类型或者 int32_t 类型的值, 数组都需要使用 int64_t 类型的空间去保存它们, 从而出现浪费内存的情况。而整数集合现在的做法既可以让集合能同时保存三种不同类型的值, 又可以确保升级操作只会在有需要的时候进行, 这可以尽量节省内存。

    4.5.3 降级

    整数集合不支持降级操作, 一旦对数组进行了升级, 编码就会一直保持升级后的状态。

    4.6 跳表

    相关阅读 https://mp.weixin.qq.com/s/NOsXdrMrWwq4NTm180a6vw

5. 持久化

Redis 的数据 全部存储内存 中,如果突然宕机,数据就会全部丢失,因此必须有一套机制来保证Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。

5.1 快照

Redis 快照 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照。Redis进行快照持久化时,会fork一个子进程,快照持久化完全交给子进程处理,父进程则继续处理客户端请求。子进程 做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程 不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改

5.2 AOF

5.2.1 AOF原理

AOF(Append Only File - 仅追加文件) 它的工作方式非常简单:每次执行 修改内存 中数据集的写操作时,都会 记录 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例 顺序执行所有的指令,也就是 「重放」,来恢复 Redis 当前实例的内存数据结构的状态。

5.2.2 AOF重写

Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志 “瘦身”
Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其 原理 就是 开辟一个子进程 对内存进行 遍历 转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件 中。序列化完毕后再将操作期间发生的 增量 AOF 日志 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

fsync
Redis 同样也提供了另外两种策略,一个是 永不 fsync,来让操作系统来决定合适同步磁盘,很不安全,另一个是 来一个指令就 fsync 一次,非常慢。但是在生产环境基本不会使用,了解一下即可。

5.2.3 混合持久化

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小:
Redis - 图8
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,重启效率因此大幅得到提升。

6. 常见故障

6.1 缓存雪崩

产生原因:同一时间大面积缓存失效(key设置相同的过期时间),这一瞬间等于没有缓存,大量请求直接请求数据库,导致数据库被打死。
解决办法:批量往Redis存数据的时候,把每个key的失效时间都加个随机值,可以保证数据不会在同一时间大面积失效。或者设置热点数据永不过期,有更新操作更新缓存

6.2 缓存穿透

产生原因:缓存和数据库都没有的数据,而用户不断发起请求,导致数据库压力过大,严重会击垮数据库。
解决办法:

  1. 接口层增加校验,比如用户鉴权校验、参数校验。
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、未知错误、稍后重试这样的值具体取啥问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)
  3. Redis还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

    6.3 缓存击穿

    产生原因:一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,
    解决办法:设置热点数据永远不过期。或者加上互斥锁就能搞定了

    7. 过期策略

    7.1 过期策略

    如果将一个过期的键删除,我们一般都会有三种策略:
  • 定时删除 :为每个键设置一个定时器,一旦过期时间到了,则将键删除。这种策略对内存很友好,但是对 CPU 不友好,因为每个定时器都会占用一定的 CPU 资源。
  • 惰性删除 :不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。
  • 定期扫描 :系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。

在 Redis 当中,其选择的是策略 2 和策略 3 的综合使用。不过 Redis 的定期扫描只会扫描设置了过期时间的键,因为设置了过期时间的键 Redis 会单独存储,所以不会出现扫描所有键的情况

7.2 淘汰策略

假如 Redis 当中所有的键都没有过期,而且此时内存满了,那么客户端继续执行 set 等命令时 Redis 会怎么处理呢?Redis 当中提供了不同的淘汰策略来处理这种场景。
Redis 提供了一个参数 maxmemory 来配置 Redis 最大使用内存:

  1. maxmemory <bytes>

或者也可以通过命令 config set maxmemory 1GB 来动态修改。
Redis 中提供了 8 种淘汰策略,可以通过参数 maxmemory-policy 进行配置:

淘汰策略 说明
volatile-lru 根据 LRU 算法删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错
allkeys-lru 根据 LRU 算法删除所有的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错
volatile-lfu 根据 LFU 算法删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错
allkeys-lfu 根据 LFU 算法删除所有的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错
volatile-random 随机删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错
allkeys-random 随机删除所有键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错
volatile-ttl 根据键值对象的 ttl 属性, 删除最近将要过期数据。如果没有,则直接报错
noeviction 默认策略,不作任何处理,直接报错

PS:淘汰策略也可以直接使用命令 config set maxmemory-policy <策略> 来进行动态配置。

LRU:全称为:Least Recently Used。即:最近最长时间未被使用。这个主要针对的是使用时间。
LFU:全称为:Least Frequently Used。即:最近最少频率使用,这个主要针对的是使用频率。这个属性也是记录在redisObject 中的 lru 属性内。
参考:https://mp.weixin.qq.com/s/-caMTrOXQu-o0O44e6I9dQ

8. 缓存一致性

推荐“先更新数据库,后删除缓存”,并配合消息队列或订阅变更日志的方式来做。
https://mp.weixin.qq.com/s/D4Ik6lTA_ySBOyD3waNj1w

9. 分布式锁

10. 常见问题

10.1 Redis是单线程的,为什么又使用多线程?

  1. 为什么Redis最开始被设计成单线程?

Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
使用多线程的目的就是通过并发的方式提升I/O的利用率和CPU的利用率。

  • 因为Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈,所以,通过多线程技术提升Redis的CPU利用率是没有必要的。
  • 使用多线程需要处理并发问题,增加了实现的复杂性,而且,多线程模型中,多个线程的相互切换也会带来一定的性能开销。

Redis并没有在网络请求模块和数据操作模块中使用多线程模型,主要是基于以下四个原因:

  • Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU
  • 使用单线程模型,可维护性更高,开发,调试和维护的成本更低
  • 单线程模型,避免了线程间切换带来的性能开销
  • 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
  1. Redis为什么快?

Redis能够有这么高的性能,不仅仅和采用多路复用技术和单线程有关,此外还有以下几个原因:

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
  • 数据结构简单,对数据操作也简单,如哈希表、跳表都有很高的性能。
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
  • 使用多路I/O复用模型
  1. Redis为什么还使用多线程?

Redis - 图9
从上图我们可以看到,在多路复用的IO模型中,在处理网络请求时,调用 select (其他函数同理)的过程是阻塞的,也就是说这个过程会阻塞线程,如果并发量很高,此处可能会成为瓶颈。
如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。

仅供学习交流使用 详细请见:https://mp.weixin.qq.com/s/qptE172slg_6Tl1yuzdbfw