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
出于性能考虑,当数据满足一定条件时会导致编码发生变化。两种因素会影响数据对象的编码,包括:
- 元素宽度
- 元素个数
以哈希对象为例,当哈希对象满足如下两个条件时,哈希对象采用 ziplist
编码,否则采用 hashtable
编码。
- 所有键值对的键和值的字符串长度都小于 64 字节
- 键值对数量小于 512 个
ziplist
有利于节约内存,但查找的时间复杂度是线性的,在数据数量不多时也可以接受;hashtable
有更好的查询性能。尽管哈希对象可能采用 ziplist
编码,但数据元素不多,因此 HGET
命令的时间复杂度是 O(1)。
其他数据对象的编码转化过程时类似的。导致编码变化的限值一般是可以修改的,在实际使用时可以根据需要来配置。
使用场景
字符串对象
- 计数
简单计数可以对字符串的键或者哈希对象的字段执行 incr
操作。例如对网站 pv,按钮点击次数的统计等。
uv 的统计需要区分不同的用户,同一个用户不会累积。当数据很多时,记录每个用户的 id 会造成空间的浪费。Redis 提供了操作 bitmap
和 hyperloglog
的命令,两者都是基于字符串对象实现的,不是独立的数据对象。bitmap
以位为单元来操作字符串,可以大大节约空间。在用于计数时, 将标识别映射到一个位上。hyperloglog
基于概率型算法,统计会有一定误差,适用于不是严格计数的场景,如网站的 uv。
- 限速
出于安全考虑,一些场景需要对用户的某些操作进行限速,如频繁修改数据、获取手机验证码等。通过设置关键字的过期机制来实现限速功能。下面是个示例:
// 当键不存在时设置初始值和过期时间
SET IPLim_user101 1 EX 60 NX
// 增加计数后未超过限值时,不限速;否则限速。
// 增加并返回计数
INCR IPLim_user101
除此之外还需要在应用程序中将数据进行持久存储,便于后续分析统计。
- 缓存
对存储层的数据进行缓存,数据优先从缓存区获取。一个典型的示例如下:
使用缓存可以提升数据的访问速度以及降低存储层的压力,但使用时也需要留意诸多问题:
- 缓存增加了应用程序代码的复杂度,增加了维护成本
- 优先对存储层进行优化,不可过度依赖缓存
- 缓存带来的数据不一致,更新机制
- 缓存穿透、雪崩等问题
- 高并发时的高可用性
哈希对象
哈希对象可以保存多个键值对,适合存储关系型对象,如用户信息等。字符串对象的值也可以是 json,但这意味着编码与解码操作。采用哈希对象可以操作单独的字段,无需处理整条数据。
列表
列表的典型应用场景是消息队列模型:生产者使用 lpush 向队列中塞入数据;多个消费者使用阻塞的 brpop 从队列中获取数据。
通过列表的不同操作的组合,可以构建抽象的数据结构:
·lpush+lpop=Stack(栈)
·lpush+rpop=Queue(队列)
·lpsh+ltrim=Capped Collection(有限集合)
·lpush+brpop=Message Queue(消息队列)
集合对象
集合对象提供了集合运算,非常适合一些需要聚合分类的场景:
- 标签系统,如给用户添加标签
- 社交系统,如共同关注
- 数据统计,如客户端 IP
- 生成随机数,如进行抽奖
有序集合对象
典型的场景是榜单系统。有序集合的分值是榜单的纬度。