Redis总结

Redis总结

缓存作用

  • 高性能:查询速度快
  • 高并发:支持并发高

Redis模型

  1. 先注册server socket的可读事件。
  2. 当有客户端请求接入,IO多路复用就会把server socket放进队列里。
  3. 然后事件分派器会把server socket分派到应答处理器里,获得与客户端连接的socket,将socket的可读事件与命令请求处理器关联。
  4. 当客户端发出命令请求,socket有可读事件,事件分派器就会把socket分派到命令请求处理器。
  5. 执行命令完成后,会把socket的可写事件与命令回复处理器关联。
  6. 等到客户端可写,有可写事件,又会把socket放进队列里,分派到命令回复处理器回复命令执行结果。
  • 使用纯内存,速度快
  • 使用IO多路复用
  • 命令执行为单线程,避免上写文切换

redis的单线程只是命令执行使用单线程,处理网络IO和后台删除过期key是使用多线程处理的。
redis一开始使用单线程处理是因为IO多路复用,加上操作都是纯内存操作,处理速度快,单个线程就可以处理大量的连接。
但是其中有很多的cpu时间是用来进行网络IO的,所以把网络IO和其他后台任务用多线程提高处理速度,命令执行依然用单线程。

Redis数据类型与使用场景

  • string
    • 分布式锁
    • 自增计数器、秒杀、访问频率
    • 分布式唯一ID:时间戳+自增值
  • list
    • 消息队列:往左边加数据lpush key value。往右边取数据,没有数据则阻塞到超时(如10s)brpop key value 10
    • 列表最新消息:消息从左边插入,查左边前十个消息lrange key 0 9
  • hash
  • set
    • 抽奖活动:所有抽奖用户smembers key,随机抽N个用户并删除spop key N,随机抽N个用户不删除srandmember key N
    • 点赞、签到、like功能
    • 共同关注、可能认识的人:交并差集
  • sorted set
    • 排行榜

string

redis的字符串没有使用c自带的字符串,而是重新实现了一个简单动态字符串(simple dynamic string,SDS)。
但SDS除了扩容还会惰性缩容,缩容后会把空间腾出来,以备以后扩容,并不会重新分配内存。
扩容规则是小于1M则比len*2大的2^n,大于1M的则len+1M。
有点像StringBuilder,StringBuilder内部也有个char[]可以扩容追加内容。
+ buf:二进制数组,保存数据,休止符为'\0'
+ len:字符串长度
+ alloc:标头和休止符的长度
+ flags:字节属性,是sdshdr8还是sdshdr16

list

redis的链表就是个标准的双向链表,list对象有头尾节点和节点数量

hash

redis的hash也与java的类似,hash桶的个数是2^n,每个桶里是个链表。
但实际hash桶的数组有两个,在ht这个数组里。
ht[0]ht[1]是一样的,不过ht[1]是在rehash的时候使用。
扩容rehash条件是负载因子(ht[0].used / ht[0].size)大于1,缩容rehash条件是负载因子小于0.1。
扩容是创建一个大于ht[0].used*2的2^n大小的ht[1]
缩容则是创建一个大于ht[0].used的2^n大小的ht[1]
ht[0]的数据rehash到ht[1]里,完成后将ht[1]改为ht[0]ht[1]改置空。
并且避免数据量大时rehash导致卡顿。所以会使用渐进式rehash。
也就是会初始化一个新的ht[1],在每次增删查改的时候,把ht[0]rehash到ht[1],把rehashidx+1。

set

redis的set在数据量不大的时候会时候整数集合(intset)的结构,如果数据量大的话,则使用hash来保存。
intset包含三个字段,分别是
+ contents[]:正数数组,从小到大排序,不重复
+ length:元素数量
+ encoding:编码方式

sorted set

sorted set的底层结构是跳跃表。跳跃表其实就是多层的链表,每一次链表都是有序的。
底层链表包含着全部的节点,新增节点时,首先节点肯定是在底层链表里的。
然后这个节点有50%的机会向上一层,所以一二三层节点的概率为0.5,0.25,0.125……。
查找是从上层链表查起,比较大小之后逐层往下,知道找到或者在底层也找不到数据为止。
查找、增删的时间复杂度是logN。
由于是随机定节点所在的层,所以跳跃表只是大致均匀,不同于普通二叉树的不均匀和红黑树的严格均匀。

Redis过期策略

  • 定期删除:每隔一段时间随机抽一些key看看过期了没,过期了清掉
  • 惰性删除:当某个key被查询到,先检查这个key过期了没,过期了清掉,返回空
  • 定时删除:每个key设置一个定时器,时间到了就删除此key

定期删除+惰性删除,但是会导致大量过期key积累,耗尽内存

缓存淘汰策略

  • noeviction: 写入新数据就报错
  • allkeys-lru:删除最少使用key(最常使用,可以简单用LinkedHashMap实现)
  • allkeys-random:删除随机key
  • volatile-lru:删除有过期时间的最少使用key
  • volatile-random:删除有过期时间的随机key
  • volatile-ttl:删除最快过期的key

缓存淘汰算法(LRU、LFU)

LRU:如果数据在列表里,则把数据移到顶部。如果不在则弹出底部数据,插入新数据。
java里可以用LinkedHashMap简单实现,用remove来get,之后put回去。但是没相应的api删除底部数据。
也可以map+双向链表实现。链表用于增删数据。map用于查找,key-链表节点。
map可以参考ThreadLocal用弱引用,或者能通过链表节点获取key。

redis的LRU,对象维护一个24位的时间戳作为最后活跃时间。
当要清理数据时,会随机拿出N个对象(默认五个),删除活跃时间最久远的那个。

但是活跃最久远不等于最不活跃,所以LFU改造了时间戳,前16位表示时间,后八位表示活跃频率。
LFU会根据时间长短和活跃频率来权衡活跃程度。为了避免删除新键,可以对新键设置默认活跃频率。

Redis持久化

RDB

  • fork一个子进程进行RDB持久化,不用线程是避免锁竞争

  • 父进程和子进程共享内存,操作系统会对内存进行分页

  • 当父进程需要修改数据时,会把相应的内存页复制一份,子进程看到的内存依然是不变的

  • RDB可以设置多久之内有多少个数据发送改变就触发持久化

  • 备份间隔长,丢的数据比较多

  • 对性能影响小,适合做冷备份

  • 重启恢复速度比AOP快
    ## AOF

  • 所以写命令会写入aof缓存,子进程根据策略追加到文件里(例如每秒)

  • 当AOF文件过大,进行重写压缩的时候会生成新的文件,旧的文件照常写入

一般是RDB和AOF两种都同时开启,这样既能RDB的快速恢复,也有AOF的少丢数据

Redis主从架构

对于缓存一般都是支持读高并发,所以使用一主多从,写在主节点,读在从节点。主节点必须开启持久化。

  • 当从节点初次连接到主节点,会触发一次全量复制

  • 主节点会起一个线程生产一个RDB快照,同时将接受到的写命令缓存到内存里

  • 主节点会把RDB快照发给从节点,从节点会把RDB文件保存到硬盘里

  • 从节点会清空自己的数据,加载RDB数据到内存里。期间以旧数据对外提供服务

  • 如果因为RDB文件过大等原因,导致复制时间超时(默认60s)会认为复制失败

  • 主节点会把内存里缓存的写命令发给从节点

  • 主节点会在内存里维护一个backlog,以及主从节点都会保存一个replica offset

  • 如果主从网络断开,恢复时会从上次的replica offset里继续复制数据

  • 如果找不到对应的replica offset,则会触发一次全量复制

Redis哨兵集群

  • 集群监控:负责监控集群的节点是否正常
    • 每隔1s对节点发ping心跳检测,如果超时时间内没有回应,则为主观下线
    • 询问其他哨兵节点此节点的状态,如果超半数都主观下线,则为客观下线
  • 故障转移:如果主节点挂了,会转移到从节点上
    • 哨兵集群选出领导者
    • 剔除下线和未主从同步时间过长的从节点
    • 同步偏移量最大->运行ID最小,选择新的主节点
  • 消息通知:将新主节点信息通知客户端
  • 配置中心:客户端可在哨兵节点获取主节点信息

哨兵集群选举

  • 每个发现主观下线的哨兵节点向其他哨兵节点发送命令,请求自己成为领导者
  • 同意第一个请求,拒接其他请求
  • 如果哨兵节点发现自己获得过半数票,则为领导者
  • 如果有多个领导者,稍后再试
  • 哨兵节点至少三个或以上(两个哨兵节点共同决策,如故障转移。所以最少要三个节点,保证哨兵集群的高可用)

哨兵集群的数据丢失

  • 故障转移后,从节点没完整复制数据
  • 主节点脱离了正常网络但还运行着,故障转移后就有两个主节点。
    客户端可能还没来得及切换主节点,导致往旧主节点写数据。
    当旧主节点网络恢复加入集群成为从节点,就会被清空数据

可以配置使主节点至少跟一个从节点的数据同步少于10s,大于就拒绝写操作

集群模式

  • 使用CRC16(key) & 16384将key分派到一万六千多个槽的其中一个

  • 槽的数量是固定的,集群的每一个主节点负责一部分的槽

  • 客户端连接到集群的某个节点,如果key对应的槽在本节点就直接处理

  • 否则会告知客户端槽所在节点的信息,让客户端再去自行请求

  • 集群节点之间是网状结构,每个节点都会与其他节点保存连接,并且传播加入和离开的节点信息与槽信息

  • 所以新加和删除节点,通过迁移槽和槽数据,迁移完成后刷新槽信息

  • 如果某个节点ping另外的节点,在超时时间内都没有效响应,则为主观下线

  • 节点会向其他节点询问,如果过半数节点都认为下线了,则为客观下线

  • 从节点根据自己的同步偏移量设置参与选举时间。同步越全则选举越早

  • 全部主节点给从节点投票,过半数者成为新的主节点

缓存雪崩

由于缓存被删除/过期/宕机导致请求都到了数据库里
+ 事前:高可用,主从哨兵或者Redis集群
+ 事中:使用本地缓存、服务限流降级,避免数据库被打死
+ 事后:使用RDB/AOF重启恢复redis数据

缓存穿透

编造大量不正常数据,无缓存,导致都打到数据库
+ 检查数据合法性
+ 将这些数据也缓存到Redis里

缓存击穿

热点数据过期,大量请求打到数据库
+ 基本不跟新,考虑将数据设置为不过期
+ 用Redis锁进行更新
+ 起一个定时任务主动更新

缓存与数据库的一致性

先更新数据库再删除缓存,缓存lazy生成

如何保证缓存与数据库双写一致性?里说的:

先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。
因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。

我觉得是无稽之谈,缓存数据不是一般都有过期时间吗,过期兜底不就好了吗?
为什么redis删数据失败,读数据还能成功?这时候是不是应该考虑一下是不是redis挂了?
先删了缓存,还没改数据库的数据又被其他请求查回缓存里了,不也一样导致不一致吗?

参考文章:

Redis 集群模式 Redis Cluster

Redis系列(三):Redis Cluster集群模式

Redis为什么又引入了多线程?作者也逃不过“真香定理”?

Redis设计原理之底层8种数据结构(一)

图解redis五种数据结构底层实现(动图哦)

深入了解Redis底层数据结构

浅谈Redis五种数据结构的底层原理

通俗易懂的Redis数据结构基础教程


Redis总结
https://cellargalaxy.github.io/posts/中间件/9.Redis总结/
作者
cellargalaxy
发布于
2020年6月3日
许可协议