前言
最近面试经常被问到redis常用的数据类型,以及使用场景。这次对redis做一个简单的总结,主要基于Redis5.0若有不足之处还望诸位多多指正。
Redis对象由结构体redisObject表示
typedef struct redisObject {
unsigned type:4;//对象的类型
unsigned encoding:4;//节省空间,采用不同的存储方式
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;//// 引用计数
void *ptr;
} robj;
/* The actual Redis Object 常用的5种基本数据类型*/
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
一、String:
基础
- 特点
OBJ_ENCODING_INT、OBJ_ENCODING_EMBSTR 和 OBJ_ENCODING_RAW - 常用命令:
set,get,decr,incr,mget
使用场景
- 首页数据缓存:访问频次高缓存首页数据,分页从数据库查。
- 数据缓存:缓存不经常变动的序列化对象。
- 数据统计:计数器,统计请求数量,分表操作主键id。
- 时间内限制请求次数:请求限流,短信验证码。
- 分布式session:多用户登录存储token。
- 临时开关:上线临时修数据接口,使用string作为开关。
- 分布式锁。
注意事项
- 大小控制在 10KB 以下
- String、Set:尽可能存储int类型数据,字符串长度小于20且可以解析为整数使用int。
- string上限为Long.MAX_VALUE,9223372036854775807,注意这个特性。
实现原理
源码地址: sds
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 字符串的真正长度,不包含NULL结束符*/
uint64_t alloc; /*字符串的最大容量,不包含最后多余的那个字节*/
unsigned char flags; /*占用一个字节。其中的最低3个bit用来表示header的5种类型 */
char buf[];
};
struct sdshdr定义简单动态字符串(SDS):
- 常数复杂度获取字符串长度。
- 减少修改字符串长度时所需要的内存重新分配次数。空间预分配,SDS的长度小于1MB时,free和len的长度一致;大于1MB时,会分配1MB的未使用空间;惰性空间释放,不立即回收多出来的字节,利用flags来记录回收的字节数。
- 二进制安全,buf保存二进制文;杜绝缓冲区溢出;兼容部分c字符串函数。
二、List
基础
- 特点: 元素有序可重复。
- 常用命令:
lpush,rpush,lpop,rpop,lrange等。
使用场景
- 有序集合:接受mq发送的消息放入list,批量操作数据库,Excel批量读取消息放入list,批量操作数据库。
- 消息队列:将Redis用作日志收集器,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。(不如mq)
- 消息列表:lpush+brpop,产者客户端使用lrpush从列表左插入元素,多个消费者客户端使用brpop消费元素。(不如mq)
注意事项
- 3.2以前尽量元素数量小于512且每个元素小于64byte,防止向linkedlist转化,增加时间复杂度。
- 元素数量控制在 1 万以下。
- 不确定长度情况下,禁止使用
LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1 - 删除元素时间复杂度为 O(N),长度很大时应分批次删除。
实现原理
- quicklist
- 版本以后quicklist,可以认为是双向链表+压缩表;3.2之前使用ziplist和linkedlist
- uickList就是一个标准的双向链表的配置,有head有tail;每一个节点是一个quicklistNode(含一个ziplist,prev和next指针)。
源码地址:quicklist
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
...
} quicklistNode;
2. ziplist
源码地址:ziplist
ziplist的内存结构:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
NOTE: all fields are stored in little endian, if not specified otherwise.
ziplist的数据类型不是struct,是连续内存,每个entry可以是字节数组或整数值,无法用固定数据类型表达;添加或删除节点可能或引起连锁更新操作,但是几率不高。
三、Hash
基础
- 特点:键值对,键不能重复,hash结构中一个key可以为null。
- 常用命令:
hset,hget,hmset,hmget,hgetall,hexists。
使用场景
- 存储对象:redis的Key用户ID, map也就是用户属性,适用于频繁修改。
- 主从管理(一对多):店主管理多个店员,redis的key为店主,map中多个店员。
- 垂直分表:redis的key为用户id,map的field为表名,value为序列化的对象。
- 购物车:电商系统中,存放用户购物车内容。
注意事项
- 存储对象与string+json相比:消耗内存和cpu更小,也更节省内存,易操作单个属性。
- 过期时间无法作用在field上,集群架构下不适合大规模使用。
- 不确定长度情况下,禁止使用LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1
- 删除元素时间复杂度为 O(N),长度很大时应分批次删除。
- Hash元素数量控制在转换阈值之下,以压缩列表存储,节约内存最大不超过1w。
实现原理
- 使用ziplist和dick完成,Hash的数目超过512也就是ziplist的个数超过1024。当ziplist数据量很大时,查找时间复杂度O(N)喜人,连续内存空间拷贝也会很惊人。
- hash表作为dict的底层实现,hash表使用头插法单向链表解决地址冲突。
- 对hash表扩容或收缩通过渐进式rehash,ht[0]和ht[1]两个hash表,ht[0]平时使用,ht[1]是在rehash使用。
- 扩容操作:ht[1]的大小为第一个大于等于ht[0].used*2的2的整数幂。
- 缩容操作:ht[1]的大小为第一个大于等于ht[0].used的2的整数幂。
- dick
源码地址:dict
-
使用dict作为底层实现而dict使用hash实现,非常类似于java的hashmap。
-
hash表作为字典的底层实现,每个字典有两个hash表,一个平时使用一个rehash时使用。
-
hash表使用头插法单向链表解决地址冲突,对hash表进行扩容或收缩的时候,通过渐进式rehash到新的hash表完成。
typedef struct dictht { dictEntry **table;// 存放key value的entry unsigned long size;// hashtable的容量 unsigned long sizemask; unsigned long used;// hashtable的元素个数 } dictht;
四、set
基础
- 特点:元素不可重复且无序,可进行交集、并集、差集操作。
- 常用命令: sadd,srem,smembers,sismember,sdiff,sinter,sunion。
使用场景
- 集合操作:兴趣标签,相同爱好,共同好友。
- 统计ip:统计网站的独立IP。
- 一对多关联关系:店铺和会员关系。redis的key为店铺id,set中的值为会员id,判断会员是否存在。
- 抽奖,随机事件(不多)。
注意事项
- Set 会采用整数编码int,降低内存消耗,当元素不为整数或个数大于512转化为hashtable。
- set的时间复杂度O(N),元素数量控制在 1 万以下。
- 元素个数未知的情况下,分批删除,禁止
LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1。
实现原理
- IntSet只含有整数并且元素不多,有序的整型数组
源码地址:Inset
typedef struct intset {
uint32_t encoding;// 编码方式
uint32_t length;// 集合包含的元素数量
int8_t contents[];// 保存元素的数组
} intset;
2. dick,value为null 见hash结构。
五、sortedset
基础
- 特点:成员是唯一的,但分数(score)却可以重复
- 常用命令:
zadd, zrange,zrevrange,zrem
使用场景
- 排行:根据时间排序的新闻列表,阅读排行榜,商品热度,首页banner广告展示顺序
- 延时队列:时间戳score排名,消费者取第一个值。
注意事项
元素数量小于128个,所有元素长度小于60字节使用ziplist,尽量在转化之内
实现原理
- ziplist内的集合元素按score从小到大进行排序,分值较小的元素被放置在靠近表头的位置,分值较大的则放置在靠近表尾的位置。
- 跳表是随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。
- 关于跳表,可以参考张铁蕾的 Redis内部数据结构详解(6)——skiplist
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
Redis为什么用skiplist而不用平衡树?
-
skiplist比平衡树算法算法时间简单,ZRANK操作还能达到 O(logn)的时间复杂度。
-
有序集合经常会进行ZRANGE和ZREVRANGE这样的范围查找操作,平衡树比skiplist操作要复杂,跳表内部的双向链表更方便。
-
插入和删除操作可能引发平衡树子树的调整,逻辑复杂;而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
-
从内存占用上来说,skiplist比平衡树更灵活一些。