Redis单机架构(一)

目录

1、Redis入门

1.1、Redis诞生历程

1.1.1、从一个故事开始

1.2、Redis的定位与特性

1.2.1、SQL、NoSQL与NewSQL

                  1.2.2、Redis特性

1.3、Redis的数据模型

​编辑

 1.4、Redis中的数据类型

1.4.1、String类型

1.4.2、Hash类型

1.4.3、List

1.4.4、Set

1.4.5、ZSet 有序集合

1.4.6、其他数据结构

1.4.7、总结


1、Redis入门

1.1、Redis诞生历程

1.1.1、从一个故事开始

        Redis的作者笔名叫antirez,2008年的时候他做了一个记录网站访问情况的系统,比如每天有多少个用户、多少个页面被浏览、访客的IP、操作系统、浏览器、使用的搜索关键词等等(跟百度统计CNZZ功能一样。 最开始存储方案用MySQL, 但是实在慢得不行,09 年的时候 antirez 就自己写了一个内存的 List, 这个就是 Redis。

早期list

        最开始 Redis 只支持 List。现在数据类型丰富了、 功能也丰富了,在全世界都非常流行。

        为什么叫 REDIS 呢?它的全称是 REmote Dictionary Service, 直接翻译过来是远程字典服务。

        

1.2、Redis的定位与特性

1.2.1、SQL、NoSQL与NewSQL

        在绝大部分时候,我们都会首先考虑用关系型数据库来存储业务数据,比如 SQLServer、 Oracle,、MySQL等等。

        关系型数据库的特点:

        1、它以表格的形式,基于行存储数据,是一个二维的模式。

        2、它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构。

        3、表与表之间存在关联(Relationship)。

        4、大部分关系型数据库都支持SQL (结构化查询语言)的操作,支持复杂的关联查询。

        5、通过支持事务(ACID酸)来提供严格或者实时的数据一致性。

        关系型数据库也存在一些限制,比如:

        1、要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据的存储,就要扩大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实现,比如分库分表。

        2、表结构修改困难,因此存储的数据格式也受到限制。

        3、关系型数据库通常会把数据持久化到磁盘,在高并发和高数据量的情况下, 基于磁盘的读写压力比较大。

        为了规避关系型数据库的一系列问题,我们就有了非关系型的数据库,我们一般把它叫做"non-relational"或者 "Not Only SQL"。NoSQL 最开始是不提供 SQL(Structured Query Language结构化查询语言)的数据库的意思,但是后来意思慢慢地发生了变化。

        非关系型数据库的特点:

        1、存储非结构化的数据,比如文本、图片、音频、视频。

        2、表与表之间没有关联,可扩展性强。

        3、保证数据的最终一致性,遵循BASE (碱)理论。

        Basically Available (基本可用);Soft-state(软状态);Eventually Consistent (最终一致性)。

        4、支持海量数据的存储和高并发的高效读写。

        5、支持分布式,能够对数据进行分片存储,扩缩容简单。

        对于不同的存储类型,我们又有各种各样的非关系型数据库,比如有几种常见的类型:

        1、KV存储:Redis 和 Memcached。

        2、文档存储:MongoDBo

        3、列存储:HBaseo

        4、图存储:Neo4j

        5、对象存储。

        6、XML存储。

        7、等等等等。

        这个网站列举了各种各样的NoSQL数据库 NoSQL Databases List by Hosting Data - Updated 2022

        能不能把SQL和NoSQL的特性结合在一起呢?当然可以。所以现在又有了所谓的NewSQL数据库。

        NewSQL 是对各种新的可扩展/高性能数据库的简称,这类数据库不仅具有NoSQL对海量数据的存储管理能力,还保持了传统数据库支持ACIDSQL等特性。

        NewSQL是指这样一类新式的关系型数据库管理系统,针对OLTP(读-写)工作负载,追求提供和NoSQL系统相同的扩展性能,且仍然保持ACID和SQL等特性(scalable and ACID and (relational and/or sql -access))。

        NewSQL 结合了 SQL和 NoSQL 的特性。例如 TiDB (PingCAP)、VoltDB、ScaleDB等。

特性SQLNoSQLNewSQL

关系模型

×

SQL语法

×

ACID

×

水平扩展

×

海量数据

×

无结构化

×v×

1.2.2、Redis特性

速度快

        Redis所有数据是存放在内存中的,

        Redis源代码采用C语言编写,距离底层操作系统更近,执行速度相对更快,

        Redis处理请求,避免了多线程可能产生的竞争开销,

基于K_V的数据结构

功能相对丰富

        Redis对外提供了键过期的功能,可以用来实现缓存,

        提供了发布订阅功能,可以用来实现简单的消息系统,解耦业务代码,

        支持Lua脚本,

        提供了简单的事务功能(不能rollback),

        提供了Pipeline功能,客户端能够将一批命令一次性传输到Server端,减少了网络开销。

简单稳定

        Redis源码共六万行,但是不代表它不稳定

客户端语言多

        Redis提供了简单的TCP通信协议,这样使得很多编程语言可以很方便的接入Redis

持久化

        Redis提供两种持久化方案AOF和RDB

主从复制

高可用和分布式

        Redis从2.8版本正式提供了高可用实现哨兵模式,可以保证Redis节点的故障发现和故障自动转移,

        Redis从3.0版本后开始支持集群模式

1.3、Redis的数据模型

        Redis 默认有 16 个库 (0-15) 。 可以在配置文件 redis.conf 中修改。

        因为没有完全隔离,不像数据库的 database, 不适合把不同的库分配给不同的业务使用。 默认使用第一个db0。在集群中只能使用第一个db。

        Redis是KV的数据库,Key-Value我们一般会用哈希表来存储它。 Redis的最外层是通过hashtable实现的(我们把这个叫做外层的哈希)。

         Redis 里面,这个哈希表怎么实现呢?可以查看一下C语言的源码(dict.h 47行),

        每个键值对都是一个dictEntry, 通过指针指向 key 的存储结构和 value 的存储结构, 而且 next 存储了指向下一个键值对的指针。

typedef struct dictEntry {
    void *key;    /* key 关键字定义 */
    union {
        void *val; /* value 定义 会指向一个redisObject*/
        uint64_t u64;
        int64_t s64; 
        double d;
    } v;
    struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry ;

        实际上最外层是 redis Db, redisDb 里面放的是 diet。源码server.h 661行

typedef struct redisDb {
    diet *diet;            /* 所有的键值对 */
    diet *expires;         /* 设置了过期时间的键值对 */
    diet *bloeking_keys;   /* 客户端等待访问密钥 */
    diet *ready _keys;     /* 接收PUSH的被阻止密钥 */
    diet *watched_keys;    /* WATCHED keys for MULTI/EXEC CAS */
    int id;                /* Database ID */ 
    long long avg_ttl;      /* Average TTL, just for stats */
    unsigned long expires_ cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;     /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

        以 set hello word 为例, 因为 key 是字符串, Redis 自己实现了一个字符串类型SDS,所以 hello 指向一个SDS结构,后面会对SDS的解释。

        value 存储一个字符串的时候,Redis并没有直接使用SDS存储,而是存储在RedisObj中。实际上五种常用的数据类型的任何一种的 value, 都是通过 redisObject来存储的。

        最终 redisObject 再通过一个指针指向实际的数据结构, 比如字符串或者其他。

        redisObject源码 server.h 622行

typedef struct redisObject {
    unsigned type:4; /* 对象的类型,包括: OBJ STRING、 OBJ LIST、 OBJ HASH、 OBJ SET、 OBJ ZSET */
    unsigned enco山ng:4; /* 具体的数据结构 */
    unsigned lru:LRU BITS; /* 24位, 对象最后 次被命令程序访问的时间, 与内存回收有关 */
    int refcount; /* 引用计数。 当refcount为0 的时候, 表示该对象已经不被任何对象引用, 则可以进行回收 */
    void *ptr; /* 指向对象实际的数据结构 */
} robj;

 1.4、Redis中的数据类型

        Redis中有9种数据结构:String、Hash、List、Set、Zset、BitMap、Geo、Hyperloglogs、Streams。
       其中常用的有:String、Hash、List、Set、Zset。

1.4.1、String类型

        String类型主要存储三种类型的数据:int、float、String

操作命令:

#获取指定范围的字符
getrange [key] 0 1 
#获取值长度
strlen [key]
#字符串追加内容
append [key] [value] 
#设置多个值(批量操作,原子性)
mset [key] [value] [key] [value] 
#获取多个值
mget [key] [key] 
#设置值, 如果 key 存在, 则不成功
setnx [key] [value]
#基于此可实现分布式锁。 用 del key 释放锁。
#但如果释放锁的操作失败了, 导致其他节点永远获取不到锁, 怎么办?
#加过期时间。 单独用 expire加过期, 也失败了, 无法保证原子性, 怎么办?

#可使用多参数命令

set [key] [value] [expiration EX seconds | PX milliseconds] [NXIXX] 
#使用参数的方式
set kl vl EX 10 NX 
# (整数)值递增(值不存在会得到I)
incr [key]
incrby [key] 100 
# (整数)值递减

deer [key]
decrby [key]100

#浮点数增量
set mf 2.6 
incrbyfloat mf 7.3 

       

实现原理:

        Redis并没有使用C语言的字符数组实现字符串,而是自己实现了一个SDS(Simple Dynamic String)简单动态字符串的结构。

        源码:sds.h 47行

struct __attribute__((__packed__)) sdshdr8 {
    uint8_t len;         /* 当前字符数组的长度 */
    uint8_t alloc;       /* 当前字符数组总共分配的内存大小 */
    unsigned char flags; /* 当前字符数组的属性,用来标识到底是 sdshdr8 还是 sdshdrl6 等 */
    char buf[];          /* 字符串真正的值 */

         其本质上还是字符串数组

        为什么Redis要用SDS实现字符串?

        我们知道, 因为 C 语言本身没有字符串类型, 只能用字符数组char[]实现,但这样会有很多问题。

        1、使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。

        2、如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。

        3、字符串长度的变更会对字符数组做内存重分配。

        4、通过从字符串开始到结尾碰到的第—个 \0'来标记字符串的结束 ,因此不能保存图片音频视频压缩文件等二进制(bytes)保存的内容 , 二进制不安全。

        SDS 的特点:

        1、不用担心内存溢出问题, 如果需要会对 SDS 进行扩容。

        2、获取字符串长度时间复杂度为 0(1), 因为定义了len 属性。

        3、通过“空间预分配” (sdsMakeRoomFor) 和“惰性空间释放” ,防止多次重分配内存。

        4、判断是否结束的标志是 len 属性 , 可以包含\0'(它同样以'\0'结尾是因为这样就可以使用C语言中函数库操作字符串的函数了)。

        

        SDS有多种结构 :

        sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表sdshdr5:2^5=32byte,sdshdr8:2^8=256byte ,sdshdr16:2^16=65536byte=64KB,sdshdr32:2^32=4GB;

        SDS有三种编码:

        int:存储8个字节的长整型(long, 2^63-1);

        embstr:代表embstr格式的SDS,存储小于44个字节的字符串;

        raw:存储大于44个字节的字符串。


        SDS编码转换条件:

        当int数据不再是整数是则变为raw,int数据大小超过了long的范围则变为embster,embster长度超过44个字节则会变为raw。转换过程是不可逆的。

        embstr编码和raw编码的区别:

        embstr编码只会分配一次内存,RedisObject和SDS内存是连续的。raw会分配两次内存空间,RedisObject和SDS内存是不连续。
        embstr编码是只读的,如果更改其内容则会变为raw编码,因为如果改变embstr的内容,会对RedisObject和SDS都进行内存分配。

        

使用场景:

        可用来缓存一些热点数据:如网页首页的数据看板,或红点提醒、验证码、token、分布式锁等业务不重的数据。

1.4.2、Hash类型

        Hash用来存储多个无序的键值对。 最大存储数量2A 32-1 (40亿左右)

        一个key对应多个键值对数据,value只能存储字符类型。String类型可以做的,hash都能做,另外还可以存储对象。

        

         注意:

        1、前面说Redis所有的KV本身就是键值对, 用dietEntry实现的, 叫做外层的哈希。现在讲的是内层的哈希。

        2、Hash的value只能是字符串,不能嵌套其他类型,比如hash或者Iist。

        同样是存储字符串, Hash与Sting的区别:

        1、所有相关的值聚集到—个key中, 节省内存空间。

        2、只使用—个key, 减少key冲突。

        3、当需要批量获取值的时候, 只需要使用—个命令, 减少内存/IO/CPU 的消耗。

        Hash不适合的场景:

        1、Field不能单独设置过期时间

        2、需要考虑数据量分布的问题 (field 非常多的时候, 无法分布到多个节点)

操作命令:

hset hl f 6 
hset hl e 5 
hmset h 1 a 1 b 2 c 3 d 4

hget hl a 
hmget h 1 a b c d hkeys hl 
hvals hl 
hgetall hl 

实现原理:

        内层的哈希底层可以使用两种数据结构实现:

ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)

hashtable:OBJ_ ENCOD-1 NG_ HT (哈希表)

命令

hdel hl a

hlen hl 

object encoding h2

object encoding h3

       

一、ziplist压缩列表

        ziplist是一个经过特殊编码的, 由连续内存块组成的双向链表。

        不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度当前节点长度。这样读写可能会慢一些, 因为你要去算长度, 但是可以节省内存,是一种时间换空间的思想。

 ziplist源码 ziplist.c 第16行:

typedef struct zlentry {
    unsigned int prevrawlensize;     /* 存储上一个链表节点的长度数值所需要的字节数 */
    unsigned int prevrawlen;         /* 上一个链表节点占用的长度 */
    unsigned int lensize;            /* 存储当前链表节点长度数值所需要的字节数 */
    unsigned int len;                /* 当前链表节点占用的长度 */
    unsigned int headersize;         /* 当前链表节点的头部大小 (prevrawlensize + lensize) , 即非数据域的大小 */
    unsigned char encoding;          /* 编码方式 */
    unsigned char *p;                /*压缩链表以字符串的形式保存, 该指针指向当前节点起始位置 */
} zlentry;

数据展开是这样的:

 ziplist有三种编码:

ZIP_STR_06B(0<<6)//长度小于等于63字节

ZIP_STR_04B(1<<6)//长度小于等于16383字节

ZIP_STR_32B(2<<6)//长度小于等于4294967295字节

 ziplist使用条件:

当hash对象同时满足以下两个条件的时候, 使用ziplist编码:

1)    哈希对象保存的键值对数量< 512个;
2)    所有的键值对的健和值的字符串长度都< 64byte (—个英文字母一个字节);

可通过配置rc/redis.conf修改

hash-max-ziplist-value 64 // ziplist 中最大能存放的值长度
hash-max-ziplist-entries 512 // ziplist 中最多能存放的 entry 节点数量

超过这两个阈值中的任何一个,存储结构都会转换为hashtable

二、hashtable

        在Redis中,hashtable被称为字典(dictionary) 。前面说过Redis的KV结构是通过—个dietEntry来实现的。而hashtable中,又对dietEntry进行了多层的封装。

        源码位置: dict.h 47 行

/* 首先有—个dictEntry */
typedef struct dictEntry {
    void *key;        /* key关键字定义 */
    union {
        void *val;    uint64_t_u64; /* value定义 */
        int64_t_s64;  double d; 
    } v;
    struct dictEntry *next;   /* 指向下一个键值对节点 */
} dictEntry;

/* diet Entry放到了dictht (hashtable里面) */
typedef struct dictht {
    dictEntry **table;         /* 哈希表数组 */
    unsigned long size;        /* 哈希表大小 */
    unsigned long sizemask;    /* 掩码大小,用于计算索引值。总是等于 size-I */
    unsigned long used;        /* 已有节点数 */
} dictht ;

/* ht放到了dict里面 */
typedef struct dict {
    dictType *type;         /* 字典类型 */
    void *privdata;         /* 私有数据 */
    dictht ht[2];           /* 一个字典有两个哈希表 */
    long rehashidx;         /* rehash 索引 */
    unsigned long iterators;/* 当前正在使用的迭代器数量 */
} dict;

        从最底层到最高层 dictEntry——dictht——dict。是一个数组加链表的结构。

        展开后哈希的整体结构为:

        

 注意: dictht 后面是 NULL 说明第二个 ht 还没用到。 dictEntry* 后面是 NULL 说明没有 hash 到这个地址。 dictEntry 后面是NULL 说明没有发生哈希冲突。

dict中包含了两个dictht哈希表,这种设计就是为了rehash,每个hashtable的默认初始长度为4。

rehash扩缩容时机:

1、扩容的条件:
正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做bgsave(持久化),为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。

2、缩容条件:当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考虑 Redis 是否正在做 bgsave。

static int diet_can_resize  = 1;        //是否需要扩容
static unsigned int dict_force_resize_ratio = 5;        //扩容因子

 rehash的步骤

        收缩或者扩展哈希表需要将ht[0]表中的所有键全部rehash到ht[1]中,但是rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的,这样才不会对服务器性能造成影响。

渐进式rehash:
1、为ht[1]分配空间,根据ht[0]所使用的空间大小,会算出一个最接近2n的realsize,然后进行扩展或收缩,比如ht[0]原来为500m,扩容时给ht[1]分配空间不是1000m,而是1024m。


2、将rehashindex的值设置为0,表示rehash工作正式开始。


3、在rehash期间,每次对字典执行增删改查操作时,都会判断是否正在进行rehash操作,如果是,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashindex索引上的所有键值对rehash到ht[1],当rehash工作完成以后,rehashindex的值+1。


4、随着字典操作的不断执行,最终会在某一时间段上ht[0]的所有键值对都会被rehash到ht[1],这时将rehashindex的值设置为-1,表示rehash操作结束。释放ht[0]的空间,将ht[1]设为ht[0],并创建新的ht[1]。

 Hash的使用场景

String可以做的,Hash都可以做,另外还可以存储对象

1.4.3、List

存储类型

存储有序的字符串(从左到右), 元素可以重复。 最大存储数量2A 32-1 (40 亿左右)。

 操作命令

元素增减:

lpush queue a

lpush queue b c 

rpush queue d e

lpop queue

rpop queue 

取值

lindex queue 0

lrange queue 0 -1

实现原理

早期的版本中, 数据量较小时用 zipl ist 存储(特殊编码的双向链表),达到临界值时转换为 linkedlist 进行存储, 分别对应 OBJ_ENCODING_ZIPLISTOBJ_ENCODING_LINKEDLIST

3.2 版本之后,统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点都是一个ziplist,所以是ziplist和linkedlist的结合体

一、quicklist

总体结构:

 quicklist.h 105行:

typedef struct quicklist {
    quicklistNode *head; /* 指向双向列表的表头*/
    quicklistNode *tail; /* 指向双向列表的表尾*/
    unsigned long count; /* 所有的 ziplist 中一共存了多少个元素 */
    unsigned long len; /* 双向链表的长度, node 的数量 */
    int fill : QL_FILL_BITS; /* ziplist 最大大小,对应list-max-ziplist-size */ 
    unsigne山nt compress : QL_C01\1P _BITS;/* 压缩深度,对应 list-compress-depth */
    unsigned血bookmark_count: QL_BM_BITS; /*4 位, bookmarks 数组的大小 */
    quicklistBookmark bookmarks[]; /*bookmarks 是一个可选字段, quicklist 重新分配内存空间时使用, 不使用时不占用空间 */
} quicklist;

 redis.conf 相关参数:

参数含义
list-max-ziplist-size(fill)

正数表示单个ziplist最多包含的entry个数。

负数代表单个ziplist的大小,默认8k。

1 : 4 KB; -2 : 8 KB; -3 : 16 KB ; -4: 3 2 KB; -5 : 64 KB

list-compress-depth(compress)

压缩深度,默认是0。

1: 首尾的ziplist 不压缩; 2: 首尾前两个ziplist不压缩,以此类推。

quicklist.h 46行:

typedef struct quicklistNode {
    struct quicklistNode *prev; /* 指向前一个节点 */
    struct quicklistNode *next; /* 指向后一个节点 */
    unsigned char *zl;          /* 指向实际的ziplist */ 
    unsigned int sz;            /* 当削 ziplist 占用多少字节 */ 
    unsigned int count: 16;     /* 当前 ziplist 中存储了多少个元素, 占 16bit (下同), 最大 65536个 */ 
    unsigned int encoding: 2;   /* 是否采用了LZF压缩算法压缩节点  RAW==1 or LZF==2 */ 
    unsigned int container: 2;  /* 2: ziplist, 未来可能支持其他结构存储 NONE==l or ZIPLIST==2 */ 
    unsigned int recompress:1;  /* 当前ziplist是不是已经被解压出来作临时使用 */ 
    unsigned int attempted_compress : 1; /* 测试用 */ 
} quicklistNode;

ziplist的结构前面已经分析,不再重复。

总结:quicklist是—个数组+链表的结构。

应用场景:

List 主要用在存储有序内容的场景。

1、用户的消息列表、 网站的公告列表、 活动列表、 博客的文章列表、 评论列表等。

2、队列/栈

List 提供了两个阻塞的弹出操作:BLPOP/BRPOP, 可以设置超时时间(单位:秒)。

BLPOP: BLPOP key1 timeout 移出并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

BRPOP: BRPOP key1 timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

 1.4.4、Set

存储类型:

Set 存储 String 类型的无序集合, 最大存储数量 2^32-1 (40 亿左右)。

 操作命令

// 添加一个或者多个元素
sadd myset a b c d e f g 
// 获取所有元素
smembers myset 
// 统计元素个数
scard myset 
// 随机获取一个元素

srandmember myset

// 随机弹出一个元素

spop myset

// 移除一个或多个元素

srem myset d e f

// 查看元素是否存在

sismember myset a

实现原理

 Redis用intset或hashtable存储set。如果元素都是整数类型,就用inset存储。

inset.h 35行:

typedef struct intset {
    uint32_t encoding;  /* 编码类型int16_t、int32_t、int64_t */
    uint32_t length;    /* 长度,最大长度2^32 */
    int8_t contents[];  /* 用来存储成员的动态数组 */
} intset;

如果不是整数类型, 就用 hashtable (数组+链表的存来储结构),value存null。

如果元素个数超过512个 , 也会用hashtable存储 。跟—个配置有关:

set-max-intset-entries 512;

 应用场景:‘

随机获取元素:spop myset

点赞、 签到、 打卡、用户关注、推荐模型

1.4.5、ZSet 有序集合

存储类型

sorted set存储有序的元素。 每个元素有个score, 按照score从小到大排名。

score相同时, 按照key的ASCII码排序。

 数据结构对比:

数据结构是否允许重复元素是否有序有序实现方式
list索引下标
set
zset分值 score

操作命令:

// 添加元素
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python 
// 获取全部元素
zrange myzset 0-1 withscores 
zrevrange myzset 0-1 withscores 
// 根据分值区间获取元素
zrangebyscore myzset 20 30
// 移除元素也可以根据 score rank 删除
zrem myzset php cpp 
// 统计元素个数
zcard myzset 
// 分值递增
zincrby myzset 5 python 
// 根据分值统计个数
zcount myzset 20 60
// 获取元素 rank
zrank myzset python 
// 获取元素 score
zscore myzset python 

// 也有倒序的 rev 操作 (reverse)

实现原理

默认使用 ziplist 编码(hash 的小编码,quicklist 的 Node,都是ziplist)。

在 ziplist 的内部, 按照 score 排序递增来存储。 插入的时候要移动之后的数据。

如果元素数量大于等于 128 个, 或者任一member 长度大于等于 64 字节使用skiplist+dict 存储。

zset-max-ziplist-entries 128

zset-max-ziplist-value 64

什么是skiplist(跳表)?

先看一下有序链表

 在这样一个链表中,如果要查找某个数据,那么需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点为止。时间复杂度伟O(n)。同样,当我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。 二分查找法只适用于有序数组, 不适用于链表。

假如我们每相邻两个节点增加一个指针, 让指针指向下下个节点(或者理解为有三个元素进入了第二层)。

这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。

哪些元素进入上一层取决一个算法源码:t_ zset.c 122 行:

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&OxFFFF) < (ZSKIPLIST_P * OxFFFF)) 
        level+= 1; 
        return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
} 

现在当我们想查找数据的时候,可以先沿着这个新链表进行查找。当碰到比待查数据大的节点时, 再到下一层进行查找。

 

 比如,我们想查找33,查找的路径是沿着标红的指针所指向的方向进行的:

1、33 首先和 5 比较,再和 22 比较,比它们都大,继续向后比较。

2、但 33 和 35 比较的时候,比 35 要小,因此回到上一个元素22,与下一层的 28 比较。

3、33 要比 28 大,继续向后与 35 比较,发现33要比35小,说明待查数33不在链表中。

在这个查找过程中, 由于新增加的指针,不再需要与链表中每个节点逐个进行比较。 需要比较的节点数大概只有原来的一半,这就是跳跃表。

因为 level 是随机的,得到的 skiplist 可能是这样的,有些在第四层,有些在第三层,有些在第二层,有些在第一层。

源码:server.h 904 行

typedef struct zskiplistNode {
    sds ele;         /* zset 的元素 */
    double score;    /* 分值 */
    struct zskiplistNode *backward;    /* 后退指针 */
    struct zskiplistLevel {
        struct zskiplistNode *forward; /*前进指针, 对应 level 的下一个节点 */
        unsigned long span; /*从当前节点到下一个节点的跨度(跨越的节点数) */
    } level[]; /* 层级 */
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; /* 指向跳跃表的头结点和尾节点 */
    unsigned long length;      /* 跳跃表的节点数 */
    int level;                 /* 最大的层数 */
} zskiplist;

typedef struct zset {
    diet *diet; 
    zskiplist *zsl;
} zset;

应用场景

顺序会动态变化的列表,如:排行榜。

1.4.6、其他数据结构

一、BitMaps

Bitmaps是在字符串类型上面定义的位操作。 —个字节由8个二进制位组成。

 二、Hyperloglogs

Hyperloglogs: 提供了—种不太精确的基数统计方法,用来统计—个集合中不重复的元素个数,比如统计网站的UV,或者应用的日活、 月活,存在—定的误差。

在 Redis 中实现的 Hyperloglog, 只需要12K内存就能统计2A 64个数据。

三、Geo

一个球面数据结构,可直接计算两个经纬度之间的距离等等

四、Streams

5.0 推出的数据类型。支持多播的可持久化的消息队列,用于实现发布订阅功能, 借鉴了kafka的设计。

1.4.7、总结

数据结构:

对象对象type属性值type命令输出底层可能的数据结构object encoding
StringOBJ_STRING"string"

OBJ_ENCODING_INT

OBJ_ENCODING_EMBSTR

OBJ_ENCODING_RAW

int

embster

raw

ListOBJ_LIST"list"OBJ_ENCODING_QUICKLISTquicklist
HashOBJ_HASH"hash"

OBJ_ENCODING_ZIPLIST

OBJ_ENCODING_HT

ziplist

hashtable

SetOBJ_SET"set"

OBJ_ENCODING_INTSET

OBJ_ENCODING_HT

intset

hashtable

ZSetOBJ_ZSET"zset"

OBJ_ENCODING_ZIPLIST

OBJ_ENCODING_SKIPLIST

ziplist

skiplist+hashtable

编码转换:

对象原始编码二级编码三级编码
String

INT

整数并且小于long 2^63-1

embstr

INT不为整数,或大于long的长度

raw

embstr大于44字节,或被修改

List

quicklist

Hash

ziplist

键和值的长度小于64byte

键值对不超过512个

需同时满足

hashtable

Set

intset

元素都是整数类型

元素个数小于512个

需同时满足

hashtable

ZSet

zpilist

元素个数不超过128个

任何一个member的长度小于64字节

需同时满足

skiplist

应用场景:

缓存——提升热寺数据的访问速度

共享数据——数据的存储和共享的问题

全局ID——分布式全局ID的生成方案(分库分表)

分布式锁——进程间共享数据的原子操作保证

在线用户统计和计数

队列栈——跨进程的队列/栈

消息队列——异步解耦的消息机制

服务注册与发现 —— RPC 通信机制的服务协调中心 (Dubbo 支持 Redis)

购物车

新浪/Twitter 用户消息时间线

抽奖逻辑(礼物、转发)

点赞、签到、打卡

商品标签

用户(商品)关注(推荐)模型

电商产品筛选

排行榜

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值