Redis基础篇

2022-05-10 09:42:33 浏览数 (1)

Redis基础篇

一.起源

Redis作者antirez,2008年做网站访问记录,统计每天的用户量,页面浏览数,访客的IP,访客使用的操作系统等等。最开始用的是MySQL,实在太慢了,自己就写了基于内存的List,就是Redis。

为什么叫Redis? 全称Remote Dictionary Service。翻译成中文远程字典服务。

二.Redis的定位与特性

1.SQL与NoSQL

SQL关系型数据库特点:
  1. 以表格的形式并基于行存储数据,是一个二维的模式;
  2. 存储的是结构化的数据,数据存储具有固定的格式,数据需要适应表结构;
  3. 表于表之间存在关联关系;
  4. 大部分关系型数据库都支持 **SQL** 的操作,支持复杂的关联查询;
  5. 通过支持 **ACID** 来提供严格或实时的数据一致性。
SQL关系型数据库缺点:
  1. 如果要实现扩容,就只能向上(垂直)扩容;例如磁盘限制了数据的存储,就需要通过扩展硬件的方式扩大磁盘容量。不支持动态扩缩容,水平扩容需要复杂的技术来实现,例如分库分表;
  2. 表结构修改困难导致存储的数据格式受到限制;
  3. 在高并发和高数据量的情况下,关系型数据库通常会把数据持久化到磁盘,但磁盘的读写压力会比较大。

为了解决关系型数据库带来的一些列问题,就出现了非关系型数据库。**NOSQL** 最开始不提供 **SQL**** **数据库,在后来慢慢地发生了变化。

NoSQL非关系型数据库特点:
  1. 存储非结构化的数据(文本、图片、音频、视频等);
  2. 表与表之间没有关联,可扩展性强;
  3. 遵循 **BASE** 理论来保证数据的最终一致性;
    1. Basically Available 基本可用
    2. **Soft-State**:软状态
    3. **Eventually Consistent** 最终一致性
  4. 支持海量数据的存储和高并发的高效读写操作;
  5. 支持分布式,对数据进行分片存储,扩缩容简单
NoSQL非关系型数据库的分类:
  1. **Key-Value** 形式存储数据(**Redis、Memcache**);
  2. 文档存储(**MongoDB**);
  3. 列存储(**HBase**);
  4. 图存储(Neo4j
  5. 对象存储以及 **XML** 存储等

2.Redis 的特性

1、为什么要把数据放在内存中?

1)内存的速度更快,10w QPS 2)减少计算的时间,减轻数据库压力

2、如果是用内存的数据结构作为缓存,为什么不用HashMap或者Memcached?

1)更丰富的数据类型 2)支持多种编程语言 3)功能丰富:持久化机制、内存淘汰策略、事务、发布订阅、pipeline、lua 4)支持集群、分布式

3、Memcached和Redis的主要区别是什么?

Memcached只能存储KV、没有持久化机制、不支持主从复制、是多线程的。

3.Redis安装启动

1、服务安装

安装好 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

2、服务启动

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。

3、客户端工具

Redis Desktop Manager

4、基本操作

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

代码语言:javascript复制
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代表是不成功的。

代码语言:javascript复制
#存值(如果对同一个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

4.Redis基本类型

最基本也是最常用的数据类型就是String。set和get命令就是String的操作命令。 Reids的字符串被叫做二进制安全的字符串,为什么是Binary-safe Strings呢? 下面对于所有的数据类型我们都会从4个维度来分析:存储类型、操作命令、存储结构、应用场景。

1、String字符串**

存储类型 可以用来存储INT(整数)、float(单精度浮点数)、String(字符串)。

操作命令

代码语言:javascript复制
# 获取指定范围的字符
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存储了指向下一个键值对的指针。

代码语言:javascript复制
# 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行

代码语言:javascript复制
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再通过一个指针指向实际的数据结构,比如字符串或者其他。

redisObject定义:
代码语言:javascript复制
//源码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:

那么实际的编码到底是什么呢?

代码语言:javascript复制
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个字节的字符串。

代码语言:javascript复制
/* object.c*/
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
问题 1、什么是 SDS?

Redis 中字符串的实现。   在 3.2 以后的版本中,SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 25=32byte,28=256byte,216=65536byte=64KB,232byte=4GB。

代码语言:javascript复制
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[];/* 字符串真正的值 */
};
问题 2、为什么 Redis 要用 SDS 实现字符串
代码语言:javascript复制
因为C语言没有字符串类型,只能用字符数组char[]实现。
1.使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
2.如果要获取字符长度,必须遍历字符数组,时间复杂度O(n)。
3.C字符串长度的变更会对字符数组做内存重分配。
4.通过从字符串开始到结尾碰到的第一个''来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。

SDS的特点:

代码语言:javascript复制
1、不用担心内存溢出问题,如果需要会对SDS进行扩容。
2、获取字符串长度时间复杂度O(1),因为定义了len属性。
3、通过"空间预分配"(sdsMakeRoomFor)和"惰性空间释放",防止多次重分配内存。
4、判断是否结束的标志是len属性,可以包含''(它同样以''结尾是因为这样就可以使用C语言中函数库操作字符串的函数了)。

存储二进制:BytesTest.java

c字符数组

SDS

获取字符串长度的复杂度为O(N)

获取字符串长度的复杂度为O(1)

API是不安全的,可能会造成缓冲区溢出

API是安全的,不会造成缓冲区溢出

修改字符串长度N次必然需要执行N次内存重分配

修改字符串长度N次最多需要执行N次内存重分配

只能保存文本数据

可以保存文本或者二进制数据

可以使用所有<string.h>库中的函数

可以使用一部分<string.h>库中的函数

问题 3、embstr和raw编码的区别?为什么要为不同大小设计不同的编码?
代码语言:javascript复制
embstr的使用只分配一次内存空间(因为RedisObject和SDS是连续的)。而raw需要分配两次内存空间(分别为RedisObject和SDS分配空间)。
因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。

而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存是,整个RedisObject和SDS都需要重新分配空间,因此Redis中的embstr实现为只读(这种编码的内容是不能修改的)。

问题 4、int和embstr什么时候转化为raw?

1、int数据不再是整数–raw 2、int大小超过了long的范围(2^63-1)–embstr 3、embstr长度超过了44个字节–raw

代码语言:javascript复制
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个字节。

问题 5、当长度小于阈值的时候,会还原吗?
代码语言:javascript复制
关于Redis内部编码的转换,都符合以下规律:编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新set)。
问题 6、为什么要对底层的数据结构使用redisObject进行一层包装呢?
代码语言:javascript复制
其实无论是设计redisObject,还是对存储字符设计这么多的SDS,都是为了根据存储的不同内容选择不同的存储方式,这样可以实现尽量的节省内存空间和提升查询速度的目的。
String应用场景:
  • 缓存
    • String类型,缓存热点数据。例如网站首页、报表数据等。
    • 可以显著提升热点数据的访问速度。
  • 分布式数据共享
    • String类型,因为Redis是分布式的独立服务,可以在多个应用之间共享;
    • 例如:分布式Session
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • 分布式锁
    • String类型的setnx方法,只有不存在时才能添加成功,返回true。
代码语言:javascript复制
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);
}
  • 全局ID
    • INT类型,INCRBY,利用原子性
    • incrby userid 1000 (分库分表的场景,一次性拿一段)
  • 计数器
    • INT类型,INCR方法
    • 例如:文章的阅读量,微博点赞数,允许一定的延迟,先写入Redis再定时同步到数据库。
  • 限流
    • INT类型,INCR方法
    • 以访问者的IP和其他信息作为key,访问一次增加一次计数,超过次数则返回false。

总结:利用redis本身的特性,String内存的存储内容,以及提供的操作方式,我们可以用来达到很多的业务目的。

如果一个对象的value有多个值的时候,怎么存储?

例如用一个key存储一张表的数据。

序列化的方式?例如JSON/Protobuf/XML,会增加序列化和反序列化的开销,并且不能单独获取、修改一个值。 可以通过key分层的方式来实现,例如:

代码语言:javascript复制
mset student:1:sno GP16666 student:1:sname 彪哥 student:1:company 京东
# 获取值的时候一次获取多个值,用list接受:
mget student:1:sno student:1:sname student:1:company

缺点: key太长,占用的空间太多。那么有没有好的方式呢?

2、Hash哈希

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

注意:前面我们说Redis所有的KV本身就是键值对,用dictEntry实现的,叫做外层的哈希、现在我们分析的是内层哈希。

Hash操作指令
代码语言:javascript复制
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行的注释:

代码语言:javascript复制
hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
object encoding h2
object endoding h3

那么上面entry的内容呢?ziplist.c

所以展开应该是这个样子的:

编码有哪些?

代码语言:javascript复制
#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配置

代码语言:javascript复制
hash-max-ziplist-value 64 //ziplist中能最大存放的值长度
hash-max-ziplist-entries 512 //ziplist中最多能存放的entry节点数量

如果超过这两个阈值的任何一个,存储结构就会转换成hashTable。 总结:字段个数少,字段值少,用ziplist。

hashTable(dict)

在Redis中,hashTable被称为字典(dictionary)。 前面分析,Redis的KV结构是通过一个dictEntry来实现的。 在hashTable中,又对dictEntry进行了多层的封装。

源码位置:dict.h 47行。

代码语言:javascript复制
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里面)

代码语言:javascript复制
/* 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里面:

代码语言:javascript复制
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:为什么要定义两个哈希表,其中一个不用呢?

代码语言:javascript复制
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)

代码语言:javascript复制
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 :商品数量。

3、List列表(有序)
存储类型

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

list操作命令
代码语言:javascript复制
lpush queue a
lpush queue b c
lpush queue d e
lpop queue
lpop queue
lindex queue 0
lrange queue 0 -1
存储原理(quicklist)

在早期的版本中,数据量较小时用ziplist存储(特殊编码的双向链表),达到临界值时转化为linkedList进行存储,分别对应OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_LINKEDLIST。

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

代码语言:javascript复制
object encoding queue
quicklist

总体结构: quciklist.h 105行

代码语言:javascript复制
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行

代码语言:javascript复制
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,可以设置超时时间(单位:秒)。

代码语言:javascript复制
blpop queue
brpop queue

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

队列:先进先出 :rpush blpop , 左头右尾,右边进入队列,左边出队列。 栈:先进后出:rpush brpop 总结: List存储有序的内容,用quicklist实现,本质上是数组 链表。 hashTable也是数组加链表,只是内部编码结构不一样。

4、Set集合
存储类型

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

代码语言:javascript复制
//添加一个或者多个元素
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行

代码语言:javascript复制
typedef struct intset{
      unint32_t encoding; //编码类型 int16_t 、int32_t 、int64_t
      unint32_t length;  //长度 最大长度:2^32
      int8_t contents[]; //用来存储成员的动态数组
} intset;

如果不是整数类型,就用hashTable(数组 链表的存储结构)。 如果元素个数超过512个,也会用hashTable存储。跟一个配置有关:

代码语言:javascript复制
set-max-insert-entries 512

QA: set的key没有value,怎么用hashTable存储? value这里存储的是null。

应用场景

抽奖 随机获取元素: spop myset 点赞、签到、打卡 商品标签 商品筛选

代码语言:javascript复制
//获取差集
sdiff set1 set2
//获取交集(intersection)
sinter set1 set2
//获取并集
sunion set1 set2

用户关注、推荐模型

5、ZSet有序集合
存储类型

sorted set 存储有序的元素。每个元素有个score ,按照score从小到大排名。 score 相同时。按照key的ASCII码排序。 数据结构对比:

数据结构

是否允许重复元素

是否有序

有序实现方式

列表list

索引下标

集合set

有序集合zset

分值score

操作命令
代码语言:javascript复制
//添加元素
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存储。

代码语言:javascript复制
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

那么什么是skiplist(跳表)? 先看一下有序链表:

在这样一个链表中,如果我们要查找某个数据,那么需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到一个比给定数据大的节点位置。时间复杂度为O(n)。同样,我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。二分查找法只适用于有序数组,不适用于链表。 假如我们每相邻两个节点增加一个执政,让指针指向下下个节点(或者理解为有元素进入了第二层)。

这样所有新增加的链表连成一个新的链表,但它包含的节点个数只有原来的一半。 那么那些元素会进入到第二层呢?在插入一个数据的时候,决定要放到哪一层,取决于一个算法,源码t_zset.c 122行

代码语言:javascript复制
int zslRandomLevel(void){
      int level = 1;
      while((random() && 0xFFFF) < ZSKIPLIST_P * 0xFFFF)
            level  = 1;
      return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

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

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

  1. 15首先和3比较,比它大继续向后比较。
  2. 但是15和19比较时,要比19小,因此回到下面的链表(原链表),与7在第一层的下一个节点11比较。
  3. 15要比11大,沿下面的指针继续向后和19比较。15比19小,说明待查数据23在原链表中不存在。 在这个查找过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了。需要比较的节点数大概只有原来的一半。这就是跳跃表。 这里为什么不用AVL树或者红黑树?因为skiplist更加简洁。 因为level是随机的,得到的skiplist可能是这样的,有些在第四层,有些在第三层,有的在第二层、第一层。 我们看一下Redis里面skiplist的实现: 源码: server.h 904行
代码语言:javascript复制
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

6、其他数据结构

Bitmap Geospatial Hyperloglogs Streams

0 人点赞