Redis
NoSQL
概述
NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充
MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力
作用:应对基于海量用户和海量数据前提下的数据处理问题
特征:
- 可扩容,可伸缩,SQL 数据关系过于复杂,Nosql 不存关系,只存数据
- 大数据量下高性能,数据不存取在磁盘 IO,存取在内存
- 灵活的数据模型,设计了一些数据存储格式,能保证效率上的提高
- 高可用,集群
常见的 NoSQL:Redis、memcache、HBase、MongoDB
参考书籍:https://book.douban.com/subject/25900156/
参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc
Redis
Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性能键值对(key-value)数据库
特征:
- 数据间没有必然的关联关系,不存关系,只存数据
- 数据存储在内存,存取速度快,解决了磁盘 IO 速度慢的问题
- 内部采用单线程机制进行工作
- 高性能,官方测试数据,50 个并发执行 100000 个请求,读的速度是 110000 次/s,写的速度是 81000 次/s
- 多数据类型支持
- 字符串类型:string(String)
- 列表类型:list(LinkedList)
- 散列类型:hash(HashMap)
- 集合类型:set(HashSet)
- 有序集合类型:zset/sorted_set(TreeSet)
- 支持持久化,可以进行数据灾难恢复
安装启动
安装:
-
Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中
sudo apt update
sudo apt install redis-server -
检查 Redis 状态
sudo systemctl status redis-server
启动:
-
启动服务器——参数启动
redis-server [--port port]
#redis-server --port 6379 -
启动服务器——配置文件启动
redis-server config_file_name
#redis-server /etc/redis/conf/redis-6397.conf -
启动客户端:
redis-cli [-h host] [-p port]
#redis-cli -h 192.168.2.185 -p 6397注意:服务器启动指定端口使用的是--port,客户端启动指定端口使用的是-p
基本配置
系统目录
-
创建文件结构
创建配置文件存储目录
mkdir conf创建服务器文件存储目录(包含日志、数据、临时配置文件等)
mkdir data -
创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录
/etc/redis中cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port.conf
服务器
-
设置服务器以守护进程的方式运行,关闭后服务器控制台中将打印服务器运行信息(同日志内容相同):
daemonize yes|no -
绑定主机地址,绑定本地IP地址,否则SSH无法访问:
bind ip -
设置服务器端口:
port port -
设置服务器文件保存地址:
dir path -
设置数据库的数量:
databases 16 -
多服务器快捷配置:
导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis 实 例配置文件,便于维护
include /path/conf_name.conf
客户端
-
服务器允许客户端连接最大数量,默认 0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接:
maxclients count -
客户端闲置等待最大时长,达到最大值后关闭对应连接,如需关闭该功能,设置为 0:
timeout seconds
日志配置
设置日志记录
-
设置服务器以指定日志记录级别
loglevel debug|verbose|notice|warning -
日志记录文件名
logfile filename
注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志 IO 的频度
配置文件:
bind 192.168.2.185
port 6379
#timeout 0
daemonize no
logfile /etc/redis/data/redis-6379.log
dir /etc/redis/data
dbfilename "dump-6379.rdb"
基本指令
帮助信息:
-
获取命令帮助文档
help [command]
#help set -
获取组中所有命令信息名称
help [@group-name]
#help @string
退出服务
-
退出客户端:
quit
exit -
退出客户端服务 器快捷键:
Ctrl+C
数据库
服务器
Redis 服务器将所有数据库保存在服务器状态 redisServer 结构的 db 数组中,数组的每一项都是 redisDb 结构,代表一个数据库,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小。在初始化服务器时,根据 dbnum 属性决定创建数据库的数量,该属性由服务器配置的 database 选项决定,默认 16
struct redisServer {
// 保存服务器所有的数据库
redisDB *db;
// 服务器数据库的数量
int dbnum;
};

在服务器内部,客户端状态 redisClient 结构的 db 属性记录了目标数据库,是一个指向 redisDb 结构的指针
struct redisClient {
// 记录客户端正在使用的数据库,指向 redisServer.db 数组中的某一个 db
redisDB *db;
};
每个 Redis 客户端都有目标数据库,执行数据库读写命令时目标数据库就会成为这些命令的操作对象,默认情况下 Redis 客户端的目标数据库为 0 号数据库,客户端可以执行 SELECT 命令切换目标数据库,原理是通过修改 redisClient.db 指针指向服务器中不同数据库
命令操作:
select index #切换数据库 ,index从0-15取值
move key db #数据移动到指定数据库,db是数据库编号
ping #测试数据库是否连接正常,返回PONG
echo message #控制台输出信息
Redis 没有可以返回客户端目标数据库的命令,但是 redis-cli 客户端旁边会提示当前所使用的目标数据库
redis> SELECT 1
OK
redis[1]>
键空间
key space
Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict 字典中保存了数据库的所有键值对,将这个字典称为键空间(key space)
typedef struct redisDB {
// 数据库键空间,保存所有键值对
dict *dict
} redisDB;
键空间和用户所见的数据库是直接对应的:
- 键空间的键就是数据库的键,每个键都是一个字符串对象
- 键空间的值就是数据库的值,每个值可以是任意一种 Redis 对象

当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会进行一些维护操作:
- 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数,这两个值可以在
INFO stats命令的 keyspace_hits 属性和 keyspace_misses 属性中查看 - 更新键的 LRU(最后使用)时间,该值可以用于计算键的闲置时间,使用
OBJECT idletime key查看键 key 的闲置时间 - 如果在读取一个键时发现该键已经过期,服务器会先删除过期键,再执行其他操作
- 如果客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务注意到这个键已经被修改过
- 服务器每次修改一个键之后,都会对 dirty 键计数器的值增1,该计数器会触发服务器的持久化以及复制操作
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知
读写指令
常见键操作指令:
-
增加指令
set key value #添加一个字符串类型的键值对 -
删除指令
del key #删除指定key
unlink key #非阻塞删除key,真正的删除会在后续异步操作 -
更新指令
rename key newkey #改名
renamenx key newkey #改名值得更新需要参看具体得 Redis 对象得操作方式,比如字符串对象执行
SET key value就可以完成修改 -
查询指令
exists key #获取key是否存在
randomkey #随机返回一个键
keys pattern #查询keyKEYS 命令需要遍历存储的键值对,操作延时高,一般不被建议用 于生产环境中
查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号
keys * #查询所有key
keys aa* #查询所有以aa开头
keys *bb #查询所有以bb结尾
keys ??cc #查询所有前面两个字符任意,后面以cc结尾
keys user:? #查询所有以user:开头,最后一个字符任意
keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t -
其他指令
type key #获取key的类型
dbsize #获取当前数据库的数据总量,即key的个数
flushdb #清除当前数据库的所有数据(慎用)
flushall #清除所有数据(慎用)在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库
时效设置
客户端可以以秒或毫秒的精度为数据库中的某个键设置 生存时间(TimeTo Live, TTL),在经过指定时间之后,服务器就会自动删除生存时间为 0 的键;也可以以 UNIX 时间戳的方式设置过期时间(expire time),当键的过期时间到达,服务器会自动删除这个键
expire key seconds #为指定key设置生存时间,单位为秒
pexpire key milliseconds #为指定key设置生存时间,单位为毫秒
expireat key timestamp #为指定key设置过期时间,单位为时间戳
pexpireat key mil-timestamp #为指定key设置过期时间,单位为毫秒时间戳
- 实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令底层都是转换为 PEXPIREAT 命令来实现的
- SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,但是该命令是一个类型限定命令
redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,字典称为过期字典:
- 键是一个指针,指向键空间中的某个键对象(复用键空间的对象,不会产生内存浪费)
- 值是一个 long long 类型的整数,保存了键的过期时间,是一个毫秒精度的 UNIX 时间戳
typedef struct redisDB {
// 过期字典,保存所有键的过期时间
dict *expires
} redisDB;
客户端执行 PEXPIREAT 命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间:
def PEXPIREAT(key, expire_time_in_ms):
# 如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
# 在过期字典中关联键和过期时间
redisDB.expires[key] = expire_time_in_ms
# 过期时间设置成功
return 1
时效状态
TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差,返回这个键的剩余生存时间
- 返回正数代表该数据在内存中还能存活的时间
- 返回 -1 代表永久性,返回 -2 代表键不存在
ttl key #获取key的剩余时间,每次获取会自动变化(减小),类似于倒计时
pttl key #获取key的剩余时间,单位是毫秒,每次获取会自动变化(减小)
PERSIST 是 PEXPIREAT 命令的反操作,在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联
persist key #切换key从时效性转换为永久性
Redis 通过过期字典可以检查一个给定键是否过期:
- 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间
- 检查当前 UNIX 时间戳是否大于键的过期时间:如果是那么键已经过期,否则键未过期
补充:AOF、RDB 和复制功能对过期键的处理
- RDB :
- 生成 RDB 文件,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中
- 载入 RDB 文件,如果服务器以主服务器模式运行,那么在载入时会对键进行检查,过期键 会被忽略;如果服务器以从服务器模式运行,会载入所有键,包括过期键,但是主从服务器进行数据同步时就会删除这些键
- AOF:
- 写入 AOF 文件,如果数据库中的某个键已经过期,但还没有被删除,那么 AOF 文件不会因为这个过期键而产生任何影响;当该过期键被删除,程序会向 AOF 文件追加一条 DEL 命令,显式的删除该键
- AOF 重写,会对数据库中的键进行检查,忽略已经过期的键
- 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制
- 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致)
过期删除
删除策略
删除策略就是针对已过期数据的处理策略,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露
针对过期数据有三种删除策略:
- 定时删除
- 惰性删除(被动删除)
- 定期删除
Redis 采用惰性删除和定期删除策略的结合使用
定时删除
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作
- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
- 缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量
- 总结:用处理器性能换取存储空间(拿时间换空间)
创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实
惰性删除
数据到达过期时间不做处理,等下次访问到该数据时执行 expireIfNeeded() 判断:
- 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空
- 如果输入键未过期,那么 expireIfNeeded 函数不做动作
所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键
惰性删除的特点:
- 优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间
- 缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏
- 总结:用存储空间换取处理器性能(拿空间换时间)
定期删除
定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响
- 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上
- 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况
定期删除是周期性轮询 Redis 库中的时效性数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度
-
Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次
serverCron() → activeExpireCycle() -
activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式:
-
轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒
-
全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理
-
随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查
-
定期删除特点:
- CPU 性能占用设置有峰值,检测频度可自定义设置
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
- 周期性抽查存储空间(随机抽查,重点抽查)
数据淘汰
逐出算法
数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 freeMemoryIfNeeded() 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为逐出算法
逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,出现 Redis 内存打满异常:
(error) OOM command not allowed when used memory >'maxmemory'
策略配置
Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三
内存配置方式:
-
通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节
-
通过命令修改(重启失效):
-
config set maxmemory 104857600:设置 Redis 最大占用内存为 100MB -
config get maxmemory:获取 Redis 最大占用内存 -
info:可以查看 Redis 内存使用情况,used_memory_human字段表示实际已经占用的内存,maxmemory表示最大占用内存
-
影响数据淘汰的相关配置如下,配置 conf 文件:
-
每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能
maxmemory-samples count -
达到最大内存后的,对被挑选出来的数据进行删除的策略
maxmemory-policy policy数据删除的策略 policy:3 类 8 种
第一类:检测易失数据(可能会过期的数据集 server.db[i].expires):
volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰
volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰
volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰
volatile-random # 对设置了过期时间的 key 选择任意数据淘汰第二类:检测全库数据(所有数据集 server.db[i].dict ):
allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰
allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰
allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机第三类:放弃数据驱逐
no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory)
数据淘汰策略配置依据:使用 INFO 命令输出监控 信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置
排序机制
基本介绍
Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,并不更改集合中的数据位置,只是查询
SORT key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询
SORT key ALPHA #对key中字母排序,按照字典序
SORT
SORT <key> 命令可以对一个包含数字值的键 key 进行排序
假设 RPUSH numbers 3 1 2,执行 SORT numbers 的详细步骤:
-
创建一个和 key 列表长度相同的数组,数组每项都是 redisSortObject 结构
typedef struct redisSortObject {
// 被排序键的值
robj *obj;
// 权重
union {
// 排序数字值时使用
double score;
// 排序带有 BY 选项的字符串
robj *cmpobj;
} u;
} -
遍历数组,将各个数组项的 obj 指针分别指向 numbers 列表的各个项
-
遍历数组,将 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存在对应数组项的 u.score 属性里
-
根据数组项 u.score 属性的值,对数组进行数字值排序,排序后的数组项按 u.score 属性的值从小到大排列
-
遍历数组,将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端,程序首先访问数组的索引 0,依次向后访问

对于 SORT key [ASC/DESC] 函数:
- 在执行升序排序时,排序算法使用的对比函数产生升序对比结果
- 在执行降序排序时,排序算法使用的对比函数产生降序对比结果
BY
SORT 命令默认使用被排序键中包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序
SORT <key> BY <pattern> # 数值
SORT <key> BY <pattern> ALPHA # 字符
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"
redis> MSET apple-price 8 banana-price 5.5 cherry-price 7
OK
# 使用水果的价钱进行排序
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple"
实现原理:排序时的 u.score 属性就会被设置为对应的权重
LIMIT
SORT 命令默认会将排序后的所有元素都返回给客户端,通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素
LIMIT <offset> <count>
- offset 参数表示要跳过的已排序元素数量
- count 参数表示跳过给定数量的元素后,要返回的已排序元素数量
# 对应 a b c d e f g
redis> SORT alphabet ALPHA LIMIT 2 3
1) "c"
2) "d"
3) "e"
实现原理:在排序后的 redisSortObject 结构数组中,将指针移动到数组的索引 2 上,依次访问 array[2]、array[3]、array[4] 这 3 个数组项,并将数组项的 obj 指针所指向的元素返回给客户端
GET
SORT 命令默认在对键进行排序后,返回被排序键本身所包含的元素,通过使用 GET 选项, 可以在对键进行排序后,根据被排序的元素以及 GET 选项所指定的模式,查找并返回某些键的值
SORT <key> GET <pattern>
redis> SADD students "tom" "jack" "sea"
#设置全名
redis> SET tom-name "Tom Li"
OK
redis> SET jack-name "Jack Wang"
OK
redis> SET sea-name "Sea Zhang"
OK
redis> SORT students ALPHA GET *-name
1) "Jack Wang"
2) "Sea Zhang"
3) "Tom Li"
实现原理:对 students 进行排序后,对于 jack 元素和 *-name 模式,查找程序返回键 jack-name,然后获取 jack-name 键对应的值
STORE
SORT 命令默认只向客户端返回排序结果,而不保存排序结果,通过使用 STORE 选项可以将排序结果保存在指定的键里面
SORT <key> STORE <sort_key>
redis> SADD students "tom" "jack" "sea"
(integer) 3
redis> SORT students ALPHA STORE sorted_students
(integer) 3
实现原理:排序后,检查 sorted_students 键是否存在,如果存在就删除该键,设置 sorted_students 为空白的列表键,遍历排序数组将元素依次放入
执行顺序
调用 SORT 命令,除了 GET 选项之外,改变其他选项的摆放顺序并不会影响命令执行选项的顺序
SORT <key> ALPHA [ASC/DESC] BY <by-pattern> LIMIT <offset> <count> GET <get-pattern> STORE <store_key>
执行顺序:
- 排序:命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项,对输入键进行排序,并得到一个排序结果集
- 限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制
- 获取外部键:根据排序结果集中的元素以及 GET 选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集
- 保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上面去
- 向客户端返回排序结果集:最后一步命令遍历排序结果集,并依次向客户端返回排序结果集中的元素
通知机制
数据库通知是可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况
- 关注某个键执行了什么命令的通知称为键空间通知(key-space notification)
- 关注某个命令被什么键执行的通知称为键事件通知(key-event notification)
图示订阅 0 号数据库 message 键:

服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型
- AKE 代表服务器发送所有类型的键空间通知和键事件通知
- AK 代表服务器发送所有类型的键空间通知
- AE 代表服务器发送所有类型的键事件通知
- K$ 代表服务器只发送和字符串键有关的键空间通知
- EL 代表服务器只发送和列表键有关的键事件通知
- .....
发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的:
- 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回
- 如果给定的通知是服务器允许发送的通知
- 检测服务器是否允许发送键空间通知,允许就会构建并发送事件通知
- 检测服务器是否允许发送键事件通知,允许就会构建并发送事件通知