ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

redis面试问题

redis面试问题 redis为什么快DeepSeek回答一、核心答案必答项面试官问这个问题最想听到以下三个最根本的原因基于内存操作Redis的数据完全存储在内存中读写操作直接操作内存避免了磁盘I/O的开销。这是Redis快的最主要原因。高效的数据结构Redis为不同场景设计了专门的高效数据结构如SDS、跳表、哈希表等这些结构在内存中经过高度优化操作速度极快。单线程架构核心Redis使用单线程模型来处理网络请求和命令执行避免了多线程的上下文切换开销和竞态条件没有锁竞争让CPU专注于处理数据。二、加分项展示深度在说完核心点后可以补充以下两点体现你对Redis的深入理解I/O多路复用机制Redis利用epoll/kqueue等I/O多路复用技术让一个单线程能高效监听并处理大量并发连接实现了高吞吐量。这也正是它能用单线程支撑高并发的关键。高效的数据持久化与内存管理Redis提供了高效的持久化机制且底层使用jemalloc等高效内存分配器减少内存碎片进一步提升性能。三、避坑提示防追问面试官可能会追问“Redis是单线程的吗”届时你可以这样精准回答“Redis的网络IO和命令执行核心确实是单线程的保证了数据操作的原子性。但Redis在6.0版本后引入了多线程来处理网络IO读写以提升性能。此外像持久化、异步删除、集群同步等后台任务是由其他线程负责的。”redis为什么快内存存储数据存储方式Redis 将数据存储在内存中这使得数据的读写操作可以直接在内存中进行避免了磁盘 I/O 的开销。内存的访问速度远远高于磁盘能够极大地提高数据的读写性能。高效的数据结构Redis 针对不同的使用场景设计了多种高效的数据结构如字符串SDS、哈希表Dict、链表Adlist、跳表Zskiplist等。这些数据结构经过精心优化能够在内存中高效地存储和操作数据。例如字符串对象在 Redis 中使用了简单动态字符串SDS实现它不仅支持高效的字符串操作还能自动进行内存管理避免了传统 C 字符串可能出现的缓冲区溢出问题。单线程架构redis是很快的官方表示redis是基于内存操作cpu不是redis性能瓶颈redis的瓶颈是根据机器的内存和网络带宽。redis为什么单线程还这么快误区1高性能的服务器一定是多线程的。误区2多线程cpu上下文会切换一定比单线程效率高。核心redis是将所有的数据全部放在内存中的所以说使用单线程去操作效率就是最高的对于内存系统来说如果没有上下文切换效率就是最高的。多次读写都是在一个cpu上的在内存情况下这个就是最佳的方案避免线程切换开销Redis 采用单线程模型避免了多线程环境下的线程切换开销。在多线程程序中线程切换需要保存和恢复上下文信息这会消耗一定的 CPU 时间。而 Redis 在单线程模式下无需进行线程切换从而提高了执行效率。简化编程模型单线程架构使得 Redis 的编程模型更加简单避免了多线程并发带来的复杂问题如死锁、竞态条件等。开发人员可以更专注于数据结构和算法的设计提高了代码的可读性和可维护性。高效的网络模型非阻塞 I/ORedis 使用非阻塞 I/O 模型能够同时处理多个客户端连接。当一个客户端的请求到达时Redis 不会被阻塞在这个请求上而是可以继续处理其他客户端的请求。这种方式可以充分利用 CPU 资源提高服务器的并发处理能力。事件驱动todo完全不明白是什么意思有时间再说吧Redis 采用事件驱动的编程模型通过 epoll、kqueue 等多路复用技术监听多个文件描述符的事件。当有事件发生时Redis 会迅速响应并处理相应的事件。这种方式可以高效地处理大量的并发连接提高服务器的吞吐量。数据持久化的优化多种持久化方式Redis 提供了多种持久化方式如 RDB快照和 AOFAppend Only File。可以根据实际需求选择合适的持久化方式在保证数据安全性的同时尽量减少对性能的影响。RDB 持久化通过创建数据的快照来保存数据在恢复数据时速度非常快。而 AOF 持久化则通过记录所有的写操作命令来保证数据的完整性在数据恢复时可以重放这些命令来重建数据。后台执行持久化操作Redis 在进行持久化操作时会尽量避免影响服务器的正常运行。例如在进行 RDB 快照生成时Redis 会使用 fork 子进程的方式来创建快照父进程可以继续处理客户端请求。而在进行 AOF 重写时Redis 也会在后台进行不会阻塞客户端的写操作。redis是单线程的还是多线程的因为Redis的瓶颈不是cpu的运行速度而往往是网络带宽和机器的内存大小。再说了单线程切换开销小容易实现既然单线程容易实现而且CPU不会成为瓶颈那就顺理成章地采用单线程的方案了Redis是单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的6.0之后网络IO是多线程的需要手动开启这也是Redis对外提供键值存储服务的主要流程。但Redis的其他功能比如持久化、异步删除、集群数据同步等其实是由额外的线程执行的。Redis 从4.0版本开始就有了多线程的概念虽然处理命令请求的核心模块确实是保证了单线程执行然而在其他许多地方已经有了多线程比如在后台删除对象通过 Redis 模块实现阻塞命令生成dump文件以及6.0版本中网络 I/O实现了多线程等而且在未来 Redis 应该会有越来越多的模块实现多线程。所谓的单线程只是说 Redis 的处理客户端的请求即执行命令时是单线程去执行的并不是说整个 Redis 都是单线程。redis是单线程的还是多线程的 第二种解释在一开始的时候Redis采用的是单线程模型因为Redis是一个基于内存的数据库将所有的数据放入内存所以使用单线程的操作效率是最高的多线程会上下文切换消耗大量时间对于内存系统来说单线程才能产生更高的效率。但是单线程不意味着整个Redis就一个线程Redis其他模块还有各自的线程。使用I/O多路复用机制同时监听多个客户端在单线程模式下即使网络处理很多因为存在I/O多路复用依然可以在高速的内存处理下得到忽略。Redis为什么引入多线程Redis基于内存操作CPU并不是性能瓶颈Redis的瓶颈是根据机器的内存和网络带宽。在6.0的版本中引入了多线程。这个I/O threads 指的是网络I/O处理方面使用了多线程如网络数据的读写和协议解析等这是因为读写网络的read/write在Redis执行期间占用了大部分CPU时间如果把这部分模块使用多线程方式实现会对性能带来极大地提升。但是Redis执行命令的核心模块还是单线程的。需要注意的是在 Redis6.0 中多线程机制默认是关闭的需要在 redis.conf 中完成以下两个设置才能启用多线程。1设置 io-thread-do-reads 配置项为 yes表示启用多线程。io-threads-do-reads yes2设置线程个数。⼀般来说线程个数要小于 Redis 实例所在机器的 CPU 核数 例如对于⼀个 8 核的机器来说Redis 官⽅建议配置 6 个 IO 线程。io-threads6单线程的缺点单线程处理最大的缺点就是如果前一个请求发生耗时比较久的操作那么整个Redis都会被阻塞其他请求也无法进来直到这个耗时久的操作处理完成并返回其他请求才能被处理到。我们平时遇到Redis响应变慢或长时间阻塞的问题大部分都是因为Redis处理请求是单线程这个原因导致的。所以我们在使用Redis时一定要避免非常耗时的操作例如使用时间复杂度过高的方式获取数据、一次性获取过多的数据、大量key集中过期导致Redis淘汰key压力变大等等这些场景都会阻塞住整个处理线程直到它们处理完成势必会影响业务的访问。如果万一CPU成为你的Redis瓶颈了或者你就是不想让服务器其他核闲置那怎么办那也很简单你多起几个Redis进程就好了。Redis是keyvalue数据库又不是关系数据库数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。redis-cluster可以帮你做的更好。redis内存满了怎么办redis集群加服务器。使用内存淘汰策略。假如Redis里面有1亿个key其中有10w个key是以某个固定的已知的前缀开头的如何将它们全部找出来KEYS 的速度非常快例如Redis在一个有1百万个key的数据库里面执行一次查询需要的时间是40毫秒 。但在一个大的数据库中使用它仍然可能造成性能问题.假如Redis里面有1亿个key其中有10w个key是以某个固定的已知的前缀开头的如果将它们全部找出来答使用keys指令可以扫出指定模式的key列表。对方接着追问如果这个redis正在给线上的业务提供服务那使用keys指令会有什么问题这个时候你要回答redis关键的一个特性redis的单线程的。keys指令会导致线程阻塞一段时间线上服务会停顿直到指令执行完毕服务才能恢复。这个时候可以使用scan指令scan指令可以无阻塞的提取出指定模式的key列表但是会有一定的重复概率在客户端做一次去重就可以了但是整体所花费的时间会比直接用keys指令长。keys命令的原理就是扫描整个redis里面所有的db的key数据然后根据我们的通配的字符串进行模糊查找出来。更为致命的是这个命令会阻塞redis多路复用的io主线程如果这个线程阻塞在此执行之间其他的发送向redis服务端的命令都会阻塞从而引发一系列级联反应导致瞬间响应卡顿从而引发超时等问题所以应该在生产环境禁止用使用keys和类似的命令smembers这种时间复杂度为ON且会阻塞主线程的命令是非常危险的。取而代之的如果需要查找然后删除key的需求那么在生产环境我们应该使用scan命令代替keys命令同样是ON复杂度的scan命令支持通配查找scan命令或者其他的scan如SSCANHSCANZSCAN命令可以不用阻塞主线程并支持游标按批次迭代返回数据所以是比较理想的选择。keys相比scan命令优点是keys是一次返回而scan是需要迭代多次返回。但scan命令的也有缺点返回的数据有可能重复需要我们在业务层按需要去重scan命令的游标从0开始也从0结束每次返回的数据都会返回下一次游标应该传的值我们根据这个值再去进行下一次的访问如果返回的数据为空并不代表没有数据了只有游标返回的值是0的情况下代表结束如果使用redis当key很多的时候如何进行筛选当key很多可以把key存放到集合中例如list集合set集合。商品的放一个集合其他的放一个集合。系统使用了缓存使用redis作为缓存服务器在数据缓存的时候有热点数据和冷门数据如何保证缓存服务器存放的是热点数据冷门数据并不会太多的占用缓存服务器的资源首先热点、冷门数据并不是规定好的所以不能以字段来做判断。解决办法使用过期时间热点数据经常使用如果到了过期时间自动清理了再次有人访问就加回来。Redis在你们项目中是怎么用的代驾项目呼叫代驾的功能中使用redis的set集合以oderId为key司机id为val避免把一个订单重复推送给代驾司机代驾项目中把订单推送给司机的时候用的是redis的list集合key为司机idval为订单信息代驾项目中抢单功能为了避免两个司机抢到同一个订单使用了Redisson来做分布式锁代驾项目中使用redis的GEO来存储定位。商城项目中提交订单时幂等性的保证商城项目中秒杀服务商品预热的时候加redis分布式锁保证只有一个定时任务执行商城项目中秒杀服务秒杀商品用redis的信号量如果抢到直接返回秒杀成功的消息后续交给rabbitmq来处理java和redis的hashmap扩容的区别在 Java 和 Redis 中HashMap 的扩容机制存在一些区别具体如下一、Java 中的 HashMap 扩容触发条件当 HashMap 中的元素数量size超过了负载因子load factor与容量capacity的乘积时就会触发扩容。默认的负载因子是 0.75。例如如果初始容量为 16那么当元素数量达到 16 * 0.75 12 时就可能会触发扩容。扩容方式Java 中的 HashMap 扩容是通过创建一个新的更大容量的数组并将原有的键值对重新哈希到新数组中来实现的。新的容量通常是原容量的两倍。在重新哈希的过程中需要对每个键重新计算哈希值并确定其在新数组中的位置。这个过程可能比较耗时特别是当 HashMap 中存储了大量元素时。性能影响扩容操作会导致性能下降因为需要重新计算哈希值和复制元素。频繁的扩容会严重影响性能因此在使用 HashMap 时可以根据预期的元素数量提前设置合适的初始容量以减少扩容的次数。二、Redis 中的哈希表hashmap 的一种实现扩容触发条件Redis 中的哈希表在负载因子超过一定阈值时也会触发扩容。具体的负载因子可以通过配置参数进行调整。与 Java 不同的是Redis 中的哈希表不仅在元素数量增加时可能触发扩容当哈希表中的键值对被频繁删除导致负载因子过低时也可能会触发缩容操作。扩容方式Redis 中的哈希表扩容是渐进式的。当触发扩容时Redis 不会立即创建一个新的巨大的哈希表并进行一次性的重新哈希而是逐步将旧哈希表中的键值对迁移到新哈希表中。在迁移过程中Redis 仍然可以正常处理客户端的请求只是在处理请求的同时会顺便将涉及到的键值对迁移到新哈希表中。这样可以避免因一次性扩容带来的长时间阻塞。性能影响由于采用渐进式扩容Redis 的哈希表扩容对性能的影响相对较小。在扩容过程中虽然可能会有一些额外的开销但不会导致服务长时间不可用。总之Java 中的 HashMap 扩容是一次性的、可能比较耗时的操作而 Redis 中的哈希表扩容是渐进式的对性能的影响相对较小。在实际应用中可以根据具体的需求和场景选择合适的哈希表实现。redis的大key会发生什么问题有什么影响什么是Redis大key问题没有固定的判别标准通常认为字符串类型的key对应的value值占用空间大于1M或者集合类型的k元素数量超过1万个就算是大key。大key带来的影响内存占用过高。大Key占用过多的内存空间可能导致可用内存不足从而触发内存淘汰策略。在极端情况下可能导致内存耗尽Redis实例崩溃影响系统的稳定性。性能下降。大Key会占用大量内存空间导致内存碎片增加进而影响Redis的性能。对于大Key的操作如读取、写入、删除等都会消耗更多的CPU时间和内存资源进一步降低系统性能。阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如使用DEL命令删除一个大Key时可能会导致Redis实例在一段时间内无法响应其他客户端请求从而影响系统的响应时间和吞吐量。怎么解决大key拆分成多个小key。这是最容易想到的办法降低单key的大小读取可以用mget批量读取。设置合理的过期时间。为每个key设置过期时间并设置合理的过期时间以便在数据失效后自动清理避免长时间累积的大Key问题。启用内存淘汰策略。启用Redis的内存淘汰策略例如LRULeast Recently Used最近最少使用以便在内存不足时自动淘汰最近最少使用的数据防止大Key长时间占用内存。redis和mysql事务的区别是什么Redis的事务只是一组命令的集合按顺序执行这组命令中有部分命令执行失败其他的命令还是会执行不会回滚这么设计的目的是批量执行命令提高效率减少网络开销而不是保证数据可靠性。啥时候使用 Redis 的事务呢如果我们只是需要把多个操作打包进行使用 Redis 的事务是比较合适的例如 “超卖场景”。例如现在有一款手机只放货了 500 台但如果让 501 个人下单成功就构成了超卖。比如我现在有一种典型的写法来记录这 500 台手机的个数Redis 在集群环境中部署时就无法使用“事务”Redis 中事务的操作命令开启事务MULTI从下图可以看到在开启事务后set 命令并没有生效只是返回了一个 queued这就说明当前的命令只是被放入了事务队列中并没有生效此时如果通过另一个客户端获取的话是获取不到的。执行事务EXEC输入 exec 命令之后队列中的命令就会全部执行。放弃当前事务discard当开启时候后并且给服务器发起若干个命令之后此时服务器重启了此时的这个事务咋办此时的效果其实就等同于 discardwatch命令watch的作用监控某个 key 是否在执行事务之前发生了改变如下图有这样一种场景客户端1开启了事务并且设置了 key但是在执行事务之前客户端2 修改了key此时按照常理来讲key 应该是 222但是因为在客户端1中只是将命令放到了队列中并没有实际的去执行只有在输入 exec 命令之后才会执行所以最终的 key 的 value 是 111对于上述这种在事务执行完之前但key 发生了变化此时就可以使用 watch 进行监控如下图当 watch 监控了 key 开启事务之后在 set key 111 执行前在另一个客户端中对 key 进行了修改当在客户端1中输入 exec 命令执行事务的时候就会执行失败如果没有使用 watch 进行监控的话最后key 的value值就是 111watch 的实现原理watch 是基于版本号机制实现的如下图例如在执行 watch key 时会给 key 设置一个“版本号”可以理解成是一个整数每次修改 key 版本号都会变大。假设初始情况下key 的版本号为 1如果从事务开启时到事务执行之前对 key 进行了修改此时 key 的版本号就会变大当执行事务时也就是 exec 时就会判断一下当前 key 的版本号和key watch 时的初始版本号是否一致如果一致才会真正的设置如果不一致则返回nil如果redis key过期了我们有什么方式可以感知到有一次面试的时候被问到如果设计一个红包假设这个红包的过期时间是24h怎么设计我回答通过延时队列死信队列的方式面试官说那比较适合时间比较短的场景24h还是有点长的我又回答通过设置redis的过期时间他说那如果过期了怎么感知到呢我哑巴了当时想应该就是监听器之类的东西但具体的方案是想不出来的。解决使用 Redis 的键空间通知Redis 可以配置为发送键空间通知当一个键过期时会发送一个特定的通知消息。你可以使用 Redis 的订阅 / 发布功能来监听这些通知。具体步骤如下配置 Redis 以启用键空间通知在 Redis 配置文件中将 notify-keyspace-events 设置为合适的值例如 Kxge表示关注键过期和键被删除事件。然后在 Java 中可以使用 Jedis 库来实现订阅和处理键空间通知packagecom.example.redpacket.service.impl;importcom.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;importcom.example.redpacket.entity.RedPacket;importcom.example.redpacket.mapper.RedPacketMapper;importcom.example.redpacket.service.RedPacketService;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importjava.math.BigDecimal;importjava.time.LocalDateTime;importjava.time.temporal.ChronoUnit;importjava.util.concurrent.TimeUnit;Slf4jServicepublicclassRedPacketServiceImplimplementsRedPacketService{privatestaticfinalStringREDIS_KEY_PREFIXred_packet:;privatestaticfinallongEXPIRE_HOURS24;AutowiredprivateRedPacketMapperredPacketMapper;AutowiredprivateRedisTemplateString,ObjectredisTemplate;OverrideTransactionalpublicRedPacketcreateRedPacket(LonguserId,BigDecimalamount){// 1. 入库RedPacketpacketnewRedPacket();packet.setUserId(userId);packet.setAmount(amount);packet.setStatus(1);packet.setExpireTime(LocalDateTime.now().plus(EXPIRE_HOURS,ChronoUnit.HOURS));redPacketMapper.insert(packet);log.info(红包创建成功, id: {}, expireTime: {},packet.getId(),packet.getExpireTime());// 2. 设置Redis key过期时间为24小时StringredisKeyREDIS_KEY_PREFIXpacket.getId();redisTemplate.opsForValue().set(redisKey,1,EXPIRE_HOURS,TimeUnit.HOURS);log.debug(Redis key已设置: {},redisKey);returnpacket;}OverridepublicRedPacketgetRedPacket(Longid){RedPacketpacketredPacketMapper.selectById(id);if(packetnull){returnnull;}// 实时检查过期被动触发if(packet.getStatus()1packet.getExpireTime().isBefore(LocalDateTime.now())){log.info(红包 {} 查询时发现已过期主动更新状态,id);redPacketMapper.expireIfActive(id);packet.setStatus(3);}returnpacket;}OverrideTransactionalpublicvoidhandleExpireEvent(Longid){// 幂等处理只有状态为1时才更新intupdatedredPacketMapper.expireIfActive(id);if(updated0){log.info(红包 {} 过期事件处理成功状态更新为已过期,id);}else{log.debug(红包 {} 过期事件已处理或状态已变更无需重复处理,id);}}OverridepublicintbatchExpire(){intcountredPacketMapper.batchExpire();if(count0){log.info(批量过期补偿完成共处理 {} 个红包,count);}returncount;}}packagecom.example.redpacket.listener;importcom.example.redpacket.service.RedPacketService;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.CommandLineRunner;importorg.springframework.data.redis.connection.Message;importorg.springframework.data.redis.connection.MessageListener;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.listener.PatternTopic;importorg.springframework.data.redis.listener.RedisMessageListenerContainer;importorg.springframework.stereotype.Component;importjavax.annotation.Resource;Slf4jComponentpublicclassRedisKeyExpirationListenerimplementsCommandLineRunner{privatestaticfinalStringEXPIRED_CHANNEL__keyevent0__:expired;privatestaticfinalStringKEY_PREFIXred_packet:;ResourceprivateRedisConnectionFactoryredisConnectionFactory;AutowiredprivateRedPacketServiceredPacketService;Overridepublicvoidrun(String...args){RedisMessageListenerContainercontainernewRedisMessageListenerContainer();container.setConnectionFactory(redisConnectionFactory);container.addMessageListener(newMessageListener(){OverridepublicvoidonMessage(Messagemessage,byte[]pattern){StringexpiredKeymessage.toString();log.debug(收到Redis过期事件key: {},expiredKey);// 只处理红包相关的keyif(expiredKey!nullexpiredKey.startsWith(KEY_PREFIX)){StringidStrexpiredKey.substring(KEY_PREFIX.length());try{LongidLong.parseLong(idStr);log.info(红包 {} 过期触发失效处理,id);redPacketService.handleExpireEvent(id);}catch(NumberFormatExceptione){log.error(解析红包ID失败key: {},expiredKey,e);}}}},newPatternTopic(EXPIRED_CHANNEL));container.afterPropertiesSet();container.start();log.info(Redis过期事件监听器已启动监听频道: {},EXPIRED_CHANNEL);}}在上述代码中创建了一个 Jedis 连接到本地的 Redis 服务器并定义了一个JedisPubSub的匿名内部类来处理接收到的通知消息。当接收到键过期的通知时会打印出过期的键名。请注意这种方式需要确保 Redis 服务器正确配置了键空间通知功能并且在实际应用中你可能需要根据具体情况进行适当的错误处理和资源管理。部分内容转载自https://zhuanlan.zhihu.com/p/81195864https://juejin.cn/post/6993902033178722312https://juejin.cn/post/6915599025882431501https://blog.csdn.net/lxw1844912514/article/details/118526528https://www.cnblogs.com/gaojinhang/articles/17743295.htmlhttps://blog.csdn.net/Shine0115/article/details/140911645她口中的词汇越来越少只剩下“这个、那个”。有一次家里的洗衣机洗完衣服“滴滴”响了刘丹想提醒陈玉去晾衣服跟她说“那个”响了。陈玉鼓励妈妈说出洗衣机这个词但她怎么都说不出来。她偶尔也会忘记家人的名字。陈文不在身边的时候刘丹会问女儿“那个人去哪里了”状态好的时候她又能说出来老伴儿的名字。https://new.qq.com/omn/20220329/20220329A0AQ0000.html?pgv_refaio2015ptlang2052
返回列表