之前《MongoDB是什么?看完就知道了》收到许多赞,作为该文章的姐妹篇,本文主要从运维开发的角度总结Redis的知识,力求简单,形成思维导图,总结Redis的一些特点优点,为之后的技术选型做一个有力的指导笔记。或是给新手苦恼找不到Redis的学习资料,不知道如何入手,深入时的一个好的建议。本文1w+字,如果你只想了解其中一些知识,看目录啦
注意!本文不是满满的API干货也不是实战,他更像是一个缓存选型时对Redis的统揽,如果你需要API的话移步百度啦
本文的整理心路历程
1、快读《Redis开发与运维》
2、精度读《Redis开发与运维》+整理xmind
3、xmind输出文章
一、Redis总览
1、书籍推荐(后面审核后将会分享书籍)
了解Redis的过程中,主要从三本书籍入手,分别说下测评,让新手能第一次就选中合适的书籍。
1.1、《Redis开发与运维》:强烈推荐,该书从开发与运维的角度解析Redis的每个知识点,采用分点论述的结构,容易梳理成思维导图,由浅入深,从基本使用到原理解析全面覆盖,本文章也是按该书的读后思维导图进行编写。
1.2、《Redis设计与实现》:该书主要从源码的角度讲述Redis知识点,比较适合玩过一段时间的老鸟看,特别是适合从事Redis相关运维平台开发的人员观看,它带你理解透彻底层源码。
1.3、《Redis实战》:主要通过叙事的方式讲述Redis的知识点,逻辑层次结构不是很明显突出,不好整理,但是对于急于入门Redis开发的人员来说也是一个比较好的选择,毕竟我也是实战系列的崇拜者。
2、Redis是什么?
2.1、一句话描述:是一个键值对的NoSQL的内存数据库。
2.2、存储区域
- 基于内存存储:读写惊人,读写都在内存
- 支持硬盘存储:异步(基于AOF/RDB机制)刷磁盘,防丢失
2.3、基本数据结构
- string
- hash
- list
- set
- zset
- GEO等(不常用先忽略)
这里可以对标java的Collection框架,但是Redis牛逼它几个数量级
2.4、其他牛逼的功能
- 支持键过期
- 支持发布订阅(效果肯定没RabbitMq理想啦)
- 支持事务(你真想用事务,那你不应该选NoSQL)
- 流水线(联想Batch操作)
3、为什么要用Redis?
3.1、速度快
- 基于内存存储
- C语言开发
- 单线程架构(划重点,Redis不会有并发问题且快!使用epoll做I/O多路复用技术实现,避免线程切换和竞态消耗)
3.2、基于键值对的数据结构服务器,为缓存场景而产生
3.3、拥有许多丰富的功能
- 键过期
- 发布订阅
- 支持Lua扩展
- 支持事务
- 支持流水线
3.4、简单稳定,它的代码不超过1w行,你敢信?
3.5、支持客户端语言多,Jedis u know
3.6、支持持久化
3.7、支持主从复制(哨兵)
3.8、支持高可用与分布式(分布式,你可以理解为分区表)
4、Redis经典的使用场景
- 缓存
- 排行榜
- 计数器
- 社交网络
- 消息队列
5、如何用好它?
- 不要当黑盒的使用,开发与运维同样重要
- 要会阅读源码或是阅读书籍啦!
二、API
1、常用全局命令
keys * #获得所有键,不要用咯,会卡死你的服务器
dbsize #键总数,统计的,不用担心啦
exist {key} #是否存在
del key {key}
expire {key} seconds #设置过期
ttl {key} #查看是否过期
type {key} #键的类型
object encoding {key} #查看内部编码,有助于内存优化,编码类型决定内存使用大小
rename key newkey #重命名
randomkey #随机返回一个
select dbIndex #选择数据库,注意因为Redis是单线程的,所以不建议使用多个数据库
flushdb #清库(危险!小心!)
fluashall #清所有(危险!小心!)
config set #修改配置(并不是所有配置都可以动态修改)
config rewrite #配置写出到文件
2、字符串
2.1、单个键值对的最大值不能超过512m
2.2、内部编码
- int 8个字节长整型
- embstr小于等于39个字节的字符串
- raw大于39个字节的字符串
2.3、经典使用场景
- 缓存
- 计数
- 共享session
- 限速
2.4、命令
set key value [ ex seconds ] [ px milliseconds ] [ nx|xx ] #设置值
setex #设置值
setnx #设置值
get key #获取值
mset key value [ key value ] #批量设置值
mget key [ get key ] #批量获取值
incr key #自增
decr key #自减
incrbyfloat #自增浮点数
append key value #追加值
strlen key #字符串长度
getset key value #设置并返回值
setrange key offset value #设置指定位置的字符
getrange key start end #获取部分字符串
3、哈希
3.1、哈希类型中的映射关系叫做field-value 对应的键是叫field
3.2、内部编码
- ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀
- hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
3.3、使用场景
- 缓存
3.4、命令
hset key field value #设置值
hget key field #获取值
hdel key field #删除
hlen key #计算个数
hmset key field value [ field value ] #批量设置
hmget key field #批量获取
hexist key field #是否存在
hkeys key #获取所有域
hvals key #获取所有值
hincrby bincrbyfloat #累加
htrlen key field #计算字符串长度
4、列表
4.1、可以充当栈与队列
4.2、特点
- 可以通过索引下标获取某个元素
- 支持重复元素
4.3、内部编码
-
ziplist:当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使
用 -
linkedlist:当列表类型无法满足ziplist的条件时,Redis会使用
linkedlist作为列表的内部实现。 -
quicklist(^3.2)
4.4、经典使用场景
- 消息队列
- 文章列表(分页获取时)
- lpush+lpop=Stack
- lpush+rpop=Queue
- lpush+ltrim=Capped Collection
- lpush+brpop=Message Queue
4.5、命令
rpush key value #右插入
lpush key value #左插入
linsert key before|after pivot value #在某个元素的前或后插入
lrange key start end #获取指定范围的元素
lindex key index #获取指定下标的元素
llen key #获取长度
lpop key #从左侧弹出元素
rpop key #从右侧弹出元素
lrem key count value #删除指定元素
ltrim key start end #按索引范围修剪
lset key index newValue #修改
blpop key timeout #阻塞式的弹出
rlpop key timeout #阻塞式的弹出
5、集合
5.1、内部编码
- intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用
- hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现
5.2、使用场景
- 给用户添加标签
- 给标签添加用户
- 生成随机数
5.3、命令
sadd key element #添加元素
srem key element #删除元素
scard key #计算元素个数
sismember key element #是否存在
srandmemeber key #随机从集合中返回个数元素
spop key #从集合中随机弹出元素
smembers key #获取所有元素
sinter key #交集
sunion key #并集
sdiff key #差集
6、有序集合
6.1、内部编码
- ziplist:当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist
可以有效减少内存的使用 - skiplist:当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降
6.2、使用场景
- 添加用户赞数
- 取消用户赞数
- 获取赞数最多的十个用户
- 展示用户信息以及用户分数
6.3、命令
zadd key score member #添加成员
zcard key #计算个数
zscore key member #计算成员分数
zrank key member #计算成员排名
zrevrank key member
zrangebyscore key min max #根据范围获取
zcount key min max #返回指定分数范围的个数
zremrangebyrank key start end #删除指定排名内的升序元素
zremrangebyscore key min max #删除指定分数范围的成员
zinterstore destination numkeys key #交集
zunionstore destination numkeys key #并集
7、Bitmaps
7.1、可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id
7.2、在存储量很大的情况下可以节省内存,但是在存储量很小的情况下,并不会节省内存
7.3、命令
setbit key offset value #设置值
gitbit key offset #获取值
bitcount #统计范围个数
bitop op destkey key #Bitmaps间的运算
bitops key targetBi #计算偏移量
8、HyperLogLog
8.1、HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计
8.2、特点
- 海量存储
- 计算有一定的误差
- 占用内存小
8.3、命令
pfadd key element #添加
pfcount key #计算
pfmerge dseskey sourcekey #合并
9、GEO
9.1、Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能
9.2、命令
geoadd #增加
geopos key member #获取
geodist key member #计算距离
georadius key #获取指定范围的数据
zrem #删除
三、附属功能
1、慢查询
1.1、客户端命令的生命周期
客户端->缓冲队列->被执行->响应结果
1.2、相关配置
- showlog-log-slower-than 预设阀值,它的单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000
- showlog-max-len 一个新的命令满足慢查询条件时,被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出
1.3、配置方法
- 写入配置文件
- 通过动态 config set 配置
1.4、获取
slowlog get [n]
slowlog len
slowlog reset
2、shell 工具
2.1、redis-cli
-r #重复执行
-i #每隔几秒执行
-x #执行命令
-a #认证
--latency #检测网络延迟
--stat #统计信息
--pipe #执行流水线
2.2、redis-server
--test-memory #探测内存是否足够
2.3、redis-benchmark
一款自带的压测工具
3、Pipeline 流水线
3.1、它能将一组命令进行组装,通过RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端
3.2、性能
- 比逐条执行块
- 网络延迟越大,效果越明显
3.3、与原生批量命令的区别
- 原生命令是原子性的
- 原生命令是一个命令对应多个key
- 原生命令是服务端支持实现的
4、事务
4.1、执行命令
multi #事务开始
exec #事务结束
discard #取消事务
注意:不支持事务回滚
5、支持使用Lua扩展
6、发布订阅
6.1、使用场景
- 业务解耦
6.2、特点
- 客户端在执行订阅命令之后进入了订阅状态,只能接受subscribe、psubscribe、unsubscribe、punsubscribe的四个命令
- 新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化
- 和很多专业的消息队列系统(例如Kafka、RocketMQ)相比,Redis的发布订阅略显粗糙,例如无法实现消息堆积和回溯。但胜在足够简单,如果当前场景可以容忍的这些缺点,也不失为一个不错的选择
6.3、命令
publish channel message #发布
subscribe channel #订阅
unsubscribr #取消订阅
psubscribe pattern #按模式订阅
pubsub channels #查看活跃频道
pubsub numsub channel #查看频道订阅数
pubsub numpat #查看模式订阅数
四、客户端
1、客户端通信协议
客户端与服务器端之间的通信协议是在TCP协议之上的,Redis制定RESP协议,实现客户端与服务器端的正常交互
2、协议格式
2.1、请求格式
*3 #参数数量
$3 #下面这个参数的字节数
SET
$5 #下面这个参数的字节数
hello
$5 #下面这个参数的字节数
world
#注意以上是一条命令,它们之间使用 \r\n 去隔离
2.2、响应格式
+OK
#状态回复:在RESP中第一个字节为"+"。
#错误回复:在RESP中第一个字节为"-"。
#整数回复:在RESP中第一个字节为":"。·字符串回复:在RESP中第一个字节为"$"。
#多条字符串回复:在RESP中第一个字节为"*"。
#nil结果响应 $-1
3、Java客户端 Jedis
3.1、用法
- 支持直连
- 支持使用连接池连
- 支持使用集群方式连
- 支持Pipeline
- 支持Lua
4、客户端管理
4.1、罗列当前所有连接客户端
client list
id=2296950 addr=20.X.X.XX:17969 fd=19 name= age=66163 idle=17 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
- 标志
- id:唯一标志
- addr :发起的IP和端口
- fd:socket的文件描述符
- name:名字
- 存活状态
- age:已连接的时间
- idel:最近一次空闲的时间
- 客户端类型
- flags:类型
- 输入缓冲区(Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能)
- qbuf:总容量
- qbuf-free:剩余容量
- 注意1:Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能。
- 注意2:输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等。
- 输出缓冲区
- obl:代表固定缓冲区的长度
- oll:代表动态缓冲区列表的长度
- omem:代表使用的字节数
4.2、杀死客户端
client kill ip:port
4.3、阻塞客户端
client pause timeout
4.4、监控
monitor #注意会导致输出缓冲区急剧增大
4.5、关于客户端的相关配置
- timeout:检测客户端的超时时间
- maxclients:客户端的最大连接数
- tcp-keepalive:检测链接是否存货
4.6、关于客户端统计的信息片段
info clients
info stats
5、常见与客户端有关的问题
- 无法从连接池获取到链接
- 读写超时
- 链接超时
- 缓冲区异常
- Redis使用的内存超过配置
- 客户端连接数过大
五、持久化
1、RDB
1.1、触发机制
1.1.1、RDB持久化是吧当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程可以是自动触发,也可以是手动触发
1.1.2、手动触发
save #阻塞当前Redis服务器,直到RDB过程完成为止,注意如果内存使用较多的话阻塞的时间会比较长
注意:线上环境不建议使用
bgsave #Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束
注意:阻塞只发生在fork阶段,此时需要一定量的内存去支持子线程
1.1.3、自动触发
- 根据配置文件自动触发
#这里是配置文件
save m n #表示m秒内存在n次修改时,自动触发bgsave
- 从节点复制时:从节点执行全量复制操作时,自动触发主节点bgsave生成RDB文件并发送给从节点
- 执行debug reload 时会重新加载Redis且会做一次保存操作
- shutdown时
1.2、流程说明
- 执行bgsave 命令,判断父进程是否正在执行 RDB/AOF子进程,如有,则直接返回
- 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过以下可以看到最近一个fork操作的耗时
info stat|grep latest_fork_usec
- fork成功后,通知父进程, 不会再阻塞
- 子进程创建RDB文件,完成对原文件进行原子替换。此时,通过以下命令可查看最后一次RDB生成时间
lastsave #最后一次备份时间
info |grep rdb_last_save_time
1.3、RDB文件的处理
1.3.1、保存
dbfilename #通过配置执行文件名
config set dir
config set dbfilename
1.3.2、压缩
config set rdbcompression {yes|no}
1.3.3、文件修复
redis-check-dump #使用改工具检测并获得报告
1.4、RDB的优缺点
- 优点:代表Redis在某个时间点上的数据快照。非常适用于备份,全量恢复等场景,利用该种方式恢复速度比AOF方式快
- 缺点:无法做到实时持久化,新旧版本可能存在格式不兼容问题
2、AOF
以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的,可以联想mysql的bin-log
2.1、使用
#以下是配置文件
appendonly yes #默认不开启
appendfilename appendonly.aof #存储名称
2.2、执行流程
- 所有的写入命令会追加到aof_buf 缓冲区中
- AOF缓冲区根据对应的策略向硬盘做同步操作
- 随着AOF文件越来越大,需要定期重写,达到压缩的目的
- Redis重启时,加载文件数据恢复
2.3、命令写入的问题
set hello world -> *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n #是不是很熟悉,对的就是RESP协议
使用这种协议的好处:
- 文本协议具有很好的兼容性
- 避免二次开销
- 具有可读性
为什么先写缓冲区?
这样可以在性能和安全性方面做出平衡
2.4、写入策略
- always
- 命令写入缓存后直接同步硬盘
- 每次写入的都要同步,不建议配置
- everysec
- 每秒刷盘
- 最多丢失1s数据
- no
- 由系统决定刷盘时间,周期最长30s
- 安全性无法保证
2.5、重写机制
2.5.1、为什么可以变小
- 超时数据不再写入
- 剔除无效的命令
- 命令合并
2.5.2、手动触发
bgrewriteaof
2.5.3、自动触发
根据以下配置参数触发
auto-aof-rewire-min-size
auto-aof-rewire-percentage
触发算法
自动触发时机=aof_current_size>auto-aof-rewrite-min-
size&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite-percentage
2.5.4、重写流程
- 执行AOF重写流程
- 父进程执行fork创建子进程,开销等同于bgsave过程
- 主进程fork操作后,继续响应其他命令
- 子进程根据内存快照,按照命令合并规则写入到新的AOF文件
- 新AOF文件写入完成分后,子进程发送信号给父进程,父进程更新统计信息
- 父进程把AOF重写缓冲区的数据写入到新的AOF文件
- 使用新的AOF文件替换老文件,完成AOF重写
2.6、重启加载
- 优先加载AOF文件
- AOF关闭或AOF文件不存在时,加载RDB文件
- 加载AOF/RDB文件后,Redis启动成功
- 失败打印失败信息
2.7、文件校验
- 对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof–fix命令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可以人工修改补全
- AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启
3、问题与优化
3.1、fork操作
3.1.1、问题定位
info stats|grep lastest_fork_usec #获取最近一次fork时间
3.1.2、如何改善
- 优先使用高效支持fork操作的虚拟化技术
- 控制实例的最大可用内存,建议内存控制在10G以内
- 合理配置Linux内存分配策略,避免物理内存不足导致fork失败
- 降低fork操作的频率
3.2、子进程开销监控与优化
- CPU
- 不要进行CPU绑定
- 不要和其他CPU密集型服务部署一起
- 内存
- 避免在大量写入时做子进程重写操作
- 硬盘
- AOF重写时会消耗大量硬盘I/O
3.3、AOF追加阻塞
Redis使用另一条线程每秒执行fsync同步硬盘。当系统繁忙时,会造成主线程阻塞
3.3.1、刷盘流程
- 主线程负责写入AOF缓冲区
- AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间
- 主线程负责对不上次 AOF时间
- 2s直接返回
- 超过2s,阻塞,直到同步操作完成
3.3.2、存在的问题
- 最多丢失2s数据
- 如果系统fsync缓慢,将会导致Redis主线程阻塞影响效果
3.3.3、阻塞问题定位
- 发生AOF阻塞时,会在日志中打印
- 每当发生AOF追加阻塞事件发生时,在info Persistence统计中,aof_delayed_fsync指标会累加,查看这个指标方便定位AOF阻塞问题
- AOF同步最大允许2s延,当延迟发生时,可以监控是否是在写磁盘
六、复制
1、配置
1.1、建立复制
- 通过配置文件
slave of mastetHost masterPort #配置文件
- 通过启动命令参数
./redis-server --slaveof
- 通过运行时命令
slaveof
- 查看主从信息
info replication
1.2、断开复制
slaveof no one
1.2.1、切主流程
- 使用命令断开主从关系
- 与新主节点建立复制关系
- 删除从节点当前所有数据
- 对新主节点进行复制操作
注意:切主后从节点会清空之前所有的数据,线上人工操作时小心slaveof在错误的节点上执行或指向错误的主节点
2、拓扑
- 一主一从
- 一主多从
- 树状主从
3、原理
3.1、复制过程
- 保存主节点信息
- 从节点尝试与该节点建立网络链接
- 发送ping命令
- 检测主从之间网络套接字是否可用
- 检测主节点是否可接受处理命令
- 权限验证
- 同步数据集
- 命令持续部分复制
3.2、同步数据使用了什么机制?
- 复制偏移量
- 主节点记录偏移量信息
- 从节点每秒钟上报自身偏移量
- 复制积压缓冲区:当有一条命令进来的时候,需要先转存到复制积压缓冲区,这里的复制积压缓冲区有一定的大小
- 主节点运行id
- Redis节点的唯一标志,会因重启而改变
- debug reload重启不会改变,但是该命令会阻塞,触发持久化操作
- psync命令
- 利用改命令完成部分复制与全量复制
- 1、从节点发送psync命令给?节点,参数runid是当前从节点保存的主节点运行Id,如果没有则默认值为,参数offset是当前从节点保存的复制偏移量,如果是第一次参与复制则默认值为-1
- 2、主节点根据psync参数和自身数据情况决定响应结果
- +FULLRESYNC,那么从节点将粗发全量复制流程
- +CONTINUE,触发部分复制流程
- +ERR,说明错误
- 利用改命令完成部分复制与全量复制
3.3、全量复制
3.3.1、流程
- 主节点执行bgsave保存RDB文件到本地
- 发送RDB文件给从节点
- 发送过程中,接受的命令写到复制客户端缓冲区(不同于积压缓冲区),当复制结束时,再一并发给从节点
- 从节点接受数据,并清空自身数据
- 从节点加载RDB文件
- 从节点触发一次AOF
3.3.2、时间消耗
- 主节点bgsave
- RDB文件网络传输时间
- 从节点清空数据
- 从节点加载RDB
- 可能的AOF重写时间
3.4、部分复制
你可以理解为是一个实时同步的过程咯
- 主节点写入命令到复制积压缓冲区
- 从节点发送偏移量给主节点
- 主节点根据偏移量在缓冲区中找到数据给从节点
3.5、心跳的维持
主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令
- 主从节点彼此都有心跳检测机制,各自模拟成对方的客户端
- 主节点默认每隔10s对从节点发送ping命令,判断从节点的存活性和连接状态
- 从节点在主线程中每隔1s发送replconf ack {offset } 命令,给主节点上报自身当前的复制偏移
- 实时监测主从节点网络状态
- 上报自身复制偏移量
- 实现保证从节点数量和延迟性功能
3.6、异步复制
主节点不但负责数据读写,还负责把写命令同步给从节点。写命令的发送过程是异步完成,也就是说主节点自身处理完命令后直接返回给客户端,并不等待从节点复制完成
- 主节点 6379 接受处理命令
- 命令处理完成后返回响应结果
- 对于修改命令异步发送给6380从节点,从节点在主线程中执行复制命令
4、开发运维中的问题
4.1、读写分离
- 数据延迟
- 监控
- 读到过期数据
- 主动检测
- 从节点故障
- 切节点
4.2、主从配置不一致
建议全量复制主从配置
4.3、全量复制
- 规避单节点复制风暴
- 规避Redis id的改变
- 规避复制积压缓冲区不足
七、阻塞
1、内在原因
1.1、API或数据结构使用不合理
发现慢查询
showlog get {n}
注意点
- 修改为低算法度的命令
- 调整大对象:缩短大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多数据
1.2、CPU饱和的问题
./redis-cli -h {ip} -p {port} --stat #统计redis使用情况
info commandstats #命令分析
1.3、持久化相关阻塞
1.3.1、fork阻塞
fork操作发生在RDB和AOF重写时
info stats|grep lastest_fork_use #查看最近一次fork耗时
1.3.2、AOF刷屏阻塞
文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作
info persistence|grep aof_deplayed_fsync #刷盘时间统计
2、外在原因
2.1、CPU竞争
- 进程竞争
- Redis被绑定CPU
2.2、内存交换
识别方法
- 查询Redis进程号
- 根据进程号查询内存交换信息
cat /proc/4476/smaps|grep Swap
预防方法
- 保证机器充足的可用内存
- 确保所有Redis实例设置最大可用内存,防止使用内存暴增
- 降低系统使用swap优先级
echo 10 > /proc/sys/vm/swagppiness
2.3、网络问题
2.3.1、链接拒绝
- 网络闪断
- 链接溢出
- 进程限制
- backlog队列溢出
2.3.2、网络延迟
./redis-cli-h {host} -p {port} --latency #持续进程延迟测试,分别统计最小值、最大值、平均值、采样次数
2.3.3、网卡软中断
八、内存
1、内存使用统计
info memory #查看内存使用情况
重点关注:used_memory_rss、used_memory以及它们的比值mem_fragmemtation_ratio
- mem_fragmentation_ration>1:说明内存碎片消耗严重
- mem_fragmentation<1:操作系统把redis内存交换到硬盘导致
2、内存消耗划分
内存消耗=自身内存+对象内存+缓冲内存+内存碎片 #自身内存消耗很小可以忽略不计
2.1、对象内存
- 对象内存占用最大,存储着用户所有的数据
- 每次创建键值对时,至少创建key对象和value对象
- 尽量减少key的长度
- 尽量优化value对象
2.2、缓冲内存
-
客户端缓冲:指的是所有接入到Redis服务器TCP连接的输入输出缓冲,这部分无法控制,最大空间为1G,如果超过将断开链接
-
普通客户端:一般普通客户端的内存消耗可以忽略不计,大量慢链接客户端接入时将会消耗大量内存
-
从客户端:主节点会为每个从节点单独建立一条链接用于命令复制
-
订阅客户端:当订阅服务器的消息生产快于消费速度时,输出缓冲区会产生积压造成输出缓冲区空间溢出
-
复制积压缓冲区:用于实现部分复制功能
-
AOF缓冲区:用于在Redis重写期间保存最近的写入命令、内存消耗小
2.3、内存碎片
正常的碎片率(mem_fragmentation_ratio)在1.03左右
某些场景会出现高内存碎片问题
- 频繁做更新操作
- 大量过期键删除
解决方案
- 数据对齐:数据尽量采用数字类型或者固定长度字符串等
- 安全重启
2.4、子进程内存消耗
子进程消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。
如果父进程有大量写命令,会加重内存拷贝量,从而造成过度内存消耗。
解决方案
- Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出
- 需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败
- 排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-on-write期间内存过度消耗
3、内存管理
3.1、设置内存上限
目的
- 用于缓存场景,当超出内存上限maxmemory时使用LRU删除策略释放空间
- 防止所有内存超过服务器物理内存
注意点
- 由于内存碎片的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出
- redis默认无限使用服务器内存,防止极端情况下导致系统内存耗尽,建议都要配置maxmemory
config set maxmemory #动态调整内存上限 可用于清理过期键
3.2、内存回收策略
3.2.1、删除过期键对象
- 惰性删除:惰性删除用于当客户端读取带有超时属性的键,会执行删除并返回空
- 定时任务删除:内存维护一个定时任务,执行过期随机删除
3.2.2、内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy限制
- noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时Redis只响应读操作
- volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略
- allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止
- allkeys-random:随机删除所有键,直到腾出足够空间为止
- volatile-random:随机删除过期键,直到腾出足够空间为止
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略
运维提醒
- 建立maxmemory>used_memory,避免频繁开销
- 通过调整maxmemory可以做一次内存回收处理
九、哨兵
1、哨兵存在的意义
因为需要主从
- 一旦主节点出现故障,需要手动将从节点升级
- 主节点的写能力受到单机的限制
- 主节点的存储能力受到单机的限制
为了高可用,需要切主的动作
2、哨兵的高可用性
当主节点出现故障时,哨兵能自动完成故障发现和故障转移
2.1、故障转移逻辑
- 主节点出现故障,此时两个从节点与主节点失去连接,主从复制失败
- 哨兵发现故障
- 哨兵集群选举领导并完成故障转移(相等于做了切主的动作)
2.2、哨兵功能点描述
- 监控
- 通知
- 故障转移
- 配置提供
3、部署
- 使用奇数个哨兵节点集群
- 不应该把哨兵部署在同一台机器
4、API
sentinel masters #展示所有被监控的主节点状态
sentinel slaves #展示从节点状态
sentinel sentinels {master name} #展示监控主节点信息
sentinel get-master-addr-by-nmae {master name} #返回主节点的地址端口
sentinel reset {pattern} #重置
sentinel failover #强制故障转移
Sentinel ckquorum #检测当前可达节点
sentinel flushconfig #强刷配置
sentinel monitor #动态配置监控主节点
5、客户端链接
5.1、Redis Sentinel客户端
Sentinel节点集合具备了监控、通知、自动故障转移、配置提供者若干功能,也就是说实际上最了解主节点信息的就是Sentinel节点集合,而各个主节点可以通过进行标识的,所以,无论是哪种编程语言的客户端,如果需要正确地连接Redis Sentinel,必须有Sentinel节点集合和masterName两个参数
5.2、实现原理
- 遍历并获取一个可用的节点
- 获取被监控的主节点信息
- 时刻监控,保持切主
6、实现原理
6.1、三个定时监控任务
-
每10s通过info向主节点和从节点获取哨兵自身拓扑
- 新节点记入可以快速感知
- 服务不可达快速感知
-
每2s向被监控节点告知自身的信息
- 用于哨兵集群之间的信息交流
-
每1s,向主节点、从节点发送一次ping做心跳检测,确认是否可达
6.2、下线定义
- 主观下线:哨兵节点ping不通时
- 客观下线:其他哨兵节点告知
6.3、领导选举
-
每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令,求将自己设置为领导者
-
收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝
-
如果该Sentinel节点发现自己的票数已经大于等于max(quorum,num(sentinels)/2+1),那么它将成为领导者
-
如果此过程没有选举出领导者,将进入下一次选举
-
选举的过程非常快,基本上谁先完成客观下线,谁就是领导者
6.4、故障转移
- 在从节点列表中选出一个节点作为新的主节点
- 过滤不健康节点
- 选择复制偏移量最大的从节点
- 选择runid最小的从节点
- 执行slaveof no one
- 让其他从节点去复制新主节点
- 当旧主节点恢复时,自动去复制新主节点
7、开发与运维中的细节
7.1、在日志中可体现工作细节
7.2、节点运维
sentinel failover #节点下线
slaveof #从节点上线
十、集群
集群:这里的意思指的是分布式数据库,当数据存储时,随机分布存储到各个数据库节点
1、数据分布相关知识
1.1、数据分布理论
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点点,每个节点负责整体数据的一个子集
1.2、常见的哈希分区规则
- 节点取余分区:根据key的hash值进行取余,然后存储在对应的节点上
- 算法简单
- 扩容或收缩时,会导致数据的迁移
- 一致性哈希分区:先根据key计算hash值,然后顺时找到第一个大于等于该哈希值的token节点,做成一个环结构
- 虚拟槽分区:槽是集群内数据管理和迁移的基本单位。把数据映射到一个固定的范围的整数集合(槽)中
1.3、Redis数据分区
Redis集群采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个槽中。每一个节点负责维护一部分槽以及槽所映射的键值数据
特点
- 优化了结构数据与节点之间的关系,简化了节点扩容和收缩难度
- 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
- 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景
- 数据分区是分布式存储的核心,理解和灵活运用数据分区规则对于掌握Redis cluster非常有帮助
1.4、集群功能限制
- 批量支持有限
- 事务操作支持有限
- 最小粒度为key
- 不支持多数据库空间
- 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构
2、集群搭建
- 准备节点
- 节点握手
- 分配槽
3、节点内部通信方法
通信流程
- 集群中每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000
- 每个节点在固定周期内通过特定规则选择几个节点发送ping消息
- 接收ping消息的节点用pong消息作为响应
Gossip消息协议:主要职责就是信息交换。信息交换的载体就是节点彼此发送Gossip消息,了解这些消息有助于我们理解集群如何完成信息交换
4、集群伸缩
伸缩原理:集群伸缩=槽和数据在节点之间的移动
扩容集群
- 准备新节点
- 加入集群
- 迁移槽和数据
收缩集群
- 下线迁移槽
- 忘记节点
5、请求路由
当发起一个key获取操作时,客户端慧自动完成请求重定向操作
6、故障转移
故障发现
- 主观下线:集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态
- 客观下线:当半数以上持有槽的主节点都标记某个节点是主观下线时
故障恢复流程:
- 领导资格检查
- 准备选举时间
- 发起选举
- 选举投票
- 替换主节点
7、集群运维
7.1、集群自身会保证其完整性:为了保证集群完整性,默认情况下当集群16438个槽任何一个没有指派到节点时整个集群不可用
7.2、带宽消耗
-
主要消耗
- 消息发送
- 消息的数据量
- 节点部署的机器规模越均匀,整体的可用带宽越高
-
合理规划
- 在满足业务需要的情况下尽量避免大集群
- 适度提高cluster-node-timeout降低消息发送频率,同时cluster-node-timeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平衡
- 如果条件允许集群尽量均匀部署在更多机器上
-
规避广播
- 在集群模式下内部实现对所有的publish命令都会向所有的节点进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重带宽负担
-
集群倾斜
- 数据倾斜
- 节点和槽分配不均
- 不同槽对应键数量差异过大
- 集合对象包含大量元素
- 内存相关配置不一致
- 请求倾斜
- 合理设计键
- 不要使用热点键作为hash_tag
- 对于一致性要求不高的场景,客户端可使用本地缓存减少热键调用
- 数据倾斜
-
集群读写分离
- 从节点只读链接
- 通过从节点做读写分离
8、手动故障迁移
Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为它的从节点
9、数据迁移
指的使应用Redis集群时,常需要把单机Redis数据迁移到集群环境
注意点
- 迁移只能从单机节点向集群环境导入数据
- 不支持在线迁移,迁移数据时,应用方法必须停写,无法平滑迁移数据
- 迁移过程中途如果出现超时等错误,不支持断点续传,只能重新全量导入
- 使用单线程进行数据迁移,大数据迁移速度过慢
十一、缓存设计
Redis说白了就是一个缓存服务,所以对于缓存相关问题凸显的相当重要,要清晰的认识到缓存,储备缓存的相关知识,才能用好Redis这九把刀。
1、缓存的收益与成本
- 收益
- 加速读写
- 降低下游负载
2、缓存更新策略
- LRU/LFU/FIFO剔除算法
- 超时剔除
- 超时更新
- 最佳实践
- 低一致性业务建议配置最大内存和淘汰策略的方式使用
- 高一致业务可以结合使用超时剔除和主动更新
3、缓存粒度控制
- 从通用角度分析
- 从空间占用角度分析
- 从代码维护角度分析
4、穿透问题
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中。穿透场景如下
- 缓存层不命中
- 存储层不命中,并不将空结果写回缓存
- 返回空结果
会出现穿透的原因
- 自身业务代码或者数据出现问题
- 恶意攻击
解决方案
- 缓冲空对象
- 内存中会多了许多值为空的键
- 存在不同步的情况
- 在缓存层的上一层使用布隆过滤拦截器拦截一波
5、无底洞问题
无底洞指的是更多的节点不代表更高的性能,所谓的无底洞就是说投入越多不一定产出越多。但是分布式又是不可避免的。
5.1、原因是什么?
通常来说添加节点使得Memcache集群性能应该更强了,但事实并非如此。键值数据库由于通常采用哈希函数将key映射到各个节点上,造成key的分布与业务无关,但是由于数据量和访问
量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的节点上,所以无论是Memcache还是Redis的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间
5.2、解决方案
- 使用微服务架构,尽量控制Redis集群的大小
6、雪崩问题
缓存雪崩是因为缓存因为某些原因不能提供服务,于是所有的请求都会到达存储层,存储层压力爆增。
解决方案
- 保证缓存层服务高可用性
- 依赖隔离组件为后端限流并降级
- 提前演练做好故障保护
7、热点key问题
热点Key存在的问题
- 并发量大
- 重建缓存不能在短时间内解决
解决方案
- 当构建热点Key时,使用互斥锁
- 将热点Key设置永不过期,再由单一线程去做更新动作
十二、运维Redis相关知识
1、Linux配置优化
1.1、内存分配控制
vm.overcommit_memory=1 #linux系统配置
cat /proc/sys/vm/overcommit_memory #查看当前配置
echo "vm.overcommit_memory=1" >> /etc/sysctl.conf #设置
建议设置为1,是为了让fork操作能够在低内存下也执行成功
合理分配内存才是优化内存的关键
- 保证机器有20%~30%的闲置内存
- 集中化管理AOF重写和RDB的bgsave
- 设置vm.overcommit_memory
1.2、swappiness
swap对操作系统来说比较重要,当物理内存不够时,可以将一部分内存页进行swap操作,已解燃眉之急。
但。
swap空间由硬盘提供,将会很慢。
在Linux中, 并不是要等到所有物理内存都使用完才使用swap,系统参数swappiness会决定操作系统使用swap的倾向程度。取值范围为0到100,值越大,说明使用swap的概率越大。
echo {bestvalue} > /proc/sys/vm/swappiness
echo vm.swappiness={bestvalue} >> /etc/sysctl.conf
#设置方法
free #查看总体内存使用情况
/proc/{pid}/smaps #查看某条进程使用情况
1.3、OOM killer
OOM killer 进程会为每个用户进程设置一个权值,这个权值越高,被杀的概率越高
cat /proc/{pid}/oom_score #查看
echo -17 > /proc/${process_id}/oom_adj #配置
运维提示
- 合理规划内存最重要
- 高可用情况,被杀掉比僵死好
1.4、ulimit
限制进程打开最大文件数
ulimit -Sn {max-open-files} #设置
2、flushall/flushdb误操作的快速恢复
- 借助AOF机制恢复
- 调大重写参数
- auto-aof-rewrite-percentage
- auto-aof-rewrite-min-size
- 移除文件中的flushall语句
- 调大重写参数
- RDB
- 防止手动执行save、bgsave
3、安全保护Redis方案
- 使用Redis密码机制
- 重命名危险命令
- 使用防火墙
- 网卡绑定
bind 0.0.0.0
- 定期备份数据
- 不使用默认端口
- 使用非root用户启动
4、处理BigKey的方案与最佳实践
- BigKey的危害
- 内存空间不均匀
- 超时阻塞
- 网络阻塞
- 发现
- 被动收集:在发生错误的时候,记录具体的Key
- 主动检测:在从节点上扫描键并判断大小
如果感觉这对您有帮助的话!您的支持是我最大的动力