Redis知识点总结
Redis
Redis是一个基于内存的高性能KV存储数据库。
- 数据读写为单线程操作,线程安全;
- 基本是内存操作,且没有并发资源竞争,性能较好;
- 与客户端连接采用多路复用IO,支持连接数较多;
- 有可用性保障,支持数据持久化;
- value支持数据类型较多,满足日常需求;
- 支持主从模式、集群模式;哨兵监控,可用性高;
单线程 与 多路复用模型
Redis使用一个线程接收网络请求,一个线程执行从网络请求读取到的指令。多路复用模型是用一个线程监听全部的网络连接,并不阻塞的读取数据,只在哪个网络连接的数据就绪时,从连接的流中读取指令数据,然后交给work线程去执行命令。
Redis的数据结构
String
普通的字符串类型,底层不是使用C语言字符串实现,是自行设计的字符串类型SDS(Simple Dynamic String),主要不同是SDS可以存储二进制数据,不仅仅是文本;而C语言字符串仅能存储文本信息;
- C语言字符串获取长度是遍历字符数组,复杂度O(N);SDS有一个变量存储字符串长度,复杂度O(1);
- C语言字符串拼接时需要由调用者保证空间足够,否则字符串会溢出数组范围;而SDS会自动扩容,并记录使用的空间大小,便于释放;
- SDS在字符串长度变化时不是申请刚好的内存或立即释放,在申请内存时进行空间预分配;在释放资源时是惰性释放,没有立即释放内存;在字符串长度变化频繁时,减少很多内存申请的开销;
- C语言字符串不能存在空白字符,SDS可以存在,因此SDS可以存储音视频、图片等二进制数据;
List
List是一个双向链表,存储简单字符串,可从链表的头(左边)或尾(右边)插入或取出元素。双向链表的每一个节点是ziplist结构,里面是一块连续的数据,存储了多个元素,一定程度减少内存碎片,且两端读取的复杂度为O(1)。
- ziplist:占用一块连续的内存,内部存储多个元素,每个元素通过编码规则减少内存占用。特点是在删除和新增元素时,会需要内存扩容或者移动元素位置来保证空间连续可用,适用于存储数字与短字符串。
Hash
Hash底层有两种实现,一种是ziplist,一种是hashtable。当K的数量小于hash-max-ziplist-entries(512),所有的值都小于hash-max-ziplist-value(64)时,会采用ziplist,否则使用基于hashtable的字典实现。
字典是Redis的基础,KV也是基于字典来实现的。Redis的字典数据接口与Java的HashMap类似,采用一维数组+链表做桶的方式存储数据。
当字典中的K数量过多,达到一维数组的长度时,字典内部维护的哈希表开始扩容,扩容数组长度为原来的2倍,扩容之后的rehash并不是立即执行(如果K较多,立即rehash会耗时影响后续指令执行,降低吞吐量),而是采用了渐进式哈希。
渐进式哈希:在进行rehash时,字典内部创建一个新的一维数组,还有一个rehashidx变量。新的一维数组就是rehash后存放数据的地方,rehashidx代表旧数组的下标,从0开始,每次对字典进行读取、修改、删除操作的时候,将旧数组中rehashidx下标桶中的元素rehash到新的数组中,并把rehashidx加一。这样每次rehash一部分数据,不会长时间阻塞指令执行,对吞吐量影响较小。读取操作会先从旧数组中读,读不到再从新数组中读。对字典的新增操作只会在新数组中进行。即使后续没有对字典的删、查、改操作,Redis内也有会定时任务进行渐进式rehash(serverCron)。
- Hash的应用
由于hash的较少时候使用的是ziplist结构,比较节约内存,所以针对大量的数据存储可以考虑使用hash来分段存储来达到压缩数据量,节约内存的目的,例如,对于大批量的商品对应的图片地址名称。比如:商品编码固定是10位,可以选取前7位做为hash的key,后三位作为field,图片地址作为value。这样每个hash表都不超过999个,只要把redis.conf中的hash-max-ziplist-entries改为1024,即可。
Set
Set是一个字符串的无序集合,使用hashtable实现,内部元素没有重复。spop操作是返回集合内的一个随机元素。
ZSet
ZSet是字符串的有序集合,内部元素没有重复,通过分值设置排序,使用字典+跳表实现。通过分数保证有序,首先将集合元素作为K,分值作为V,存储在字典中。然后以分值为排序标准放到跳表中。
Redis过期Key清除
Redis的DB中有两个字典类型属性,一个是存储缓存key和缓存的value的字典,一个是存储缓存key和key过期时间的字典(过期字典)。过期字典中的value是一个long类型的Unix时间戳,表示过期时间。
Redis有三种删除策略:
- 定时删除
设置过期时间的时候设置一个定时器,当定时器到时间会执行删除操作。Redis的定时器基于定时事件,定时事件放在一个无序链表中,每次时间事件执行器运行时都遍历这个无序链表找到要执行的任务。如果设置了过期的key过多,会花费大量的时间在遍历链表寻找定时事件上,浪费CPU。 - 惰性删除
每次对key进行读写操作时,都先判断key是否过期,然后再执行操作。这种方式如果key过期后一直没有访问的话,会长时间驻留内存,浪费内存。 - 定期删除
由Redis内的定时器每隔固定时间间隔找到过期的key进行清除。
Redis采用定期删除+惰性删除的策略。
-
定期删除在Redis的周期时间事件serverCorn中执行,每次serverCorn执行时,进行如下过期key清除操作:
- 随机找到100个设置了过期时间的key,然后应用过期算法判断是否过期;
- 删除算法判定过期的key;
- 如果删除过期key任务执行时间超过REDIS_EXPIRELOOKUPS_TIME_LIMIT,则跳出清除逻辑;为了防止清除任务执行过久,阻塞指令执行;
- 否则如果有25个以上的key过期被删除,则继续执行1;
只有主节点会触发过期key删除,且删除之后,del key命令会同步给从节点。通过调整hz参数来调整serverCorn任务每秒钟执行的次数,默认10也就是100ms执行一次。serverCorn执行的太频繁会花太多时间在寻找过期key上,执行的太少又不能及时清理内存,需要折中考虑。
-
惰性删除是,在读写一个key的时候先判断是否过期,过期则删除key,在执行指令。
Redis执行指令时,如果已使用的内存大于maxmemory,则会先清理内存,清理过程是阻塞的,如果未过期的数据已经超出了内存限制还在写入的话,写入指令会反复清理过期key频繁阻塞。
清理时根据配置的maxmemory-policy过期算法来决定清理哪些key。清理时是从全部有过期时间的key中采样选择maxmemory-samples(默认5)个,然后从采样的数据中应用过期算法。选择用采样而不是全部遍历因为全部遍历很耗时。
maxmemory-policy过期算法有6种,默认是volatile-lru:
- volatile-lru:从已设置过期时间的key中选择最近最少使用的数据淘汰掉,其实是记录了key的访问时间,取长时间没有访问的一个删除;
- volatile-ttl:从已设置过期时间的key中选择即将过期的数据淘汰,即最接近过期时间的key;
- allkeys-lru:和volatile-lru类似,但选择范围不只是设置了过期时间的key,而是全部key;
- volatile-random:从设置了过期时间的key中随机选择删除;
- allkeys-random:从全部key中随机选择删除;
- noeviction:不删除,返回错误;
Redis的事件
- 文件事件
文件事件是Redis对Socket操作的封装。通过多路复用将客户端socket统一用一个线程管理。当一个客户端Socket接入、写入、读取、断开时都产生对应的事件,Redis事件分配器将其交给事件处理器执行。 - 时间事件
Redis时间事件有定时事件、周期事件两种。每个时间事件对象有三个重要属性:- id:事件ID,在Redis服务内递增
- when:一个Unix时间戳,什么时候执行
- timeProc:一个回调函数
时间事件放在一个无序链表中,新的事件放到队尾,因为无序,所以每次时间事件检测时都要遍历整个链表查找有哪些事件需要被执行。
Reids有一个全局的周期时间事件serverCorn,每秒钟执行次数通过hz配置来指定,在这个任务里做了下面一些事情:
- 执行过期key清理;
- 更新服务器统计信息,如已使用内存、时间、数据库占用情况等;
- 关闭清理连接失效的客户端;
- 尝试进行持久化;
- 如果是主节点,对从节点进行数据同步;
- 如果是集群模式,对集群进行定期同步和连接测试;
- 进行渐进式哈希;
Redis的持久化
有两种持久化机制:RDB和AOF
RDB 持久化
RedisDataBase,是将内存中的数据的快照保存到二进制文件中(dump.rdb)。RDB触发的方式有三种
- SAVE命令,会同步进行快照保存,保存完成前命令会阻塞;保存后新的rdb文件会替换原来的。
- BGSAVE命令,会开一个子进程去保存快照;
- 自动触发,由配置文件决定多久进行一次快照保存,在serverCron中执行,也是通过BGSAVE命令来保存的。
优点:是紧凑数据格式,适合用于数据备份与恢复;在恢复大数据时速度快快;生成rdb文件时可以由后台线程执行,不会因写入文件阻塞用户命令执行;
缺点:使用子进程去持久化,子进程拥有父进程数据但父进程的修改在子进程中表现不出来,因此在备份过程中执行的写入命令可能没有被备份到。
AOF 持久化
AOF通过将写命令存入备份文件尾部来备份,类似MySQL的binlog。
写入文件的机制:
- always:每次执行写命令都写入AOF文件尾部,会阻塞影响执行速度;
- everysec:每秒钟将写命令写入AOF尾部,不会过多阻塞命令执行,但以为宕机时会丢失最近一秒内执行的写命令;
- none:不进行AOF备份。
优点:因为保存的是写命令,可读性好;增量同步不易丢失数据;
缺点:AOF文件过大(因为不是二进制数据,而是写入命令);恢复时间慢(要依次执行命令);
Redis如何解决AOF文件过大?
当AOF文件过大时,Redis会启动一个子进程,将内存中的数据以写命令的方式写到一个新的AOF文件中;在生成新AOF文件的过程中执行的写命令,会在内存中保留,等到新的AOF备份完成后,将这些命令再次追加写入到AOF文件;最后用新的AOF文件替换原来的。
Redis与Memcached的区别
- 存储方式:Redis可以开启持久化,而Memcached全部在内存中,服务意外停止时Redis能够恢复部分数据(也不能保证全部恢复)。
- 数据类型:Redis支持5中value类型,而Memcached只支持简单字符串。
- 数据大小:Redis单个value最大1G,而Memcached只有1M。
Redis的主从、哨兵、集群
主从
单机部署的Redis,在Redis意外挂掉的时候,客户端将无法连接Redis服务,导致不可用。而且在未开启持久化的情况下,挂掉的Redis重启后数据为空,导致数据丢失。
为保障可用性,Redis可以配置从服务器作为主服务器数据的备份,可以通过执行slaveof命令或者修改配置文件中的slaveof配置项来为一个Redis服务指定主服务。
同步策略:从服务器刚连接到主服务器时要求全量同步;全量同步完成后进行增量同步。
指定主服务器后,从服务器将向主服务器发送SYNC命令,接收到命令后,主服务器开始执行BGSAVE命令持久化数据(RDB)。数据备份过程中,所有的写入命令都正常执行并被记录到复制积压缓冲区中。BGSAVE执行完成后,向所有待同步的从服务器发送RDB快照文件。从服务器接收到RDB文件后,丢弃所有数据并加载RDB文件。主服务器RDB发送完成后,向从服务器发送复制积压缓冲区中的命令,从服务器加载完RDB之后执行复制积压命令,执行完成数据和主服务器相同。此时完成全量同步。
后续主服务器收到写命令的时候,会发送给所有从服务器,达到增量同步的目的(指令传播)。
从服务器重启后,会执行全量同步,如果多个从服务器同时重启,且数据里较大的话,会对主服务器性能造成较大影响。
为防止主从同步时给主服务器带来较大影响,应尽量减少从服务器同步的可能,一个方法是将一主多从变更为主从从模式。即主服务器只有一个从服务器,但这个从服务器可以再有一个从服务器,像链一样可以延伸很长,这样每个服务只需要向一个从服务器发送数据即可。
在主服务器意外挂掉时,可以手动将一个从服务器切换为主服务器,但这样需要修改全部从服务器的配置,而且主服务器重启后,应当作为一个新的从服务器挂载到新的主服务器上,操作较为复杂,因此出现了哨兵这样一个东西。
哨兵 Sentinel
Redis Sentinel是一个分布式架构的Redis监控系统,用于Redis应用了主从模式时,监控多台主从服务器的状态。在主服务器意外挂掉时,Sentinel将选举一个从服务器作为新的主服务器,并将自动切换其余从服务器的配置。哨兵之间也会互相监控状态。
哨兵其实也是Redis服务的一部分,Redis安装目录下有sentinel.conf文件,是哨兵的配置,修改"sentinel monitor mymaster 192.168.11.128 6379 2"即可设置哨兵监控的主服务器。redis-server命令用于启动Redis服务,redis-sentinel命令用于启动哨兵。哨兵只需要配置主服务器地址,从服务器地址可从info命令的返回中获取到。
-
如何确认主服务器挂掉了?
- 哨兵每隔10秒会向监控的redis主服务器发送info命令获取网络拓扑状态;
- 哨兵每隔2秒向Redis主节点的__sentinel__:hello频道上发布哨兵对于主节点状态的判断,以及哨兵本身的信息。哨兵本身也会订阅这个状态来了解其他哨兵对于主节点的判断。
- 每隔1秒,哨兵向所有redis服务和哨兵服务发送心跳检测(ping/pong命令),确认节点是否可达。如果在限定时间内(down-after-milliseconds)服务器没有响应,则哨兵认为该redis节点处于主动下线状态ODown。
- 当主节点主动下线时,哨兵会向其他哨兵询问关于主节点的判断,如果超过配置个数(quorum)的哨兵认为主节点ODown,则此时哨兵认为主节点客观下线SDown。
- 当哨兵认为主节点SDown时,会向其他哨兵发起请求成为leader(收到请求的节点如果没有同意过其他节点成为leader则同意此次请求,否则拒绝)。如果有半数以上哨兵节点同意,且同意的哨兵个数超过quorum配置,则哨兵成为leader。如果有多个哨兵成为leader,则等待一段时间重新选举。一般情况下是最先确认主节点客观下线的哨兵会成为leader
-
如何进行故障转移
- 主从切换由leader哨兵执行,leader选择一个’合适的’从节点作为新的主节点(怎么知道相似度最高?),通过执行 slave of no one让其成为主节点。
- 对其他从节点执行“slave of xxxx”命令,让其成为新主节点的从节点。同时从主节点同步数据的从节点个数通过(parallel-syncs)配置项指定,这个配置值越小,则完成故障转移耗时越长。值过大会导致同一时间不可用的从节点个数较多。一般设置为1,优先保障可用性。在从节点进行全量同步时,主节点的RDB快照文件和缓存的指令是要发送给从节点的,设置为1也能减少同步时对网络资源的占用。
- 监控原来的主节点,当期重启后,执行slave of指令作为新主节点的从节点。
-
故障转移时如何选择’合适的’的从节点
- 从服务器可以配置slave-priority,优先选择优先级最高的从节点。
- 如果没有配置优先级,则选择复制偏移量最大的从节点,这样能保证从节点与主节点的数据相似度最高。
- 如果没有,则选择runId最小的节点。(runId是redis启动时生成的随机的40位16进制字符串,每台机器可以生成多次,每次重启都会生成新的runId)
-
为什么哨兵要部署三个或以上
首先单点哨兵不可靠,一旦哨兵挂掉则无法完成主从切换,因此哨兵需要集群部署。
在进行主从切换时,是由哨兵的leader来决定选择哪一个从节点提升为主节点,因此哨兵需要进行选举。哨兵选举基于Raft算法,因此需要配置奇数个节点来防止平票。 -
哨兵如何实现报警
当哨兵监控的节点状态异常时,会执行notification-script配置的脚本。脚本的执行结果若为1,即稍后重试(最大重试次数为10);若为2,则执行结束。并且脚本最大执行时间为60秒,超时会被终止执行。 -
哨兵主从切换的数据丢失问题
- 异步同步数据数据丢失。主从数据同步是异步的,如果在同步的过程中主节点挂掉,可能会有一部分没有持久化也没有发送给从节点的数据丢失。
无法通过设置主节点持久化来解决这个问题,因为原主节点挂掉后,哨兵会选举出新的主节点,此时新主节点的数据是不完整的,而旧主节点重启后,会放弃之前的持久化数据,从新的主节点同步,此时数据完全丢失。 - 脑裂数据丢失。有可能某一时刻,主节点与从节点和哨兵之间网络出现分区,但与客户端连接正常,此时客户端正常向主节点写入数据。哨兵认为主节点挂掉,选举出新的主节点。等到原来的主节点网络恢复时,会被设置为从节点从新的主节点同步数据,此时网络分区这段时间内原主节点写入的数据丢失。
可以设置特殊情况下主节点不接受写入数据指令来防止脑裂导致的数据丢失。
- min-slaves-to-write
最少同时写入的从节点个数,默认0不限制,设为3则最少有三个从节点在同步数据,否则不执行写入命令 - min-slaves-max-lag
主从同步复制的最大延迟秒数,如果同步复制延迟超过这个值不执行写入命令。默认10s。
如果min-slaves-to-write=3,min-slaves-max-lag=10,则表示进行同步复制的从节点个数至少有3个复制延迟时间在10秒以内才执行写入命令,否则不执行。
- 异步同步数据数据丢失。主从数据同步是异步的,如果在同步的过程中主节点挂掉,可能会有一部分没有持久化也没有发送给从节点的数据丢失。
缓存
一般数据查询操作是通过MySQL等关系数据库执行,当并发访问量高时,关系数据库性能会下降。一般高并发时使用K-V数据库对MySQL查询结果做缓存,一些基于内存的K-V数据库访问速度快,能够提高响应速度。
什么时候适合使用缓存?对于数据访问频率高适合使用,数据读多写少适合使用,数据一致性要求低适合使用。
缓存雪崩、穿透、击穿
雪崩
缓存雪崩是在某一刻,大量的key过期或者缓存服务器挂了,导致很多请求的查询使用DB。有可能出现DB压力过大甚至挂掉的现象。
解决方案:
- 保证缓存服务器的可用性,例如设置Redis主从和集群,Sentinel会保证Redis主从故障切换。这个是预防操作。
- 缓存失效后通过加锁或队列控制DB层并发访问量。并通过降级措施防止请求挂起过久,防止吞吐量降低。或通过熔断直接拒绝更多请求。存在用户体验问题。
- 使用两级缓存。一级缓存在JVM本地,比如使用ehcache,当二级缓存Redis挂掉时,一级缓存可以抵挡一段时间流量。但是存在数据不一致问题,更新缓存时可能某些一级缓存更新不到,只能等待自然失效。
- 对于需要定时过期的大量key,可以将key的过期时间设置在一个随机的范围,如设置8点过期,则可以让key在7:55~8:05过期,防止同时过期大量key。
穿透
缓存穿透是请求查询一个不存在的数据,缓存层没有对应的该数据,请求就会被放到DB层执行。并发访问量大时可能导致DB挂掉。
解决方案:
- 将不存在的key结果也缓存下来,缓存一个null。
- 使用布隆过滤器判断缓存是否存在。布隆过滤器存在缺陷是对于一个key存在的判断有误差,但是对一个key不存在是一定的。即布隆过滤器说缓存存在实际上有可能不存在,但布隆过滤器认为缓存不存在则一定不存在。
- 布隆过滤器原理:用多个哈希函数计算key的hash值,然后将hash值映射到一个二进制位数组中,将对应索引的位设置为1。
击穿
缓存击穿是指,在某一刻一个热点key过期,同时有大量的并发请求出现,请求通过DB执行,请求响应速度慢设置DB被查询宕机。这种在key过期时请求直接查询数据库的情况叫缓存击穿。
解决方案:
- 热点数据永不过期。
写入DB时同时写入缓存,存在数据不一致问题。 - 通过定时任务定时更新热点key。
也是保证数据不过期,当key较多时不适合使用,对于key固定,缓存内容较多时比较好。 - 惰性过期缓存。
数据存储到缓存时将过期时间存入缓存,访问时当发现缓存即将过期(比如还有1分钟过期),就从缓存中取出数据并主动更新缓存。
如果持续访问频率不平均,刚好过期前没有请求的话,仍然会出现击穿问题。 - 加锁。
访问时如果缓存过期,则对访问资源加锁,然后进行DB查询与缓存放入。这样可以保证只有一个线程对数据库操作,但是降低了吞吐量,阻塞响应可能对用户体验不好。 - 分级缓存。
添加两级缓存,一级缓存过期时间较短,二级缓存过期时间长。查询时先查一级缓存,不存在则查询二级缓存并存入一级缓存。二级缓存不存在则通过加锁方式查询并更新二级缓存。存在数据不一致问题,数据更新时,一级缓存中的数据可能是旧的。