Redis 核心知识点深度剖析:原理、机制与应用
在当今数字化浪潮汹涌澎湃的时代,数据如同企业的生命线,高效的数据存储、处理与管理成为众多应用程序成功的关键。在这数据管理的竞技场上,Redis作为一款开源的内存数据结构存储系统,凭借其卓越的性能、丰富的数据结构和强大的功能,脱颖而出,成为开发者手中的得力利器。无论是高并发场景下的数据缓存,还是实时数据分析、消息队列等应用,Redis都展现出无可替代的价值。
本文将深入探讨Redis的核心知识点,带你领略其内部的奥秘,助力你在数据处理的领域中如鱼得水。
一、详解Redis基础知识点
1. 为什么Redis被设计成是单线程的redis本质上都是在内存操作,性能瓶颈不在CPU,通过单线程处理客户端指令可以避免线程上下文切换开销。
此时如果使用多线程进行操作,势必要保护临界资源并发安全而采用较粗力度的锁,由此导致的大量线程阻塞争抢临界资源而导致操作各种大耗时操作显然是得不偿失的,并且多线程操作一般会引入各种同步原语,对于我们这种动辄十几万的内存数据库问题的定位和排查的难度都会大大增加。
2. 为什么Redis单线程也能这么快通过IO多路复用保证单线程处理多连接数据结构做了极致的优化活跃于内存即纯粹的内存操作,性能表现出色单线程处理所有指令,避免线程上下文切换和同步原语的使用的开销3. 说说Redis6.0中的多线程Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的:
特定场景下我们某些业务需要针对redis执行多条指令,按照传统做法我们需要逐条发送指令,这样的做法使得一个业务针对redis的操作存在多次网络往返即多次RTT:
于是就有了pipeline的概念,通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,redis收到这些指令之后会依次执行并响应给客户端:
Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。
在性能方面, pipeline 有下面两个优势:
节省了RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数。减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 pipeline将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。唯一需要注意的是redis的pipeline不保证原子性,即使我们通过pipeline处理多条指令,它也是逐条执行的,这一点我们还是需要注意一下。
5. Redis如何保证命令原子性使用原子命令:
Redis 提供了 INCR/DECR/SETNX 命令,把RMW三个操作转变为一个原子操作Redis 是使用单线程串行处理客户端的请求来操作命令,所以当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的.加锁:
加锁主要是考虑多个客户端对相同业务方法进行修改操作,我们可以使用加锁的方式保证原子性,大致的方式为:
使用setnx上锁上锁成功后,执行业务修改操作使用del释放锁这期间你可能会遇到两个问题:
假如在操作期间出现了业务异常(或者服务器宕机了),就会导致key未能及时释放,进而导致锁无法释放,我们必须对这个锁设置时效,并且在操作期间定时监测和续命。误删除,比如用户1持有锁,用户2拿不到锁,用del命令把这个锁删除,对此我们可以使用setnx的value比对看看上锁和用户和解锁的用户是不是同一个进行进一步的操作。
使用lua脚本:多个操作写到一个 Lua 脚本中(Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性)
是resp自己设计的RESP协议,该协议的特点为:
简单高效易于解析保证二进制安全7. Redis 与 Memcached 有什么区别数据结构层面:redis支持多种数据结构例如字符串、列表、集合、有序集合、哈希,Memcached 仅仅支持简单的键值对存储。持久化层面:Redis 支持RDB或者AOF的方式进行持久化,后者不支持持久化。数据分片层面:redis通过hash slot实现自动分片和负载均衡,而后者只能手动进行分片。处理数据的方式:redis通过单线程处理所有的指令,并且支持事务、lua脚本等高级功能,而后者使用多线程处理请求,且仅仅支持get、set操作。协议:redis使用自定义的resp协议、同时支持多个数据库并且支持密码认证,而后者仅仅支持文本协议且只有一个默认的数据库。内存管理:redis内存层面各种缓存置换、数据持久化等策略相比后者更加健壮和复杂。8. Redis为什么这么快操作数据活跃于内存:通过内存进行数据操作速度远快于硬盘访问速度。单线程:通过单线程处理所有客户端请求,避免线程上下文切换开销,大大提高的redis的运行效率和响应速度。IO多路复用:以Linux系统为例,redis通过epoll模型实现单线程处理大量客户端并发请求,提升了redis的并发性能。数据结构:redis提供了各种各样的数据结构,并且针对这些数据类型都进行了各种极致的优化,例如哈希对象,在数据大小较小的情况下使用压缩列表,一旦数据大小达到阈值后就会转为哈希集。6.0引入多线程:在高并发场景下,性能的瓶颈往往处于网络连接上,为了进一步提升IO性能,redis通过多线程来充分利用CPU核心处理尽可能多个客户端连接。9. Redis 支持哪几种数据类型比较常见的有:
字符串列表集合有序集合字典需要补充的是redis还有一些高级的数据结构例如:
streambitmapGeoHyperLogLog10. Redis为什么要自己定义SDS这里我们直接引用《redis设计与实现》一书中的说法:
C语言的字符串用\0收尾,在redis的使用场景下,很可能因为这个结束符导致数据被截断。C语言字符串获取长度需要进行遍历,即O(n)级别的时间复杂度。C语言进行字符串拼接总是需要预先做好分配,否则很容易出现缓冲区溢出的问题。对于不断扩大的字符串还需要反复创建新的字符数组解决问题。11. Redis中的Zset是怎么实现的这个问题我们可以针对不同的版本进行回答:
在5.0之前:有序集合在数据体积不是很大的情况下,通过ziplist或dict+skiplist的方式实现有序集合。7.0 之后:完全取消了压缩列表,改为dict+listpack/skiplist。以我们最常用的版本,即5.0左右的版本,本质上redis的有序集合是通过dict保证O(1)级别的直接映射定位,通过跳表实现O(logN)级别的范围有序查询。
12. 什么是GEO,有什么用用于表示地理坐标信息,从而实现经纬度数据检索,它主要支持的命令有: Redis 的 GEO 模块提供了一系列用于处理地理位置数据的命令。以下是一些常见的 GEO 命令:
GEOADD:将一个或多个地理空间元素(经度、纬度和成员)添加到指定的键中。这些命令可以帮助您在 Redis 中高效地存储和查询地理位置信息。
13. 为什么Redis 6.0引入了多线程redis处理能力即qps大约在8w-10w之间,对于某些高并发存在大量客户端连接的请求,本质上可以通过增加实例解决,但是这种做法在资源消耗和成本无疑是非常大的:
经过分析,这些场景大概率性能瓶颈在连接处理上,虽说redis采用epoll等多路复用技术,但epoll本质还是一个同步阻塞IO模型,所以redis增加多个线程,充分利用CPU核心,从而减少网络等待的影响,提升程序执行性能。
14. 为什么Lua脚本可以保证原子性redis针对lua脚本的处理上,会一次性将lua脚本封装成一个单独的事务,从而保证操作的指令执行的原子性但不保证发生错误后的回滚兜底。
15. Redis中的setnx命令为什么是原子性的以下两个原因保证了setnx的原子性:
该指令只有在key不存在时才会插入/单机情况下redis是单线程执行,所以保证执行执行的有序性,间接保证临界资源操作的线程安全。16. Redis 5.0中的 Stream是什么5.0版本新增的数据结构,主要用于处理有序且可追朔的消息流,每个消息都有唯一的id,按照添加顺序进行排序,并且开发人员可以从中添加、读取和删除消息,同时它还是支持让多个消费者并发的处理消息流。 在5.0之前redis通过使用发布订阅模型实现消息队列,但缺点是不支持持久化,如果出现网络断开、redis宕机等情况,就会造成消息丢失。 而stream提供了消息持久化和主从复制功能保证消息不丢失,保证客户端可以访问任何时刻的数据,并且还能记住访问位置。
总的来说,stream有几个几个优点:
有序性多消费者支持持久化支持消息分组17. Redis的虚拟内存机制是什么2.4 之前的版本,redis提供了一种虚拟内存的机制,当内存空间不足时,将部分数据持久化到磁盘上,避免redis进程占用过多的内存。
18. Redis的持久化机制是怎样的rdb:按照协议规范定期生成持久化二进制数据,文件小,恢复速度快,适合做备份和灾难恢复,当然缺点也很明显,定期更新可能导致丢失某一部分的数据。aof:实时完成指令持久化,有着更高的数据可靠性和更细粒度的数据恢复,缺点是文件可能占用空间更多,每次写操作都需要写磁盘导致负载过高。19. Redis 的事务机制是怎样的20. Redis的定期内存淘汰策略是怎么样的redis通过定期删除和惰性删除处理过期key:
定期删除:redis的serverCron函数会每个100ms随机抽检一些key查看是否过期,如果过期则将这些key删除,通过随机抽检保证单线程执行不会阻塞。惰性删除:当用户查询某个key的时候,redis函数会检查该key是否会过期,如果过期则将其删除并返回nil。Redis 的内存淘汰策略用于在内存不足时决定如何移除数据,以确保 Redis 可以继续正常运行。以下是 Redis 支持的主要内存淘汰策略:
noeviction:默认策略,当达到最大内存限制时,任何写入操作都会返回错误(读取操作仍然可以进行)。allkeys-lru:从所有键中使用最近最少使用的算法来驱逐键。volatile-lru:仅从设置了过期时间的键中使用最近最少使用的算法来驱逐键。allkeys-random:从所有键中随机选择键来驱逐。volatile-random:仅从设置了过期时间的键中随机选择键来驱逐。volatile-ttl:优先根据剩余生存时间(TTL)来驱逐键,即 TTL 较短的键会被优先驱逐。这些策略可以在 Redis 配置文件 redis.conf 中通过 maxmemory-policy 参数设置。选择合适的淘汰策略取决于具体的应用场景和需求。例如,如果希望尽可能保留热点数据,可以选择 allkeys-lru 或 volatile-lru;如果希望更公平地处理所有数据,则可以选择 allkeys-random 或 volatile-random。
21. Redis如何实现发布/订阅redis发布订阅是通过pub和sub指令实现的,如果客户端对某个事件感兴趣可以通过sub订阅,这些客户端就会存储到主题的channel中的链表,一旦有发送者用pub消息,channel就会遍历订阅者通知消息。
当然随着stream的出现,可能更多的企业会考虑使用更可靠的stream实现发布订阅。
22. 除了做缓存,Redis还能用来干什么消息队列延迟队列排行版分布式id分布式锁地理位置运用分布式限流分布式session布隆过滤器状态统计共同关注推荐关注数据库23. 为什么ZSet 既能支持高效的范围查询,还能以 O(1) 复杂度获取元素权重值?底层数据结构由字典和调表构成,两者共同维护持有元素指针,当进行键定位时通过字典的哈希算法完成O(1)级别的定位,当需要有序的范围查询时,又可以通过跳表完成O(logN)级别的范围检索定位。
24. 什么是Redis的渐进式rehashredis底层字典本质上是通过数组+哈希算法和拉链法解决冲突,随着时间推移可能会重现大量的链表导致查询性能下降,又因为redis是单线程,为避免哈希表扩容耗时长导致性能下降,redis采用渐进式哈希逐步迁移数据到新表。
对于源码感兴趣的读者可以参考这篇文章:《聊聊redis中的字典设计与实现》
25. Redis中key过期了一定会立即删除吗不一定,serverCron的定时函数会批量抽取一批key进行检查然后删除。
26. Redis中有一批key瞬间过期,为什么其它key的读写效率会降低出现读写效率低,大体是因为主动过期即用户手动提交一批删除过期key的任务,因为redis的单线程的原因,对于瞬时的过期key操作势必出现大量指令需要处理,这时候就会对其他客户端的读写请求造成一定的阻塞,对此我们的解决策略大体有:
设置时间为随机过期采用被动过期设置key,即通过redis ex指令完成27. 什么是Redis的Pipeline,和事务有什么区别redis的pipeline主要为了解决网络延迟的技术,客户端可以一次性批量提交请求,且无需等待每个命令的响应,redis收到这些请求后会依次执行并返回,需要注意的是该操作与事务不同的是它不保证操作处理的原子性,唯一与事务的相同点都是一条指令失败后,后续的指令都还会执行且不会回滚操作。
28. Redis的事务和Lua之间有哪些区别事务和lua之间的相同点是两者都可以保证操作的原子性,不同点是前者一条指令失败不影响后续指令的执行,而后者反之。
29. 为什么Redis不支持回滚本质来说redis支持组队时事务异常回滚,但是不支持执行时异常回滚,设计者针对这种情况也给出相应的原因:
redis的设计初衷就是为了简单、高效,过于复杂的事务实现会让系统复杂并影响性能从使用场景来说,redis本质上就是一个缓存工具,不需要复杂的事务支持redis中出错的问题基本上都是指令不正确,这些问题一般都需要预先解决,而不是依靠事务30. 关于redis中的布隆过滤器布隆过滤器是一种概率性的数据结构,用户快速判断一个元素是否存在于某个集合中,它的特点是:
通过尽可能少的物理空间维护尽可能多的数据的存在情况允许误判(这一点后续会补充)无法进行元素删除对于redis而言实现布隆过滤器的方式有两种:
基于bitmap结合多个哈希函数模拟布隆过滤器引入redis官方的redisBloom模块,对应的操作指令示例如下:我们来一个实际的场景,例如我们要统计系统中千万用户是否在线,我们就布隆过滤器进行记录和维护,整体的流程比较简单:
通过多次哈希运算定位当前用户id对应的布隆过滤器中的位置。定位到bit array将索引i位置标记为1。需要了解的是布隆过滤器在进行哈希的时候是可能存在碰撞的,例如id为1和id为13232的用户可能因为哈希算法导致的bitmap索引位是一样的,所以我们可以得出以下结论:
当布隆过滤器认为数据不存在的时候,它100%不存在。当布隆过滤器认为数据存在的时候,它不一定存在。二、详解Redis持久化机制
1. Redis持久化方式有哪些?有什么区别?持久化分为rdb和aof两种。
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。分别使用命令save或者bgsave。 同时rdb是一个二进制的压缩文件,
以下几个场景会自动触发rdb持久化:
使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点执行debug reload命令重新加载Redis时,也会自动触发save操作默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。而AOF则是以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的,整体工作过程为:
所有的写入命令会追加到aof_buf(缓冲区)中。AOF缓冲区根据对应的策略向硬盘做同步操作。随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩 的目的。当Redis服务器重启时,可以加载AOF文件进行数据恢复。2. rdb和aof各自有什么优缺点?rdb优点:
只有一个紧凑的二进制文件 dump.rdb,非常适合备份、全量复制的场景。容灾性好,可以把RDB文件拷贝道远程机器或者文件系统张,用于容灾恢复。恢复速度快,RDB恢复数据的速度远远快于AOF的方式rdb的缺点:
实时性低,RDB 是间隔一段时间进行持久化,没法做到实时持久化/秒级持久化。如果在这一间隔事件发生故障,数据会丢失。存在兼容问题,Redis演进过程存在多个格式的RDB版本,存在老版本Redis无法兼容新版本RDB的问题。aof优点:
实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。aof缺点:
AOF文件比RDB 文件大,且 恢复速度慢。数据集大的时候,比RDB 启动效率低。3. rdb和aof如何选择如果想达到足以媲美数据库的 数据安全性,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。
当然如果既要保证同步和故障恢复效率,又要尽可能减少数据丢失的概率,也可以考虑混合持久化机制。
4. Redis的数据恢复如何做到的?AOF持久化开启且存在AOF文件时,优先加载AOF文件。AOF关闭或者AOF文件不存在时,加载RDB文件。加载AOF/RDB文件成功后,Redis启动成功。AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。5. Redis4.0的混合持久化持久化将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
三、Redis场景架构设计
1. 缓存击穿、缓存穿透、缓存雪崩问题以及应对策略缓存击穿:要查询的某一个缓存数据刚刚好过期,导致大量查询的请求直接打到数据库上,让数据库处于高负载状态。
解决策略:
加个互斥锁保证单位时间内只有一个请求处理SQL查询并缓存数据。设置热点数据永不过期。缓存穿透:尽管我们将数据库中某些数据换到到内存中,但是若有些攻击者使用一些数据库中不存在的key进行恶意攻击,这时候,所有的查询请求就像穿透了缓存中间件一样直接在数据库中进行查询操作,在高并发场景,这样的攻击就会使得数据压力过大,从而导致数据库被打死
针对缓存穿透问题,对此我们的应对策略有:
使用过滤器,我们可以使用布隆过滤器来减少对数据库的请求,布隆过滤器的原理是将数据库的数据哈希到 bitmap 中(在initialBean阶段将数据缓存到内存中),每次查询之前,借用布隆过滤器的特性(不能保证数据一定存在,但一定能保证数据不存在),过滤掉一定不存在的无效请求,从而避免了无效请求给数据库带来的查询压力。缓存空结果,我们可以把每次从数据库查询的数据都保存到缓存中,为了提高前台用户的使用体验 (解决长时间内查询不到任何信息的情况),我们可以将空结果的缓存时间设置得短一些,例如 3~5 分钟,但是有可能导致数据一致性问题,所以我们建议查询或者更新的时候要对这个类型的缓存上个锁进行进一步的操作。缓存雪崩:大量定时缓存失效或缓存服务器宕机,导致数据库服务器被打死。解决策略:
加锁排队,示例代码如下所示,如果数据库中没有值的话直接上锁到数据库查在放到缓存中,有点类似于单例模式的双重锁校验,但是并发场景性能表现会差一些:某些数据查询一次就被缓存在数据库中,随着时间推移,缓存空间已经满了,这时候redis就要根据缓存策略进行缓存置换。这就造成没意义的数据需要通过缓存置换策略来淘汰数据,而且还可能出现淘汰热点数据的情况。
解决方案:选定合适的缓存置换策略,而redis缓存策略主要分三类。
noeviction (v4.0后默认的):不会淘汰任何过期键,满了就报错,对设置了过期时间的数据中进行淘汰volatile-random:随机删除过期keyvolatile-ttl:根据过期时间进行排序,越早过期的数据就优先被淘汰。volatile-lru:即最近最少使用算法(推荐),redis的lru缓存置换算法相比传统的算法做了一定优化,根据 maxmemory-samples从缓存中随机取出几个key值,然后进行比较在进行淘汰,这样就避免了缓存置换时需要操作一个大链表进行key值淘汰了。volatile-lfu:lru只知晓用户最近使用次数,而不知道该数据使用频率,所以lfu就是基于lru进一步的优化,进行淘汰时随机取出访问次数最少的数据,如果最少的数据有多个,按按照lru算法进行淘汰。但是redis只用8bit记录访问次数,超过255就无法进行自增了,所以我们可以使用lfu-log-factor 和lfu-decay-time来用户访问次数增加的频率。lfu-decay-time:控制访问次数衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。若设置为0,则意味着每次扫描访问次数都会扣减。lfu-log-factor:用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。 全部数据进行淘汰allkeys-random:从所有键值对中使用lru淘汰allkeys-lru:从所有键值对中随机删除allkeys-lfu:从所有键值对中使用lfu随机淘汰3. 基于Redis定位亿级数据假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
我们可以使用 keys 指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
4. 什么情况下会出现数据库和缓存不一致的问题?大体有以下两种情况: 我们先来说说更新数据库,然后更新缓存的情况,如下图所示,线程1和线程2都是先更新数据再更新缓存,由于线程1因为网络波动或者线程调度顺序原因导致后更新缓存,最终导致数据库和缓存不一致,而先更新缓存再更新数据库同理这里就不多赘述:
还有一种情况是针对读场景的,如下所示:
线程2查询缓存发现没有数据,到数据库读取到值10。此时,线程1更新缓存值为20,准备写数据库。线程2将数据库读取到的10写入缓存。线程1将数据库更新为20。自此,缓存不一致问题又出现:
第一次删除避免读请求读到脏数据 第二次删除避免读请求将脏数据写入缓存.
7. Redis如何实现延迟消息通过配置notify-keyspace-events Ex开启过期key事件,再通过程序继承KeyExpirationEventMessageListener监听过期的事件,这种做法的缺点也很明显,即过期的key不一定会立即删除,且该消息没有持久化可能出现丢失。 关于过期key不一定会立即删除的这一点。
通过zset将过期时间作为score,然后key作为member,程序通过计算过期时间差值进行休眠,到期后删除这个key,当然我们需要保证的就是如果有时效更短的key进来注意更新时间。
通过redission内存轮子提交一个任务,原理和方法2差不多,只不过对于并发消费等问题有了较好的优化,且使用更加简单。
8. 如何基于Redis实现滑动窗口限流?滑动窗口本质上就是通过有序集合的方式保证单位时间内保持一定流量数据,避免突然流量突刺的问题,假设我们现在有个接口,希望每秒对应请求控制在2000,对应的落地方案为:
将请求接口作为key。当某个请求到来时,生成唯一id作为member,时间戳作为value。基于当前时间戳减去60s看看60s以内的请求数。查看当前有序集合中元素是否小于2000,如果是则允许新的请求到来。反之不允许。当然我们也可以直接用redisson中的RRateLimiter,它底层本质就是用一个令牌桶算法。
9. 怎么处理热key什么是热Key? 所谓的热key,就是访问频率比较的key。 比如,热门新闻事件或商品,这类key通常有大流量的访问,对存储这类信息的 Redis来说,是不小的压力。 假如Redis集群部署,热key可能会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过 Redis本身能够承受的OPS。
怎么处理热key?
热key处理 对热key的处理,最关键的是对热点key的监控,可以从这些端来监控热点key: 客户端 客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。 代理端 像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。
Redis服务端 使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。
只要监控到了热key,对热key的处理就简单了: 把热key打散到不同的服务器,降低压⼒ 加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询
10. 缓存预热怎么做?所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:
直接写个缓存刷新页面或者接口,上线时手动操作数据量不大,可以在项目启动的时候自动进行加载(我们目前就是执行这种操作,通过继承InitializingBean实现)定时任务刷新缓存.11. 热点key重建问题了解过?你是如何解决的呢?开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。要解决这个问题也不是很复杂,解决问题的要点在于:
减少重建缓存的次数。数据尽可能一致。较少的潜在危险。 所以一般采用如下方式:互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。永远不过期 “永远不过期”包含两层意思:从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期,注意数据更新后要实时加锁更新。从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
四、详解Redis日常运维
1. Redis阻塞问题如何解决(1) API或数据结构使用不合理:通常Redis执行命令速度非常快,但是不合理地使用命令,可能会导致执行速度很慢,导致阻塞,对于高并发的场景,应该尽量避免在大对象上执行算法复杂 度超过O(n)的命令。
对慢查询的处理分为两步:
发现慢查询: slowlog get{n}命令可以获取最近 的n条慢查询命令;发现慢查询后,可以从两个方向去优化慢查询: 1)修改为低算法复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命 令 2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。(2) CPU饱和的问题:单线程的Redis处理命令时只能使用一个CPU,而CPU饱和是指Redis单核CPU使用率跑到接近100%。
针对这种情况,处理步骤一般如下:
判断当前Redis并发量是否已经达到极限,可以使用统计命令`redis-cli-h{ip}-p{port}--stat`获取当前 Redis使用情况如果Redis的请求几万+,那么大概就是Redis的OPS已经到了极限,应该做集群化水品扩展来分摊OPS压力如果只有几百几千,那么就得排查命令和内存的使用(3) 持久化相关的阻塞:对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。
fork阻塞 fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
AOF刷盘阻塞 当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了 数据安全性它会阻塞直到后台线程执行fsync操作完成。
HugePage写操作阻塞 对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
2. Redis大key问题Redis使用过程中,有时候会出现大key的情况, 比如:
(1) 单个简单的key存储的value很大,size超过10KBhash, set,zset,list 中存储过多的元素(以万为单位) 大key会造成什么问题呢?
客户端耗时增加,甚至超时对大key进行IO操作时,会严重占用带宽和CPU造成Redis集群中数据倾斜主动删除、被动删等,可能会导致阻塞(2) 如何找到大key?
bigkeys命令:使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Keyredis-rdb-tools:redis-rdb-tools是由Python写的用来分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成报表用来分析Redis的使用详情。(3) 如何处理大key?
删除大key:当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。 当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。压缩和拆分key:当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。 当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。 当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。3. Redis常见的性能问题和解决方案了解嘛?Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。 尽量避免在压力较大的主库上增加从库。
Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
4. 什么是热Key问题,如何解决热key问题即同一个时间点上,redis中同一个key被大量访问,导致流量过于集中,进而导致服务器资源无法支撑严重情况下可能导致服务瘫痪。 所以,对应的处理策略要针对不同的情况,如果是事前,我们可以根据往年经验识别出对应的热点key,提前扩充实例,做预热缓存。 如果是事中,需要考虑线上进行热点key拆分,多级缓存、增加实例甚至通过限流等策略来解决问题。
5. 如何用Redis实现乐观锁、可重入锁大体可以通过以下几个指令:
watch监视一个或者多个键get查询数据multi开始事务set执行命令exec提交事务而可重入锁通过setnx+incr和decr指令完成上锁和解锁逻辑。
6. Redis实现分布锁的时候,哪些问题需要考虑互斥性能误解锁锁超时锁续命单点故障锁重入网络分区时间漂移7. Redis如何高效安全的遍历所有key加入redis中存在大量的key,使用常规的keys * 请求会导致其他的客户端请求阻塞,所以针对遍历key的需求,我们更建议使用scan,它会以游标的方式分批次迭代键集合,这个概念和游标查询是优点类似的:
这里笔者简单制造了百万级别的热key进行演示:
这里笔者用了keys * 尝试了一下遍历,可以看到耗时约70s,这也就意味则在这70s之间其他的请求是阻塞的:
而使用scan就可以很好的解决问题,通过scan指令从0开始,每次基于上一次的游标进行数据检索获取,通过逐批次的检索和遍历很好的解决keys *的阻塞问题:
虽说scan很好的解决的遍历阻塞问题,但它对于数据实时性的把控不是很好,从上面我可以知道scan指令本质上就是渐进式的遍历,这意味着在扫描过的区间上进行的任何修改操作我们都是无法感知的。