REDIS
核心数据结构
-
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
逻辑上的库的数据结构
-
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; unsigned long iterators; } dict; ``` **字典,存储有两个hashtable,第二个用来为第一个做扩容.有点类似Java 中的 Map,key通过hash()去命中**
-
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
hashtable 数组,当存储数据数量==size,会触发扩容
-
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节点
-
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
使用场景
- 对象缓存: ① set user:id value(user对象的json) ② mset user:id value(id) user:name value(name) … 相对第一种方式,第二种方式修改会更为方便,也会占用更多的空间。
- 分布式锁:Redission 中有实现的相关红锁,使用了 nx 实现。但基于redis的自身结构,在集群环境下无法保证锁的绝对安全。
- 计数器:incr 操作。如果在分布式架构中,需求特别频繁,那么应该使用 incr key num 这种操作,一次多获取一些数量的ID,存在获取服务的内存中,慢慢使用。以免redis压力过大
数据结构
- sds : simple dynamic string
- 参数: int length、int free、unsigned char flags、char buf[] 四种
- 概述: 在redis3.2之后,String 类型的数据根据长度的不同,会使用不同的数据结构: sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64.这里的数字代表存储长度的位数,例如sdshdr5 最多能存储 2^5 个字节,下标 0–(2^5 - 1).
redis底层使用c语言,redis字符的数据结构没有直接使用c的char,而是自定义了一个新的数据结构sds,是因为redis作为中间件,会与不同的语言进行交互,char类型存储字符时,会使用"/0"作为结束符,可能产生错误.当字符进行修改时,如果不需要改变数据类型,会覆盖修改. - sdshdr5:
struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }
这种数据结构,使用一个字节存储标志与长度,没有存储剩余可用字节长度.
-
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位空置.
- 缓存行大小一般为64byte,当一个字符长度不超过44个字节(redisObject:16 + sds:1+1+1+(44+1))时,可以放入缓存行中(char[]的末尾不被填充/0,占一个字节).此时它的类型为嵌入式字符串embstr(embedded str)
- redis规定,sds最大位512M
- bitmap: 底层也是sds,但进行的是位操作,通过偏移量操作每一位的值(0/1).可以用来记录大数据量的、只有两种变化的信息.如:登陆、签到 等等信息.但要注意,如果偏移量差距过大,数据量又比较小,就不太适用(浪费大量内存空间,可以考虑位目标添加一个新的自增ID.例如当统计用户在线情况,用户量巨大,存在分片.可以对每一个分片单独添加一个自增的ID,不需要全局唯一.统计时,分别对每一个分片单独记录.这里没有考虑用户活跃情况,是否存在大量非活跃用户.具体情况,具体分析).
bitmap内置了位运算函数,可以进行位计算.(如连续7天登陆情况,进行7天对位与操作) - 剩余三种与sdshdr8类似,不再赘述
list
使用场景
- list使用更类似一个双端队列,它默认拥有正负索引(左:0、1、2; 右:-1、-2、-3).
- list常用的场景,可以作为栈、队列、阻塞队列. 通过 LPUSH、LPOP、RPUSH、RPOP、BLPOP、BRPOP 命令完成. 阻塞获取数据超时事件不宜过长.
- 带有时间线的消息信息流. 例如微博动态、订阅消息等等. 以订阅文章为例,当订阅当目标发出了一篇新当文章时,可以根据订阅数量,选择向目标推送消息或通过特定标记告知目标自己拉去消息(拉去消息不能使用POP命令),通过list轻量级的完成需求.还可以通过LRANGE命令,查看最新的消息.
数据结构
redis中的list使用了quickList和zipList来实现的.
zipList:
- zlbytes: 当前list中存储的数据大小
- zltail : 尾节点位置
- zllen : 存储了多少个元素
- zlend : 标识list结束位置,大小恒等于255
-
prerawlen : 前一个字节的数据长度,如果前一个数据占用字节数小于254 (8位最大表示255,255已经用于标识list结束,所以这里取到254) ,满足条件,用一个字节记录前一个数据字节大小,否则加4个字节用来记录地址(用来进行上一个数据的寻址.这里没有直接使用4个字节记录地址,因为list存储的数据是未知的,这可能会导致胖地址的问题.)
-
len : 用来记录长度,当长度较小时,还可以记录数据类型,这个参数比较复杂(根据数据长度,可以知道这个字段的长度)
-
data: 具体数据
-
一个zipList的数据大小不可以存储太大.这是因为过大的数据量会导致数据迁移(各种pop操作)速度降低,影响整体效率.这个值可以根据配置文件修改: list-max-ziplist-size,这个值redis已经定义好了(-1 ~ -5),一般使用-1 -> 4kb 或者 -2 -> 8kb.当数据量过大时,会导致ziplist分裂(根据zlbytes判断).
quickList:
- quickList 符合常规意义上的双端链表,它的ZL指向的是一个zipList.
- 为节省空间,quickList可以对不常用对节点进行压缩(首尾位置对节点使用频率最高),如何压缩可以通过配置文件配置: list-compress-depth. 这里配置对数字代表从首、尾开始,多少个节点开始压缩.
hash
使用场景
例如购物车这种场景,数据相对来说重要性不高,需要经常修改。相比于string,数据更加方便管理,占用空间也更小,但只能整体设置超时。而且集群模式下,可能导致数据分片不均匀、要小心 bigKey 。
数据结构
- 当数据量较小时,redis直接使用了zipList存储数据.当数据量与数据大小超过某个阀值,会转为dict. hash-max-ziplist-entries : 512 zipList存储当最大数据量512个. hash-max-ziplist-value : 64 zipList单个元素最大值64byte.
- 由于采用量zipList这种数据结构,当创建一个小hash数据时,数据是有序的.
- 阀值的设计,是根据查询效率与迁移效率综合考虑的
set
使用场景
- set主要用于集合的各种运算:包含、并集、交集、差级 等等
- 用于抽奖 : 包括获取成员(SMEMBER)、随机获取特定数量的成员(SPOP/SMOVE) 等操作
- 点赞、关注、共同等朋友、可能认识等人 等等功能, 所有类似等集合操作, 都可以使用set轻量级的完成
数据结构
- 当存储数据是整形,且数据量不超过阀值, 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))
- 当不满足上述条件,set使用value值位null(dict中hashtable的value)的dict来存储数据
zset
相较于set, zset是一个有序的集合,它的每一个数据, 拥有一个分值, 数据会按照分值排序. 与set不同, zset并不是用来进行多个集合运算的, 更多的是用来顺序/倒序访问, 范围查询, 分值查询等操作.
使用场景
- 新闻点击量, zincreby(自动怎见指定数据分值)、zrange(范围获取目标)
- 7日点击量、一个月内点击量. zunionstore 对不同日期的数据,进行分数合并
数据结构
zset 是由 dict + zskipList 组成的,是一组有序的数据.
- dict用来查询数据与分值的关系,skipList用来通过通过分值,获取数据(精确查找、范围查找)
- zskipList的查询效率为 logn
- 如上图所示,外层对象存储了头节点指针、尾节点指针、存储的数据量、层高
- 层高的生成使用了冥次定律(是一个随机数,层高越高,几率越小).
持久化
RDB(快照)
通过配置, 设置数据持久化条件. 将内存数据存储到dump.rdb的二进制文件中.
策略配置: save 60 1000 // 60 秒内有至少有 1000 个键被改动. 可以配置多个策略.
- save命令: 阻塞线程, 执行持久化操作
- bgsave命令: 非阻塞, 执行命令后, 会 fork 一个子线程, 将内存中对数据进行持久化. 同时, 在持久化过程中, 新命令产生到数据也会同步到持久化文件中(写时复制).
- rdb模式会持久化所有的数据, 这也导致它不可能频繁的触发. 所以这种模式下, 可能发生大规模的数据丢失.
AOF(append-only file)
通过配置, 将命令写入到 appendonly.aof 中.
同步策略:
- appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
- appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
- appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
重写策略:
- auto‐aof‐rewrite‐min‐size 64mb // aof文件最小要达到64M开始重写
- auto‐aof‐rewrite‐percentage 100 // aof 文件大小增加一倍, 再次重写, 这里就是126M
aof 写如的是RESP命令, 当用户对一条数据进行多次操作的时候, 会导致前面执行的命令被覆盖, 对于持久化文件来说, 被覆盖的命令是无效命令. 重写会将这些无效的命令去除. 重写时, 会创建一个新的文件写入命令, 当重写结束后, 覆盖appendonly.aof.
格式:
aof 模式持久化数据, 时将执行的命令(不包括查询),以RESP命令写入本地磁盘, 在需要时, 重新执行这些命令. aof 虽然可以解决数据丢失问题, 但每次刷盘会极大的降低吞吐量, 在实际应用场景下并不适用. 实际上, 一般都会适用 eversec 这种模式(可能导致丢失一秒钟的数据), 它也是 aof 的默认模式.
混合持久化
RDB 持久化会带来数据大量丢失(或频繁重写全部数据)的问题, AOF 持久化的磁盘数据较大,且重写时耗时较大. 在 redis 4.0 后, 新增了混合模式.
配置策略:
- 需要开启 aof .
- 放开注解 aof-use-rdb-preamble yes
混合模式其实就是一种 aof 模式, 不过在重写时,会将旧数据以rdb的模式写入磁盘, 而新的命令, 将会以aof的模式追加到文件中. 当满足配置要求, 再次触发重写时, 又会进行这个流程.
混合模式较好的解决了rdb与aof各自存在的问题. 但仍然不能保证数据但绝对安全(aof 不配置为 always), 但 redis 作为一个缓存中间件, 并不需要保证数据的绝对安全, 内存中缺失的数据, 可以通过数据库或其他方式作为补充查找.
持久化策略:
- 基于 redis 的持久化模式(无论哪一种), 都无法将数据回复到一个特定的状态下, 所以可以根据需求, 每个一个特定的时间将数据拷贝出来一份.
- 对于拷贝出来的数据, 要考虑到定期删除和磁盘损毁的问题.
集群
主从模式
redis 的主从模式非常简单, 只需要配置主节点地址, 并添加只读配置, 就完成了.
- replicaof 127.0.0.1 6379
- replica‐read‐only yes
数据全量复制流程:
- 从节点向主节点发起同步请求 psync 命令.
- 主节点收到命令, 后台启用 bgsave 命令, 将最新的数据rdb 发给从节点. 通知清空主节点的缓存(如果收到多个请求, 只会执行一次 bgsave).
- 从节点清空旧数据. 加载新数据
- 备份期间, 新的数据会存储在buffer中, 在rdb同步完成后, 将缓存的数据同步给从节点
- 从节点执行收到的缓存中的命令.
数据断点续传流程:
- 从节点与主节点断开链接. 从节点与主节点重连, 向主节点发送 psync 命令, 命令中携带 offset
- 主节点中会缓存一段时间内的操作数据. 收到 psync 命令后, 检测 offset.
- offset 在主节点的缓存(repl-backlog-buffer)中, 主节点将从节点缺失的数据, 从缓存中读取后, 发送给从节点.
- offset 不在主节点的缓存中, 主节点将会发送全量数据(正常同步流程).
- 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 个.
配置策略:
- cluster‐enabled yes 开启集群
- cluster‐config‐file nodes‐6379.conf 集群信息文件. 每个节点你使用自己的文件.
- cluster‐node‐timeout 10000 集群节点超时时间(ms)
集群部署:
- 配置完成后, 分别启动所有的节点.
- 使用命令 (redis 5.0 以后) redis‐cli ‐a zhuge ‐‐cluster create ‐‐cluster‐replicas 1 2 ip1:port1 ip2:port2 … # ‐‐cluster‐replicas 1 2 代表每1个主节点拥有2个从节点. 后面要添加所有的 redis 服务地址
- 命令执行后, 服务会自动组建子集群, 并为每个子集群平均分配槽位(16384个槽位).
槽位:
- redis 的 key-value 结构在 c 中是通过定义的 dict (与java中的map概念相似) 类型实现的(详细实现可见核心数据类型部分). 数据最终要落在 dictht(dict hash table) 上, 也就是对 key 值做hash, 通过运算决定落在数组的哪一位上(桶的概念).
- 槽位与key的桶计算很类似,的集群部署时, redis 自动分了16384个槽位, 分别赋给不同个的小集群. 当进行数据操作时, 通过计算 key 的 hash 值(crc16 算法)与槽位分配结果, 选择调用哪个小集群的 master.
- 重定位, 当用户计算得到子集群master的地址后, 如果尝试调用, 发现槽位错误. redis会发起通知,告诉客户端目标槽位的映射地址. 这时, 客户端需要重新拉取槽位信息, 槽位与服务映射出现了变化,缓存信息需要更新(redis 的各种客户端都实现了重定位的功能).
选举:
redis 使用 goosip 协议进行选举, 当slave发现它的master无法联通(FAIL 状态)后(无法联通的时长需要超过配置中的超时时间), 它会向所有其他节点(所有小集群)发送信息, 请求成为新的master. 收到消息的节点中, 只有 master 节点会响应且只会响应一次(其他 master 得知了目标master 进入FAIL状态, 否则会拒绝投票). 当slave节点收到半数以上 master 节点的响应, 就会成为新的 master , 并通知所有其他节点. 这也是为什么至少需要3个 master 节点集群才能正常运行. 基于它的选举方式, 为加速选举速度, 可以配置投票信息随机延时发送, 防止 slave 均分票数, 长时间无法选举成功.
脑裂问题:
- 成因: redis 会出现脑裂问题. 当原始的 master 与它的slave出现网络中断, 它的子节点被选举成为新的master后. 如果网络中断没有恢复, 那么旧的master仍然认为自己是master, 也就是它仍然可以提供写功能. 当网络恢复后, 它会获知自己成为了slave. slave 需要与master 同步数据, 那么以 redis 的同步方式, 所有在双 master 时间内写入的数据, 都会被丢弃.
- 解决方案: redis 支持配置 min‐slaves‐to‐write 1 . 1 代表最少有一个 slave 节点写入成功, master 节点才会返回. 这里可以根据 slave 节点数量采用半数成功机制. 同时, 这项配置一定程度上会影响 redis 的工作新能.
注意:
- 集群模式, 最少需要3个节点(选举需要最少3个节点).
- 集群模式的从节点不提供任何服务, 它们只是数据的备份.
- cluster-require-full-coverage yes/no 当插槽部分不可用时, 集群是否仍然可用.
- 批量操作时, 如果它们的槽位不在一个子服务上, 会操作失败.
- 使用 {} 设置槽位计算的 key 值部分. 例如: {user}:admin:12 这个key, 槽位计算时, 只计算 user 的 hash 值.
- 哨兵模式与集群模式下, 节点数量最好为奇数, 且至少需要有 3 个. 因为 redis 的选举成功需要半数以上投票. 以 3 个投票人为例, 也就是至少有 2 票才能选举成功. 也就是 3 个投票人中, 有一个宕机了, 集群仍然正常运行. 而 4 个投票人, 最少需要 3 票 , 仍然最多只能允许一个人宕机. 所以相较与 3 个投票人, 增加一个并不能增加集群的可用性, 不如使用5个投票人.
- 由于 redis 作为一个基于内存的非关系型数据库, 一般作为缓存曾使用, 对它的性能要求很高, 不会使用 aof 的 appendfsync always 模式, 也就是说, 当出现服务宕机等问题时, 数据一定会产生丢失问题, 不能保证数据的绝对安全.
*小技巧:
- redis单节点内存不宜使用过大, 一般不超过10g.
- 在redis客户端, 可以使用 help + tab键的命令组合, 查找目标帮助(help @string 会显示string类型所有操作命令). 每次按tab键, 都会随意一个帮助类型.
- redis 只有指令操作是单线程的, 如数据持久化、异步删除、集群数据同步等都是其他线程操作
- redis 速度快等最根本原因, 是因为它基于内存. 所以持久化策略选择强一致, 性能就会大幅度下降.其次, redis 使用了epoll实现IO等多路复用.
- redis 不要使用全量遍历(keys命令), 使用渐进式遍历(scan 命令).