余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

👌redis是单进程单线程?

发表于 2025-05-09 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis是单进程单线程的吗?

口语化回答

这个问题其实是两个角度来看,我们通常说 redis 说单线程,是因为从接收到客户端的请求,然后 redis 进行内部操作,再返回结果。这个过程是单线程的。单线程的吞吐量可以达到 10w/s,来自 redis 的官方数据,由于 redis 的操作都是在内存中完成,所以性能的瓶颈时内存和带宽,而不是 cpu。如果在这种情况下,使用多线程,反而又要去处理线程安全和数据竞争的问题,还有上下文切换的消耗。那么另一个角度就是除了 redis 执行命令的时候,像持久化数据操作,内存释放操作,这些是由多线程来进行实现的。在 redis6.0 之后,为了减轻网络的性能瓶颈,还增加了多个 io 线程来处理请求。也是一个比较大的优化。所以这个问题,两个角度来看,以上。

题目解析

还是问的比较常见的,一道陷阱题。如果你单纯的说,是单线程的,基本就 gg,面试官会认为的你广度是有问题的。所以说大家一定要答出另一个角度。

面试得分点

操作单线程、多线程异步、多路 io

题目详细答案

Redis核心操作是单线程的。Redis在处理并发请求时有简单、高效和一致性的优点。但是Redis在某些方面使用了额外的线程来处理后台任务。

Redis的主要操作,包括网络IO和键值对读写,确实是由一个线程来完成的。这保证了Redis在处理客户端请求时的简单性和一致性,避免了多线程可能带来的上下文切换开销和竞争条件。利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。这意味着,虽然多个客户端可能同时发送请求,但Redis会将这些请求放入队列中,并逐个处理它们。

Redis 单线程性能高的原因:

1、 高效的 I/O 多路复用:Redis使用网络IO多路复用技术(如epoll)来同时处理多个客户端连接。这使得Redis能够高效地利用系统资源,为大量并发连接提供高性能的服务。官网数据 10w/qps。

2、 由于Redis基于内存操作,并且采用了单线程模型,不需要处理线程切换问题和多线程之间资源竞争,以及锁的问题。

Redis 多线程主要做的事情:

持久化(例如,在保存RDB快照时,Redis会自动fork一个子进程去处理)、异步删除和集群数据同步等。这些任务不会阻塞Redis的主线程,从而确保Redis能够持续地为客户端提供服务。

👌redis的setnx和setex的区别?

发表于 2025-05-09 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis过期策略有哪些?

口语化回答

过期策略主要分为主动和被动,主动又分为定时、定期,被动就是常说的惰性清理。先说结论,redis 采取的方案是定期+惰性配合的方式来进行实现。定期策略主要是通过周期性执行的函数来扫描即将过期的键,立马将其进行失效操作。这种方式比较消耗 cpu。于是产生了定期操作,没隔多少 ms 来进行执行,这种减少了 cpu 的消耗。也能比较准时的删除过期的键。算是定时的一种优化,比较难的点就是寻求平衡。最后就是惰性删除,所有的 key 即使过期了也不会立马删除,当这个键过期之后,下一次访问的时候,才会被删除,容易造成内存泄漏的问题。最后 oom 就会触发内存淘汰策略了,优点就是大大减轻了 cpu 的压力。以上两种方式配合,能达到一个平衡。

题目解析

常考题,很多人把过期策略和淘汰策略混在一起。二者既不同,当惰性删除的时候,又有联系。大家要注意多层面来回答,注意辩证 cpu 性能的问题处理。

面试得分点

定期删除,定时删除,惰性删除,主动于被动

题目详细答案

从行为上,我们可以把过期策略分为两大点。主动删除,被动删除。主动删除又分为定时删除和定期删除。

主动删除

定时删除

当设置键的过期时间时,Redis会为该键创建一个定时器,当过期时间到达时自动删除该键。redis.c 下的 activeExpireCycle 函数实现了定期删除粗略,配合 Redis的服务器的 serverCron函数,在服务器周期执行serverCron 的时候,activeExpireCycle函数就会被调用,在一定的时间内,分多次遍历 redis 中的数据库,从数据库的expires字典中检查一部分键的过期时间,此操作是随机性的,然后删除其中的过期键。

优点:删除操作会在数据到期时立即进行,确保内存及时释放。

缺点:定时器的管理会消耗系统资源,特别是在大量键设置过期时间的情况下,删除 key 会对响应时间和吞吐量产生影响。

定期删除

Redis会定期扫描数据库中的键,并删除其中已过期的键。通过随机抽取一定数量的键,并检查它们是否过期,如果过期就删除,Redis默认每隔100ms(可以通过配置文件中的hz参数进行调整)就执行一次过期扫描任务。

配置redis.conf的hz选项,默认为10,1s刷新的频率。即1秒执行10次,相当于100ms执行一次,hz值越大,说明刷新频率越快,Redis性能损耗也越大

优点:通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,同时能有效释放过期键占用的内存。

缺点:难以确定删除操作执行的时长和频率,如果执行的太频繁,会对CPU造成负担,就变成了定时删除;如果执行的太少,则过期键长时间占用的内存没有及时释放,造成内存浪费。

内存不足

当Redis的内存达到最大限制时,还会触发内存淘汰策略,策略不同决定哪些数据会被删除以腾出空间。
no eviction:禁止淘汰,达到内存限制时拒绝新的写请求。
allkeys-lru:从所有键中淘汰最近最少使用的键。
volatile-lru:从设置了过期时间的键中驱逐最近最少使用的键。
allkeys-random:从所有键中随机驱逐键。
volatile-random:从设置了过期时间的键中随机驱逐键。
volatile-ttl:从设置了过期时间的键中驱逐剩余时间最短的键。

被动删除

惰性删除

Redis不会在键过期时立即删除它,而是在下一次访问这个键时检查其是否过期,然后删除过期的键。假设这个键已经过期,但是后面一直没有被访问,则会永远存在。不会被删除,这就是惰性删除。

惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查。如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除;如果输入键未过期,那么expireIfNeeded函数不做动作。

优点:惰性删除不会增加额外的系统开销,不浪费 cpu,只在访问时进行检查。

缺点:如果某个键永远不会被访问,即使设置了过期时间,它也不会被自动删除,造成内存泄漏问题。

Redis 实际使用的是定期删除+惰性删除的方式!定期删除减少 cpu 消耗和浪费,配合惰性删除,二次检查保险。

👌Redis的从服务器的作用?

发表于 2025-05-07 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌Redis的从服务器的作用?

口语化回答

好的,面试官,从服务器在 redis 集群模式下,起着非常重要的角色。一方面可以复制数据,保证数据不丢失,当主节点发生故障的时候,可以切换到从服务器继续提供服务,还可以为主服务器分担压力,做读写分离,一般 redis 的都是读多写少,我们可以把从服务器承担读请求,把主服务器承担写请求。还有就是可以通过添加从节点,来提供额外的存储和计算能力。以上。

题目解析

业务比较大的情况下,redis 一般都会搭建集群,这道题其实可以考察到,你为什么要选择使用从服务器,看你对于高可用有没有自己的理解,以及集群模式的扩展,故障转移这些有没有一定的考虑。

面试得分点

数据冗余备份、故障转移、读写分离、高可用

题目详细答案

Redis集群中的从服务器(也称为副本或副本节点)在Redis的高可用性和扩展性中扮演着重要角色。主要的作用有以下几点。

提供数据冗余和备份

在集群模式下,Redis集群中的每个主节点可以有一个或多个从节点。这些从节点会复制主节点的数据,从而提供数据的冗余和备份。当主节点出现故障或不可用时,从节点可以接管其工作,确保数据不会丢失,并提高系统的容错能力。

支持故障自动转移

在Redis集群中,当主节点发生故障时,系统可以自动将其中一个从节点升级为主节点,继续处理客户端的请求。这种自动转移的过程称为故障转移,可以确保Redis集群在节点故障时仍然保持高可用性和稳定性。

支持读写分离

在Redis集群配置中,可以将读请求发送到从节点,而将写请求发送到主节点。这种读写分离的配置可以提高系统的并发处理能力和读性能,因为从节点可以处理更多的读请求,而主节点则可以专注于处理写请求。

扩展性能和容量:

通过添加从节点,Redis集群可以扩展其性能和容量。从节点可以分担主节点的负载,并提供额外的存储和计算能力。在Redis集群中,可以动态地添加或删除从节点,以扩展或缩小系统的规模和容量。这种灵活性使得Redis集群能够适应不同的业务需求和工作负载。

👌redis常见性能问题和解决方案

发表于 2025-05-07 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis常见性能问题和解决方案?

口语化回答

redis 常见性能问题主要有内存空间不足,大 key 问题,阻塞操作等等。像内存空间问题,主要发生在大规模的数据量下产生,针对这种我们可以采取数据结构层面的优化,或者集群模式的水平扩。大 key 问题一般就是最初设计的时候,没有考虑清楚,导致业务日积月累,一个小的 key 变成了大 key。会导致我们的性能下降,耗时增加,这种需要从根上进行业务的梳理和处理拆分。还有就是阻塞,如果执行一些 keys 命令会大致阻塞,生产要避免进行这些操作。以上

题目解析

这道题大家主要是从以下的几个问题中,选取常见的 3 个即可。建议大家选择空间不足,大 key 和阻塞,这三点比较好回答,也好解释和扩展。

面试得分点

内存不足,大 key,阻塞,网络延迟,慢查询,持久化性能

题目详细答案

问题一、redis 内存空间不足

****由于Redis的数据存储在内存中,当数据量增大时,可能会出现内存不足的情况,导致性能下降或服务不可用。

解决方案:

内存优化:使用更高效的数据结构(如哈希表、压缩列表)来存储数据,减少内存占用。

水平扩展:使用Redis集群模式,将数据分片存储在多个节点上,扩展内存容量。

问题二、redis 的大 key

****某些键可能存储了大量数据(如大列表、大哈希表),操作这些大键可能导致阻塞,影响性能。

解决方案:

拆分大键:将大键拆分成多个小键,减少单个键的操作时间。

分批处理:对于需要迭代处理的大键,使用SCAN、SSCAN、HSCAN、ZSCAN等命令进行分批处理,避免单次操作时间过长。

监控和预警:定期监控Redis中的大键,及时发现并处理。

问题三、阻塞操作

某些Redis命令(如KEYS、FLUSHALL、SAVE等)会阻塞服务器,导致其他操作无法执行。

解决方案:

避免阻塞命令:尽量避免使用阻塞命令,使用非阻塞的替代命令(如SCAN代替KEYS)。

异步操作:对于需要执行的阻塞操作,尽量使用异步方式(如FLUSHALL ASYNC)。

问题四、网络延迟

Redis是基于TCP协议的网络服务,高网络延迟会影响Redis的性能。

解决方案:

本地部署:尽量将Redis服务器部署在与应用服务器同一内网,减少网络延迟。

连接池:使用连接池来复用Redis连接,减少连接建立和关闭的开销。

问题五、慢查询

****某些复杂的查询或数据操作可能会导致Redis响应变慢,影响整体性能。

解决方案:

慢查询日志:启用Redis的慢查询日志功能,定期检查慢查询并优化。

索引优化:合理使用Redis的数据结构和索引,优化查询性能。

问题六、主从复制延迟

****在主从复制架构中,从服务器可能会因为网络或负载问题导致复制延迟,影响数据一致性。

解决方案:

优化网络:确保主从服务器之间的网络连接稳定,带宽充足。

调整复制参数:优化Redis的复制参数(如repl-backlog-size、repl-timeout等),减少复制延迟。

监控复制状态:定期监控主从复制状态,及时发现并处理延迟问题。

问题七、持久化性能问题

****Redis的持久化操作(如RDB快照和AOF日志)可能会影响性能,尤其是在大数据量或高并发情况下。

解决方案:

合理配置持久化策略:根据业务需求配置合理的持久化策略,平衡性能和数据安全性。

异步持久化:使用异步持久化方式(如AOF的fsync策略),减少对主线程的影响。

原文: https://www.yuque.com/jingdianjichi/xyxdsi/eg6c7clpzgsazknn

👌redis的setnx和setex的区别?

发表于 2025-05-07 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis的setnx和setex的区别?

口语化答案

setnx 和 setex 都是在最基础的 set 命令上面所做的升级。setnx 的一个特性就是如果 key 已经存在,在此放入会放不进去,这种特性也常常用来做分布式锁。如果 setnx 成功,则证明获取到锁成功,没有 set 成功,则证明获取锁失败。setex 只多了一个过期时间的概念,比如我希望本次放入的缓存 3 秒后自动失效,就可以通过这个命令来实现。

题目解析

比较基础的一道题,面试官主要是借这个问题,聊到分布式锁相关的一些处理。注意二者的区别即可。

面试得分点

不存在则设置,过期时间,分布式锁

题目详细答案

SET:最基础的命令,setnx 和 setex 都是在此基础上进行变种。set 命令就是设置键值对,如果已经有值则覆盖,没值就放进去,不涉及过期时间的概念。

SETNX:是一个设置键-值对的命令,但仅在键不存在时才设置该键。如果键已经存在,则不进行任何操作。它是“Set if Not Exists”的缩写,即“如果不存在则设置”。

SETEX:这个命令用于为指定的键设置值及其过期时间。如果键已经存在,SETEX命令将会替换旧的值和过期时间。

命令使用

SETNX的语法为:SETNX key value。其中,key是要设置的键名,value是要设置的值。如果key不存在,则返回1表示设置成功;如果key已经存在,则返回0表示设置失败。

SETEX的语法为:SETEX key seconds value。其中,key是要设置的键名,seconds是过期时间(以秒为单位),value是要设置的值。如果设置成功,则返回“OK”。

应用场景

SETNX常用于分布式场景中的锁机制。例如,在多个客户端同时访问共享资源或执行关键操作时,可以使用SETNX命令尝试在Redis中设置一个特定的键作为锁键,从而确保只有一个客户端能够成功设置该键并执行关键操作。其他执行命令因为设置不成功,所以就可以认为是未获得到锁。

SETEX则用于为键设置值和过期时间。这在需要临时存储数据或限制数据有效期的场景中非常有用。例如,可以使用SETEX命令存储会话信息或缓存数据,并为其设置适当的过期时间以自动删除过期的数据。

👌JVM主要组成部分有哪些?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM主要组成部分有哪些?

题目详细答案

1725898764439-e7db4bb9-4d60-4b85-9b3d-02c5dbfd311c.png

类加载子系统

类加载子系统负责将 .class 文件加载到内存中,并进行验证、准备、解析和初始化。

主要功能:

加载:从文件系统或网络中读取 .class 文件。

验证:确保字节码文件的正确性和安全性。

准备:为类的静态变量分配内存并设置默认初始值。

解析:将符号引用转换为直接引用。

初始化:执行类的静态初始化块和静态变量的初始化。

运行时数据区

JVM 在运行时将内存划分为多个不同的数据区域,每个区域都有特定的用途。

方法区(Method Area):存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等。

堆(Heap):存储所有对象实例和数组,是垃圾收集的主要区域。

Java 栈(Java Stacks):每个线程都有自己的 Java 栈,存储局部变量表、操作数栈、动态链接、方法返回地址等信息。

本地方法栈(Native Method Stacks):为本地方法调用服务,存储本地方法调用的状态。

程序计数器(Program Counter Register):每个线程都有自己的程序计数器,指示当前线程执行的字节码行号。

执行引擎

执行引擎负责执行字节码指令。

主要组件:

解释器(Interpreter):逐条解释执行字节码指令,速度较慢。

即时编译器(Just-In-Time Compiler, JIT):将热点代码(频繁执行的代码)编译为本地机器码,提高执行速度。

垃圾收集器(Garbage Collector, GC):自动管理内存,回收不再使用的对象,防止内存泄漏。

本地接口

本地接口(通常是 Java Native Interface, JNI)允许 Java 代码与本地(非 Java)代码进行交互。

主要功能:

调用本地方法(通常是用 C 或 C++ 编写的)。

允许 Java 代码使用操作系统特性或访问硬件。

本地方法库

本地方法库是存储本地方法实现的动态链接库(如 .dll 文件或 .so 文件)。

主要功能:

提供本地方法的具体实现。

由本地接口调用以执行本地代码。

JVM 主要组成部分的图示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+---------------------------+
| 类加载子系统 (Class Loader Subsystem) |
+---------------------------+
| 运行时数据区 (Runtime Data Area) |
| - 方法区 (Method Area) |
| - 堆 (Heap) |
| - Java 栈 (Java Stacks) |
| - 本地方法栈 (Native Method Stacks) |
| - 程序计数器 (Program Counter) |
+---------------------------+
| 执行引擎 (Execution Engine) |
| - 解释器 (Interpreter) |
| - 即时编译器 (Just-In-Time Compiler, JIT) |
| - 垃圾收集器 (Garbage Collector, GC) |
+---------------------------+
| 本地接口 (Native Interface) |
+---------------------------+
| 本地方法库 (Native Method Libraries) |
+---------------------------+

👌redis的内存用完了会发生什么?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis的内存用完了会发生什么?

redis 内存用完之后发生的现象主要取决于我们配置的内存回收策略。默认是noeviction,这个策略不会删除任何的键,当内存不足的时候,就会报错。这种策略,我们一般不使用。常见使用的就是 lru,回收最近最少使用的有过期时间的键。其他的策略还比如 randow,可以回收随机的键。ttl 按照最短的过期时间来进行回收。以上。

面试得分点

lru,lfu,random,无过期

题目详细答案

当Redis的内存用完时,会根据配置的内存回收策略采取不同的措施。可以在内存达到限制时决定如何处理新的写请求。主要的策略有如下 8 种。

内存回收策略

  1. noeviction:不删除任何键,当内存不足时返回错误。这是默认策略。

当内存达到限制时,Redis将不再接受任何写请求,并返回错误。例如,客户端尝试设置新键时,会收到类似以下的错误信息:

1
(error) OOM command not allowed when used memory > 'maxmemory'.
  1. allkeys-lru:使用最近最少使用(LRU)算法回收所有键。
  2. volatile-lru:使用最近最少使用(LRU)算法回收设置了过期时间的键。

Redis将根据LRU算法选择最近最少使用的键进行删除,以腾出空间存储新的数据。allkeys-lru会在所有键中选择,volatile-lru只会在设置了过期时间的键中选择。

  1. allkeys-random:随机回收所有键。
  2. volatile-random:随机回收设置了过期时间的键。

Redis会随机选择一些键进行删除,以腾出空间。allkeys-random会在所有键中选择,volatile-random只会在设置了过期时间的键中选择。

  1. volatile-ttl:回收那些剩余生存时间(TTL)最短的键。

Redis将选择那些剩余生存时间(TTL)最短的键进行删除。

  1. volatile-lfu:使用最长时间没有被使用(LFU)算法回收设置了过期时间的键。
  2. allkeys-lfu:使用最长时间没有被使用(LFU)算法回收所有键。

Redis将根据LFU算法选择最近最少使用的键进行删除。volatile-lfu只会在设置了过期时间的键中选择,allkeys-lfu会在所有键中选择。

配置内存回收策略的方式

redis.conf文件中配置内存回收策略,例如:

1
2
maxmemory 100mb
maxmemory-policy allkeys-lru

也可通过命令行参数设置:

1
redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru

👌redis的同步机制是什么

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis的同步机制是什么?

口语化回答

redis 的同步机制主要是主从同步,一开始从服务器发送同步命令,主服务器接收到之后,就会生成一个 rdb 的文件,然后传输给从服务器,从服务器接收到之后,立马进行数据的恢复。然后当主服务器再次接收到写命令的时候,会发给从服务器。这个过程是一个异步复制,主服务器不会等待结果。这样就完成了主从复制,主要的核心步骤就是这些。如果同步机制发生问题的话,从服务器可以进行断线重连。还可以做集群、哨兵,来自动切换。以上。

题目解析

主要还是考主从同步原理和如何进行配置,还可以带一点故障的处理。面试官主要是想看看你对集群有没有一定的了解,redis 主从的数据复制有没有了解。

面试得分点

rdb 快照,增量同步,故障机制

题目详细答案

redis的同步机制主要涉及主从复制,主从复制机制允许一个服务器(主服务器)将数据复制到一个或多个服务器(从服务器)。从服务器可以是只读的,也可以接受写操作,但这些写操作不会被同步回主服务器。

初次同步

当从服务器第一次连接到主服务器时,或者当从服务器与主服务器的连接中断后重新连接时,会触发一次全量同步过程。

  1. 从服务器发送SYNC命令:从服务器向主服务器发送SYNC命令,请求进行同步。
  2. 主服务器生成RDB快照:主服务器接收到SYNC命令后,会生成一个RDB(Redis Database)快照文件,并在生成过程中将所有新写入的命令记录到一个缓冲区中。
  3. 传输RDB文件:主服务器将生成的RDB文件发送给从服务器。从服务器接收到RDB文件后,会清空自身的数据库并加载这个RDB文件。
  4. 传输缓冲区中的命令:主服务器将缓冲区中的所有写命令发送给从服务器,从服务器依次执行这些命令,以确保数据完全同步。

增量同步

在初次同步完成后,主从服务器会保持连接状态,主服务器会将后续的所有写命令实时发送给从服务器,从服务器执行这些命令以保持数据的一致性。

同步机制如何配置

主服务器配置

主服务器的配置通常不需要特别设置,只需要确保其能够接受从服务器的连接请求。

从服务器配置

在从服务器的配置文件中,需要指定主服务器的IP地址和端口号:

1
2
replicaof <master-ip> <master-port>
replicaof 192.168.1.100 6379

同步机制的故障处理

断线重连

当从服务器与主服务器的连接中断时,从服务器会自动尝试重连。在重连成功后,从服务器会根据情况选择进行全量同步或增量同步。

主从切换

在高可用环境中,可以使用Redis Sentinel或Redis Cluster来实现自动主从切换。当主服务器发生故障时,Sentinel或Cluster会自动选举一个新的主服务器,并通知其他从服务器进行同步。

👌redis的高级数据类型有哪些?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌redis的高级数据类型有哪些?

口语化回答

面对一些复杂的场景,redis提供了一些高级数据类型,来进行了功能的扩展。主要有四种,bitmaps,hyperloglog,geo,stream。stream 不是非常常用,主要是用来实现消息队列功能。常用的就是 bitmap,bitmap 的 0,1 特性,非常实用于签到,或者存在,不存在这种类型判断,以及在大量数据下,快速统计是否结果。bitmap 非常节省空间,相比于传统的存储数据后,在 mysql 等层面统计,bitmap 更加适用。其次就是hyperloglog 主要是用于一些数量的统计,不过要允许误差,他不会存具体的内容,会帮助我们进行数据的统计,像常见的网站访问统计,就非常适合这个数据结构。geo 主要是做地理位置的计算,通过经度和纬度来定位位置,经过运算可以得到距离,附近范围的坐标等等。像比如美团外卖的附近商家,地图的距离测算,都可以通过 geo 的结构来进行实现,以上。

题目解析

这道题问的比较少,如果在问你基础数据类型的时候,你补了一句,还有三种高级类型,如果面试官感兴趣的话,会继续的追问你。不过三种里面最常用的就是 bitmap,其他用的比较少,重点关注 bitmap 即可。hyperloglog,geo 都不常见,无需关注。作为了解即可。

面试得分点

bitmap,二进制位统计,签到功能,hyperloglog,大数据量统计,geo,地理位置,经纬度,附近的人

题目详细答案

一、 Bitmaps

位图就是一个用二进制位(0和1)来表示数据的结构。可以把它想象成一排开关,每个开关只能是开(1)或者关(0)。这些开关排成一行,从左到右编号,编号从0开始。

目的就是操作某一个位置的数据变成 1 或者 0。

主要操作命令

1
SETBIT jichi 4 1

按照上图,我们其实就是把 4 位设置成了 1。

1
GETBIT jichi 4
1
BITCOUNT jichi  //获取bitmap里面有多少个1

举个例子

基于上面我们按照大家常见的比如用户签到系统,来做一个例子的说明。

假设我们有一个用户签到系统,我们可以用 bitmap 来记录每个用户每天是否签到。比如,一个月有30天,我们可以用30个位来表示这个月的签到情况,我们就可以如此设计。

第1天签到:第0位设为1。第2天没签到:第1位设为0。第3天签到:第2位设为1。以此类推…

这个例子就用上面三个命令即可完成,setbit 设置签到位置,getbit 判断某一天有没有签到,bitcount 获取总共签了多少次到。

假设用户在第1天和第3天签到,那么 bitmap 的值就是下面这样的:

1
101000000000000000000000000000

为什么用 bitmap

类似签到,活跃情况,这些场景,假设我们用数据库存储,可能是一条一条的,统计起来也费时和麻烦,如果使用 bitmap,可以进行非常快速的统计,并且 bitmap 每个位只是二进制位,非常节省空间。

扩展起来,其实比如判断用户有没有权限,假设把某个权限作为一个位置,新增作为 1,删除作为 2,那么这种场景也是可以很快知道用户是否有权限的一种方式。

总之涉及单位置判断的,是否的场景,bitmap 比较靠谱。


二、HyperLogLog

HyperLogLog 用于计算数据集中不重复元素的数量,是 Redis 提供的一种基数统计的数据结构。当我们需要统计大量数据中有多少不同的元素时,直接存储所有元素会占用大量内存。例如,统计一个网站一天内有多少不同的IP地址访问。如果直接存储所有IP地址,内存消耗会非常大。HyperLogLog通过巧妙的数学方法,可以在很小的内存占用下,提供一个非常接近的估算值。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

什么是基数??

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

常用命令

HyperLogLog 在 Redis 中以字符串的形式存在,但是只能作为计数器来使用,并不能获取到集合的原始数据。

主要涉及三个命令:

添加元素:

1
2
PFADD key element1 element2 ...
例如:PFADD jichihll jichi jitui

估算基数:

1
2
3
4
PFCOUNT key
PFCOUNT jichihll

返回的就是 2

合并多个HyperLogLog:

1
PFMERGE destkey sourcekey1 sourcekey2 ...

应用场景

凡是大量的数据下,统计不同数据的数量的情况都可以使用,非常的方便,同时要接受误差的场景。比如

网站访问统计:估算鸡翅 club 网站每天有多少独立访客。

日志分析:估算日志文件中有多少不同的错误类型。

三、 Geospatial Indexes

Geo数据指的是与地理位置相关的数据。简单来说,就是关于“东西在哪里”的数据。它可以描述物体的位置、形状和关系,比如城市的坐标、商店的位置、路线的路径等等。

有主要的三个要素,经度,纬度,和位置名称。

比如鸡哥所在的位置

1
GEOADD jichi 16.281231 37.1231241 jd

常用命令

添加地理位置:

1
2
GEOADD key longitude latitude member [longitude latitude member ...]
GEOADD cities 116.4074 39.9042 "Beijing"

获取地理位置:

1
2
3
4
5
GEOPOS key member
GEOPOS cities "Beijing"
会返回
116.4074
39.9042

计算距离:

1
2
GEODIST key member1 member2 [unit]
GEODIST cities "Beijing" "Shanghai" km(计算北京和上海之间的距离,单位为公里)

查找附近的位置:

1
2
GEORADIUS key longitude latitude radius [unit]
GEORADIUS cities 116.4074 39.9042 100 km(查找北京附近100公里内的所有城市)

查找某个位置附近的位置:

1
2
GEORADIUSBYMEMBER key member radius [unit]
GEORADIUSBYMEMBER cities "Beijing" 100 km(查找北京附近100公里内的所有城市)

georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

georadiusbymember 和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 georadiusbymember 的中心点是由给定的位置元素决定的, 而不是使用经度和纬度来决定中心点。

应用场景

附近的人:比如类似微信的附近的人,以自己为中心,找其他的人,这种场景,就可以使用GEORADIUS 。

基于地理位置推荐:比如推荐某个位置附近的餐厅,都可以实现

计算距离:大家会遇到这种场景,比如当你购物的时候,美团外卖会告诉你商家距您多远,也可以通过 geo 来进行实现。

四、Stream(不是重点)

stream 是 redis5.0 版本后面加入的。比较新,以至于很多老八股题目,都没有提到这个类型。还有就是本身应用度的场景真的不多,类似 mq,但是如果 mq 的场景,大家一般会选择正宗的 rokcetmq 或者 rabbit 或者 kafka,所以这种类型,大家稍微知道即可。

Redis中的流结构用来处理连续不断到达的数据。你可以把它想象成一条流水线,数据像流水一样源源不断地流过来,我们可以在流水线的不同位置对这些数据进行处理。

主要目的是做消息队列,在此之前 redis 曾经使用发布订阅模式来做,但是发布订阅有一个缺点就是消息无法持久化。非常脆弱,redis 宕机,断开这些,都会产生造成丢失。stream 提供了持久化和主备同步机制。

概念解析

消息(Message):流中的每一条数据。每条消息都有一个唯一的ID和一组字段和值。

流(Stream):存储消息的地方。可以把它看作一个消息队列。

消费者组(Consumer Group):一个或多个消费者组成的组,用来处理流中的消息。

消费者(Consumer):处理消息的终端,可以是应用程序或服务。

应用场景

如果需要轻量级,很轻很轻,没有 mq 的情况下,可以使用 redis 来做,适合处理需要实时处理和快速响应的数据。比如做成用户消息实时发送和接收、服务器日志实时记录和分析、传感器数据实时收集和处理。

不过需要注意的是,正常来说 mq,mqtt 等等在各自场景有比较好的应用。

常见命令

添加消息到流:

1
2
3
XADD stream-name * field1 value1 [field2 value2 ...]
XADD mystream * user jichi message "Hello, world!"
他会向流mystream添加一条消息,消息内容是user: jichi, message: "Hello, world!"。

读取消息:

1
2
3
XREAD COUNT count STREAMS stream-name ID
XREAD COUNT 2 STREAMS mystream 0
会从流mystream中读取前两条消息,也就是读取到jichi 的hello world

创建消费者组:

1
2
3
XGROUP CREATE stream-name group-name ID
XGROUP CREATE mystream mygroup 0
会为流mystream创建一个名为mygroup的消费者组。

消费者组读取消息:

1
2
3
XREADGROUP GROUP group-name consumer-name COUNT count STREAMS stream-name ID
XREADGROUP GROUP mygroup consumer1 COUNT 2 STREAMS mystream >
会让消费者组mygroup中的消费者consumer1读取流mystream中的前两条消息。

确认消息处理完成:

消费者处理完成,应该进行 ack。

1
2
3
XACK stream-name group-name ID
XACK mystream mygroup 1526569495631-0
确认消费者组mygroup已经处理完了ID为1526569495631-0的消息。

👌什么情况下redis哨兵模式会产生数据丢失

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌什么情况下redis哨兵模式会产生数据丢失?

口语化答案

其实 redis 在这种情况下的数据丢失,主要就是 4 点,第一点是主从复制的延迟,如果本身复制的过程存在延迟,然后数据还没有从节点的时候,就发生故障,会导致数据丢失。第二个就是故障转移,比如在从节点升级为新主节点的过程中,可能还有部分数据请求写入老节点。也会造成数据丢失。第三代呢就是同步故障,比如一个新的节点刚加入,正同步数据呢,结果主节点挂了,也会丢失。最后一个就是万恶的网络,网络分区会导致集群节点无法通信。以上。

题目解析

这个主要是看你对数据丢失这块,是否有过一定的完整性思考。常见的问题,无非总结就是 延迟,切换,网络三大点。其他的数据丢失题,其实也可以用这个往上套。

面试得分点

主从复制延迟,故障转移写入,同步故障,网络分区

预防措施

为了尽量减少哨兵模式下的数据丢失,可以采取以下措施:

1、 尽量使用半同步复制:通过配置min-slaves-to-write和min-slaves-max-lag参数,确保主节点在写入数据时,至少有一定数量的从节点已同步数据。

2、 优化故障检测和切换参数:根据实际情况优化哨兵的故障检测和切换参数,确保能够及时、准确地进行故障检测和切换。

什么是redis事务机制

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌什么是redis事务机制?

口语化回答

好的,面试官,redis 的事务机制不算是一个像 mysql 一样的正常的事务,只是保证了正常情况下的原子性,一场情况不提供回滚机制。redis 主要是将事务的命令,放在一个队列,一起准备顺序执行。主要是 multi,exec,watch 命令配合使用。使用非常简单,先标记事务开始后,放入命令,再通过 exec 执行即可。以上。

题目解析

实际场景的事务还是用的很少的,这道题也不是特别重点,了解一下 redis 这个事务特性即可。

面试得分点

原子性、不支持回滚、watch 乐观锁

题目详细答案

redis中事务是一组命令的集合,一组命令要么全部执行,要么全部不执行。事务在Redis中是通过流水线(Pipeline)技术实现的,所有命令在执行之前都会被放入一个队列中,直到执行EXEC命令时,所有命令才会按顺序执行。

命令操作

MULTI

MULTI命令用于标记一个事务的开始。执行MULTI后,所有的命令都会被放入一个队列中,而不是立即执行。

1
MULTI

EXEC

EXEC命令用于执行从MULTI命令开始后放入队列中的所有命令。所有命令会按顺序执行,并且在执行过程中不会被其他客户端的命令打断。Redis事务在执行EXEC命令时具有原子性,即所有命令要么全部执行,要么全部不执行。Redis事务并不支持回滚机制。如果在事务执行过程中发生错误,已经执行的命令不会被回滚。

1
EXEC

DISCARD

DISCARD命令用于放弃从MULTI命令开始后放入队列中的所有命令,并且取消事务。

1
DISCARD

WATCH

WATCH命令用于监视一个或多个键,在事务执行之前,如果这些键被其他客户端修改,事务将被中止。WATCH命令通常用于实现乐观锁。这样可以防止事务中的数据竞争问题。

1
WATCH key1 key2

事务的工作原理

事务的执行过程

1、 开始事务:使用MULTI命令开始一个事务。

2、 命令入队:在事务开始之后,所有的命令都会被放入队列中,而不是立即执行。

4、 执行事务:使用EXEC命令执行队列中的所有命令。如果在使用WATCH监视的键在事务执行前被修改,事务将被中止。

4、 放弃事务:使用DISCARD命令可以放弃当前事务队列中的所有命令。

代码示例

1
2
3
4
5
6
7
# 开始事务
MULTI
# 添加命令到事务队列
SET key1 value1
SET key2 value2
# 执行事务
EXEC

如果在事务执行之前,使用WATCH命令监视了某个键,并且该键在事务执行前被修改,事务将被中止:

1
2
3
4
5
6
7
8
9
# 监视键
WATCH key1
# 开始事务
MULTI
# 添加命令到事务队列
SET key1 value1
SET key2 value2
# 执行事务(如果key1在此之前被修改,事务将被中止)
EXEC

什么是redis哈希槽

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌什么是redis哈希槽的概念?

口语化回答

好的,面试官,redis 的哈希槽是集群模式下的一个概念,目的是为了实现数据的分布和管理,水平扩展,哈希槽总共有 16384 个,每个节点管理一部分的哈希槽,然后 key 通过取模算法后映射过来。再根据哈希槽对应的管理节点,就可以找到数据。初始的分配哈希槽可以采用手动指定的方式,也可以采用 redis 的工具的形式分配。有了哈希槽这种方式,能够将数据均匀分布到集群中的各个节点上,避免某些节点过载。同时实现负载均衡,确保每个节点的负载大致均衡。以上。

题目解析

集群模式必考题,如果简历写了 redis 的集群模式,这道题必考,大家一定注意。

面试得分点

哈希槽,取模,集群,数据分配

题目详细答案

哈希槽是Redis集群里的一个概念,主要用于实现数据的分布和管理。集群模式中数据是分布在多个节点上的,哈希槽的机制是确定每个键应该存储在哪个节点上。Redis集群中有16384个哈希槽(编号从0到16383)。每个键在存储之前都会根据其哈希值被映射到其中一个哈希槽。

哈希槽的工作原理

Redis使用CRC16算法对每个键进行哈希计算,然后对16384取模,得到一个哈希槽编号。例如,假设键为”mykey”,其哈希值计算结果为12345,那么12345 % 16384的结果就是哈希槽编号。集群中的每个节点负责管理一定范围的哈希槽。Redis可以把数据分布到多个节点上。例如,节点A可能负责哈希槽0到5000,节点B负责哈希槽5001到10000,节点C负责哈希槽10001到16383。

哈希槽的分配和迁移

初始分配: 集群启动时哈希槽会被分配到不同的节点上。可以手动指定每个节点负责的哈希槽范围,也可以使用Redis提供的工具自动分配。

数据迁移: 当集群中的节点发生变化(如新增节点或删除节点)时,Redis会重新分配哈希槽(重新分片resharding),相应的数据从一个节点迁移到另一个节点。

哈希槽的作用

数据分布: 通过哈希槽机制,Redis能够将数据均匀分布到集群中的各个节点上,避免某些节点过载。

负载均衡: 当集群扩展或缩减时,Redis可以通过重新分配哈希槽来实现负载均衡,确保每个节点的负载大致均衡。

高可用性: Redis集群中的每个节点可以有一个或多个副本(从节点),这些副本也会根据哈希槽进行数据复制,从而提高系统的高可用性和容错能力。

示例

假设我们有一个Redis集群,包含三个节点:节点A、节点B和节点C。哈希槽的分配如下:

  • 节点A:负责哈希槽0到5460
  • 节点B:负责哈希槽5461到10922
  • 节点C:负责哈希槽10923到16383

当我们插入一个键”mykey”时,Redis会计算其哈希值,并映射到相应的哈希槽。例如:

1
2
CRC16("mykey") = 12345
12345 % 16384 = 12345

键”mykey”会被存储在负责哈希槽12345的节点C上。

👌什么是缓存击穿?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌什么是缓存击穿?

口语化回答

好的,面试官,缓存击穿主要是高并发情况下,某个热点key突然失效或者未被缓存,导致大量请求直接穿透到后端数据库,从而使得数据库负载过高,甚至崩溃的问题。经常会有小伙伴和缓存穿透弄混,一个比较好的区分点就是可以理解为单 key,同时重建缓存需要时间。解决这个问题,一般常见的两种方案,一个是互斥锁,在多请求情况下,只有一个请求会去构建缓存,其他的进行等待,这种主要是要考虑好死锁的问题和请求阻塞的问题。另一种就是设置一个逻辑过期时间,去进行异步的缓存更新,缓存本身永远不会过期,这样也就避免了击穿的问题。但是复杂性和逻辑时间的设置就比较考验设计。一般情况下互斥锁方案即可。以上。

题目解析

redis 经典三问之一,要注意和穿透的区分,很多小伙伴分不清,这是不行的。主要是考核大家对于互斥锁的死锁隐患的考虑以及数据一致性的考虑。看是否有这些方面的一些思考。一定要细致理解其中的图和数据。

面试得分点

互斥锁,不过期,一致性,高并发吞吐量

题目详细答案

缓存击穿是指在高并发的情况下,某个热点key突然失效或者未被缓存,导致大量请求直接穿透到后端数据库,从而使得数据库负载过高,甚至崩溃的问题。

这里要注意一个点就是比如构建这个 key 的缓存需要一定的时间,例如当缓存没有,查询数据后,重新放入缓存的过程需要一定的时间,如果这个时候,不进行控制,可能有很多请求都在做同一件事构建缓存,可能会引发数据库的压力剧增,或者影响到第三方服务。

解决方案

1、互斥锁

在缓存失效时,通过加锁机制保证只有一个线程能访问数据库并更新缓存,其他线程等待该线程完成后再读取缓存。核心重点 :只有一个线程访问数据库和建立缓存。

根据上面的流程图,我们可以看到一个非常具体的实现步骤:

  1. 当缓存失效时,尝试获取一个分布式锁。
  2. 获取锁的线程去数据库查询数据并更新缓存。
  3. 其他未获取锁的线程等待锁释放后,再次尝试读取缓存。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public String getValue(String key) {
String value = redis.get(key);
if (value == null) {
// 尝试获取锁
boolean lockAcquired = redis.setnx("lock:" + key, "1");
if (lockAcquired) {
try {
// 双重检查锁,防止重复查询数据库
value = redis.get(key);
if (value == null) {
value = database.query(key);
redis.set(key, value, 3600); // 1小时过期
}
} finally {
// 释放锁
redis.del("lock:" + key);
}
} else {
// 等待锁释放,再次尝试获取缓存
while ((value = redis.get(key)) == null) {
try {
Thread.sleep(100); // 等待100毫秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
return value;
}

注意:锁的实现要确保高效和可靠,避免死锁和性能瓶颈。可以设置锁的过期时间,防止因异常情况导致锁无法释放。

2、不过期

设置一个较长的缓存过期时间,同时在缓存中存储一个逻辑过期时间。当逻辑过期时间到达时,后台异步更新缓存,而不是让用户请求直接穿透到数据库。这种方案可以彻底防止请求打到数据库,不过就是造成了代码实现过于复杂,因为你需要尽可能的保持二者的一致。

实现步骤:

  1. 在缓存中存储数据时,附带一个逻辑过期时间。
  2. 读取缓存时,检查逻辑过期时间是否到达。
  3. 如果逻辑过期时间到达,异步线程去数据库查询新数据并更新缓存,但仍返回旧数据给用户,避免缓存失效时大量请求直接访问数据库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CacheEntry {
private String value;
private long expireTime;

public CacheEntry(String value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}

public String getValue() {
return value;
}

public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}

public String getValue(String key) {
CacheEntry cacheEntry = redis.get(key);
if (cacheEntry == null || cacheEntry.isExpired()) {
// 异步更新缓存
executorService.submit(() -> {
String newValue = database.query(key);
redis.set(key, new CacheEntry(newValue, System.currentTimeMillis() + 3600 * 1000)); // 1小时逻辑过期
});
}
return cacheEntry != null ? cacheEntry.getValue() : null;
}

方案对比

互斥锁要注意的点是,阻塞等待可能会存在死锁或者请求阻塞的情况,降低了高并发的吞吐量。

不过期这种方式,设置逻辑时间是一个非常考验功底的情况,设置的过程,数据不一致性的时间就越长,所以要考虑好方案和业务情况。互斥锁,就不存在这种问题。各有优势,按照情况来进行选择。

👌什么是缓存穿透

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌什么是缓存穿透?

口语化回答

好的,面试官,缓存穿透的最核心就是当高并发请求来的时候,但是 key 在缓存中不存在的时候,就会请求数据库,如果数据库还是没有的话,就会返回,但是这个时候,由于没有数据,也不会存入到缓存中,下次请求过来还会重复这个操作。如果说这个 key 一直没有数据,就会不断的打到数据库中。这就是缓存穿透。缓存穿透主要可以通过缓存空值,布隆过滤器的方式来进行解决。常用的就是缓存空值,当数据库也查询不到的时候,在缓存中将空值写入,这样后面的请求就会命中缓存,不会造成数据库的大压力。布隆过滤器比较适合一些固定值,来进行初步的过滤,这样可以减少误判率,同时减轻压力,以上。

题目解析

redis 算是必问的三个概念之一,其他两个是缓存击穿和缓存雪崩,后面也有介绍。这道题很多人容易把缓存穿透和缓存击穿来弄乱。要注意好识别,还有就是三种常见的解决方案要理解透彻。

面试得分点

穿透的核心概念,缓存空对象解决,布隆过滤器,缓存预热。

题目详细答案

缓存穿透是指在高并发场景下,如果某一个key被高并发访问,但该key在缓存中不存在,那么请求会穿透到数据库查询。如果这个key在数据库中也不存在,就会导致每次请求都要到数据库去查询,给数据库带来压力。严重的缓存穿透会导致数据库宕机。可以根据图看到核心的重点在于不命中和返回空。解决方案也围绕这些即可。

解决方案

1、 缓存空对象

当数据库中查不到数据时,缓存一个空对象(例如一个标记为空或不存在的对象),并给这个空对象的缓存设置一个过期时间。这样,下次再查询该数据时,就可以直接从缓存中拿到空对象,从而避免了不必要的数据库查询。

这种解决方式有两个缺点:

需要缓存层提供更多的内存空间来缓存这些空对象,当空对象很多时,会浪费更多的内存。

会导致缓存层和存储层的数据不一致,即使设置了较短的过期时间,也会在这段时间内造成数据不一致问题。比如缓存还是空对象,这个时候数据库已经有值了。这种引入复杂性,当数据库值变化的时候,要清空缓存。

1
2
3
4
5
6
7
8
9
10
11
String key = "jichiKey";
String value = redis.get(key);
if (value == null) {
value = database.query(key);
if (value == null) {
// 缓存空结果,设置短过期时间
redis.set(key, "", 60); // 60秒过期
} else {
redis.set(key, value, 3600); // 1小时过期
}
}

2、 使用布隆过滤器

布隆过滤器用于检测一个元素是否在集合中。访问缓存和数据库之前,先判断布隆过滤器里面有没有这个 key,如果 key 存在,可以继续往下走,如果 key 不存在,就不用往下进行走了。比较适合数据 key 相对固定的场景。可以减少误识别率。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BloomFilter<String> bloomFilter = new BloomFilter<>(expectedInsertions, falsePositiveProbability);
// 初始化布隆过滤器,插入所有可能存在的键
for (String key : allPossibleKeys) {
bloomFilter.put(key);
}

// 查询时使用布隆过滤器
String key = "jichiKey";
if (!bloomFilter.mightContain(key)) {
// 布隆过滤器判断不存在,直接返回
return null;
} else {
// 布隆过滤器判断可能存在,查询缓存和数据库
String value = redis.get(key);
if (value == null) {
value = database.query(key);
redis.set(key, value, 3600); // 1小时过期
}
return value;
}

3、缓存预热

在系统启动时,提前将热门数据加载到缓存中,可以避免因为请求热门数据而导致的缓存穿透问题。需要根据系统的实际情况和业务需求来判断是否需要对缓存进行预热。比如在一些高并发的系统下,提前预热可以大大减少毛刺的产生,以及提高性能和系统稳定。

缓存预热的经典代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.jingdianjichi.redis.init;

import org.springframework.stereotype.Component;

@Component
public abstract class AbstractCache {

public void initCache(){}

public <T> T getCache(String key){
return null;
}

public void clearCache(){}

public void reloadCache(){
clearCache();
initCache();
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class InitCache implements CommandLineRunner {

@Override
public void run(String... args) throws Exception {
//我要知道哪些缓存需要进行一个预热
ApplicationContext applicationContext = SpringContextUtil.getApplicationContext();
Map<String, AbstractCache> beanMap = applicationContext.getBeansOfType(AbstractCache.class);
//调用init方法
if(beanMap.isEmpty()){
return;
}
for(Map.Entry<String,AbstractCache> entry : beanMap.entrySet()){
AbstractCache abstractCache = (AbstractCache) SpringContextUtil.getBean(entry.getValue().getClass());
abstractCache.initCache();
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component
public class CategoryCache extends AbstractCache {

private static final String CATEGORY_CACHE_KEY = "CATEGORY";

@Autowired
private RedisUtil redisUtil;

@Autowired
private RedisTemplate redisTemplate;

@Override
public void initCache() {
//跟数据库做联动了,跟其他的数据来源进行联动
redisUtil.set("category","知识");
}

@Override
public <T> T getCache(String key) {
if(!redisTemplate.hasKey(key).booleanValue()){
reloadCache();
}
return (T) redisTemplate.opsForValue().get(key);
}

@Override
public void clearCache() {
redisTemplate.delete(CATEGORY_CACHE_KEY);
}
}

👌什么是缓存雪崩?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌什么是缓存雪崩?

口语化回答

好的,面试官,缓存雪崩主要是在同一时间,系统大量缓存失效,这个时候的大量请求都要打到数据库,增加了数据库压力,导致数据库崩溃或者不可用,一般如果产生了雪崩,就是比较严重的后果。雪崩主要的解决方案,一方面是设置合理的缓存过期时间,不要让同一时间失效,尽量的分散。另一个方案就是可以在比如服务刚启动的时候,进行缓存的预热,防止刚启动的时候,大量请求打到数据库。另一方面也要提升缓存架构的高可用,避免因为缓存服务的问题,导致请求打到数据库。以上。

题目解析

经典的缓存三大问题之一。问的也是挺多的,面试官主要是想知道你对他的理解,相比击穿和穿透,雪崩很好理解。要注意的一个点就是雪崩相对应的解决方案要重点记一下。

面试得分点

同时过期失效,设置分散过期时间,热点数据预加载,高可用

题目详细答案

缓存雪崩是指在某一时刻,大量缓存数据同时失效,导致大量的请求直接穿透到数据库,瞬间给数据库带来巨大的压力,可能导致数据库崩溃或服务不可用。

雪崩的常见原因

  1. 缓存数据过期时间相同:当缓存系统中大量数据的过期时间被设置为同一时间点或相近的时间段时,这些数据会同时失效,从而引发缓存雪崩。
  2. 缓存服务器故障:当缓存服务器发生故障时,如果没有有效的容错机制,缓存中的数据将无法被访问,系统可能直接请求后端服务或数据库,导致系统性能下降。

解决方案

设置合理的缓存过期时间

缓存过期时间的设置需要根据业务需求和数据的变化频率来确定。对于不经常变化的数据,可以设置较长的过期时间,以减少对数据库的频繁访问。对于经常变化的数据,可以设置较短的过期时间,确保缓存数据的实时性。总之就是尽量打散缓存的过期时间,最好做到均匀的时间分布,减轻系统同一时刻的压力。

使用热点数据预加载

预先将热点数据加载到缓存中,并设置较长的过期时间,可以避免在同一时间点大量请求直接访问数据库。可以根据业务需求,在系统启动或低峰期进行预热操作,将热点数据提前加载到缓存中。

热点数据预加载可以提升系统的性能和响应速度,减轻数据库的负载。

缓存高可用

缓存做成集群的形式,提高可用性,防止缓存挂掉后,造成的穿透问题。

当缓存服务器发生故障或宕机时,需要有相应的故障转移和降级策略。可以通过监控系统来及时发现缓存故障,并进行自动切换到备份缓存服务器。同时,可以实现降级策略,当缓存失效时,系统可以直接访问数据库,保证系统的可用性。通过缓存故障转移和降级策略,可以保证系统在缓存不可用或故障的情况下仍然可以正常运行,提高系统的稳定性和容错性。

👌使用redis统计网站的uv,应该怎么做

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌使用redis统计网站的uv,应该怎么做

题目详细答案

常见的是使用Set数据结构和HyperLogLog数据结构。

使用Set统计UV

Set是一种集合数据结构,可以存储不重复的元素。将每个访客的唯一标识(如用户ID或IP地址)添加到Set中,可以很方便地统计独立访客数。

  1. 记录访客访问:每次有访客访问时,将其唯一标识添加到当天的Set中。
  2. 获取UV:使用SCARD命令获取Set中元素的数量,即为独立访客数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import redis.clients.jedis.Jedis;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class UVTrackerSet {
private Jedis jedis;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

public UVTrackerSet(String redisHost, int redisPort) {
this.jedis = newJedis(redisHost, redisPort);
}

public void recordVisit(String userId) {
String date= LocalDate.now().format(DATE_FORMATTER);
String key="uv:set:" + date;
jedis.sadd(key, userId);
// 设置键的过期时间为30天,防止内存无限增长
jedis.expire(key, 30 * 24 * 60 * 60);
}

public long getUV(String date) {
String key="uv:set:" + date;
return jedis.scard(key);
}

public long getUVRange(String startDate, String endDate) {
LocalDatestart= LocalDate.parse(startDate, DATE_FORMATTER);
LocalDateend= LocalDate.parse(endDate, DATE_FORMATTER);

String[] keys = start.datesUntil(end.plusDays(1))
.map(date -> "uv:set:" + date.format(DATE_FORMATTER))
.toArray(String[]::new);

StringtempKey="uv:set:range";
jedis.sunionstore(tempKey, keys);
longuvCount= jedis.scard(tempKey);
jedis.del(tempKey);
return uvCount;
}

public static void main(String[] args) {
UVTrackerSet tracker = new UVTrackerSet("localhost", 6379);

// 记录访客访问
tracker.recordVisit("user_123");
tracker.recordVisit("user_456");

// 获取指定日期的UV
Stringtoday= LocalDate.now().format(DATE_FORMATTER);
System.out.println("UV for " + today + ": " + tracker.getUV(today));

// 获取一段时间内的UV
StringstartDate="2023-07-01";
StringendDate="2023-07-07";
System.out.println("UV from " + startDate + " to " + endDate + ": " + tracker.getUVRange(startDate, endDate));
}
}

使用HyperLogLog统计UV

HyperLogLog是一种概率性数据结构,可以在固定的内存空间内提供高效的基数估计。它适合处理大规模数据。

  1. 记录访客访问:每次有访客访问时,将其唯一标识添加到当天的HyperLogLog中。
  2. 获取UV:使用PFCOUNT命令获取HyperLogLog的基数估计。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import redis.clients.jedis.Jedis;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class UVTrackerHLL {

private Jedis jedis;
private static final DateTimeFormatter DATE_FORMATTER= DateTimeFormatter.ofPattern("yyyy-MM-dd");

public UVTrackerHLL(String redisHost, int redisPort) {
this.jedis = newJedis(redisHost, redisPort);
}

public void recordVisit(String userId) {
Stringdate= LocalDate.now().format(DATE_FORMATTER);
Stringkey="uv:hll:" + date;
jedis.pfadd(key, userId);
// 设置键的过期时间为30天,防止内存无限增长
jedis.expire(key, 30 * 24 * 60 * 60);
}

public long getUV(String date) {
Stringkey="uv:hll:" + date;
return jedis.pfcount(key);
}

public long getUVRange(String startDate, String endDate) {
LocalDatestart= LocalDate.parse(startDate, DATE_FORMATTER);
LocalDateend= LocalDate.parse(endDate, DATE_FORMATTER);

String[] keys = start.datesUntil(end.plusDays(1))
.map(date -> "uv:hll:" + date.format(DATE_FORMATTER))
.toArray(String[]::new);

StringtempKey="uv:hll:range";
jedis.pfmerge(tempKey, keys);
longuvCount= jedis.pfcount(tempKey);
jedis.del(tempKey);
return uvCount;
}

public static void main(String[] args) {
UVTrackerHLLtracker=newUVTrackerHLL("localhost", 6379);

// 记录访客访问
tracker.recordVisit("user_123");
tracker.recordVisit("user_456");

// 获取指定日期的UV
Stringtoday= LocalDate.now().format(DATE_FORMATTER);
System.out.println("UV for " + today + ": " + tracker.getUV(today));

// 获取一段时间内的UV
StringstartDate="2023-07-01";
StringendDate="2023-07-07";
System.out.println("UV from " + startDate + " to " + endDate + ": " + tracker.getUVRange(startDate, endDate));
}
}

优缺点对比

set hyperloglog
精准度 精确统计,无误差 存在一定误差(通常在0.81%左右)
占用内存 内存占用较大,尤其是当访客数量很大时 内存占用小,通常只需要12KB内存。
内存占用情况 小数据量,同时对内存不敏感可以 适合大规模数据

👌如果有大量的key需要设置同一时间过期,一般需要注意什么?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌如果有大量的key需要设置同一时间过期,一般需要注意什么?

题目详细答案

大量的 key 同一时间过期,就是非常常见的缓存雪崩场景。

缓存雪崩是指在同一时间大量缓存key同时失效,导致大量请求直接涌向数据库或后端服务,可能引发系统崩溃或性能严重下降。

雪崩解决方案

过期时间随机化:在设置过期时间时,添加一个随机的偏移量,使得不同key的过期时间稍微不同,避免在同一时刻大量key同时失效。

1
2
3
4
5
Random random=new Random();
int baseExpiry=3600; // 基础过期时间,单位为秒
int randomOffset= random.nextInt(300); // 随机偏移量,最大300秒
int finalExpiry= baseExpiry + randomOffset;
redisClient.set(key, value, finalExpiry);

分散过期时间:根据业务逻辑,将key的过期时间分散在不同的时间段内。例如,可以根据key的某些属性(如用户ID、商品ID等)分散设置过期时间。

缓存预热:在缓存失效前,提前预热缓存,确保缓存中始终有数据。

监控报警机制

使用Redis自身的监控工具或第三方监控工具(如Prometheus、Grafana等)监控缓存的命中率、延迟、内存使用等指标。设置报警规则,当缓存命中率下降或延迟增加时,及时发送报警通知,便于快速定位和解决问题。

👌看门狗机制的原理是什么?

发表于 2025-05-06 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌看门狗机制的原理是什么?

口语化答案

在我们平常使用分布式锁的时候,一般会设置一个锁的过期时间,那么如果锁过期的时候,业务还没执行完怎么办,于是就有了看门狗。看门狗机制是一种用于自动续约分布式锁的机制,确保在持有锁的客户端处理完业务逻辑之前,锁不会过期。当客户端获取到锁时,会在 Redis 中设置一个键和一个过期时间(默认30秒)。同时,Redisson 会启动一个后台任务,这个任务会定期检查锁的状态。看门狗任务会每隔一段时间,默认是锁的过期时间的1/3,即10秒,检查锁的状态。如果锁仍然被持有,看门狗任务会将锁的过期时间重置为初始值。这样,锁的过期时间不断被延长,直到客户端明确释放锁或者客户端挂掉。以上。

题目解析

看门狗机制非常常问,一旦问到分布式锁的问题,就会产生这个问题,当你说出异常情况下,传统的分布式锁,采取过期时间释放,防止死锁的时候,就会引入到这个问题。

面试得分点

后台任务续约,高可靠,自动管理

题目详细答案

Redisson 的看门狗机制是一种用于自动续约分布式锁的机制,确保在持有锁的客户端处理完业务逻辑之前,锁不会过期。比如,我们平时使用分布式锁的时候,一般会设置一个锁的过期时间,那么如果锁过期的时候,业务还没执行完怎么办,于是就有了看门狗。

看门狗机制的原理

初始锁定:

当客户端获取到锁时,会在 Redis 中设置一个键(代表锁)和一个过期时间(默认30秒)。同时,Redisson 会启动一个后台任务(看门狗),这个任务会定期检查锁的状态。

自动续约:

看门狗任务会每隔一段时间(默认是锁的过期时间的1/3,即10秒)检查锁的状态。如果锁仍然被持有(即客户端还在持有锁且没有释放),看门狗任务会将锁的过期时间重置为初始值(例如,再次设置为30秒)。这样,锁的过期时间不断被延长,直到客户端明确释放锁或者客户端挂掉。

释放锁:

当客户端完成业务逻辑后,会显式地调用unlock()方法释放锁。一旦锁被释放,看门狗任务会停止续约,锁在 Redis 中的键会被删除或自然过期。

看门狗机制的工作流程

获取锁:客户端请求获取锁,Redis 中创建一个键表示锁,并设置一个过期时间(例如30秒)。启动看门狗任务,定期检查锁的状态。

定期续约:看门狗任务每隔一定时间(例如10秒)检查锁的状态。如果锁仍然被持有(即客户端还在处理业务逻辑),看门狗任务会重置锁的过期时间(例如,再次设置为30秒)。

锁的释放:客户端业务逻辑完成后,调用unlock()方法释放锁。看门狗任务停止续约,锁在 Redis 中的键被删除或自然过期。

看门狗机制的优势

高可靠性:通过自动续约机制,确保锁在持有者处理完业务逻辑之前不会过期,避免了锁意外过期导致的并发问题。

自动管理:无需手动续约锁的过期时间,简化了分布式锁的使用和管理。

容错性:如果客户端在持有锁期间崩溃或断开连接,锁会在过期时间后自动释放,避免了死锁问题。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;

public class RedissonLockExample {
public static void main(String[] args) {
RedissonClient redissonClient = RedissonConfig.createClient();
RLock lock = redissonClient.getLock("myLock");

try {
// 尝试获取锁,等待时间为100秒,锁的过期时间为10秒
if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
try {
// 业务逻辑
System.out.println("Lock acquired, executing business logic");

// 模拟长时间运行的任务
Thread.sleep(20000);

} finally {
lock.unlock();
System.out.println("Lock released");
}
} else {
System.out.println("Could not acquire lock");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
redissonClient.shutdown();
}
}
}

👌分布式锁的特点?

发表于 2025-05-05 | 更新于 2025-06-22 | 分类于 Redis
字数统计 | 阅读时长

👌分布式锁的特点?

实现,zookeeper 的实现。数据库的实现方案基本不常见,这种方式性能很低,还容易影响业务。redis 比较常见使用,主要是用 setnx 的特性来做。成功 set 进去,意味着上锁成功。失败意味着没有获取到锁,业务执行完成后,在通过 del 来释放锁。一般会配合看门狗机制,来做异常的续约处理。以上。

题目详细答案

分布式锁主要有三个特点,是我们要时刻进行注意的。

  1. 互斥性:在某一时刻,只有一个客户端可以持有锁。
  2. 容错性:即使某个持有锁的客户端崩溃或失去连接,锁也能够被其他客户端重新获取。
  3. 高可用性:锁服务需要高可用,通常需要在分布式环境中实现。

分布式锁的实现方式

基于数据库

使用数据库的SELECT … FOR UPDATE 语句或类似的行级锁机制来实现分布式锁。优点是实现简单,缺点是性能较低,依赖于数据库的高可用性。高并发情况下也会对数据库造成非常大的压力。

1
2
3
4
5
-- 获取锁
SELECT * FROM locks WHERE resource = 'resource_name' FOR UPDATE;

-- 释放锁
DELETE FROM locks WHERE resource = 'resource_name';

基于 Redis

Redis 提供了原子操作和高性能的特性,非常适合用来实现分布式锁。通常使用SETNX命令来实现。

1
2
3
4
5
6
7
8
9
10
// 获取锁
String result = jedis.set("lock_key", "lock_value", "NX", "PX", 30000);
if ("OK".equals(result)) {
// 锁获取成功
}

// 释放锁
if (lock_value.equals(jedis.get("lock_key"))) {
jedis.del("lock_key");
}

基于 Zookeeper

Zookeeper 提供了分布式协调服务,可以用来实现分布式锁。通过创建临时顺序节点来实现锁机制。

1
2
3
4
5
6
7
8
9
// 创建一个临时顺序节点
String path = zookeeper.create("/locks/lock-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

// 检查是否获取到锁
List<String> children = zookeeper.getChildren("/locks", false);
Collections.sort(children);
if (path.equals("/locks/" + children.get(0))) {
// 获取到锁
}

分布式锁的使用场景

  1. 分布式事务:在分布式系统中,需要确保多个节点上的操作在同一事务中执行。
  2. 资源共享:如分布式系统中的限流、分布式任务调度等场景。
  3. 数据一致性:在多个节点并发访问同一资源时,确保数据一致性。

分布式锁的常见常见问题

  1. 死锁:如果某个节点在持有锁期间崩溃或失去连接,可能会导致其他节点无法获取锁。
  2. 性能:分布式锁的实现需要考虑性能问题,尤其是在高并发场景下。
  3. 可靠性:锁服务需要高可用,通常需要在分布式环境中实现。

分布式锁的改进

  1. 锁过期时间:设置锁的过期时间,避免死锁问题。
  2. 租约机制:使用租约机制,定期续约锁,确保锁在持有期间不会被其他节点获取。
  3. 锁竞争优化:使用合适的锁竞争算法,减少锁竞争的开销。

👌JVM内存分配的指针碰撞与空闲列表是什么?

发表于 2025-05-04 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

👌JVM内存分配的指针碰撞与空闲列表是什么?

题目详细答案

指针碰撞(Bump-the-pointer)和空闲列表(Free-list)是两种常见的内存分配策略。

指针碰撞(Bump-the-pointer)

指针碰撞是一种高效的内存分配策略,适用于堆内存是连续且规整的情况。这种方法的基本思想是通过移动一个指针来分配内存。具体步骤如下:

  1. 内存布局:堆内存被划分为已使用的内存和空闲的内存,中间有一个指针(称为分配指针)作为分界线。
  2. 分配内存:当需要为新对象分配内存时,只需将分配指针向空闲内存方向移动一段与对象大小相等的距离。
  3. 更新指针:分配指针更新后,新的对象内存区域就被标记为已使用。

这种方法的优点是分配速度非常快,只需简单的指针移动操作。然而,它的缺点是在堆内存不规整(例如存在内存碎片)的情况下无法使用。

1
2
| 已使用内存 | 分配指针 | 空闲内存 |
|------------|----------|----------|

当分配一个对象时,分配指针向右移动:

1
2
| 已使用内存 | 已使用内存 | 分配指针 | 空闲内存 |
|------------|------------|----------|----------|

空闲列表(Free-list)

空闲列表是一种适用于堆内存不规整的情况下的内存分配策略。它通过维护一个列表来记录所有可用的空闲内存块。具体步骤如下:

  1. 空闲列表:JVM 维护一个空闲列表,记录所有可用的内存块及其大小。
  2. 查找空闲块:当需要为新对象分配内存时,JVM 会在空闲列表中查找一个足够大的内存块。
  3. 分配内存:找到合适的内存块后,将其从空闲列表中移除,并将其标记为已使用。如果内存块大于所需大小,可能会将剩余部分重新放回空闲列表中。
  4. 回收内存:当对象被垃圾回收器回收后,JVM 会将其内存块重新添加到空闲列表中。

这种方法的优点是能够更好地利用内存,适用于内存碎片较多的情况。然而,它的缺点是分配速度较慢,因为需要在空闲列表中查找合适的内存块。

假设空闲列表如下:

1
空闲列表: [块1(大小: 32), 块2(大小: 64), 块3(大小: 128)]

当需要分配一个大小为 50 的对象时,JVM 会在空闲列表中查找:

1
找到块2(大小: 64)

将块2分成两部分:

1
2
分配块2的前50个单位,剩余部分重新放回空闲列表
空闲列表: [块1(大小: 32), 块2剩余部分(大小: 14), 块3(大小: 128)]

指针碰撞(Bump-the-pointer):适用于堆内存规整的情况,分配速度快,但不适用于内存碎片较多的情况。

空闲列表(Free-list):适用于堆内存不规整的情况,能够更好地利用内存,但分配速度较慢。

这两种内存分配策略各有优缺点,JVM 会根据具体情况选择合适的策略,以优化内存分配和垃圾回收的效率。

<i class="fa fa-angle-left"></i>123…12<i class="fa fa-angle-right"></i>

232 日志
18 分类
28 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini