Redis是单线程还是多线程
- Redis6.0版本之前的单线程指的是其网络IO和键值对读写是由一个线程完成的
- Redis6.0引入的多线程是指网络请求过程IO采用了多线程,而键值对读写命令仍然是单线程处理的,所以Redis仍然是并发安全的
- Redis6.0之前,只有网络IO和数据操作模块是单线程的,其他的持久化,集群数据同步等,其实是由额外的线程执行的。
Redis单线程快的原因
- 命令执行是纯内存操作
- 核心是基于非阻塞的IO多路复用机制,提升了Redis的IO利用率
- 命令执行是单线程,反而避免了多线程频繁的上下文切换带来的性能
- 高校的数据存储结构:全局Hash表以及多种高效数据结构,比如:跳表、压缩列表、链表等等。
Redis的持久化机制
RDB:Redis DataBase 将某个时刻的内存快照(snapshot),以二进制的方式写入磁盘
- 手动触发:
- save命令,使Redis处于阻塞状态,知道RDB持久化完成,才会响应其他客户端发来的命令,在生产环境下不推荐用
- bgsave命令,fork出一个子进程执行持久化主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了。 (主进程操作数据时涉及到copyonwrite读写分离的机制)
- 自动触发
- save m n, 在m秒内,如果有n个健发生改变则自动触发持久化,通过bgsave执行,如果设置多个,只要满足其一就会触发,配置文件有默认配置
- flushall, 用于清空redis所有的数据库,flushdb是清空当前数据库( 默认是0号数据库),会清空RDB文件,同时也会生成dump.rdb,内容为空
- 主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点
优点:
- 整个数据库只包含一个dumb.rdb文件,方便持久化。
- 容灾性好,方便备份
- 性能最大化,fork子进程来完成写操作,让主进程处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行IO操作,保证了redis的高性能。
- 相对于数据集大时,比AOF的启动效率高。
缺点:
- 数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此如果当数据集较大时,可能会导致整个服务停止几百毫秒,甚至是几秒钟,会占用cpu。
AOF: Append Only File 以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会被记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘。
- 所有的写命令会追加到AOF缓冲中。
- AOF缓冲区根据对应的策略向硬盘进行同步操作。
- 随着AOF文件越来越大,需要定期重写AOF文件,以达到压缩的目的。
- 当Redis重启时,可以加载AOF文件进行数据恢复。
同步策略:
- 每秒同步:异步完成,效率非常高,一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会被丢失
- 每修改同步:同步持久化,每次发生的数据变化都会被记录到磁盘中,最多丢一条。
- 不同步:由操作系统控制,可能丢失较多数据
优点:
- 数据安全
- 通过append模式写文件,即使中途服务器宕机也不会损坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题
- AOF具有rewrite模式,定期对AOF文件进行重写,以达到压缩的目的
缺点:
- AOF文件比RDB文件大,且回复速度慢。
- 数据集大的时候,比RDB启动效率低。
- 运行效率没有RDB高
RDB和AOF两者对比?
- AOF比RDB更新频率高,优先使用AOF还原数据
- AOF比RDB更安全
- RDB性能更好
- 优先AOF
简述redis主从同步机制
为了提高Redis的吞吐量以及保证高可用
- 从节点执行slaveof masterip port, 保存主节点信息。
- 从节点中的定时任务发现主节点信息,建立和主节点的socket连接。
- 从节点发送信号,主节点返回,确认两边能互相通信。
- 连接建立后,主节点将所有数据发送给从节点(数据同步)
- 主节点把当前的数据同步给从节点后,便完成了复制过程(全量)。接下来,主节点会持续的把写命令发送给从节点(增量),保证主从一致
runld: 每个redis节点启动都会生成唯一的uuid,每次redis重启后,runld都会发生变化。
offset: 主从节点各自维护自己的复制偏移量ofset,当主节点有写入命令时,offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点。主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致。
repl backlog_size: 缓冲区,保存在主节点上的一个固定长度的先进先出队列,默认大小是1MB。
主从同步流程图如下:
全量复制:
- 从节点发送psync命令,psync runid offset (由于是第一次,runid为? ,offset为-1)
- 主节点返回FULLRESYNC runld offset,runld是主节点的runld,offset是主节点目前的offset。从节点保存信息
- 主节点启动bgsave命令fork子进程进行RDB持久化
- 主节点将RDB文件发送给从节点,到从节点加载数据完成之前,写命令写入缓冲区
- 从节点清理本地数据并加载RDB,如果开启了AOF会重写AOF
增量复制(部分复制):
- 复制偏移量:psync runid offset
- 复制积压缓冲区:当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制 (到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
说一下redis的高并发、高可用(集群)方案
解决单体故障且访问有限的问题
-
主从复制:单节点的Redis并发能力是有上限的,搭建主从集群,实现读写分离,主节点负责写数据,从节点负责读数据。
-
哨兵模式:
sentinel,哨兵是 redis 集群中非常重要的一个组件,主要有以下功能:
- 集群监控: 负责监控 redis master 和 slave 进程是否正常工作。
- 消息通知: 如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。.
- 故障转移: 如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心: 如果故障转移发生了,通知 client 客户端新的 master 地址。
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个 master node 是否宕机了,需要大部分(最好超过Sentinel实例数量的一半)的哨兵都同意才行,涉及到了分布式选举。
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
- 哨兵通常需要 3 个实例,来保证自己的健壮性。
- 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
-
Redis Cluster分片集群
使用分片集群可以解决如下问题:- 海量数据存储问题
- 高并发写的问题
特征如下:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过
ping
监测彼此健康状态 - 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
Redis分片集群中数据是怎么存储和读取的?
Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16
校验后对16384取模来决定放置哪个槽,集群的每个节点负责每一部分hash槽。
读写数据:读取key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身作为有效部分)余数作为插槽,寻找插槽所在的实例。
简述Redis集群脑裂,该怎么解决呢?
集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新的master同步数据,就会导致数据丢失。
解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量数据丢失。
简述Redis事务实现(区别于Mysql)
简单地说,事务表示一组动作,要么全部执行,要么全部不执行。
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令表示事务开始,exec命令表示事务结束,discard命令是回滚。
Redis事务原理:
事务是在Redis服务器端的行为,用户执行MULTI命令时,服务器会将对应的这个用户的客户端对象设置为一个特殊的状态,在这个状态下后续用户执行的查询命令不会真的被执行,而是将服务器缓存起来,直到用户执行exec命令为止,服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。
需要注意的是,Redis的事务功能很弱,在事务回滚机制上,Redis只能对基本的语法错误进行判断。
- 语法命令错误,如将set写成了sett,属于语法错误,会造成整个事务无法执行,事务内的操作都没有执行
- 运行时错误,Redis并不支持回滚功能
简述redis数据结构
String:字符串
List:列表
Hash:哈希表
Set:无序集合
Sorted Set:有序集合
Bitmap:布隆过滤器
GeoHash:坐标,借助于Sorted Set实现,通过zset的zscore进行排序就可以得到坐标附近的其他元素,通过将score还原成坐标值就可以得到元素的原始坐标
HyperLogLog:统计不重复数据,用于大数据基数统计
Streams:内存版的kafka
缓存雪崩、缓存穿透、缓存击穿
缓存雪崩
指的是缓存在同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短期内承受大量请求而崩掉。(重启+初始化时)
解决方案:
- 缓存数据的过期时间设置为随机,防止同一时间大量数据过期现象发生。
- 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存失效,则更新缓存。(比较消耗性能)
- 缓存预热:将热点数据放进缓存中去,再启动服务
- 互斥锁:让请求排队
缓存穿透
指的是缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。(来自于攻击)
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验(id<=0的直接拦截);
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写成key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
- 采用布隆过滤器,将所有可能存在的数据哈希放到一个巨大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
缓存击穿
指的是缓存中没有但是数据库中有的单个数据,这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库中读取数据,引起数据库压力瞬间增大。(一般是缓存时间到期)
解决方案
- 设置热点数据永不过期
- 加互斥锁
注意:缓存击穿和缓存雪崩的区别——缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
Redis的Key过期了为什么内存还没释放? Redis处理过期key的删除策略
如果重复使用set命令修改key的值,而没有加上过期时间的参数,那么这个key的过期时间将会被擦除,导致数据永远不过期。
Redis处理过期key的删除策略如下:
- 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,如果过期了直接删除这个key
对CPU友好,对内存不友好
- 定时删除:由于惰性删除策略无法保证冷数据被及时删除,所以Redis会定期(默认每100ms)主动淘汰一批已过期的key,但是也有可能会出现key已经过期但是还没被清理的情况,导致内存没有被释放。
定时扫描的策略:Redis默认会每秒进行10次扫描,将每个设置了过期时间的key放入到一个独立的字典中,采用一种贪心策略。
1. 从过期字典中随机20个Key;
2. 删除这20个key中已经过期的Key;
3. 如果过期的Key比率超过1/4,那么就重复步骤1;
因此从业人员一定要注意过期时间,如果有大批量的key过期,要给过期时间设置一个随机范围,不能再同一时间过期。
从库的过期策略
从库不会进行过期扫描,从库对过期的处理是被动的。主库的key到期时,会在AOF文件中增加一条del指令,同步到所有的从库,从库通过执行这条del指令来删除过期的key.
Redis淘汰Key算法的LRU和LFU的区别
LRU算法
LRU算法(Least Recently Used, 最近最少使用):淘汰很久没被访问过的数据,以最近一次访问时间作为参考这个算法通常使用在内存淘汰策略中,用于将不常用的数据转移出内存,将空间腾给最近更常用的“热点数据”。
一般来讲,LRU将访问数据的顺序或时间和数据本身维护在一个容器当中。当访问一个数据时:
- 若该数据不在容器中,则设置该数据的优先级为最高并放入容器中。
- 若该数据在容器当中,则更新该数据的优先级至最高。
当数据的总量达到上限后,则移除容器中优先级最低的数据。下图是一个简单的LRU原理示意图:
LFU算法
LFU算法,(least frequently used,最近最不经常使用算法):淘汰最近一段时间被访问次数最少的数据,以次数作为参考
对于每个条目,维护其使用次数 cnt、最近使用时间 time。
cache容量为 n,即最多存储n个条目。
那么当我需要插入新条目并且cache已经满了的时候,需要删除一个之前的条目。
删除的策略是:优先删除使用次数cnt最小的那个条目*,因为它最近最不经常使用,所以删除它。
如果使用次数cnt最小值为min_cnt,这个min_cnt对应的条目有多个,那么在这些条目中删除最近使用时间time最早的那个条目(举个栗子:a资源和b资源都使用了两次,但a资源在5s的时候最后一次使用,b资源在7s的时候最后一次使用,那么删除a,因为b资源更晚被使用,所以b资源相比a资源来说,更有理由继续被使用,即时间局部性原理)。
两者相比较:
- LRU算法适合:较大的文件比如游戏客户端(最近加载的地图文件);
- LFU算法适合:较小的文件和零碎的文件比如系统文件、应用程序文件 ;
- LRU消耗CPU资源较少,LFU消耗CPU资源较多。
与数据淘汰策略相关的其他面试问题
- 数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
回答: 使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
- Redis的内存用完了会发生什么?
回答:主要看数据淘汰策略是什么?如果是默认的配置(noeviction:不淘汰热任何key,但是内存满了不允许写入新数据),则会直接报错。
redis做为缓存,mysql的数据如何与redis进行同步呢?
(先介绍业务背景)
- 在一致性要求高的背景下:双写一致
当修改了数据库中的数据也要同时更新缓存数据,缓存和数据库的数据要保持一致。- 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
- 写操作:
- 延迟双删
- 先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据
- 为什么要删除两次缓存:降低脏数据出现的概率。
- 为什么要延时删除?
让数据库进行主从复制,将主节点的数据同步到从节点需要延时,但是具体延时多久不好控制。做不到绝对的强一致性。
- 分布式锁(读写锁)
一般放到缓存中的数据都是读多写少。
共享锁:读锁readlock,加锁之后其他线程可以共享读操作。
排他锁:独占锁writelock,加锁之后,阻塞其他线程的读写操作。
性能低,除非是追求强一致性场景,否则不推荐
- 异步通知保证数据的最终一致性
使用MQ、Canal等(二进制日志binlog记录了所有的DDL语句和DML语句,但不包括数据查询语句)- 使用MQ中间件,更新数据后,通知缓存数据;
- 采用Canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存。
- 延迟双删
Redis分布式锁是如何实现的?
使用的场景:集群情况下的定时任务、抢单、幂等性问题
可以加Synconized锁,但是该锁是本地锁,属于JVM的,只能解决同一个jvm下线程的互斥
Redis实现分布式锁主要利用Redis的setnx命令,setnx是set if not exists的缩写。
如何合理的控制锁的有效时长?
- 根据业务的执行时间预估
- 给锁续期(保证分布式锁的可靠性,防止业务没执行完锁过期释放)
注:加锁、设置过期时间等都是基于Lua脚本完成的,保证执行的原子性
Redis实现的分布式锁是否可重入呢?
同一个线程下Redisson实现的分布式锁是可重入的
Redis实现的分布式锁能否实现主从一致性
Redis master主节点 负责写数据
Redis slave从节点 负责读操作
不能解决,但是可以使用redisson提供的红锁来解决
- 加RedLock(红锁)或者也叫MultiLock:不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2 + 1),避免在一个redis实例上加锁
缺点:实现复杂,性能差,运维繁琐 - 如果非要保证数据的强一致性,建议采用zookeeper实现的分布式锁。