江西省美丽乡村建设公布网站,wordpress加速优化服务,最牛的房地产网站建设,工程资质SpringCloud常见面试题 1.微服务篇1.1.SpringCloud常见组件有哪些#xff1f;1.2.Nacos的服务注册表结构是怎样的#xff1f;1.3.Nacos如何支撑阿里内部数十万服务注册压力#xff1f;1.4.Nacos如何避免并发读写冲突问题#xff1f;1.5.Nacos与Eureka的区别有哪些#xff… SpringCloud常见面试题 1.微服务篇1.1.SpringCloud常见组件有哪些1.2.Nacos的服务注册表结构是怎样的1.3.Nacos如何支撑阿里内部数十万服务注册压力1.4.Nacos如何避免并发读写冲突问题1.5.Nacos与Eureka的区别有哪些1.6.Sentinel的限流与Gateway的限流有什么差别1.7.Sentinel的线程隔离与Hystix的线程隔离有什么差别? 2.MQ篇2.1.你们为什么选择了RabbitMQ而不是其它的MQ2.2.RabbitMQ如何确保消息的不丢失2.3.RabbitMQ如何避免消息堆积2.4.RabbitMQ如何保证消息的有序性2.5.如何防止MQ消息被重复消费2.6.如何保证RabbitMQ的高可用2.7.使用MQ可以解决那些问题 3.Redis篇3.1.Redis与Memcache的区别3.2.Redis的单线程问题3.2.Redis的持久化方案由哪些3.3.Redis的集群方式有哪些3.4.Redis的常用数据类型有哪些3.5.聊一下Redis事务机制3.6.Redis的Key过期策略**参考资料**为什么需要内存回收过期删除策略内存淘汰策略 面试话术 3.7.Redis在项目中的哪些地方有用到?3.8.Redis的缓存击穿、缓存雪崩、缓存穿透1缓存穿透2缓存击穿3缓存雪崩 3.9.缓存冷热数据分离3.10.Redis实现分布式锁1最基本的分布式锁2可重入分布式锁3高可用的锁 3.11.如何实现数据库与缓存数据一致 1.微服务篇
1.1.SpringCloud常见组件有哪些
问题说明这个题目主要考察对SpringCloud的组件基本了解
难易程度简单
参考话术
SpringCloud包含的组件很多有很多功能是重复的。其中最常用组件包括
•注册中心组件Eureka、Nacos等
•负载均衡组件Ribbon
•远程调用组件OpenFeign
•网关组件Zuul、Gateway
•服务保护组件Hystrix、Sentinel
•服务配置管理组件SpringCloudConfig、Nacos
1.2.Nacos的服务注册表结构是怎样的
问题说明考察对Nacos数据分级结构的了解以及Nacos源码的掌握情况
难易程度一般
参考话术
Nacos采用了数据的分级存储模型最外层是Namespace用来隔离环境。然后是Group用来对服务分组。接下来就是服务Service了一个服务包含多个实例但是可能处于不同机房因此Service下有多个集群ClusterCluster下是不同的实例Instance。
对应到Java代码中Nacos采用了一个多层的Map来表示。结构为MapString, MapString, Service其中最外层Map的key就是namespaceId值是一个Map。内层Map的key是group拼接serviceName值是Service对象。Service对象内部又是一个Mapkey是集群名称值是Cluster对象。而Cluster对象内部维护了Instance的集合。
如图 1.3.Nacos如何支撑阿里内部数十万服务注册压力
问题说明考察对Nacos源码的掌握情况
难易程度难
参考话术
Nacos内部接收到注册的请求时不会立即写数据而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务异步来完成实例更新从而提高并发写能力。
1.4.Nacos如何避免并发读写冲突问题
问题说明考察对Nacos源码的掌握情况
难易程度难
参考话术
Nacos在更新实例列表时会采用CopyOnWrite技术首先将旧的实例列表拷贝一份然后更新拷贝的实例列表再用更新后的实例列表来覆盖旧的实例列表。
这样在更新的过程中就不会对读实例列表的请求产生影响也不会出现脏读问题了。
1.5.Nacos与Eureka的区别有哪些
问题说明考察对Nacos、Eureka的底层实现的掌握情况
难易程度难
参考话术
Nacos与Eureka有相同点也有不同之处可以从以下几点来描述
接口方式Nacos与Eureka都对外暴露了Rest风格的API接口用来实现服务注册、发现等功能实例类型Nacos的实例有永久和临时实例之分而Eureka只支持临时实例健康检测Nacos对临时实例采用心跳模式检测对永久实例采用主动请求来检测Eureka只支持心跳模式服务发现Nacos支持定时拉取和订阅推送两种模式Eureka只支持定时拉取模式
1.6.Sentinel的限流与Gateway的限流有什么差别
问题说明考察对限流算法的掌握情况
难易程度难
参考话术
限流算法常见的有三种实现滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法。
而Sentinel内部却比较复杂
默认限流模式是基于滑动时间窗口算法排队等待的限流模式则基于漏桶算法而热点参数限流则是基于令牌桶算法
1.7.Sentinel的线程隔离与Hystix的线程隔离有什么差别?
问题说明考察对线程隔离方案的掌握情况
难易程度一般
参考话术
Hystix默认是基于线程池实现的线程隔离每一个被隔离的业务都要创建一个独立的线程池线程过多会带来额外的CPU开销性能一般但是隔离性更强。
Sentinel是基于信号量计数器实现的线程隔离不用创建线程池性能较好但是隔离性一般。
2.MQ篇
2.1.你们为什么选择了RabbitMQ而不是其它的MQ
如图 话术
kafka是以吞吐量高而闻名不过其数据稳定性一般而且无法保证消息有序性。我们公司的日志收集也有使用业务模块中则使用的RabbitMQ。
阿里巴巴的RocketMQ基于Kafka的原理弥补了Kafka的缺点继承了其高吞吐的优势其客户端目前以Java为主。但是我们担心阿里巴巴开源产品的稳定性所以就没有使用。
RabbitMQ基于面向并发的语言Erlang开发吞吐量不如Kafka但是对我们公司来讲够用了。而且消息可靠性较好并且消息延迟极低集群搭建比较方便。支持多种协议并且有各种语言的客户端比较灵活。Spring对RabbitMQ的支持也比较好使用起来比较方便比较符合我们公司的需求。
综合考虑我们公司的并发需求以及稳定性需求我们选择了RabbitMQ。
2.2.RabbitMQ如何确保消息的不丢失
话术
RabbitMQ针对消息传递过程中可能发生问题的各个地方给出了针对性的解决方案
生产者发送消息时可能因为网络问题导致消息没有到达交换机 RabbitMQ提供了publisher confirm机制 生产者发送消息后可以编写ConfirmCallback函数消息成功到达交换机后RabbitMQ会调用ConfirmCallback通知消息的发送者返回ACK消息如果未到达交换机RabbitMQ也会调用ConfirmCallback通知消息的发送者返回NACK消息超时未发送成功也会抛出异常 消息到达交换机后如果未能到达队列也会导致消息丢失 RabbitMQ提供了publisher return机制 生产者可以定义ReturnCallback函数消息到达交换机未到达队列RabbitMQ会调用ReturnCallback通知发送者告知失败原因 消息到达队列后MQ宕机也可能导致丢失消息 RabbitMQ提供了持久化功能集群的主从备份功能 消息持久化RabbitMQ会将交换机、队列、消息持久化到磁盘宕机重启可以恢复消息镜像集群仲裁队列都可以提供主从备份功能主节点宕机从节点会自动切换为主数据依然在 消息投递给消费者后如果消费者处理不当也可能导致消息丢失 SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制消费者失败处理策略 消费者的确认机制 消费者处理消息成功未出现异常时Spring返回ACK给RabbitMQ消息才被移除消费者处理消息失败抛出异常宕机Spring返回NACK或者不返回结果消息不被异常 消费者重试机制 默认情况下消费者处理失败时消息会再次回到MQ队列然后投递给其它消费者。Spring提供的消费者重试机制则是在处理失败后不返回NACK而是直接在消费者本地重试。多次重试都失败后则按照消费者失败处理策略来处理消息。避免了消息频繁入队带来的额外压力。 消费者失败策略 当消费者多次本地重试失败时消息默认会丢弃。Spring提供了Republish策略在多次重试都失败耗尽重试次数后将消息重新投递给指定的异常交换机并且会携带上异常栈信息帮助定位问题。
2.3.RabbitMQ如何避免消息堆积
话术
消息堆积问题产生的原因往往是因为消息发送的速度超过了消费者消息处理的速度。因此解决方案无外乎以下三点
提高消费者处理速度增加更多消费者增加队列消息存储上限
1提高消费者处理速度
消费者处理速度是由业务代码决定的所以我们能做的事情包括
尽可能优化业务代码提高业务性能接收到消息后开启线程池并发处理多个消息
优点成本低改改代码即可
缺点开启线程池会带来额外的性能开销对于高频、低时延的任务不合适。推荐任务执行周期较长的业务。
2增加更多消费者
一个队列绑定多个消费者共同争抢任务自然可以提供消息处理的速度。
优点能用钱解决的问题都不是问题。实现简单粗暴
缺点问题是没有钱。成本太高
3增加队列消息存储上限
在RabbitMQ的1.8版本后加入了新的队列模式Lazy Queue
这种队列不会将消息保存在内存中而是在收到消息后直接写入磁盘中理论上没有存储上限。可以解决消息堆积问题。
优点磁盘存储更安全存储无上限避免内存存储带来的Page Out问题性能更稳定
缺点磁盘存储受到IO性能的限制消息时效性不如内存模式但影响不大。
2.4.RabbitMQ如何保证消息的有序性
话术
其实RabbitMQ是队列存储天然具备先进先出的特点只要消息的发送是有序的那么理论上接收也是有序的。不过当一个队列绑定了多个消费者时可能出现消息轮询投递给消费者的情况而消费者的处理顺序就无法保证了。
因此要保证消息的有序性需要做的下面几点
保证消息发送的有序性保证一组有序的消息都发送到同一个队列保证一个队列只包含一个消费者
2.5.如何防止MQ消息被重复消费
话术
消息重复消费的原因多种多样不可避免。所以只能从消费者端入手只要能保证消息处理的幂等性就可以确保消息不被重复消费。
而幂等性的保证又有很多方案
给每一条消息都添加一个唯一id在本地记录消息表及消息状态处理消息时基于数据库表的id唯一性做判断同样是记录消息表利用消息状态字段实现基于乐观锁的判断保证幂等基于业务本身的幂等性。比如根据id的删除、查询业务天生幂等新增、修改等业务可以考虑基于数据库id唯一性、或者乐观锁机制确保幂等。本质与消息表方案类似。
2.6.如何保证RabbitMQ的高可用
话术
要实现RabbitMQ的高可用无外乎下面两点
做好交换机、队列、消息的持久化搭建RabbitMQ的镜像集群做好主从备份。当然也可以使用仲裁队列代替镜像集群。
2.7.使用MQ可以解决那些问题
话术
RabbitMQ能解决的问题很多例如
解耦合将几个业务关联的微服务调用修改为基于MQ的异步通知可以解除微服务之间的业务耦合。同时还提高了业务性能。流量削峰将突发的业务请求放入MQ中作为缓冲区。后端的业务根据自己的处理能力从MQ中获取消息逐个处理任务。流量曲线变的平滑很多延迟队列基于RabbitMQ的死信队列或者DelayExchange插件可以实现消息发送后延迟接收的效果。
3.Redis篇
3.1.Redis与Memcache的区别
redis支持更丰富的数据类型支持更复杂的应用场景Redis不仅仅支持简单的k/v类型的数据同时还提供listsetzsethash等数据结构的存储。memcache支持简单的数据类型String。Redis支持数据的持久化可以将内存中的数据保持在磁盘中重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。集群模式memcached没有原生的集群模式需要依靠客户端来实现往集群中分片写入数据但是 redis 目前是原生支持 cluster 模式的.Redis使用单线程Memcached是多线程非阻塞IO复用的网络模型Redis使用单线程的多路 IO 复用模型。 3.2.Redis的单线程问题
面试官Redis采用单线程如何保证高并发
面试话术
Redis快的主要原因是
完全基于内存数据结构简单对数据操作也简单使用多路 I/O 复用模型充分利用CPU资源
面试官这样做的好处是什么
面试话术
单线程优势有下面几点
代码更清晰处理逻辑更简单不用去考虑各种锁的问题不存在加锁释放锁操作没有因为锁而导致的性能消耗不存在多进程或者多线程导致的CPU切换充分利用CPU资源
3.2.Redis的持久化方案由哪些
相关资料
1RDB 持久化
RDB持久化可以使用save或bgsave为了不阻塞主进程业务一般都使用bgsave流程
Redis 进程会 fork 出一个子进程与父进程内存数据一致。父进程继续处理客户端请求命令由子进程将内存中的所有数据写入到一个临时的 RDB 文件中。完成写入操作之后旧的 RDB 文件会被新的 RDB 文件替换掉。
下面是一些和 RDB 持久化相关的配置
save 60 10000如果在 60 秒内有 10000 个 key 发生改变那就执行 RDB 持久化。stop-writes-on-bgsave-error yes如果 Redis 执行 RDB 持久化失败常见于操作系统内存不足那么 Redis 将不再接受 client 写入数据的请求。rdbcompression yes当生成 RDB 文件时同时进行压缩。dbfilename dump.rdb将 RDB 文件命名为 dump.rdb。dir /var/lib/redis将 RDB 文件保存在/var/lib/redis目录下。
当然在实践中我们通常会将stop-writes-on-bgsave-error设置为false同时让监控系统在 Redis 执行 RDB 持久化失败时发送告警以便人工介入解决而不是粗暴地拒绝 client 的写入请求。
RDB持久化的优点
RDB持久化文件小Redis数据恢复时速度快子进程不影响父进程父进程可以持续处理客户端命令子进程fork时采用copy-on-write方式大多数情况下没有太多的内存消耗效率比较好。
RDB 持久化的缺点
子进程fork时采用copy-on-write方式如果Redis此时写操作较多可能导致额外的内存占用甚至内存溢出RDB文件压缩会减小文件体积但通过时会对CPU有额外的消耗如果业务场景很看重数据的持久性 (durability)那么不应该采用 RDB 持久化。譬如说如果 Redis 每 5 分钟执行一次 RDB 持久化要是 Redis 意外奔溃了那么最多会丢失 5 分钟的数据。
2AOF 持久化
可以使用appendonly yes配置项来开启 AOF 持久化。Redis 执行 AOF 持久化时会将接收到的写命令追加到 AOF 文件的末尾因此 Redis 只要对 AOF 文件中的命令进行回放就可以将数据库还原到原先的状态。 与 RDB 持久化相比AOF 持久化的一个明显优势就是它可以提高数据的持久性 (durability)。因为在 AOF 模式下Redis 每次接收到 client 的写命令就会将命令write()到 AOF 文件末尾。 然而在 Linux 中将数据write()到文件后数据并不会立即刷新到磁盘而会先暂存在 OS 的文件系统缓冲区。在合适的时机OS 才会将缓冲区的数据刷新到磁盘如果需要将文件内容刷新到磁盘可以调用fsync()或fdatasync()。 通过appendfsync配置项可以控制 Redis 将命令同步到磁盘的频率
always每次 Redis 将命令write()到 AOF 文件时都会调用fsync()将命令刷新到磁盘。这可以保证最好的数据持久性但却会给系统带来极大的开销。noRedis 只将命令write()到 AOF 文件。这会让 OS 决定何时将命令刷新到磁盘。everysec除了将命令write()到 AOF 文件Redis 还会每秒执行一次fsync()。在实践中推荐使用这种设置一定程度上可以保证数据持久性又不会明显降低 Redis 性能。
然而AOF 持久化并不是没有缺点的Redis 会不断将接收到的写命令追加到 AOF 文件中导致 AOF 文件越来越大。过大的 AOF 文件会消耗磁盘空间并且导致 Redis 重启时更加缓慢。为了解决这个问题在适当情况下Redis 会对 AOF 文件进行重写去除文件中冗余的命令以减小 AOF 文件的体积。在重写 AOF 文件期间 Redis 会启动一个子进程由子进程负责对 AOF 文件进行重写。 可以通过下面两个配置项控制 Redis 重写 AOF 文件的频率
auto-aof-rewrite-min-size 64mbauto-aof-rewrite-percentage 100
上面两个配置的作用当 AOF 文件的体积大于 64MB并且 AOF 文件的体积比上一次重写之后的体积大了至少一倍那么 Redis 就会执行 AOF 重写。
优点
持久化频率高数据可靠性高没有额外的内存或CPU消耗
缺点
文件体积大文件大导致服务数据恢复时效率较低
面试话术
Redis 提供了两种数据持久化的方式一种是 RDB另一种是 AOF。默认情况下Redis 使用的是 RDB 持久化。
RDB持久化文件体积较小但是保存数据的频率一般较低可靠性差容易丢失数据。另外RDB写数据时会采用Fork函数拷贝主进程可能有额外的内存消耗文件压缩也会有额外的CPU消耗。
ROF持久化可以做到每秒钟持久化一次可靠性高。但是持久化文件体积较大导致数据恢复时读取文件时间较长效率略低
3.3.Redis的集群方式有哪些
面试话术
Redis集群可以分为主从集群和分片集群两类。
主从集群一般一主多从主库用来写数据从库用来读数据。结合哨兵可以再主库宕机时从新选主目的是保证Redis的高可用。
分片集群是数据分片我们会让多个Redis节点组成集群并将16383个插槽分到不同的节点上。存储数据时利用对key做hash运算得到插槽值后存储到对应的节点即可。因为存储数据面向的是插槽而非节点本身因此可以做到集群动态伸缩。目的是让Redis能存储更多数据。
1主从集群
主从集群也是读写分离集群。一般都是一主多从方式。
Redis 的复制replication功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品其中被复制的服务器为主服务器master而通过复制创建出来的服务器复制品则为从服务器slave。
只要主从服务器之间的网络连接正常主从服务器两者会具有相同的数据主服务器就会一直将发生在自己身上的数据更新同步 给从服务器从而一直保证主从服务器的数据相同。
写数据时只能通过主节点完成读数据可以从任何节点完成如果配置了哨兵节点当master宕机时哨兵会从salve节点选出一个新的主。
主从集群分两种 带有哨兵的集群 2分片集群
主从集群中每个节点都要保存所有信息容易形成木桶效应。并且当数据量较大时单个机器无法满足需求。此时我们就要使用分片集群了。 集群特征 每个节点都保存不同数据 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽. 节点的fail是通过集群中超过半数的节点检测失效时才生效. 客户端与redis节点直连,不需要中间proxy层连接集群中任何一个可用节点都可以访问到数据 redis-cluster把所有的物理节点映射到[0-16383]slot插槽上实现动态伸缩
为了保证Redis中每个节点的高可用我们还可以给每个节点创建replicationslave节点如图 出现故障时主从可以及时切换 3.4.Redis的常用数据类型有哪些
支持多种类型的数据结构主要区别是value存储的数据格式不同 string最基本的数据类型二进制安全的字符串最大512M。 list按照添加顺序保持顺序的字符串列表。 set无序的字符串集合不存在重复的元素。 sorted set已排序的字符串集合。 hashkey-value对格式
3.5.聊一下Redis事务机制
相关资料
参考http://redisdoc.com/topic/transaction.html
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。Redis会将一个事务中的所有命令序列化然后按顺序执行。但是Redis事务不支持回滚操作命令运行出错后正确的命令会继续执行。
MULTI: 用于开启一个事务它总是返回OK。 MULTI执行之后客户端可以继续向服务器发送任意多条命令这些命令不会立即被执行而是被放到一个待执行命令队列中EXEC按顺序执行命令队列内的所有命令。返回所有命令的返回值。事务执行过程中Redis不会执行其它事务的命令。DISCARD清空命令队列并放弃执行事务 并且客户端会从事务状态中退出WATCHRedis的乐观锁机制利用compare-and-setCAS原理可以监控一个或多个键一旦其中有一个键被修改之后的事务就不会执行
使用事务时可能会遇上以下两种错误
执行 EXEC 之前入队的命令可能会出错。比如说命令可能会产生语法错误参数数量错误参数名错误等等或者其他更严重的错误比如内存不足如果服务器使用 maxmemory 设置了最大内存限制的话。 Redis 2.6.5 开始服务器会对命令入队失败的情况进行记录并在客户端调用 EXEC 命令时拒绝执行并自动放弃这个事务。 命令可能在 EXEC 调用之后失败。举个例子事务中的命令可能处理了错误类型的键比如将列表命令用在了字符串键上面诸如此类。 即使事务中有某个/某些命令在执行时产生了错误 事务中的其他命令仍然会继续执行不会回滚。
为什么 Redis 不支持回滚roll back
以下是这种做法的优点
Redis 命令只会因为错误的语法而失败并且这些问题不能在入队时发现或是命令用在了错误类型的键上面这也就是说从实用性的角度来说失败的命令是由编程错误造成的而这些错误应该在开发的过程中被发现而不应该出现在生产环境中。因为不需要对回滚进行支持所以 Redis 的内部可以保持简单且快速。
鉴于没有任何机制能避免程序员自己造成的错误 并且这类错误通常不会在生产环境中出现 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。
面试话术
Redis事务其实是把一系列Redis命令放入队列然后批量执行执行过程中不会有其它事务来打断。不过与关系型数据库的事务不同Redis事务不支持回滚操作事务中某个命令执行失败其它命令依然会执行。
为了弥补不能回滚的问题Redis会在事务入队时就检查命令如果命令异常则会放弃整个事务。
因此只要程序员编程是正确的理论上说Redis会正确执行所有事务无需回滚。
面试官如果事务执行一半的时候Redis宕机怎么办
Redis有持久化机制因为可靠性问题我们一般使用AOF持久化。事务的所有命令也会写入AOF文件但是如果在执行EXEC命令之前Redis已经宕机则AOF文件中事务不完整。使用 redis-check-aof 程序可以移除 AOF 文件中不完整事务的信息确保服务器可以顺利启动。
3.6.Redis的Key过期策略
参考资料
为什么需要内存回收
1、在Redis中set指令可以指定key的过期时间当过期时间到达以后key就失效了2、Redis是基于内存操作的所有的数据都是保存在内存中一台机器的内存是有限且很宝贵的。
基于以上两点为了保证Redis能继续提供可靠的服务Redis需要一种机制清理掉不常用的、无效的、多余的数据失效后的数据需要及时清理这就需要内存回收了。
Redis的内存回收主要分为过期删除策略和内存淘汰策略两部分。
过期删除策略
删除达到过期时间的key。
1定时删除
对于每一个设置了过期时间的key都会创建一个定时器一旦到达过期时间就立即删除。该策略可以立即清除过期的数据对内存较友好但是缺点是占用了大量的CPU资源去处理过期的数据会影响Redis的吞吐量和响应时间。
2惰性删除
当访问一个key时才判断该key是否过期过期则删除。该策略能最大限度地节省CPU资源但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问因此不会被清除导致占用了大量的内存。 在计算机科学中懒惰删除英文lazy deletion指的是从一个散列表也称哈希表中删除元素的一种方法。在这个方法中删除仅仅是指标记一个元素被删除而不是整个清除它。被删除的位点在插入时被当作空元素在搜索之时被当作已占据。 3定期删除
每隔一段时间扫描Redis中过期key字典并清除部分过期的key。该策略是前两者的一个折中方案还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时在不同情况下使得CPU和内存资源达到最优的平衡效果。
在Redis中同时使用了定期删除和惰性删除。不过Redis定期删除采用的是随机抽取的方式删除部分Key因此不能保证过期key 100%的删除。
Redis结合了定期删除和惰性删除基本上能很好的处理过期数据的清理但是实际上还是有点问题的如果过期key较多定期删除漏掉了一部分而且也没有及时去查即没有走惰性删除那么就会有大量的过期key堆积在内存中导致redis内存耗尽当内存耗尽之后有新的key到来会发生什么事呢是直接抛弃还是其他措施呢有什么办法可以接受更多的key
内存淘汰策略
Redis的内存淘汰策略是指内存达到maxmemory极限时使用某种算法来决定清理掉哪些数据以保证新数据的存入。
Redis的内存淘汰机制包括
noeviction: 当内存不足以容纳新写入数据时新写入操作会报错。allkeys-lru当内存不足以容纳新写入数据时在键空间server.db[i].dict中移除最近最少使用的 key这个是最常用的。allkeys-random当内存不足以容纳新写入数据时在键空间server.db[i].dict中随机移除某个 key。volatile-lru当内存不足以容纳新写入数据时在设置了过期时间的键空间server.db[i].expires中移除最近最少使用的 key。volatile-random当内存不足以容纳新写入数据时在设置了过期时间的键空间server.db[i].expires中随机移除某个 key。volatile-ttl当内存不足以容纳新写入数据时在设置了过期时间的键空间server.db[i].expires中有更早过期时间的 key 优先移除。 在配置文件中通过maxmemory-policy可以配置要使用哪一个淘汰机制。 什么时候会进行淘汰
Redis会在每一次处理命令的时候processCommand函数调用freeMemoryIfNeeded判断当前redis是否达到了内存的最大限制如果达到限制则使用对应的算法去处理需要删除的key。
在淘汰key时Redis默认最常用的是LRU算法Latest Recently Used。Redis通过在每一个redisObject保存lru属性来保存key最近的访问时间在实现LRU算法时直接读取key的lru属性。
具体实现时Redis遍历每一个db从每一个db中随机抽取一批样本key默认是3个key再从这3个key中删除最近最少使用的key。
面试话术
Redis过期策略包含定期删除和惰性删除两部分。定期删除是在Redis内部有一个定时任务会定期删除一些过期的key。惰性删除是当用户查询某个Key时会检查这个Key是否已经过期如果没过期则返回用户如果过期则删除。
但是这两个策略都无法保证过期key一定删除漏网之鱼越来越多还可能导致内存溢出。当发生内存不足问题时Redis还会做内存回收。内存回收采用LRU策略就是最近最少使用。其原理就是记录每个Key的最近使用时间内存回收时随机抽取一些Key比较其使用时间把最老的几个删除。
Redis的逻辑是最近使用过的很可能再次被使用
3.7.Redis在项目中的哪些地方有用到?
1共享session
在分布式系统下服务会部署在不同的tomcat因此多个tomcat的session无法共享以前存储在session中的数据无法实现共享可以用redis代替session解决分布式系统间数据共享问题。
2数据缓存
Redis采用内存存储读写效率较高。我们可以把数据库的访问频率高的热点数据存储到redis中这样用户请求时优先从redis中读取减少数据库压力提高并发能力。
3异步队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作这使得Redis能作为一个很好的消息队列平台来使用。而且Redis中还有pub/sub这样的专用结构用于1对N的消息通信模式。
4分布式锁
Redis中的乐观锁机制可以帮助我们实现分布式锁的效果用于解决分布式系统下的多线程安全问题
3.8.Redis的缓存击穿、缓存雪崩、缓存穿透
1缓存穿透
参考资料 什么是缓存穿透 正常情况下我们去查询数据都是存在。那么请求去查询一条压根儿数据库中根本就不存在的数据也就是缓存和数据库都查询不到这条数据但是请求每次都会打到数据库上面去。这种查询不存在数据的现象我们称为缓存穿透。 穿透带来的问题 试想一下如果有黑客会对你的系统进行攻击拿一个不存在的id 去查询数据会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。 解决办法 缓存空值之所以会发生穿透就是因为缓存中没有存储这些空数据的key。从而导致每次查询都到数据库去了。那么我们就可以为这些key对应的值设置为null 丢到缓存里面去。后面再出现查询这个key 的请求的时候直接返回null 。这样就不用在到数据库中去走一圈了但是别忘了设置过期时间。BloomFilter布隆过滤将所有可能存在的数据哈希到一个足够大的bitmap中一个一定不存在的数据会被 这个bitmap拦截掉从而避免了对底层存储系统的查询压力。在缓存之前在加一层 BloomFilter 在查询的时候先去 BloomFilter 去查询 key 是否存在如果不存在就直接返回存在再走查缓存 - 查 DB。
话术
缓存穿透有两种解决方案其一是把不存在的key设置null值到缓存中。其二是使用布隆过滤器在查询缓存前先通过布隆过滤器判断key是否存在存在再去查询缓存。
设置null值可能被恶意针对攻击者使用大量不存在的不重复key 那么方案一就会缓存大量不存在key数据。此时我们还可以对Key规定格式模板然后对不存在的key做正则规范匹配如果完全不符合就不用存null值到redis而是直接返回错误。
2缓存击穿
相关资料
什么是缓存击穿
key可能会在某些时间点被超高并发地访问是一种非常“热点”的数据。这个时候需要考虑一个问题缓存被“击穿”的问题。
当这个key在失效的瞬间redis查询失败持续的大并发就穿破缓存直接请求数据库就像在一个屏障上凿开了一个洞。
解决方案 使用互斥锁(mutex key)mutex就是互斥。简单地来说就是在缓存失效的时候判断拿出来的值为空不是立即去load db而是先使用Redis的SETNX去set一个互斥key当操作返回成功时再进行load db的操作并回设缓存否则就重试整个get缓存的方法。SETNX是「SET if Not eXists」的缩写也就是只有不存在的时候才设置可以利用它来实现互斥的效果。软过期也就是逻辑过期不使用redis提供的过期时间而是业务层在数据中存储过期时间信息。查询时由业务程序判断是否过期如果数据即将过期时将缓存的时效延长程序可以派遣一个线程去数据库中获取最新的数据其他线程这时看到延长了的过期时间就会继续使用旧数据等派遣的线程获取最新数据后再更新缓存。
推荐使用互斥锁因为软过期会有业务逻辑侵入和额外的判断。
面试话术
缓存击穿主要担心的是某个Key过期更新缓存时引起对数据库的突发高并发访问。因此我们可以在更新缓存时采用互斥锁控制只允许一个线程去更新缓存其它线程等待并重新读取缓存。例如Redis的setnx命令就能实现互斥效果。
3缓存雪崩
相关资料
缓存雪崩是指在某一个时间段缓存集中过期失效。对这批数据的访问查询都落到了数据库上对于数据库而言就会产生周期性的压力波峰。
解决方案
数据分类分批处理采取不同分类数据缓存不同周期相同分类数据采用固定时长加随机数方式设置缓存热点数据缓存时间长一些冷门数据缓存时间短一些避免redis节点宕机引起雪崩搭建主从集群保证高可用
面试话术
解决缓存雪崩问题的关键是让缓存Key的过期时间分散。因此我们可以把数据按照业务分类然后设置不同过期时间。相同业务类型的key设置固定时长加随机数。尽可能保证每个Key的过期时间都不相同。
另外Redis宕机也可能导致缓存雪崩因此我们还要搭建Redis主从集群及哨兵监控保证Redis的高可用。
3.9.缓存冷热数据分离
背景资料
Redis使用的是内存存储当需要海量数据存储时成本非常高。
经过调研发现当前主流DDR3内存和主流SATA SSD的单位成本价格差距大概在20倍左右为了优化redis机器综合成本我们考虑实现基于热度统计 的数据分级存储及数据在RAM/FLASH之间的动态交换从而大幅度降低成本达到性能与成本的高平衡。
基本思路基于key访问次数(LFU)的热度统计算法识别出热点数据并将热点数据保留在redis中对于无访问/访问次数少的数据则转存到SSD上如果SSD上的key再次变热则重新将其加载到redis内存中。
目前流行的高性能磁盘存储并且遵循Redis协议的方案包括
SSDBhttp://ssdb.io/zh_cn/RocksDBhttps://rocksdb.org.cn/
因此我们就需要在应用程序与缓存服务之间引入代理实现Redis和SSD之间的切换如图 这样的代理方案阿里云提供的就有。当然也有一些开源方案例如https://github.com/JingchengLi/swapdb
3.10.Redis实现分布式锁
分布式锁要满足的条件
多进程互斥同一时刻只有一个进程可以获取锁保证锁可以释放任务结束或出现异常锁一定要释放避免死锁阻塞锁可选获取锁失败时可否重试重入锁可选获取锁的代码递归调用时依然可以获取锁
1最基本的分布式锁
利用Redis的setnx命令这个命令的特征时如果多次执行只有第一次执行会成功可以实现互斥的效果。但是为了保证服务宕机时也可以释放锁需要利用expire命令给锁设置一个有效期
setnx lock thread-01 # 尝试获取锁
expire lock 10 # 设置有效期面试官问题1如果expire之前服务宕机怎么办
要保证setnx和expire命令的原子性。redis的set命令可以满足
set key value [NX] [EX time] 需要添加nx和ex的选项
NX与setnx一致第一次执行成功EX设置过期时间
面试官问题2释放锁的时候如果自己的锁已经过期了此时会出现安全漏洞如何解决
在锁中存储当前进程和线程标识释放锁时对锁的标识判断如果是自己的则删除不是则放弃操作。
但是这两步操作要保证原子性需要通过Lua脚本来实现。
if redis.call(get,KEYS[1]) ARGV[1] thenredis.call(del,KEYS[1])
end2可重入分布式锁
如果有重入的需求则除了在锁中记录进程标识还要记录重试次数流程如下 下面我们假设锁的key为“lock”hashKey是当前线程的id“threadId”锁自动释放时间假设为20
获取锁的步骤
1、判断lock是否存在 EXISTS lock 存在说明有人获取锁了下面判断是不是自己的锁 判断当前线程id作为hashKey是否存在HEXISTS lock threadId 不存在说明锁已经有了且不是自己获取的锁获取失败end存在说明是自己获取的锁重入次数1HINCRBY lock threadId 1去到步骤3 2、不存在说明可以获取锁HSET key threadId 13、设置锁自动释放时间EXPIRE lock 20
释放锁的步骤
1、判断当前线程id作为hashKey是否存在HEXISTS lock threadId 不存在说明锁已经失效不用管了存在说明锁还在重入次数减1HINCRBY lock threadId -1获取新的重入次数 2、判断重入次数是否为0 为0说明锁全部释放删除keyDEL lock大于0说明锁还在使用重置有效时间EXPIRE lock 20
对应的Lua脚本如下
首先是获取锁
local key KEYS[1]; -- 锁的key
local threadId ARGV[1]; -- 线程唯一标识
local releaseTime ARGV[2]; -- 锁的自动释放时间if(redis.call(exists, key) 0) then -- 判断是否存在redis.call(hset, key, threadId, 1); -- 不存在, 获取锁redis.call(expire, key, releaseTime); -- 设置有效期return 1; -- 返回结果
end;if(redis.call(hexists, key, threadId) 1) then -- 锁已经存在判断threadId是否是自己 redis.call(hincrby, key, threadId, 1); -- 不存在, 获取锁重入次数1redis.call(expire, key, releaseTime); -- 设置有效期return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己获取锁失败然后是释放锁
local key KEYS[1]; -- 锁的key
local threadId ARGV[1]; -- 线程唯一标识
local releaseTime ARGV[2]; -- 锁的自动释放时间if (redis.call(HEXISTS, key, threadId) 0) then -- 判断当前锁是否还是被自己持有return nil; -- 如果已经不是自己则直接返回
end;
local count redis.call(HINCRBY, key, threadId, -1); -- 是自己的锁则重入次数-1if (count 0) then -- 判断是否重入次数是否已经为0redis.call(EXPIRE, key, releaseTime); -- 大于0说明不能释放锁重置有效期然后返回return nil;
elseredis.call(DEL, key); -- 等于0说明可以释放锁直接删除return nil;
end;3高可用的锁
面试官问题redis分布式锁依赖与redis如果redis宕机则锁失效。如何解决
此时大多数同学会回答说搭建主从集群做数据备份。
这样就进入了陷阱因为面试官的下一个问题就来了
面试官问题如果搭建主从集群做数据备份时进程A获取锁master还没有把数据备份到slavemaster宕机slave升级为master此时原来锁失效其它进程也可以获取锁出现安全问题。如何解决
关于这个问题Redis官网给出了解决方案使用RedLock思路可以解决 在Redis的分布式环境中我们假设有N个Redis master。这些节点完全互相独立不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每N)个实例上使用此方法获取和释放锁。在这个样例中我们假设有5个Redis master节点这是一个比较合理的设置所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例这样保证他们不会同时都宕掉。 为了取到锁客户端应该执行以下操作: 获取当前Unix时间以毫秒为单位。依次尝试从N个实例使用相同的key和随机值获取锁。在步骤2当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应客户端应该尽快尝试另外一个Redis实例。客户端使用当前时间减去开始获取锁时间步骤1记录的时间就得到获取锁使用的时间。当且仅当从大多数这里是3个节点的Redis节点都取到锁并且使用的时间小于锁失效时间时锁才算获取成功。如果取到了锁key的真正有效时间等于有效时间减去获取锁所使用的时间步骤3计算的结果。如果因为某些原因获取锁失败没有在至少N/21个Redis实例取到锁或者取锁时间已经超过了有效时间客户端应该在所有的Redis实例上进行解锁即便某些Redis实例根本就没有加锁成功。 3.11.如何实现数据库与缓存数据一致
面试话术
实现方案有下面几种
本地缓存同步当前微服务的数据库数据与缓存数据同步可以直接在数据库修改时加入对Redis的修改逻辑保证一致。跨服务缓存同步服务A调用了服务B并对查询结果缓存。服务B数据库修改可以通过MQ通知服务A服务A修改Redis缓存数据通用方案使用Canal框架伪装成MySQL的salve节点监听MySQL的binLog变化然后修改Redis缓存数据