• 0

  • 524

万字长文——一文解答你对 Redis 的所有疑惑

1个月前

万字长文 —— 一文解答你对 Redis 的所有疑惑

目录

[TOC]

序言

Redis 作为目前市面上应用最广泛的 key-value 数据库之一,有着它独一无二的魅力。它可用于缓存,事件发布或订阅,高速队列等场景。该数据库使用 ANSI C 语言编写,支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。同时丰富的 API 支持广泛的客户端语言。希望兄弟们看完这篇文章可以对 Redis 有一个更深的认识。

PS:标题纯粹在吹牛逼,勿怪勿怪🌚🌚🌚

前置知识

  1. 在操作系统中,系统从磁盘中读取一次数据是按照 4k 大小进行读取的。
  2. 在各类数据库软件中也是按照 4k 大小进行数据分片的。
  3. 使用 Redis 的原因是为了解决在高并发下磁盘 I/O 速度不够的问题。

Redis 简介

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis 基本类型及使用

  • String
    • 字符类型
    • 数值类型
    • bitmaps
  • Hashes
  • Lists
  • Sets
  • Sorted Sets

String 类型

type key: 获取 key 指定的 value 值类型

object encoding k1: 获取 key 指定的 value 的编码类型

二进制安全是指 redis 中存的 value 是字节数组,不用担心客户端的编码问题。

为了解决每次命令出现的判断 value 数据类型的操作,redis 在key上标注了 encoding 属性来解决

  • 字符类型

    • 设置值 SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
      • nx:只 key 不存在时设置;xx:只 key 存在时设置
    • 同时设置多个值 MSET key value [key value ...]
    • 获取值 GET key
    • 同时获取多个值 MGET key [key ...]
    • 字符串追加 APPEND key value
    • 获取字符串某一部分 GETRANGE key start end(索引从0开始,前后包含)
      • 正反向索引
    • 覆盖字符串的某一部分 SETRANGE key offset value
    • 获取字符串长度 STRLEN key
  • 数值类型

    • 指定 key 加一 INCR key
    • 指定 key 加某一个数 INCRBY key increment
    • 指定 key 减一 DECR key
    • 指定 key 减某一个数 DECRBY key decrement
    • 指定 key 加一个小数 INCRBYFLOAT key incrementc
  • bitmap

    • 设置指定 key 值的指定偏移量的二进制值 SETBIT key offset value

    • 查找指定 key 值的指定字节区间内第一次出现的指定二进制值 BITPOS key bit [start] [end]

    • 统计指定 key 值得指定字节区间二进制 1 的个数 BITCOUNT key [start end]

    • 为指定 key 做指定的二进制操作,同时把值赋给 destkey BITOP operation destkey key [key ...]

      bitmap 应用:

      • 有一个用户系统,统计用户登录天数,且天数随机

        1. 设置 366 个二进制位
        2. setbit 用户名 1 1(代表用户名代表的用户第二天登录)
        3. bitcount 用户名 0 -1(随机天数,一个字节代表8天)
      • 商城 6.18 做活动:凡用户登录就送一份礼物。需要知道备货多少礼物(商城有 2亿 用户)

        用户有僵尸用户、冷热用户、忠诚用户

        所以具体做的是活跃用户统计:随机天数内的活跃用户

        如统计 1-3号的活跃用户,分别算出1-3号每天的用户数,连续登陆需去重

        1. 对每天设置 key 值 bitset 20200908 1[用户id对应1] 1
        2. 将对应的二进制位对应到用户id bitset 20200910 9[用户id对应9] 1
        3. 将指定区间内的所有 key 值做或运算 bitop resultKey 20200908 20200910
        4. bitcount resultKey 0 -1 获取结果 key 中所有二进制 1 的个数

List 类型

使用同向命令 list可以看作为栈的使用,使用反向命令 list可以看作为队列的使用

有序(指元素的输入顺序)

  • 从左到右向指定 key 中加入元素 LPUSH key value [value …]
    • 最终结果为加入顺序的倒序
  • 从右到左向指定 key 中加入元素 RPUSH key value [value ...]
    • 最终结果和加入顺序相同
  • 移除并且返回 key 对应的 list 的第一个元素 LPOP key
  • 移除并返回存于 key 的 list 的最后一个元素 RPOP key
  • 返回存储在 key 的列表里指定范围内的元素 LRANGE key start stop(包含 stop 对应的那个元素)
  • 返回存储在 key 列表里的元素的索引 index 对应的值 LINDEX key index
  • 设置对应 key index 位置的 list 元素的值为 value LSET key index value
  • 从存于 key 的列表里移除前 count 次出现的值为 value 的元素 LREM key count value
    • count > 0: 从头往尾移除值为 value 的元素。
    • count < 0: 从尾往头移除值为 value 的元素。
    • count = 0: 移除所有值为 value 的元素。
    • 把 value 插入存于 key 的列表中在基准值 pivot 的前面或后面 LINSERT key BEFORE|AFTER pivot value
      • 其中 pivot 为列表中的元素,如果 有两个基准值则从左到右第一个基准值上操作
  • 修剪(trim)一个已存在的 list,这样 list 就会只包含指定范围的指定元素 LTRIM key start stop
    • 例如: LTRIM foobar 0 2 将会对存储在 foobar 的列表进行修剪,只保留列表里的前3个元素。
  • BLPOP 是阻塞式列表的弹出原语,即为 LPOP 的阻塞版 BLPOP key [key ...] timeout
    • timeout 参数表示的是一个指定阻塞的最大秒数的整型值。当 timeout 为 0 是表示阻塞时间无限制
  • BRPOP 是一个阻塞的列表弹出原语,即为 RPOP 的阻塞版 BLPOP 和 BRPOP 会返回 key 和 弹出的元素值

Hashes 类型

  • 设置 key 指定的哈希集中指定字段的值 HSET key field value
  • 设置 key 指定的哈希集中指定字段的值 HMSET key field value [field value ...]
    • 该命令将重写所有在哈希集中存在的字段
  • 返回 key 指定的哈希集中该字段所关联的值 HGET key field
  • 返回 key 指定的哈希集中指定字段的值 HMGET key field [field ...]
  • 返回 key 指定的哈希集中所有字段的名字 HKEYS key
  • 返回 key 指定的哈希集中所有字段的值 HVALS key
  • 返回 key 指定的哈希集中所有的字段和值 HGETALL key
  • 为指定 key 的 hash 的 field 字段值执行 float 类型的加 increment HINCRBYFLOAT key field increment

Sets 类型

无序但去重

  • 添加一个或多个指定的 member 元素到集合的 key中 SADD key member [member ...]

  • 返回成员 member 是否是存储的集合 key的成员 SISMEMBER key member

    • 如果 member 元素是集合key的成员,则返回1
    • 如果 member 元素不是key的成员,或者集合key不存在,则返回0
  • 在key集合中移除指定的元素 SREM key member [member ...]

  • 返回集合存储的 key 的基数 (集合元素的数量) SCARD key

  • 返回 key 集合所有的元素 SMEMBERS key

  • 返回指定所有的集合的成员的交集 SINTER key [key ...]

    • 例如:

      key1 = {a,b,c,d}
      key2 = {c}
      key3 = {a,c,e}
      SINTER key1 key2 key3 = {c}
      复制代码
  • 这个命令与 SINTER 命令类似, 但是它并不是直接返回结果集,而是将结果保存在 destination集合中 SINTERSTORE destination key [key ...]

    • 如果 destination 集合存在, 则会被重写
  • 返回给定的多个集合的并集中的所有成员 SUNION key [key ...]

  • 该命令作用类似 SUNION 命令,不同的是它并不返回结果集,而是将结果存储在destination集合中 SUNIONSTORE destination key [key ...]

    • 如果 destination 已经存在,则将其覆盖
  • 返回一个集合与给定集合的差集的元素 SDIFF key [key ...]

    • 举例:

      key1 = {a,b,c,d}
      key2 = {c}
      key3 = {a,c,e}
      SDIFF key1 key2 key3 = {b,d}  
      复制代码
  • 该命令类似 SDIFF , 不同之处在于该命令不返回结果集,而是将结果存放在 destination 集合中 SDIFFSTORE destination key [key ...]

    • 如果 destination 已经存在, 则将其覆盖重写
  • 仅提供 key 参数,那么随机返回 key 集合中的一个元素 SRANDMEMBER key [count]

    • 如果 count 是正整数,则取出一个去重的结果集(不会超过已有集合的总数)
    • 如果 count 是负整数,则取出一个带重复的结果集,(一定满足 count 的绝对值个数)
    • 如果 count 是 0,不返回
  • 从存储在 key 的集合中移除并返回一个或多个随机元素 SPOP key [count]

    • 如果 count 大于集合内部的元素数量,此命令将会返回整个集合,不会有额外的元素

Sorted Sets类型

排序是如何实现的?

SkipList

  • 原理及实现

其实跳表就是在普通单向链表的基础上增加了一些索引,而且这些索引是分层的,从而可以快速地查的到数据。如下是一个典型的跳表:

 

跳跃表

 

  • 查找

查找示意图如下:

 

跳跃表-查找

 

比如我们要查找 key 为19的结点,那么我们不需要逐个遍历,而是按照如下步骤:

  • 从 header 出发,从高到低的 level 进行查找,先索引到9这个结点,发现9 < 19,继续查找(然后在 level == 2 这层),查找到21这个节点,由于21 > 19, 所以结点不往前走,而是level由2降低到1
  • 然后索引到17这个节点,由于17 < 19, 所以继续往后,索引到21这个结点,发现21>19, 所以level由1降低到0
  • 在结点17上,level==0 索引到19,查找完毕。
  • 如果在 level==0 这层没有查找到,那么说明不存在 key 为19的节点,查找失败
  • 将所有指定成员添加到键为 key 有序集合(sorted set)里面。 添加时可以指定多个分数/成员(score/member)对 ZADD key [NX|XX] [CH] [INCR] score member [score member ...]

    • 如果指定添加的成员已经是有序集合里面的成员,则会更新改成员的分数(scrore)并更新到正确的排序位置
    • XX: 仅仅更新存在的成员,不添加新成员。
    • NX: 不更新存在的成员。只添加新成员。
    • CH: 修改返回值为发生变化的成员总数,原始是返回新添加成员的总数 (CH 是 changed 的意思)。更改的元素是新添加的成员,已经存在的成员更新分数。 所以在命令中指定的成员有相同的分数将不被计算在内。注:在通常情况下,ZADD返回值只计算新添加成员的数量。
    • INCR: 当ZADD指定这个选项时,成员的操作就等同 ZINCRBY 命令,对成员的分数进行递增操作。
  • 返回存储在有序集合 key 中的指定范围的元素。 返回的元素可以认为是按得分从最低到最高排列。 如果得分相同,将按字典排序 ZRANGE key start stop [WITHSCORES]

    • WITHSCORES 将元素的分数与元素一起返回
  • 返回 key 的有序集合中的分数在 min 和 max 之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

    • LIMIT 指定返回结果的数量及区间

    • min 和 max 可以是 -inf 和 +inf,这样一来,可以在不知道有序集的最低和最高 score 值的情况下,使用 ZRANGEBYSCORE 这类命令

    • 默认情况下,区间的取值使用闭区间(小于等于或大于等于),可以通过给参数前增加(符号来使用可选的开区间(小于或大于)

      • 举个例子:

        返回所有符合条件1 < score <= 5的成员;

        ZRANGEBYSCORE zset (1 5
        复制代码

        返回所有符合条件5 < score < 10 的成员

        ZRANGEBYSCORE zset (5 (10
        复制代码
  • 返回有序集 key 中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列 ZREVRANGE key start stop [WITHSCORES]

    • 具有相同 score 值的成员按字典序的反序排列
  • 返回有序集合中指定分数区间内的成员,分数由高到低排序 ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

  • 返回有序集 key 中,成员 member 的 score 值 ZSCORE key member

    • 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回nil
  • 返回有序集 key 中成员 member 的排名 ZRANK key member

    • 其中有序集成员按 score 值递增(从小到大)
  • 返回有序集 key 中成员 member 的排名 ZREVRANK key member

    • 其中有序集成员按 score 值从大到小排列
  • 为有序集 key 的成员 member 的 score 值加上增量 increment ZINCRBY key increment member

    • 如果 key 中不存在 member,就在 key 中添加一个 member,score 是 increment。如果 key 不存在,就创建一个只含有指定 member 成员的有序集合
  • 计算给定的 numkeys 个有序集合的并集,并且把结果放到 destination 中 ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [SUM|MIN|MAX]

    • WEIGHTS 选项:为每个给定的有序集指定一个乘法因子,意思就是,每个给定有序集的所有成员的 score 值在传递给聚合函数之前都要先乘以该因子。如果 WEIGHTS 没有给定,默认就是1
    • AGGREGATE 选项:指定并集的结果集的聚合方式,默认使用的参数 SUM,可以将所有集合中某个成员的 score 值之和作为结果集中该成员的 score 值。如果使用参数 MIN 或者 MAX,结果集就是所有集合中元素最小或最大的元素。

Redis 进阶使用

管道(Pipelining)

  • 简介:

Redis 管道(Pipelining)

一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。

  • 使用:

echo -e "set k2 99\nincr k2\nget k2" |nc localhost 6379
# nc 命令可以开启一个 socket 链接,后面接 ip 地址及端口号
# echo 命令将输入字符串以管道的方式传递给 redis

# 在 redis 2.6 以后,redis-cli 开始支持一种新的被称之为 pipe mode 的新模式用于大量数据插入工作。
cat data.txt | redis-cli --pipe

# 这将产生类似如下的输出:
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000
复制代码
  • pipe mode的工作原理是什么?

难点是保证redis-cli在pipe mode模式下执行和netcat一样快的同时,如何能理解服务器发送的最后一个回复。

这是通过以下方式获得:

  • redis-cli –pipe 试着尽可能快的发送数据到服务器。
  • 读取数据的同时,解析它。
  • 一旦没有更多的数据输入,它就会发送一个特殊的ECHO命令,后面跟着 20 个随机的字符。我们相信可以通过匹配回复相同的 20 个字符是同一个命令的行为。
  • 一旦这个特殊命令发出,收到的答复就开始匹配这 20 个字符,当匹配时,就可以成功退出了。

同时,在分析回复的时候,我们会采用计数器的方法计数,以便在最后能够告诉我们大量插入数据的数据量。

重要说明:使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如 10k 的命令,读回复,然后再发送另一个 10k 的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。

Redis 的发布/订阅 (Pub/Sub)

订阅,取消订阅和发布实现了发布/订阅消息范式(引自wikipedia),发送者(发布者)不是计划发送消息给特定的接收者(订阅者)。而是发布的消息分到不同的频道,不需要知道什么样的订阅者订阅。订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道什么样的发布者发布的。这种发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑。

  • 相关命令:

    • 将信息 message 发送到指定的频道 channel PUBLISH channel message
    • 订阅给指定频道的信息 SUBSCRIBE channel [channel ...]
      • 一旦客户端进入订阅状态,客户端就只可接受订阅相关的命令 SUBSCRIBEPSUBSCRIBEUNSUBSCRIBEPUNSUBSCRIBE除了这些命令,其他命令一律失效

注意事项

  • Redis 中发布者发送的消息会通知所有订阅了这个频道订阅者
  • 没有订阅者的情况,发布者发布的消息会丢失

Redis 事务

  • 相关命令:

    • 标记一个事务块的开始。 随后的指令将在执行 EXEC 时作为一个原子执行 MULTI
    • 执行事务中所有在排队等待的指令并将链接状态恢复到正常 当使用 WATCH 时,只有当被监视的键没有被修改,且允许检查设定机制时,EXEC会被执行 EXEC
    • 标记所有指定的key 被监视起来,在事务中有条件的执行(乐观锁) WATCH key [key ...]
    • 刷新一个事务中已被监视的所有 key UNWATCH
      • 如果执行 EXEC 或者 DISCARD, 则不需要手动执行 UNWATCH
    • 刷新一个事务中所有在排队等待的指令,并且将连接状态恢复到正常DISCARD
      • 如果已使用 WATCH,DISCARD 将释放所有被 WATCH 的 key

Redis Modules(Redis 模块)

  • RedisBloom (Redis 布隆过滤器)

    • 相关链接:zhuanlan.zhihu.com/p/43263751

    • 简介:

      本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 某样东西一定不存在或者可能存在。相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

    • 实现原理

      • 准备一个 bitmap
      • 当一个元素被访问时,会将这个元素经过 x 个映射函数映射到 bitmap 中 x 个二进制位上。
      • 当下次该元素被访问时,即可通过相同的 映射函数检查在 bitmap 中对应的二进制位是否为 1 来判断该元素是否存在
    • 注意事项

      • 当元素被访问时对应的二进制位都为 1 不能证明该元素一定存在,只能说是可能存在,具体原因是因为不同的元素可能在映射函数映射后映射至相同的二进制位上。并且随着元素量增多的时,被置为 1 的二进制位会越来越多。

      • 如何选择哈希函数个数和布隆过滤器长度?

        k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

        如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:

        img

Redis LRU算法

  • 什么是 LRU 算法

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”

  • 相关配置

    • maxmemory 在 redis.conf 中 maxmemory 配置指令用于配置Redis存储数据时指定限制的内存大小。

      设置maxmemory为0代表没有内存限制。对于64位的系统这是个默认值,对于32位的系统默认内存限制为3GB。

      当指定的内存限制大小达到时,需要选择不同的行为,也就是策略。 Redis可以仅仅对命令返回错误,这将使得内存被使用得更多,或者回收一些旧的数据来使得添加数据时可以避免内存限制。

    • maxmemory-policy 配置 redis 的过期策略。

  • 回收策略

    过期集合:即设置了 key 过期时间的键值对

    LFU全称是最不经常使用算法(Least Frequently Used)

    LRU算法全称是最近最少使用算法(Least Recently Use)

    • noeviction: 返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
    • allkeys-lru: 尝试回收最近最少使用的键(LRU),使得新添加的数据有空间存放。
    • volatile-lru: 尝试回收最近最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
    • allkeys-lfu: 尝试回收最少使用的键(LFU),使得新添加的数据有空间存放。
    • volatile-lfu:尝试回收最少使用的键(LFU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
    • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
    • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
    • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
  • Redis 如何淘汰过期的 keys

    Redis keys 过期有两种方式:被动和主动方式。

    • 当一些客户端尝试访问它时,key 会被发现并主动的过期。

      当然,这样是不够的,因为有些过期的 keys,永远不会访问他们。 无论如何,这些 keys 应该过期,所以定时随机测试设置 keys 的过期时间。所有这些过期的 keys 将会从密钥空间删除。

      具体就是 Redis 每秒 10 次做的事情:

      测试随机的 20 个 keys 进行相关过期检测。 删除所有已经过期的 keys。 如果有多于 25% 的 keys 过期,重复步奏1. 这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的 keys 的百分百低于 25%,这意味着,在任何给定的时刻,最多会清除 1/4 的过期 keys。

Redis 持久化

Redis 提供了不同级别的持久化方式:

  • RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储。
  • AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾。 Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
  • 你也可以同时开启两种持久化方式,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF文件保存的数据集要比 RDB 文件保存的数据集要完整。

RDB 方式

  • RDB的优点:

    • RDB 是一个非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。
    • RDB 是一个紧凑的单一文件,很方便传送到另一个远端数据中心,非常适用于灾难恢复。
    • RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 redis 的性能。
    • 与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些.

    fork

    linux 中的 fork 指令可以很快速的创建子进程,在调用 fork 命令时,linux 系统会将父进程中的环境变量使用指针的方式传递给子进程。在 linux 系统中不同进程的数据是相互隔离的,在父进程使用 export命令将环境变量继承给子进程,但在子进程中的修改变量值仍不会破坏父进程中的变量值。所以通过 fork 命令来创建子进程的速度是非常快的。

    copy on write

    在使用 fork 命令时会采用 copy on write(写时复制)的方式来修改子进程的数据。 即在子进程不修改变量值时,变量的指针是指向与父进程在内存中相同的指针,在子进程修改时,会先在内存中创建一个新值再将指针指向新值。

  • RDB的缺点:

    • 如果你希望在 redis 意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么 RDB 不适合你。虽然你可以配置不同的 save 时间点(例如每隔5分钟并且对数据集有100个写的操作),但是 Redis 要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在 Redis 意外宕机,你可能会丢失几分钟的数据。
    • RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且 CPU 性能不是很好的情况下,这种情况会持续1秒,AOF 也需要 fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度。
  • RDB配置方式:

    • save <seconds> <changes> 指定时间间隔后,如果数据变化达到指定次数,则导出生成快照文件。

      如果指定 save "",则相当于清除前面指定的所有 save 设置

      • save 900 1 900 秒(15 分钟)内至少有1个key被修改
      • save 300 10 300 秒(5分钟)内至少有10个key被修改
      • save 60 10000 60 秒(1分钟)内至少有10000个key被修改
    • rdbcompression yes RDB 快照中字符串值是否压缩

    • rdbchecksum yes 如果开启,校验和会被放在文件尾部。这将使快照数据更可靠,但会在快照生成与加载时降低大约 10% 的性能,追求高性能时可关闭该功能

    • dbfilename dump.rdb 指定保存快照文件的名称

    • dir /var/lib/redis/6379指定保存快照文件的目录,AOF(Append Only File) 文件也会生成到该目录

  • RDB相关命令:

    • BGSAVE后台保存DB。会立即返回 OK 状态码。 Redis forks,父进程继续提供服务以供客户端调用,子进程将 DB l数据保存到磁盘然后退出。如果操作成功,可以通过客户端命令LASTSAVE来检查操作结果。

AOF 方式

  • AOF的优点:

    • 使用 AOF 会让你的 Redis 更加耐久:你可以使用不同的 fsync 策略:无 fsync,每秒 fsync,每次写的时候 fsync。使用默认的每秒 fsync 策略,Redis 的性能依然很(fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。
    • AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题。
    • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
    • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
  • AOF缺点:

    • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
      • 在 Redis 4.0 之前使用 AOF 方式会使用重写的方式来进行删除抵消命令,合并重复命令、在 Redis 4.0 以后会使用 RDB 和 AOF 合并的方式来缩小 AOF 文件的体积。
    • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
  • AOF配置方式:

    • appendonly no可以同时启用AOF和RDB持久性而不会出现问题。 如果在启动时检查到启用了 AOF,Redis 将优先加载 AOF。

    • appendfilename "appendonly.aof"AOF持久化文件名称默认为 appendonly.aof

    • appendfsync everysec fsync() 调用会告诉操作系统将缓冲区的数据同步到磁盘,可取三种值:always、everysec 和 no。

      在操作系统中向磁盘中写入数据时,会先向缓冲区中写入数据,缓冲区满时操作系统内核会将缓冲区中的数据写入磁盘中以此来加速写入磁盘的数据。对应到代码中的命令就是 flush。

      所以以下三种方式对应的就是,always: 每次写入立刻 flush、everysec: 每秒 flush 一次、no: 操作系统自动 flush

      • always:实时会极大消弱Redis的性能,因为这种模式下每次 write 后都会调用 fsync。
      • no:write 后不会有 fsync 调用,由操作系统自动调度刷磁盘,性能是最好的。
      • everysec:每秒调用一次fsync(默认)
    • auto-aof-rewrite-percentage 100当 AOF 增长超过指定比例时,重写 AOF 文件,设置为 0 表示不自动重写 AOF 文件,重写是为了使 aof 体积保持最小,而确保保存最完整的数据。

    • auto-aof-rewrite-min-size 64mb触发 aof rewrite的最小文件大小,这里表示,文件大小最小 64mb 才会触发重写机制

    • no-appendfsync-on-rewrite no在 AOF 文件 rewrite 期间,是否对 aof 新记录的 append 暂缓使用文件同步策略,主要考虑磁盘 IO 开支和请求阻塞时间。默认为 no,表示"不暂缓”,新的 aof 记录仍然会被立即同步。

    • aof-use-rdb-preamble yes开启混合持久化。

  • AOF相关命令:

    • BGREWRITEAOF异步执行一个 AOF(AppendOnly File)文件重写操作。重写会创建一个当前AOF文件的体积优化版本。
      • 如果一个子 Redis 是通过磁盘快照创建的,AOF 重写将会在 RDB 终止后才开始保存。这种情况下BGREWRITEAOF仍然会返回 OK 状态码。从 Redis 2.6 起你可以通过 INFO 命令查看 AOF 重写执行情况。
      • 如果在执行的 AOF 重写返回一个错误,AOF 重写将会在稍后一点的时间重新调用。
  • AOF文件详解:

    *2 // 代表 Redis 命令由几段组成
    $6 // 代表 Redis 命令由几个字符组成
    SELECT
    $1
    0
    *3
    $3
    set
    $2
    k1
    $5
    hello
    复制代码
    • *[num] 代表 Redis 命令由几段组成,即接下来的 num*2 行为命令的组成
    • $[num]代表 Redis 命令由几个字符组成,即接下来的一行的 num 字符为命令
    • 其余为命令本身

Redis 集群

Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。

Redis 集群并不支持处理多个 keys 的命令,因为这需要在不同的节点间移动数据,从而达不到像 Redis 所需要的那样的性能,在高负载的情况下可能会导致不可预料的错误。

Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。

Redis 集群的优势:

  • 自动分割数据到不同的节点上。
  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

Redis 的 CAP 保证

  • 在 redis 中并没有保证数据的强一致性,既在 redis 中满足了 CAP 理论中的 AP。这意味着在实际过程中 redis 集群在特定情况下可能会丢失写操作。
    • 其主要原因是在 redis 的集群方式中采用数据异步复制的方式。写操作过程:
      • 客户端向主节点 A 写入一条命令
      • 主节点 A 向客户端回复命令状态
      • 主节点将写操作复制给他的从节点 A1、A2、A3
  • 在监控某一个 redis 服务中,为确保高可用及避免网络分区的问题导致出现脑裂问题。redis 采用了 ( n / 2 ) + 1 (n/2) + 1 的方式来确定集群的势力范围。

Redis 的主从复制

*Redis使用默认的异步复制,其特点是低延迟和高性能*

Redis 主从复制的三个主要机制

  • 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave ,包括客户端的写入、key 的过期或被逐出等等。
  • 当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。
  • 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave 。

Redis 主从复制要点

  • Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量
  • 一个 master 可以拥有多个 slave
  • slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一个 master 之外, slave 之间也可以像层叠状的结构(cascading-like structure)连接到其他 slave 。自 Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流。
  • Redis 复制在 master 侧是非阻塞的。这意味着 master 在一个或多个 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求。
  • 复制在 slave 侧大部分也是非阻塞的。当 slave 进行初次同步时,它可以使用旧数据集处理查询请求,前提是你在 redis.conf 中配置了让 Redis 这样做的话。否则,你可以配置如果复制流断开, Redis slave 会返回一个 error 给客户端。但是,在初次同步之后,旧数据集必须被删除,同时加载新的数据集。 slave 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 slave 。
  • 复制既可以被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全。
  • 可以使用复制来避免 master 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 master 节点的 Redis.conf 以避免对磁盘进行持久化,然后连接一个 slave ,其配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重新启动的 master 程序将从一个空数据集开始:如果一个 slave 试图与它同步,那么这个 slave 也会被清空

全量复制的工作流程:

  • master 节点 fork 一个子进程用来生成一个 RDB 文件。同时缓冲新到来的写入命令。
  • 当 RDB 保存完成时,master 将文件发送给 slave,slave 保存在磁盘上再加载文件至内存。
  • master 发送所有缓冲指令到 slave,同步新到来的命令。

Redis 主从复制的配置

  • replicaof <masterip> <masterport>主从复制。 使 Redis 实例成为另一台 Redis 服务器的副本。
  • masterauth <master-password>如果主服务器受密码保护,则可以在启动复制同步过程之前告知副本服务器进行身份验证,否则主服务器将拒绝副本服务器请求。
  • replica-serve-stale-data yes当从库与主库连接中断,或者主从同步正在进行时,如果有客户端向从库读取数据:
    • yes: 从库答复现有数据,可能是旧数据(初始从未修改的值则为空值)
    • no: 从库报错“正在从主库同步”
  • replica-read-only yes从库只允许读取
  • repl-diskless-sync no无磁盘形式:主库创建子进程,子进程把 RDB 文件直接写入从库的 SOCKET 连接。
  • repl-backlog-size 1mb设置复制积压大小(backlog)。 积压是一个缓冲区,当副本断开连接一段时间后会累积副本数据,因此当副本想要再次重新连接时,通常不需要完全重新同步,只需要部分重新同步就足够了
    • 复制 backlog 越大,副本可以断开连接的时间越长。
  • 如果可用连接的副本数少于 N 个,并且延迟小于或等于 M 秒,则 master 节点停止接受写入。以秒为单位的延迟(必须<=指定值)是根据从副本接收的最后一次 ping 计算的,通常每秒发送一次。
    • 例如,要求至少3个在线且滞后时间<= 10秒的副本:
      • min-replicas-to-write 3
      • min-replicas-max-lag 10

配置 Redis 的 Sentinel 系统

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
  • 配置 Sentinel

    # sentinel.conf
    
    sentinel monitor mymaster 127.0.0.1 6379 2 # 配置 sentinel 监听一个名为 mymaster 的 redis 主服务器,而将这个主服务器判断失效至少需要两个 sentinel 同意(势力范围为 2)
    sentinel down-after-milliseconds mymaster 60000 # 指定了 Sentinel 认为服务器已经断线所需的毫秒数
    sentinel parallel-syncs mymaster 1 # 在故障转移期间,多少个副本节点进行数据同步
    复制代码

搭建 Redis 集群

  • 配置项:

    • cluster-enabled yes开启集群功能,此redis实例作为集群的一个节点
    • cluster-config-file nodes.conf集群配置文件此配置文件不能人工编辑,它是集群节点自动维护的文件,主要用于记录集群中有哪些节点、他们的状态以及一些持久化参数等,方便在重启时恢复这些状态。通常是在收到请求之后这个文件就会被更新
    • cluster-node-timeout 5000 集群中的节点能够失联的最大时间,超过这个时间,该节点就会被认为故障。如果主节点超过这个时间还是不可达,则用它的从节点将启动故障迁移,升级成主节点
    • appendonly yes
  • Redis 集群的数据分片

    Redis 集群方案没有使用一次性 hash,而是引入了哈希槽的概念。在 Redis 中有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定当前 key 具体要放置在哪个槽位。集群的每一个节点都复制一部分的槽位。举个例子:假设当前集群有 3 个节点,那么

    • 节点 A 包含 0 到 5500 号哈希槽
    • 节点 B 包含5501 到 11000 号哈希槽
    • 节点 C 包含11001 到 16384号哈希槽

    这种结构的优点是很容易添加或删除节点,比如要添加一个节点 D 时,只需从节点 A、B、C 中的部分槽分到节点 D 上。如果要移除节点 A,只需将节点 A 中的槽移到 B 和 C节点上。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

  • Redis 集群的实现方案

Redis 常见问题

缓存击穿

  • 原因:

    • 当 Redis 的某一个 key 值是热点数据,不停地扛住大并发的请求,这是当此 key 过期时所带来的持续的大并发请求就会透过这个 key 值打到 DB造成 DB 的宕机。
  • 解决方案:

    • 设置热点 key 不过期。或者加上互斥锁。

缓存雪崩

  • 原因:

    • 同一时间缓存大面积失效,就像没有缓存一样,所有的请求直接打到 DB 上来
  • 解决方案:

    • 批量往 redis 存数据的时候,把每个 key 的失效时间加上个随机数,这样的话就能保证数据不会在同一个时间大面积失效。

缓存穿透

  • 原因:

    • 用户不断请求的数据在 DB 和 Redis 中都没有,则这类请求会不停地通过 Redis 打到 DB 上造成宕机。因为每次都绕开了缓存直接查询 DB。
  • 解决方案:

    • 在接口层增加校验,不合法的参数直接返回。
    • 若出现在缓存中查不到在 DB 中也查不到的数据则将此条数据对应的 key 值的 value 设置为 null 或是其他特殊值写入缓存。同时将过期时间设置的短一些以免影响正常情况。
    • 为避免大量同一 ip 的请求可以在网关中设置 ip 黑名单。
    • 使用 Bloom 过滤器防止。原理就是利用高效的数据结构和算法快速判断出你这个 key 是否在 DB 中存在,不存在 return 就好了,存在就查了 DB 刷新 K-V 再 return。

Redis 补充知识

RESP 协议

  • 协议特点

    • 简单的实现
    • 快速地被计算机解析
    • 简单得可以能被人工解析
  • 应用场景

    • 开发定制化的客户端:RESP 设计成简单的文本协议, 一大原因就是为了降低各种语言开发客户端的复杂度
    • 理解RESP方便:我们分析 AOF 文件,了解 redis 的内部设计
    • 在没有 redis-cli 的情况下, 方便开发调试 redis 命令
  • 数据类型

    • simple string 简单的字符串

      • 第一个字节是+中间是字符串的的内容,最后以 CRLF (\r\n) 结尾。例如 "+OK\r\n"
    • error 表示一个错误异常

      • 第一个字节是 -后面接着的是错误的信息,最后以 CRLF(\r\n) 结尾。例如"-ERR unknown command 'foobar'\r\n"
    • integer 表示一个整数

      • 第一个字节是:后面是整数,最后以 CRLF(\r\n) 结尾。例如":1000\r\n"
    • bulk string 表示一个长字符串但必须小于 512M

      • 第一个字节是$紧接着是一个整数,表示字符串的字节数,字节数后面接一个 CRLF。CRLF 后面是字符串的内容,最后以 CRLF(\r\n) 结尾。例如
        • "$0\r\n” $ 后面的 0 表示这是一个空字符串
        • "$-1\r\n" $后面的-1表示这是一个 null 字符串,Null Bulk String 要求客户端返回空对象,而不能简单地返回个空字符串
        • "$6\r\nABCDEF\r\n” ABCDEF 是 6 个字节,所以 $ 后面是6
    • arrays 表示一个数组

      • 第一个字节是 *紧接着后面是一个数字,表示这个数组的长度,数字后面是一个 CRLF。需要注意的是这个 CRLF 之后才是数组的真正内容,而且数组内容可以是任意类型,包括 arrays 和 bulk string,每个元素也要以 CRLF 结尾. 最后以 CRLF(\r\n) 结尾

        • "*0\r\n”后面的 0 表示表示空的数组

        • "*-1\r\n" *后面的 -1 表示表示是 null 数组

        • "*5\r\n*5 表示这是一个拥有 5 个元素的数组

          +bar\r\n 第1个元素是简单的字符串

          -unknown command\r\n第 2 个元素是个异常

          :3\r\n第 3 个元素是个整数

          $3\r\n第 4 个元素是长度为 3 个字节的长字符串 foo

          foo\r\n第4个元素的内容

          *2\r\n第 5 个元素又是个数组

          :1\r\n第 5 个元素数组的第 1 元素

          :2\r\n第 5 个元素数组的第 2 元素

          \r\n 第 5 个元素数组的结束

          \r\n数组结束

使用 Redis 实现分布式锁

分布式锁在很多场景中是非常有用的原语, 不同的进程必须以独占资源的方式实现资源共享就是一个典型的例子。

有很多分布式锁的库和描述怎么实现分布式锁管理器(DLM) 的博客,但是每个库的实现方式都不太一样,很多库的实现方式为了简单降低了可靠性,而有的使用了稍微复杂的设计。

Redis 提出了一种算法叫RedisLock,认为这种实现比普通的但实例更安全

Redis Lua 脚本

  • 相关命令

    • EVAL script numkeys key [key ...] arg [arg ...]

      • EVAL的第一个参数是一段 Lua 5.1 脚本程序。 这段Lua脚本不需要(也不应该)定义函数。它运行在 Redis 服务器中。

      • EVAL的第二个参数是参数的个数。

      • EVAL的第三个参数表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。

      • EVAL的第四个参数那些不是键名参数的附加参数 arg [arg …] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

        • 举例:

          > eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
          1) "key1"
          2) "key2"
          3) "first"
          4) "second"
          复制代码

参考引用

总结

总的来说 Redis 涉及到的知识非常复杂,不仅有基本的数据类型,磁盘I/O 和 内存的地址都有涉猎到,正是因为这些细节的把控,才让 Redis 成为目前市面上最快的 key-value 数据库,也可以发现在 Redis 的设计理念里所有的行为都是为了让 Redis 更快。

如果我在哪一部分写的不够到位或者写错了,还请在座的各位大佬提出意见。

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

程序员

524

相关文章推荐

未登录头像

暂无评论