Redis高性能原理
前言
简介
Redis 是一种开源(BSD 许可)、内存中数据结构存储,用作数据库、缓存和消息代理。底层使用c语言进行实现。Redis 提供了诸如字符串、散列、列表、集合、带范围查询的排序集合、位图、超级日志、地理空间索引和流等数据结构。Redis 内置复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久化,并通过 Redis Sentinel 和 Redis Cluster 自动分区提供高可用性。
地址
官网:https://redis.io/
GitHub:https://github.com/redis/redis
Redis数据类型简介:https://redis.io/topics/data-types-intro
命令完整列表:https://redis.io/commands
完整文档地址:https://redis.io/documentation
基本特性
- 非关系型的键值对数据库,可以根据键以O(1)的时间复杂度取出或插入关联值;
- 数据是存储在内存中的;
- 键值对中键的类型可以是字符串、整形、浮点型等,且键是唯一的,相同的键会覆盖值;
- 键值对中值类型可以是:String、Hash、List、Set、ZSet等
- 内置了复制、磁盘持久化、LUA脚本、事务、SSL、ACLS、客户端缓存、客户端代理等功能;
- 提供了Redis哨兵、Redis cluster等高可用性的模式;
核心数据结构
String
String类型的结构对应JAVA的Map,key-val的存储形式
常用操作
命令 | 描述 |
---|---|
SET key value | 存入字符串 |
MSET [key val …] | 批量存入字符串 |
GET key | 获取值 |
MGET [key …] | 批量获取值 |
DEL [key …] | 删除一个或多个键 |
EXPIRE key seconds | 设置一个键过期时间(s) |
SETNX key val | key不存在且保存成功返回1,失败返回0,可用做分布式锁 |
incrby key val | 批量生产序列号 |
SET key val EX 10 NX | 防止程序意外终止导致死锁 |
应用场景
- 单值缓存
- 对象缓存
- 分布式锁
- 计数器
- Web集群session共享
- 分布式系统全局系列号等
Hash
Hash类型的结构,存储一个类型的key-val
常用操作
命令 | 简述 |
---|---|
HASH key field value | 存储一个哈希表key的键值 |
HASHNX key field value | 存储一个不存在的哈希表key的键值 |
HMSET key field | 获取哈希表key对应的field键值 |
HMGET key [field …] | 批量获取field键值 |
HDEL key [field …] | 批量删除 |
HLEN key | 返回哈希表key中field的数量 |
HGETALL key | 返回哈希表key中所有的键值 |
HINCRBY key field increment | 为哈希表key中field键的值加上增量 |
应用场景
- 对象缓存
- 电商购物车等
优缺点
- 优点
- 同类数据归类整合储存,方便数据管理
- 相比String操作小号的内存与cpu更小
- 相比Stirng更节约存储空间
- 缺点
- 过期功能不能使用在field上,只能使用在key上
- Redis集群架构下不适合大规模使用
List
List,列表结构,跟JAVA中的List基本相似
命令 | 描述 |
---|---|
LPUSH key [value …] | 将一个或多个value值插入到key列表的表头 |
RPUSH key [value …] | 将一个或多value值插入多key列表表尾 |
LPOP key | 返回并移除key列表的头元素 |
RPOP key | 返回并移除key列表的尾元素 |
LRANGE key start stop | 返回列表key中制定区域内的元素,区间则以start和stop指定 |
BLPOP [key…] timeot | 从key列表表头弹出元素,若列表为空则阻塞等待,若timeout=0则一直阻塞 |
BRPOP [key…] timeout | 从key列表尾部弹出元素,若列表为空则阻塞等待,若timeout=0则一直阻塞 |
应用场景
- stack(栈) LPUSH + LPOP
- Queue (队列) LPUSH + RPOP
- Blocking MQ(阻塞队列)LPUSH + BRPOP
Set
Set集合,val中存储多个不重复的元素
常用命令
命令 | 描述 |
---|---|
SADD key [momber …] | 往集合key中存入元素,元素存在则忽略 |
SREM key [momber …] | 从集合key中删除元素 |
SMEMBERS key | 获取集合key中所有元素 |
SCARD key | 获取集合key的元素个数 |
SISMEMBER key momber | 判断val元素是否存在于key集合中 |
SRANDMENMBER key [count] | 从集合key中选出count个元素,元素不移除 |
SPOP key [count] | 从集合key中选出count个元素并移除元素 |
SINTER [key …] | 交集运算 |
SINTERSTORE newSet [key …] | 将交集结果存入新集合newSet中 |
SUNION [key …] | 并集运算 |
SUNIONSTORE newSet [key …] | 将并集结果存入新集合newSet中 |
SDIFF [key … ] | 差集运算 |
SDIFFSTORE newSet [key …] | 将差集结果存入新集合newSet中 |
应用场景
- 抽奖
- 去重
- 点赞/取消点赞
- 集合操作实现关注模型等
- 集合操作实现商品赛选等
ZSet
与Set集合类似,ZSet集合是有序的
命令 | 描述 |
---|---|
ZADD key [[score member] …] | 往有序集合key中加入带分值元素 |
ZREM key [member …] | 从有序集合key中删除元素 |
ZSCORE key member | 返回有序集合key中member元素的分值 |
ZINCRBY key increment member | 为key中元素member的分值加上increment |
ZCARD key | 返回key中的元素个数 |
ZRANGE key start stop [withscores] | 正序获取key中start到stop的元素 |
ZREVRANGE key start stop [withscores] | 倒序获取key中start到stop的元素 |
应用场景
- 微博排行榜
- 七日排行榜单等
Redis的单线程和高性能
-
Redis的单线程并非是真正意义上的单线程。单线程主要是指网络IO和键值对读写是由一个线程来完成,而这也是Redis对外提供键值存储服务的主要流程。Redis内部的其他功能,例如持久化、 异步删除、数据同步等等是会有额外的线程去执行的。
-
单个线程访问之所以还会那么快,是因为数据都存在内存中,所有的运算都是内存级别的运算,单线程同时避免了多线程所带来的上下文切换问题。
-
单线程的任务我们需要注意访问的命令,对于那些耗时的命令以及当访问一个bigkey的时候可能会导致Redis卡顿。
-
对于客户端的并发连接,Redis底层利用epoll模型来实现IO多路复用,将链接信息和时间放到队列中,依次放到文件事件分派器,事件分派器将事件分发给对应的事件处理器。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WDzBj2Sb-1681226950199)(/Users/aieny/Documents/学习笔记/redis/Redis-IO多路复用.png)]
其他高级命令
keys
全局遍历所有的key,用来列出所有满足特定正则字符串规则的key,当Redis数据量较大时,性能会有所下降,应避免使用
scan
SCAN cursor [MATCH pattern] [COUNT count] 渐进式便利所有的键,相对于keys性能消耗更小,安全性更低
scan 参数提供了三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式,第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。
⚠️注意:scan并非完美无瑕, 如果在scan的过程中如果有键的变化(增加、 删除、 修改) ,那么遍历效果可能会碰到如下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说scan并不能保证完整的遍历出来所有的键, 这些是我们在开发时需要考虑的。
info
查看Redis服务运行信息,分为以下9大块,每块都存在很多参数
- Server 服务器运行的环境参数
- Clients 客户端相关信息
- Memory 服务器运行内存统计数据
- Persistence 持久化信息
- Stats 通用统计数据
- Replication 主从复制相关信息
- CPU CPU使用情况
- Cluster 集群情况
- KeySpace 键值对统计数量信息
Redis持久化
RDB快照(snapshot)
默认情况下,Redis会将内存数据库快照保存在一个名称为dump.rdb的二进制文件中。我们可以通过**.conf配置文件中的save**属性进行设置,让它在"N秒内数据集至少有M个改动"这一条件被满足时,自动保存一次数据。
当我们需要关闭RDB只需要将所有的save保存策略注释掉即可
不仅如此,还可以手动执行命令生成RDB快照。进入Redis客户端执行命令save或者bgsave就可以生成dump.rdb文件,每执行一次命令,就会重新生成一个当前Redis内存的快照,并覆盖原有的rdb快照文件内容。
save 写入是同步执行的,当我们的redis内存过大时,要将如此大的数据量快照写入rdb文件中,是很耗时的,会阻塞我们其他线程的执行。因此redis也提供了另一种写入策略bgsave。
bgsave:写时复制(COW机制)
Redis借助操作系统的写时复制技术(Copy-On-Write,COW),在生成快照的同时,依旧可以正常处理写命令。其原理时由主线程fork生成的bgsave子进程,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它写入rdb文件。此时如果主线程对这些数据也都是读操作,则主线程与bgsave子进程互不影响;如果主线程要修改某一块数据时,则会将这块数据复制一份生成改数据的副本,再由bgsave子进程写入rdb文件,在这个过程中主线程仍然可以直接修改原来的数据。
save与bgsave对比
命令 | I/O类型 | 是否阻塞其他命令 | 复杂度 | 优点 | 缺点 |
---|---|---|---|---|---|
save | 同步 | 是 | O(n) | 不回消耗额外内存 | 阻塞客户端命令 |
bgsave | 异步 | 否(在生成子进程执行调用fork函数时会短暂阻塞) | O(n) | 不阻塞客户端命令 | 需要fork子进程,消耗内存 |
⚠️注意:配置自动生成rdb文件后台使用的是bgsave方式
AOF (append-only file)
快照功能并不是非常耐久(durable)。如果redis因为某些原因而造成故障停机,那么服务器将丢失最近写入和还未保存到快照中的数据。从1.1版本开始,redis增加了一种完全耐久的持久化方式:AOF持久化,价格修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)。
可以通过修改配置文件来打开AOF功能:
appendoly yes
打开AOF功能后,每当redis执行一次改变数据集的命令时(比如SET),这个命令就会被追加到AOF文件的末尾。这样的话,当redis重新启动时,程序就可以通过重新执行AOF文件中的命令来达到重建数据集的目的。
可以通过以下三个配置,告知redis多久才将数据fsync到磁盘一次:
- appendfsync always:每次有新命令追加到AOF文件时就执行一次,非常慢,很安全。
- appendfsync everysec:每秒一次,足够快,并且故障时仅丢失1s的数据。(默认)
- appendfsync no:从不提交,将数据交给操作系统处理,速度很快,最不安全的选择。
AOF重写
当文件里存在太多没用指令,AOF会定期根据内存的最新数据生成aof文件,也可以执行命令 bgrewriteaof 手动进行重写。
⚠️注意:AOF重写redis会fork出一个子进程去执行,不会对redis正常命令处理有太多影响。
以下两个配置可以控制AOF重写频率
- auto-aof-rewrite-min-size 64mb // 文件至少达到64m才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
- auto-aof-rewrite-percentage 100 //文件自上一次重写后文件大小增长了100%则再次触发重写
RDB 和 AOF 对比
命令 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
回复速度 | 快 | 慢 |
安全性 | 容易丢失 | 根据策略决定 |
在生产环境下,我们可以选择两者都开启,当redis启动时如果rdb文件与aof文件都存在时,优先选择aof文件进行数据恢复,因为aof相对安全一点。
混合持久化 (Redis 4.0)
重启redis时,通常不会使用RDB来恢复内存状态,因为会丢失大量的数据。通常会使用AOF日志重放,但是日志系性能对RDB来说要慢得多,因此在redis实例很大的情况下,启动需要花费很长的时间。Redis 4.0为了结局这个问题,带来一种新的持久化 - 混合持久化。
aof-use-rdb-preamble yes //开启混合持久化
⚠️注意:开启混合持久化,必须先开启AOF
当开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存的命令存放在一起,都写入到新的AOF文件,新的文件一开始不叫 appendonly.aof ,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。于是在redis重启时,可以先加载RDB的内容,然后再放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率会大幅提高。
Redis备份策略
- 写crontap定时调度脚本,每小时都copy一份rdb或aof到一个目录中去,仅仅保留最近48小时的备份;
- 每天都保留一份当日的数据备份到一个目录中去,可以保留最近一个月的备份;
- 每次copy备份的时候,把太久的备份删除;
- 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏数据丢失;
Redis主从架构
搭建
-
复制一份redis.cof 文件;
-
将相关配置修改为如下的值:
port 6380 pidfile /var/run/redis_6380.pid #把pid进程号写入pidfile配置的文件 logfile "6380" dir /usr/local/redis-5.0.3/data/6380 #指定数据存放目录
-
配置主从复制
replicaof 192.168.0.1 6379 # 从本季6379端口的redis实例复制数据,redis5.0之前使用slaveof replica-read-omly yes # 配置从节点只读
-
启动从节点
-
客户端链接节点
工作原理
主从复制(全量复制)
- 如果你为master节点配置了一个slave,不管这个slave是否第一次连接上master,它都会发送一个PSYNVC命令给master请求复制数据。
- master节点收到PSYNC命令后,会在后台通过bgsave进行数据持久化生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,他会把这些可能修改数据集的请求缓存在内存中。
- 当持久化进行完毕后,maser会把这份rdb文件数据集发送给slave,slave会把己收到的数据进行rdb持久化,然后加载到内存中。若master内存中存在后续修改的数据集,再将之前缓存的命令发送给slave。
- 当master与slave之间的链接由于某种原因断开连接时,slave能够自动链接master,如果master收到多个slave并发链接请求,它只会进行一次持久化,而不是一次链接持久化一次,会把这一份持久化的数据发送给多个并发连接的slave。
流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sVWt8Axq-1681226950200)(/Users/aieny/Documents/学习笔记/redis/Redis主从复制(全量复制).png)]
主从复制(部分复制)
- 当master和slave断开连接,一般都会对正分数据进行复制。但是从2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后进行部分数据复制(断电续传);
- mastet会在内存中创建一个缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了缓存的数据下表offset和master的进程id;
- 当网络断开,slave请求master继续进行未完成的复制,从所记录的数据下标开始,如果master进程id变了或者slave节点数据下标offset已经不在master缓存队列中时,会进行一次全量复制。
流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNTcLj6z-1681226950200)(/Users/aieny/Documents/学习笔记/redis/Reids主从复制(部分复制).png)]
⚠️注意:repl buffer中存的数据是先进先出的,当偏移量(offset)已经找不到,则全量复制
问题
主从复制风暴
从上述可以看到我们redis主从同步的执行流程,当我们一个主节点存在很多从节点,从节点同时复制主节点会导致主节点压力过大。这就是我们所说的主从复制风暴。
对于主从复制风暴,我们可以通过让部分的从从节点不再从主节点同步数据,而是跟从节点同步数据,具体结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NNroIVx-1681226950200)(/Users/aieny/Documents/学习笔记/redis/主从节点同步模型.png)]
管道(Pipeline)与 Lua脚本
管道
客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完成后再一次性读取服务器的响应,这样可以极大的降低多条命令执行的网络传输开销。管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。需要注意的是用pipeline方式打包命令发送,redis必须处理完所有命令前先缓存起所有命令的处理结构。打包的命令越多,缓存消耗的内存也越多,所以并不是打包的命令越多越好。
pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达所有的命令一起成功的语义,管道中前面命令失败并不会影响到后面命令的执行,同时管道的操作并非原子的。
Lua脚本
reids 在2.6版本推出的脚本功能,允许开发者使用lua语言编写脚本传到redis中执行。通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。EVAL命令格式如下:
EVAL script numkeys key [key ..] arg [arg ...]
// 1.script参数是一段Lua脚本程序,它会被运行在redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
// 2.numkeys参数用于指定键名参数的个数。
// 3.键名参数key [key ...],从EVAL第三个参数开始算起,表示在脚本中所用到的那些redis键,这些键名参数可以在Lua中通过全局变量KEYS数组,用1/2/3形式访问
// 4.在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)
// 例如
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
// 其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的Lua脚本,数字2指定了键名参数的数量, key1和key2是键名参数,分别使用 KEYS[1] 和 //KEYS[2] 访问,而最后的 first 和 second 则是附加参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。
⚠️注意:redis时单进程、单线程执行脚本,因此不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令,所以使用时要注意不能出现死循环、耗时运算
优势
- 减少网络开销:这点跟管道类似,使用脚本也可以减少网络往返时间;
- 原子操作:redis会将脚本作为一个整体去执行,中间不会被其他命令所影响;
- 替代redis事务:redis自带事务很鸡肋,而lua脚本几乎实现了常规的事务功能,同时官方也推荐使用lua脚本替代redis本身的事务功能;
Redis哨兵
搭建
- 复制一份 sentinel.conf 文件
- 将相关配置修改为如下值:
port 26379
daemonsize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0/data"
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
# quorum 是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel/2+1),master才算真正失效
sentinel monitor mymaster 192.168.0.1 6379 2
- 启动sentinel哨兵实例
src/redis-sentinel sentinel-26379.conf
- 查看sentinel的info信息,当看到Sentinel的info里已经识别出redis主从,表示成功
工作原理
sentinel哨兵是特殊的redis服务,不提供读写,主要用来监控redis实例节点。
哨兵架构下client端第一次从哨兵找出redis主节点,后续就直接访问redis主节点,不会每次都通过sentinel代理访问redis主节点,当redis主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端。client端会实现订阅功能,订阅sentinel发布的节点变动消息。如果redis主节点挂了,哨兵集群会重新选举出新的redis主节点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XqyN6vGD-1681226950200)(/Users/aieny/Documents/学习笔记/redis/Reids主从哨兵架构.png)]
优缺点
在Redis 3.0以前的版本要实现集群一般是借助哨兵sentinel节点的状态,在高可用高并发等场景下会存在以下问题:
- 如果mastr节点异常,则会做主从切换,将某一台slave作为master,消耗时间和性能;
- 哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间会存在访问瞬断的情况;
- 哨兵模式只有一个主节点对外提供服务,没法支持很高的并发;
- 当耽搁主节点内存设置过大,否则会导致持久化文件多大,影响数据恢复或主从同步的效率。
Redis高可用集群(Redis Cluster)
redis集群是一个又多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片的特性。redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展。根据官方文档称,可线性扩展到上万个节点(推荐不超过1W个节点)。redis集群的性能和高可用性均优于哨兵模式,且配置简单。
搭建
redis集群搭建至少需要三个master节点,搭建的每个master再搭建一个或多个slave节点。每个主从节点之间会形成一个小的节点集群,小的节点集群也会进行主节点的选举。
⚠️注意:集群的slave不支持读写,为了方便水平扩展,集群所有的读写都是通过master节点来完成的,slave节点仅用于备份数据,在master挂掉之后进行选举新的master节点
搭建步骤:
1. 在第一台机器的/usr/local目录下创建文件夹redis-cluster,然后在其下面分别创建2个文件夹
(1)mkdir -p /usr/local/redis-cluster
(2)mkdir 8001 8004
2.把redis-cof配置文件copy到 8001 下,并修改成以下内容:
daemonize yes
port 8001 #每台机器的端口号都要设置
dir /usr/local/redis-cluster/8001 #指定数据文件存放位置,必须指定在不同目录位置
cluster-enabled yes #开启集群模式
cluster-config-file nodes-8001.config #集群节点信息文件,这里800x最好对应port
custer-node-timeout 5000 # 节点超时时间
bind 127.0.0.1 # bind绑定的是自己机器的网卡ip,如果有多个网卡可以配置多个ip,代表允许客户端通过机器的哪些ip去访问,内网一般不配置bind,注释掉即可
protected-mode no # 关闭保护模式
appendonly yes
requirepass xxx # 设置redis访问密码
masterauth xxx # 设置集群节点间访问密码
3.把修改好的配置文件,copy到8004,修改 port、dir、clster-config-file里的端口
4.另外两台机器重复上面 3步
5.分别启动6个redis实例
6.用reids-cli创建整个redis集群(以前的版本集群是依靠ruby脚本 redis-trib.rb实现)