redis 知识杂谈

redis有哪些好处?

  1. 数据类型丰富,提供了string,list,hash,set,zset五种基础类型,还提供了stream,geo,bitmap扩展类型
    string
    数据结构:简单动态字符串
    list
    数据结构:双向链表,压缩列表
    hash
    压缩列表,哈希表
    zset
    压缩列表,跳表
    set
    哈希表,整数数组

  2. 数据可以设置过期时间

  3. 支持事务

  4. 支持脚本化运行

  5. 哈希冲突解决
    少量的冲突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为什么这么快?

  1. 首先redis并不是真正意义上的单线程,比如持久化,异步删除,集群同步,都是用额外的线程完成的
  2. 为什么不用多线程,多线程访问共享资源,需要增加额度的机制,就会带来额外的开销
  3. redis大部分操作都是在内存中完成,加上采用的高效的数据结构,例如hash表,跳表
  4. redis采用多路复用机制,使其能够并发处理大量客户端请求

数据同步:主从库如何实现数据同步?

redis具有高可用,是什么意思?

二层意思,一是数据尽量少丢失,二是服务尽量少中断,AOF和RDB保证了一,对于二redis通过增加冗余副本量。

redis提供了主从库模式,以保证数据副本的一致,主从库采用了

读写分离的方式。

  • 读操作: 主库,从库都可以接送。
  • 写操作: 只有主库能接收执行,然后主库同步给从库执行。

第一次如何同步?

从库和主库建立连接后,会与主库协商第一次全量同步,主库会进行RDB内存快照和传输RDB内存快照。

多个副本怎么分担主库全量复制压力?

采用主-从-从模式

全量复制完了之后如何同步?

基于长链接的命令传播

主从库网络断开怎么办?

主库会维护一个repl_backlog_buffer环状缓冲区,主库会记录自己写到的位置,从库会记录自己读到的位置,
当网络重连后主库会把master_rpl_offset 到 slave_repl_offset之间的命令同步到从库,

由于是环状的缓存区,当从库的同步速度小于主库的写入,当主库赶上从库就会需要重新进行全量复制。
常规的避免办法是增加repl_backlog_buffer缓存区容量。设置repl_backlog_size。

哨兵机制,主库挂了,如何不间断服务?

哨兵三大功能 监控,选主,通知

  1. 监控主库运行状态,并判断主库是否客观下线。
  2. 主库客观下线,选择新主库
  3. 选出新主库,通知从库和客户端。

细节要点

  1. 哨兵的本质是一个redis实例。
  2. 哨兵通过心跳检测,监控主库状态,主库下线分为客观下线和主观下线。
  3. 哨兵监控是可能误判的,所以一般要集群部署,减少误判率。
  4. 选定主库先筛选打分,得分高的会被选为新主库。
  5. 筛选规则: 从库的网络状况,之前与主库的连接状况,筛选中断标准可以配置。
  6. 打分规则: 从库的优先级,数据同步状况,id号大小。

哨兵集群,哨兵挂了,主库还能切换吗?

哨兵挂的数量少于quorum就可以,至少有2个哨兵才能执行主从切换。

  1. 基于pub/sub机制的哨兵集群组成过程。
  2. 基于INFO命令的从库列表,帮助哨兵与从库建立连接。
  3. 基于哨兵自身的pub/sub功能,实现客户端和哨兵之间的事件通知。
  4. 判断主库客观下线需要投票,需要同意票数大于querum数量。
  5. 通过选举投票方式选出哨兵leader,执行主库切换通知。
  6. 要保证所以哨兵实例的配置是一致的,尤其是主观下线的判断值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日志,宕机了,如何避免数据丢失?

  1. aof通过逐一记录命令,恢复时逐一执行命令来保证数据的可靠性。
  2. aof提供了三种写回策略,always,everysec,no
  3. aof重写机制,避免日志文件过大,这个过程是fork子进程拷贝父进程内存数据,直接根据数据库里数据的最新状态,生成这些数据的插入命令。
  4. 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[];
  1. inset在元素是整型结果的时候使用。
  2. inset会根据encoding编码调整contents类型,当插入的元素字节长度大于之前所以元素的长度时会进行升级,调整contents的元素内存占用宽度,inset不会进行降级。
  3. 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. 1点钟的在线人数。 get单点查询。
  2. 1点到2点的最大在线人数,范围查询加sort排序。
  3. 1点到2点平均在线人数,聚合计算。
    4 .1点到2点玩A游戏的人数比玩b游戏的人数的时间段多的百分比。

二种方案

  1. 使用hash+zset,可以满足1,2,3,4,但是3,4需要客户端请求拿到所以数据再聚合计算,数据传输开销大。
  2. 使用扩展类型RedisTimeSeries,专门为存取时间序列数据而设计的,可以满足2,3,避免了大量数据传输,不过底层用链表实现,范围查询的复杂度为o(n)。

方案优缺点

  1. 使用hash+zset
    优点: 支持单点查询,范围查询的高效支持。
    缺点: 聚合计算需要拿出来处理,网络数据传输量大。
  2. 使用扩展类型RedisTimeSeries
    优点: 内部聚合计算,占用内存较低。
    缺点: 仅仅支持最新数据的单点查询,范围查询时间复杂度为o(n)。

个人思考

如果有全量的聚合计算且数据量较大优先使用redisTimeseries,因为全量的化,用hash,zset的时间复杂度也是o(n)

有一亿个keys要统计,应该用哪种集合?

  1. bitmap适合于二值状态统计,比如签到,登录统计,对比set,hash,zset,做聚合统计的效率会快,bitmap是o(1)是时间复杂度,其他类型至少都要o(N)。
  2. hyperloglog适用于那种不需要非常精确的统计,比如网站的访问量,优势是占用内存低,一个key固定只需要12k,精准率81%,统计成员总数2的64次方。

redis单线程处理io瓶颈主要包括2个方面?

  1. 任意一个请求在server一旦发送耗时,都会影响整个server的性能,也就是说后面的请求都要等这个耗时请求处理完成,自己才能处理到
    a、操作bigkey
    b、使用复杂度过高的命令,当N基数很大时,非常耗时
    c、大量key过期
    d、淘汰策略,当内存达到设置上限后
    e、AOF写盘开始always策略,写盘速度比写内存速度低太多
    f、主从全量同步,fork生成快照完成之前,redis一直不可写

  2. 并发量非常大时,单线程读写客户端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变慢?

  1. 查看redis运行时响应延迟: redis-cli –latency -h host -p port
    如果延迟达到一秒或一秒以上,基本可以认定redis变慢了。

  2. 当前环境的基线性能:redis-cli –intrinsic-latency 120
    如果运行时响应延迟是基线性能的2倍以上,就可以认定redis变慢了。

如何解决redis变慢?

redis自身的操作特性,操作系统,文件系统,它们是影响redis性能的三大要素。

一:自身的操作特性

  1. 慢查询命令
    原因: sunion,sort,smembers操作复杂度分别是O(N + M*log(M))和O(N),时间复杂度过高。
    定位方式: redis日志或者latency monitor工具。
    解决办法: 用其他命令代替,比如sscan,排序,交集,并集可以放在客户端做。

  2. KEYS命令
    原因: 遍历所以keys,比如redis有1百万个keys,就会遍历1百万次,时间复杂度为O(N。
    解决办法: 不用,或者直接禁用。

  3. 过期key操作
    删除机制:

    1. ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20默认配置,100毫秒删除20个过期key,1秒删除200个,
    2. 如果超过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大小的内存块就是内存碎片,无法利用。

内存碎片的形成原因

  1. 内因: 内存分配器分配机制。
  2. 外因: 键值对大小不一样和删除操作。

如何判断是否有内存碎片?

命令: 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 的值,以减轻对正常请求处理的影响。

缓冲区,一个可以引发惨案的地方

缓冲区主要有两个应用场景:

  1. 在客户端与服务端进行通信时,用来存在客户端和命令,和服务端返回给客户端的数据。
  2. 在主从同步进行时,用来存放主节点接收的写命令和数据。

风险:

由于缓冲区写入速度大于读出速度,引发缓冲区溢出,造成数据丢失。
缓冲区过大,耗尽机器内存,导致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做缓存时通常有两种模式:

  1. 只读缓存:
    数据有修改会直接改数据库,改完删除缓存,下次有读取请求会先从数据库读出数据,然后写入redis缓存,之后的读取命令都会命中缓存。
    好处,可以保证数据库的数据是最新的,适用与读多写少或者对数据安全性要求较高的业务。
  2. 读写缓存:
    数据有修改先改缓存,再改数据库,有两种策略,同步只写,和异步写回。
    同步直写:
    优点:可以保证数据安全性。
    缺点:降低性能。
    异步写回:
    优点:提高业务响应速度。
    缺点:有数据丢失风险。

缓存满了,怎么办

缓存应该设置多大,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同时过期,导致大量请求落到数据库。
解决办法:

  1. 在设置过期时间时增加随机过期时间。
  2. 服务降级。

缓存击穿

原因: 某个热点数据,无法在缓存中处理,导致压力全部落到数据库。
解决办法: 热点数据不设置过期时间。

缓存穿透

原因: 数据不存在。
解决办法:

  1. 布隆过滤器。
  2. 设置代表空值的缓存信息。

事务功能,ADIC能保证吗?

  • 原子性(Atomicity)
    概念:一系列操作要不都成功,要么都失败。
    redis并没有提供回滚操作,不能保证原子性。

  • 一致性(Consistency)
    概念:数据库中的数据在执行前后是一致的。
    可以保证,错误的命令不会执行。

  • 隔离性(lsolation)
    概念:事务执行期间,其他操作无法取到执行事务访问的数据。
    可以保证,入队有watch命令监控保证,执行由单线程天然保证。

  • 持久性(Durability)
    持久化数据。
    不能保证,不管是开启aof还是RDB,都存在数据丢失的可能。


redis 知识杂谈
https://huahua132.github.io/2023/05/13/redis/redis/
作者
huahua132
发布于
2023年5月13日
许可协议