很哇塞的Redis(一)基础数据类型

307 阅读8分钟

前言

最近面试经常被问到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. */

整体结构.jpg

一、String:

基础
  1. 特点OBJ_ENCODING_INT、OBJ_ENCODING_EMBSTR 和 OBJ_ENCODING_RAW
  2. 常用命令: set,get,decr,incr,mget
使用场景
  1. 首页数据缓存:访问频次高缓存首页数据,分页从数据库查。
  2. 数据缓存:缓存不经常变动的序列化对象。
  3. 数据统计:计数器,统计请求数量,分表操作主键id。
  4. 时间内限制请求次数:请求限流,短信验证码。
  5. 分布式session:多用户登录存储token。
  6. 临时开关:上线临时修数据接口,使用string作为开关。
  7. 分布式锁。
注意事项
  1. 大小控制在 10KB 以下
  2. String、Set:尽可能存储int类型数据,字符串长度小于20且可以解析为整数使用int。
  3. 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):

  1. 常数复杂度获取字符串长度。
  2. 减少修改字符串长度时所需要的内存重新分配次数。空间预分配,SDS的长度小于1MB时,free和len的长度一致;大于1MB时,会分配1MB的未使用空间;惰性空间释放,不立即回收多出来的字节,利用flags来记录回收的字节数。
  3. 二进制安全,buf保存二进制文;杜绝缓冲区溢出;兼容部分c字符串函数。

sds结构.jpg

二、List

基础
  1. 特点: 元素有序可重复。
  2. 常用命令: lpush,rpush,lpop,rpop,lrange等。
使用场景
  1. 有序集合:接受mq发送的消息放入list,批量操作数据库,Excel批量读取消息放入list,批量操作数据库。
  2. 消息队列:将Redis用作日志收集器,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。(不如mq)
  3. 消息列表:lpush+brpop,产者客户端使用lrpush从列表左插入元素,多个消费者客户端使用brpop消费元素。(不如mq)
注意事项
  1. 3.2以前尽量元素数量小于512且每个元素小于64byte,防止向linkedlist转化,增加时间复杂度。
  2. 元素数量控制在 1 万以下。
  3. 不确定长度情况下,禁止使用LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1
  4. 删除元素时间复杂度为 O(N),长度很大时应分批次删除。
实现原理
  1. 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;

quicklist.jpg 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

基础
  1. 特点:键值对,键不能重复,hash结构中一个key可以为null。
  2. 常用命令:hset,hget,hmset,hmget,hgetall,hexists
使用场景
  1. 存储对象:redis的Key用户ID, map也就是用户属性,适用于频繁修改。
  2. 主从管理(一对多):店主管理多个店员,redis的key为店主,map中多个店员。
  3. 垂直分表:redis的key为用户id,map的field为表名,value为序列化的对象。
  4. 购物车:电商系统中,存放用户购物车内容。
注意事项
  1. 存储对象与string+json相比:消耗内存和cpu更小,也更节省内存,易操作单个属性。
  2. 过期时间无法作用在field上,集群架构下不适合大规模使用。
  3. 不确定长度情况下,禁止使用LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1
  4. 删除元素时间复杂度为 O(N),长度很大时应分批次删除。
  5. Hash元素数量控制在转换阈值之下,以压缩列表存储,节约内存最大不超过1w。
实现原理
  1. 使用ziplist和dick完成,Hash的数目超过512也就是ziplist的个数超过1024。当ziplist数据量很大时,查找时间复杂度O(N)喜人,连续内存空间拷贝也会很惊人。
  2. hash表作为dict的底层实现,hash表使用头插法单向链表解决地址冲突。
  3. 对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的整数幂。
  1. 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;
    

dict.jpg

四、set

基础
  1. 特点:元素不可重复且无序,可进行交集、并集、差集操作。
  2. 常用命令: sadd,srem,smembers,sismember,sdiff,sinter,sunion。
使用场景
  1. 集合操作:兴趣标签,相同爱好,共同好友。
  2. 统计ip:统计网站的独立IP。
  3. 一对多关联关系:店铺和会员关系。redis的key为店铺id,set中的值为会员id,判断会员是否存在。
  4. 抽奖,随机事件(不多)。
注意事项
  1. Set 会采用整数编码int,降低内存消耗,当元素不为整数或个数大于512转化为hashtable。
  2. set的时间复杂度O(N),元素数量控制在 1 万以下。
  3. 元素个数未知的情况下,分批删除,禁止LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1
实现原理
  1. IntSet只含有整数并且元素不多,有序的整型数组

源码地址:Inset

 typedef struct intset {	    
    uint32_t encoding;// 编码方式
    uint32_t length;// 集合包含的元素数量
    int8_t contents[];// 保存元素的数组
} intset;

2. dick,value为null 见hash结构。

五、sortedset

基础
  1. 特点:成员是唯一的,但分数(score)却可以重复
  2. 常用命令:zadd, zrange,zrevrange,zrem
使用场景
  1. 排行:根据时间排序的新闻列表,阅读排行榜,商品热度,首页banner广告展示顺序
  2. 延时队列:时间戳score排名,消费者取第一个值。
注意事项

元素数量小于128个,所有元素长度小于60字节使用ziplist,尽量在转化之内

实现原理

源码地址:zsetskiplist

  1. ziplist内的集合元素按score从小到大进行排序,分值较小的元素被放置在靠近表头的位置,分值较大的则放置在靠近表尾的位置。
  2. 跳表是随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。
  3. 关于跳表,可以参考张铁蕾的 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;

zset.jpg Redis为什么用skiplist而不用平衡树?

  • skiplist比平衡树算法算法时间简单,ZRANK操作还能达到 O(logn)的时间复杂度。

  • 有序集合经常会进行ZRANGE和ZREVRANGE这样的范围查找操作,平衡树比skiplist操作要复杂,跳表内部的双向链表更方便。

  • 插入和删除操作可能引发平衡树子树的调整,逻辑复杂;而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

  • 从内存占用上来说,skiplist比平衡树更灵活一些。