基本数据类型
- string
- hash
- list
- set
- zset
数据迁移方法
- move(基本废除)
- dump+restore(非原子性)
- migrate (原子性)
键的遍历
- keys(全量)
- scan(渐进式,解决keys可能带来的阻塞问题)
redis高性能的三个因素
- 纯内存存储,IO多路复用,单线程
慢查询
slowlog-log-slower-than 和slowlog-max-len
慢查询不包含命令网络和排队时间
日志列表是一个先进先出的队列,日志比较多的情况下,可能会丢失部分慢查询命令,一般需要做日志持久化存储。
pipeline
- 可以有效减少RTT次数,但是每次Pipeline的命令数量不能无节制
redis-lua脚本
- 可以创建出原子,高效,自定义命令组合
- redis执行lua脚本两种方法: eval和evalsha
节省内存
bitmaps (位数组)可以用来做独立用户统计,有效节省内存;如果存在大的偏移量,由于申请大内存会导致阻塞
hyperLogLog (伯努利实验,统计总数,存在误差)
发布订阅
- publish,subscribe(用于聊天室,系统解耦,公告消息)
- 相比专业消息队列系统功能较弱,不过足够简单。
GEO
- 底层实现用zset
redis客户端总结
客户端api
-
客户端类型(11种,常用下面三种)
- normal 普通客户端(flag=N)
- slave 用于复制(flag=S)
- pubsub 发布订阅(flag=O)
client list 列出与redis服务器相连接的所有客户端信息
-
info clients 列出输入和输入缓冲区的对象数
qbuf,qbuf-free 客户端缓冲区,作用将客户端发送的命令临时保存,同时redis从输入的缓冲区拉取命令并执行
qbuf缓冲的总容量,qbuf-free缓冲剩余容量
输入缓冲区过大的原因
- 是因为redis的处理速度跟不上输入缓冲区的速度
- redis发生了阻塞,短期里不能处理命令,造成客户端输入的命令积压在缓冲区
- 合理防范,通过定期执行client list命令,收集qbuf和qbuf-free找到异常连接记录进行分析,通过info clients找到输入缓冲区的预警参数进行预警提示
- 在开发中要减少bigkey,减少redis阻塞,合理的监控报警
输出缓冲区(固定缓冲区->字节数组,动态缓冲区->列表)
obl固定缓冲区的长度
oll动态缓冲区的长度
omem代表使用的字节数
预防输出缓冲区出现异常
- 进行监控,设置阈值,超过阈值及时处理
- 限制普通客户端输出到缓冲区,把错误扼杀在摇篮
- 适当增大slave的输出缓冲区,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
- 限制容易造成输出缓冲区增大的命令
- 及时监控内存,一旦发生抖动频繁,可能就是输出缓冲区过大。
-
客户端限制
- maxclients 限制最大客户端连接数(通过config set maxclients对客户端连接数动态设置)
- timeout 限制连接的最大空闲时间,一旦客户端连接的idle(空闲时间)超过了timeout, 连接将会被关闭
-
客户端常见异常
- 无法从连接池获取连接(1>高并发下连接池供不应求, 2>连接池没有正确释放,3>客户端存在慢查询,4>服务端执行命令阻塞)
- 读写超时
- 客户端连接超时
- 客户端缓存区异常
- lua脚本正在执行
- redis加载持久化文件
- 使用内存超过maxmemory设置
- 客户端连接数过大
-
数据持久化
-
RDB
命令bgsave
执行流程
-
优点
RDB是一个紧凑的二进制压缩文件,代表Redis在某个时间点上的数据快照,非常适合备份,全量复制。比如每隔6小时执行bgsave
-
缺点
RDB无法实时持久化(秒级别),因为bgsave运行时会执行fork创建子进程,属于重量级操作,频繁执行成本过高
RDB使用特定二进制保存,redis版本演进过程中,存在老版本无法兼容新版本
-
AOF
实现数据实时持久化,以独立日志的方式记录每次写命令,在重启时重新执行AOF文件达到数据恢复的作用
-
执行流程
1)所有的写入命令会追加到AOF_buf(缓冲区)
2)AOF缓冲区根据一定策略项硬盘做同步操作
3)随着AOF文件越来越大,需要定期对AOF重写,达到压缩的目的
4)当redis服务重启时,可以加载AOF文件进行数据恢复
-
持久化方式
1)执行AOF重写请求
2)父进程执行fork创建子进程,开销等同于bgsave
3)主进程完成fork后,继续执行其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步硬盘,保证AOF机制正确;由于fork操作运用复写技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,redis使用“AOF重写缓冲区”保存这部分新数据,防止AOF文件生成期间丢失这部分数据。
4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。
-
启动加载
1)AOF持久化开启,优先加载AOF文件
2)AOF文件不存在加在RDB文件
3)加载AOF/RDB文件成功后,Redis启动成功
4)AOF/RDB文件存在错误时,Redis启动失败,打印错误日志
-
改善fork操作耗时
1)优先使用物理机或者高效支持fork操作的虚拟技术,避免使用Xen.
2)控制redis实例最大使用内存,fork耗时跟内存量成正比,线上建议10GB内
3)合理配置linux内存分配策略
4)降低fork操作频率,适度放宽AOF触发时机,避免全量复制
-
-
-
复制
参与复制的redis节点分为主节点和从节点,默认redis实例都是主节点,每个从节点只能有一个主节点。复制的流程是单向的,只能从主节点复制到从节点。
启动1>,配置文件添加slaveof {masterHost} {masterPort} 随redis启动生效;2> 客户端redis-server --slaveof {masterHost} {masterPort} 生效;3> 直接使用slaveof {masterHost} {masterPort} 生效
slave 是异步命令,执行后节点只保留主节点信息后返回,后续复制流程在节点内部异步执行。可使用info replication 命令查看复制信息
断开连接 slave of one
-
切主操作 slave {newMasterIP} {newMasterPort}
- 断开当前主节点连接复制关系
- 建立与新主节点复制关系
- 清除从节点当前所有数据
- 对新节点进行复制操作
-
复制拓扑
- 一主一从(注意主节点重启时的数据持久化)
- 一主多从,网状(实现读写分离,可以用其中一台从节点集中实现慢查询,主节点负责写入,从节点负责读取)
- 树状拓扑(防止主节点到从节点数据传输造成主节点压力过大)
-
复制过程(原理)
从节点执行salveof 命令后,就开始执行复制
- 保存主节点信息
- 从节点salveof通过内部定时任务维护复制逻辑,从节点发起scoket套接字到主节点建立网络连接
- 发送ping命令验证网络套接字是否可用
- 权限校验,如果主节点设置了requriepass认证,从节点在配置文件中设置masterauth进行校验
- 同步数据集,首次建立连接需要全量同步数据集比较耗时,在首次同步后,可进行数据部分复制。
- 命令持续复制。
全量复制
部分复制
-
主从心跳检查
主节点默认每隔十秒向从节点发送ping
-
从节点主线程每隔一秒通过 replconf ack offset 给主节点上报自身偏移量
replconf 作用, 检查网络状态, 检查偏移量从缓冲区复制增量数据
-
异步复制
异步复制指在写命令完成后并不等主节点同步复制到从节点完成,直接返回客户端写入结果
-
阻塞
-
阻塞的常见原因
-
API或数据结构使用不合理
减少慢查询,减少大对象
CPU饱和问题
持久化相关阻塞
-
-
阻塞外在原因
- CPU竞争
- 内存交换
- 网络问题
-
-
内存管理
- 内存信息查看(info memory)
- 对象内存
- 缓冲内存
- 内存碎片
-
内存调整
内存上线
动态调整内存上线
-
内存回收
- 删除过期键对象
- 惰性删除
- 定时任务删除
-
内存溢出控制策略(config set maxmemory-policy{policy}动态配置)
- noeviction: 默认策略,不删除任何数据,拒绝所有写入
- volatile-lru: 根据LRU算法设置过期键,直到腾出足够空间。如果没有课删除的键对象,回退到noeviction策略
- allkeys-lru: 根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止
- allkeys-random: 随机删除所有键,直到腾出足够空间
- volatile-random:随机删除过期键,直到腾出足够空间
- volatile-ttl:根据键值对象ttl属性,删除最近将要过期的数据,如果没有,回到noeviction策略
-
内存优化
-
redisObject对象
redis存储的所有值在内部定义为redisObject结构体,内部结构包括:
type:对象类型;encoding:内部编码类型:lru:REDIS_LRU_BITS LRU计时时钟
init refcount: 引用计算器
void *ptr 数据指针(与存储的数据内有有关,如果是整数,直接存储数据;否则表示指向数据的指针,redis3.0之后字符串长度<=39字节,内部编码embstr类型)
-
缩减键值长度减少key和value的长度
使用工具压缩json和xml内容后存入redis 例如使用 Snappy
-
共享对象池
redis在启动的时候内部维护了[0-9999]的整数对象池
-
编码优化
ziplist编码主要是为了节约内存,因此所有数据都是采用线性连续的内存结构。ziplist编码是应用广泛的一种,可以作为hash,list,zset类型的底层数据结构。
ziplist (压缩列表编码)
intset编码
-
控制键的数量
当redis存储大量数据时,通常会存储大量的键名,过多的键会消耗较多的内存,使用相同的数据结构可以减少外层键名,减少内存占用
-
hash结构降低键数量分析
根据键的规模在客户端通过分组映射到一组hash对象中,如存在100万个键,可以映射到1000个hash中,每个hash保存1000个元素
hash的field可用于记录原始key字符串,方便哈希查找
hash的value保存原始值对象,确保不要超过hash-max-ziplist-value限制
-
ziplist编码的hash和string比较
同样数据ziplist编码比string节约内存;
随着value空间的减少,节约内存越来越明显
hash-ziplist比string写入耗时,但随着value空间的减少,耗时逐渐减少
-
hash类型内存节省原理
- hash使用ziplist编码节约内存,如果使用hashtable反而会增加内存
2)ziplist长度需要控制在1000以内,否则由于存取时间复杂度在O(N)到O(N2)之间,长列表会导致CPU消耗严重,得不偿失。
3)ziplist适合存储小对象,对于大对象不但内存优化效果不明显还会增加命令耗时。
4)需要预估键的规模,从而确定每个hash结构需要存储的数量。
5)根据hash长度和元素大小,调整hash-max-ziplist-entries和hash-max-ziplist-value参数,确保hash类型使用ziplist编码
-
hash键和field键的设计
1)当键离散度较高时,可以按字符串位截取,把后三位作为哈希的field,之前那部分作为哈希的键。如:key=194888 哈希key=group : hash:194 , 哈希field=888
2)当键离散度较低时,可以使用哈希算法打散键,如使用crc32(key)&10000函数把键映射到“0-9999”整数范围,哈希field存储的原始值。
3)尽量减少hash键和field的长度。
-
hash结构控制键存在的问题
虽然hash存储可以降低内存,但是同样会带来问题:
1)客户端需要预估键的规模设计分组规则,加重客户端开发成本
2)hash重构后所有键无法使用超时和LRU淘汰机制,需要手动维护
ziplist+hash优化keys后,如果想使用超时删除功能,开发人员可以存储每个对象的写入时间,然后通过定时任务使用hscan命令扫描数据,找出hash内超时的数据删除
-
-
哨兵
-
实现原理
-
三个定时任务完成对各节点的发现和监控
1)每隔10秒,每个sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构,通过向主节点执行info命令,获取从节点的信息,这也是为什么sentinel节点不需要显示配置监控从节点;感知新节点的加入;节点不可达或者故障转移后,通过info更新拓扑信息。
2)每隔2秒,每个sentinel节点会向redis数据节点的_ sentinel _ :hello频道上发送Sentinel节点对于主节点的判断以及当前sentinel节点的信息,同时每个sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对节点判断。
3)每隔一秒,每个Sentinel节点会向主节点,从节点,其余Sentinel节点发送一条ping命令做一次心跳检查。来确认这些节点是否可达。
因此通过三个定时任务实现个sentinel节点,主节点,从节点之间的连接和监控
-
主观下线
每个Sentinel节点会每隔1秒对主节点,从节点,其他sentinel发送ping命令做心跳检测,当这些节点超过down-after-millseconds没有进行有效回复,sentinel节点就会对该节点做失败判定,这个行为为主观下线。
-
客观下线
当sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is-master-down-by-addr 命令向其他节点询问对主节点的判断,当超过< quorum >个数,Sentinel节点会认为主节点确实有问题,这时该Sentinel节点会做出客观下线决定。
-
领导者选举
raft算法实现leader选举
1)每个在线的sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他Sentinel节点发送 sentinel is-master-down-by-addr命令,要求自己成为领导者
-
-
集群
-
数据分区
- 节点取余分区
2)一致性哈希分区()
3)虚拟槽分区
-
集群功能限制
- key 批量操作支持有限。如mset,mget,目前只支持slot值的key执行批量操作,对于slot值的key用于执行mget,mset等操作存在于多个节点上不支持
2)key事务操作支持有限,同样只支持同一节点上的事务操作
3)key作为数据分区的最小粒度,因此不能将一个大的键值对象hash,list映射到不同节点。
- 不支持多数据库空间。单机下redis支持16个库,集群只支持db0数据库空间。
5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
-
搭建集群
- 准备节点
2)节点握手(Gossip流言算法)
3)分配槽(16384个槽全部分配完成,集群才可以上线)
-
节点通信
1)集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础上加10000.
2)每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
3)接收到ping消息的节点用pong消息作为响应。
-
消息种类
(消息格式:消息头和消息体)
消息头包含发送节点自身状态数据接收节点消息头就可以获取到发送的相关数据。(节点id,槽映射,节点标识)
消息体:定义发送消息的数据,使用cluster MsgDataGossip数组作为消息体数据。
meet 用于通知新节点加入
ping 用于检测和交换信息,包含了自身节点和部分其他节点状态数据
pong 用于接收到meet和ping后返回自身状态,也可以在集群里广播自身状态
fail当判断某个节点下线后,在集群里进行广播该节点状态
-
集群伸缩
扩容集群
1)准备新节点
2)加入集群
3)迁移槽和数据
加入的新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。
迁移槽和数据
槽迁移需要保证每个负责相似数量的槽,从而保证各节点的数量均匀
数据迁移过程是逐个槽进行的,每个槽数据迁移流程
1)对目标节点发送cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据
-
-