redis:数据结构底层解析

前言

本文章内容是根据JavaGuide的文档内容,根据自己的理解写下的文档,主要是为了自己学习记忆.

5种基础数据结构

Redis是遵循ANSI 标准使用C语言书写的.

1. String
(1)String实现

字符串,redis在字符串中做了极致优化,根据存储的内容来使用不同的结构来存储.在Redis中叫Simple Dynamic String,底层都是通过字符数组实现的.

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

为什么不使用C语言的字符串而自己定义字符串?

  • 有三个原因
    • C语言数据存储字符串,在后续的操作中很可能出现越界溢出问题.
    • 获取字符串长度为O(n)操作:C语言不保存字符串长度,如果要获取需要遍历整个字符串数组.
    • C语言只能保存文本字符串,出现’\0’就会结束读取.
(2) String常见命令
//基本获取
SET key Value
GET key

//判断是否存在key
EXISTS key
DEL key

//批量设置
MSET key1 value1 key2 value2
MGET key1 key2
 
//设置存活时间
EXPIRE key 5

//等价于 SET + EXPIRE 的 SETEX 命令:
SETEX key 5 value 
 
//key不存在才设置成功
SETNX key value

//设置value自增,前提value是整数
//增加1
INCR key
//增加任意整数,n为任意整数
INCRBY key n 
2. List

在Redis中list通过双向链表实现的,也就是说可以轻松的实现增删.通过C语言中双向指针实现.

(1) list实现
/* Node, List, and Iterator are the only data structures used currently. */

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

在这里插入图片描述
可以看到每一个节点都具有指向前后节点的指针,同时在Redis创建的结构List中还具备指向头指针和尾指针,具备list长度.操作起来非常方便.
虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 list 结构来持有链表的话,操作起来会更加方便:
在这里插入图片描述

(2) List基本命令
//自定义left是list的头部方向,right是尾部的方向,所有命令上添加L或者R

//向list头部添加数值
RPUSH key value
//向尾部添加
LPUSH key value

//取出
LPOP key value
RPOP key value

//获取一定范围的数值,表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有
LRANGE key 0 -1 

//根据索引获取值
LINDEX key 0

由于List的数据结构可以很简单模拟出队列和栈的操作.

3. hash

提到hash第一时间想到Hash冲突,在Redis中通过拉链法解决Hash冲突,也就是通过数组+链表的方式.

(1) hash实现
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    // 内部有两个 dictht 结构
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

上面dictht 是链表数组,dictEntry 中每个节点,dict 用来管理,之中有两个链表数组,这个是实现做渐进式扩容或者缩容准备的.具体实现过程需要研究,比如界限,初始化大小设置

  • 大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个 O(n) 级别的操作,作为单线程的 Redis 很难承受这样耗时的过程,所以 Redis 使用 渐进式 rehash 小步搬迁.(就是在向旧的表中添加数据的同时,也会向新的表中添加数据)
    在这里插入图片描述
  • 渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,如上图所示,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。
  • 扩缩容的条件 正常情况下,当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
    当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave。
(2) hash基本命令
> HSET books java "think in java"    # 命令行的字符串如果包含空格则需要使用引号包裹
(integer) 1
> HSET books python "python cookbook"
(integer) 1
> HGETALL books    # key 和 value 间隔出现
1) "java"
2) "think in java"
3) "python"
4) "python cookbook"
> HGET books java
"think in java"
> HSET books java "head first java"  
(integer) 0        # 因为是更新操作,所以返回 0
> HMSET books java "effetive  java" python "learning python"    # 批量操作
OK

4. Set

跟java中的set一样,Set可以实现去重,无序

(1) Set实现

Redis中的Set是通过HashMap的key实现的,Set中的所有值都会被放到Set内部的HashMap结构的key中,存在这个KEY就无法插入,每个HashMap的Value都是null.

(2) Set基本命令
//value不能重复
SADD key value

//遍历出所有的value
SMEMBERS books  

//判断value是否存在
SISMEMBER  key Value

//获取Set长度
SCARD key

//取出一个值
SPOP key 
5. Zset

Zset是在set的基础上给每一个Value服务权重,这个是通过跳跃表实现的.
在跳跃表中,在最底层添加数据,然后概率提升层数,如果概率命中,就在上一层添加,同时打通上下层的联系,可以通过上层元素直接定位到下层元素.
在这里插入图片描述

(1) Zset实现

可以查看我写的数据接口跳跃表理解.

(2) Zset基本命令
> ZADD books 9.0 "think in java"
> ZADD books 8.9 "java concurrency"
> ZADD books 8.6 "java cookbook"

> ZRANGE books 0 -1     # 按 score 排序列出,参数区间为排名范围
1) "java cookbook"
2) "java concurrency"
3) "think in java"

> ZREVRANGE books 0 -1  # 按 score 逆序列出,参数区间为排名范围
1) "think in java"
2) "java concurrency"
3) "java cookbook"

> ZCARD books           # 相当于 count()
(integer) 3

> ZSCORE books "java concurrency"   # 获取指定 value 的 score
"8.9000000000000004"                # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题

> ZRANK books "java concurrency"    # 排名
(integer) 1

> ZRANGEBYSCORE books 0 8.91        # 根据分值区间遍历 zset
1) "java cookbook"
2) "java concurrency"

> ZRANGEBYSCORE books -inf 8.91 withscores  # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"

> ZREM books "java concurrency"             # 删除 value
(integer) 1
> ZRANGE books 0 -1
1) "java cookbook"
2) "think in java"

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝多芬也爱敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值