Redis 的数据系统设计体现了分层设计的特点。自顶向下来看,用户程序(如缓存模块)使用 Redis 提供的命令来操作数据。每个操作数据的命令与特定的数据对象绑定,每个数据对象的由不同编码来实现。我们可以根据具体场景选择特定的数据对象,而每个特定的数据对象又可以根据性能等因素选择相应的编码。这种分层设计是数据抽象思想的运用,它可以降低系统的复杂度,让设计复杂的软件变得容易。每一层将使用与实现隔离,有利于保证不同场景下的使用效率,同时也很容易增加命令、添加新的数据对象以及修改数据对象的编码等。

Redis 设计

数据对象

不同的书籍、文章中对于 Redis 数据系统有不同的表述。这里使用数据对象来表示与命令直接关联的数据;使用数据结构来表示数据对象的具体编码。数据对象是抽象的数据结构。redisObject 表示一个 Redis 数据对象:

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层数据结构的指针
    void *ptr;
    // 计数,用于垃圾回收
    int refcount;
    // lru 模型
    unsigned lru:LRU_BITS;
} robj;

type 指定了数据对象的类型,包括:

  • 字符串 string
  • 列表 list
  • 哈希 hash
  • 集合 set
  • 有序集合 zset

encoding 指定了数据对象的编码,即数据结构,包括:

  • 整数 int
  • 字符串 embstr
  • 字符串 raw
  • 哈希表 hashtable
  • 链表 linkedlist (已弃用)
  • 压缩列表 ziplist
  • 整数集合 intset
  • 跳跃表 skiplist

Redis 设计

出于性能考虑,当数据满足一定条件时会导致编码发生变化。两种因素会影响数据对象的编码,包括:

  • 元素宽度
  • 元素个数

以哈希对象为例,当哈希对象满足如下两个条件时,哈希对象采用 ziplist 编码,否则采用 hashtable 编码。

  • 所有键值对的键和值的字符串长度都小于 64 字节
  • 键值对数量小于 512 个

ziplist 有利于节约内存,但查找的时间复杂度是线性的,在数据数量不多时也可以接受;hashtable 有更好的查询性能。尽管哈希对象可能采用 ziplist 编码,但数据元素不多,因此 HGET 命令的时间复杂度是 O(1)。

其他数据对象的编码转化过程时类似的。导致编码变化的限值一般是可以修改的,在实际使用时可以根据需要来配置。

使用场景

字符串对象

  • 计数

简单计数可以对字符串的键或者哈希对象的字段执行 incr 操作。例如对网站 pv,按钮点击次数的统计等。

uv 的统计需要区分不同的用户,同一个用户不会累积。当数据很多时,记录每个用户的 id 会造成空间的浪费。Redis 提供了操作 bitmaphyperloglog 的命令,两者都是基于字符串对象实现的,不是独立的数据对象。bitmap 以位为单元来操作字符串,可以大大节约空间。在用于计数时, 将标识别映射到一个位上。hyperloglog 基于概率型算法,统计会有一定误差,适用于不是严格计数的场景,如网站的 uv。

  • 限速

出于安全考虑,一些场景需要对用户的某些操作进行限速,如频繁修改数据、获取手机验证码等。通过设置关键字的过期机制来实现限速功能。下面是个示例:

// 当键不存在时设置初始值和过期时间
SET IPLim_user101 1 EX 60 NX

// 增加计数后未超过限值时,不限速;否则限速。
// 增加并返回计数
INCR IPLim_user101

除此之外还需要在应用程序中将数据进行持久存储,便于后续分析统计。

  • 缓存

对存储层的数据进行缓存,数据优先从缓存区获取。一个典型的示例如下:

Redis 设计

使用缓存可以提升数据的访问速度以及降低存储层的压力,但使用时也需要留意诸多问题:

  1. 缓存增加了应用程序代码的复杂度,增加了维护成本
  2. 优先对存储层进行优化,不可过度依赖缓存
  3. 缓存带来的数据不一致,更新机制
  4. 缓存穿透、雪崩等问题
  5. 高并发时的高可用性

哈希对象

哈希对象可以保存多个键值对,适合存储关系型对象,如用户信息等。字符串对象的值也可以是 json,但这意味着编码与解码操作。采用哈希对象可以操作单独的字段,无需处理整条数据。

列表

列表的典型应用场景是消息队列模型:生产者使用 lpush 向队列中塞入数据;多个消费者使用阻塞的 brpop 从队列中获取数据。

通过列表的不同操作的组合,可以构建抽象的数据结构:

·lpush+lpop=Stack(栈)
·lpush+rpop=Queue(队列)
·lpsh+ltrim=Capped Collection(有限集合)
·lpush+brpop=Message Queue(消息队列)

集合对象

集合对象提供了集合运算,非常适合一些需要聚合分类的场景:

  • 标签系统,如给用户添加标签
  • 社交系统,如共同关注
  • 数据统计,如客户端 IP
  • 生成随机数,如进行抽奖

有序集合对象

典型的场景是榜单系统。有序集合的分值是榜单的纬度。

参考