Redis 基础数据结构
1. 背景
为什么会产生NoSQL数据库? 和传统的RDBM的区别在哪里? 有什么突出优势吗?
传统数据库如MySQL, 更多的是表格结构化的行存储数据, 固定的模式(需要数据适应表结构), 通过事务特性控制数据一致性;存在问题: 扩展性-面临表结构修改困难导致的存储数据格式受限, 读写性能-传统关系型数据库由于会将数据持久化到磁盘, 并发和海量数据的情况下, 磁盘读写压力大.
NoSQL的特性: 存储非结构化的数据-如文本, 图片, 音频, 视频; 扩展性强-表与表之间没有关联, 能分片存储, 扩缩容简单; 最终一致性(BASE理论)-保证数据的最终一致性;高频读写支持-海量数据的存储; 针对不同的存储类型: 可分为K-V存储(Redis), 文档存储(MongoDB), 列存储(HBase), 图存储(Neo4j), 对象存储(OSS); NoSQL 产品汇总(https://hostingdata.co.uk/nosql-database/)
NewSQL则是前两者的结合, 如TiDB(PingCAP), ScaleDB
2. 基础数据类型
数据结构: String, Hash, Set, List, Zset
2.1 String字符串
(1)存储数据类型: int, float, string
(2)存储原理⭐
SDS
, redisObject
, 内部编码格式比较
- int: 存储8个字节的长整型(long, 2^64 - 1)
- embstr: emdstr格式的SDS, 存储小于44个字节的字符串
- raw: 存储大于44个字节的字符串.
(3)底层分析:
SDS: Simple Dynamic String 简单动态字符串, (本质字符串数组) 空间换时间
为什么采用SDS实现字符串? C语言本身没有字符串类型, 只能是使用字符数组:
a.使用字符串数组需要给目标变量分配足够的空间, 否则可能会溢出
b.获取字符串长度需要遍历整个字符串
c.C 中字符串长度变更会对字符串数组做内存重分配
d.存储图片, 音视频, 压缩文件等二进制文件的不安全性
引入SDS带来的优势
1.不用担心内存溢出, 会对SDS扩容
2.通过空间换时间方式, 获取字符串的长度
3.通过"空间预分配" 和 “惰性空间释放”, 防止多次重分配内存
Q1: embstr 和 raw 编码的区别? 为什么要为不同字符串大小设置不同的编码?
embstr 的使用只分配一次内存空间(RedisObject 和 SDS 是连续的), 而raw需要分配两次内存空间, 简单来说, 即少分配, 少释放, 查询方便; embstr 面临字符串长度增加时, 需要重新分配内存, 内存空间重分配
Q2: int、embstr、raw三者之间的转化关系?
int 数据不再是整数 --> raw
int 大小超过long的范围(2^63 - 1) --> embstr
embstr 长度超过44字节 --> raw, 此外 只要修改embstr对象, 修改后一定是raw对象
Q3: 为什么对底层数据结构采用RedisObject进行一次封装
存储内容和存储方式的对应, 尽量节省内存空间和提升查询速度
(4) 应用场景
缓存
String类型, 缓存热点数据(如何实时统计热点数据)
分布式数据共享
独立地作为分布式的独立服务, 可以在多个应用之间共享, 分布式Session
分布式锁 🚩
全局ID
INT 类型, 原子性, 分库分表中一次获取一段数据
计数器
INT类型, INCR 方法, 微博点赞, 允许一定延迟, 先写入Redis然后定时同步到数据库
限流
INT类型, INCR方法, 将访问者的IP和其他信息作为key, 每访问一次就增加一次计数, 超过则返回false
2.1 Hash 哈希
(1) 存储类型:
无序键值对, 最大存储数量2^31 - 1
(2) 存储原理
由dictEntry 实现, 内层Hash层可以采用两种数据结构
ziplist: OBJ_ENCODING_ZIPLIST 压缩列表
hashtable: OBJ_ENCODING_HT 哈希表
ziplist: 一个经过特殊编码的, 由连续内存块组成的双向链表. 存储的是上一个节点的长度和当前节点的长度, 读写速度可能会下降, 但可以节省内存(时间换空间)
hashtable: 对dictEntry进行了多层的封装, 将定义的dictEntry 封装到dictht中, 然后又将dictht封装入dict中, 即数组+链表的结构(注意: dictht后面为NULL表示 第二个ht 还没有用到, dictEntry*后面是NULL说明没有hash到这个地址, dictEntry后面为NULL说明没有发生哈希冲突)
Hash 的 value 只能是字符串, 不能嵌套其他类型
**Q1: Hash 和 String 的区别 **
1.将所有相关的值聚集到一个Key中, 节省内存空间
2.批量获取对象的值时, 只需要使用一个命令, 减少内存/CPU/IO 的消耗
3.Field不能设置过期时间
4.数据量的分布
Q2: 什么时候用ziplist存储?
当hash对象同时满足以下两个条件时候, 采用ziplist编码 (redis.conf可以配置)
a. 哈希对象保存的键值对数量 < 512;
b.所有的键值对的键和值的字符串长度 < 64 byte
如果超过两个阈值的任何一个, 存储结构会转为hashtable
Q3: 为什么要定义两个哈希表, 而其中一个不用呢
redis的hash默认使用ht[0], ht[1]不会初始化和分配空间
哈希表dictht是使用链地址法来解决哈希冲突问题,这种情况下, 哈希表的性能取决于他的大小和保存节点数量之间的比率.
如果单个哈希表的节点数量过多, 哈希表的大小需要扩容rehash:
1.为字符ht[1]哈希表分配空间. ht[1]的大小为第一个大于等于ht[0].used * 2 的 2 的N次方幂
2.将所有的ht[0]上的节点rehash到ht[1]上, 重新计算hash值和索引, 然后放入到指定的位置
3.当ht[0]全部迁移到ht[1]后, 释放ht[0]的空间, 将ht[1]设置为ht[0], 并创建新的ht[1], 为下次rehash做准备
(3)应用场景
购物车的缓存设计
List类型
(1)存储类型
存储有序的字符串, 元素可以重复
(2)底层原理
早期版本中, 数据量较小时用ziplist存储(特殊编码的双向链表), 达到临界值转为linkedlist进行存储; 3.2版本统一使用quicklist来存储(数组+链表), quicklist存储了一个双向链表, 每个节点都是一个ziplist
(3)应用场景
列表
例如用户的消息列表, 网站公告列表, 活动列表, 博客的文章列表, 评论列表: 存储所有字段, lrange 取出一页
队列/栈
当作分布式环境中的队列或栈使用, 提供阻塞的弹出操作(可设置超时时间),
Set类型
(1)存储类型
存储String类型的无序集合
(2)底层原理
使用inset或hashtable存储set, 如果元素是整数类型, 就是inset存储; 不是整数类型的话 采用hashtable存储. 不过元素个数操作512个, 也会使用hashtable存储
(3)应用场景
抽奖
随机获取元素
点赞, 签到, 打卡
用户点赞的维护, 点赞数, 取消点赞等
商品标签
商品的删选, sinter sdiff
用户关注, 推荐模型
相互关注,我关注的人, 可能认识的人
ZSet类型
(1)存储类型
存储有序元素, 每个元素有一个score
(2)存储原理
默认使用ziplist编码, 在ziplist内部, 按照score排序递增来存储, 插入时需要移动后面的数据, 如果元素大于等于128个, 任一member 长度大于等于64则使用skiplist + dict存储.
(3)应用场景
排行榜
BitMaps
位图是定义在字符串类型上的位操作, bit存储数据, 节省空间, 用来做大数据统计, 例如在线用户统计, 留存用户统计等
Hyperloglogs
提供一种不太准确的基数统计方法, 用来统计一个集合中不重复的元素个数, 如统计用户的UV, 应用的日活, 月活
3.总结
3.1 数据结构
对象 | type 属性 | command | 底层存储结构 | object encoding |
---|---|---|---|---|
字符串 | OBJ_STRING | “string” | OBJ_ENCODING_INT OBJ_ENCODING_EMBSTR OBJ_ENCODING_RAW | int embstr raw |
列表 | OBJ_LIST | “list” | OBJ_ENCODING_QUICKLIST | quicklist |
哈希 | OBJ_HASH | “hash” | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_HT | ziplist hashtable |
集合 | OBJ_SET | “set” | OBJ_ENCODING_INTSET OBJ_ENCODING_HT | intset hashtable |
有序集合 | OBJ_ZSET | “zset” | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_SKIPLIST | ziplist skiplist + hashtable |
3.2 编码转换
对象 | 原始编码 | 升级编码 | |
---|---|---|---|
字符串 | INT 整数且小于 long 型 | embstr 超过 44 字节, 被修改 | raw |
哈希 | ziplist K-V的长度小于64byte 且 元素个数<=512 | hashtable | |
列表 | quicklist | ||
集合 | intset 元素均为整型类型 且个数 < 512个 | hashtable | |
有序集合 | ziplis 元素数量不超过128个 且member的长度小于64byte | skiplist |