一、Redis简介
Redis
是一个开源的内存中的数据结构存储系统,它可以作用于:数据库、缓存、和消息中间件。
它是一个key-value存储系统。
和memcached
类似,它支持存储的value类型相对更多,包括string
(字符串),list
( 链表)、set
(集合)、zset
(sorted set ----有序集合)、和哈希类型。
这些数据类型都支持push/pop、add/remove
及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。再此基础上,Redis
支持各种不同方式的排序。
与memcached
一样,为了保证效率,数据都是缓存在内存中的。区别的是redis
会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件上, 并且在此基础上实现了master-slave
(主从)同步。
Redis
具有速度快、持久化、多种数据结构的特性以及支持多种编程语言、功能丰富、简单、主从复制、高可用、分布式的特点。
二、速度快
-
1、完全基于内存、绝大部分请求是纯粹的内存操作,非常快速。数据存储在内存中,类似于
HashMap
(哈希映射),HashMap
的优势就是查找和操作的时间复杂度都是O(1)
;哈希映射:键必须是唯一的,键不是以特定的顺序排列的。
时间复杂度: 算法复杂度分为时间复杂度与空间复杂度;时间复杂度是指改算法的运行时间;而空间复杂度指执行这个算法需要的内存空间。(算法的复杂度体现在运行该算法时计算机所需的资源多少上,计算机资源最重要的是时间和空间(即寄存器 )资源 。 )
寄存器:寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
-
2、数据结构简单,对数据操作也简单,
Redis
中的数据结构是 专门进行设计的。 -
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程和多线程导致的切换而消耗
CPU
,不用去考虑各种锁的问题,不存在各种加锁释放锁的操作,没有因为可能出现死锁而导致的性能消耗; -
4、使用 多路
I/O
复用模式,非阻塞IO
; -
5、使用底层模型不同,它们之间底层实现方式以及与客户端通信的应用协议不一样,
redis
直接构建自己的VM
机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
三、单进程
因为Redis
是基于内存的操作,CPU
不是redis
的瓶颈,Redis
的瓶颈最有可能是机器的内存大小或网络带宽,既然单线程容易实现,而cpu
不会成为瓶颈,就顺利成章的采用单线程的方案了。(官方解释)
redis
核心就是 如果我的数据全在内存中里,我单线程的去操作 就是效率最高的,因为多线程的本质就是CPU
模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换,对一个内存系统来说,它没有上下文的切换就是效率最高的。redis
用单个CPU
绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU
上完成的,所以它是单线程处理。
优势:
- 代码更清晰,处理逻辑更简单;
- 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁导致的性能消耗
- 不存在多进程或者多线程导致的切换而消耗
CPU
弊端:
- 无法发挥多核
CPU
性能,不过可以通过在单个机开多个redis
实例来完善。
1、进程
当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程组成的。
2、线程
线程是程序中的一个执行流,每个线程都有自己的 专有寄存器,但代码是共享的,即不同的线程可以执行同样的函数
3、多线程
指程序包含多个执行流,即在一个程序中可以运行多个不同的线程来执行不同的任务;也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
利弊
好处:
- 可以提高
CPU
利用率。在多线程程序中,一个线程必须等待的时候,CPU
可以运行其他线程而不是等待,这样就提高了程序的效率;
坏处:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存越多;
- 多线程需要协调和管理 ,所以需要
CPU
时间跟踪线程; - 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
- 线程太多会导致控制太复杂,最终可能会造成很多
bug
;
四、持久化
由于Redis
的数据都存在内存中的,如果没有配置持久化,redis
重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存在磁盘上,当Redis重启后,可以从磁盘中恢复数据。
Redis
提供两种方式进行持久化,一种是RDB
持久化(半持久化模式)(原理是将Redis
在内存中的 数据库定时dump
到磁盘上),另外一种是AOF(append only file)
持久化(原理是将Redis
的操作日志以追加的方式写入文件)。
1、RDB(Redis database)
可以理解为快照/内存快照,RDB
持久化过程是将当前进程中的数据生成快照存储到硬盘中。
1.1、触发机制
手动触发和自动触发
1.1.1、手动触发
save
与bgsave
命令:
save
命令会阻塞当前服务器,直到RDB
完成为止,如果数据量大的话会造成长时间的阻塞,线上环境一般禁止使用bgsave
很好理解,就是backgroud save
,执行bgsave
命令时Redis
会fork
一个子进程来完成RDB
的过程,完成后自动借宿,所以Redis
主进程发生在fork
那一下,相对save
,阻塞时间很短。
1.1.2、自动触发
在redis.conf
配置文件中可以配置自动触发,配置方式如下:
save 900 10 //在900秒内发生10次写操作,就会执行一次bgsave。
只有在shutdown
命令时,如果没有开启AOF
功能,那么会自动执行一次bgsave
。
1.2、RDB执行流程
- 执行
bgsave
时,redis
主进程会检查是否有子进程在执行RDB/AOF
持久化任务,若有,就直接返回; redis
主进程会fork
一个子进程来执行RDB
操作,fork
操作会对主进程造成阻塞(影响Redis
读写),fork
操作完后会发消息给主进程,从而不再阻塞主进程;RDB
子进程会根据radis
主进程的内存生成临时的快照文件,RDB
完成后会使用临时快照文件替换掉原来的RDB
;RDB
子进程完成RDB
持久化后会发消息给主进程。通知持久化完成;
注:在有子进程执行RDB
过程的时候,Redis
主进程的读写不受影响,但是对于子Redis
的写操作不会同步到主进程的内存中,而是会写到一个临时的内存区域作为一个副本,等到主进程接收到子进程完成RDB
的消息后再将内存副本中的数据同步到主内存。
Redis
采用LZF
算法对RDB
进行压缩,所以生成的内存文件小。
> LZF算法:一种压缩文件的算法
1.3、RDB的优缺点
优点:
RDB
文件小,非常适合用于定时备份,用于灾难恢复;Redis
加载RDB
文件比AOF
文件快很多,内存数据 VS 命令一目了然;
缺点:
- 不能做到时刻持久化,因为
fork
子进程属于重量级操作,会阻塞主进程,而且距上次bgsave
之间的数据都会丢失; - 存在老版本
Redis
不兼容新版本RDB
格式文件的问题。
2、AOF(append only file)
以日志的方式记录每次写命令,服务重启的时候重新执行AOF
文件中的命令来恢复内存数据。因为解决了数据持久化实时性问题,所以目前AOF
是Redis
持久化的主流方式。
2.1、开启AOF
AOF
默认是关闭的
///redis.conf 中
appendonly yes
2.2、AOF执行流程
- 所有的写命令都会追加到
aof_buf
(缓冲区)中; - 可以使用不同的策略将
AOF
缓冲区中的命令写到AOF
文件中; - 随着
AOF
文件的越来越大,会对AOF
文件进行重写; - 当服务器重启的时候,会加载
AOF
文件并执行其中命令用于恢复数据。
简单分析一下AOF
执行流程中的一些问题:
- 因为
Redis
为了效率,使用内存 + 单线程,如果每次写命令都追加写硬盘的操作,那么Redis
的响应速度换要取决于硬盘的IO效率,显然不现实,所以每次将写命令先写到AOF
缓冲区; - 写到缓冲区去还有一个好处是可以采用不同的策略来实现缓冲区到硬盘的同步,可以让用户自行在安全性和性能之间做出权衡。
2.3、同步策略
在了解同步策略之前,需要先了解一个函数两个方法 flushAppendOnlyFile
(write
和save
)
redis
的服务器进程是一个事件循环,文件事件负责处理客户端的命令请求,而时间实际负责执行serverCron
函数这样定时运行的函数,在处理文件事件执行写命令,使得命令被追加到缓冲区中,然后在处理时间事件执行 serverCron
函数会调用 flushAppendOnlyFile
函数进行文件的写入和同步:
write
: 根据条件,将缓冲区中的缓存写入到AOF
文件save
: 根据条件,调用fsync
或fdatasync
函数将AOF
文件保存到磁盘。
下面来介绍Redis支持的三种同步策略:
AOF_FSYNC_NO : 不保存,write和read命令都由主进程执行;
AOF_FSYNC_EVERYSEC : 每秒钟保存一次(write主进程完成,save子进程完成);
AOF_FSYNC_ALWAYS : 每执行一个命令保存一次(write和read命令由主进程执行);
AOF_FSYNC_NO
在这种策略下,每次flushAppendOnlyFile
函数被调用的时候都会执行一次write
方法,但不会执行save
方法。
只有下面三种情况下才执行save
方法:
Redis
被关闭AOF
功能被关闭- 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的save
操作者都会引起主进程阻塞,并且由于长时间没有执行save
命令,所以save
命令执行时,阻塞时间会很长。
AOF_FSYNC_EVERYSC
在这种策略下,save
操作原则上每隔1秒中就会执行一次,因为save
操作是由于后台于进程调用的,所以它不会引起服务器主进程阻塞
其实根据redis
的状态,没当flushAppendOnlyFile
函数被调用时,write命令和save命令又分为四种不同情况
if('save正在执行'){
if('运行时间超过两秒'){
'函数直接返回不执行write或新的save';//情况1
}else{
'执行write,但不执行新的save';//情况2
}
}else{
if('距离上次成功执行save相隔超过1秒'){
'执行write,但不执行新的save';//情况3
}else{
'执行write和新的save';//情况4
}
}
根据上面可知,在AOF_FSUNC_EVERYSEC
策略下,如果在情况1时发生宕机,那么用户最多损失2s内所产生的的数据;而如果在情况2时发生故障宕机,堆积了很多save
命令,那么用户损失的数据是超过2秒的。
AOF_FSYNC_ALWAYS
在这种模式下,每次执行完一个命令后,write和save命令都会被执行
另外,此save
命令由Redis
主进程执行的,所以在save命令执行期间,主进程阻塞
三种策略的优缺点:
AOF_FSYNC_NO
策略虽然表面上看起来提升了性能,但是会存在每次save
命令执行的时候相对长时间阻塞主进程的问题。并且数据的安全性得不到保证,如果Redis
服务器突然宕机,那么没有从AOF
缓存中保存到硬盘中的数据都会丢失。AOF_FSYNC_ALWAYS
策略的安全性得到了保障,理论上最多丢失最后一次写操作,但是由于每个写操作都会阻塞主进程,所以Redis
主进程的响应收到了很大的影响。AOF_FSYNC_EVERYSEC
策略是比较建议的配置,也是Redis
的默认配置,相对来说兼顾安全性和性能。
2.4、重写机制
随着命令不断从AOF
缓存中写入到AOF
文件,AOF
文件会越来越大,为了解决这个问题,Redis
引入了AOF
重写机制来压缩AOF
文件。
AOF
文件的压缩和RDB
文件的压缩原理不一样,RDB
文件的压缩是使用压缩算法将二进制的RDB
文件压缩,而AOF
文件的压缩主要是去除AOF
文件中的无效命令,比如说:
- 同一个
key
的多次写入只保留最后一个命令 - 已删除、已过期的
key
的写命令不再保留
AOF
重写的触发机制也分为手动触发与自动触发两种方法:
- 手动触发: 执行
bgrewriteaof
命令 - 自动触发: 在
redis.conf
中有两个配置项:
auto-aof-rewrite-min-save = 64M AOF文件小于64M不重写
auto-aof-rewrite-min-percenrage `100 AOF文件比上次AOF重写后的文件大100%时重写
2.5、AOF重写流程
- ①执行
bgrewirteaof
命令时,如果当前有进程进行AOF
重写,那么直接返回;如果有进程正在进行bgsave
,则等待bgsave
执行完毕再进行AOF
重写 - ②、
Redis
主进程会fork
一个子进程进行AOF
重写,开销和RDB
重写一样 - ③、
AOF
重写过程中,不影响Redis
原有的AOF
进程,包括写消息到AOF
缓存以及同步AOF
缓存中的数据到磁盘。 - ④、
AOF
重写过程中,主进程收到的写操作还会将命令写到AOF
重写缓冲区,注意和AOF
缓冲区区分开 - ⑤、由于
AOF
重写过程中原AOF
文件还在陆续写入数据,所以AOF
重写子进程只会拿到fork
子进程时的AOF
文件进行重写 - ⑥、子进程拿到原
AOF
文件中的数据写到一个临时的AOF
文件中 - ⑦、子进程完成
AOF
重写后会发消息给主进程,主进程回吧AOF
重写缓冲区的数据写到AOF
缓冲区,并且用新的AOF
文件替换旧的AOF
文件。
上面是AOF
重写的主要流程,以下说说细节上的东西:
Redis
对AOF
的重要性看得比RDB
重,因为RDB
的时候如果有进程正在执行AOF
,那么直接返回;而AOF
的时候如果有进程在执行RDB
,那么等RDB
结束再执行AOF
;Redis
在AOF
重写的时候新建一个重写缓冲区的目的是为了保证重写过程中写命令不会丢失;- 子进程在重写
AOF
的时候,每次写硬盘的数据量是由配置决定,不能太大,否则会导致硬盘阻塞(32M
); AOF
重写的整个过程有三个部分会阻塞进程:- 主进程
fork
子进程时; - 主进程把
AOF
重写缓冲区中的数据写到AOF
缓冲区的时候; - 使用新的
AOF
文件替换掉旧的AOF
文件的时候
- 主进程
3、流程
Redis
重启时,会先加载AOF
,再加载RDB
,任一发生错误均会打印错误信息,并重启失败。
4、总结
RDB
持久化基于内存快照,存储二进制文件;AOF
持久化基于写命令,存储文本文件;RDB
采用压缩算法;AOF
采用重写;- 回复
RDB
文件的速度比AOF
快 RDB
实用性不好,所以AOF
更主流;- 合理使用
AOF
,理论上不会丢大量的数据。
五、数据结构
1、string
Redis
中的string
是可以修改的,成为动态字符串(Simple Dynumic String
) 简称SDS
说是字符串但它的内部结构更像是一个 ArrayList
,内部维护着一个字节数组,并在其内部预分配了一定的空间,以减少内存的频繁分配。
1.1、空间预分配
Redis
内存分配的机制是这样的:
- 当字符串长度小于1M时,每次扩容都是加倍现有的空间;
- 当字符串长度超过1M时,每次扩容只会扩展1M的空间;
这样既保证了内存够用,还不至于造成内存的浪费,字符串最大长度512MB;
struct sdshdr {
int len; //buf已占用的空间长度
int free; //buf中剩余的空间长度
char but[]; //数据 真实存储c字符串
}
用SDS保存字符串 ‘Redis’ 图示如下:
buf[] 数组用来保存字符串的每个元素
T len 表示SDS保存字符串实际长度 为 5
T capacity 表示数组容量 是5+1 (+1:最后的一个’\0’是空字符,表示字符串的结尾。)
free 表示buf数组中未使用的字节长度 为0
T :泛型:把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型
- 由于len属性的存在,我们获取SDS字符串长度只需要读取 len 属性,时间复杂度为O(1)。而对于C语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为O(n)。(n=字符串实际计算出的长度)
- 通过 strlen key 命令可以获取 key 的字符串长度。
1.2、当执行append
(追加)操作
APPEND key "_php" //对不存在的 key 进行 APPEND ,等同于 SET key "_php"
这时 key = 'Redis_php'
; 长度 len = 9
,则 free
由0
变为 9
;此时buf = 'Redis_php\0 '
;
也就是buf
的内存空间(capacity
)变为了 9+9+1=19
个字节;
这就是redis
给字符串多分配了9
个字节的预分配空间,所以下次还有append
追加的时候,如果预空间足够,就无需进行空间分配了。
①.capacity
和len
两个属性都是泛型,为什么不直接用int
类型?
答:因为Redis
内部有很多优化方案,为更合理地分配内存,不同长度的字符串采用不同的数据类型表示,且在创建字符串的时候len
会和capacity
一样大,不会产生冗余的空间,所以string
值可以是字符串、数字或者二进制
②. Redis
追加操作的分配策略会浪费内存资源吗?
答:执行过append
命令的字符串会带有额外的预分配空间,这些预分配空间不会被释放,除非该字符串所对应的键被删除,或者等到关闭Redis
之后,再次重启时重新载入的字符串对象将不会有预分配空间。
因为执行append
命令的字符串键数量通常并不多,占用内存的体积通常也不大,所以这一般并不算什么问题。另一方面,如果执行append
操作的键很多,而字符串体积又很大的话,那可能就需要修改Redis
服务器,让它释放一些字符串键的预分配空间,从而有效地使用内存。
1.3、惰性释放
惰性释放用于优化SDS
的字符串缩短操作:当SDS
的api
要缩短SDS
保存的字符串时,程序并不需要立即使用内存重分配策略来回收缩短后多出来的字节,而是使用free
熟悉将这些字节记录起来,并等待使用。
1.4、小结
- ①、
redis
的字符串表示为SDS
(动态字符串); - ②、有高效的执行长度计算、高效的执行追加操作、二进制安全等特性;
- ③、
SDS
会为追加操作进行优化,加快追加操作的速度,并降低内存分配的次数,代价是多占了一些内存,而且这些内存不会被主动释放;
1.5、应用场景
存储 key-value 键值对
1.6、常用命令
set key value //给指定key设置值(可覆盖)
get key //获取指定key的值
del key //删除指定key的值
exists key //检查给定key是否存在
mset key1 value1 key2 value2....... //同时设置一个或多个key时
mset key1 key2 key3 ...... //返回多个key的值,某个key不存在,则返回特殊值 nil
expire key time //给指定key设置过期时间 单位秒
setex key time value //相当于set + expire
setnx key value //key不存在则 set ,否则返回 0 (互斥锁)
incr key //若value 为整数,每次增加1 规则:key不存在则key初始为0,然后执行incr;不是为数字类型的则返回错误
incrby key num //每次增加 num 规则同 incr
decr key //若 value为整数 每次递减1 不存在则初始化为0 ,然后执行decr操作
2、List (列表)
Redis
中的list
与java
中的 linkedlist
很像,底层都是一种链表结构。
list
的插入和删除操作非常快,时间复杂度为O(1)
,不像数组结构插入、删除需要移动数据
像归像,但是Redis
中的list
底层可不是一个双向链表那么简单。
当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist
(压缩列表),它将所有的元素紧挨着存储,分配的是一块连续的内存;
当数据量较多时将会变成 quicklist
(快速链表结构)
2.1、数据结构
ziplist
数据结构:
struct ziplist <T>{
int 32 zlbytes; //压缩列表占用字节数
int 32 zltail; //最后一个元素距离起始位置的偏移量,用于快速定位到最后一个节点
int 16 zllen; //元素个数
T[] entries; //元素内容
int 8 zlend; //结束为 0xFF
}
压缩列表为了支持双向遍历,所以才有zltail
这个字段,用来快速定位到最后一个元素,然后倒着遍历。
entry
的数据结构:
struct entry {
int<var> prevlen; //前一个entry长度
int<var> enconding; //元素类型编码
optional byte[] content; //元素内容
}
entry
它的prevlen
字段表示前一个entry
字节长度,当压缩列表倒着遍历时,需要通过这个字段来快速定位到下一个元素的位置。
2.2、应用场景
- ①、消息队列:
lpop
和rpush
(或者反过来,lpush
和rpop
)能实现队列的功能 - ②、朋友圈的点赞列表、评论列表、排行榜:
lpush
命令和lrange
命令能实现最新列表功能。
2.3、常用命令
lpush key value1 value2...... //将一个或多个值插入到列表头部,key不存在则创建一个空列表并执行 lpush。key不是列表返回错误; 执行lpush命令后,返回列表的长度
rpush key value1 value2...... //同lpush 不过是在 尾部插入
lpop key // 移除并返回列表第一个元素
rpop key //移出并返回最后一个元素
llen key //返回列表的长度
lrem key count value //移除count个与value相等的值, count>0从表头开始,count<0从表尾开始,=0则移除全部;然后返回移除的个数
lindex key num //返回列表第 num 下标的值,负数表示倒数 ,不存在则返回 nil
lrange key start end //返回列表指定区间内的元素,负数为倒数
ltrim key start stop //保留指定区间内的元素,其余删除
3、hash(字典)
3.1、数据结构
redis
中的hash
和java
的hashmap
更加相似,都是数组+链表的结构,当发生hash
碰撞时将会把元素追加到链表上,值得注意的是在Redis
的hash
中,value
只能是字符串。
hash
和string
都可以用例存储用户信息,但不同的是hash
可以对用户信息的每个字段单独存储;string
存的是用户全部信息经过序列化的字符串,如果想要修改某个字段,必须将用户信息字符串全查出来,解析成相对应的用户信息对象,修改完后再序列化成字符串存入。而hash
可以只对某个字段修改,从而节约网络流量,不过hash
占用内存更大。
3.2、应用场景:
- ①购物车:
hset key field value
可以实现以用户id
、商品id
、为field
,商品数量为value
,恰好构成了购物车的三大要素;
②存储对象:hash
类型的(key,field,value
)结构与对象的(对象id,属性,值
)结构相似,也可以用来存储对象
3.3、常用命令
hset key field value //为hash表中的 field字段赋值,新字段则创建字段赋值返回1,旧字段则覆盖旧值,返回0
hget key field // 返回hash表指定字段的值
hdel key field1 field2 //删除hash表中一个或多个字段,返回删除数量,不存在的会被忽略
hlen key //保存的字段个数
hgetall key // ‘慎用’ 返回所有字段和值
hmset key field1 value1 field2 value2 ...... //同时设置多个字段
hincr
hincrby
4、set(集合)
它内部的键值对是无序的,唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value
都是一个值 null
。当集合中最后一个元素被移除后,数据结构被自动删除,内存被回收。
4.1、应用场景
-
好友、关注、粉丝、感兴趣的人 集合:
- ①
sinter
命令可以获取A.B两个用户的共同好友 - ②
sismember
命令可以判断A是否是B的好友 - ③
scard
命令可以获取好友的数量 - ④关注时,
smove
命令可以将B
从A
的粉丝转为A
的好友
- ①
-
首页随机展示:
比如美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而srandmenber命令可以随机取几个。 -
存储某活动中中奖用户的id,自带去重。
2、常用命令
sadd key value1 value2..... //将一个或多个成员元素加入到集合中
smembers key //返回集合中所有的成员
sismember key value //判断成员元素是否是集合的成员
scard key //返回集合中元素的数量
srem key value1 value2 //移除集合中指定元素,不存在则忽略,返回移除是数量
sinter key1 key2 .... //返回给定集合的交集
smove key1 key2 value //将元素value从kay1集合移动到key2集合
spop key count //移除集合中 count 个随机元素,返回移除的元素
srandmember key count //返回集合中count 个随机元素 count为正数,则返回的元素各不相同,为负数,则元素可能会重复
5、zset (有序集合)
zset
也叫sortedSet
,一方面它是个set
,保证了内部value
的唯一性,另一方面它可以给每个value
赋予一个score
,代表这个value
的排序权重。它的内部实现用的是一种叫做‘跳跃列表’的数据结构。
5.1、应用场景
zset
可以用做排行榜,但是和list不同的是zset
它能够实现动态的排序,例如:可以用来存储粉丝列表,value
值是粉丝id
,score
是关注时间,我们可以对粉丝列表按关注时间进行排序。zset
还可以用来存储学生的成绩,value
值是学生id
,score
是考试成绩,我们对成绩按照分数排序就是他的名次。
5.2、常用命令
zadd key score1 value1 score2 value2 //将一个或多个成员元素及其分数值加入到有序集合中
zrange key start stop//返回集合中指定区间的成员(第几个) 从小到大
zrevrange key start stop//返回集合中指定区间的成员(第几个) 从大到小
zcard key //返回集合中元素的数量
zrank key value //返回指定成员的 score 排名
zrangebgscore key score1 score2 //返回 score范围内的元素列表
zrem key value1 value2 //移除集合中一个或多个成员,不存在的会被忽略, 返回成功移除的数量
zscore key value //返回成员的分数值 score ,不存在则返回nil
5.3、跳跃表
跳跃表简介:
跳跃表(skiplist
)是一种随机化的数据结构。由wiiam Pugh
提出,是一种可以与平衡媲美的层次化链表结构——查、删、添加等操作都可以在对数期望时间下完成,以下是一个典型的跳跃表例子:
zset
为什么使用跳跃表:
首先,因为zset
要支持随机的插入和删除,所以它不宜使用数组来实现,关于排序问题,我们也很容易就想到红黑树/平衡树这样的树形结构,为什么Redis
不使用这样的结构呢?
- ①性能考虑:在高并发情况下,树形结构需要执行一些类似于
rebalance
这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部。 - ②实现考虑:在复杂度数与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更直观。
本质是解决查找问题,先看一个普通的链表结构:
我们需要对这个链表按score
值进行排序,这也就意味着,当我们需要添加新的元素时,我们需要定位到插入点,这样才可以继续保证链表是有序的,通常我们采用二分查找法,但二分查找是有序数组的,链表没有办法进行位置定位,我们除了遍历整个,找到第一个比给定数据大的节点为止(O(n)
),以外似乎没有好的办法。
但假如我们每两个相邻节点之间就增加一个指针,让指针指向下一个节点。
这样所有新增的指针连成了一个新的链表,但它包含的数据只有原来的一半。
现在假设我们想要查找数据时,可以根据这条新的链表查找,如果碰到比待查数据大的节点时,再回到原来的链表继续查找。
利用同样的方式,我们可以在新产生的链表上,继续为每两个相邻的节点增加一个指针,从而产生第三层链表。
可以想象,当链表足够长,这样的多层链表结构可以帮助我们跳过很多下层节点,从而加快查找效率。
六、redis性能优化的13条军规
- 缩短键值对的存储长度
- 使用lazy free (延迟删除)特性
- 设置键值的过期时间
- 禁用长耗时的查询命令
- 使用showlog优化耗时命令
- 使用pipeline 批量操作数据
- 避免大量数据同时失效
- 客户端使用优化
- 限制redis内存大小
- 使用物理机而非虚拟机安装Redis服务
- 检查数据持久化策略
- 禁用THP特性
- 使用分布式架构来增加读写速度
下面对策略做详细介绍:
1、缩短键值对的存储长度
键值对的长度是和性能成反比的;在key不变的情况下,value值越大操作效率越慢;
键值对内容较大时,还会带来几个问题:
- 内容越大需要的持久化时间就越长,需要挂起的时间越长,Redis的性能就会越低;
- 内容越大在网络上传输的内容就越多,需要的时间就越长,整体的运行速度就会越低;
- 内容越大占用的内存就越多,就会频繁的触发内存淘汰机制,从而给Redis带来了更多的运行负担。
2、使用lazy free (延迟删除)特性
lazy free
特性是redis 4.0
新增的一个非常实用的功能,它可以理解为惰性删除或者延迟删除。意思是在删除的时候,提供一部延迟释放键值的功能,把键值释放操作放在BIO(background I/O)
单独的子线程处理中,以减少删除操作对主进程的阻塞,可以有效的避免删除big key
时带来的性能和可用性问题。
lazy free
设置了4种 场景,默认都是关闭的
lazyfree-lazy-eviction no //表示当Redis运行内存超过 maxmemory 时,是否开启lazy free机制
lazyfree-lazy-expire no //表示设置了过期时间的键值,当过期后是否开启lazy free机制
lazyfree-lazy-server-del no //有些指令在处理已存在的键值时,会带有一个隐式的del操作,比如rename 命令,当目标键以存在,Redis会优先删除目标键,如果这些目标键是一个big key 就会造成阻塞删除的问题,此配置表示在这种场景中是否开启lazy free机制
slave-lazy-flush no //针对slave(从节点)进行全量数据同步,slave在加载 master的RDB文件前,会运行 flushall来清理自己的数据,塔表示此事是否开启 lazy free机制。
建议开启 lazyfree-lazy-eviction
、lazyfree-lazy-expire
、lazyfree-lazy-server-del
等配置,这样就可以有效的提高主线程的执行效率。
3、设置键值的过期时间
应该根据实际的业务情况,对键值设置合理的过期时间,这样Redis会帮你自动清除过期的键值对,以节约对内存的占用,以免键值过多的堆积,频繁的触发内存淘汰策略
4、禁用长耗时的查询命令
要避免O(n)
命令对Redis
造成的影响,可以从以下几个方面入手改造:
- 禁止使用
keys
命令; - 避免一次查询所有成员,要使用
scan
进行分批次的游标式的遍历; - 通过机制严格控制
Hash
、set
、sorted
、set
等结构数据的大小; - 将排序、并集、交集等操作放在客户端执行,以减少
Redis
服务器运行压力; - 删除(
del
)一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式unlink
,它会启动一个新的线程来删除目标数据,而不阻塞Redis
的主进程
5、使用slowlog
优化耗时命令
可以使用slowlog
功能查找出最耗时的Redis
命令进行相关的优化,以提升Redis
的运行速度,慢查询有两个重要配置项:
slowlog-log-slower-than //用于设置慢查询的评定时间,执行单位是微妙(1秒等于1000000微妙);
slowlog-max-len //用来配置慢查询日志的最大记录数;
可以根据相关业务情况进行配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中的,可以使用 slowlog get n
来获取相应的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。
6、使用pipeline
批量操作数据
pipeline
(管道技术)是客户端提供的一种批量处理技术,用来一次处理多个Redis
请求,从而提高整个交互的性能。
7、避免大量数据同时失效
Redis
过期键值删除使用的是贪心策略,它每秒会进行10
次过期扫描,此配置可在 redis.conf
进行更改,默认值是 hz 10
,redis
会速记抽取20
个值,删除这20
个键中过期的键,如果过期key
比例超过25%
,则重复执行此流程。
如果大型系统中有大量缓存在同一时间过期,那么会导致Redis
循环多次持续扫描删除过期字典,直到字典中过期键值被删除的比较稀疏为止,而整个执行过程会导致Redis
的读写出现明显的卡顿,卡顿的另一种原因是内存管理需要频繁回事内存页,因此也会消耗一定的CPU
;
为了避免这种卡顿,我们需要预防大量缓存在同一时刻一起过期,最简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数。
8、客户端使用优化
在客户端的使用上我们除了要尽量使用pipeline
的技术外,还需要注意尽量使用Redis
连接池,而不是频繁创建销毁Redis
连接,这样就可以减少网络传输次数和减少了非必要调用指令。
9、限制redis内存大小
在64位操作系统中Redis
的内存大小是没有限制的,也就是配置项 maxmemory
是被注释掉的,这样就导致在物理内存不足时,使用 swap
空间(交换空间),而当操心系统将Redis
所用的内存分页移至swap
空间时,将会阻塞Redis
进程,导致Redis
出现延迟,从而影响Redis
的整体性能。因此我们需要限制Redis
的内存大小为一个固定的值,当Redis
运行到此值时会触发内存淘汰策略,内存淘汰策略在Redis 4.0
之后有8
种:
noeviction //不淘汰任何数据,当内存不足时,新增操作会报错,Redis默认淘汰策略;
allkeys-lru //淘汰整个键值中最久未使用的键值;
allkeys-random //随机淘汰任意键值;
volatile-lru //淘汰所有设置了过期时间的键值中最久未使用的键值;
volatile-random //随机淘汰设置了过期时间的任意键值;
volatile-ttl //优先淘汰更早过期的键值;
在Redis 4.0
版本又新增了2种淘汰策略:
volatile-lfu //淘汰所有设置了过期时间的键值中,最少使用的键值;
allkeys-lfu //淘汰整个键值中最少使用的键值。
10、使用物理机而非虚拟机安装Redis
服务
在虚拟机中运行 Redis
服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,我们可以通过 ./redis-cli --intrinsic-latency 100
命令查看延迟时间,如果对 Redis
的性能有较高要求的话,应尽可能在物理机上直接部署 Redis
服务器。
11、检查数据持久化策略
Redis
持久化策略是将内存数据复制到硬盘上,这样才可以进行容灾恢复或者数据迁移,但维护此持久化功能,需要很大的性能开销。
在Redis 4.0
之后,Redis
有了3
种持久化的方式:
- RDB(快照方式): 将某一时刻的内存数据,以二进制的方式写入磁盘;
- AOF(追加文件): 记录所有的操作命令,并以文本的形式追加到文件中;
- 混合持久化方式:
Redis 4.0
之后新增的方式,混合持久化是结合RDB
和AOF
的优点,在写入的时候,先把当前的数据以RDB
的形式写入文件开头,再将后续的操作命令以AOF
的格式存入文件,这样既能包装Redis
重启时的速度,又能减少数据丢失的风险。
混合持久化开启方式:
config get aof-use-rdb-preamble //查询混合持久化是否开启
//①通过命令行开启:
config set aof-use-rdb-preamble yes
//通过配置文件开启:
Redis根路径下 redis.conf文件 把配置文件中的 aof-use-rdb-preamble no 改为 aof-use-rdb-preamble yes
配置完成后,重启Redis
服务,配置才能生效。
12、禁用THP特性
Linux kernel
(内核)在2.6.38
内核增加了 Transparent Huge Pages
(THP)特性,支持大内存页 2M
分配,默认开启。
当开启了THP
时,fork
的速度会变慢,fork
之后每个内存页从4kb
变为2M
,会大幅增加重写期间父进程的消耗。同事每次写命令引起的复制内存页单位放大了512
倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的 incr
命令也会出现在慢查询中,因此Redis
建议将此特性进行禁用:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
为了使机器重启后THP
配置依然生效,/etc/rc.local
同样追加上述指令。
13、使用分布式架构来增加读写速度
Redis
分布式架构有三个重要手段:
- 主从同步
- 哨兵模式
Redis Cluster
集群
①、使用主从同步功能我们可以把 ‘写’ 放在主库上执行,‘读’ 转移到从库上,因此就可以在单位时间内处理更多的请求,从而提升Redis
整体的执行速度。
②、而哨兵模式是对主从功能的升级,但当主节点崩溃后,无需人工干预就能自动恢复Redis的正常使用。
③、Redis Cluster
是 Redis 3.0
正式推出的,Redis
集群是通过将数据库分散存储到多个节点上来平衡各个节点的压力。
Redis Cluster
采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0-16383
整数槽内,计算公式 slot =CRC16(key) & 16383
,每一个节点负责维护一部分槽以及槽所映射的键值数据。这样Redis
就可以把读写压力从一台服务器,分散给多台服务器了,因此性能会有很大的提升。
这三个功能中,我们只需使用一个就可以了,毫无疑问 Redis Cluster
应该是首选方案 ,它可以把读写压力自动的分担给更多的服务器,并且拥有自动容灾的能力。