Redis作者antirez,2008年做网站访问记录,统计每天的用户量,页面浏览数,访客的IP,访客使用的操作系统等等。最开始用的是MySQL,实在太慢了,自己就写了基于内存的List,就是Redis。
为什么叫Redis? 全称Remote Dictionary Service。翻译成中文远程字典服务。
**SQL**
的操作,支持复杂的关联查询;**ACID**
来提供严格或实时的数据一致性。为了解决关系型数据库带来的一些列问题,就出现了非关系型数据库。**NOSQL**
最开始不提供 **SQL**
** **数据库,在后来慢慢地发生了变化。
**BASE**
理论来保证数据的最终一致性; Basically Available
基本可用**Soft-State**
:软状态**Eventually Consistent**
最终一致性**Key-Value**
形式存储数据(**Redis、Memcache**
);**MongoDB**
);**HBase**
);Neo4j
)**XML**
存储等1)内存的速度更快,10w QPS 2)减少计算的时间,减轻数据库压力
1)更丰富的数据类型 2)支持多种编程语言 3)功能丰富:持久化机制、内存淘汰策略、事务、发布订阅、pipeline、lua 4)支持集群、分布式
Memcached只能存储KV、没有持久化机制、不支持主从复制、是多线程的。
安装好 Redis 的服务: 1、《CentOS7 安装 Redis 6.0.9 单实例》 https://gper.club/articles/7e7e7f7ff3g5bgccg69
做完以后可以 clone 三台机器搭建 Setinel 架构: 2、《Redis6.0.9 一主二从 Sentinel 监控配置》 https://gper.club/articles/7e7e7f7ff3g5bgccg68
3、还可以在单机实例的机器上,再装一个伪集群: 《CentOS 7 单机安装 Redis Cluster6.0.9(3 主 3 从伪集群)》 https://gper.club/articles/7e7e7f7ff3g5bgcdg60
4、阿里云 CentOS7 Docker 安装 Redis https://gper.club/articles/7e7e7f7ff7g5egc5g6c er.club/ar ticles/7e7e7f7ff7g5egc5g
src目录下,直接启动 ./redis-server
后台启动(指定配置文件) 1、redis.conf修改两行配置 daemonize yes bind 0.0.0.0 2、启动Redis redis-server /usr/local/soft/redis-6.0.9/redis.conf
总结:redis的参数可以通过三种方式配置,一种是redis.conf,一种是启动时携带参数,一种是config set。
Redis Desktop Manager
Redis默认有16个库(0-15)。可以在配置文件redis.conf中修改。
database 16
因为没有完全隔离,不像数据库的 database,不适合把不同的库分配给不同的业务 使用。默认使用第一个db0。在集群里面只能使用第一个db。 切换数据库 select 0 清空当前数据库 flushdb 清空所有数据库 flushall
Redis的存储我们叫做key-value存储,或者叫做字典结构。key的最大长度限制是 512M,值的限制不同,有的是用长度限制的,有的是用个数限制的。 我们先从key的基本操作入手,包括大家最熟悉的get set。
命令怎么学习比较好?比如Linux命令,你们是怎么学习的?我有几个建议: 1、多练习,长时间不用肯定会忘。 2、分类成体系(千万不要按照字母顺序学),比如文件命令、网络命令、用户命令 Redis 可以按数据类型分。 3、学会查命令(Tab可以提示,–help可以查看参数)。
既然大家天天都做数据库的CRUD,我们来看看Redis的CRUD。 注意看返回值,有的情况下返回0或者nil代表是不成功的。
#存值(如果对同一个key set多次会直接覆盖旧值)
set qingshan 2673
#取值
get qingshan
#查看所有键
keys *
#获取键总数(生产环境数据量大,慎用)
dbsize
#查看键是否存在
exists qingshan
#删除键
del qingshan huihui
#重命名键
rename qingshan penyuyan
#查看类型
type qingshan
那么,Redis一共有几种数据类型?(注意我们说的是数据类型不是数据结构) String、Hash、Set、List、Zset、Hyperloglog、Geo、Streams
最基本也是最常用的数据类型就是String。set和get命令就是String的操作命令。 Reids的字符串被叫做二进制安全的字符串,为什么是Binary-safe Strings呢? 下面对于所有的数据类型我们都会从4个维度来分析:存储类型、操作命令、存储结构、应用场景。
存储类型 可以用来存储INT(整数)、float(单精度浮点数)、String(字符串)。
操作命令
# 获取指定范围的字符
getrange snail 0 1
# 获取值长度
strlen snail
# 字符串追加内容
append snail good
# 设置多个值(批量操作,原子性)
mset snail 0 xiaobai 666
# 获取多个值
mget snail xiaobai
# 设置值。如果key存在,则不成功
setnx snail 11
# 基于此可实现分布式锁。用del key释放锁。
# 但如果释放锁的操作失败了,导致其他节点永远获取不到锁,怎么办?
# 加过期时间。单独用expire加过期,也失败了,无法保证原子性,怎么办?多参数
set key value [expiration EX seconds|PX milliseconds][NX|XX]
#使用参数的方式
set k1 v1 EX 10 NX
# (整数)值递增(值不存在会得到1)
incr snail
incrby snail 100
# (整数)值递减
decr snail
decrby snail 100
# 浮点数增量
set mf 2.6
incrbyfloat mf 7.3
存储(实现)原理 数据模型
Redis是KV的数据库,Key-Value我们一般会用什么数据结构来存储它?哈希表。Redis 的最外层确实是通过 hashtable实现的(我们把这个叫做外层的哈希)。 在Redis里面,这个哈希表怎么实现呢?我们看一下C语言的源码(dict.h47行) 每个键值对都是一个 dictEntry(怪不得叫远程字典服务),通过指针指向key的存储结构和value的存储结构,而且next存储了指向下一个键值对的指针。
# dict.h 47行
typedef struct dictEntry {
void *key;/* key关键字定义*/
union {
void *val; /* value定义 */
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /*指向下一个键值对节点 */
} dictEntry;
实际上最外层是redisDb,redisDb里面放的是dict,后面hash我们会把这部分串起来,源码server.h 661行
typedef struct redisDb {
dict *dict; /* 所有的键值对 */ /* The keyspace for this DB */
dict *expires; /* 设置了过期时间的键值对 */ /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *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的结构。
value是world,同样是一个字符串,是不是也用SDS存储呢? 当value存储一个字符串的时候,Redis并没有直接使用SDS存储,而是存储在redisObject中。 实际上五种常用的数据类型的任何一种的value,都是通过redisObject来存储的。 最终redisObject再通过一个指针指向实际的数据结构,比如字符串或者其他。
//源码src/server.h 622行
typedef struct redisObject {
unsigned type:4; /* 对象的类型,包含:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET*/
unsigned encoding:4; /* 具体的数据结构 */
/* 24位, 对象最后一次被命名程序访问的时间,与内存回收有关 */
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; /* 引用计数。当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了 */
void *ptr; /* 指向对象实际的数据结构 */
} robj;
用type命令看到的类型就是type的内容: type snail -> String 为什么一个value会有一钟对外的类型,还有一种实际的编码呢?我们刚才的字符串使用SDS存储,那么这个redisObject的value就会指向一个SDS:
那么实际的编码到底是什么呢?
set number 1
set snail "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
set xiaobai yes
type number
type xiaobai
type snail
object encoding number
object encoding snail
object encoding xiaobai
虽然对外都是String,用的String的命令,但是出现了三种不同的编码。 这三种编码有什么区别呢? 1、int ,存储8个自己的长整型(long , 2^63-1)。 2、embstr,代表embstr格式的SDS,存储小于44个字节的字符串。 3、raw,存储大于44个字节的字符串。
/* object.c*/
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
Redis 中字符串的实现。 在 3.2 以后的版本中,SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 25=32byte,28=256byte,216=65536byte=64KB,232byte=4GB。
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 */ /* 当前字符数组的属性、用来标识到底是sdshdr8还是sdshdr16等 */
char buf[];/* 字符串真正的值 */
};
因为C语言没有字符串类型,只能用字符数组char[]实现。
1.使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
2.如果要获取字符长度,必须遍历字符数组,时间复杂度O(n)。
3.C字符串长度的变更会对字符数组做内存重分配。
4.通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。
SDS的特点:
1、不用担心内存溢出问题,如果需要会对SDS进行扩容。
2、获取字符串长度时间复杂度O(1),因为定义了len属性。
3、通过"空间预分配"(sdsMakeRoomFor)和"惰性空间释放",防止多次重分配内存。
4、判断是否结束的标志是len属性,可以包含'\0'(它同样以'\0'结尾是因为这样就可以使用C语言中函数库操作字符串的函数了)。
存储二进制:BytesTest.java
c字符数组 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h>库中的函数 | 可以使用一部分<string.h>库中的函数 |
embstr的使用只分配一次内存空间(因为RedisObject和SDS是连续的)。而raw需要分配两次内存空间(分别为RedisObject和SDS分配空间)。
因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。
而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存是,整个RedisObject和SDS都需要重新分配空间,因此Redis中的embstr实现为只读(这种编码的内容是不能修改的)。
1、int数据不再是整数–raw 2、int大小超过了long的范围(2^63-1)–embstr 3、embstr长度超过了44个字节–raw
set k1 1
Object encoding k1
append k1 a
Object encoding k1
set k2 108213812381230812123
object encoding k2
set k3 108213812381230812124
object encoding k3
set k4 aaaaaaaaaaaabbbbbbbbbcccccccccccddddddeeee
object encoding k4
set k5 aaaaaaaaaaaabbbbbbbbbcccccccccccddddddeeeeeee
object encoding k5
set k6 a
object encoding k6
append k6 b
object encoding k6
上面的命令会发现一个问题:明明没有超过44个字节,为什么变成raw了? 前面分析了embstr的结构,由于它的实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw在进行修改、 因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否到达44个字节。
关于Redis内部编码的转换,都符合以下规律:编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新set)。
其实无论是设计redisObject,还是对存储字符设计这么多的SDS,都是为了根据存储的不同内容选择不同的存储方式,这样可以实现尽量的节省内存空间和提升查询速度的目的。
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
public Boolean getLock(Object lockObject){
jedisUtil = getJedisConnection();
boolean flag = jedisUtil.setNX(lockObject, 1);
if(flag){
expire(lockObject, 10);
}
return flag;
}
public void releaseLock(Object lockObject){
del(lockObject);
}
总结:利用redis本身的特性,String内存的存储内容,以及提供的操作方式,我们可以用来达到很多的业务目的。
例如用一个key存储一张表的数据。
序列化的方式?例如JSON/Protobuf/XML,会增加序列化和反序列化的开销,并且不能单独获取、修改一个值。 可以通过key分层的方式来实现,例如:
mset student:1:sno GP16666 student:1:sname 彪哥 student:1:company 京东
# 获取值的时候一次获取多个值,用list接受:
mget student:1:sno student:1:sname student:1:company
缺点: key太长,占用的空间太多。那么有没有好的方式呢?
存储类型 hash用来存储多个无序的键值对。最大存储数量2^32-1(40亿左右)
注意:前面我们说Redis所有的KV本身就是键值对,用dictEntry实现的,叫做外层的哈希、现在我们分析的是内层哈希。
hset h1 f 6
hset h1 e 5
hmset h1 a 1 b 2 c 3 d 4
hget h1 a
hmget h1 a b c d
hkeys h1
hvals h1
hgetall h1
# key操作
hdel h1 a
hlen h1
存储原理 Redis的Hash本身也是一个KV的结构。是不是与外层的哈希一样,用dictEntry实现呢? 内存的哈希底层可以使用两种数据结构实现: ziplist: OBJ_ENCODING_ZIPLIST(压缩列表) hashTable:OBJ_ENCODING_HT(哈希表)
ziplist压缩列表 ziplist是一个经过特殊编码的,由连续内存块组成的双向链表。 它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度。这样读写可能会慢一些,因为要计算长度,但是可以节省内存,是一种时间换空间的思想。
ziplist的内部结构?源码ziplist.c第16行的注释:
hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
object encoding h2
object endoding h3
那么上面entry的内容呢?ziplist.c
所以展开应该是这个样子的:
编码有哪些?
#define ZIP_STR_06B(0<<6) //长度小于等于63字节
#define ZIP_STR_14B(1<<6) //长度小于等于16383字节
#dedine ZIP_STR_32B(2<<6) //长度小于等于4294967295字节
什么时候使用ziplist存储? 当hash对象同事满足以下两个条件的时候,使用ziplist编码: 1)哈希对象保存的键值对数量小于512个; 2)所有的键值对的键和值的字符串长度都小于64byte(一个英文字母一个字节)。
src/redis.conf配置
hash-max-ziplist-value 64 //ziplist中能最大存放的值长度
hash-max-ziplist-entries 512 //ziplist中最多能存放的entry节点数量
如果超过这两个阈值的任何一个,存储结构就会转换成hashTable。 总结:字段个数少,字段值少,用ziplist。
在Redis中,hashTable被称为字典(dictionary)。 前面分析,Redis的KV结构是通过一个dictEntry来实现的。 在hashTable中,又对dictEntry进行了多层的封装。
源码位置:dict.h 47行。
typedef struct dictEntry {
void *key; /* key关键字定义 */
union {
void *val;
uint64_t u64; /* value定义 */
int64_t s64;
double d;
} v;
struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry;
dictEntry放到了dictht(hashTable里面)
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table; /* 哈希表数组 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 掩码大小,用于计算索引值。总是等于size-1 */
unsigned long used; /* 已有节点数 */
} dictht;
ht放到了dict里面:
typedef struct dict {
dictType *type; /* 字典类型 */
void *privdata; /* 私有数据 */
dictht ht[2]; /* 一个字典有两个哈希表 */
long rehashidx; /* rehash 索引 */
unsigned long iterators; /* 当前正在使用的迭代器数量 */
} diet;
从底层到最高层dictEntry—dictht–dict。他是一个数组+链表的结构。 hashtable-dict存储结构
dictht 后面是NULL说明第二个ht还没用到。dictEntry*后面是NULL说明没有hash到这个地址。dictEntry后面是NULL说明没有发生哈希冲突。 QA:为什么要定义两个哈希表,其中一个不用呢?
redis的hash默认使用的是ht[0], ht[1]不会初始化和分配空间。
哈希表dictht是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size属性)和它所保存的节点数量(userd属性)之间的比率:
- 比率在1:1时(一个哈希表ht只存储一个节点entry),哈希表的性能最好;
- 如果节点数量比哈希表的大小要大很多的话(这个比例用ratio表示,5表示平均一个ht存储5歌entry),那么哈希表就会退化成多个链表,哈希表本身的性能就不再存在。
如果单个哈希表的节点数量过多,哈希表的大小需要扩容。Redis里面的这种操作叫做rehash。
rehash的步骤:
1、为字符ht[1]哈希表分配空间。ht[1]的大小为第一个大于等于ht[0].used *2的2的N次方幂。比如已经使用了10000,那就是16384.
2、将所有的ht[0]上的节点rehash到ht[1]上,重新计算hash值和索引,然后放入指定的位置。
3、当ht[0]全部迁移到ht[1]之后,释放ht[0]的空间,将ht[1]设置为ht[0]表,并创建新的ht[1],为下次rehash做准备。
QA:什么时候触发扩容? 负载因子(源码dict.c)
static int dict_can_resize = 1; //是否需要扩容
static unsigned int dict_force_resize_ratio = 5; //扩容因子
扩容判断和扩容操作类似HashMap,也有缩容。 总结:Redis的Hash类型,可以用zipList和hashTable实现。
和string一样 String可以做的事情,Hash都可以做。 存储对象类型的数据 比如对象或者一张表的数据,比String节省了更多key的空间,也更加便于集中管理。 购物车的操作 key:用户id; field :商品id; value :商品数量。
存储有序的字符串(从左到右),元素可以重复。最大存储数量2^32-1(40亿左右)。
lpush queue a
lpush queue b c
lpush queue d e
lpop queue
lpop queue
lindex queue 0
lrange queue 0 -1
在早期的版本中,数据量较小时用ziplist存储(特殊编码的双向链表),达到临界值时转化为linkedList进行存储,分别对应OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_LINKEDLIST。
3.2版本之后,统一使用quickList来存储。quickList存储了一个双向链表,每个节点都是一个zipList,所以是zipList和LinkedList的结合体。
object encoding queue
总体结构: quciklist.h 105行
typedef struct quicklist{
quicklistNode *head; /* 指向双向列表的表头 */
quciklistNode *tail;/* 指向双向列表的表尾 */
unsigned long count;/* 所有的ziplist中一共存了多少个元素 */
unsigned long len; /* 双向链表的长度,对应list-compress-depth */
int fill:QL_FILL_BITS;/* ziplit最大大小,对应list-max-ziplist-depth */
unsigned int compress:QL_COMP_BIT;/* 压缩深度,对应list-compress-depth */
unsigned int bookmark_count:QL_BM_BITS;/* 4位,bookmarks数组的大小 */
quciklistBookmark booksmarks[];/* bookmarks是一个可选字段。quciklist重新分配内存空间时使用,不使用时不占空间 */
} quicklist;
redis.conf相关参数:
参数 | 含义 |
---|---|
List-max-ziplist-size(fill) | 正数表示单个ziplist最多所包含的entry个数。负数表示单个ziplist的大小,默认8K。-1: 4KB; -2: 8KB; -3: 16KB; -4: 32KB; -5: 64KB |
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 是不是已经被解压出来作临时使用 */
unsigned int attempted_compress: 1; /* 测试用 */
unsigned int extra: 10; /* 预留给未来使用 */
} quicklistNode;
ziplist的结构:quicklist是一个数组 + 链表的结构。
List主要用在存储有序内容的场景。 列表
** 用户的消息列表、网站的公告列表、活动列表、博客的文章列表、评论列表等。顺序储存显示。 队列/栈
List还可以当做分布式环境的队列/栈使用。 List还提供两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间(单位:秒)。
blpop queue
brpop queue
BLPOP:BLPOP key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 BRPOP:BRPOP key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
队列:先进先出 :rpush blpop , 左头右尾,右边进入队列,左边出队列。 栈:先进后出:rpush brpop 总结: List存储有序的内容,用quicklist实现,本质上是数组 + 链表。 hashTable也是数组加链表,只是内部编码结构不一样。
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。如果元素都是整数类型,就用intset存储。 insert.h 35行
typedef struct intset{
unint32_t encoding; //编码类型 int16_t 、int32_t 、int64_t
unint32_t length; //长度 最大长度:2^32
int8_t contents[]; //用来存储成员的动态数组
} intset;
如果不是整数类型,就用hashTable(数组 + 链表的存储结构)。 如果元素个数超过512个,也会用hashTable存储。跟一个配置有关:
set-max-insert-entries 512
QA: set的key没有value,怎么用hashTable存储? value这里存储的是null。
抽奖 随机获取元素: spop myset 点赞、签到、打卡 商品标签 商品筛选
//获取差集
sdiff set1 set2
//获取交集(intersection)
sinter set1 set2
//获取并集
sunion set1 set2
用户关注、推荐模型
sorted set 存储有序的元素。每个元素有个score ,按照score从小到大排名。 score 相同时。按照key的ASCII码排序。 数据结构对比:
数据结构 | 是否允许重复元素 | 是否有序 | 有序实现方式 |
---|---|---|---|
列表list | 是 | 是 | 索引下标 |
集合set | 否 | 否 | 无 |
有序集合zset | 否 | 是 | 分值score |
//添加元素
zadd myset 10 Java 20 php 30 ruby 40 cpp
//获取全部元素
zrange myzset 0 -1 withscore
zrevrange myzset 0 -1 withscore
//根据分值区间获取元素
zrangebyscore myzset 20 30
//移除元素 也可以根据sore rank 删除
zrem myzset php cpp
//统计元素个数
zcard myzset
//分值递增
zincrby myzset 5 python
//根据分值统计个数
zcount myzset 20 60
//获取元素rank
zrank myzset python
//获取元素score
zscore myzset python
默认使用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() && 0xFFFF) < ZSKIPLIST_P * 0xFFFF)
level += 1;
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
现在我们查询数据的时候,可以先沿着这个新的链表进行查找。当碰到比待查数大的节点时,在到下一层进行查找。
比如,我们想要找15,查找的路径是沿着标红的指针所指向的方向进行的:
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{
dict *dict;
zskiplist *zsl;
} zset;
排行榜
id为6001的新闻点击数加1: zincrby hotNews:20251111 1 n6001 获取今天点击最多的15条: zrevrange hotNews:20251111 0 15 withscores
Bitmap Geospatial Hyperloglogs Streams