2022-10-06
Redis持久化
2022-09-01
本次更新没有内容,就是调整一下目录结构。
2022-06-04 ????
Redis
管理命令:trollface:Redis
底层实现机制:trollface:2022-08-03????
2022-8-19????
关于
Redis
本身的一些语法、命令的使用。
Redis
是一款基于键值对的NoSQL
数据库,它的值支持多种数据结构,比如,字符串,哈希,列表,集合,有序集合(sorted sets
)等。
Redis
将所有的数据都存放在内存中,所以它的读写性能方面堪称秀儿。同时,它还可以将内存中的数据以快照或日志的形式保存在硬盘上,以保证数据的安全性。
Redis
典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等等。
传送门:
msi
文件,点击安装,一路 next
即可。redis-cli
回车。默认内置了16个库(0-15) 切库命令:
select [索引]
命令
select 1
如果不需要之前的操作数据,执行
flushdb
可以将其刷掉。 命令 flushdb
以键值对的形式存储字符串形式的数据,如果需要存储以’-‘连接的字符串,用冒号【:】分隔。 示例:存储键为text-count,值为1的字符串。 set test:count 1
获取存储的字符串数据也很简单,一条
get
命令即可。 示例:取到key
为test:count
的value
。 get test:count 返回: "1"
redis
支持对存储的字符串数据进行一些基本的修改操作。 示例:将如上字符串加一、减一。 #加一 incr test:count #返回 2 #--------------------- #减一 decr test:count #返回 1
存储哈希的命令:
hset
由于哈希值本身也是kv的形式,所以需要两次进行key_value的输入。 示例:存于一个id为1,用户名为Tisox
的用户数据,名为【test-user】。 # 存入用户id hset test:user id 1 #提示:表示操作成功的提示 (integer) 1 #存入用户名 hset test:username Tisox #提示 (integer) 1 #------------------------------
和字符串存取类似,哈希的取值命令为
hget
示例:对上述存入的test-user信息进行读取。 #取id hget test:user id #返回 "1" #----------------------------------- #取用户名 hget test:user username # 返回 "Tisox"
redis
里的列表比较特殊,它存储数据的方式可以从左右两边进行,可以视为一个横向的容器。 容器的左右两边都可以进行存取操作。并且列表是有序可重复的。
从左边存入:101 102 103 # 从左边存入101 102 103 lpush test:ids 101 102 103 # 提示 (integer) 3 # 查看其长度 llen test:ids # 返回 (integer) 3
列表支持不同的方式进行查看 # 按索引查看 lindex test:ids 0 # 返回 "103" lindex test:ids 2 # 返回 "101" # 按范围查看 lrange test:ids 0 2 # 返回 1) "103" 2) "102" 3) "101"
由于列表的特性,其取值也可以看作是队列或者栈的出队、出栈等操作。 # 从右侧弹出一个值 lpop test:ids #返回 "101" lpop test:ids #返回 "102"
集合(sets),无序且不重复。
往集合中存入一个key为test-language ,值为
Java
,C++
,Python
的数据。 # 存入 add test:language Java C++ Python # 提示 (integer) 3 # ----------------------------------- # 统计集合中有多少个元素 scard test:language # 返回 5 #------------------------------------ # 从集合中随机弹出一个元素:应用场景:【抽奖业务】 spop test:language # 返回 "Java" spop test:language # 返回 "Python"
查看集合中的元素 # 查看当前集合中的剩余元素 smembers test:language # 返回 "C++" # 因为上面已经随机弹出了另外两个数据,所以就剩下了"Python"
给每一个存入的值附加一个分数,按照该分数进行排序的集合。
添加学生数据 # 添加学生以及其分数 zadd test:students 10 aaa 20 bbb 30 ccc 40 ddd 50 eee # 提示 (integer) 5 # 统计集合元素个数 zcard test:students # 返回 (integer) 5 # 查询某个值的分数 zscore test :students ccc # 返回 "30" # 返回目的按照分数排行(0、1、2、3、4.....) zrank test:students ccc # 返回 2 # 按照排序,取0-2大小的值 zrange test:students 0 2 # 返回 1) "aaa" 2) "bbb" 3) "ccc"
字面意思,这些命令针对全局生效。
# keys *
keys *
# 返回
1) "test:user"
2) "test:language"
3) "text:count"
4) "test:ids"
5) "test:students"
# ----------------------------------
# keys test*
keys test*
# 返回
1) "test:user"
2) "test:language"
3) "text:count"
4) "test:ids"
5) "test:students"
# ------------------------------------
# 查看类型
type test:user
# 返回
hash
# 查看某个key是否存在
exists test:user
# 返回
1 #表示存在
# 输出key
del test:user
# ---------------------------------------
# 设置有效期:秒为单位,过期自动删除
expire test:students 10
Redis
前面所有的命令都是基于key的基础上进行的,那么又怎样取管理和操作这些key呢?
以下是一些用来管理 key
的常用命令:这里只作一个列举,不会全部进行演示。
# 选择库
select index
index为redis库的索引,共有0-15个索引16个库,默认启用第一个库,索引为0。
# 查看全部的key
keys *
# 通过通配符进行匹配查看
# 查看所有key中以my开头的key
keys my*
注意,如果数据量很大的情况下,一般不建议直接使用
keys*
进行查看,该操作的时间复杂度是O(N),的,数据量太大可能会导致阻塞崩溃。
# 查看当前库中有多少key
dbsize
# 清理库中的key
flushdb
这是一个危险且强大的命令,如果使用不当,可能造成将所有的key全部删除,是不可逆的操作,在使用时应该三思。
# 查看key的数据类型
type key
# 判断某个key是否存在
exists key
# 随机返回一个key
randomkey
# 给key重命名
rename key newkey
建议在使用时结合
nx
参数使用。
# 删除一个或者多个key
del key1 key2 .....
# 或者
unlink key1 key2 ....
del
带阻塞,unlink
则没有。
# 渐进式遍历key
# 基础用法,跟一个整数作为游标,表示从何处开始遍历
scan 0
# 进阶用法
scan 0 match counter* count 10
counter*
表示匹配的规则,所有以该字符开头的key,count
后的数值表示每轮遍历的数量。为了方便演示
scan
的用法,我们需要有一定数量的key。可以使用redis
自带的压力测试工具来生成这些测试数据。 这个工具存在于/usr/bin/redisbenmark
。
# redisbenchmark的使用命令
redis-benchmark -c 5 -n 100 -r 1000 -a reids密码
上面的命令中:
5
表示启动的客户端数量100
表示请求数量1000
表示插入的数据量密码
表示你登录redis客户端的密码
以上就是 管理redis
部分的全部内容
在redis中,每一种数据类型的底层都是由一种或者多种编码进行实现的,具体如下:
int embstr raw
ziplist linkedlist quicklist
ziplist hashtable
intset hashtable
ziplist skiplist
可以看到,某一种编码可以同时应用在不同的数据类型的实现中。
在前面有提到过一个命令:
type key
这是用来查看某个key的数据类型,这里的类型即指的是上面诸如 String,hash,set....
。而不能查看他们对应源码实现上所用的数据编码。可以通过下面的命令查看:
object encoding key
具体的源码会在下一节中进行展开,这里介绍redis源码的结构。
数据结构 | 数据类型 | 数据库 | 服务端与客户端 | 其他 |
---|---|---|---|---|
动态字符串sds.c | 对象object.c | 数据库db.c | 事件驱动ae.c,ae_epoll.c | 主从复制replication.c |
压缩列表ziplist.c | 字符串t_string.c | 持久化rdb.c,aof.c | 网络连接anet.c,networking.c | 哨兵sentinel.c |
快速列表quicklist.c | 列表t_list.c | 服务端server.c | 集群cluster.c | |
整数集合intset.c | 哈希t_hash.c | 客户端redis-cli.c | 其他类型hyperloglog.c,geo.c | |
字典dict.c | 集合t_set.c | |||
有序集合t_zset.c |
简单动态字符串(SDS),是
Simple Dynamic String
的缩写,是Redis
内部自定义实现的一种数据类型。在Redis
数据库内部,包含字符串的键值对在底层都是由SDS
实现的,它还被用于缓冲区的实现,如AOF
缓冲区、客户端的输入缓冲区。
set text "hello world"
rpush names "john" "lucy" "tony"
sadd users "liubei" "guanyu" "zhangfei"
诸如上面的类型,底层实现都用到了SDS.
学过或者了解过C语言的都知道,C语言它是有字符串这种数据类型的,那为什么 Redis
不是直接使用原生的字符串类型,而是自己自定义呢?
char
类型的数组中,进而来表示字符串。\0
来标记字符串的结束,空字符串不是数字0,它的 ASCII
码值为0;从上面的信息中,总结了以下几点原因,导致不能直接使用原生字符串,而是需要自定义。
C字符串的实现中是不记自身长度的,想要获取字符串的长度就必须遍历整个字符串来统计,这种方式复杂度为
O(n)
,但要知道,在Redis
中,获取字符串的长度是一个操作频繁的需求,因此为了提升性能,必须降低操作的复杂度。
几乎每次修改C字符串,程序就要对保存的这个字符串的数组重新分配一次内存空间。
因为C字符串以空字符串结尾,所以不适合保存二进制数据(内部可能携带空字符串)。
对源码这里不作深入的研究,只作了解。
鉴于几个比较典型的版本来分别看一下他们在
SDS
的实现的源码中是怎样的逻辑。
Redis3.2之前的是实现
下面是v3.0中对sds结构的自定义实现源码:这是sds的头文件sds.h,具体的实现逻辑在sds.c中。
/*sds.h*/
struct sdshdr {
//已使用的字节数量
unsigned int len;
//未使用的字节数量
unsigned int free;
//保存字符串的数组
char buf[];
};
上面代码的意思大致是这样的,默认会开辟一个buf[]字符数组来存储需要的字符串,该字符数组的长度为
len
的长度加上空闲的空间长度 free
。len
用来实时存储并记录当前已使用掉的字符空间,它可以实时的返回字符串的长度,从而可以将获取长度这个操作的复杂度降到常数的 O(1)
级别。
而
free
的作用相当于一个预留空间,这部分空间未必是一开始就能全部用上的,可能会在用户修改字符串数量之后用上,通过这种空间预分配和惰性空间释放修改字符串时所需的内存分配次数。
此外,SDS不会对buf中的数据作任何的限制,因为它采用len属性来判定字符串是否结束,它依然以空字符(
\0
)结尾,这样其内部可以方便的重用一部分C字符串库中的函数。
预分配
用于优化增长操作,即不仅为其分配存放字符串所需的空间,还会为其分配一定大小的额外空间,如果修改后的SDS长度小于
1MB
,则分配的未使用空间与len
相同,否则分配的未使用空间为1MB
。
惰性释放
用来优化缩短操作,当检测到SDS缩短时,程序不会立即重新分配内存,而是使用
free
属性记录这些字节。也就是将缩短后空余出来的空间加到free
中,以备下一次增长时使用。
不足之处
在该版本的源码实现中,除了具备上述优点之外,也是有不足之处的,比如 len
,free
,都是无符号int类型,他们在C语言中一般占用4个字节的空间,但对于较短的字符串来说,这免不了造成了一定空间的浪费。
为什么会这么说,一个 len
不过4个字节,加上 free
也不过8个字节,这么就浪费空间了呢?别忘了,在 Redis
核心中,它的数据一般都是存在内存中的,内存对它来说确实值得 斤斤计较
,再者,SDS
实现的数据类型在整个的 Redis
数据结构中占用的比例是相当大的,当数量达到一定量级,浪费的空间可不是几个字节能搞定的。
上面提到了这种方案的一些弊端,那么在后续的版本中,自然也得到了优化,毕竟写出 Redis
的那些大佬可不是盖的,我们能想到的,他们自然也想到了。所以下面是优化后的版本,也就是在 v3.2
中相同部分的实现源码。
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
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 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
主要的优化方向:
通过字符串长度,将其分为5种类型,分别为1字节、2字节、4字节、8字节、小于1字节。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; //使用的字节数量
uint8_t alloc; //全部的字节数量
unsigned char flags; //低3位存储类型,高5位预留
char buf[]; //存放实际的内容
};
在 v3.2
版本的优化中,针对每一个长度定义了不同的结构体处理,还新增了一个 char
类型的 flag
属性。这个属性是用来标记数据类型的,属性占1个字节(8位),其中3位用来标记类型,剩余的5位作为预留空间待用。
==在处理小于1字节的情况上,它的结构体是定义如下==:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; //低3位存储类型,高5位存储长度
char buf[];//存放实际的内容
};
==注意到==,这里去掉了 len
和 alloc
属性不代表它不存储长度,而是将长度和类型标记合二为一。既然存储的长度小于1字节,那么在8位长度中,用前3位来标记数据类型,后5位存储长度是足够的,因此没必要开辟额外的属性。这样的作法更有效的利用空间。
在每一个结构体的定义中有这样一段修饰符:
__attribute__ ((__packed__))
这是用来将结构体中内存的分配规则指定为==按照1字节来对齐==,如此可以进一步的节约内存。而在默认不作处理的情况下,它对结构体内存的分配规则是按照其中各个属性的字节最小公倍数来对齐的,相对比较浪费。
整数集合(intset)是一个有序的、存储整型数据的结构;其中的元素按照值由小到大的顺序排列。 它可以保存
int16_t,int32_t,int64_t
类型的整数值,在存储数据时,整数集合可以保证内部不出现重复数据。
在 Redis
中并没有大范围的使用到整数集合这样的编码,只有当一个set只包含整数元素,并且这个set的元素数量不多时,Redis
才会使用整数集合作为set的底层实现。这个的数量是可以通过配置文件进行配置的。
集合的升级与降级
当添加新的元素,其类型比现有元素类型都长时,集合需要先升级再添加。
整数集合不支持降级,一旦对数组进行了升级,编码就会一直保持升级的状态。
int16_t,int32_t,int64_t
,最简单的方法是使用 int64_t
,但这样显然浪费内存空间,而升级操作可以尽量的节约内存的使用。
下面是 v3.2
版本中对该结构的定义;
typedef struct intset {
uint32_t encoding;//编码类型
uint32_t length;//元素数量
int8_t contents[];//元素数组
} intset;
注意其中的元素数组的类型虽然声明是 int8_t
的,但实际上不是说只能存 int8_t
类型的元素,具体的,在后面的源码中,对它作了一些设计。
在 intset.c
中:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
也就是说 contents
数组的实际类型取决于 encoding
属性的值。
上面的常量定义可以在 intset.c
中找到。
字典又称散列表,是一种用来存储键值对的数据结构。C语言没有内置这种数据结构,所以
Redis
构建了自己的字典规范。 字典在Redis
中的应用广泛,redis数据库底层就是采用它实现的,字典也是集合,哈希类型的底层实现之一;redis的哨兵模式,就是以字典存储所有的主从节点的。
Redis
字典实现主要涉及三个结构体:字典、哈希表、哈希表节点。其中,每一个哈希表节点保持一个键值对,每一个哈希表由多个哈希表节点构成,而字典则是对哈希表的进一步封装。看一下哈希表在 dict.h
中的定义源码:
dict
typedef struct dict {
//字典类型,内置若干特定的操作函数
dictType *type;
//该字典特有的私有数据
void *privdata;
//哈希表数组,固定长度为2
dictht ht[2];
//rehash标识,存储rehash的偏移量,默认-1
long rehashidx;
//记录绑定在此字典上,正在运行的迭代器数量
int iterators;
} dict;
dictht
typedef struct dictht {
//节点数组
dictEntry **table;
//数组大小
unsigned long size;
//掩码(size-1)
unsigned long sizemask;
//已用节点数量
unsigned long used;
} dictht;
dictEntry
typedef struct dictEntry {
//键
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;//值
//下一节点
struct dictEntry *next;
} dictEntry;
大致的结构关系:
关于哈希算法的具体逻辑其实和语言无关,核心思想是类似的。
向字典中添加新的键值对时,程序需要先根据键来计算出对应的一个哈希值,再根据哈希值计算出索引值,最后将此键值对封装在哈希表节点中后,放到节点数组的指定索引上,关键步骤参考如下代码:
// 使用哈希函数计算键的哈希值
hash=dict->type->hashFunction(key);
//使用哈希值和掩码,计算索引值
//等价于哈希值和哈希表容量取余,使用位运算提高效率
index =hash & dict->ht[x].sizemask;
上面只是一个大体的逻辑关系,具体的源码实现可以参考dict.c源码
键冲突问题
Redis
采用链表来解决冲突,即使用next指针将这些节点链接起来,形成单向链表。Redis
的哈希表中没有设计表尾指针,每次添加新节点时都是将新节点插入到表头的位置,而非表尾追加。在
Redis
中,哈希表的扩容和缩容是通过rehash实现的,执行rehash
的大致步骤如下:
ht[0]
中的数据迁移到 ht[1]
上。ht[1]
哈希表的指定位置上。ht[1]
哈希表晋升为默认哈希表ht[0]
,再交换 ht[0]
和 ht[1]
的值,为下一次 REHASH
做准备。触发rehash的条件:
bgsave
或者 bgrewriteof
命令,并且哈希表的负载因子大于1;只要满足上面两个条件之一,就会触发rehash。
其中,负载因子的计算公式:load_factor=\frac{ht[0].used}{ht[0].size}
另外,当哈希表的负载因子小于0.1时,程序会自动开始对哈希表执行收缩操作。
rehash的详细步骤
为了避免rehash对服务器性能造成影响,rehash操作不是一次性完成,而是渐进式的分为多次进行。详细过程如下:
ht[1]
分配空间,让字典同时持有 ht[0]
和 ht[1]
两个哈希表。rehashidx
设置为0,表示将开始 rehash
操作。rehash
期间,每次对字典执行添加、删除、修改、查找等操作时,程序除了执行指定的操作之外,还会顺带将 ht[0]
中位于 rehashidx
上所有的键值对迁移到 ht[1]
中,再将 rehashidx
的值加一。ht[0]
上所有的键值对都被迁移到 ht[1]
上,此时程序将 rehashidx
属性值设置为-1,标识 rehash
操作完成。rehash期间的访问规则
在rehash期间啊,字典会同时持有两个哈希表,此时的访问将按照下面的规则进行处理:
ht[1]
中;ht[0]
中访问要操作的数据,如果不存在则添加到 ht[1]
中访问,再对访问到的数据做相应的处理。链表(
LinkedList
)是一种有序的数据结构,且增删效率较高,同样,C语言中也是没有内置该种数据结构的, 所以Redis
构建了自己的链表实现。
链表在 Redis
中应用广泛:
Redis
服务器采用链表保存多个客户端的状态信息;Redis
客户端输出缓冲区是在链表的基础上实现的;链表的实现主要涉及两个结构体,定义如下,下面的源码也可以在
adlist.h
中找到:
typedef struct listNode {
//前驱节点
struct listNode *prev;
//后继节点
struct listNode *next;
//节点的值
void *value;
} listNode;
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;
链表(双端链表)也算是比较基础的一种数据结构类型了,这里不再赘述。
压缩列表(ziplist),是
Redis
为了节约内存而设计的一种线性数据结构,它是由一系列具有特殊编码的连续内存块构成;一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
在 Redis
中,列表、哈希、有序集合都直接或者间接的使用了压缩列表。
压缩列表相对来说,是一种比较复杂的结构,下面是它的结构示意图:
组成说明:
节点构成
previous_entry_length
该属性以字节为单位,记录当前节点的前一节点的长度,其自身占据1字节或5字节:
在这一个字节内;
为0xFE,之后的四个字节用来保存前一节点的长度;
基于“pel”属性,程序便可以通过指针运算,根据当前节点的起始地址计算出前一节点的起始
地址,从而实现从表尾向表头的遍历操作。
content属性负责保存节点的值(字节数组或整数),其类型和长度则由encoding属性决定。
连锁更新出现的概率很低,压缩列表中需要恰好有多个连续的,长度介于250-253字节的节点;适当控制节点的数量可以消除这种影响,即便出现连锁更新,对性能也不会造成任何影响。
快速列表(quicklist)是
Redis3.2
新引入的数据结构,该结构是链表和压缩列表的结合; 快速列表中的每个节点是一个压缩列表,这种设计能够在时间效率和空间效率之间实现较好的折中。
在 v3.2
之前,列表类型是采用压缩列表以及双向链表实现的,但 v3.2
开始,改用了快速列表作为底层的唯一实现。
下面的结构定义源码在
quicklist.h
中可以找到。
typedef struct quicklistNode {
//前驱节点
struct quicklistNode *prev;
//后继节点
struct quicklistNode *next;
//ziplist
unsigned char *zl;
//ziplist的字节数量
unsigned int sz;
//ziplist的元素个数
unsigned int count : 16;
//编码方式(RAW==1,LZF==2)
unsigned int encoding : 2;
//容器类型(NONE==1 or ZIPLIST==2)
unsigned int container : 2;
//该节点是否被压缩
unsigned int recompress : 1;
//用于测试期间的验证
unsigned int attempted_compress : 1;
//预留字段
unsigned int extra : 10;
} quicklistNode;
typedef struct quicklist {
//头结点
quicklistNode *head;
//尾节点
quicklistNode *tail;
//压缩列表的元素总数
unsigned long count;
//快速列表的节点个数
unsigned int len;
//单个节点的填充因子
int fill : 16;
//不参与压缩的节点个数
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
简单的说,快速列表是由一个带有头尾节点等属性构成的列表,列表每一个元素又是一条双向链表构成,双向链表的每一个元素再由一个个的压缩列表组成。
为了进一步降低 ziplist
占用的内存空间,Redis
允许采用 LZF
算法对 ziplist
进行压缩。该算法的基本思想是,如果数据与前面出现重复的,记录重复位置以及长度,否则直接记录原始数据,压缩后的数据分为多个片段,每个片段包括解释字段和数据字段两个部分,其中数据字段可能不存在。
有序集合的底层可以采用数组、链表、平衡树等结构来实现。数组不便于元素的插入和删除,链表的查询效率低平衡树/红黑树的效率高但是实现复杂; Redis采用跳跃表(skiplist)来作为有序集合的一种实现方案,跳跃表的查询复杂度平均为O(log^{N}),效率堪比红黑树,实现上却比红黑树简单很多。
提到跳跃表之前,先看一下普通链表,链表的插入、删除复杂度为O(1),而查找的复杂度为O(N);明显查找的效率成本是比较高的,特别是在数据量很大的情况下。
比如在上面的这条链表中查找值为60的节点,就需要遍历前面5个节点,这也是就效率拉跨的原因。
而跳跃表的实现原理就是从链表中选取一部分的节点,组成一个新的链表,并以此作为原始链表的一级索引。
再从一级索引中选取部分节点组成一个新链表作为原始链表的二级索引,以此递归。
有了这个结构之后,我们在查找某个节点元素的时候,就会由原来的遍历几乎所有节点变成遍历部分节点甚至无需遍历,直接根据索引定位元素,这样的操作效率会高很多。 就上图来说,同样是查找60这个节点,在链表中需要遍历前面5个节点,而在跳跃表中只需要三次。
跳跃表中查找元素会优先从高层开始查找,若 next
节点值大于目标值,或 next
指针指向 null
,则从当前节点下降一层继续往后找。比如 L2->L1->L0
跳跃表的实现主要涉及两个结构体:zskiplist
,zskiplistNode
,在 v3.0
版本之前,他们被定义在 redis.h
中,该版本之后,被改为在 server.h
中。
typedef struct zskiplistNode {
// 节点数据
sds ele;
// 节点分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层级数组(各节点不一样)
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度(节点间的距离,用于计算排名)
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
// 表头指针、表尾指针
struct zskiplistNode *header, *tail;
// 跳跃表的长度(除表头之外的节点总数)
unsigned long length;
// 跳跃表的高度(除表头之外的最高层数)
int level;
} zskiplist;
可以借助下面的结构图来理解。
即越大的数出现的概率越小。
Redis
数据库中的键值对用对象来表示,键是一个对象,值也是一个对象,使用了 redisObject
结构来表示一个对象,该结构的源码如下:// redis.h, server.h
typedef struct redisObject {
unsigned type:4; // 对象类型
unsigned encoding:4; // 对象编码
unsigned lru:LRU_BITS; // 访问时间
int refcount; // 引用计数
void *ptr; // 指向底层数据结构
} robj;
lru
属性用于记录对象最后一次被程序访问的时间,可用来实现缓存淘汰策略;OBJECT IDLETIME
命令可以打印出某个键的空闲时间,该事件由 lru
计算而来。OBJECT IDELTIME命令的实现是比较特殊的,通过该命令访问键时,不会修改其
lru
属性。
refcount
属性用于记录对象的引用次数:
对象的引用计数可用于实现对象的内存回收以及对象共享功能。
Redis
会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值。当服务器需要用到值为0到9999的字符串对象时,就会使用这些共享对象,而不是创建新的对象。
embstr
和 raw
编码都采用 SDS
来存储字符串。raw
编码会调用2次内存分配函数,分配两块内存空间,分别存储 redisObject
和 SDS
结构。embstr
编码只调用1次内存分配,分配一块内存空间,连续存储 redisObject
和 SDS
结构。redis
中也是作为字符串来存储的,在需要的时候,程序会将字符串值直接转换回浮点数。redis
没有为 embstr
编码的字符串提供修改函数,所以该编码的字符串实际是只读的,对 embstr
编码的字符串执行修改时,程序会将字符串从 embstr
转换为 raw
,再执行修改操作。哈希对象的编码可以是 zipllist
或者 hashtable
;
同时满足下面条件时,哈希对象采用 ziplist
编码,否则采用 hashtable
编码。
可以在 redis
配置文件中通过下面的配置修改上述的触发条件。
hash-max-ziplist-value
hash-max-ziplist-entries
ziplist编码采用压缩列表作为底层实现,hashtable编码采用字典作为底层实现。
在3.2版本之前,列表对象的编码可以是
ziplist
或者linkedlist
;在同时满足下列条件时,列表对象采用ziplist
编码实现,否则采用linkedlist
编码。
同样可以在
redis
配置文件中通过下面的配置修改上述的触发条件
hash-max-ziplist-value
hash-max-ziplist-entries
从3.2开始,列表对象的编码升级为
quicklist
;
ziplist、linkedlist、quicklist编码分别采用压缩列表、双端链表、快速列表作为底层实现
集合对象的编码可以是
intset
或者hashtable
; 同时满足下面条件时,集合对象采用intset
编码,否则采用hashtable
编码;
可以通过修改
set-max-intset-entries
选项改变上述条件。
intset编码采用整数集合作为底层实现,hashtable编码采用字典作为底层实现,字典的键存储字符串,字典的值全部为NULL
有序集合对象的编码可以时
ziplist
或者skipllist
; 同时满足下面条件时使用ziplist
编码,否则使用skiplist
编码。
可以通过修改
zset-max-ziplist-entries,zset-max-ziplist-value
改变上述条件。
ziplist编码底层采用压缩列表实现,skiplist底层采用zset结构实现
typeof struct zset{
//字典,保存了从成员到分值的映射关系
dict *dict;
//跳跃表,按分值由小到大保存所有集合元素
zskiplist *zsl;
}zset;
redis
是单线程的,主要是指 redis
的网络IO和键值对读写是由一个线程来完成的。
而 redis
的其他功能,比如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。
所以说关于 redis
是单线程这个问题不能一口咬定,需要了解它背后的原因。
除了redis之外,像Nginx、Node.js也是单线程的,但他们也都是高性能的服务器。
redis
的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因,其次,它还采用了IO多路复用的机制,使得它在网络IO操作中能并发的处理大量的客户端请求,实现高吞吐率。
redis单线程IO模型示意图
由图可知,它的单线程主要是指在文件事件分派器这部分的实现上。
Bitmap
本身不是一种数据类型,它实际上就是前面学过的字符串,但他可以对字符串进行按位的操作。redis
为Bitmap
单独提供了一套命令,所以使用Bitmap
与使用普通字符串的方式不同。Bitmap
看作是一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标叫做偏移量。当用户执行命令尝试对一个bitmap
进行设置的时候,如果该bitmap
不存在,或者大小不满足用户想要执行的设置操作,redis
则会对被设置的bitmap
进行扩展,使得bitmap
可以满足用户的设置需求。
redis
对bitmap
的扩展操作是以字节为单位进行的,所以扩展后的位图包含的二进制数量可能会比用户需要的稍微多一些,并且在扩展bitmap
的同时,redis
还会将所有为未被设置的二进制的值初始化为0。
给bit设置键值对,注意bit是按位操作的,设置的时候是通过索引将指定位设置为1或者0.
# 假设我们将下面的数据的末位都设置为1
00000001 00000001 00000001 00000001
setbit bitmap1 7 1
setbit bitmap1 15 1
setbit bitmap1 23 1
setbit bitmap1 31 1
一个字节等于8位。
一般情况下,有set,自然有get,该命令用来获取指定索引处的值。
# 获取索引为 7 15 4的值
getbit bitmap1 7
getbit bitmap1 15
getbit bitmap1 4
用来统计指定key上1的个数,也可以统计指定的范围。
# 统计bitmap1上1的个数
bitcount bitmap1
# 统计bitmap1上 0到1位置的1的个数,
bitcount bitmap1 0 1
# 统计bitmap1上倒数第一和第2个字节上1的个数
bitcount bitmap1 -2 -1
解释一下,这里的索引指是以字节为单位的,某个字节上值为1的位的个数。而不是每个字节位的索引。
返回字符串中设置为 1 或 0 的第一位的位置。注意是第一个位置。
BITPOS key bit [ start [ end [ BYTE | BIT]]]
# 返回整个bitmap1上值位为1的索引位置。
bitpos bitmap1 1
# 返回bitmap1上1到3范围内位值为1的位置
# 比如00000001 00000001 00000001 00000001中以字节为单位的索引分别是0 1 2 3
bitpos bitmap1 1 1 3
# 上面命令返回00000001 00000001 00000001中首个出现1的位置,那就是字节单位为1-3中位为单位索引的第15也即是第二个00000001中1的索引位置,即15
在多个键(包含字符串值)之间进行位操作,并将结果存储在目标键中。
BITOP operation destkey key [key ...]
# 将bitmap1和bitmap2进行或运算之后的结果存储在bitmap3中。
bitop or bitmap3 bitmap1 bitmap2
bitmap1: 00000001 00000001 00000001 00000001 bitmap2: 10000000 10000000 10000000 10000000 结果: 10000001 10000001 10000001 10000001
其他如按位与,非,异或可以去官方文档查看。
同样,它也不是新的数据类型,本质还是字符串类型。
HyperLogLog
是一个专门为了计算集合的基数而创建的概率算法,其优点在于它十分的节约内存空间;
只需要12KB
的内存就可以对**2^{64}**个元素进行计数,其标准误差为0.81%
。
PFADD key [element [element ...]] 向指定key中添加一个或者多个元素。
# 向hlll 中添加1 2 3
pfadd hlll 1 2 3
PFCOUNT key [key ...]
返回指定key中元素的统计个数,如果key不存在返回0.注意返回的结果是去重后的计数。
# 统计key为hlll的基数
pfcount hlll
PFMERGE destkey sourcekey [sourcekey ...] 将多个key值合并到指定的key中。
统计网站的独立访客(UV):
示例:uv:20200101 -> 1.1.1.101, 1.1.1.102, 1.1.1.103, 1.1.1.102, 1.1.1.103, ...
说明:每当用户来访时,都通过HLL记录他的IP,可以统计出每个数据集的基数,也可以对多个数据集进行合并!
使用set集合也可以实现同样的功能,但在内存的使用率上却不是一个等级的。
假设网站每天的UV为1000万。
通过上表不难感受到,当时间达到一定程度时,对空间的需求差别是非常大的。
GEO
是redis
在3.2
版本中新增的功能,该功能允许用户将经纬度格式的地理坐标存储到redis中,并对这些坐标执行基于距离的计算以及范围查找等功能。redis
为GEO
功能提供了一系列的命令,通过这些命令可以实现:GEO不是一种新的数据类型,它的本质其实还是有序集合。通过GEO命令存储地理数据时,redis会将经纬度转换成一个geohash值,并以该值为分数,以位置名称为成员,将数据存入一个有序集合中。
约定
为了方便演示,下面的地理坐标均为浙江省杭州市的真实经纬度数据,演示将使用这些准备好的数据进行。
# 杭州西湖
120.12199 30.226122 xihu
# 余杭区
119.987408 30.275946 yuhang
# 临安区
119.719616 30.24036 linan
# 萧山区
120.263439 30.184583 xiaoshan
# 临平区
120.300518 30.422897 linping
# 柯桥区
120.300518 30.413423 keqiao
坐标数据可以去这里获取地理经纬度查询
GEOADD key [ NX | XX] [CH] longitude latitude member [ longitude latitude member ...] 向集合中添加一个或多个经纬度地理数据。
# 以杭州为key,将上述几个坐标添加到集合中。
geoadd hangzhou 经度 纬度 对应的地名
GEOPOS key member [member ...] 返回指定key的地名的经纬度数据。
# 返回上述地理位置的全部经纬度数据
geopos hangzhou xihu yuhang linan xiaoshan linping keqiao
GEODIST key member1 member2 [ M | KM | FT | MI] 返回两个地名之间的距离,可以指定距离的单位。可以指定距离单位米、千米、英里、英尺
# 返回西湖到萧山之间的距离,默认单位为米
geodist hangzhou xihu xiaoshan
GEORADIUS key longitude latitude radius M | KM | FT | MI [WITHCOORD] [WITHDIST] [WITHHASH] [ COUNT count [ANY]] [ ASC | DESC] [STORE key] [STOREDIST key] 返回指定位置半径范围内的地名。
# 返回西湖200千米半径范围内的其他地名
georadius hangzhou 120.12199 30.226122 200 km
# 指定返回值中携带对应地名的经纬度数据
georadius hangzhou 120.12199 30.226122 200 km withcoord
# 指定返回值中携带对应地名的经纬度数据并限定返回的数据条数为3
georadius hangzhou 120.12199 30.226122 200 km withcoord count 3
GEORADIUSBYMEMBER key member radius M | KM | FT | MI [WITHCOORD] [WITHDIST] [WITHHASH] [ COUNT count [ANY]] [ ASC | DESC] [STORE key] [STOREDIST key] 命令和上一个命令的作用是一样的,不过这是通过指定地名进行返回,而上一个命令是通过指定经纬度返回。
GEOHASH key member [member ...] 返回指定位置的地理经纬度的哈希值。
既然geo的本质是有序集合,那么使用有序集合的命令也可以操作geo数据。
redis
提供了基于发布/订阅模式的消息机制,此模式下,消息的发布者和订阅者不直接通信,发布者只是将消息发布到指定的频道上,而订阅该频道的每个客户端都可以接收到这个消息;
当客户端新订阅某个频道时,它无法接收该频道之前的消息,因为redis
不会对发布的消息进行持久化。
为了方便演示,我将同时打开多个(3个)
redis
客户端,将当前的客户端作为发布者,其他三个客户端作为订阅者,演示发布/订阅的基本使用。
PUBLISH channel message 发布者用来发布一个消息,会自动创建消息的主题。
# 在Java的新闻主题中发布一条消息
publish news:java "hello java"
# 在js的新闻主题中发布一条消息
publish news:js "javascript"
SUBSCRIBE pattern [ pattern ...] 用在订阅端订阅发布者的内容,完成订阅之后会自动进入阻塞状态,等待接收发布者发布的消息。
# 在客户端1中订阅上面发布的Java主题消息。
subscribe news:java
# 在客户端2中订阅上面发布的js主题消息。
subscribe news:java
接下来我们在发布者客户端发一条消息试试。
publish news:java "This is java"
可以看到,当我发布成功之后,有订阅的两个客户端会收到消息提示。
在订阅者中的
(Integer) 1
代表该客户端的订阅数。而在发布者中这代表收到该条发布消息的客户端数量。
PSUBSCRIBE pattern [ pattern ...] 和上一个命令类似,也是用在客户端订阅中,不同的是,该命令支持模式匹配订阅,可以通过通配符的形式同时订阅多个主题的消息。
# 在客户端3中通过模式匹配同时订阅前面的两个主题(java和js)主题
psubscribe news:*
此时我们通过发布者客户端发布的消息在客户端3中都能收到。
PUBSUB CHANNELS [pattern] 返回主题列表。
# 看看与news相关的订阅有哪些。
pubsub channels newws*
# numsub 参数,返回指定主题被订阅数(非模式订阅)
# numpat 参数 返回指定主题被订阅数,模式订阅
UNSUBSCRIBE [channel [channel ...]] 取消订阅。该命令某些客户端中无法生效。
# 取消订阅news:js
unsubscribe news:js
Stream是Redis 5.0新增加的数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。
在Stream出现之前,redis中可以用来实现消息队列的方式主要有:
Stream
是一个消息链表,它将所有加入的消息都串接起来,每个消息都有一个唯一的IDStream
中的消息可以持久化,Redis
重启之后消息不会丢失。Stream
可以挂载多个消费组,每个消费都有一个游标,用于标识当前消费组的消费进度。Stream
中消费组的状态是独立的,互相不影受响,即同一流内的消息会被多个消费组共享。PEL
(Pending Entries List
),它记录了当前已被客户端读取的消息。演示之前,先开启多个客户端,方便演示生产者/消费者模式。
XADD key [NOMKSTREAM] [ MAXLEN | MINID [ = | ~] threshold [LIMIT count]] * | id field value [ field value ...] 往Stream流里添加一条或者多条消息。
# 向流中写入用户信息。
xadd mystream * name lisi age 23 .....
# 这里的*代表该消息的ID我使用系统自己生成的ID,它的格式是:时间戳-序号
XLEN key 返回指定流中的消息数。若指定的key不存在返回0
XRANGE key start end [COUNT count] XREVRANGE key end start [COUNT count] 返回指定范围内的消息数据。默认是由小到大的顺序。
# 查看刚刚添加的所有消息数据
xrange mystream - +
# - ,+ 用来指定开始到结束的范围内的全部数据。
# 也可以使用count 参数限定返回结果的条数。
# XREVRANGE 则是倒序返回,用法一样。
XDEL key id [id ...] 删除指定节点消息,通过ID指定。
XTRIM key MAXLEN | MINID [ = | ~] threshold [LIMIT count] 修剪消息流,指定删除超出某个范围之外的数据。
# 保留5个以内的数据,表示超出5个的都被删除
xtrim mystream maxlen 5
# 新添加的消息会被追到尾部,同时检查如果数量大于5的部分将会被删除
xadd mystream maxlen 5 * name lisi age 99
下面切到另一个客户端,演示消费者命令。
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...] 消费指定一个或者多个流中消息数据。
xread streams mystream 0-0
# streams表示同时消费多个流
# 0-0表示从每个消息流的起始位置开始消费
# 你也可以根据实际需求消费指定数量的消息
# 以阻塞模式进行消费
xread block 10000 count 3 streams mystream 0-0
# 上面的命令如果再次执行消费的话,ID不能再使用0-0开始,而是之前命令返回的最后一条消息的ID
# 以此类推,如果后面没有消息可以继续消费,该方法会进入指定时间内的阻塞状态。超时退出,否则如果生产者有数据,则会立即消费
xread block 10000 count 3 streams mystream $
# 上面的命令只会消费指定流中新增的消息,之前的消息不会被消费
以上就是生产者和单个消费者模式的全部演示内容,其余内容将在后半部分进行演示。
演示以消费组的方式进行消费。
XGROUP CREATE key groupname id | $ [MKSTREAM] [ENTRIESREAD entries_read] 创建消费组。
# 创建消费组g1,消费mystrean流中的消息,从头开始消费
xgroup create mystream g1 0
# 创建消费组g2,消费mystrean流中的消息,从尾开始消费
xgroup create mystream g1 $
XINFO STREAM key [FULL [COUNT count]] 返回指定流的具体信息。
返回指定流中消费组的信息。
返回指定流中消费组中消费者的信息。
XPENDING key group [ [IDLE min-idle-time] start end count [consumer]] 查看指定消费组中待处理的消息。
以上命令全部在生产者客户端使用。
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] id [id ...] 以消费组的模式进行消费。
# c1表示组g1中的一个消费者>表示从指定流中的头部开始消费
xreadgroup group g1 c1 count 1 block 10000 streams mystream >
关于消费者2的方式是类似的,不过它只能消费尾部的消息,在阻塞时间内,我们可以在生产者端发送一条消息,消费者会自动消费。
Redis
客户端执行一条命令分为四个步骤:发送命令、命令排队、命令执行、返回结果。
其中第一步和第四步称为Round Trip Time(RTT)
,即往返时间。
redis
提供了批量操作命令(如mget,mset等),可以有效的节约RTT。为了改善上面的问题,使用Pipeline(流水线),它可以有效的减少RTT。
redis
命令打包在一起,然后一次性的将他们发送给服务器。Pipeline和批量命令的对比
每次封装的流水线命令不宜过多,否则会增加客户端的等待时间,也会造成一定的网络阻塞。 建议将一次打包的大量命令拆分为多个流水线来实现。
演示一条简单的流水线命令。
echo -en '*3\r\ns3\r\nset\r\ns5\r\ncount\r\ns3\r\n100\r\n*2\r\ns4\r\nincr\r\ns5\r\ncount\r\n' | redis-cli --piple -a 密码
# 就是将key为ncount的值设置为100并自增1
注意该命令不能在客户端登录状态下执行,必须退出该状态执行之后再登录查看执行结果。
Redis
提供了简单的事务功能,该功能主要由multi
和exec
命令实现:
multi
命令代表事务的开始,exec
命令代表事务的结束,他们之间按顺序执行。multi
命令之后,他就进入了事务模式,这时用户输入所有命令会按顺序放入一个事务队列中。exec
命令之后,它才开始执行当前事务,执行成功后它会按照命令入队顺序返回各个命令执行的结果。discard
代替exec
命令即可,它会清空事务队列中已有的命令,并让客户端退出事务模式。为什么说Redis
提供是简单事务功能?
ACID
(原子性、一致性、隔离性、持久性)中的ACI
特性,当它运行在特定的持久化模式下时,也支持D
特性。很多时候,要确保事务中的数据没有被其他客户端修改才执行该事务。
Redis
提供了watch
命令来解决这类问题,这是一种乐观锁的机制。watch
命令,要求服务器对一个或多个key进行监视,如果在客户端执行事务之前,这些key发生了变化,则服务器将拒绝执行客户端提交的事务,并向它返回一个空值。Lua语言是在1993年由巴西一个大学研究小组发明的,其设计的目标是作为嵌入式持续移植到其他应用程序,由C语言实现,虽然简单小巧,但是功能强大,很多应用都选择它作为脚本语言,尤其是在游戏领域。
redis从2.6版本开始引入了Lua脚本,很方便的对redis服务器的功能进行了扩展:
EVAL script numkeys key [key ...] arg [arg ...]
SCRIPT LOAD script
EVEALSHA sha1 numkeys key [key….] arg [arg…]
在Lua脚本中执行Redis命令(command-命令名称,省略号-命令参数): redis.call(command, ...), redis.pcall(command, ...) 二者唯一的区别是对错误的处理方式不同,前者在命令出错时会返回一个错误, 后者会将错误封装起来,返回一个表示错误的Lua表格。
# 将指定的脚本缓存到redis服务器上。
SCRIPT LOAD script
# 检查校验和对应的脚本是否存在于redis服务器中。
SCRIPT EXISTS sha1 [sha1....]
# 移除所有已缓存的脚本
SCRIPT FLUSH
# 强制停止正在运行的脚本
SCRIPT KILL
演示一些简单的Lua脚本命令。
# 打印一个字符串。
eval 'return "Hello Lua!"' 0
# 向redis中添加一个set类型的键值对 Hello:Lua
eval 'redis.call("set",KEYS[1],ARGV[1])' 1 Hello Lua
命令中,我们通过
redis.call()
来接收redis命令。“set”表示存入一个字符串,键值对的具体内容通过参数的形式传入,而不是写死。 所以后的KEYS[1],ARGV[1]
代表从后面Hello Lua
分别取第一个值就是对应的key和value的值,Lua索引从1开始。
# 使用for循环向set集合中512个添加数据
eval 'for i=1,512 do redis.call("sadd",KEYS[1],i) end' 1 test:set:1
# 缓存脚本
script load 'for i=1,512 do redis.call("sadd",KEYS[1],i) end'
# 加载脚本
evalsha "f1b96e57574c72649eda263530f0ae2215313f67" 1 test:set:2
# 检查脚本是否存在
script exists "f1b96e57574c72649eda263530f0ae2215313f67"
# 删除脚本
script flush
Redis
提供了流水线、事务、Lua脚本,用于扩展redis服务器的功能,但这些功能都有一定的缺陷;
watch
的命令就很容易出错,而Lua脚本又需要熟悉Lua语法。redis在4.0增加了“模块”这个功能,它允许开发者通过redis开放的一簇API,基于C语言(能与C交互的语言)在redis之上构建任意复杂的、全新的数据结构和功能。 对于开发者,redis为他们提供了一个可以按需扩展redis的机会,对于普通用户,有了大量的第三方定制功能可以拿来使用,他们可以将redis应用在更多领域。
官方API手册:https://redis.io/topics/modules-api-ref
# 配置文件
loadmodule /path/to/mymodule.so
# 启动命令
redis-server loadmodule /path/to/mymodule.so
# redsi命令
module load /path/to/mymodule.so
主要记录各种第三方与
Redis
的整合使用。
spring
对redis
进行了比较完善的整合,使用方式也比较简单,主要分为三步。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
配置 Redis
配置数据量参数
以 application.properties
配置文件为例
# 配置Redis:RedisProperties类
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port = 6379
编写配置类、构造 RedisTemplate
/**
* @author: Tisox
* @date: 2022/1/28 10:08
* @description: 编写redis配置类
* @blog:www.waer.ltd
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
/*实例化*/
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
/*设置key的序列化方式*/
template.setKeySerializer(RedisSerializer.string());
/*设置value的序列化方式*/
template.setValueSerializer(RedisSerializer.json());
/*设置哈希的key的序列化方式*/
template.setHashKeySerializer(RedisSerializer.string());
/*设置哈希的value的序列化方式*/
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
访问 Redis
redisTemplate.opsForValue()
redisTemplate.opsForHash()
redisTemplate.opsForList()
redisTemplate.opsForSet()
redisTemplate.opsForZset()
官方文档
/**
* @author: Tisox
* @date: 2022/1/28 10:16
* @description: spring整合redis使用测试demo
* @blog:www.waer.ltd
*/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {
@Autowired
private RedisTemplate redisTemplate;
//----------------------字符串--------------------------------------------------------
@Test
public void testStrings(){
String redisKey = "test:count";
/*存*/
redisTemplate.opsForValue().set(redisKey,1);
/*取*/
System.out.println(redisTemplate.opsForValue().get(redisKey));
/*加*/
System.out.println(redisTemplate.opsForValue().increment(redisKey));
/*减*/
System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}
//执行结果:
//1
//2
//---------------------------哈希-------------------------------------------------------
@Test
public void testHashTests(){
String redisKey = "test:user";
/*存*/
redisTemplate.opsForHash().put(redisKey,"id",1);
redisTemplate.opsForHash().put(redisKey,"username","Tisox");
/*取*/
System.out.println(redisTemplate.opsForHash().get(redisKey,"id"));
System.out.println(redisTemplate.opsForHash().get(redisKey,"username"));
}
//执行结果:
//1
//Tisox
//----------------------------列表--------------------------------------------------------
@Test
public void testLists(){
String redisKey="test:ids";
/*存*/
redisTemplate.opsForList().leftPush(redisKey,101);
redisTemplate.opsForList().leftPush(redisKey,102);
redisTemplate.opsForList().leftPush(redisKey,103);
/*取*/
System.out.println(redisTemplate.opsForList().size(redisKey));
System.out.println( redisTemplate.opsForList().index(redisKey,0));
System.out.println(redisTemplate.opsForList().range(redisKey,0,2));
/*pop*/
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}
//执行结果
//3
//103
//[103, 102, 101]
//103
//102
//101
//-----------------------集合-------------------------------------------------------------
@Test
public void testSetson(){
String redisKey="test:language";
/*存*/
redisTemplate.opsForSet().add(redisKey,"java","C++","python","甲骨文");
/*取*/
System.out.println(redisTemplate.opsForSet().size(redisKey));
System.out.println(redisTemplate.opsForSet().pop(redisKey));
System.out.println(redisTemplate.opsForSet().members(redisKey));
}
//执行结果
//4
//python
//[甲骨文, C++, java]
//------------------------有序集合--------------------------------------------------------
@Test
public void testSortedSets(){
String redisKey="test:students";
/*存*/
redisTemplate.opsForZSet().add(redisKey,"王萌萌",80);
redisTemplate.opsForZSet().add(redisKey,"赵诗倩",90);
redisTemplate.opsForZSet().add(redisKey,"肖鹤云",78);
redisTemplate.opsForZSet().add(redisKey,"张成",100);
redisTemplate.opsForZSet().add(redisKey,"陶映红",60);
/*取*/
System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey,"肖鹤云"));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey,"李诗情"));
System.out.println(redisTemplate.opsForZSet().range(redisKey,0,3));
System.out.println(redisTemplate.opsForZSet().removeRange(redisKey,0,3));
}
//执行结果
//5
//78.0
//1
//[陶映红, 肖鹤云, 王萌萌, 李诗情]
//4
//---------------------------------全局命令--------------------------------------------
@Test
public void testKeys(){
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user"));
/*设置过期时间:10秒*/
redisTemplate.expire("test:students",10, TimeUnit.SECONDS);
}
//执行结果:自测
}
RDB(Redis Database)
是redis默认采用的持久化方式,它以快照的形式将进程数据持久化到硬盘中;RDB
会创建一个经过压缩的二进制文件,文件以.rdb
结尾。内部存储各个数据库的键值对数据等信息,它的触发方式有两种:
.rdb
文件。.rdb
文件。在SAVE命令执行期间,redis服务器将会阻塞,直到.rdb
文件创建完毕为止。
.rdb
文件。该命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求。
BGSAVE命令是对SAVE阻塞问题做的优化,redis内部所有涉及RBD的操作都采用BGSAVE的方式,而SAVE命令已经废弃。
save<seconds> <changes>
seconds
秒内,对数据库总共执行了changes
次修改,则自动执行一次BGSAVE命令;为了避免同时使用多个触发条件而导致服务器过于频繁地执行BGSAVE,redis服务器在每次成功创建.rdb文件之后,负责将自动触发BGSAVE命令的时间计数器以及修改计数器清零并重新计数,无论这个
.rdb
文件是由自动触发的BGSAVE创建还是由用户执行SAVE或BGSAVE命令创建,都是如此。
.rdb
文件,并存储父进程内存中的数据;.rdb
文件;COW(Copy On Write)
在Linux
系统中,可以通过glibc
中的fork
函数创建一个子进程,该进程和父进程完全相同,并且共享父进程的内存空间。
当父进程中任意进程需要修改内存中的数据时,会将对应的page
进行复制,然后对副本进行修改操作。
找到dump.rdb文件并删除。
打开redis.conf文件,如下,可以看到,当900秒内有一次改动或者300秒内10次改动以及60秒内的1000次改动都会触发RDB。
向服务器中随便存点数据之后输入save,手动触发持久化。
再查看dumpp.rdb:
od -c dump.rdb
REDIS
五个字符,redis服务器在尝试载入RDB文件的时候,可以通过这个标识符快速的判断该文件是否为真正的RDB文件;0xFF
,当redis服务器读取到这个EOF时,就知道正文部分已经全部读取完毕了;REDIS
,如果是则继续后面的操作,否则抛出错误并终止载入操作。AOF(Append Only File),解决了数据持久化的实时性,是目前redis持久化的主流方式;它以独立日志的方式,记录每次写入命令、重启时再重新执行AOF中的命令来恢复数据。AOF的工作流程包括:
AOF默认不开启,需要修改配置项来启用它:
appendonly yes # 启用AOF
appendfilename "appendonly.aof" # 设置文件名
AOF以文本协议格式写入命令,这种格式在前面的内容中提到过。
为什么采用这种格式?
为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写入操作优化为一次:
write
对文件写入时,系统不会直接把数据写入硬盘,而是先将数据写入内存的缓冲区中。flush
操作,将缓冲区数据冲洗到硬盘中;这种优化机制虽然提高了性能,但也给程序的写入操作带来了不确定性:
AOF
这样的持久化功能来说,冲洗机制将直接影响AOF
持久化的安全性;redis
向用户提供了appendfsync
选项,来控制系统冲洗AOF
的频率;、Linux
的glibc
提供了fsync
函数,可以将指定的文件强制性的从缓冲区刷到硬盘,上述的选项也是基于该函数实现。将appendonly
置为yes,下面的文件名不需要改动。
重启服务
redis-server /root/6379/redis.conf
RDB
持久化可能丢失大量的数据相比,AOF
持久化的安全性要高很多。everysec
选项,用户可以将数据丢失的时间窗口限制在1
秒内;AOF
文件存储的时协议文本,它的体积要比二进制格式的.rdb
文件大很多;AOF
需要通过执行AOF
文件中的命令来恢复数据,其恢复的速度也比RDB
慢很多。AOF
在进行重写时也需要创建子进程,在数据库体积较大时,将会占用大量的资源,会导致服务器的短暂阻塞。随着写入操作的不断进行,AOF
文件内会包含越来越多的冗余命令:
冗余命令不仅增加了AOF
文件的体积,也会严重影响到恢复数据的速度;
redis
提供了AOF
重写的功能;AOF
文件,并让文件只包含恢复当前数据库数据所需的尽可能少的命令;BGREWRITEAOF
# 设置触发AOF重写所需的最小文件体积,即当AOF文件体积达到该值时,触发AOF重写;
auto-aof-rewrite-min-size <value>
# 设置AOF重写所需的文件增长比例,即当AOF文件体积比上次重写后的体积增长一倍时,触发AOF重写;
auto-aof-rewrite-percentage <value>
AOF
重写AOF
重写,则直接返回;BGSAVE
操作,则延迟到BGSAVE
完成后再执行;fork
操作创建子进程;aof_buf
中,进而同步到硬盘,保持原有的逻辑;rewrite_buf
,防止重写操作遗漏这些数据;AOF
文件;AOF
文件:rewrite_buf
中的数据刷入到新的AOF
文件;AOF
文件替换旧的文件,完成AOF
重写;记录在
Redis
使用过程中遇到了一些问题、踩过的坑。
搜集整理关于
Redis
的面试题目、面试技巧。
总结一些
Redis
方面的使用技巧、方法。