[缓存 - redis] Redis的基本知识点 数据结构和常用操作命令

1. 简介

redis(remote dicitionary server)是一个高性能的key-value数据库,对数据库的操作是原子性的,采用C语言编写。

1.1 全局哈希表

为了实现基于Key的快速访问,Redis采用了hash表作为最底层的存储结构。
在这里插入图片描述
上图是Redis中的一个底层数据结构

1.2 dictEntry

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

每一个dictEntry元素,都保存了key和value的指针,*value指针保存在联合体v中,既然保存的是指针,所以也就能保存任何类型的数据结构(String,Hash,List,Set)

1.3 哈希表

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

1.4 RedisObject

Redis 的 key 是 String 类型,但 value 可以是很多类型(String/List/Hash/Set/ZSet等),所以 Redis 要想存储多种数据类型,就要设计一个通用的对象进行封装,这个对象就是 redisObject。结构体定义如下:

typedef struct redisObject {
    unsigned type:4; //redisObject的数据类型,4个bits
    unsigned encoding:4; //redisObject的编码类型,4个bits
    unsigned lru:LRU_BITS;  //redisObject的LRU时间,LRU_BITS为24个bits
    int refcount; //redisObject的引用计数,4个字节
    void *ptr; //指向值的指针,8个字节
} robj;

结构一共定义了 4 个元数据和一个指针:

  • type:redisObject 的数据类型,面向用户的数据类型(String/List/Hash/Set/ZSet等)。占用 4 bit
  • encoding:redisObject 的编码类型,是 Redis 内部实现各种数据类型所用的数据结构,每一种数据类型,可以对应不同的底层数据结构来实现(SDS/ziplist/intset/hashtable/skiplist等)。占用4bit
  • lru:redisObject 的 LRU 时间。占用24bit
  • refcount:redisObject 的引用计数。占用4个字节
  • ptr:指向值的指针。占用8个字节

1.5 哈希冲突

Redis中解决哈希冲突采用链式哈希的方式,一旦冲突变多,就会导致链表过长,最终退化为O(n)的时间复杂度,这对Redis来说肯定是不能接受的。

所以,解决链表过长问题,直接进行一次rehash操作。简单来说就是扩大Entry数组的大小,从而来减少哈希冲突。当然为了不对线上访问造成影响,Redis还采用渐进式rehash的方式,实际上Redis每次在进行rehash操作时,会新准备一个比原哈希表大一倍的新哈希表,然后在每一次处理请求的时候,顺便处理一次数据迁移,比如从原哈希表的第一个索引位置开始,把这个位置上的Entry全部挪到新的哈希表中,这样通过分摊处理,就避免了一次性全量处理所带来的阻塞问题。

Redis在dict.h中定义了一个dict结构体。这个结构体中有一个数组(ht[2]),包含了两个 Hash 表 ht[0]和 ht[1]。dict 结构体的代码定义如下所示:

typedef struct dict {
    …
    dictht ht[2]; //两个Hash表,交替使用,用于rehash操作
    long rehashidx; //Hash表是否在进行rehash的标识,-1表示没有进行rehash} dict;
  • 首先,Redis 准备了两个哈希表,用于 rehash 时交替保存数据。
  • 其次,在正常服务请求阶段,所有的键值对写入哈希表 ht[0]。
  • 接着,当进行 rehash 时,键值对被迁移到哈希表 ht[1]中。
  • 最后,当迁移完成后,ht[0]的空间会被释放,并把 ht[1]的地址赋值给 ht[0],ht[1]的表大小设置为 0。这样一来,又回到了正常服务请求的阶段,ht[0]接收和服务请求,ht[1]作为下一次 rehash 时的迁移表。

1、触发 rehash 的条件?

负载因子:Hash 表当前承载的元素个数 / Hash 表当前设定的大小。
dict 在负载因子超过 1 时(used: bucket size >= 1),会触发 rehash。但如果 Redis 正在 RDB 或 AOF rewrite,为避免父进程大量写时复制,会暂时关闭触发 rehash。但这里有个例外,如果负载因子超过了 5(哈希冲突已非常严重),依旧会强制做 rehash(重点)

2、触发 rehash 的时候?

当我们往 Redis 中写入新的键值对或是修改键值对时,Redis 都会判断下是否需要进行 rehash

3、rehash 扩容扩多大?

如果当前表的已用空间大小为 size,那么就将表扩容到 size*2 的大小。

4、渐进式 rehash 的实现?

所谓「渐进式 rehash」是指,把很大块迁移数据的开销,平摊到多次小的操作中,目的是降低主线程的性能影响「全局哈希表」在触发渐进式 rehash 的情况有 2 个:

  • 增删改查哈希表时:每次迁移 1 个哈希桶 — 操作出发
  • 定时 rehash:如果 dict 一直没有操作,无法渐进式迁移数据,那主线程会默认每间隔 100ms 执行一次迁移操作。这里一次会以 100 个桶为基本单位迁移数据,并限制如果一次操作耗时超时 1ms 就结束本次任务,待下次再次触发迁移 — 时间触发

注意:定时 rehash 只会迁移全局哈希表中的数据,不会定时迁移 Hash/Set/Sorted Set 下的哈希表的数据,这些哈希表只会在操作数据时做实时的渐进式 rehash

总结

  • Redis 中的 dict 数据结构,采用「链式哈希」的方式存储,当哈希冲突严重时,会开辟一个新的哈希表,翻倍扩容,并采用「渐进式 rehash」的方式迁移数据
    所谓「渐进式 rehash」是指,把很大块迁移数据的开销,平摊到多次小的操作中,目的是降低主线程的性能影响。
  • Redis 中凡是需要 O(1) 时间获取 k-v 数据的场景,都使用了 dict 这个数据结构,也就是说 dict 是 Redis 中重中之重的「底层数据结构」
  • dict 封装好了友好的「增删改查」API,并在适当时机「自动扩容、缩容」,这给上层数据类型(Hash/Set/Sorted Set)、全局哈希表的实现提供了非常大的便利
  • 全局哈希表 在触发渐进式 rehash 的情况有 2 个: - 增删改查哈希表时:每次迁移 1 个哈希桶(文章提到的 dict.c 中的 _dictRehashStep 函数) - 定时 rehash:如果 dict 一直没有操作,无法渐进式迁移数据,那主线程会默认每间隔 100ms 执行一次迁移操作。这里一次会以 100 个桶为基本单位迁移数据,并限制如果一次操作耗时超时 1ms 就结束本次任务,待下次再次触发迁移(文章没提到这个,详见 dict.c 的 dictRehashMilliseconds 函数) (注意:定时 rehash 只会迁移全局哈希表中的数据,不会定时迁移 Hash/Set/Sorted Set 下的哈希表的数据,这些哈希表只会在操作数据时做实时的渐进式 rehash)
  • dict 在负载因子超过 1 时(used: bucket size >= 1),会触发 rehash。但如果 Redis 正在 RDB 或 AOF rewrite,为避免父进程大量写时复制,会暂时关闭触发 rehash。但这里有个例外,如果负载因子超过了 5(哈希冲突已非常严重),依旧会强制做 rehash(重点)
  • dict 在 rehash 期间,查询旧哈希表找不到结果,还需要在新哈希表查询一次

2 数据结构

2.1 SDS - string 简单动态字符串

数据结构

struct sdshdr { 
	int len;// 数组中已使用字节的数量 等于SDS 所保存字符串的长度
	int free;// 记录buf 数组中未使用字节的数量
	char buf[]; // 字节数组,用于保存字符串
};

为什么不直接使用C字符串 ?

1、常数复杂度获取字符串长度
C字符串并不记录自身的长度信息,获取一个C字符串的长度,程序必须遍历整个字符串,这个操作的复杂度O(N),和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所 以获取一个SDS长度的复杂度仅为O(1)

Redis将获取字符串长度所需的复杂 度从O(N)降低到了O(1),这确保了获取字符串长度的工作不会成为Redis的性能瓶颈

2、杜绝缓冲区溢出
C字符串不记录自身长度带 来的另一个问题是容易造成缓冲区溢出(buffer overflow)。举个栗子,<string.h>/strcat 函数可以将src字符串中的内容拼接到dest字符串的末尾:

char *strcat(char *dest, const char *src);

假设程序员在执行strcat函数前已经为dest分配足够的内存,但是在拼接src字符串时一旦超过了dest分配空间的最大长度则会产生缓冲区溢出。

对于紧邻字符串的操作也容易出现问题
在这里插入图片描述

strcat(s1, " Cluster"); // 会出问题

SDS空间分配策略

  1. 空间预分配
    空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个 SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须要的空间,还会为SDS分配额外的未使用空间。

  2. 惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要 缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后 多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

2.2 list:双向链表

数据结构

typedef struct listNode {
    struct listNode *prev;//前置节点
    struct listNode *next; //后置节点
    void *value;//节点的值
} listNode;

在这里插入图片描述
因为有前置节点和后置节点,所以可以看出这是一个双向链表。不过,Redis 在 listNode 结构体基础上又封装了 list 这个数据结构,这样操作起来会更方便。

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;

list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是用于实现多态链表所需的类型。

  • dup函数用于复制链表节点所保存的值;
  • free函数用于释放链表节点所保存的值;
  • match函数则用于对比链表节点所保存的值和另一个输入值是否相等。

特点

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL, 对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序 获取链表的表头节点和表尾节点的复杂度为O(1)。
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

2.3 字典

与全局哈希表一样

2.4 set:整数集合 IntSet 和哈希表

set简介

Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。相对于列表,集合也有两个特点:无序、不可重复

Set应用场景

常见的应用场景有:投票系统、标签系统、共同好友、共同关注、共同爱好、抽奖、商品筛选栏,访问 IP 统计等

使用场景:

  • 点赞、踩、收藏:Set 类型可以保证一个用户只能点一个赞;
  • 共同关注、标签:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等;
  • 抽奖活动:存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次

整数集合IntSet

typedef struct IntSet{
     uint32_t encoding;// 编码格式
     uint32_t length;// 集合中的元素个数
     int8_t contents[];// 保存元素数据
} IntSet;

“contents” 是整数集合的底层实现,保存了整数集合的每一个元素,每个元素在该数组中从小到大有序排列,并且不重复(如何保证有序性和唯一性我们后面讨论插入的时候在说)。“contents” 数组虽然声明为 int8_t 类型,但其实真正的类型取决于 “encoding” 的值。在操作一个整数集合的时候,会首先获取 “encoding” 的值。

举个栗子,当我们执行 SADD numbers 1 3 5 向集合对象插入数据时,该集合对象在内存的结构如下:
在这里插入图片描述

哈希表HashTable

同全局哈希表

2.5 zset(sorted set):压缩列表和跳表

简介

相比于set,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
zset有两种不同的实现,分别是zipList和skipList。

typedef struct zset{ // 跳表
	dict *dict;
	zskiplist *zsl;
}

typedef struct zset{ // 压缩列表
	dict *dict;
	ziplist *zpl;
}

压缩列表 zipList:

满足以下两个条件底层使用zipList:

  • [score,value]键值对数量少于128个;
  • 每个元素的长度小于64字节;
结构体
 //这个结构体只是在操作ziplist使用,实际内存结构还是前面图示的一样
typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

整体编码结构图
ziplist整体由三部分构成,header、entry、zlend。

  • header
    4字节的zlbytes,表示整个ziplist的大小
    4字节的zltail, 表示最后一个entry基于整个ziplist的偏移量
    2字节的zllen,表示entry的个数,当个数超过216-2时,zllen恒等于216-1,entry的个数需要遍历计算
  • zlen值恒等于255(0xFF)
    在这里插入图片描述
    entry编码结构图:entry由prevlen, encoding, entry-data三部分构成
    在这里插入图片描述
    prevlen
    prevlen表示前一个entry的总大小,当前一个entry的大小[0,253]时,prevlen只需要一个字节表示
    当大于等于254时,prevlen为5字节,第一个字节恒等于254(0xfe),后续4字节表示长度(小端序)
    在这里插入图片描述

encoding
encoding根据后续的entry-data进行的编码,所以分为了字符串和整数
字符串编码
|00xxxxxx| encoding 为1字节,其中6位最表示字符串长度 [0,63], 2^6-1
|01xxxxxx|xxxxxxxx|encoding 为2字节, 14位(大端序)表示字符串长度[0,16383], 2^14-1
|10000000|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx|encoding为5字节, 4bytes(大端序)表示字符串长度[0,2^32-1]
在这里插入图片描述

整数编码(开头两位都是1)
|11000000|,数据为int16_t(2字节)
|11010000|,数据为int32_t(4字节)
|11100000|,数据为int64_t(8字节)
|11110000|, 数据为3字节, 24bit有符号数
|1111xxxx|, 数据范围在[1-13], 并且没有数据部分,就包含在编码中
在这里插入图片描述

优点
  • 占用空间少,比如整数为实际值,不是数字字符串
  • 任意一个entry都可以向前或者向后遍历(类似双向链表)
  • 对比双向链表,ziplist是连续的内存,可随机访问,并且减少了next指针空间
缺点
  • 因为内存连续的,push/pop操作都需要重新分配空间
  • 因为内存连续的,修改数据可能需要重新分配空间
  • 插入或者修改数据将产生连锁更新问题
  • 查询某个entry需要遍历整个entry链

跳表 skipList:

不满足以上两个条件时使用跳表(组合了hash和skipList)

  • hash用来存储value到score的映射,这样就可以在O(1)时间内找到value对应的分数;
  • skipList按照从小到大的顺序存储分数;
  • skipList每个元素的值都是[score,value]对

跳表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
大部分情况下,跳表效率可以跟平衡树媲美。

结构体
/*
 * 跳跃表
 */
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;// 头节点,尾节点
    unsigned long length;// 节点数量
    int level;// 目前表内节点的最大层数
} zskiplist;

/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {
    robj *obj;// member 对象
    double score;// 分值
    struct zskiplistNode *backward;// 后退指针
    struct zskiplistLevel {// 节点的level数组,保存每层上的前向指针和跨度
        struct zskiplistNode *forward;// 前进指针
        unsigned int span;// 这个层跨越的节点数量
    } level[];
} zskiplistNode;

在这里插入图片描述

zskiplist结构:

  • header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1);
  • tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1);
  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内),通过这个属性可以再O(1)的时间复杂度内获取层高最好的节点的层数;
  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内),通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。

zskiplistNode结构:

  • 层(level):
    节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。
    每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越远)。在上图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
    每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。

  • 后退(backward)指针:
    节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后退指针,因此每次只能后退一个节点。

  • 分值(score):
    各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

  • 成员对象(oj):
    各个节点中的o1、o2和o3是节点所保存的成员对象。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

跳表原理
  1. 一个普通的单项链表
    在这里插入图片描述
    如果要查找节点8,需要依次遍历8个节点找到 8

  2. 向上简历一层索引层
    在这里插入图片描述
    在查询的时候,我们可以从索引层去查询,先找到节点1,然后到节点4,然后到节点7,节点7之后没有节点了,那么就去下一层去寻找,下一层的节点7之后就是节点8,这次我们只遍历了4次就找到了答案。

那么上层的节点层的个数取多少合适呢?如果我们下层的节点个数是10W个,而上层只有三个索引节点的话,从节点7之后遍历,也要遍历9W多个,肯定是达不到我们预期的效率的。所以最好的策略就是,上层节点的个数是下层节点个数的一半,且尽量是随机均匀的,这样就可以节省一半的遍历次数,比如有10W个节点,我们就建立5W个索引节点,这样遍历次数就可以减少一半。

但是遍历5W次肯定也是不行的,我们知道O(n)和O(2n)这种在算法中是近似相等的,那么怎么办呢?一层不够我们就建立两层,两层不够就建立三层。直到最上层的节点个数是一两个,这样一层一层查找下去,我们就可以做到对数级别的时间复杂度,就可以媲美二分查找。
在这里插入图片描述
这样的话,肉眼可见的我们需要消耗更多的空间去存储这些多余的索引层的节点,那么具体需要多多少空间的,我们以最理想的情况下举例,第一层索引层是实体层的一半,然后第二层索引层是第一层索引层的一半,(n/2 + n/4 + n/8 + … + 1)差不多就是多出一个n的样子,所以跳表理论上要多消耗O(n)的内存空间。但是我们的索引层只做索引,可以不存具体的值,这样就可以节省不少的内存空间了

一些操作的时间复杂度

  • 查询:O(logN)
  • 插入:O(logN)
  • 删除:O(logN)
跳表相关问题

为什么没有用红黑树替代跳表?

  • 在Redis中会有大量的范围查询的功能需求,红黑树在范围查找这方面的效率不如跳跃表,所以在范围查询方面使用跳跃表更优
  • 红黑树的数据结构更为复杂,红黑树的插入和删除操作可能会引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速
  • 一般来说,红黑树每个节点包含2个指针,而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

关于什么时候要向上建立索引?
我们在新加一个节点的时候,可以考虑一下,现在最多有几层索引,如果现在一层都没有,你上来就建了8层,就很没有必要,索引新加一个节点最好最多再往上简历一层索引,那么具体多少层,可以这么想,在第一层建立的概率是1/2,第二层建立的概率是1/4,第三层建立的概率是1/8…代码怎么实现呢?

/**
 * 跳表
 *
 */
public class SkipList {
    double factor = 0.5d;
    int maxLevel = 16;
    int currentMaxLevel = 1;
    Node head = new Node(maxLevel, -1);

    public Node find(int value) { // 查找目标值
        Node p = head;
        // 从最大层开始查找,找到前一节点,通过--i,移动到下层再开始查找
        for (int i = maxLevel - 1; i >= 0; --i) {
            while (p.nextNodes[i] != null && p.nextNodes[i].value < value) {
                // 找到前一节点
                p = p.nextNodes[i];
            }
        }
        if (p.nextNodes[0] != null && p.nextNodes[0].value == value) {
            return p.nextNodes[0];
        } else {
            return null;
        }
    }

    public boolean insert(int value) {
        // level代表这个节点存在于几层链表中
        int level = randomLevel();
        if (level > currentMaxLevel) {
            level = ++currentMaxLevel;
        }
        Node newNode = new Node(level, value);
        Node point = head;
        for (int i = currentMaxLevel - 1; i >= 0; i--) {
            while (point.nextNodes[i] != null && point.nextNodes[i].value < value) {
                point = point.nextNodes[i];
            }
            if (level > i) {
                Node temp = point.nextNodes[i];
                point.nextNodes[i] = newNode;
                newNode.nextNodes[i] = temp;
            }
        }
        return true;
    }

    private int randomLevel() {
        int level = 1;
        while (level < maxLevel && Math.random() > factor) {
            level++;
        }
        return level;
    }

    public void printAll(int level) {
        if (level > currentMaxLevel) {
            throw new RuntimeException("还没有到这个层数");
        }
        Node point = head;
        while (point.nextNodes[level - 1] != null) {
            System.out.print(point.nextNodes[level - 1] + " ");
            point = point.nextNodes[level - 1];
        }
        System.out.println();
    }

    static class Node {
        int value; // 节点值

        /**
         * 这个节点在某一层的下一个节点的集合,比如nextNodes[2]就是node在第二层的下一个节点
         */
        Node[] nextNodes;

        public Node(int level, int value) {
            nextNodes = new Node[level];
            this.value = value;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    '}';
        }
    }

    public static void main(String[] args) {
        SkipList skipList = new SkipList();
        for (int i = 0; i < 1000; i++) {
            skipList.insert(i);
        }
        for (int i = skipList.currentMaxLevel; i > 0; i--) {
            skipList.printAll(i);
        }
    }
}

3. 常用命令

3.1 简单动态字符串

  1. 设置键值

SET name myname
SETNX mykey “lock” // 分布式锁,值被设置返回1,值没被设置返回0 (已经有了,则设置失败)

  1. 获取键值

GET name // 返回myname

  1. 设置过期时间

EXPIRE name 60 // name的过期时间为60秒

  1. 查看过期时间

TTL name // 返回 54 即剩余时间,如果返回-1 则无过期时间

  1. 判断键是否存在

EXISIT name // 存在返回1 否则返回0
TYPE name // 返回string 说明存在 否则返回none
TTL name // 返回 -1 键值存在,无过期时间。返回-2 键值不存在

  1. 删除键

DEL name

  1. 自增、自减

INCR cnt
DECR cnt

3.2 哈希表

  1. 设置hash值

HSET user name “LiLei” // 将user 表中name设置为Li Lei age设置为 29
HSET user age 29

  1. 获取hash值

HGET user name // 返回 LiLei

  1. 获取所有hash值

HGETALL user

  1. 删除hash值

HDEL user age
HGET user // 只返回name LiLei

  1. 检查hash值是否存在

HEXIT user name // 存在返回1 否则返回0

  1. 获取所有键

HKEYS user // 如果没删除 返回 name age

  1. 获取所有值

HVALS user // 如果没删除 返回 LiLer 29

3.3 List

  1. 链表头部插入元素

LPUSH mylist c1
LPUSH mylist c2
LPUSH mylist c3

  1. 链表尾部插入元素

RPUSH mylist c4
RPUSH mylist c5 //返回长度5
c3 c2 c1 c4 c5

  1. 获取链表长度

LLEN mylist //返回5

  1. 获取链表元素

LINDEX mylist 0 // 返回c3

  1. 获取列表的范围

LRANGE mylist 1 3 // 返回 c2 c1 c4

  1. 删除列表中的元素

LREM mylist 0 c3

  1. 弹出列表中的元素

LPOP mylist //弹出并返回列表的第一个元素c2
RPOP mylist //弹出并返回列表的最后一个元素c5

  1. 获取所有元素

LRANGE mylist

3.4 SET

  1. 向集合中添加元素

SADD myset ctt

  1. 获取集合中的元素数量

SCARD myset

  1. 获取集合中的所有元素

SMEMBERS myset // 这将返回一个包含所有元素的列表。

  1. 判断元素是否在集合中存在

SISMEMBER myset ctt //如果元素存在于集合中,则返回1;否则返回0。

  1. 从集合中删除元素

SREM myset ctt2

  1. 求多个集合的交集、并集或差集

SINTER set1 set2 set3
SUNION set1 set2 set3
SDIFF set1 set2

3.5 ZSET

  1. 向有序集合中添加元素

ZADD myzset 100 ctt //将值为“ctt”的元素添加到名为“myzset”的有序集合中,且它的分数为100

  1. 获取有序集合中的元素数量

ZCARD myzset

  1. 获取有序集合中指定范围内的元素

ZRANGE myzset 0 2 // 获取名为“myzset”的有序集合中排名在1到3之间的元素

  1. 获取有序集合中指定分数范围内的元素

ZRANGEBYSCORE myzset 0 20 //要获取名为“myzset”的有序集合中分数在0到20之间的元素,

  1. 获取有序集合中指定元素的排名

ZRANK myzset ttc

  1. 获取有序集合中指定元素的分数

ZSCORE myzset ctt

  1. 从有序集合中删除元素

ZREM myzset a

  1. 查看有序集合中所有元素

ZRANGE myzset 0 -1

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值