目录
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对海量数据的存储管理能力,还保持了传统数据库支持ACID和SQL等特性。
NewSQL是指这样一类新式的关系型数据库管理系统,针对OLTP(读-写)工作负载,追求提供和NoSQL系统相同的扩展性能,且仍然保持ACID和SQL等特性(scalable and ACID and (relational and/or sql -access))。
NewSQL 结合了 SQL和 NoSQL 的特性。例如 TiDB (PingCAP)、VoltDB、ScaleDB等。
特性 | SQL | NoSQL | NewSQL |
---|---|---|---|
关系模型 | √ | × | √ |
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 4hget 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_ZIPLIST和OBJ_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 |
---|---|---|---|---|
String | OBJ_STRING | "string" | OBJ_ENCODING_INT OBJ_ENCODING_EMBSTR OBJ_ENCODING_RAW | int embster raw |
List | OBJ_LIST | "list" | OBJ_ENCODING_QUICKLIST | quicklist |
Hash | OBJ_HASH | "hash" | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_HT | ziplist hashtable |
Set | OBJ_SET | "set" | OBJ_ENCODING_INTSET OBJ_ENCODING_HT | intset hashtable |
ZSet | OBJ_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 用户消息时间线
抽奖逻辑(礼物、转发)
点赞、签到、打卡
商品标签
用户(商品)关注(推荐)模型
电商产品筛选
排行榜