2024-06-170

redis 常用的数据结构以及使用场景

  • string
  • hash
  • list
  • set
  • zset
    • 底层结构为跳表

Redis 快主要有五个原因:

  • 基于内存操作,没有磁盘随机 IO。
  • 单线程处理命令(网络模型可多线程,但命令执行单线程),避免线程切换和锁竞争。
  • 高效的数据结构(SDS、HashTable、SkipList、QuickList 等)。
  • IO 多路复用(epoll/kqueue)提高网络处理能力。
  • C 语言实现,运行时开销小。

Redis 为什么采用单线程?

Redis 不是整个程序只有一个线程,而是“核心命令执行”主要是单线程

也就是说,客户端发来的 GET、SET、HGET、ZADD 等命令,在 Redis 主线程中按顺序执行。

为什么单线程还快

因为 Redis 的主要操作都在内存中完成,不需要频繁磁盘 IO

单线程的好处

  • 没有锁竞争
    • 多线程操作同一份数据,需要加锁;Redis 单线程执行命令,不需要为数据结构加锁。
  • 没有线程切换成本
    • 多线程频繁切换会有上下文切换开销,Redis 避免了这部分成本。
  • 命令执行很快
    • 大多数 Redis 命令复杂度很低,比如 GET、SET 通常是 O(1)。
  • 使用 IO 多路复用
    • Redis 通过 epoll/kqueue 同时处理大量连接,不是一个连接一个线程

请求流程

text
多个客户端连接 ↓ IO 多路复用监听事件 ↓ Redis 主线程读取请求 ↓ 单线程执行命令 ↓ 返回结果

什么算大 Key?

常见判断:

  • String:value 超过几百 KB 或 1MB 以上
  • Hash/List/Set/ZSet:元素数量特别多,比如几万、几十万
  • 单个元素本身很大,比如一个 field/value 很长

如何解决大key问题

  • 不要一次性取全量
    • 通过scan的方式获取数据
  • 拆分key
    • 按分页、时间、hash 分片拆
  • 控制写入大小
  • 大对象不写入redis
  • 已经写入的如何处理
    • 不要del直接删,通过unlink异步释放
    • 集合类大 Key 分批删除

单线程有什么缺点么

  • 一个慢命令会阻塞后面的所有命令
    • case : keys *
    • case2: 对一个大key 执行hgetall、smembers、lrange等操作

主从复制

概念

  • 将数据库分为主从节点,主节点源源不断的将数据复制给从节点,保证主从节点中存有相同的数据

同步方式

  • 全量同步
    • 新从节点加入或主节点重启时
    • 主节点生成 RDB 快照 → 发送给从节点 → 从节点加载
    • 同时生成 复制缓冲区(Replication backlog) 保存写命令
  • 增量同步(Partial Sync)
    • 使用 复制偏移量(offset)
    • 从节点只接收主节点缓冲区内的增量写命令
    • 避免每次都全量同步

作用:

  • 提升服务的请求处理能力
    • 一个节点支持的读流量有限,部署多个节点,主从节点可以共同提供服务
  • 提升整个系统的高可用性
    • 从节点有主节点数据的副本,当主节点宕机后,可以立刻提升其中一个从节点为主节点,继续提供服务

哨兵

  • 哨兵对应的是额外的哨兵进程,负责监督主节点的健康状态
  • 一般一个哨兵系统需要最起码三个哨兵节点
  • 特点
    • 部署相对简单、对现有单机redis实例做主从复制,兼容性好
    • 不支持数据分片、手动扩容麻烦

redis 集群

  • 概念:redis 集群不同于redis哨兵的高可用方案,他是多个redis节点组成,每个节点负责数据的一部分
  • 特点
    • 节点之间互相通信,自动完成数据分布和故障转移
    • 跨槽操作有限制(多键事务(hash tag解决))

跳表

实现简单,范围查询和排名操作更方便,平均性能和红黑树接近,非常适合 ZSet 的使用场景

  • 时间复杂度
    js
    查找:O(log n) 插入:O(log n) 删除:O(log n) 范围遍历:O(log n + m)
  • 相比红黑树的优势
    • 范围查询简单
      • 不需要中序遍历,跳表找到起点后,只要沿着最底层链表往后走即可
    • 实现简单
      • 靠随机层数,插入删除只需要改指针,实现更直观,维护成本低
    • 性能足够好(时间复杂度)
    • 天然有序链表结构
      • ZSet 很多操作本质是“找到位置,然后顺序扫描”。
      • 跳表底层就是多层链表,最底层天然保存全量有序链表,特别适合顺序遍历。

单机切换集群or单机和集群共存的问题

同一套代码不同的部署环境可能需要时单机部署,可能是集群部署

需要考虑的问题

  • 客户端的初始化方式不同
  • 多 Key 命令在集群下可能报错
    • 尽量避免跨 key 操作;
    • 用hash tag 保证被操作的key 落在同一个slot上
  • Lua 脚本在集群下也有 slot 限制
  • 事务 MULTI/EXEC 受限

为什么不用 Redis Sentinel,而用 Cluster?

Sentinel 更适合单机容量够用、只需要主从高可用的场景;Cluster 适合数据量或 QPS 超过单机瓶颈,需要水平扩容的场景。我们选择 Cluster,是因为它不仅能故障转移,还能通过 slot 分片把数据分散到多个主节点上。

  • Redis Sentinel 解决高可用的问题
    • sentinel 是1主 + N 从 + 哨兵节点
    • 本质上环时一份完整的数据
  • 而cluster解决高可用 + 水平扩容问题
    • 多个 Master 分片存储数据
    • 每个 Master 可以有 Slave

Redis Cluster 为什么是 16384 个 Slot(槽)?

redis cluster 不是直接将key映射到节点上,而是 key -> slot -> node

  • 计算方式可以简单理解为
    • CRC16(key) % 16384
  • 为什么要有槽的概念
    • 一旦扩容节点数变化,大量 key 都要重新映射,迁移成本会很高
    • 有了slot的概念后,就只需要迁移一部分即可
  • 为什么是16384个slot
    • 16384 是 Redis 在节点数量、slot 管理开销、心跳包大小、迁移粒度之间做的折中。
    • slot 太少,分片粒度粗;
    • slot 太多,元数据和心跳传播成本更高。
    • 16384 对绝大多数集群规模已经足够

为什么 Hash Tag 能解决 CROSSSLOT?

Hash Tag 的作用是指定 key 中参与 slot 计算的部分。Redis Cluster 如果发现 key 中有 {xxx},会只对 xxx 做 CRC16 计算,因此多个 key 只要 {} 中内容相同,就会落到同一个 slot,从而支持 MGET、Lua、事务等同 slot 操作

  • hash计算只对 {} 内的内容进行计算,所以会落到同一个slot

本文作者:曹子昂

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!