Redis 学习笔记

核心数据结构

  1. typedef struct redisDb {
        dict *dict;                  
        dict *expires;          
        dict *blocking_keys;          
        dict *ready_keys;           
        dict *watched_keys;         
        int id;                      
        long long avg_ttl;           
        unsigned long expires_cursor;  
        list *defrag_later;          
    } redisDb
    

    逻辑上的库的数据结构

  2. typedef struct dict {
        dictType *type;
        void *privdata;
        dictht ht[2];
        long rehashidx; 
        unsigned long iterators;  
        } dict;
        ```
    **字典,存储有两个hashtable,第二个用来为第一个做扩容.有点类似Java 中的 Map,key通过hash()去命中**
    
    
  3. typedef struct dictht {
        dictEntry **table;
        unsigned long size;
        unsigned long sizemask;
        unsigned long used;
    } dictht;
    

    hashtable 数组,当存储数据数量==size,会触发扩容

  4. typedef struct dictEntry {
        void *key;
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        struct dictEntry *next; // 链表
    } dictEntry;
    

    key-value,key统一使用sds(simple dynamic string)类型存储,类似Map中的Node节点

  5. typedef struct redisObject {
        unsigned type:4; // 类型 4bit
        unsigned encoding:4; // 编码方式 4bit
        unsigned lru:LRU_BITS;  // 24bit
        int refcount; //引用计数,回收内存 4 byte
        void *ptr; 8 byte
    } robj;
    

    value存储对象的封装

string

使用场景

  1. 对象缓存: ① set user:id value(user对象的json) ② mset user:id value(id) user:name value(name) … 相对第一种方式,第二种方式修改会更为方便,也会占用更多的空间。
  2. 分布式锁:Redission 中有实现的相关红锁,使用了 nx 实现。但基于redis的自身结构,在集群环境下无法保证锁的绝对安全。
  3. 计数器:incr 操作。如果在分布式架构中,需求特别频繁,那么应该使用 incr key num 这种操作,一次多获取一些数量的ID,存在获取服务的内存中,慢慢使用。以免redis压力过大

数据结构

  1. sds : simple dynamic string
  2. 参数: int length、int free、unsigned char flags、char buf[] 四种
  3. 概述: 在redis3.2之后,String 类型的数据根据长度的不同,会使用不同的数据结构: sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64.这里的数字代表存储长度的位数,例如sdshdr5 最多能存储 2^5 个字节,下标 0(2^5 - 1).
    redis底层使用c语言,redis字符的数据结构没有直接使用c的char,而是自定义了一个新的数据结构sds,是因为redis作为中间件,会与不同的语言进行交互,char类型存储字符时,会使用"/0"作为结束符,可能产生错误.当字符进行修改时,如果不需要改变数据类型,会覆盖修改.
  4. sdshdr5:
    struct __attribute__ ((__packed__)) sdshdr5 {
        unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
        char buf[];
    }
    

在这里插入图片描述
这种数据结构,使用一个字节存储标志与长度,没有存储剩余可用字节长度.

  1. sdshdr8:

     struct __attribute__ ((__packed__)) sdshdr8 {
         uint8_t len; /* used */
         uint8_t alloc; /* excluding the header and null terminator */
         unsigned char flags; /* 3 lsb of type, 5 unused bits */
         char buf[];
     }
    

在这里插入图片描述
这种数据类型使用一个字节存储标识,只有前3位使用,后面5位空置.

  1. 缓存行大小一般为64byte,当一个字符长度不超过44个字节(redisObject:16 + sds:1+1+1+(44+1))时,可以放入缓存行中(char[]的末尾不被填充/0,占一个字节).此时它的类型为嵌入式字符串embstr(embedded str)
  2. redis规定,sds最大位512M
  3. bitmap: 底层也是sds,但进行的是位操作,通过偏移量操作每一位的值(0/1).可以用来记录大数据量的、只有两种变化的信息.如:登陆、签到 等等信息.但要注意,如果偏移量差距过大,数据量又比较小,就不太适用(浪费大量内存空间,可以考虑位目标添加一个新的自增ID.例如当统计用户在线情况,用户量巨大,存在分片.可以对每一个分片单独添加一个自增的ID,不需要全局唯一.统计时,分别对每一个分片单独记录.这里没有考虑用户活跃情况,是否存在大量非活跃用户.具体情况,具体分析).
    bitmap内置了位运算函数,可以进行位计算.(如连续7天登陆情况,进行7天对位与操作)
  4. 剩余三种与sdshdr8类似,不再赘述

list

使用场景

  1. list使用更类似一个双端队列,它默认拥有正负索引(左:0、1、2; 右:-1、-2、-3).
  2. list常用的场景,可以作为栈、队列、阻塞队列. 通过 LPUSH、LPOP、RPUSH、RPOP、BLPOP、BRPOP 命令完成. 阻塞获取数据超时事件不宜过长.
  3. 带有时间线的消息信息流. 例如微博动态、订阅消息等等. 以订阅文章为例,当订阅当目标发出了一篇新当文章时,可以根据订阅数量,选择向目标推送消息或通过特定标记告知目标自己拉去消息(拉去消息不能使用POP命令),通过list轻量级的完成需求.还可以通过LRANGE命令,查看最新的消息.

数据结构

redis中的list使用了quickList和zipList来实现的.

zipList:
在这里插入图片描述

  1. zlbytes: 当前list中存储的数据大小
  2. zltail : 尾节点位置
  3. zllen : 存储了多少个元素
  4. zlend : 标识list结束位置,大小恒等于255

在这里插入图片描述

  1. prerawlen : 前一个字节的数据长度,如果前一个数据占用字节数小于254 (8位最大表示255,255已经用于标识list结束,所以这里取到254) ,满足条件,用一个字节记录前一个数据字节大小,否则加4个字节用来记录地址(用来进行上一个数据的寻址.这里没有直接使用4个字节记录地址,因为list存储的数据是未知的,这可能会导致胖地址的问题.)

  2. len : 用来记录长度,当长度较小时,还可以记录数据类型,这个参数比较复杂(根据数据长度,可以知道这个字段的长度)

  3. data: 具体数据

  4. 一个zipList的数据大小不可以存储太大.这是因为过大的数据量会导致数据迁移(各种pop操作)速度降低,影响整体效率.这个值可以根据配置文件修改: list-max-ziplist-size,这个值redis已经定义好了(-1 ~ -5),一般使用-1 -> 4kb 或者 -2 -> 8kb.当数据量过大时,会导致ziplist分裂(根据zlbytes判断).

quickList:
在这里插入图片描述

  1. quickList 符合常规意义上的双端链表,它的ZL指向的是一个zipList.
  2. 为节省空间,quickList可以对不常用对节点进行压缩(首尾位置对节点使用频率最高),如何压缩可以通过配置文件配置: list-compress-depth. 这里配置对数字代表从首、尾开始,多少个节点开始压缩.

hash

使用场景

例如购物车这种场景,数据相对来说重要性不高,需要经常修改。相比于string,数据更加方便管理,占用空间也更小,但只能整体设置超时。而且集群模式下,可能导致数据分片不均匀、要小心 bigKey 。

数据结构

  1. 当数据量较小时,redis直接使用了zipList存储数据.当数据量与数据大小超过某个阀值,会转为dict. hash-max-ziplist-entries : 512 zipList存储当最大数据量512个. hash-max-ziplist-value : 64 zipList单个元素最大值64byte.
  2. 由于采用量zipList这种数据结构,当创建一个小hash数据时,数据是有序的.
  3. 阀值的设计,是根据查询效率与迁移效率综合考虑的

set

使用场景

  1. set主要用于集合的各种运算:包含、并集、交集、差级 等等
  2. 用于抽奖 : 包括获取成员(SMEMBER)、随机获取特定数量的成员(SPOP/SMOVE) 等操作
  3. 点赞、关注、共同等朋友、可能认识等人 等等功能, 所有类似等集合操作, 都可以使用set轻量级的完成

数据结构

  1. 当存储数据是整形,且数据量不超过阀值, set-max-intset-entries : 512. 满足条件时,使用intset 作为存储类型.
    在这里插入图片描述
    typedef struct intset {
        uint32_t  encoding;
        uint32_t  length;
        int8_t    contents[];
    } intset; 
    
    #define INTSET_ENC_INT16 (sizeof(int16_t))
    #define INTSET_ENC_INT32 (sizeof(int32_t))
    #define INTSET_ENC_INT64 (sizeof(int64_t))
    
  2. 当不满足上述条件,set使用value值位null(dict中hashtable的value)的dict来存储数据

zset

相较于set, zset是一个有序的集合,它的每一个数据, 拥有一个分值, 数据会按照分值排序. 与set不同, zset并不是用来进行多个集合运算的, 更多的是用来顺序/倒序访问, 范围查询, 分值查询等操作.

使用场景

  1. 新闻点击量, zincreby(自动怎见指定数据分值)、zrange(范围获取目标)
  2. 7日点击量、一个月内点击量. zunionstore 对不同日期的数据,进行分数合并

数据结构

zset 是由 dict + zskipList 组成的,是一组有序的数据.

  1. dict用来查询数据与分值的关系,skipList用来通过通过分值,获取数据(精确查找、范围查找)
  2. zskipList的查询效率为 logn
    在这里插入图片描述
  3. 如上图所示,外层对象存储了头节点指针、尾节点指针、存储的数据量、层高
  4. 层高的生成使用了冥次定律(是一个随机数,层高越高,几率越小).

持久化

RDB(快照)

通过配置, 设置数据持久化条件. 将内存数据存储到dump.rdb的二进制文件中.
策略配置: save 60 1000 // 60 秒内有至少有 1000 个键被改动. 可以配置多个策略.

  1. save命令: 阻塞线程, 执行持久化操作
  2. bgsave命令: 非阻塞, 执行命令后, 会 fork 一个子线程, 将内存中对数据进行持久化. 同时, 在持久化过程中, 新命令产生到数据也会同步到持久化文件中(写时复制).
  3. rdb模式会持久化所有的数据, 这也导致它不可能频繁的触发. 所以这种模式下, 可能发生大规模的数据丢失.

AOF(append-only file)

通过配置, 将命令写入到 appendonly.aof 中.

同步策略:

  1. appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
  2. appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
  3. appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

重写策略:

  1. auto‐aof‐rewrite‐min‐size 64mb // aof文件最小要达到64M开始重写
  2. auto‐aof‐rewrite‐percentage 100 // aof 文件大小增加一倍, 再次重写, 这里就是126M

aof 写如的是RESP命令, 当用户对一条数据进行多次操作的时候, 会导致前面执行的命令被覆盖, 对于持久化文件来说, 被覆盖的命令是无效命令. 重写会将这些无效的命令去除. 重写时, 会创建一个新的文件写入命令, 当重写结束后, 覆盖appendonly.aof.

格式:
aof 模式持久化数据, 时将执行的命令(不包括查询),以RESP命令写入本地磁盘, 在需要时, 重新执行这些命令. aof 虽然可以解决数据丢失问题, 但每次刷盘会极大的降低吞吐量, 在实际应用场景下并不适用. 实际上, 一般都会适用 eversec 这种模式(可能导致丢失一秒钟的数据), 它也是 aof 的默认模式.

混合持久化

RDB 持久化会带来数据大量丢失(或频繁重写全部数据)的问题, AOF 持久化的磁盘数据较大,且重写时耗时较大. 在 redis 4.0 后, 新增了混合模式.

配置策略:

  1. 需要开启 aof .
  2. 放开注解 aof-use-rdb-preamble yes

混合模式其实就是一种 aof 模式, 不过在重写时,会将旧数据以rdb的模式写入磁盘, 而新的命令, 将会以aof的模式追加到文件中. 当满足配置要求, 再次触发重写时, 又会进行这个流程.

混合模式较好的解决了rdb与aof各自存在的问题. 但仍然不能保证数据但绝对安全(aof 不配置为 always), 但 redis 作为一个缓存中间件, 并不需要保证数据的绝对安全, 内存中缺失的数据, 可以通过数据库或其他方式作为补充查找.

持久化策略:

  1. 基于 redis 的持久化模式(无论哪一种), 都无法将数据回复到一个特定的状态下, 所以可以根据需求, 每个一个特定的时间将数据拷贝出来一份.
  2. 对于拷贝出来的数据, 要考虑到定期删除和磁盘损毁的问题.

集群

主从模式

redis 的主从模式非常简单, 只需要配置主节点地址, 并添加只读配置, 就完成了.

  1. replicaof 127.0.0.1 6379
  2. replica‐read‐only yes

数据全量复制流程:

  1. 从节点向主节点发起同步请求 psync 命令.
  2. 主节点收到命令, 后台启用 bgsave 命令, 将最新的数据rdb 发给从节点. 通知清空主节点的缓存(如果收到多个请求, 只会执行一次 bgsave).
  3. 从节点清空旧数据. 加载新数据
  4. 备份期间, 新的数据会存储在buffer中, 在rdb同步完成后, 将缓存的数据同步给从节点
  5. 从节点执行收到的缓存中的命令.

数据断点续传流程:

  1. 从节点与主节点断开链接. 从节点与主节点重连, 向主节点发送 psync 命令, 命令中携带 offset
  2. 主节点中会缓存一段时间内的操作数据. 收到 psync 命令后, 检测 offset.
  3. offset 在主节点的缓存(repl-backlog-buffer)中, 主节点将从节点缺失的数据, 从缓存中读取后, 发送给从节点.
  4. offset 不在主节点的缓存中, 主节点将会发送全量数据(正常同步流程).
  5. repl-backlog-buffer 默认大小是1M. redis 中可以配置这个参数, # repl-backlog-size 1mb

主从风暴:

主从架构, 数据是与主节点发送到从节点的. 如果从节点过多, 如果出现从节点同时请求 rdb 数据, 可能导致瞬间压力过大. 这里可以使用阶梯式的架构, 让一些从节点以另一个从节点作为主节点.

LUA脚本
redis支持使用lua脚本, lua脚本变相实现了 redis 的事务操作(不建议使用redis自带的事务功能). 不要在lua脚本中使用耗时操作.

哨兵模式

哨兵模式是在主从模式的基础上, 再启动一个、或多个哨兵服务. 由哨兵服务负责监听 redis 的连接状况. 当主节点宕机时, 哨兵会根据配置, 在从节点中选择一个新的主节点.

配置文件: sentinel.conf
核心配置: sentinel monitor mymaster 127.0.0.1 6379 2 # ip、port 是主节点的 ip、port . 最后的整数是最小失效值. 它代表当多少个哨兵认为主节点失效, 才确认主节点已经失效. 一般配置为: sentinelsize / 2 + 1
当哨兵模式启动后, 主从当配置会写在 sentinel.conf 当最下端.

cluster

redis 的集群模式, 简单说, 是将 redis 分成 16384 个槽, 每一段槽位分配给一个小的“哨兵”小集群(类似). 这些小的集群是可以水平扩容的, 官方推荐, 最优不超过1000 个.
在这里插入图片描述
配置策略:

  1. cluster‐enabled yes 开启集群
  2. cluster‐config‐file nodes‐6379.conf 集群信息文件. 每个节点你使用自己的文件.
  3. cluster‐node‐timeout 10000 集群节点超时时间(ms)

集群部署:

  1. 配置完成后, 分别启动所有的节点.
  2. 使用命令 (redis 5.0 以后) redis‐cli ‐a zhuge ‐‐cluster create ‐‐cluster‐replicas 1 2 ip1:port1 ip2:port2 … # ‐‐cluster‐replicas 1 2 代表每1个主节点拥有2个从节点. 后面要添加所有的 redis 服务地址
  3. 命令执行后, 服务会自动组建子集群, 并为每个子集群平均分配槽位(16384个槽位).

槽位:

  1. redis 的 key-value 结构在 c 中是通过定义的 dict (与java中的map概念相似) 类型实现的(详细实现可见核心数据类型部分). 数据最终要落在 dictht(dict hash table) 上, 也就是对 key 值做hash, 通过运算决定落在数组的哪一位上(桶的概念).
  2. 槽位与key的桶计算很类似,的集群部署时, redis 自动分了16384个槽位, 分别赋给不同个的小集群. 当进行数据操作时, 通过计算 key 的 hash 值(crc16 算法)与槽位分配结果, 选择调用哪个小集群的 master.
  3. 重定位, 当用户计算得到子集群master的地址后, 如果尝试调用, 发现槽位错误. redis会发起通知,告诉客户端目标槽位的映射地址. 这时, 客户端需要重新拉取槽位信息, 槽位与服务映射出现了变化,缓存信息需要更新(redis 的各种客户端都实现了重定位的功能).

选举:
redis 使用 goosip 协议进行选举, 当slave发现它的master无法联通(FAIL 状态)后(无法联通的时长需要超过配置中的超时时间), 它会向所有其他节点(所有小集群)发送信息, 请求成为新的master. 收到消息的节点中, 只有 master 节点会响应且只会响应一次(其他 master 得知了目标master 进入FAIL状态, 否则会拒绝投票). 当slave节点收到半数以上 master 节点的响应, 就会成为新的 master , 并通知所有其他节点. 这也是为什么至少需要3个 master 节点集群才能正常运行. 基于它的选举方式, 为加速选举速度, 可以配置投票信息随机延时发送, 防止 slave 均分票数, 长时间无法选举成功.

脑裂问题:

  1. 成因: redis 会出现脑裂问题. 当原始的 master 与它的slave出现网络中断, 它的子节点被选举成为新的master后. 如果网络中断没有恢复, 那么旧的master仍然认为自己是master, 也就是它仍然可以提供写功能. 当网络恢复后, 它会获知自己成为了slave. slave 需要与master 同步数据, 那么以 redis 的同步方式, 所有在双 master 时间内写入的数据, 都会被丢弃.
  2. 解决方案: redis 支持配置 min‐slaves‐to‐write 1 . 1 代表最少有一个 slave 节点写入成功, master 节点才会返回. 这里可以根据 slave 节点数量采用半数成功机制. 同时, 这项配置一定程度上会影响 redis 的工作新能.

注意:

  1. 集群模式, 最少需要3个节点(选举需要最少3个节点).
  2. 集群模式的从节点不提供任何服务, 它们只是数据的备份.
  3. cluster-require-full-coverage yes/no 当插槽部分不可用时, 集群是否仍然可用.
  4. 批量操作时, 如果它们的槽位不在一个子服务上, 会操作失败.
  5. 使用 {} 设置槽位计算的 key 值部分. 例如: {user}:admin:12 这个key, 槽位计算时, 只计算 user 的 hash 值.
  6. 哨兵模式与集群模式下, 节点数量最好为奇数, 且至少需要有 3 个. 因为 redis 的选举成功需要半数以上投票. 以 3 个投票人为例, 也就是至少有 2 票才能选举成功. 也就是 3 个投票人中, 有一个宕机了, 集群仍然正常运行. 而 4 个投票人, 最少需要 3 票 , 仍然最多只能允许一个人宕机. 所以相较与 3 个投票人, 增加一个并不能增加集群的可用性, 不如使用5个投票人.
  7. 由于 redis 作为一个基于内存的非关系型数据库, 一般作为缓存曾使用, 对它的性能要求很高, 不会使用 aof 的 appendfsync always 模式, 也就是说, 当出现服务宕机等问题时, 数据一定会产生丢失问题, 不能保证数据的绝对安全.

*小技巧:

  1. redis单节点内存不宜使用过大, 一般不超过10g.
  2. 在redis客户端, 可以使用 help + tab键的命令组合, 查找目标帮助(help @string 会显示string类型所有操作命令). 每次按tab键, 都会随意一个帮助类型.
  3. redis 只有指令操作是单线程的, 如数据持久化、异步删除、集群数据同步等都是其他线程操作
  4. redis 速度快等最根本原因, 是因为它基于内存. 所以持久化策略选择强一致, 性能就会大幅度下降.其次, redis 使用了epoll实现IO等多路复用.
  5. redis 不要使用全量遍历(keys命令), 使用渐进式遍历(scan 命令).
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值