重学Redis

redis

redis 是BSD许可的开源内存数据中间件,可以用作 数据库、缓存、消息Broker、流引擎。

redis 数据类型

data structures:

  • strings
  • hashes
  • lists
  • sets
  • sorted sets [range queries]
  • bitmaps
  • hyperloglogs
  • geospatial indexes
  • streams

redis key 的推荐规则

keys rule:

  • 不要设置过长的KEY
  • 过短的KEY也不建议
  • 适中,并且能达意的,考虑用分隔符
  • 最大的KEY SIZE: 512MB.

String

Redis 存储字符序列、可以是 文本、序列话对象、二进制数组。支持 incr 操作。

  • SET
  • SETNX
  • GET
  • MEGT

大部分字符串操作都是 O(1), 但是 SUBSTR、GETRANGE、SETRANGE 可能是 O(n)

Redis String 是才赢得 SDS 实现的(Simple dynamic string)

在执行 set hello helloval后底层是

  • 那么键的字符串对象,底层创建保存 ‘hello’ 的SDS
  • 那么值也是字符串对象,底层创建保存字符串的 ‘helloval’ 的SDS

SDS 的结构体

    struct sdshdr {
        char buf[]; //字节数组,用于保存字符串
        int len;  //记录buf数组中已经使用字节的数量
        int free; //记录buf数组内未使用的字节的数量
    }
  • 对比C,有专门的字段记录buf数组的字符串长度和空闲 时间复杂度O(1)
  • 对比C,记录字符串了长度,在进行分配的时候,或修改的时候,杜绝了缓冲区溢出的可能性,在SDS API对SDS进行修改的时候,会先检查空间是否满足修改的需求,不满足的话,会先拓展至执行所需要的大小,再执行修改操作。
  • 减少字符串修改带来的内存重分配操作, 通过 free 参数记录未使用的空间,这样实现了空间预分配和惰性空间释放策略
  • 二进制安全。因为有 len 属性,不需要判断字符串确认是否是末尾。可以存储任意结构的二进制数据
  • 兼容部分C函数

Lists

通过链表实现的、即使有数百万元素,在列表头部或尾部添加新元素的操作都是O(1): LPUSH 同时向10个元素和100万个元素添加头部元素的速度是相同的。

最大的长度 2^32 - 1

  • 实现 堆栈 或 队列

  • 后端分布式系统构建队列使用

  • LPUSH

  • LPOP

  • LLEN

  • LMOVE

  • LTRIM

例子

  • 记录用户发布社交网络的最新更新(LPUSH、LRANGE)
  • 进程间通讯,生产消费模式,生产者将元素推进入列表中(LPUSH),消费者消费这些元素(RPOP)

访问其头部或尾部的列表操作是 O(1),但是操作列表中元素通常是 O(n), LINDEX、LINSERT、LSET

Sets

唯一字符串成员的无序集合。

最大的长度 2^32 - 1

  • SADD
  • SREM
  • SISMEMBER
  • SINTER
  • SCARD

SADD、SREN、SISMEMBER 是 O(1)。 SMEMBERS 在大型集合时候是 O(n), 替换方案 SSCAN

Sorted Sets

通过相关分数排序的唯一字符串成员集合, 当分数相同的时候,这些字符串按字典顺序排列

  • 排行榜,排序最高分的有序列表

  • 速率限制器,通过滑动窗口速率限制器防止更多的API请求

  • ZADD

  • ZRANGE

  • ZRANK

  • ZREVRANK

大多数集合操作都是 O(log(n)), n 是成员数

Hashes

通过 键值对 的集合,

最多存储散列的是 2^32 - 1, 但是往往 值受限于 VM 总内存限制

  • HSET
  • HGET
  • HMGET
  • HINCRBY

大部分 O(1) , HKEYS,HVALS、HGETALL 是 O(n), n是键值对的数量

Hash 是一个很常见的数据结构、Redis 的实现是一个比较经典的方案。 其采用了链式哈希,在不扩容哈希表的前提下,将相同哈希值的数据链起来。 在针对rehash 的上,设计了渐进式rehash设计,缓解了rehash操作带来的额外开销。

链式哈希

  • 哈希是将不定长的变成定点长的操作,
  • 一个最简单的哈希是一个数组,数组的每个元素等于一个哈希桶
  • 会存在多个key哈希完后是同一个值,这个就是哈希冲突。那么解决哈希冲突最简单的方式就是,哈希桶下挂载链表,链表内存储的是值

往往我们在新开启的哈希桶的时候,因为无法预知数据大小,所以不会一下子开辟很大哈希表。链式哈希带来了问题是

  • hash碰撞:带来了在单个Hash桶上退化为链表,损失了高性能( 数组O(1)的时间复杂度 变成了 链表的O(N)的复杂度 )
  • 无序性:HashMap的Hash桶 数组的顺序与put操作的先后顺序无关

无序性还好,但是 hash碰撞带来的问题是导致Hash桶的扩容,如果大批量的数据,触发负载因子后触发hash桶的扩容。如果不设计一套扩容方案,会导致要数据分配完毕才进行其他操作,会丧失 redis 的高性能, 所以 Redis 设计了一套 渐进式Rehash

渐进式Rehash

Redis 默认使用了 两个 全局Hash表,先使用的是 Hash表A,此时另外的 Hash表B 并未被分配空间,随着 Hash表A 的数据增多,Hash碰撞越来越多拉低了 redis 的性能,开启 rehash操作:

  • 给 Hash表B 申请较大的空间,入 Hash表A 的哈希桶数组容量的 2 倍
  • 将 Hash表A 的元素重新计算 Hash 和 取模 运算,映射到 Hash表B 中
  • 清理 Hash表A

具体过程是

  • 从Hash桶数组 的第一个 节点开始,每次处理客户端一个请求,想Hash表B中Copy一组Hash桶的数据。直到所有的节点数据 COPY完成

渐进式rehash

rehash 和 hashmap 的扩容区别

  • rehash 扩容 两个 hash表,hash表B 的扩容是 Hash表A 的 2倍,扩容过程中新加入的元素直接添加到 hash表B
  • hashmap 的 resize,

Streams

Stream 是一种数据结构、作用类似于 附加日志,可以使用 Stream 实时记录 和 同步事件。

Redis 为 每个Stream 条目生成一个唯一的ID,可以使用这个ID进行检索和关联条目

  • 事件溯源

  • 设备监控

  • 通知

  • XADD 向 Stream 添加条目

  • XREAD 读取或者多个条目,从给定位置开始

  • XRANGE 返回两个ID之间的条目范围

  • XLEN 返回 Stream 的长度

向 Stream 添加是 O(1), 访问任何单个条目都是 O(n), Nx是 ID 的长度

Geospatial

地理空间坐标用来搜寻给定半径 或 边界范围 的附近点的。

  • GEOADD 将位置添加到给定的地理空间索引,经度位于纬度之前
  • GEOSEARCH 返回具有给定半径或边界框的位置

HyperLogLog

HyperLogLog 是一种估计集合基数的数据结构、是一种概率数据结构。以准确性换取了空间利用率。

HyperLogLog 可以估计具有多达 2^64 个成员的基数

  • PFADD 将元素添加到 HyperLogLog
  • PFCOUNT 返回集合中元素的估计值
  • PFMERGE 将两个或者多个 HyperLogLog 合并成一个

PFADD、PFCOUNT 都是恒定时间和空间完成的 ,合并 HLL 是 O(n), n 是草图的数量

BitMaps

bitmaps 是字符串类型数据的扩展,可以将字符串视为响亮,可以对一个或者多个字符串执行按位运算。

  • 对于 集合的成员对应与 0-N 的请客,有效的集合

  • 对象权限、每个位代表一个特定权限。

  • SETBIT 将提供偏移量设置为 0 或 1

  • GETBIT 返回给定偏移量的位值

  • BITOP 允许对一个或多个字符串执行按位运算

SETBIT 、GETBIT 都是 O(1), BITOP 是 O(n), n 是比较中最长字符串的长度

Bitfields

Client-side caching

客户边缘缓存技术是一种提高性能的服务技术。顾名思义,是借助客户端的可用内存去存储部分从数据库端获取到数据子集。

特点

  • 数据可以以非常小的延迟可用
  • 减少数据库的查询,提高性能

Key eviction

maxmemory 配置的是为数据集使用制定数量的内存,设置为 0 ,没有内存限制(64位)【32位的是默认的3GB】

  • noeviction:达到内存限制,怒不保存新的
  • allkeys-lru:保留最近使用的,删除最近最少使用的
  • allkeys-lfu:保存经常使用的,删除最少使用的 LFU
  • volatile-lru:删除 expire 为 true,最近最少使用的
  • volatile-lfu:删除 expire 为 true 的最不常用的
  • allkeys-random:随机删除
  • volatile-random:随机删除 expire 为 true
  • volatile-ttl: 删除 expire 字段为 true 且 最短剩余生存时间(TTL)

淘汰驱逐的过程:

  • 客户端运行新命令、服务端添加新的数据。
  • Redis 检查内存使用情况、如果大于 maxmemory 限制, 它回根据策略驱逐
  • 执行命令,添加数据

High availability Sentinel

Sentinel 是在不实用 Cluster 提高可用性的。包含的功能有

  • 监控:sentinel节点会定期检测 redis 数据节点(主从)、其余 Sentinel 节点是否可达
  • 通知: sentinel节点会将故障转移的结构通知给应用方
  • 自动故障转移: 主节点不可用,从节点晋升为主节点并维续后续正巧的主从关系
  • 配置提供者: Redis Sentinel,客户端在初始化的时候连接的是 Sentinel 节点的集合,从中获取主节点的信息

keyspace notifications

Keyspace 通知允许客户端订阅 Pub/Sub 频道,以便接收以某种方式影响 Redis 数据集的事件。

  • All the commands affecting a given key.
  • All the keys receiving an LPUSH operation.
  • All the keys expiring in the database 0.

键空间通知是通过为 影响数据空间的每个操作发送两种不同类型的事件实现的。

Persistence

redis 如何将数据写入磁盘的,append-only files、snapshots!

持久性是将数据写入持久存储设备。

  • RDB(Redis Database) 以指定时间间隔执行数据集的时间点快照
  • AOF(Append Only File): AOF 持久化记录服务器接受的每个写操作,在服务器启动的时候,再次进行回放,重建原始数据集。 指令的写入是与Redis协议相同,以追加的方式记录。
  • No persistence
  • RDB + AOF 可以在同一个实例中结合使用 AOF + RDB, 这种情况下,数据最完整。

RDB 优势

  • RDB 是 redis 数据非常紧凑的单元时间点表示的,RDB文件非常适合备份, 可以在最近24小时内每小时归档一次RDB文件,并在30天每天保存一份RDB文件。可以多个版本的
  • RDB 非常适合灾难性恢复(他其实就是一个加密的压缩文件)
  • RDB 最大化的提高性能,
  • RDB 在大数据集的情况下重启比AOFå¿«
  • 在同步副本上, RDB 在重启 和 故障转移的情况下支持 部分同步

RDB 缺点

  • 在要求对数据丢失的可能性最低的情况下,RDB并不好。因为是定期备份,在这个单位时间内停止工作的会,导致丢失最新的数据
  • RDB 需要经常 fork() ,在子进程持久化数据存盘的时候。Fork操作可能很耗时,同时大数据集的时候会影响CPU性能。AOF 也需要 fork 但是频率很低。

AOF 优势

  • 更多的 fsync 策略: 从不 fsync、每秒 fsync, 每次查询时 fsync, fsync 是后台线程执行,
  • AOF 日志是 追加日志,因此没有寻址问题,也不会在断电的时候出现损坏问题,即使某个原因日志写一半命令结束,也可以使用 redis-check-aof 工具恢复它
  • AOF 变得很大的时候,redis 可以在后台自动重写 AOF。重写是完全安全的,因为当redis继续追加旧文件的时候,会使用创建当前的数据集做少的操作集重新生成一个全新的文件,一旦文件准备完毕,redis 就会切换两者并追加哦到新的文件
  • AOF 操作日志,是易于理解和解析的。可以轻松的导出 AOF 文件

AOF 缺点

  • AOF 文件通常比相同的数据集大小的 RDB 文件大
  • 根据确定的 fsync 策略,AOF 可能比 RDB 慢,
  • redis < 7.0 在重写期间对数据库的写入,AOF 会使用大量的内存(当前的写入被缓冲在内存,到最后写入)
  • redis < 7.0 重写期间到达的写入命令会写入磁盘两次
  • redis < 7.0 Redis 可以在重写结束时候,冻结写入并将这些写入命令同步到新的 AOF 文件

Pub/Sub

7.0 之后 引入了 Sharded Pub/Sub, 在之前的版本,对于集群的 channel 不被当成数据处理,导致了就不回参与 hash计算,无法按 slot 分发到节点,所以在 7.0之前 集群模式下的 redis 对于 pub/sub 是采用广播的方式,这会带来广播风暴的问题。 假设集群 100 个节点,用户在对 节点1 的某个 channel 进行 publish 发布消息,该节点需要将消息广播给其他 99 个节点,如果有的节点只有少量订阅,且绝大部分消息对其来说是无效的。这对 CPU 和 网络都是开销。 而 Shared pub/sub 就是解决该问题, 会把 channel 按 分片来进行分发,一个分片节点只负责处理属于自己的 channel 而不进行广播

Replication

replication 是 提供 支持高可用性 和 故障转移的根本。

  • Master 和 Replica 连接良好时候,Master 通过发送命令数据集来对副本进行更新,包含了写入、过期、驱逐、更改等任何操作
  • 当 Master 和 Replica 连接中端时候,由于 网络问题 或者 Master 和 Replica 之间检测超时,Replica 会重连、在重连成功后获取断开连接期间错过的命令流。
  • 当 Replica 确实无法重新同步是,Replica 将采用备用方案,请求全部重新同步,这边 Master 需要创建一个所有数据的快照,将其发送到 Replica,并随着 数据的变化继续发送命令流

Redis 默认采用的 异步复制。异步复制的特点:

  • Redis 是异步复制,异步复制的是 Master acknowledges 的数据
  • 一个 Master 允许多个 Replicas
  • Replica 能接受其他 Replica 的连接,除了多个 Replica 连接 同一个 Master 外,Replica 还可以类似级联的结构连接到其他副本
  • Redis 复制在 Master 是非阻塞的,当一个或多个 Replica 执行 同步 或 部分重新同步的时候,主Master 将继续处理查询。
  • Replica 在 复制的绝大部分都是非阻塞的,
  • 复制 可以用于拓展 redis 的扩展性,也可以用于 复制只读查询的副本
  • 复制 可以 避免 让 Master 将完整的数据写入磁盘

redis 复制

复制的流程:

  • Master实例 都有一个 复制ID(一串大的随机字符串),标记数据集的,还有一个 offset(它随着要发送给副本的数据流的字节增加),即使没有实际联结的副本, offset 也会增加。 也技术说 Replocation ID, Offset 是标记着数据集合的准确版本。
  • 当 副本实例 连接 Master 时候,使用 PYSNC 命令发送 他们保存的 旧主服务器 复制ID 和 目前他们自己处理的 offset。 这样 master 可以只发送所需要的增量部分
  • 如果 主Master 的缓冲区没有足够的数据堆积、或者副本饮用的不是已知的 复制ID 的 历史记录,则会触发完全同步。这样副本集合将会获得完整的数据副本。

完全同步的工作模式:

  • Master 后台开启生成 RDB文件,并同时缓冲客户端接受的新的写命令
  • 后台保存完成后,Master 将 RDB文件 传输到 replica, replica 将其保存在磁盘上,然后加载到内存中。
  • 后续 Master 将所有缓冲的命令 发送到 replica, Replica 执行这个命令流完成。

复制ID

如果两个实例具有相同的 复制ID 和 复制偏移量(offset), 则它们具有完全相同的数据。但是实际上 实例 上有两个 复制ID : 主ID 辅助ID

Redis 实例为什么会有两个 复制ID,因为 副本 升级为 Master。 故障转移后, 升级的副本仍需要记住它过去的 复制ID,因为这样的 复制ID 是以前的主副本,当其他副本与新的 master 同步时候,他们将尝试使用 旧的Master 复制ID 执行部分重新同步。

当副本升级为 Master 的时候,将把其 辅助ID 设置成 主ID,并记录 发生ID切换的偏移量 是多少。然后,生成一个新的随机 复制ID,用于记录新的开始 。

comments powered by Disqus