1.Redis是单线程还是多线程?
答:redis6.0版本之前的单线程指的是其网络i/o和键值对读写是由一个线程完成的
redis6.0引入的多线程是指网络请求过程采用了多线程,而键值对的读写仍然是单线程,所以redis依然是并发安全的
也即是说只有网络请求模块和数据操作模块是单线程的,而它的持久化,集群数据同步等,其实是额外的线程执行的。
2.redis单线程为什么还能这么快?
1.命令执行是基于内存的,一条命令在内存里才操作的时间是几十纳秒
2.命令执行是单线程的,没有线程切换的开销
3.基于多路复用机制提升redis的i/o利用率
4.高效的数据存储结构:全局hash表以及多种高级数据结构。比如:跳表,压缩列表,链表等等。
4.redis底层是如何用调表来存储的?
答:调表:将有序链表改造为近似折半查找算法,可以快速进行插入,删除,查找操作。
5.redis设置的key过期了为什么没有释放内存?
答:1.在设置key的时候,设置了过期时间,在key还没有过期的这段时间内,重新设置了这个key的值,没有设置过期时间,那么这个key就永久存在了。
2.redis对于过期的key的处理一般有惰性删除和定时删除两种策略
惰性删除:当读写一个已过期的key时,会触发惰性策略,判断key是否过期,如果过期了直接删掉。
定时删除:由于惰性删除无法保证冷数据及时的被删除掉,所以redis会将设置了过期时间的key放到一个独立的字典中,并对该字典每秒进行10次 的扫描,扫描不会全部扫描,
这里采用的时一种简单的贪心策略。逻辑如下:
a.从过期字典中随机选择20个key
b.删除20个中已过期的lkey
c.如果已过期key的比例超过25%,则重复步骤a。
6.redis中key没设置过期时间,为啥被redis主动删除了?
答:当redis已用内存超过maxmemory限定时,触发主动清理策略
主动清理策略在redis4.0之前,有6中,4.0之后又加了两种共8种。
针对设置了过期时间的key的处理策略:
1.volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后顺序进行删除。
2.volatile-random:在设置了过期时间的key中进行随机删除
3.volatile-lru:会使用LRU算法筛选设置了过期时间的键值对进行删除
4.volatile-lfu:会使用lfu算法
针对所有key进行处理
5.allkeys-random:从所有键值对中随机选择并删除数据
6.allkeys-lru:使用LRU算法在所有数据中删除
7.allkeys-lfu:使用lfu算法
8.noeviction:不会剔除任何数据,拒绝所有的写入操作,并返回客户端信息错误“OOM command not allowed when used memory”,此时redis只响应读操作。
LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来!
7.redis淘汰key的算法LRU与LFU
LRU算法:(Least Recently Used 最近最少使用),淘汰很久没被访问的数据,以最近一次访问时间作为参考)
LFU算法:(Least Frequently Used 最不经常使用),淘汰最近一段时间访问次数最少的数据,以次数作为参考。
绝大多数采用LRU策略,当存在大量热点缓存数据时,LUF可能更好
标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。
近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通过maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。
LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。
LFU算法正式用于解决上述问题,LFU(Least Frequently Used)是Redis4新增的淘汰策略,它根据key的最近访问频率进行淘汰。LFU在LRU的基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出内存。如果两个数据的访问次数相同,LFU再比较这两个数据的访问时间,把访问时间更早的数据淘汰出内存。
8.redis的主从同步是如何实现的?
从版本2.8开始。redis使用psync命令来完成主从数据同步,同步过程分为全量复制和部分复制,全量复制一般用于初次复制的场景,部分复制用于处理网络中断等原因造成的数据丢失的场景,psync命令需要以下参数的支持:
1.复制偏移量:主节点处理写命令后,会把命令长度做累加记录,从节点在接收到写命令后,也会做累加,从节点会每秒上报一次自身的复制偏移量给主节点,而主节点会保存从节点的复制偏移量
2.挤压缓冲区:保存在主节点上的一个固定长度队列,默认为1m,当主节点有连接的从节点时被创建,主节点处理写命令时,不但会把命令发送给从节点,还会写如挤压缓冲区,缓存区是先进先出的队列。可以保存最近已复制的数据,用于部分复制和命令丢失时的数据补救。
3.主节点运行id:每个redis节点启动之后都会动态分配一个40位的16进制字符串作为运行id,如果使用ip和端口的方式标识主节点,那么主节点重启后变更了数据集(RDB/AOF),从节点再基于复制偏移量复制数据是不安全的,因此当主节点的id变化后,从节点将做全量复制。
psync命令的执行过程及返回结果,如下图:
a,若回复+FULLRESYNC,则从节点将触发全量复制;
b,若回复+CONHTINUE,则从节点触发部分复制;
c,若回复-err,说明主节点版本过低,无法识别psync命令
9.如何实现redis的高可用?
实现redis的高可用主要有哨兵模式和集群两种方式:
哨兵:
redis sentinel 是一个分布式架构,它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点和其余哨兵节点进行监控,当发现节点不可达时会对节点做下标识。如果被标识的是主节点,它就会与其他哨兵节点进行协商,当多数哨兵节点认为主节点不可达时,他们会选举一个哨兵节点来完成自动故障转移的工作,同时还会将这个变化实时的通知给应用方,整个过程是自动的,不需要人工介入,有效解决了redis的高可用问题。
哨兵模式的特征:
1.会定期监控数据节点,其他哨兵节点是否可达
2.会将故障转移的结果通知给应运方
3.可以将从节点晋升为主节点,并维护后续的主从关系
4.哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点的信息
5.节点的故障判断是由多个哨兵共同完成的,可以防止误判
6.哨兵节点集合是由多个哨兵节点组成的,即使个别节点不可以,整个集合依然是健壮的
7.哨兵节点也是独立的redis节点,他们不存储数据,只支持部分命令
集群:
redis集群采用虚拟槽区分来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC165(key)&16383,每一个节点负责维护一部分槽及槽所映射的键值数据,
虚拟槽区分具有的特点:
1.解耦数据和节点之间的关系,简化了节点扩容和收缩的难度
2.节点自身维护槽的映射关系,不需要客户端或代理服务维护槽分区元数据
3.支持节点,槽,键之间的映射查询,用于数据路由,在线伸缩等场景。
10.缓存穿透,缓存击穿,缓存雪崩有什么区别,该如何解决?
缓存穿透:
问题描述:客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机。出现这种状况的原因,可能是业务层将缓存和库中的数据删除了,也可能是人为恶意攻击,专门访问不存在的数据。
解决方案:1.缓存空对象,存储层未命中后,仍将空值存入缓冲层,客户端再次访问数据时,缓冲层会直接返回空值。
2.布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在直接返回空值。
缓存击穿:
问题描述:一份热点数据,它 的访问量非常大,在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
解决方案:1.永不过期:热点数据不设置过期时间,这是物理层上的永不过期。或者为每个数据设置逻辑过期时间,当发现数据逻辑过期时,使用单独的线程重新缓存。
2.加互斥锁:对数据的访问加互斥锁,当一个线程访问数据时,其他线程只能等待,这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中获取
缓存雪崩:
问题描述:在某一时刻缓存层无法继续提供服务,导致所有的请求直达存储层,导致数据库宕机。可能时缓存中大量数据同时过期,也可能是redis节点发生故障,导致大量请求无法的到处理。
解决方案:1.避免数据同时过期:设置过期时间时,附加一个随机数,避免大量key同时过期。
2.启用降级和熔断措施:在发生雪崩时,若访问的数据不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给redis,而是直接返回。
3.构建高可用的redis服务:采用哨兵或集群模式,部署多个redis实例,个别节点宕机,依然可以保持服务的整体可用。
11.redis的持久化策略
redis支持rdb持久化,aof持久化,rdb-aof混合持久化三种
RDB:redis DataBase,是redis默认采用的持久化方式,它以快照的方式将数据持久化到硬盘中,rdb会创建一个经过压缩的二进制文件,以.rdb结尾,内部存储了各个数据库的键值对数据等信息,rdb持久化的触发方式有两种:
1.手动触发;通过SAVE和BGSAVE命令触发rdb持久化操作,创建.rdb文件;
2.自动触发:通过配置选项。让服务器在满足指定条件时自动执行BGSAVE命令。
其中SAVE命令执行期间,redis服务器将阻塞,知道.rdb文件创建完毕为止,而BGSAVE命令是异步版本的SAVE命令,它会使用redis服务器进程的子进程,创建.rdb文件。BGSAVE命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求。总之,BGSAVE命令是针对SAVE阻塞问题做的优化。redis内部所有涉及RDB的操作都采用BGSAVE的方式,而save命令已废弃
RDB持久化的优缺点如下:
优:RDB生成紧凑的压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快
缺:BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,不宜频繁执行
所以RDB持久化没办法做到实时的持久化
AOF:
Append Only File,解决了数据持久化的实时性,是目前redis持久化的主流方式,aof以独立日志的方式,记录了每次写入命令,重启时重新执行aof文件中的命令来恢复数据。工作流程为:
命令写入(append),文件同步(sync),文件重写(rewrite),重写加载(load)
aof以文本协议格式写入命令,eg:
*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
文本协议的优点:
1.具有很好的兼容性
2.直接采用文本协议格式,可以避免二次处理的开销
3.文本协议具有可读性,方便直接处理和修改
AOF持久化的同步机制:
为了提高程序的写入性能,现代操作系统会把针对磁盘的多次操作优化为一次操作
1.当程序调用write对文件写入时,系统不会直接写入硬盘,而是写入缓冲区
2.当达到指定时间周期或缓冲区写满时,系统才会执行flush操作,将缓冲区中的数据洗至磁盘中
这种优化机制虽然提高了性能,但是给程序的写入带来了不确定性
1.对于aof这样的持久化功能来说,冲洗机制将直接影响aof持久化的安全性
2.为了消除不确定性,redis向用户提供了appendfsync选项,来控制系统冲洗aof的频率;
3.linux的glibc提供了fsync函数,可以将指定文件强制从缓冲区刷到硬盘,上诉选项正是基于此函数
AOF的优缺点:
优:与RDB持久化可能丢失大量数据相比,aof持久化安全性更高,通过everysec选项,用户可以将数据丢失的时间窗口限制在1秒以内
缺:文本协议比二进制大得多,aof需要通过执行aof文件中的命令来恢复数据库,其恢复速度比rdb慢,aof在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器短暂阻塞。
RDB-AOF混合持久化:
从4.0开始引入,这种模式是基于aof持久化构建而来的,用户可以通过配置文件的aof-user-preamble yes配置项开启aof混合持久化,redis服务器在执行aof重写操作时,会按如下原则处理数据:
1.像执行BGSAVE一样,根据数据库当前状态生成对应的rdb数据,并写入aof中
2.对于重新之后执行的redis命令,则以协议文本的方式追加到aof文件的末尾,即rdb数据之后
通过使用混合持久化,用户可以同时获得rdb持久化和aof的优点,服务器可以通过aof文件包含rdb数据来实现快速的数据恢复操作,又可以通过aof文件包含的aof数据来将丢失数据的时间窗口设置在1s内。
12.redis线上数据如何备份?
1.写crontab定时调度脚本,每小时都copy一份rdb或aof到另外一台机器中去,保持最近48小时的备份
2.每天都保留一份当日的数据备份到一个目录中去,可以保留最近一个月的
3.每次copy的时候,都把太旧的备份给删了
13.如何保证缓存与数据库的双写一致性?
四种同步策略:
要想保证缓存与数据库的双写一致,有四种:
1.先更新缓存,在更新数据库
2.先更新数据库,再更新缓存
3.先删除缓存,再更新数据库
4.先更新数据库,再删除缓存
问题1:更新与删除缓存哪种方式更合适?
更新:
优:每次数据变化都及时更新,所以查询时不容易出现未命中的情况
缺:更新缓存消耗大,数据需要经过复杂的计算再写入缓存,频繁的更新操作就会影响服务器的性能,如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据
删除:
优:操作简单,无论更新是否复杂,都将缓存中数据删除
缺:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库
从上面的比较来看,一般情况下,删除缓存是更优的方案。
问题2:先操作数据库还是先操作缓存?
在两步操作都正常的情况下:
a:删除缓存,再更新数据库
1.进程A删除缓存
2.进程B读取缓存失败
3.进程B读取数据库成功,得到旧的数据库
4.进程B将旧的数据更新到缓存
5.进程A将新的数据更新到数据库
最终:数据库和缓存二者数据不一致
b:更新数据库,再删除缓存:
1.进程A更新数据库
2.进程B查询缓存成功
3.进程A删除缓存
可见最终缓存和数据库的内容时是一致的虽然B读到了旧的数据,但是这两步的执行速度快影响不大
最终结论:先更新,再删除缓存是影响更小的方案。
当然如果根据实际情况不得不使用先删除,再更新,则可以通过延时双删的策略解决,具体如下:
1.删除缓存;
2.更新数据库;
3.sleepN毫秒;
4.再次删除缓存;
阻塞一段时间后,再次删除缓存,就可以把这个过程中缓存的不一致数据删掉。
如果是读写分离的结构:
进程A先删除缓存,再更新数据库,然后主同步到从,而在同步之前,可能会有进程B访问了缓存,当发现数据不存在时,会从数据库访问,然后同步到缓存,这样也会导致不一样,这个问题解决方案依然时采用双删的策略,但是在评估延长时间的时候,要考虑主从数据库同步的时间。
第二次删除失败了则么办?
依然增加重试的次数,但次数要有限制,超出限制后要采用报错,记日志,发邮件提醒等措施。
如果两步中出现失败时,无法判断哪个更好。出现失败时采用重试机制解决。
以先更新,再删除为例:
1.更新数据库成功;
2.删除缓存失败;
3.将此数据加入消息队列;
4.业务代码消费这条数据;
5.业务代码根据这条消息的内容,发起重试机制,即从缓存中删除这条记录。