2024年java面试--redis(2)

系列文章目录

  1. 2024年java面试(一)–spring篇
  2. 2024年java面试(二)–spring篇
  3. 2024年java面试(三)–spring篇
  4. 2024年java面试(四)–spring篇

文章目录

  • 系列文章目录
  • Redis分区容错
    • 1、redis数据分区
    • 2、主从模式=简单
    • 3、哨兵模式=读多
    • 4、集群模式=写多
    • 5、分布式锁
    • 6、redis心跳检测
  • 一、为什么Redis快?
  • 二、Redis为什么是单线程的
  • 三、Redis服务器的的内存是多大
  • 四、为什么Redis的操作是原子性的,怎么保证原子性的
  • 五、Redis数据和MySQL数据库的一致性如何实现
  • 六、常用限流算法
  • 七、Redis哈希槽的概念
  • 八、Redis有哪些适合的场景
  • 九、Redis在项目中的应用


Redis分区容错

1、redis数据分区

Hash:(不稳定)

客户端分片:哈希+取余

节点伸缩:数据节点关系变化,导致数据迁移

迁移数量和添加节点数量有关:建议翻倍扩容

一个简单直观的想法是直接用Hash来计算,以Key做哈希后对节点数取模。可以看出,在key足够分散的情况下,均匀性可以获得,但一旦有节点加入或退出,所有的原有节点都会受到影响,稳定性无从谈起。

一致性Hash:(不均衡)

客户端分片:哈希+顺时针(优化取余)

节点伸缩:只影响邻近节点,但是还是有数据迁移

翻倍伸缩:保证最小迁移数据和负载均衡

一致性Hash可以很好的解决稳定问题,可以将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到先遇到的一组存储节点存放。而当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点,将数据从该节点接收或者给予。但这又带来均匀性的问题,即使可以将存储节点等距排列,也会在存储节点个数变化时带来数据的不均匀

Codis的Hash槽

Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算 哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。

RedisCluster

Redis-cluster把所有的物理节点映射到[0-16383]个slot上,对key采用crc16算法得到hash值后对16384取模,基本上采用平均分配和连续分配的方式。

2、主从模式=简单

主从模式最大的优点是部署简单,最少两个节点便可以构成主从模式,并且可以通过读写分离避免读和写同时不可用。不过,一旦 Master 节点出现故障,主从节点就无法自动切换,直接导致 SLA 下降。所以,主从模式一般适合业务发展初期,并发量低,运维成本低的情况

2024年java面试--redis(2)_第1张图片
主从复制原理:

①通过从服务器发送到PSYNC命令给主服务器

②如果是首次连接,触发一次全量复制。此时主节点会启动一个后台线程,生成 RDB 快照文件

③主节点会将这个 RDB 发送给从节点,slave 会先写入本地磁盘,再从本地磁盘加载到内存中

④master会将此过程中的写命令写入缓存,从节点实时同步这些数据

⑤如果网络断开了连接,自动重连后主节点通过命令传播增量复制给从节点部分缺少的数据

缺点

所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决,redis4.0中引入psync2 解决了slave重启后仍然可以增量同步。

3、哨兵模式=读多

由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。哨兵模式适合读请求远多于写请求的业务场景比如在秒杀系统中用来缓存活动信息。 如果写请求较多,当集群 Slave 节点数量多了后,Master 节点同步数据的压力会非常大。

当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证redis的高可用性。

检测主观下线状态

Sentinel每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他Sentinel)发送PING命令

实例在down-after-milliseconds毫秒内返回无效回复Sentinel就会认为该实例主观下线(SDown)

检查客观下线状态

当一个Sentinel将一个主服务器判断为主观下线后 ,Sentinel会向监控这个主服务器的所有其他Sentinel发送查询主机状态的命令

如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线(ODown)。

选举Leader Sentinel

当一个主服务器被判定为客观下线后,监视这个主服务器的所有Sentinel会通过选举算法(raft),选出一个Leader Sentinel去执行failover(故障转移) 操作。

Raft算法

Raft协议是用来解决分布式系统一致性问题的协议。 Raft协议描述的节点共有三种状态:Leader, Follower, Candidate。 Raft协议将时间切分为一个个的Term(任期),可以认为是一种“逻辑时间”。 选举流程:

①Raft采用心跳机制触发Leader选举系统启动后,全部节点初始化为Follower,term为0

②节点如果收到了RequestVote或者AppendEntries,就会保持自己的Follower身份

③节点如果一段时间内没收到AppendEntries消息,在该节点的超时时间内还没发现Leader,Follower就会转换成Candidate,自己开始竞选Leader。 一旦转化为Candidate,该节点立即开始下面几件事情: ​ --增加自己的term,启动一个新的定时器 ​ --给自己投一票,向所有其他节点发送RequestVote,并等待其他节点的回复。

④如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时通过 AppendEntries,向其他节点发送通知。

⑤每个节点在一个term内只能投一票,采取先到先得的策略,Candidate投自己, Follower会投给第一个收到RequestVote的节点。

⑥Raft协议的定时器采取随机超时时间(选举的关键),先转为Candidate的节点会先发起投票,从而获得多数票。

主服务器的选择

当选举出Leader Sentinel后,Leader Sentinel会根据以下规则去从服务器中选择出新的主服务器。

①过滤掉主观、客观下线的节点

②选择配置slave-priority最高的节点,如果有则返回没有就继续选择

③选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整

④选择run_id最小的节点,因为run_id越小说明重启次数越少

故障转移

当Leader Sentinel完成新的主服务器选择后,Leader Sentinel会对下线的主服务器执行故障转移操作,主要有三个步骤:

1、它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;

2、当客户端试图连接失效的 Master 时,集群会向客户端返回新 Master 的地址,使得集群当前状态只有一个Master。

3、Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即 Master 主服务器的 redis.conf配置文件中会多一行 replicaof 的配置, sentinel.conf 的监控目标会随之调换。

4、集群模式=写多

为了避免单一节点负载过高导致不稳定,集群模式采用一致性哈希算法或者哈希槽的方法将 Key 分布到各个节点上。其中,每个 Master 节点后跟若干个 Slave 节点,用于出现故障时做主备切换,客户端可以连接任意 Master 节点,集群内部会按照不同 key 将请求转发到不同的 Master 节点
集群模式是如何实现高可用的呢?集群内部节点之间会互相定时探测对方是否存活,如果多数节点判断某个节点挂了,则会将其踢出集群,然后从 Slave 节点中选举出一个节点替补挂掉的 Master 节点。整个原理基本和哨兵模式一致
虽然集群模式避免了 Master 单节点的问题,但集群内同步数据时会占用一定的带宽。所以,只有在写操作比较多的情况下人们才使用集群模式,其他大多数情况,使用哨兵模式都能满足需求

5、分布式锁

利用Watch实现Redis乐观锁

乐观锁基于CAS(Compare And Swap)比较并替换思想,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁 (秒杀) 。具体思路如下:

1、利用redis的watch功能,监控这个redisKey的状态值 2、获取redisKey的值,创建redis事务,给这个key的值+1 3、执行这个事务,如果key的值被修改过则回滚,key不加1

利用setnx防止库存超卖 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 利用Redis的单线程特性对共享资源进行串行化处理

// 获取锁推荐使用set的方式
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
String result = jedis.setnx(lockKey, requestId); //如线程死掉,其他线程无法获取到锁
// 释放锁,非原子操作,可能会释放其他线程刚加上的锁
if (requestId.equals(jedis.get(lockKey))) { 
  jedis.del(lockKey);
}
// 推荐使用redis+lua脚本
String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Object result = jedis.eval(lua, Collections.singletonList(lockKey),

分布式锁存在的问题:

  • 客户端长时间阻塞导致锁失效问题

计算时间内异步启动另外一个线程去检查的问题,这个key是否超时,当锁超时时间快到期且逻辑未执行完,延长锁超时时间。

  • Redis服务器时钟漂移问题导致同时加锁 redis的过期时间是依赖系统时钟的,如果时钟漂移过大时 理论上是可能出现的 会影响到过期时间的计算。

  • 单点实例故障,锁未及时同步导致丢失

RedLock算法

1.获取当前时间戳T0,配置时钟漂移误差T1

2.短时间内逐个获取全部N/2+1个锁,结束时间点T2

3.实际锁能使用的处理时长变为:TTL - (T2 - T0)- T1
该方案通过多节点来防止Redis的单点故障,效果一般,也无法防止:

  • 主从切换导致的两个客户端同时持有锁

大部分情况下持续时间极短,而且使用Redlock在切换的瞬间获取到节点的锁,也存在问题。已经是极低概率的时间,无法避免。Redis分布式锁适合幂等性事务,如果一定要保证安全,应该使用Zookeeper或者DB,但是,性能会急剧下降

与zookeeper分布式锁对比

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,注册个监听器即可,不需要不断主动尝试获取锁,ZK获取锁会按照加锁的顺序,所以是公平锁,性能和mysql差不多,和redis差别大

Redission生产环境的分布式锁

Redisson是基于NIO的Netty框架上的一个Java驻内存数据网格(In-Memory Data Grid)分布式锁开源组件。

但当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账),请不要使用redis分布式锁。可以使用CP模型实现,比如:zookeeper和etcd

Redis zookeeper etcd
一致性算法 paxos(ZAB) raft
CAP AP CP CP
高可用 主从集群 n+1 n+1
实现 setNX createNode restfulAPI

6、redis心跳检测

在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送ACK命令:

1、检测主从的连接状态 检测主从服务器的网络连接状态
lag的值应该在0或1之间跳动,如果超过1则说明主从之间的连接有 故障。

2、辅助实现min-slaves,Redis可以通过配置防止主服务器在不安全的情况下执行写命令

min-slaves-to-write 3 (min-replicas-to-write 3 )
​
min-slaves-max-lag 10 (min-replicas-max-lag 10)

上面的配置表示:从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令。

3、检测命令丢失,增加重传机制
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发 送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量, 然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。


一、为什么Redis快?

1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

2)数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的

3)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

4)使用I/O多路复用模型,非阻塞IO

5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求


二、Redis为什么是单线程的

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了Redis利用队列技术将并发访问变为串行访问

1)绝大部分请求是纯粹的内存操作

2)采用单线程,避免了不必要的上下文切换和竞争条件


三、Redis服务器的的内存是多大

配置文件中设置redis内存的参数:。

该参数如果不设置或者设置为0,则redis默认的内存大小为:

32位下默认是3G

64位下不受限制

一般推荐Redis设置内存为最大物理内存的四分之三,也就是0.75

命令行设置config set maxmemory <内存大小,单位字节>,服务器重启失效

config get maxmemory获取当前内存大小

永久则需要设置maxmemory参数,maxmemory是bytes字节类型,注意转换


四、为什么Redis的操作是原子性的,怎么保证原子性的

对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。

Redis的操作之所以是原子性的,是因为Redis是单线程的。

Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。

多个命令在并发中也是原子性的吗?

不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.


五、Redis数据和MySQL数据库的一致性如何实现

一、 延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
二、设置缓存的过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
三、如何写完数据库后,再次删除缓存成功?
上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。
1、方案一具体流程
(1)更新数据库数据;
(2)缓存因为种种问题删除失败;
(3)将需要删除的key发送至消息队列;
(4)自己消费消息,获得需要删除的key;
(5)继续重试删除操作,直到成功。
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
2、方案二具体流程
(1)更新数据库数据;
(2)数据库会将操作信息写入binlog日志当中;
(3)订阅程序提取出所需要的数据以及key;
(4)另起一段非业务代码,获得该信息;
(5)尝试删除缓存操作,发现删除失败;
(6)将这些信息发送至消息队列;
(7)重新从消息队列中获得该数据,重试操作。


六、常用限流算法

计数器算法:使用redis的setnx和过期机制实现

漏桶算法:一般使用消息队列来实现,系统以恒定速度处理队列中的请求,当队列满的时候开始拒绝请求。

令牌桶算法:计数器算法和漏桶算法都无法解决突然的大并发,令牌桶算法是预先往桶中放入一定数量token,然后用恒定速度放入token直到桶满为止,所有请求都必须拿到token才能访问系统


七、Redis哈希槽的概念

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。


八、Redis有哪些适合的场景

(1)会话缓存(Session Cache)
最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。
(2)全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。
再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。
此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
(3)队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。
(4)排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。
(5)发布/订阅
最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。


九、Redis在项目中的应用

Redis一般来说在项目中有几方面的应用

作为缓存,将热点数据进行缓存,减少和数据库的交互,提高系统的效率
作为分布式锁的解决方案,解决缓存击穿等问题
作为消息队列,使用Redis的发布订阅功能进行消息的发布和订阅

具体的使用场景要结合项目去说,比如说项目中有哪些场景用到Redis来作为缓存,以及分布式锁等等。

你可能感兴趣的:(java,java,面试,redis)