redis理论和原理
- redis的特性?
- 支持丰富的数据类型,包括string、list、hash、set、sorted set类型;
- 支持事务操作,多个操作也支持原子操作,使用multi和exec、dicard命令实现;
- 支持持久化,可以将内存中数据存入到磁盘,待恢复后读入内存;
- redis的读写操作是单线程,采用IO多路复用模型;
- redis支持发布订阅模式;
- 有官方的cluster集群方案;
- redis高性能原理?
- 基于内存的数据库,对数据的操作在内存中进行;
- 基于非阻塞IO复用的单线程模型:(1)redis采用单线程的非阻塞IO复用模型。单线程可以减少线程切换的性能消耗,减轻多线程并发带来的加锁消耗;非阻塞的IO复用模型核心是,非阻塞是指会有一个监听程序或者队列,将事件入队后,在调用对应的回调函数来处理。(2)Redis会fork一个写入磁盘的线程进行异步操作,不会影响redis处理数据。
- 高效的数据结构和数据编码;
- redis的单线程非阻塞IO复用模型?
由于redis的瓶颈在网络IO上,所以采用单线程可以减少线程间切换带来的吸能损耗,同时由于redis的网络通信模型是IO复用模型,提高了redis网络通信的效率。redis网络通信的原理如下:
- Redis client请求redis server,会通过IO复用程序dispatcher来接收,接收后了就会为其创建一个sockect用于通信,并按照时间类型为其分配对应的处理器。
- 分配的特定处理器为其处理好数据后,通过连接好的socket将数据返回给redis client;
- redis高可用方案?
利用redis集群模式。
redis的集群就会涉及集群间数据的同步,集群先定时全量同步RDB数据,后面宕机后同步AOF数据。
- redis的事务?
redis的事务本质上是一系列命令的集合同时串行化执行,将需要执行的命令放入到队列中,待指令下发一起执行。主要通过以下命令来实现:
- multi:开启事务,后续的命令都会被加入到队列中;
- exec/diacard:exec执行刚才加入队列的命令;discart取消要执行的事务;
- watch/unwatch:watch执行监视加入队列的key值是否被修改,如果被修改了就会回滚;unwatch取消监视;
- 如何利用lua脚本来保障redis的原子性?
redis会将lua脚本经过解释器来执行所有命令,将这些命令作为一个整体加入到队列中,当在完整执行这些命令前不会执行其他的命令,通过这种方式来保障redis的事务性。
- redis的持久化机制?
包括RDB和AOF机制:
- RDB是全量快照,redis会隔一段时间全量保存内存中的数据到磁盘;RDB是全量持久化,可以自动也可以手动调用Save命令持久化,其原理是fork一个bgsave的系统调用,将数据持久化到磁盘。
- AOF是每隔一段时间进行增量更新;AOF是redis的增量保存的方式,每次对redis的命令都会被追加到AOF文件中。这种持久化方式保存的文件大小较大。
总结:
- RDB是全量备份,保存的是二进制文件,适合全量备份;AOF适合增量备份,可以做到比较好的实时性;
- RDB会fork一个专门的写入线程,比较消耗性能;AOF只会用到append命令;
Redis专题:万字长文详解持久化原理:
Redis专题:万字长文详解持久化原理 - 码路印记 - SegmentFault 思否
- redis过期策略(expire strategy)有哪些?
过期策略是key值设置了过期时间,如何在到了过期时间使得key值失效。过期策略一般有两种:
- 定时删除,每隔一段时间都会查询设置了过期时间的key值,如果碰到key值过期了就删除;
- 惰性删除,就是等到要查询的时候再删除;
redis实际实现了以上两种过期策略,redis默认100ms就会定时随机查询key值是否过期,但不是全量查询,如果查询到的key值过期了就会删除,没有被删除的,等到下一次用到了,如果过期了,会惰性删除,即不会返回这个值。
- redis内存淘汰策略有哪些?
内存淘汰策略是当redis数据占用内存过大,讨论如何老的数据为新的数据腾空间。淘汰策略的制定包括两个维度,一个是是否对全量key值,岭南一种是对设置了过期时间的key,分别是对allkey还有对volatile。另一类是key值使用情况,其中包括最近最少使用LRU、最近失效TTL,随机Random。以及永不回收策略no-enviction。
- allkey-lru:从所有数据中找出最近最少使用的key值进行过期;
- allkey-random:从所有数据中随机找到一些key值进行过期;
- volatile-lru:从设置了过期时间的数据中找到最近最少使用的key值进行过期;
- volatile-ttl:从设置了过期时间的数据中找到最近就要过期的key值进行过期;
- volatile-random:从设置了过期时间的数据中随机找到一些数据进行过期;
- no-ennviction:不进行过期淘汰策略;
- redis怎么实现LRU算法?
在使用lru淘汰策略时候会使用lru算法。其实现是通过:redis的数据结构中有lru这个属性,会记录该数据最近被访问的时间戳。redis中采用的是一种近似的LRU算法,随机选出N个值,按照访问时间进行排序,淘汰不经常使用的key值。好处是不需要获取一个全量key值得集合,使用近似算法来实现LRU。这是个近视算法。
- redis的数据结构和编码类型是什么?
redis的5中基本数据类型包括:string、list、hash、set、sorted set,常见的底层结构如下:
- String的底层的数据结构是简单动态字符串(sds),拥有一个header和buf,其中header会保存buf长度,buf保存实际数据。这样的好处是:(1)能够在O(1)内获取buf数据的长度;(2)并且下次扩容的时候不会内存溢出;(3)可以存储非文本的数据类型,内容安全;
- List的底层数据结构是一个快表(quickList),快表是一个链表,快表拥有一个特殊节点,保存头节点和尾节点以及快表中节点信息,每个数据节点中包括一个指向ziplist的指针。
- Hash的底层结构是压缩列表(zipList)和hashtable,他是一个双端二进制的哈希表,同时每个元素的长度都不一样,和实际内容一样,用一个len来保存实际长度,这样保存比较节省内存空间;节省内存的原因:(1)相比普通list每个节点的大小相同,ziplist保存的实际数据的二进制编码数据,通过prelen字段来定位上一个entry的位置;(2)hashtable是一种数组加链表的数据结构,和java中类似。
- Set的底层数据结构是intSet;
- SortedSet的底层物理结构是一个跳表(zSkipList),跳表是一种用空间换时间的数据结构,通过增加多级索引,可以快速找到相应的数据。
总结:sorted set的实现为什么不用平衡树(AVL、红黑树)?
跳表和红黑树都是长于排序和查找的数据结构。
- 跳表在进行区间范围查找的算法复杂度更低。
- 在并发较高的常见下,平衡树可以通过改变索引的构建策略达到较好的效果,而红黑树通过旋转和着色效率较慢。
- 红黑树等平衡树的实现更难。
Redis进阶 - 数据结构:底层数据结构详解:
Redis进阶 - 数据结构:底层数据结构详解 | Java 全栈知识体系
- 为什么redis既支持高效的范围查找,也支持O(1)复杂度的查找操作?
Sorted set的数据结构能支持范围查找是因为数据结构中有跳表zskiplist ,能支持O(1)复杂度的查找操作是因为通过hash表dict 进行索引。
1. typedef struct zset 2. { 3. dict *dict; 4. zskiplist *zsl; 5. } zset;
redis实践
- redis主从复制的方式?
首次增量复制
- 建立连接:从节点通过replicaOf命令向主节点请求建立连接;
- 复制数据:主节点通过命令bgSave生成RDB文件,生成好的RDB直接传递到从节点上,在执行bgsave命令过程中的命令会暂时放在replica buffer中;
- 同步新增命令:将主节点执行bgsave命令过程中生成的命令传给从节点。
过程中基于长连接的命令同步
主从节点第一次同步好了后,主从节点就会连接TCP的长连接,每次主节点收到的命令会同步到从节点上。
再次连接的增量复制
对于网络中断,长连接断开后再次进行连接,需要进行增量同步。但是也和replica_backlog_buffer的大小有关,如果长时间从节点失去同步,新数据已经不在buffer中了,就需要进行全量同步了。
主从复制是怎么实现的?:
https://xiaolincoding.com/redis/cluster/master_slave_replication.html#第一次同步
- redis进行数据缓存的方案?
cache aside模式(缓存旁路模式)
- 当进行读操作时,先去查询缓存,如果缓存有则用缓存中数据,如果缓存中没有则查询数据库,并将更新后的数据回写到缓存中;
- 当进行写操作时,更新完数据库后,删除缓存中的数据;
总结:(1)至于为什么要先操作数据库再操作缓存是因为数据库写入操作的时间远长于缓存写入数据,数据库操作成功了才操作缓存;(2)为什么更新完数据库后删除缓存中的值原因是删除缓存相比更新缓存的值,在高并发情况下不容易多个并发操作导致脏数据问题;(3)对于写多读少的场景,频繁更新缓存没有意义。
read/write through模式(读写穿透模式)
客户端不直接和缓存、数据库直接交互,会通过一层缓存代理来交互,所有客户端的操作和缓存代理进行交互。
- 当进行读操作时,会首先请求到缓存代理,缓存代理去请求缓存,缓存中有则直接返回,缓存中没有再去请求数据库,再回写到缓存中,通过缓存代理将值返回给客户端;
- 当进行写操作时,会首先请求缓存代理,缓存代理会首先更新数据库,再更新缓存;
总结:相对于cache aside模式,read/write through是将缓存和数据库当做一个服务来对待,客户端直接和该服务进行交互。
write behind 模式(异步缓存写入)
就是先对cache进行操作,再异步更新到数据库,这种模式适合于并对并发性能要求比较高,但是对实时性要求不高的场景。这个是借鉴数据库的WAL模式,就是先写日志再落库。一般是实现是先写入MQ中,后续通过MQ对缓存进行写的操作。
缓存更新的套路:缓存更新的套路 | 酷 壳 - CoolShell
- 数据不一致的解决方案?
- 可以选择合适的缓存更新策略,就是更新数据库后让缓存失效;
- 同时增加延迟双删;
- 增加错误任务重试、任务确认、异步重试等机制;
- 增加缓存同步服务,订阅数据库变更日志,结合消息队列的可靠性操作缓存;
- 增加缓存key值的失效时间,给每个key值设置失效时间,这样能保证一段时间后的数据一致;
- 启定时任务同步缓存和数据库中的数据;
- 缓存雪崩的解决方案?
问题说明:
缓存雪崩指的是key值设置相同的失效时间会导致key值在同一时间失效,这样全部都没有命中key,就直接落在了数据库上,加重了数据库的负担。
解决方案:
- 解决这个问题的方法就是给key的失效时间设置为固定时间+随机时间。
- 如果是分布式环境,可以将热点数据分布到不同的分片上。
- 缓存击穿的解决方案?
问题说明:
缓存击穿是指短时间类请求大量没有缓存在redis中的key值,导致大量请求短时间都落到数据库上,造成很大的压力。
解决方案:
- 对于访问的热点数据进行数据预热或者定时异步同步,这样应用启动热点数据就在redis中,还可以对热点数据设置较长过期时间,这样请求就不会直接打到数据库上;对于设置了过期时间的key,可以在快失效的时候再异步同步。
- 设置互斥锁,进行加锁操作,减少一次对数据库的并发访问,但一个请求访问redis中key值不存在的时候,先将该key值的请求锁定,待该请求访问了数据库后再释放该锁,保障对该key的访问都是串行化。
- 缓存穿透的解决方案?
问题说明:
缓存穿透是查询的key值在redis和数据库中都没有。
解决方案:
- 对于海量请求的key值重复比较多的情况,可以在在数据库中查询不到的key值情况下,写入redis对应的value为null或默认值,同时设置超时时间,这样都会打到redis上,但是查询结果为null或默认值。
- 利用布隆过滤器来实现:布隆过滤器是利用bitMap来实现的,他的作用就是能够快速返回key值是否在这个set里面,如果请求的key没有在set里面,他就马上返回;
总结:
缓存雪崩是redis的key同时失效,缓存击穿是redis的key就没有,直接打到数据库上,缓存穿透就是redis和数据库中都没有。
什么是缓存雪崩、击穿、穿透?:
https://xiaolincoding.com/redis/cluster/cache_problem.html#缓存雪崩
- redis解决热点key问题?
问题说明:
就是缓存中有些key会被短时间内频繁访问,比如热点秒杀的商品,这样就有可能拖垮redis服务分片。
解决方案:
总体思路是用空间换时间。
- 进行缓存预热,保障刚开始的时候缓存中和数据库中数据一致。
- 为热点key设置过期时间,过一段时间key值会转移到其他服务分片上,降低对特定分片的长时间访问。
- 为热点key添加后缀,将热点数据尽量分配到不同的分片节点上,降低对某一节点的访问负载。
- redis解决big key问题?
问题说明:
就是redis中key对应的value太大或者太多,太大一般指一个value大于1MB,太多指的是set或者zset中的元素超过10000个。
解决方案:
- 对于value值比较大的key在业务上进行拆分,拆分成比较小的多个value;对于是value元素个数比较多的情况,可以考虑进行分片,将其分布到集群中不同的节点上。
- 设置合理的TTL失效时间,避免big key一直增大对内存造成较大压力。
- 手动或者定时任务查找出big key,进行手动删除。
- redis的应用场景?
- 可以做分布式缓存;保存token数据和热点数据等;
- 可以做消息队列,利用list这个数据结构,lpush一个消息进来,rpop一个消息出去,如果需要阻塞的话,可以用blpush和brpop方法;
- 做分布式锁:利用setnx来进行加锁,这个命令的意思是如果key值不存在他就返回1,如果存在就返回0,同时要设置失效时间,但是在setnx和expired之间可能机器宕机,所以需要设置为原子操作;
- redis实现分布式锁?
参考《redis分布式锁》
- 如何将一个对象存储到redis里面去?
- 利用redis的数据结构来存储;
- 就是将对象转化为json格式存储到redis里面去;
LRU原理和Redis实现:LRU原理和Redis实现——一个今日头条的面试题 - 知乎
- redis和zookeeper实现分布式锁的区别?
redis和zookeeper都可以实现分布式锁,redis可以通过setnx/set命令来实现分布式锁,其中zookeeper通过创建znode和监控器来是实现分布式锁。他们的区别如下:
- redis实现分布式锁需要通过其他线程轮训锁是否是否,比较消耗性能;
- zookeeper创建一个znode,创建一个监听器,当锁消除后通知其他线程,资源占用更少;
参考资料:
20道经典Redis面试题:
20道经典Redis面试题_CSDN砖家的博客-CSDN博客
- redis和memcached的区别?
redis和memcached都是常见的缓存数据库,他们的区别主要包括:
- redis的数据结构更丰富,包括String、List、Map、Set、Sorted Set等,但memcached只支持简单的kv类型;
- redis可以进行数据持久化,将内存数据持久化到磁盘,包括RDB和AOF两种类型,memcached暂时没有持久化机制;
- redis是通过单线程进行数据的读写操作,memcached是通过多线程进行读写操作,对于单核常见redis会更快,但对于多核场景可能memcached性能更高;
- redis有比较完善的内存管理机制,包括失效机制还有内存淘汰机制等,memcached暂没有相关特性;
- redis如何实现延时消息?
方案一:
采用redis原生数据结构zset来实现,在zset中每个member关联了score,并且可以根据score进行排序,我们将延时时间放在score中,再根据score排序,同时启动一个异步任务来判断当前时间和zset中score的大小,如果超过当前时间则可以执行延时任务了。
方案二:
采用第三方框架Reddisson,可以定义一个分布式延迟队列RDelayedQueue,他底层也是由zset来实现的,将数据和超时时间放入到队列中,同时会启动一个延时任务,在时间到期后自动来从zset中获取数据,