余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

学习笔记

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

springcloud服务中使用mq做消息的通知,相对于fegin有什么好处

在 Spring Cloud 服务中使用 消息队列(如 RabbitMQ、Kafka 等)来处理消息通知,相对于 Feign 进行同步调用,具有多个优势,特别是在分布式系统中。这些好处主要体现在 解耦性、可靠性、性能和可扩展性 等方面。下面详细列出它们的优缺点对比:

1. 解耦性

  • 消息队列:使用消息队列时,生产者(发送方)和消费者(接收方)是松耦合的,即使消费者不可用,消息队列仍然可以存储消息,等待消费者处理。这种异步的方式减少了服务之间的直接依赖,增强了系统的解耦性。
  • Feign:Feign 是一种同步调用方式,服务 A 直接调用服务 B,服务之间是强耦合的。如果服务 B 不可用,服务 A 会失败,依赖关系比较紧密。

优点:

  • 消息队列:更容易扩展服务,减少服务间的直接依赖。
  • Feign:服务间的直接调用,导致服务耦合性较强,依赖性高。

2. 可靠性

  • 消息队列:消息队列一般会提供消息持久化、重试机制、死信队列等功能,保证消息在系统出现故障时不会丢失,能够在消费者恢复后重新处理。即使服务 A 出现故障,消息依然可以在队列中等待消费。
  • Feign:Feign 调用是同步的,如果服务 B 出现故障,Feign 请求会直接失败,且一般不具备自动重试和消息持久化功能。

优点:

  • 消息队列:提供消息持久化、重试机制,能确保消息不丢失。
  • Feign:服务失败时请求会直接失败,除非使用像 Ribbon 或 Hystrix 这样的断路器进行处理。

3. 异步与性能

  • 消息队列:消息队列是异步处理的,即生产者发送完消息后,不需要等待消费者处理结果。生产者可以继续执行其他任务,消息队列通过缓冲提高系统吞吐量。当系统负载较高时,消费者可以批量消费消息,提高处理效率。
  • Feign:Feign 是同步的,发送请求后,调用方必须等待服务返回结果,可能会导致性能瓶颈,特别是当多个服务调用彼此依赖时,响应时间可能会增加。

优点:

  • 消息队列:通过异步处理提高系统吞吐量和性能,适合处理高并发、高负载的场景。
  • Feign:同步请求,可能会成为性能瓶颈,尤其在服务间相互调用较多时。

4. 可伸缩性

  • 消息队列:消息队列可以在消费者端进行水平扩展。例如,消费者可以横向扩展多个实例来处理消息,从而提升系统的处理能力。消息队列会自动将消息分发给空闲的消费者实例,提高系统的可伸缩性。
  • Feign:Feign 本身并不具备内建的负载均衡功能,虽然可以使用 Spring Cloud 的负载均衡组件(如 Ribbon),但它依赖于同步请求,因此在高并发场景下扩展性较差。

优点:

  • 消息队列:通过消费端的水平扩展,提高系统的可伸缩性。
  • Feign:虽然有负载均衡,但扩展性主要依赖于同步调用方式,可能会受到性能瓶颈的限制。

5. 流量控制与限流

  • 消息队列:消息队列通常支持流量控制。比如,消费者的处理速度低于生产者的发送速度时,消息队列可以进行流量调节,避免系统被大量消息打垮。此外,死信队列、延时队列等功能可以帮助处理异常消息,避免堆积。
  • Feign:Feign 是同步请求,不适合处理流量控制。如果请求量激增,可能会导致服务出现超时或者崩溃。

优点:

  • 消息队列:可以通过流量控制机制、死信队列、延时队列等功能保证系统稳定性。
  • Feign:不适合直接进行流量控制,可能导致系统超载。

6. 消息顺序

  • 消息队列:许多消息队列系统(如 Kafka 和 RabbitMQ)都支持消息的顺序保证,尤其在处理同一个主题或队列中的消息时,可以确保消息按顺序被消费。对于需要保证顺序消费的场景,消息队列是非常适合的。
  • Feign:Feign 的调用是同步的,但它不保证请求的顺序和消费顺序。多个服务之间的调用是按顺序发生的,但并不适用于保证消息顺序。

优点:

  • 消息队列:可以确保消息顺序性,适合需要严格顺序的场景。
  • Feign:没有内建的顺序保证机制。

7. 场景适配

  • 消息队列:适用于以下场景:

事件驱动的应用,异步通知、推送;
高并发、高吞吐量的系统;
系统解耦,避免微服务之间的直接调用;
消息延迟处理或异步处理的场景。

  • Feign:适用于以下场景:

服务间的同步调用,需要立即得到返回值;
处理简单的请求-响应模式;
服务间的低延迟、低耦合调用。

8. 总结

特性 消息队列 Feign
耦合性 松耦合,生产者和消费者分离 强耦合,服务间直接调用
可靠性 提供消息持久化、重试机制 依赖于服务的可用性,失败时没有自动恢复
性能 异步,适合高并发、高吞吐量的场景 同步,可能成为性能瓶颈
可伸缩性 通过水平扩展消费者来提高可伸缩性 依赖于负载均衡,扩展有限
流量控制 可以控制流量,避免系统过载 没有流量控制机制
消息顺序 可以保证消息顺序 不保证消息顺序
场景适配 事件驱动、异步通知、大量消息处理 服务间同步调用、低延迟处理

总结

使用 消息队列(如 RabbitMQ、Kafka)更适合于 高并发、异步、解耦、事件驱动 的场景,能够提高系统的可靠性、性能和扩展性。

使用 Feign 更适合 同步调用、简单的请求-响应模式、低延迟 的服务间通信。

线程池问题

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

一、线程池拒绝策略选择指南

在实际开发中,线程池拒绝策略的选择需要根据具体的业务场景和系统需求来决定。Java线程池提供了四种内置拒绝策略,每种策略适用于不同的情况。

一、四种内置拒绝策略

  1. AbortPolicy(默认策略)
    行为:直接抛出RejectedExecutionException异常

适用场景:

需要明确知道任务被拒绝的情况

关键业务系统,不能容忍任务静默丢失

开发测试环境(便于发现问题)

示例:

java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy() // 显式指定
);
2. CallerRunsPolicy
行为:将任务回退给调用者线程执行

适用场景:

适合能承受一定延迟的业务

需要保证每个任务都能被执行

不希望任务丢失但可以接受降级

特点:

会降低新任务提交速度,起到负反馈调节作用

调用者线程可能被阻塞

  1. DiscardPolicy
    行为:静默丢弃被拒绝的任务,不做任何处理

适用场景:

非核心业务

允许丢失部分任务的场景

监控日志不重要的任务

风险:

任务丢失无感知,可能造成数据不一致

  1. DiscardOldestPolicy
    行为:丢弃队列中最老的任务,然后尝试重新提交当前任务

适用场景:

允许丢弃旧任务的新业务场景

实时性要求高的任务优先处理

任务本身具有时效性(旧任务价值低)

注意事项:

可能丢失重要但处理慢的任务

不适合任务之间有依赖关系的场景

二、选择策略的决策流程

  • 是否绝对不能丢失任务
    • 是:使用CallerRunsPolicy
    • 否:是否需要立即知道被拒绝
      • 是:使用AbortPolicy
      • 否:新任务比旧任务更重要
        • 是:使用DiscardOldestPolicy
        • 否:使用DiscardPolicy

三、实际业务场景推荐

  1. 支付/交易核心系统
    推荐策略:CallerRunsPolicy
    理由:保证每个支付请求都能被处理,宁可降级也不丢失

  2. 日志记录/数据收集
    推荐策略:DiscardPolicy
    理由:日志可容忍部分丢失,避免影响主业务流程

  3. 实时数据处理
    推荐策略:DiscardOldestPolicy
    理由:新数据比旧数据更有价值

  4. 管理系统后台任务
    推荐策略:AbortPolicy
    理由:需要及时发现系统过载情况

四、自定义拒绝策略
当内置策略不满足需求时,可以实现RejectedExecutionHandler接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CustomRejectionPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 记录日志
log.warn("Task rejected: {}", r.toString());

// 2. 持久化任务到数据库/文件
persistTask(r);

// 3. 尝试重新提交
if (!executor.isShutdown()) {
executor.submit(r);
}
}
}

// 使用示例
executor.setRejectedExecutionHandler(new CustomRejectionPolicy());

五、配置建议
监控报警:无论选择哪种策略,都应监控拒绝情况

1
// 通过ThreadPoolExecutor的getRejectedExecutionCount()获取拒绝数

参数调优:合理设置队列容量和线程数比选择拒绝策略更重要

组合使用:不同业务使用不同线程池,配置不同策略

文档记录:在代码中明确注释选择该策略的原因

六、最佳实践总结
关键业务:优先保证任务执行(CallerRunsPolicy)

可丢弃任务:选择静默丢弃(DiscardPolicy)

需要感知:抛出异常(AbortPolicy)

时效敏感:丢弃旧任务(DiscardOldestPolicy)

特殊需求:自定义策略(如持久化后重试)

ThreadLocal的实现原理

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

ThreadLocal是Java中用于解决多线程环境下数据隔离问题的一个类。它提供了一种方式,使得每个线程都可以拥有其独立的变量副本,这样每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。这种机制在多线程编程中非常有用,特别是当需要在多个线程之间共享某些数据,但又不希望这些数据因为并发访问而引发问题时。

ThreadLocal的实现原理

ThreadLocal的实现主要依赖于一个内部类ThreadLocalMap。这个Map以ThreadLocal对象为键,以线程独有的变量为值。每个线程都维护着一个ThreadLocalMap的引用,这样线程就可以通过ThreadLocal对象作为键,在其自己的ThreadLocalMap中查找或设置值。

  1. ThreadLocalMap的结构:

    • ThreadLocalMap是一个哈希表,它的键(Key)是ThreadLocal对象本身的一个弱引用(WeakReference),而值(Value)则是线程独有的变量。
    • 使用弱引用作为键的好处是,当ThreadLocal对象被垃圾回收时,不会因为ThreadLocalMap中的引用而阻止其被回收。但这也带来了一个问题,即如果ThreadLocal对象被回收了,而ThreadLocalMap中的Entry还存在(且Value还未被回收),那么就会出现键为null的情况,这需要在ThreadLocalMap的操作中特别处理。
  2. ThreadLocal的方法:

    • initialValue():返回此线程局部变量的初始值。
    • get():返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则通过调用initialValue()方法创建并初始化此副本。
    • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
    • remove():移除此线程局部变量的值。这是一个清理操作,有助于避免内存泄漏,特别是在使用线程池时。
  3. ThreadLocalMap的扩容和清理:

    • ThreadLocalMap的扩容机制与HashMap类似,当元素数量超过阈值时,会进行扩容。
    • 由于ThreadLocalMap的键是弱引用,当没有强引用指向ThreadLocal对象时,ThreadLocal对象可以被垃圾回收。但是,如果ThreadLocal对象被回收了,而对应的Value对象还有强引用存在(即还没有被线程显式地调用remove()方法移除),那么这些Value对象就变成了“孤儿对象”,可能会导致内存泄漏。因此,在使用ThreadLocal时,建议显式地调用remove()方法来清理不再需要的线程局部变量。

使用场景

ThreadLocal的典型使用场景包括:

  • 在多线程环境中,每个线程需要维护一个独立的、不与其他线程共享的状态或数据。
  • 在处理用户请求时,将用户相关的信息(如用户ID、会话信息等)保存在ThreadLocal中,以便在同一个请求处理流程中方便地访问这些信息,而不需要通过参数传递。

内存泄漏与内存溢出的区别

  • 内存泄漏:指的是程序中分配的内存在不再需要时没有被正确释放或回收的情况。随着时间的推移,可用内存逐渐减少,最终可能导致程序性能下降或崩溃。
  • 内存溢出(OOM, Out of Memory):当程序运行时,如果请求的内存超出了可用内存的限制,就会抛出内存溢出异常。内存泄漏如果不及时解决,最终可能导致内存溢出。

ThreadLocal内存泄漏问题

ThreadLocal是一个用于创建线程本地变量的类。每个线程都拥有自己独立的、初始化为null的变量副本,这些变量对其他线程是不可见的。然而,ThreadLocal的内存泄漏问题是一个比较典型的问题。

ThreadLocal内存泄漏的来源

  1. ThreadLocal对象在堆上存储的ThreadLocalMap:

    • ThreadLocalMap是ThreadLocal的内部类,用于存储线程局部变量。
    • ThreadLocalMap的key是ThreadLocal对象,value是线程变量的值。
  2. ThreadLocal的引用链:

    • ThreadLocal对象有两个引用源:一个是栈上的ThreadLocal引用(方法内创建),一个是ThreadLocalMap中的key对它的引用。
    • 如果栈上的ThreadLocal引用不再使用(方法结束),但ThreadLocal对象因为还有ThreadLocalMap中的key引用而无法被回收,就会导致内存泄漏。
  3. 线程对象被重复使用:

    • 在线程池场景中,线程对象会被重复使用。如果线程中的ThreadLocalMap没有及时清理,就会导致内存泄漏。

弱引用解决部分内存泄漏问题

为了解决ThreadLocal对象因为ThreadLocalMap中的key引用而无法被回收的问题,ThreadLocalMap使用了弱引用。

  • 弱引用:如果一个对象只具有弱引用,那么这个对象就会在下次垃圾回收时被回收。
  • 在ThreadLocalMap中,key(ThreadLocal对象)是弱引用。这意味着,当栈上的ThreadLocal引用不再使用时,ThreadLocal对象可以被垃圾回收器回收,从而避免了因为ThreadLocal对象无法被回收而导致的内存泄漏。

然而,即使使用了弱引用,ThreadLocal的内存泄漏问题并没有完全解决。因为value(线程变量的值)还是强引用,如果线程对象被重复使用(如在线程池中),并且ThreadLocalMap没有及时清理,那么value所占用的内存仍然无法被回收。因此,开发者还需要在使用完ThreadLocal后,显式地调用remove()方法来清理ThreadLocalMap中的entry,以避免内存泄漏。

综上所述,ThreadLocal的内存泄漏问题是一个复杂的问题,需要开发者和JDK共同解决。JDK通过使用弱引用来解决了一部分问题,但开发者还需要在使用完ThreadLocal后显式地调用remove()方法来避免内存泄漏。
总之,ThreadLocal是一个强大的工具,它可以帮助我们在多线程环境中实现数据的隔离和线程安全。但是,使用时也需要注意其潜在的内存泄漏问题,特别是在使用线程池时。

未命名

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

当把一个线程池核心线程数设置为 0,最大线程数设置为 100,此时当任务到达会怎么运行?

题目详细答案

当你将一个线程池的核心线程数设置为 0,最大线程数设置为 100 时,线程池的行为主要取决于其工作队列和拒绝策略。

当核心线程数为 0的时候:这意味着线程池在没有任务时不会维持任何空闲线程。只有在有任务到达时,线程池才会尝试创建线程来执行任务。

最大线程数为 100:这是线程池能够创建的最大线程数量。当任务数超过核心线程数时,线程池会继续创建新线程,直到达到最大线程数。

重点就是任务到达时的行为:

1、如果工作队列为空:当一个新任务到达时,由于核心线程数为 0,线程池不会立即创建核心线程,而是直接将任务放入工作队列中。

2、如果工作队列已满:当工作队列已满时,线程池会尝试创建新的线程来处理任务,直到线程数达到最大线程数。

3、如果线程数已达到最大线程数:如果线程数已经达到了最大线程数并且工作队列也满了,那么新到达的任务将根据线程池的拒绝策略进行处理(例如,抛出异常、丢弃任务、丢弃最旧的任务或调用任务的rejectedExecutionHandler方法)。

具体的行为还取决于工作队列的类型和大小:

有界队列:如果使用的是有界队列(例如ArrayBlockingQueue),当队列已满时,新的任务会触发线程池创建新的线程,直到达到最大线程数。超过最大线程数的任务将根据拒绝策略处理。

无界队列:如果使用的是无界队列(例如LinkedBlockingQueue),线程池将优先将任务放入队列中,而不是创建新线程。因此,线程数可能不会达到最大线程数,除非队列的任务处理速度跟不上任务到达的速度。

总结当核心线程数为 0,最大线程数为 100 时,线程池在任务到达时会首先尝试将任务放入工作队列中。如果队列已满,则会创建新的线程来处理任务,直到达到最大线程数 100。如果线程数已达到最大并且队列也满了,则会根据拒绝策略处理新任务。

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

未命名

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

未命名

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

报错

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

easyexcel 导入使用shiro报错

一、背景描述

SpringBoot项目,使用Shiro进行权限管理。测试过程中发现执行文件导入时最开始一切正常,但是导入几次之后再次执行导入就会报错,此时执行其他功能一切正常

二、问题描述

  • 在使用 EasyExcel 进行多线程导入数据时,可能会遇到 org.apache.shiro.session.UnknownSessionException 异常。这个异常通常与 Apache Shiro 的会话管理有关,尤其是在多线程环境下,如果多个线程尝试访问或操作同一个会话,而没有正确处理线程间的同步,就可能抛出此类异常。

三、原因分析

1、会话管理不正确:在多线程环境中,如果多个线程尝试访问或修改同一个用户的会话(Session),而没有适当的管理和同步,就可能导致 UnknownSessionException。

2、会话过期或失效:在多线程环境下,如果会话在某个线程中被修改或过期,而其他线程还在尝试访问该会话,也可能会引发此异常。

四、问题分析

  • 排查导入代码发现,只有在执行保存语句的时候获取当前用户使用了Shiro相关代码,因此怀疑此处出现问题。
    1
    this.operator = (String) SecurityUtils.getSubject().getPrincipal();
  • 在此处断点发现,手动新增数据和批量导入数据都执行该语句,但两次获取的Subject不一致。因此阅读源码进行排查。
    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
    // 从SecurityUtils中获取Subject源码如下
    // package: org.apache.shiro.SecurityUtils
    public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject(); // ①
    if (subject == null) {
    subject = (new Subject.Builder()).buildSubject();
    ThreadContext.bind(subject);
    }
    return subject;
    }

    // 继续跟进上面①中的方法
    // package: org.apache.shiro.util.ThreadContext
    public static Subject getSubject() {
    return (Subject) get(SUBJECT_KEY); // ②
    }

    // 继续跟进上面②中的方法
    // package: org.apache.shiro.util.ThreadContext
    public static Object get(Object key) {
    if (log.isTraceEnabled()) {
    String msg = "get() - in thread [" + Thread.currentThread().getName() + "]";
    log.trace(msg);
    }

    Object value = getValue(key); // ③
    if ((value != null) && log.isTraceEnabled()) {
    String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" +
    key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]";
    log.trace(msg);
    }
    return value;
    }

    // 继续跟进上面③中的方法
    // package: org.apache.shiro.util.ThreadContext
    private static Object getValue(Object key) {
    Map<Object, Object> perThreadResources = resources.get(); // ④
    return perThreadResources != null ? perThreadResources.get(key) : null;
    }

    // 上面④中的resources在ThreadContext中定义如下
    // package: org.apache.shiro.util.ThreadContext
    private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();

根据上面对源码的跟踪,发现Subject是与ThreadLocal也就是线程绑定的。获取Subject时先获取当前线程绑定的Subject,若没有则重新创建并绑定到当前线程。而我导入的时候为了提高导入效率,使用了多线程。到此就发现问题的原因了

五、问题分析

  • 假设项目中线程池设置核心线程数量为10,而核心线程默认是不会被超时回收的
    • 可通过threadPoolExecutor.allowCoreThreadTimeOut(true);设置核心线程超时回收

1、当用户A登录后,执行导入操作,从线程池中拿出5个线程,此时这5个线程将绑定用户A的Subject
2、当用户A多次执行导入操作后,线程池全部核心线程与用户A的Subject绑定。用户A退出登录后,线程池并不会将核心线程进行销毁。
后续用户B登录,再次执行导入操作,此时线程池分配线程进行操作,但此时所有的线程都已与用户A绑定,因此获取到的Subject都是用户A的Subject,从Subject中获取session时此session已被销毁,因此报错

1
2
3
4
5
6
7
8
9
// 根据sessionId获取session,获取为空则报错
// package: org.apache.shiro.session.mgt.eis.AbstractSessionDAO
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session s = doReadSession(sessionId);
if (s == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
}
return s;
}

六、建议

多线程时不要使用Shiro相关代码。将用户名作为参数传入,不再单独获取。

七、参考文档

https://www.cnblogs.com/bbc2005/articles/15340412.html

👌redis sentinal与redis cluster的区别?

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

👌redis sentinal与redis cluster的区别?

口语化答案

哨兵和集群是两种非常不同的模式。一个重点关注高可用,一个关注水平扩展和数据分片。哨兵主要解决主服务器故障的自动切换问题。集群则是为了存储大量数据,将数据分布到多个节点。在存储上,哨兵不进行数据分片,集群则通过哈希槽的方式存储数据,集群的可用和切换采用了Gossip 协议,哨兵模式则依靠哨兵的检测。所以哨兵适合小规模的高可用,集群适合大规模的数据存储。以上。

题目解析

这道题还是挺重要的,经常有人会将两种模式弄混。二者模式区别非常大,要重点关注。

面试得分点

高可用,水平扩展,数据分片,哈希槽

题目详细答案

先来了解概念:

redis sentinal 哨兵模式

redis cluster 集群模式

哨兵模式

Sentinel是一种用于监控Redis主从复制结构并实现自动故障转移的系统。它主要关注的是高可用性,保证当主服务器发生故障时,能够自动将一个从服务器提升为新的主服务器,并通知客户端进行相应的切换。

集群模式

Cluster是一种分布式存储解决方案,支持自动分片和高可用性。支持主从复制和故障转移,还能够将数据分布在多个节点上,实现数据的水平扩展。

区别

哨兵模式 集群模式
架构区别 Sentinel节点监控主从服务器的状态,当主服务器故障时,Sentinel节点会选举一个新的主服务器。 数据自动分片存储在多个节点上,每个节点负责一部分数据。每个分片有一个主节点和一个或多个从节点。使用Gossip协议进行节点间通信,自动检测故障并进行主从切换。
数据存储 数据存储在一个主服务器及其从服务器上,不进行数据分片。 数据通过哈希槽(hash slots)机制分布在多个节点上,每个节点负责一部分哈希槽。
扩展性 只提供高可用性,不能水平扩展数据存储容量。 支持水平扩展,可以通过增加节点来扩展数据存储容量和处理能力。
高可用 通过心跳机制检测主服务器的状态,自动选举新的主服务器并更新拓扑结构。 使用Gossip协议进行节点间的故障检测,自动进行主从切换,确保集群的高可用性。
使用场景 适用于小规模的Redis部署,主要关注高可用性。不需要数据分片。 适用于大规模的Redis部署,支持水平扩展。需要同时满足高可用性和数据分片、扩展性需求。

👌redis key的过期时间和永久有效分别怎么设置?

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

👌redis key的过期时间和永久有效分别怎么设置?

题目详细答案

在Redis中,你可以使用多种命令来设置键的过期时间或将键设置为永久有效。

设置键的过期时间

  1. 使用EXPIRE命令,EXPIRE命令用于设置键的过期时间,以秒为单位。
1
2
3
EXPIRE key seconds
例如:
EXPIRE mykey 60 # 设置 mykey 的过期时间为60秒
  1. 使用PEXPIRE命令,PEXPIRE命令用于设置键的过期时间,以毫秒为单位。
1
2
3
PEXPIRE key milliseconds
例如:
PEXPIRE mykey 60000 # 设置 mykey 的过期时间为60000毫秒(即60秒)

以上的两个区别就是秒级和毫秒级的区别。

  1. 使用EXPIREAT命令,EXPIREAT命令用于设置键的过期时间为指定的 Unix 时间戳,以秒为单位。
1
2
3
EXPIREAT key timestamp
例如:
EXPIREAT mykey 1672531199 # 设置 mykey 的过期时间为指定的 Unix 时间戳
  1. 使用PEXPIREAT命令,PEXPIREAT命令用于设置键的过期时间为指定的 Unix 时间戳,以毫秒为单位。
1
2
3
PEXPIREAT key milliseconds-timestamp
例如:
PEXPIREAT mykey 1672531199000 # 设置 mykey 的过期时间为指定的 Unix 时间戳(毫秒)
  1. 使用SET命令带选项,SET命令可以在设置键值的同时指定过期时间。
1
2
3
4
5
SET key value EX seconds
SET key value PX milliseconds
例如:
SET mykey "value" EX 60 # 设置 mykey 的值为 "value" 并使其在60秒后过期
SET mykey "value" PX 60000 # 设置 mykey 的值为 "value" 并使其在60000毫秒(即60秒)后过期

设置键为永久有效

  1. 使用PERSIST命令,PERSIST命令用于移除键的过期时间,使其变为永久有效。
1
2
3
PERSIST key
例如:
PERSIST mykey # 移除 mykey 的过期时间,使其变为永久有效

👌redis中的管道有什么用?

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

👌redis中的管道有什么用?

口语化回答

好的,面试官。管道 pipeline 主要是为了解决批量查询问题而诞生的。如果说没有管道,我们查 10 个信息,就要发送 10 次命令,与 redis 进行往返 10 次的交互,这对性能的影响是很大的。如果通过管道,将 10 个命令放在一块传递给 redis,结果再一次性返回,性能会得到提升。这种思想可以用到很多地方,包括在 mysql 进行查询的时候,也可以考虑。以上。

题目解析

管道这道题还比较常问,因为在公司中还是很多场景都在使用的。比较考察你对于性能的提升优化有没有考虑,代码写的好的小伙伴,肯定会考虑到批量这一点。

面试得分点

批量,网络消耗,顺序返回

题目详细答案

Redis中的管道(Pipeline)主要用于批量执行多个命令,用来提高性能和效率。管道可以将多个命令一次性发给服务器,然后返回多个结果。

为什么会有管道

按照平时开发的过程,假设要查询 10 个用户的信息,用户的信息正好存在 redis 里面。这个时候客户端需要发送 10 次命令到服务器,服务器处理命令并返回结果,这个过程会涉及多次网络往返,尤其是在高延迟网络环境中,频繁的网络通信会降低性能。管道通过将多个命令打包在一起,一次性发送到服务器,从而减少网络往返次数,提高性能。

管道的工作原理是将多个Redis命令批量发送到服务器,而不等待每个命令的单独响应。

客户端批量发送命令:客户端将一系列Redis命令放入管道中。

一次性发送命令:客户端将所有命令一次性发送到Redis服务器。

服务器批量处理命令:Redis服务器依次处理这些命令。

批量返回结果:服务器将所有命令的结果一次性返回给客户端。

适用场景

1、 批量写入数据:如批量插入大量数据到Redis中,可以显著提高写入速度。类似 mysql 里面的批量插入。

2、 批量读取数据:如一次性获取大量键值对,可以减少网络延迟。类似 mysql 可以通过 in 的方式来查询多个数据。但是不想 mysql 那么死,redis 的管道什么命令都可以放一起。

3、 减少网络开销:在高延迟网络环境中,管道可以显著减少网络往返次数,提高性能。

Java中使用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
//批量发送1000个SET命令,最后执行管道中的命令并关闭连接。

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;

public class RedisPipelineExample {
public static void main(String[] args) {
// 创建Jedis对象,连接到Redis服务器
Jedis jedis=new Jedis("localhost", 6379);

// 开始管道操作
Pipeline pipeline= jedis.pipelined();

// 批量发送命令
for (int i=0; i < 1000; i++) {
pipeline.set("key" + i, "value" + i);
}

// 执行管道中的命令
pipeline.sync();

// 关闭Jedis连接
jedis.close();
}

}

特性

管道中的命令是按顺序执行的,结果也按顺序返回。管道中的某个命令失败不会影响其他命令的执行,需要注意如何处理返回的结果。

👌redis主从复制的核心原理?

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

👌redis主从复制的核心原理?

口语化回答

好的,面试官。redis 的主从复制原理主要是,一开始从服务器发送同步命令,主服务器接收到之后,就会生成一个 rdb 的文件,然后传输给从服务器,从服务器接收到之后,立马进行数据的恢复。然后当主服务器再次接收到写命令的时候,会发给从服务器。这个过程是一个异步复制,主服务器不会等待结果。这样就完成了主从复制,主要的核心步骤就是这些,涉及的一个同步的点就是偏移量,主从都会维护一个偏移量的概念。来判断自己复制到哪里了,如果没有达到会继续自动复制。以上

题目解析

算是比较原理的一道题了,低年限的基本不会问到。这道题可以考察到你初始同步,增量同步的一些思想。这个同步机制,在其他场景也是基本适用的,就像是 mysql 的主从复制基本也是如此。

面试得分点

sync 命令,生成 rdb,写命令增量,缓存偏移量

题目详细答案

主从复制概念

Redis支持主从复制模式,其中一台服务器作为主服务器(Master),可以接收写请求;而一个或多个服务器作为从服务器(Slave),只能接收读请求。通过SLAVEOF命令或配置文件中设置slaveof选项,让从服务器复制主服务器的数据。

核心原理

1、发送SYNC命令:从服务器启动后,连接上主服务器发送SYNC命令,请求同步数据。

2、创建RDB文件:主服务器接收到SYNC命令后,在后台执行BGSAVE命令,生成一个RDB快照文件,这个文件包含了主服务器当前的数据状态。

3、发送RDB文件:主服务器将生成的RDB文件发送给从服务器,从服务器接收并加载该文件,更新自己的数据状态。

4、异步复制:一旦从服务器加载完RDB文件,主从复制就进入了命令传播阶段。此时,主服务器会将所有接收到的写命令发送给从服务器,确保主从数据的一致性。Redis的主从复制是异步的,这意味着主服务器在发送写命令给从服务器时,不会等待从服务器的响应,从而保证了主服务器的高可用性。

复制偏移量和缓冲区概念

主从服务器都会维护一个复制偏移量(offset),用于记录复制过程中传输的数据量。通过比较主从服务器的复制偏移量,可以判断数据是否一致。

复制积压缓冲区:主服务器维护了一个固定长度的、先进先出(FIFO)的队列作为复制积压缓冲区,用于保存最近一段时间内的写命令。当从服务器重新连接主服务器时,如果复制偏移量在复制积压缓冲区范围内,主服务器可以直接将从断线位置开始的写命令发送给从服务器,实现部分重同步。

这两个概念对于短线重连也产生了极大的影响。

当从节点与主节点的连接断开后,会自动尝试重新连接。如果从节点重新连接主节点时,发现自己的复制偏移量在主节点的复制积压缓冲区范围内,则会进行增量同步。如果复制积压缓冲区中的数据不足以完成同步,则需要重新进行全量同步。

👌redis在集群中查找key的时候,怎么定位到具体的节点

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

👌redis在集群中查找key的时候,怎么定位到具体的节点?

口语化回答

这个问题主要涉及到一个哈希槽的概念,redis 集群,会将键划分为 16384 个槽。每个槽分配一个或多个节点。比如一个集群,三个节点,每个节点负责一定范围的槽。当 key 来了的时候,首先做 hash 算法获得数值后,与 16384 进行取模。得到的值就是槽的位置,然后再根据槽的编号,就可以找到对应的节点。以上。

题目解析

算是进阶点的一道题了,年限低的小伙伴不必太在意,在集群模式下,这个知识点是必须要掌握的。

面试得分点

slot 机制,取模 hash 槽,16384,节点范围机制

题目详细答案

Redis集群采用了一种哈希槽的机制用于数据存储和获取。每个 redis 集群节点会分布在一个槽范围内,我们通过对 key 进行的 hash 计算,就可以确定 key 落在哪个槽上。

哈希槽机制

Redis集群将整个键空间划分为16384个哈希槽。每个键根据其哈希值被映射到其中一个哈希槽上,每个哈希槽被分配给一个节点或多个节点(主从复制的场景)。计算过程如下:

  1. 计算哈希值:Redis使用MurmurHash算法对键进行哈希计算,得到一个整数哈希值。
  2. 映射到哈希槽:将哈希值对16384取模(即hash(key) % 16384),得到对应的哈希槽编号。
  3. 定位节点:根据哈希槽编号找到负责该哈希槽的节点。

看了上面的图,你对 slot 有了解了,那么你会产生疑问,slot 又是如何和 redis 进行关联的呢?

哈希槽分配

集群的配置时,哈希槽会被分配给不同的节点。每个节点负责一定范围的哈希槽。例如,节点A可能负责哈希槽0-5000,节点B负责哈希槽5001-10000,节点C负责哈希槽10001-16383。这样就实现了集群、节点、slot 三者联动。

查找流程

当客户端需要查找某个键时,流程如下:

  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
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.util.HashSet;
import java.util.Set;

public class RedisClusterExample {
public static void main(String[] args) {
// 定义集群节点
Set<HostAndPort> nodes = newHashSet<>();
nodes.add(newHostAndPort("127.0.0.1", 7000));
nodes.add(newHostAndPort("127.0.0.1", 7001));
nodes.add(newHostAndPort("127.0.0.1", 7002));

// 创建JedisCluster对象
JedisCluster jedisCluster=new JedisCluster(nodes);

// 存储键值对
jedisCluster.set("mykey", "myvalue");

// 获取键值对
String value= jedisCluster.get("mykey");
System.out.println("Value for 'mykey': " + value);

// 关闭JedisCluster连接
jedisCluster.close();
}
}

👌redis支持哪几种数据类型

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

👌redis支持哪几种数据类型?

口语化回答

redis 的基础数据类型,主要有五种,string ,hash,list,set 和 zset。平时最常用的就是 string,可以缓存内容、做分布式锁等等,其次就是 hash,比如缓存一些对象结构的数据,hash 就比较合理。假设缓存一个个人信息,姓名,年龄,头像这些。传统的 string 需要进行序列化转 json,hash 则可以直接拿到。zset 也用过,主要是做排行榜功能,利用分数的特性进行排序。主要就是这些,以上。

题目解析

这道题比较简单,一般面试切入到 redis 的时候,会问一下这道,但是你的目的是在这五种基础数据类型中,选出钩子,让面试官继续针对某一个类型来进行问,比如你对分布式锁准备的很充分,你答 string 类型的时候,就往上面引入,你用 zset 做过排行榜,那么就 zset 上面多说一些。

面试重点词

五种类型、string 的分布式锁、hash 存储对象、zset 做排行榜。

题目详细答案

Redis支持的数据类型主要有五种。

String(字符串)

1、字符串是 Redis 中最简单和最常用的数据类型。可以用来存储如字符串、整数、浮点数、图片(图片的base64编码或图片的路径)、序列化后的对象等。

2、每个键(key)对应一个值(value),一个键最大能存储512MB的数据。

3、命令用法

1
2
SET key "value"
GET key

Hash(哈希)

1、Redis Hash是一个String类型的field和value的映射表,类似于Java中的Map<String, Object>。

2、Hash特别适合用于存储对象,如用户信息、商品详情等。

3、 每个Hash可以存储2^32 - 1个键值对。

4、命令用法

1
2
HSET user:1000 name "John"
HGET user:1000 name

List(列表)

1、 列表是一个有序的字符串集合,可以从两端压入或弹出元素,支持在列表的头部或尾部添加元素。

2、 列表最多可存储2^32 - 1个元素。

1
2
3
LPUSH mylist "world"
LPUSH mylist "hello"
LRANGE mylist 0 -1

Set(集合)

1、Set是一个无序的字符串集合,不允许重复元素。集合适用于去重和集合运算(如交集、并集、差集)。Set的添加、删除、查找操作的复杂度都是O(1)。

2、常用命令

1
2
3
SADD myset "hello"
SADD myset "world"
SMEMBERS myset

Zset(有序集合)

Zset和Set一样也是String类型元素的集合,且不允许重复的成员。有序集合类似于集合,但每个元素都会关联一个double类型的分数(score),redis正是通过分数来为集合中的成员进行从小到大的排序。

👌redis有哪些优点?

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

👌redis有哪些优点?

口语化回答

好的,面试官 Redis 是现在非常常用的缓存,相比于其他的缓存,最大的一个优势就是高性能,redis 的速度非常快,操作都是毫秒级,吞吐量非常高,支持每秒数百万的请求。同时各种操作都基于内存进行,读写速度非常快。还有就是他提供了丰富的数据类型,像 string,hash,zset 这些,在我们日常开发都非常常见的使用。还有一些高级数据类型,像 geo,bitmap 这些。另外就是 redis 的持久化机制做的也比较好,不同的策略选择使其可以应对宕机等异常情况。配合分布式集群的支持,整体的可用性也非常高。以上。

题目解析

其实这道题一般是面试官跟你聊 redis 刚开头的开场时候,会和你问问,主要目的其实是想通过你说的一些优点,来继续发问,问你为什么这是优点,以及延伸出一个话题来继续深度聊。所以像这道题,大家答的时候,熟悉哪个优点就多说点,有点不熟悉的就一嘴带过!

面试得分点

高性能,内存,快,高吞吐,持久化,分布式

题目详细答案

Redis 是当前非常主流的缓存,基于键值对来实现的存储,支持多种数据类型。看一下鸡哥这张思维导图就知道优点是什么了。

👌redis有哪些持久化方式?

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

👌redis有哪些持久化方式?

口语化回答

主要是 rdb 和 aof 的方式,另外也可以混合使用。rdb 主要是在指定的时间间隔内生成数据集的快照,并将其保存到磁盘上,生成一个 rdb 格式的二进制文件,容易备份。恢复速度快,适合用于灾难恢复。AOF持久化方式是记录每一个写操作到日志文件中。Redis会将这些写操作以追加的方式写入到AOF文件中。每次恢复的时候,进行重放。aof 相比 rdb 就是文件会大一些,回复速度慢一些,但是数据丢失风险小。在实际中,一般可以选择混合互补的方式使用。以上。

题目解析

经典必问,面试官经常会问你 redis 数据丢不丢啊,如何持久化啊,这类问题。大家要记住 rdb 和 aof 的两个特点即可。一个比较粗,一个比较细致,重点答这一块,最后告诉他混合起来。

面试得分点

rdb,aof,混合持久化

题目详细答案

主要的持久化方式有两种:RDB(Redis Database)和AOF(Append Only File)。

Redis 4.0引入了混合持久化模式。

RDB(Redis Database)

RDB持久化方式会在指定的时间间隔内生成数据集的快照,并将其保存到磁盘上。这个快照文件的默认名称是dump.rdb。

RDB的配置可以在redis.conf文件中进行。例如:

1
2
3
save 900 1      # 如果900秒(15分钟)内至少有1个键发生变化,就触发一次RDB快照
save 300 10 # 如果300秒(5分钟)内至少有10个键发生变化,就触发一次RDB快照
save 60 10000 # 如果60秒(1分钟)内至少有10000个键发生变化,就触发一次RDB快照

优点:

1、RDB文件是一个紧凑的二进制文件,可以很容易地进行备份。

2、 恢复速度快,适合用于灾难恢复。

3、 对Redis性能影响较小,因为生成RDB文件的工作是在子进程中进行的。

缺点:

1、 数据持久化的频率较低,可能会丢失最近一次快照之后的数据。

2、 生成RDB快照时,可能会消耗较多的CPU和内存资源。

AOF(Append Only File)

AOF持久化方式记录每一个写操作到日志文件中(默认名称是appendonly.aof)。Redis会将这些写操作以追加的方式写入到AOF文件中。

AOF的配置可以在redis.conf文件中进行。例如:

1
2
3
4
5
6
appendonly yes         # 启用AOF持久化
appendfilename "appendonly.aof"
appendfsync everysec # 每秒钟同步一次AOF文件
# 其他选项:
# appendfsync always # 每个写操作都同步到AOF文件,性能较差但数据最安全
# appendfsync no # 由操作系统决定何时同步,性能最好但数据安全性较差

优点:

1、数据恢复更可靠,AOF可以记录每一个写操作,数据丢失风险较小。

2、AOF文件是可读的文本文件,方便分析和调试。

缺点:

1、 AOF文件比RDB文件大,恢复速度较慢。

2、 持久化频率高时,可能会影响Redis性能。

3、 需要定期进行AOF重写(rewrite),以避免文件过大。

3. 混合持久化(Hybrid Persistence)

混合持久化模式结合了RDB和AOF的优点。在Redis 4.0及以上版本中,混合持久化模式在生成新的AOF文件时,会首先创建一个RDB快照,然后在快照之后追加AOF日志。这种方式可以在保证数据恢复速度的同时,减少数据丢失的风险。

混合持久化的配置可以在redis.conf文件中进行。

1
aof-use-rdb-preamble yes  # 启用混合持久化模式

优点:

1、 结合了RDB和AOF的优点,既能快速恢复数据,又能减少数据丢失的风险。

选择建议

RDB:适用于对数据一致性要求不高,但需要快速恢复数据的场景,例如缓存服务器。

AOF:适用于对数据一致性要求高的场景,例如金融交易系统。

混合持久化:适用于需要综合考虑数据恢复速度和数据一致性的场景。

👌redis相比memcached有哪些优势?

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

👌redis相比memcached有哪些优势?

口语化回答

redis 相比 memcached,主要提供了丰富的数据类型和各种高级操作。memcached 仅仅支持字符串类型,redis 支持 5 种基础+4 种高级,非常丰富。还有就是 redis 提供了 rdb 和 aof 的持久化机制,memcached 一旦重启,数据就会直接丢失。分布式相关,redis 天然支持主从复制,哨兵,集群,memcached 还需要靠一些第三方库和工具类实现。再高级一点的就是 redis 支持像 lua,事务,发布订阅这些,memcached 统统不支持。

题目解析

最近几年基本上不会考这道题了,这道题在早些时候,memcached 还用的时候,问的比较多。除非碰到还在用memcached 的公司,否则不会被问到。大家稍微了解即可,比较的过程也是了解历史的过程。

面试得分点

丰富数据类型,持久化,分布式,lua,事务

题目详细答案

通过一个表格来进行对比

redis Memcached
数据类型 Redis 支持字符串、哈希、列表、集合、有序集合、位图、HyperLogLog、地理空间和流等多种数据类型 Memcached 仅支持字符串类型的键值对。
持久化机制 Redis 支持将数据持久化到磁盘,可以通过 RDB 快照和 AOF 日志来保存数据,确保在系统重启后数据不会丢失 Memcached不支持持久化,数据仅保存在内存中,一旦服务器重启或宕机,数据就会丢失。
分布式 支持主从,哨兵,集群 Memcached也支持分布式部署,但通常需要依赖第三方库或工具来实现
脚本功能 Redis 支持在服务器端执行 Lua 脚本,能够将多个操作合并为一个原子操作 不支持
事务 Redis 提供 MULTI、EXEC、WATCH 等命令,支持简单的事务操作 不支持事务
内存淘汰策略 Redis 提供多种内存淘汰策略,如 LRU、LFU 等 Memcached 仅支持 LRU 淘汰策略。

👌redis是单进程单线程?

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

👌redis为什么这么快?

口语化回答

redis 的快主要得益于以下几个点,第一个就是纯内存的操作,相比磁盘来说,纯内存带来的速度提升非常大。其次就是合理的数据结构和数据编码设计。像不同的数据类型所对应的底层结构都会有变化,基本上作者让我们在 o1 的复杂度内就可以读取到我们想要的值。还有就是单线程,因为在内存,所以压力瓶颈一般在网络和 cpu,单线程完全足够,这使得 redis 不需要考虑切换的消耗。以及 redis 采取了 io 多路复用,减少了网络压力,提高了吞吐量。以上就是 redis 这么快的原因。

题目解析

经典题目了,主要是看你对内存的速度有没有一个思考性的了解,知道内存比磁盘快的多,有这种思维,可以在平时开发的时候多注意。还有就是考察单线程不需要考虑切换和争抢的性能问题。io 多路复用是 redis 的比较特色的东西。再深一点就是要考察你对数据类型的底层结构有没有具体的了解了。

面试得分点

内存,数据结构,单线程,io 多路复用

题目详细答案

结论

redis 巨快,巨快,操作毫秒级,qps 可达 10w+。

主要原因

纯内存

Redis将数据存储在内存中,避免了大量访问数据库和直接读取磁盘数据的操作。内存的读写速度远超过磁盘I/O,使得Redis的数据访问非常迅速。

数据结构合理

Redis内部的数据结构都是为快速读写而设计的,如跳跃表、SDS(简单动态字符串)、链表和Hash等。基本都能够在 o(1)复杂度下完成大部分操作,比如 hash 的结构,想获取其中一个属性的值,非常的方便,不像数据库查询需要磁盘寻找。

单线程操作

Redis采用单线程模型来处理客户端请求,避免了多线程带来的上下文切换和竞争条件。单线程模型使得Redis不需要考虑各种锁的问题,减少了性能消耗。但是持久化,异步删除等等是异步线程处理,但这不影响性能。不过要注意一个点,因为单线程,某个命令如果耗时太大,可能会产生阻塞,也就是我们经常说的,不要使用 keys,或者直接读整个 hash 大 key。

io 多路复用模型

Redis在网络通信和磁盘写入方面采用了异步式的IO处理,即使用epoll多路复用技术同时处理多个网络请求,减少了I/O阻塞及上下文切换开销,提高了系统的吞吐量和响应时间。

专门设计的数据结构

redis 的每种数据类型对应的底层存储结构都不一样,经历过多种方式的设计。拿 string 类型来进行说,如果存储数字的话,是用int类型的编码。如果存储非数字,小于等于39字节的字符串,是embstr。大于39个字节,则是raw编码。这种根据类型和字节数的设计,在 key 越多的场景下,占用空间越少。

👌redis事务支持原子性,支持回滚?

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

👌redis事务保证原子性吗,支持回滚吗?

题目详细答案

不支持。

先来回顾两个命令:

MULTI:开启一个事务。之后的所有命令都会被放入一个队列中,直到EXEC命令执行。

EXEC:执行之前队列中的所有命令。

在MULTI和EXEC之间的命令会被放入一个队列中,直到EXEC命令执行时,这些命令才会按顺序执行。

在EXEC命令执行时,所有被MULTI命令包裹的命令会按顺序一次性执行。意味着在EXEC执行时,Redis会将所有命令作为一个整体进行处理。Redis保证单个命令的原子性,即每个命令在执行时是不可分割的。

但是,Redis事务并不完全等同于传统关系型数据库的事务。

如果在EXEC执行过程中某个命令失败(例如,命令语法错误),该命令会被跳过,但其他命令仍然会继续执行。这与关系型数据库的事务不同,后者通常会在某个命令失败时回滚整个事务。

Redis事务没有回滚机制。如果某个命令执行失败,已经执行的命令不会被撤销。

redis再执行lua脚本的时候,会封装为一个事务,不可中断;

👌redis回收进程是如何工作的?

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

👌redis回收进程如何工作的?

题目详细答案

Redis的回收进程负责定期扫描和删除过期键。服务器运行时自动执行。主要就是定期回收和惰性回收,其实也可以看之前的 redis 过期策略。来进行合二为一,其实二者答案差不多,就是内容有一定区别。

定期回收任务

Redis回收进程的核心是一个定期任务,这个任务在Redis的事件循环中执行。

事件循环:Redis使用一个单线程的事件循环模型来处理所有的客户端请求和后台任务。

定期任务:在事件循环中,Redis会定期触发一个函数来执行过期键的检查和删除操作。这个函数默认每100毫秒运行一次。

实现步骤如下:

  1. 选择数据库:每次执行定期任务时,Redis会遍历所有的数据库(默认16个)。
  2. 随机抽样:对于每个数据库,Redis会随机选择一部分带有过期时间的键进行检查(默认20个)。
  3. 检查过期键:检查这些键是否已经过期,如果过期则删除。
  4. 重复检查:如果发现超过一定比例的键是过期的(默认25%),则继续进行更多次的检查和删除,直到过期键的比例下降到合理范围内。

惰性回收

惰性回收不是一个独立的进程,而是每次客户端访问键时触发的检查机制。它的工作原理如下:

  1. 访问键:每当客户端访问一个键时,Redis会检查该键是否设置了过期时间。
  2. 检查过期:如果该键已经过期,Redis会立即删除该键,并返回空结果或相应的错误信息。

👌redis如何实现延迟队列?

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

👌redis如何实现延时队列

题目详细答案

可以使用有序集合(Sorted Set)来实现延时队列。有序集合中的每个元素有一个关联的分数,可以用来表示任务的执行时间戳。具体的步骤如下,非常简单

添加任务到延时队列

将任务添加到有序集合中,使用任务的执行时间作为分数(score)。

1
2
3
4
5
6
7
8
// 示例代码:添加任务到延时队列
String queueName = "delay_queue";
String taskId="task_1";
long delay=5000; // 延迟时间(毫秒)
long executionTime= System.currentTimeMillis() + delay;
Jedis jedis = newJedis("localhost");
jedis.zadd(queueName, executionTime, taskId);
jedis.close();

轮询延时队列并执行任务

定期检查有序集合中的任务,找到那些执行时间已经到达或超过当前时间的任务,并执行这些任务。

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
// 示例代码:轮询延时队列并执行任务
String queueName = "delay_queue";
Jedis jedis=new Jedis("localhost");
while (true) {
long currentTime= System.currentTimeMillis();
Set<Tuple> tasks = jedis.zrangeByScoreWithScores(queueName, 0, currentTime, 0, 1);

if (tasks.isEmpty()) {
// 没有任务需要执行,休眠一段时间
Thread.sleep(1000);
continue;
}

for (Tuple task : tasks) {
StringtaskId= task.getElement();
// 执行任务
executeTask(taskId);

// 从队列中移除已执行的任务
jedis.zrem(queueName, taskId);
}
}

jedis.close();
private static void executeTask(String taskId) {
// 实现任务执行逻辑
System.out.println("Executing task: " + taskId);
}
12…12<i class="fa fa-angle-right"></i>

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