redis 知识杂谈
redis有哪些好处?
数据类型丰富,提供了string,list,hash,set,zset五种基础类型,还提供了stream,geo,bitmap扩展类型
string
数据结构:简单动态字符串
list
数据结构:双向链表,压缩列表
hash
压缩列表,哈希表
zset
压缩列表,跳表
set
哈希表,整数数组数据可以设置过期时间
支持事务
支持脚本化运行
哈希冲突解决
少量的冲突redis使用链式哈希解决,当链表过长时redis会触发rehash机制,rehash机制会增加现有hash桶的数量,分散entry元素
rehash机制
redis有两张全局hash表,hash1,hash2,起初hash2没有分配空间
当触发rehash时:
会给hash2分配更大的空间,重设hash函数,增加hash桶总量,把hash1重新映射到hash2,释放hash1渐进式rehash机制
由于rehash机制非常耗时,会阻塞redis
所以将集中迁移改为分散迁移,当处理一个请求时,就从hash1中的第一个索引位置开始,把位置1的所以entries重新映射到hash2中。
单线程的redis为什么这么快?
- 首先redis并不是真正意义上的单线程,比如持久化,异步删除,集群同步,都是用额外的线程完成的
- 为什么不用多线程,多线程访问共享资源,需要增加额度的机制,就会带来额外的开销
- redis大部分操作都是在内存中完成,加上采用的高效的数据结构,例如hash表,跳表
- redis采用多路复用机制,使其能够并发处理大量客户端请求
数据同步:主从库如何实现数据同步?
redis具有高可用,是什么意思?
二层意思,一是数据尽量少丢失,二是服务尽量少中断,AOF和RDB保证了一,对于二redis通过增加冗余副本量。
redis提供了主从库模式,以保证数据副本的一致,主从库采用了
读写分离的方式。
- 读操作: 主库,从库都可以接送。
- 写操作: 只有主库能接收执行,然后主库同步给从库执行。
第一次如何同步?
从库和主库建立连接后,会与主库协商第一次全量同步,主库会进行RDB内存快照和传输RDB内存快照。
多个副本怎么分担主库全量复制压力?
采用主-从-从模式
全量复制完了之后如何同步?
基于长链接的命令传播
主从库网络断开怎么办?
主库会维护一个repl_backlog_buffer环状缓冲区,主库会记录自己写到的位置,从库会记录自己读到的位置,
当网络重连后主库会把master_rpl_offset 到 slave_repl_offset之间的命令同步到从库,
由于是环状的缓存区,当从库的同步速度小于主库的写入,当主库赶上从库就会需要重新进行全量复制。
常规的避免办法是增加repl_backlog_buffer缓存区容量。设置repl_backlog_size。
哨兵机制,主库挂了,如何不间断服务?
哨兵三大功能 监控,选主,通知
- 监控主库运行状态,并判断主库是否客观下线。
- 主库客观下线,选择新主库。
- 选出新主库,通知从库和客户端。
细节要点
- 哨兵的本质是一个redis实例。
- 哨兵通过心跳检测,监控主库状态,主库下线分为客观下线和主观下线。
- 哨兵监控是可能误判的,所以一般要集群部署,减少误判率。
- 选定主库先筛选打分,得分高的会被选为新主库。
- 筛选规则: 从库的网络状况,之前与主库的连接状况,筛选中断标准可以配置。
- 打分规则: 从库的优先级,数据同步状况,id号大小。
哨兵集群,哨兵挂了,主库还能切换吗?
哨兵挂的数量少于quorum就可以,至少有2个哨兵才能执行主从切换。
- 基于pub/sub机制的哨兵集群组成过程。
- 基于INFO命令的从库列表,帮助哨兵与从库建立连接。
- 基于哨兵自身的pub/sub功能,实现客户端和哨兵之间的事件通知。
- 判断主库客观下线需要投票,需要同意票数大于querum数量。
- 通过选举投票方式选出哨兵leader,执行主库切换通知。
- 要保证所以哨兵实例的配置是一致的,尤其是主观下线的判断值down-after-millseconds。
频道事件:
- +sdown: 主观下线
- -sdown: 退出主观下线
- +odown: 客观下线
- -odown: 退出客观下线
- +slave-reconf-sent: 哨兵发送slaveof命令重新配置从库
- +slave-reconf-inprog: 从库配置成新主库 同步中
- +slave-reconf-done: 从库配置成新主库 完成同步
- +switch-mater: 主库地址发生变化
数据量增多了,是该加内存还是该加实例?
当数据量增大时,通常会有两种选择
- 纵向扩展:加内存
- 横向扩展:加实例
优缺点
- 纵向扩展:
优点: 实施简单,直接
缺点: RDB时间长,硬件以及成本有上限 - 横向扩展:
优点: 没有硬件和成本限制
缺点: 增加系统了复杂度
切片集群数据该存哪儿?
redis cluster官方解决方案采用哈希槽,hash-slot,一个切片集群共有16384个哈希槽。
通过CRC16算法计算出一个16bit的值,然后取模16384得到对应哈希槽。
redis cluster根据实例数量平均分配哈希槽,也可以通过cluster meet,cluster addslots手动配置搭建。
手动配置需要把16384个槽分配完,否则无法工作。
客户端如何定位数据?
切片集群会共享各自的哈希槽信息,客户端与实例建立连接时,会把哈希槽的信息发送给客户端。
但是在集群新增或者减少实例的时候,redis需要重新分配哈希槽。 还有为了负载均衡,redis需要把哈希槽所有实例重新分配一遍,这就可能带来客户端哈希槽信息与服务器不一致的问题。
这时redis cluster 方案提供了一种重定向机制,客户端给一个实例发送读写操作时,数据槽不在这个实例,需要重定向发送到另一个实例。
重定向地址如何知晓,请求的第一个redis实例会返回错误,加上重定向的实例连接信息。(error) MOVED slotID ip:port
ask重定向?
当哈希槽正在迁移,命令落中正在迁移的槽时,会回复 ASK slotID ip:port
aof日志,宕机了,如何避免数据丢失?
- aof通过逐一记录命令,恢复时逐一执行命令来保证数据的可靠性。
- aof提供了三种写回策略,always,everysec,no。
- aof重写机制,避免日志文件过大,这个过程是fork子进程拷贝父进程内存数据,直接根据数据库里数据的最新状态,生成这些数据的插入命令。
- aof在恢复时,需要重放aof日志命令,这个过程会比较慢,RDB快照恢复会比aof快。
RDB内存快照,宕机了,redis如何快速恢复?
redis提供save,和bgsave两个命令来生成RDB快照
save在主线程中执行,会导致阻塞
bgsave创建一个子进程,避免了主线程的阻塞
快照时数据还能修改吗?
快照时,子进程和主进程访问的同一块内存,正常情况下,快照完成前,只能读不能写,但是redis借助了操作系统的写时复制技术,在快照期间,可以执行写命令。
快照可以每秒执行一次吗?
虽然执行快照不会影响主线程,但是会给磁盘带来很大压力,而且快照可能一秒内完成不了。redis上一个bgsave在运行,不能启动第二个bgsave。
有什么办法既可以利用RDB的快速恢复又可以做到尽量少丢数据呢?
混合使用AOF日志和RDB快照,这样AOF日志只有记录两次快照之间的命令了
string类型为什么不好用了?
string类型在保存本身占用的内存空间不大时,string类型的元数据开销就会占主导地位,这里面包括redis_object,SDS结构,dict_entry结构的内存开销。
那内存占用是怎么多出来的呢?
redis使用一个全局哈希表保存所以键值对,每一项都是dict_entry结构体,用来指向一个键值对,分别指向key,value,next,三个指针一共24字节,由于jemalloc的内存分配机制,会实际分配最接近2的幂次数,所以24字节,实际分配32字节。
然后还有分配一个key和value的redis_object对象,一个redis_object对象至少占用16字节,2个就是32,所以使用一个string类型保存数据至少要64字节。
动态字符串的编码方式有3种:
- int编码: ptr指针位直接用于保存整型
- embstr编码: 当string内容小于等于44字节时候,redis_object和sds数据使用一块连续的内存,也就是整个数据块小于等于64字节。
- row编码: redis_object和SDS内存地址可能不连续。
那用什么数据可以节省内存?
redis的hash,list,zset,set在元素小于配置限制内时会使用intset整数数组,ziplist压缩列表来存储数据。
- hash: zliblist,hash
- list: zliblist,双向链表
- zset: zliblist,跳表
- set: intset,hash
压缩列表的构成
表头有zlibytes,zltail,zllen,分别表示列表长度,列表尾的偏移量,以及列表的entry个数,压缩列表之所以可以节省内存,是因为它是用一系列连续的entry保存数据。
entry结构
- prev_len: 表示前一个entry的长度,1或5字节
- len: 表示自身长度,4字节
- encoding: 1字节
- content: 保存实际数据
如果实际数据是int类型8字节,总共是1+4+1+8=14字节往上适配是16字节。
这样新增一个数据只需要16字节,少了dict_entry和2个redis_object的创建,比使用string类型少了48字节。
inset结构
uint32_t encoding;
uint_32_t length;
int8_t contents[];
- inset在元素是整型结果的时候使用。
- inset会根据encoding编码调整contents类型,当插入的元素字节长度大于之前所以元素的长度时会进行升级,调整contents的元素内存占用宽度,inset不会进行降级。
- inset插入删除时间复杂度为o(n),查找使用2分查找时间复杂度为o(log n),查询长度为o(1)。
相关配置:
list: list-max-ziplist-size -2表示8kb,最多8kb
set: set-max-intset-entries 表示512个元素,最多512元素
zset: zset-max-ziplist-size,zset-max-ziplist-value
hash: hash-max-ziplist-size,hash-max-ziplist-value
zset和hash, size和value 分别表示最多size个元素,值字节长度最大多少。
所以结构升级后不可降级。
GEO是什么?
扩展类型之一,广泛用于LBS服务中,可以记录经纬度的地理位置信息,GEO是基于zset实现的。
实现原理
geohash编码,基本原理是二分区间,区间编码
假如有一组经纬度为,经度100,纬度30。
经度范围是[180,-180],纬度范围是[90,-90]。
geohash会分别把经度纬度编码成N位的二进制。
假如我们只编码两位经度为100。
设范围[0,-180]为0,范围[0,180]为1
此时编码为:1
然后再设[0,90]为0,[90,180]为1
此时编码为:11
根据此编码规则我们得知纬度30的编码为:10
然后geohash会以经度为奇纬度为偶的方式逐位交叉编码,得到1110,
然后把1110当做score存入zset。
不过,有点编码值相近实际距离却很远,为了避免查询不准确的问题,我们可以同时查询给定经纬度的4个或8个方格。
操作命令
- geoadd key 经度,纬度,members
- georadius key 经度,纬度,范围 其他参数
消息队列
三大需求:
消息保序
重复消息处理
消息可靠性保证
list:
优点:简单易懂
缺点:消息保序和消息可靠性需要自己实现策略保证
stream:
优点:自动生成id,提供消费组形势读取数据,满足消息队列三大需求
如何在redis中保存时间序列数据?
使用场景,比如服务器每一秒的在线人数,各个游戏的在玩人数。
查询需求
- 1点钟的在线人数。 get单点查询。
- 1点到2点的最大在线人数,范围查询加sort排序。
- 1点到2点平均在线人数,聚合计算。
4 .1点到2点玩A游戏的人数比玩b游戏的人数的时间段多的百分比。
二种方案
- 使用hash+zset,可以满足1,2,3,4,但是3,4需要客户端请求拿到所以数据再聚合计算,数据传输开销大。
- 使用扩展类型RedisTimeSeries,专门为存取时间序列数据而设计的,可以满足2,3,避免了大量数据传输,不过底层用链表实现,范围查询的复杂度为o(n)。
方案优缺点
- 使用hash+zset
优点: 支持单点查询,范围查询的高效支持。
缺点: 聚合计算需要拿出来处理,网络数据传输量大。 - 使用扩展类型RedisTimeSeries
优点: 内部聚合计算,占用内存较低。
缺点: 仅仅支持最新数据的单点查询,范围查询时间复杂度为o(n)。
个人思考
如果有全量的聚合计算且数据量较大优先使用redisTimeseries,因为全量的化,用hash,zset的时间复杂度也是o(n)
有一亿个keys要统计,应该用哪种集合?
- bitmap适合于二值状态统计,比如签到,登录统计,对比set,hash,zset,做聚合统计的效率会快,bitmap是o(1)是时间复杂度,其他类型至少都要o(N)。
- hyperloglog适用于那种不需要非常精确的统计,比如网站的访问量,优势是占用内存低,一个key固定只需要12k,精准率81%,统计成员总数2的64次方。
redis单线程处理io瓶颈主要包括2个方面?
任意一个请求在server一旦发送耗时,都会影响整个server的性能,也就是说后面的请求都要等这个耗时请求处理完成,自己才能处理到
a、操作bigkey
b、使用复杂度过高的命令,当N基数很大时,非常耗时
c、大量key过期
d、淘汰策略,当内存达到设置上限后
e、AOF写盘开始always策略,写盘速度比写内存速度低太多
f、主从全量同步,fork生成快照完成之前,redis一直不可写并发量非常大时,单线程读写客户端io数据存在性能瓶颈,虽然有多路复用,但是读写客户端数据依旧是同步io,只能单线程依次读取写入,无法利用多核。
解决办法
针对问题1 人为规避,redis4.0推出了lazy-free异步释放内存
针对问题2 redis6.0推出多线程,并发场景下可以利用多核处理客户端io读写
异步机制:如何避免单线程模型的阻塞?
redis实例运行时的四大类交互对象客户端,磁盘,主从库实例,切片集群实例
基于四大类交互对象,梳理了会导致redis性能受损的五大阻塞点:
- 集合全量查询
- 聚合操作
- bigkey删除
- 清空数据库
- AOF日志同步写
- 从库加载RDB文件
阻塞性能受损原因:
- 集合全量查询:操作时间复杂度为o(n)
- 聚合操作:操作时间复杂度>=o(n)
- bigkey删除:虽然删除key看起来的只是简单的释放内存,不过操作还需要把内存插入到空闲列表进行管理和再分配
- 清空数据库:跟bigkey删除性质一样
- AOF日志同步写:磁盘读写速度有目共睹
- 从库加载RDB文件:涉及到读文件,而且数据量大
可以异步优化的:
- bigkey删除,清空数据库:开启lazyfree-lazy相关配置
- AOF日志同步写:apendfsync配置项由always改为everysec或者no
不能异步优化的:
- 集合全量查询,聚合操作,从库加载RDB文件
可以异步的是不在关键路径上的操作,比如内存释放啊,写盘等等
不能异步的怎么优化:
- 集合全量查询,聚合操作:客户端分批读取数据,再聚合计算
- 从库加载RDB文件:控制主库的数据总量
相关配置:
lazyfree-lazy
lazy-free-lazy-expire:key在过期时异步释放内存
lazy-free-lazy-eviction:内存达到maxmemory并设置了淘汰策略是尝试异步释放内存
lazy-free-lazy-server-del:执行rename/move等命令或需要覆盖一个key时,删除旧key尝试异步释放内存
replica-lazy-flush:主从全量同步时,从库清空数据库时异步释放内存
相关命令:
unlink 删除key 异步释放内存
flushall aysnc 异步清除
如何应对redis变慢?
怎么判断redis变慢?
查看redis运行时响应延迟: redis-cli –latency -h host -p port
如果延迟达到一秒或一秒以上,基本可以认定redis变慢了。当前环境的基线性能:redis-cli –intrinsic-latency 120
如果运行时响应延迟是基线性能的2倍以上,就可以认定redis变慢了。
如何解决redis变慢?
redis自身的操作特性,操作系统,文件系统,它们是影响redis性能的三大要素。
一:自身的操作特性
慢查询命令
原因: sunion,sort,smembers操作复杂度分别是O(N + M*log(M))和O(N),时间复杂度过高。
定位方式: redis日志或者latency monitor工具。
解决办法: 用其他命令代替,比如sscan,排序,交集,并集可以放在客户端做。KEYS命令
原因: 遍历所以keys,比如redis有1百万个keys,就会遍历1百万次,时间复杂度为O(N。
解决办法: 不用,或者直接禁用。过期key操作
删除机制:- ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20默认配置,100毫秒删除20个过期key,1秒删除200个,
- 如果超过25%的key过期了,则重复删除的过程,直到过期key的比例降至25%以下。
原因: 过期key的删除是主线程在执行,触发机制2可能存在影响。
解决办法: 避免大量key同时设置相同的过期时间,比如在固定过期时间后面加一个小范围的随机时间。
二:文件系统:AOF
always同步写盘,每次都会等待写完磁盘。
everysec,每秒fork一个子线程来完成写磁盘,不过上一次fork的写盘任务没有完成,那么就会阻塞,特别是aof重写的时候,磁盘io压力较大,可能会造成阻塞。
相关配置:
appendfsync
no-appendfsync-on-rewrite
三:操作系统:swap
当系统内存不够的时候,系统会用磁盘模拟内存,window叫虚拟内存,可以想象磁盘当内存用,当然性能会下降。
查看方式:cd /proc/进程号
cat smaps | egrep ‘^(Swap|Size)’
四:内存大页
内存大页机制(Transparent Huge Page THP)
linux内核从2.6.38开始支持大页机制,该机制支持2M大小的内存页分配,常规的只有4kb的粒度来执行的。
RDB内存快照写时复制机制,当内存块数据需要修改时,会将这些数据拷贝一份,然后进行修改,这时候大页机制可能会影响到性能了。
如何关闭:
cat /sys/kernel/mm/transparent_hugepage/enabled
如果结果是always表示是开启的,如果是never表示禁止了。
生产中不建议对Redis的实例运行的机器开启大页
关闭命令:
echo never /sys/kernel/mm/transparent_hugepage/enabled
删除数据后,为什么内存占用率还是很高?
明明删除了数据,但是redis占用内存没有降下来?
这是应该redis释放的内存空间被内存分配管理器管理,并不会立即返回给操作系统。
风险点:删除数据,如果删除的是连续空间,那么这个连续空间可以继续用来存储数据;如果不是连续空间,仍然是属于操作系统分配给redis的物理内存,但无法用来存储数据。
内存碎片是什么?
应用申请的内存是连续的一块N大小的内存空间,不满足N大小的内存块就是内存碎片,无法利用。
内存碎片的形成原因
- 内因: 内存分配器分配机制。
- 外因: 键值对大小不一样和删除操作。
如何判断是否有内存碎片?
命令: INFO memory
mem_fragmentation_ratio 表示内存碎片率,rss/use_memory
use_memory_rss 实际分配内存
use_memory 实际使用内存
合理范围
ratio在1到1.5之间
如何清理内存碎片?
重启redis
redis自动清理机制:搬家让位,合并空间
不过碎片清理是有代价的
相关配置:
activedefrag 开关
activedefrag-ignore-bytes 100mb 表示内存碎片达到100mb,开始清理
activedefrag-threshold-lower 10 表示内存碎片空间占redis总分配空间的10%,开始清理
activedefrag-cycle-min 25 表示自动清理过程所用cpu时间比例不低于25%
activedefrag-cycle-max 75 表示自动清理过程所用cpu时间比例不高于75%
总结
最后,我再给你提供一个小贴士:内存碎片自动清理涉及内存拷贝,这对 Redis 而言,是个潜在的风险。如果你在实践过程中遇到 Redis 性能变慢,记得通过日志看下是否正在进行碎片清理。如果 Redis 的确正在清理碎片,那么,我建议你调小 active-defrag-cycle-max 的值,以减轻对正常请求处理的影响。
缓冲区,一个可以引发惨案的地方
缓冲区主要有两个应用场景:
- 在客户端与服务端进行通信时,用来存在客户端和命令,和服务端返回给客户端的数据。
- 在主从同步进行时,用来存放主节点接收的写命令和数据。
风险:
由于缓冲区写入速度大于读出速度,引发缓冲区溢出,造成数据丢失。
缓冲区过大,耗尽机器内存,导致redis实例崩溃。
客户端输入,输出缓冲区
如何应对输入缓存区溢出:
- 如何检测: client list命令查看,qbuf(已使用大小),qbuf-free(未使用大小),通常qbuf很大,qbuf-free很小,就要注意了。
- 应对方法:
1. 调大缓存区,没有配置,代码写死1G。
2. 从数据命令的发送和处理入手,避免写入bigkey,避免redis主线程阻塞。
如何应对输出缓存区溢出:
常见溢出场景:
返回bigkey的执行结果。
monitor命令。
缓冲区大小设置不合理。
如何设置输出缓存区大小:
output-buffer-limit配置
普通客户端:output-buffer-limit normal 0 0 0 –0表示不做限制
订阅客户端:output-buffer-limit pubsub 8mb 2mb 60 –8mb表示总量限制,2mb和60表示,60秒内不超过2mb的缓冲器占用,超过就是与该客户端断开连接。
redis为什么适合做缓存
在分层系统中,数据暂存与快速子系统有助于加速访问,缓存容量有限,缓存写满时需要淘汰机制,而redis天然满足这两个特性,所以非常适合做缓存。
redis做缓存时通常有两种模式:
- 只读缓存:
数据有修改会直接改数据库,改完删除缓存,下次有读取请求会先从数据库读出数据,然后写入redis缓存,之后的读取命令都会命中缓存。
好处,可以保证数据库的数据是最新的,适用与读多写少或者对数据安全性要求较高的业务。 - 读写缓存:
数据有修改先改缓存,再改数据库,有两种策略,同步只写,和异步写回。
同步直写:
优点:可以保证数据安全性。
缺点:降低性能。
异步写回:
优点:提高业务响应速度。
缺点:有数据丢失风险。
缓存满了,怎么办
缓存应该设置多大,8,2定律,也就是将缓存区的容量设置为总数据量的20%,能拦截80%的访问量。
redis的缓存淘汰策略:
redis总共有8种缓存淘汰策略:
按机制可以分为5大类:
* 不淘汰机制: noeviction
随机淘汰机制: allkeys-random,volatile-random
最快过期机制: volatile-ttl
最冷淘汰机制: allkeys-lru,volatile-lru
最少访问淘汰机制: allkeys-lfu,volatile-lfu
常见缓存异常场景
常见4个异常场景:数据不一致,缓存雪崩,缓存穿透,缓存击穿
数据缓存:读写缓存,只读缓存
读写缓存:
同步策略: 异步写库,同步直写
优点: 异步写库,可以提升系统的吞吐量,同步直写,可以保证数据一致性。
缺点: 异步写库,可能出现数据丢失,同步直写,会降低系统吞吐量。只读缓存:
策略顺序: 先删库,再删缓存。 先删缓存后删库。
问题: 脏数据。
缓存雪崩
原因: 大量热点key同时过期,导致大量请求落到数据库。
解决办法:
- 在设置过期时间时增加随机过期时间。
- 服务降级。
缓存击穿
原因: 某个热点数据,无法在缓存中处理,导致压力全部落到数据库。
解决办法: 热点数据不设置过期时间。
缓存穿透
原因: 数据不存在。
解决办法:
- 布隆过滤器。
- 设置代表空值的缓存信息。
事务功能,ADIC能保证吗?
原子性(Atomicity)
概念:一系列操作要不都成功,要么都失败。
redis并没有提供回滚操作,不能保证原子性。一致性(Consistency)
概念:数据库中的数据在执行前后是一致的。
可以保证,错误的命令不会执行。隔离性(lsolation)
概念:事务执行期间,其他操作无法取到执行事务访问的数据。
可以保证,入队有watch命令监控保证,执行由单线程天然保证。持久性(Durability)
持久化数据。
不能保证,不管是开启aof还是RDB,都存在数据丢失的可能。