分布式缓存之redis知识点

数据类型

字符串string、散列hash、列表list、集合set、有序集合zset、位图bitmap、基数统计HyperLogLogs、消息队列Streams

基本数据类型底层的数据结构不是固定的,会根据数据情况进行调整。


底层数据结构

redis使用c语言实现,以下的数据结构都是底层通过c语言定义的结构。

hashtable

字典结构。字典结构类似hashMap,使用哈希表,通过数组存储节点,每一个键值对节点都是一个disEntry结构,包含指向下一个键值对的指针。每来一个键值对通过计算hash值命中数组位置,如果hash冲突则利用disEntry结构,通过链地址法进行连接解决冲突。redis会维护哈希表的容量在一个合理的范围内,当数组容量过大或过小时通过rehash进行扩容或缩减,rehash的方式时将哈希表上的数据rehash放到一个新的哈希表,等到全部迁移完成,将原哈希表的丢弃使用新的哈希表。
使用的数据类型:

  • redis的key-value形式就是通过字典结构存储的,
  • hashMap键值对大于512个或某个键值对的键或值的字符串长度超过64字节时使用。
  • set数据类型数据量较多或者有非整数的数据时使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-srCawbk4-1585712365653)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p12)]

SDS

字符串类型的value采用SDS结构存储,类似java的list结构,通过字节数组存储数据,且数组长度是动态可变的。
字符串编码有三种分别为int编码、embstr编码、raw编码
编码的区别:int编码用于value为纯数字的的情况,embstr编码用于字符串小于等于44字节的,raw编码用于字符串大于44字节的。

LinkedList

维护一个双向链表,与java的linkedlist类似,维护头尾节点,节点维护前继节点和后继节点指针,增加删除操作比较方便。redis 3.2之前list类型底层数据比较多时使用此数据类型。

ziplist

压缩链表,列表包含少数据量时会使用压缩链表,即占用空间较少时使用,通过使用连续的内存空间存储数据,相对LinkedList省掉前继节点和后继节点节省空间,但是更新效率低,会导致更新时引发连环更新(删除一个节点,其他位置也要迁移),有点类似java的arrayList。
使用的数据类型

  • redis 3.2之前list类型底层数据比较少时使用。
  • hash类型键值对小于512个,所有键值对的键或值的字符串长度不超过64字节时使用。
  • Zset类型数据量比较小时使用
quicklist

快速列表,redis 3.2之后新增的数据结构,结合压缩列表和双向列表,3.2之后list类型底层使用此数据结构。该结构同样维护一个双向链表,不同的是单节点上不是单个数据,而是多个数据压缩到一个节点上,单个节点上的存储数据量少,采用ziplist的形式存储。

inset

通过数组实现,set类型数据都为int类型且数据量不多时通过此结构保存

skipList

跳跃表,支持快速查询、插入、删除等操作。一层一层建立索引,每级的索引数递减,每层索引上的查找使用二分法查找,增删查时间复杂度平均0(logN),实现比红黑树相对简单,避免红黑树旋转,zset类型底层使用此数据类型,效率高且支持排序,支持范围查询
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xC8qfXim-1585712365656)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p13)]

类型结构
listLinkedList(3.2前数据较多时)、ziplist(3.2前数据较少时)、quicklist(3.2之后使用)
key-valuehashtable
stringSDS
hashhashtable(键值对超过512、value超过64字节)、ziplist(小于512)
sethashtable(非整数时或数据太多时)、inset(存数字set)
zsetziplist(数据量比较小时使用)、skipList
bitmap数组

单线程模型

redis基于单线程实现。网络通信基于epoll实现非阻塞io多路复用,并将epoll的read、write、close等转换成事件回调处理。使用单线程避免了多线程环境线程上下文切换、竞争共享资源需要用锁导致的性能损耗、开发逻辑相对比较简单。之所以采用单线程,除了以上原因,还有就是redis都是基于内存操作,性能瓶颈主要在网络传输时延和带宽限制,如果性能瓶颈出现在多核cpu上也可以在单机器部署多个实例利用cpu性能。如果单个命令执行时间过长,会影响其他命令的阻塞。

pipelining

redis客户端普通的get set请求都需要等待服务端响应后才能接收响应,而redis的pipelining会将多个请求通过一次tcp连接发到服务端,服务端会将部分请求放到内存队列中,处理后通过一次tcp连接将多个命令执行结果返回。redis各个语言client都支持pipelining,命令行客户端则不支持,因为命令行客户端每执行一条命令都要等待上一条命令执行结束。
优点:减少了网络通信次数,批量处理避免每个命令执行都要等待上次执行结束造成的阻塞,极大提高吞吐量
缺点:无法在执行单个命令后立即获取到执行结果,相当于批量执行后才可以获取到执行结果。
注意:由于redis服务端处理多个连接需要暂时缓存到内存队列,因此会占用一定内存,所以要考虑server的物理内存情况,及网络接口的缓冲能力。对需要进行批量操作,且对命令实时性要求不高可以采用。

qps

redis是单纯内存操作,理论上基于内存操作可以达到上百万的qps,但是考虑到受到机器带宽、存在网络传输时延、连接数等等的影响,会有所降低。以下附上官方测试结果,使用pipelining可以达到几十万的qps,而不使用pipelining由于受到redis客户端吞吐量限制,也能达到10多w的qps。
官方测试

如何提高redis性能
  1. redis的瓶颈大多数存在于带宽、网络延迟等,比如1kb的字符串传输,如果要达到10w的qps,那么需要大概800mbps的带宽,传输效率才能跟上(100m/s)。因此可以提升带宽。
  2. cpu选择可以趋向大缓存快速cpu,多核不是考虑的因素。
  3. 不建议搭到虚拟机上。
  4. 主从、redis的服务端客户端要搭载内网中,降低网络时延
  5. 持久化最好不在master上进行,rdb模式的持久化要写内存快照,会阻塞进程的工作(bgsave的fork阶段也会阻塞,并且bgsave过程如果一直变动数据,会造成不断复制数据页,也会有一定的性能损耗),当快照比较大时对性能影响是非常大的,会间断性暂停服务;AOF文件过大会影响Master重启的恢复速度。因此持久化可以在单独的slave上执行,且该slave不对外提供业务
  6. 客户端可以配置读写分离
  7. redis集合的数据不要太大,最好分集合。

持久化

持久化分为两种方式,分为RDB和AOF

RDB

rdb是通过将当前进程的数据生成快照保存到磁盘过程,触发方式分为手动触发自动触发

  • 手动触发:通过save命令和bgsave命令触发,save命令不建议使用,触发生成快照时会阻塞整个进程,直到快照生成结束,严重影响性能。bgsave命令则是通过fork系统函数创建子进程,由子进程进行持久化操作。bgsave只会在fork阶段会阻塞进程,相对save性能有很大的优化。fork函数创建子进程会与父进程共享内存,基于copy on write写时复制机制,出现数据变动再复制一份数据页更改数据,因此子进程和父进程相互独立不会影响相互的内存数据。所以bgsave保存的快照是执行fork时的redis进程内存信息,快照生成后会替换原有的rbd文件
  • 自动触发: 1.在redis配置文件中增加save m n配置,表示m秒内存在n次修改时自动触发bgsave,可以配置多个规则。2.从节点进行全量复制时(比如slave第一次连接master),master会触发bgsave生成rdb文件发给slave。3. 执行debug reload重新加载redis时会触发bgsave。4. 执行shutdown命令时,如果不是使用aof持久化则自动执行bgsave。

rdb文件名和保存目录可以通过配置文件配置,rdb文件可以通过配置对生成的文件进行压缩,降低磁盘占用。

rbd优点缺点:适合全量复制和备份,redis加载rbd恢复数据的速度快。缺点是无法做到实时复制,fork操作不可频繁执行,会影响redis性能。

AOF

针对RDB无法实时复制的问题,redis提供AOF方式持久化,将每次命令的执行写到日志中,恢复数据时根据日志命令执行进行数据恢复。
执行流程:redis接收到命令时,会将命令通过系统调用write函数将命令写到文件缓冲区(delayed writer机制),缓冲区根据配置的策略刷盘(不直接往硬盘文件写是因为避免每次都进行磁盘io)。当文件达到一定程度的大小则会进行文件重写,达到压缩文件的目的。

缓存区的数据刷到硬盘的时机:redis支持三种配置,分别为always、no、everysec,默认为everysec。

  • always:每次写入命令都同步aof文件,会导致写入操作的性能依赖磁盘的io性能。违背redis基于内存操作的高性能,不建议配置。性能最差,安全性最高。
  • no:完全依赖操作系统的刷盘。操作系统会在缓存区满了之后再将数据同步到文件中,刷盘次数最少,性能最优,但是一但机器断电,缓冲区的数据都会丢失,安全性差
  • everysec:默认的配置,每秒调用一次系统函数fsync进行刷盘,理论上最多只会丢失一秒的数据。性能和安全性都兼顾,建议用此配置

文件重写:随着命令的写入,文件会越来越大,因此在文件达到一定程度后会进行文件重写,文件重写会排除掉已经超时的数据、合并集合操作命令、去除无效命令(对某个key的赋值实际上只有最后一次设值有效)。文件重写除了降低文件占用空间还可以让aof被redis加载时更快。触发文件重写分为两种:手动触发和自动触发。

  • 手动触发重写:手动触发可以调用bgrewriteaof命令。
  • 自动触发重写:自动触发通过配置auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定触发时机。auto-aof-rewrite-min-size为触发重写的aof文件大小,默认为64m。auto-aof-rewrite-percentage为当前aof超过上次重写的文件大小的比例,默认为100,表示超过上次重写的100%即2倍时,触发重写。两个条件同时满足才会触发重写

文件重写过程中新的数据会写到缓冲区中,文件重写结束后,再将缓冲区的数据与重写后的新文件数据合并。


缓存删除

缓存删除分为三种情况,分别为被动删除、主动删除、缓存淘汰。

被动删除

操作某个key时,如果发现这个key已经超时则会删除该key,并且无效操作(除了set)。这个方式性能较好,不用去扫描key去排查,但是却会导致有些key已经过期而一直占用着内存。

主动删除

redis默认每秒运行10次触发主动删除,可以通过配置。主动删除会随机获取100个设置了过期时间的key,删除过期了的key,如果删除的key超过了25%,则重复这个操作。相当于随机求概率,确保超时的key数量大致不超过设置了过期时间的key的25%。

缓存淘汰

redis可以设置最大内存大小maxmemory,当redis的内存超过这个内存大小时,会触发缓存淘汰,且在超过maxmemory大小后,对于所有的读写请求都会触发缓存淘汰,可能会导致请求有所延迟建议不要配置maxmemory,或者配置确保不要触发,可以通过提高主动删除的频率或者扩容解决。maxmemory设置为0表示不限制。缓存淘汰不会一次性针对所有的key进行处理,默认会选择5个key,然后选5个key中最符合条件(如果在所有key中找符合策略的key是不现实的,性能低),通过缓存淘汰策略处理这些key进行删除,key的个数可以通过maxmemory-samples参数配置。淘汰策略可以通过maxmemory-policy参数配置。

缓存淘汰策略

  • volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中随机选择数据淘汰
  • allkeys-lru:从数据集中(包含未设置过期时间的)挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集中(包含未设置过期时间的)随机选择数据淘汰
  • no-enviction:禁止删除数据

redis4新增lfu策略,即淘汰最近最少使用频次的数据。由于lru可能存在一种情况,有个数据访问很频繁,但是相比其他键最后一次访问的时间久,从而导致热数据被淘汰。

  • volatile-lfu:从已设置过期时间的数据集中挑选最近最少使用频次的数据淘汰
  • allkeys-lfu:从数据集中挑选最近最少使用频次的数据淘汰

Redis lru实现
lru为最近最少使用,redis会维护一个24位的全局时钟,相当于系统当前的时间戳,当新增key的时候会把全局时钟赋给每个key对象中存储的内部时钟,更新和获取会更新这个时间,redis通过随机获取几个key,从这几个key中选出与全局时钟相隔最久的key就行删除。(与传统的lru实现不一样,传统的lru会维护一个双向队列,每次访问到移到队尾,最近最少访问的就在堆头

Redis lfu实现
lfu为最近最少使用频次,如果使用lfu,则对象中的24位时钟会变成两个部分,前16位当作时钟,后8位表示访问频次。访问频次不是每次访问就加1,而是通过访问一定程度后,才会增加频次,如果隔几分钟没有访问,则会降低这个访问频次。两个参数可以调整频次的计算。lfu-log-factor计算因子,影响频次的增长速度,越大频次增长的越慢。lfu-decay-time单位是分钟,表示隔几分钟没访问就要降低频次。新分配的key默认频次为5,防止频次过低直接被删除。
官方文档


业务层缓存优化

缓存穿透:指的是缓存和数据库都不存在的数据,用户每次发起请求都命中到数据库,导致数据库压力太大。解决方案是将该参数对应的值存个短时间的空值,这样用户下次请求就直接命中缓存,返回空。
但是也会导致产生大量无效key,通过布隆过滤器填入key,保证key不是无效key,虽然不一定准确,存在hash冲突,但是可以保证一定程度有效性,过滤掉部分请求。

缓存击穿:并发量很高时需要考虑。指的是当缓存失效和重新查库设置缓存的间隙,有很多并发的请求进来,都去查库,导致请求都命中数据库。解决方案:可以通过加锁解决,在重新查库设置缓存的操作前加锁,其他请求要去查库时拿锁失败进入阻塞,等待缓存设置完直接查询缓存返回。或者热数据设置不过期。
缓存雪崩:1. 缓存都设置同个时间过期,导致突发请求到直接命中数据库。解决方案:缓存过期时间不设置固定时间,可以设置在一个范围时间内波动。设置热点数据不过期。2. redis挂了,导致流量全部到数据库。保证可用性,redis数据持久化,避免同时失效,本地缓存,限制流量。
热数据预加载:在项目发布前,如果有已知的热数据,可以先把热数据加载到缓存中,项目提供服务后,用户的请求直接命中缓存,不用去查库。


高可用架构

redis实现高可用,包含主从、哨兵sentinel、redis-cluster集群

主从

redis分为master、slave。当slave连接上master后会发送sync命令到master,master接收到命令后执行bgsave生成rdb文件,并将新的写命令存储到缓冲区,rdb文件生成后,master将rdb文件发送给slave,并发送缓冲区中的写命令。slave丢弃旧数据载入快照,完成载入后开始接收命令请求,执行缓冲区的写命令。之后master每接收到一条写命令,就发送给slave执行。

优点:可以进行读写分离,降低master的读压力。slave可以接受其他slave的连接和同步请求,可以降低master的同步压力。
缺点:不具备自动容错和恢复功能,主从宕机会导致前端请求部分失败,需要手动切换客户端的ip。主机宕机,有部分数据未能及时同步到从库,主从切换后数据不一致。
主从不一致的情况
过期时间不一致的问题。主从同步期间执行了expire,expire的本质是当前时间戳加上过期时间,如果在同步期间这个命令传到从库,会等到同步结束再执行,那么当前时间戳加上过期时间就会有延迟,延迟了同步所耗费的时间。本质上虽然master在删除key时会同步给从库,但是如果这个key没有及时被删除,而master在接受访问请求时会判断已经过期不让访问,但是slave的过期时间延迟了,则slave可以访问这个key,就导致不一致。expireat可以解决这个问题,expireat是直接指定过期的时间戳的,所以没有问题。一般只有对过期时间要求比较严苛的场景才需要考虑

哨兵sentinel

redis主从模式下,如果master出错,需要手动切换主从,因此redis提供了哨兵工具进行自动化系统监控和故障恢复功能,哨兵的功能包括监控主从是否正常,主出现故障时进行主从切换。哨兵模式本身也需要保证高可用,因此要保证至少需要有三个哨兵实例实现集群。

sentinel判断节点故障的方式:
master下线分为主观下线和客观下线,slave和Sentinel只有主观下线,不会协商。
主观下线,sentinel默认1秒会发送一次ping到master、slave、其他哨兵节点做心跳检测,当这些节点超过down-after-milliseconds参数指定的时间还没回复时或者没有正确回复时,该sentinel则主观认为该节点故障。
客观下线:redis基于gossip协议去中心化思想,sentinel采用redis的发布订阅模式相互通信,每个sentinel都订阅名为__sentinel__:hello的渠道,且每2秒发布一次监控的节点信息、master配置信息到该channel。sentinel主观认为某节点已经故障,当该sentinel通过渠道接收到配置的quorum票数的sentinel认为该节点已经故障,则客观认为该节点故障。

master故障转移
当sentinel客观认为master已经故障,则会发送is-master-down-by-addr命令给其他sentinel,让自己成为leader并执行failover.该sentinel需要获得超过半数并且大于quorum数的sentinel授权,避免并发多个sentinel进行failover。当sentinel节点被选举为leader后会进行故障转移,此时会为该master进行failover分配一个版本号(用于确认配置的新旧)。leader选择一个slave节点发送SLAVEOF NO ONE命令升级为master节点,通知其他从节点指向新的主节点,通知客户端master节点已经替换。在完成故障转移后leader会将新的master配置发布到channel中,其他sentinel发现配置的版本号比当前的维护的配置的版本号高,会替换成新的配置文件。failover-timeout参数可以设置故障转移的超时时间,如果故障转移超过这个时间,则授权过本次failover的sentinel会再一次为该master进行failover。

故障转移slave节点的选择

  1. 过滤掉被认为主观下线的slave节点
  2. 从slave中选择节点优先级最高的
  3. 选择复制偏移量最大,复制最完整的节点
  4. 运行id最小的从节点。

slave节点和Sentinel节点的自动发现
sentinel不用配置其他Sentinel节点的地址,而是通过2秒一次发布订阅方式在channel通道通信,从而发现其他的Sentinel节点。同时也不用配置slave节点的地址,sentinel配置了master的地址,会通过10秒一次发送info命令给master和salve节点获取基本信息。通过这样的机制,有新的slave节点和Sentinel节点加入集群都会被自动发现。当master被标记为客观下线后,发送info命令的频率会变成1秒一次

TILT Sentinel保护模式
Sentinel的运行依赖系统时间,因此如果系统时间出现意外变动或者进程被阻塞等,都会导致Sentinel做出错误判断。因此Sentinel内置一个计时器,正常大约100毫秒会中断一次,通过当前系统时间和上一次中断的系统时间比较,如果这个时间超过2秒或者为负数,那么此时会进入TILT模式,此时仍旧会监控master,但是在集群中不会再起作用,不对is-master-down-add-addr请求做响应。因为它变得不可信了,直到保持30秒的正常计数才会退出TILT模式。

参数配置
以下两个参数确保master网络异常后,不会继续写数据,两个条件不满足都会拒绝客户端请求。
min-slaves-to-write 1 master最少要保证能往1个slave写数据
min-slaves-max-lag 10 最长没有与slave同步数据的时间

redis-cluster集群

redis-cluster相当于redis的分布式方案,redis的数据被拆分到多台redis实例上,cluster会通过计算key的hash值与16384相与计算命中到哪个桶。每个redis实例负责存储16384内的部分桶数据。
客户端连接只需要连接任意一个redis实例,每个实例上包含一个cluster,存储key的时候会通过计算命中的桶由相应的节点处理。
redis实例之间的通信基于gossip协议,认定一个节点宕机需要半数以上的节点认同。
节点的主从结构,为了提高每个节点的可用性,每个节点可以配置主从结构,在主节点宕机后启动从节点。


redis发布订阅PUB/SUB

类似简单版的消息队列,不提供消息持久化、事务、消息重试等等,简单的发布订阅。客户端往channel发布消息,订阅了该channel的客户端都会受到消息。Sentinel就是通过该形式通信。


事务

redis支持简单的事务,通过MULTI命令开启事务,EXEC命令提交事务,在事务中执行的命令会在EXEC命令执行后才一起执行。redis事务不支持回滚,但是会分析语法错误,如果某个命令是错误的,那么redis会报错,且事务里的命令都不会执行,而如果是运行时才能知道的错误,比如对一个普通的key做集合的add操作,那么事务中可以正确执行的命令不会受到影响,即事务中只有报错的那条命令不会正确执行。如果要处理回滚,只能用户自己根据每条命令的执行结果自己做反操作回滚。


SETNX做分布式锁

SETNX 全称是SET if Not exists,即如果key不存在才能设值成功,成功返回1,失败返回0。该命令是原子操作的,即多线程操作不会有线程不安全问题。利用此特性可以用于做限时的分布式锁。


jedis、Redisson、Lettuce

java操作redis的框架,jedis提供比较全面的redis命令支持,命令与api一致。Redisson和Lettuce支持异步通信,封装性比较高提供了全面的分布式锁方案,部分redis命令不支持。

更新库应该删除缓存 不要更新缓存

并发时,如果a先更新库,b再更新库,然后b更新缓存,a再更新缓存,就会导致缓存的是脏数据。

怎么保证缓存和数据库的数据一致性
  • 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,就去数据库中读取数据写入缓存,此时缓存中为脏数据。
  • 如果先写了库,在删除缓存前宕机了,没有删除掉缓存,则也会出现数据不一致情况。
  1. 采取懒加载、双删的机制。懒加载即数据更新直接删除缓存而不是更新缓存,查询数据的时候再触发把数据加载到缓存。双删即先删除缓存,再更新数据库,再删除缓存。注意:第二次删除缓存要延迟一点时间,因为可能你第一次删除缓存后、更新数据库数据前,其他应用读取了脏数据在内存中,如果此时更新数据库后立马再删除缓存,其他应用可能在这个间隙又把数据更新到缓存。所以要延迟执行,可以通过mq延迟队列延迟执行,或者在内存中线程池延迟执行,看具体业务。
  2. 如果第二次删除失败了呢?
  • 设置超时时间,等缓存过期就失效,会有一段时间错误数据
  • 业务端重试或者中间件重试。
    (1)业务端重试就是在业务代码取发送mq消息,另一端去消费重试。但是会侵入业务代码。
    (2)中间件就是去订阅binlog文件,监听sql的更新,去删除缓存,失败了再发mq再消费重试。(删除缓存是幂等操作,可以重复执行)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值