3,1010面试题学习

73 阅读19分钟

1,聊聊你印象最深刻的项目,或者做了什么优化?

2,HashMap是怎么解决hash冲突的?

3,你说下跳表和B+树的区别?

    B+树 与跳表详细分析

4,Redis Sentinel集群和Cluster 集群有什么区别? 

Redis Sentinel旨在提供高可用性解决方案,通过监控Redis实例的状态,并在主实例发生故障时自动进行故障转移,确保系统的持续可用性。Sentinel由一组哨兵节点组成,每个哨兵节点负责监控Redis主从实例的状态,并在需要时执行故障转移操作。哨兵节点与Redis实例之间通过Pub/Sub机制进行通信‌12。

Redis Cluster则旨在提供横向扩展和负载均衡,通过将数据分片存储在多个节点上,并使用哈希分片算法将数据均匀分布到各个节点上,以实现高吞吐量和低延迟。集群模式不需要单独的Sentinel节点,每个节点都可以参与到集群管理中,实现数据的分片和负载均衡‌24。

在管理和配置方面,Redis Sentinel需要额外的Sentinel进程来监控和管理Redis实例,需要在哨兵节点上配置监控的主从实例信息。而Redis Cluster则需要在启动Redis时通过特定的配置文件或命令行参数来指定节点的集群信息,并确保所有节点都运行在相同的集群模式配置下‌2。

适用场景方面,Redis Sentinel适用于需要保证Redis实例高可用性和故障恢复能力的场景,例如关键业务数据的缓存和会话存储等。而Redis Cluster则适用于需要处理大规模数据和高并发请求的场景,例如大型Web应用程序、分布式会话存储和实时分析等‌2。

5,Redis分布式锁跟zk 分布式锁的区别在哪里?

一、实现原理

  • Redis 分布式锁:通常利用 Redis 的 SETNX(SET if Not eXists)命令来实现。当一个客户端执行 SETNX 成功时,就获得了锁,锁的过期时间通过 EXPIRE 命令设置。如果锁过期,其他客户端可以重新获取锁。Redis 分布式锁的实现相对简单,但需要考虑锁的过期时间设置、防止死锁等问题。
  • Zookeeper 分布式锁:基于 Zookeeper 的临时顺序节点和 Watcher 机制实现。客户端在 Zookeeper 中创建一个临时顺序节点,如果该节点是所有节点中序号最小的,就获得了锁。当持有锁的客户端释放锁或者出现故障时,Zookeeper 会通知等待锁的客户端,以便它们重新竞争锁。Zookeeper 分布式锁的实现相对复杂,但具有较高的可靠性和稳定性。

二、可靠性

  • Redis 分布式锁:如果 Redis 出现故障,例如主从切换、网络分区等情况,可能会导致锁的失效或者出现多个客户端同时获得锁的情况。为了提高可靠性,可以采用 Redlock 算法,在多个 Redis 实例上获取锁,但这也增加了实现的复杂性。
  • Zookeeper 分布式锁:Zookeeper 本身具有高可靠性和强一致性,一旦客户端创建了临时顺序节点,只要 Zookeeper 集群正常运行,锁就不会失效。即使客户端出现故障,Zookeeper 也会自动删除临时节点,释放锁,不会出现死锁的情况。

三、性能

  • Redis 分布式锁:Redis 是内存数据库,具有非常高的性能。获取和释放锁的操作非常快速,适用于对性能要求较高的场景。
  • Zookeeper 分布式锁:Zookeeper 的性能相对较低,主要是因为它需要进行网络通信和节点的创建、删除等操作。但是,在一般的应用场景下,Zookeeper 的性能也是可以接受的。

四、锁的特性

  • Redis 分布式锁:通常是排他锁,即同一时间只有一个客户端可以获得锁。可以通过设置不同的锁名称来实现多个不同资源的锁。
  • Zookeeper 分布式锁:可以实现排他锁和共享锁。排他锁与 Redis 类似,同一时间只有一个客户端可以获得锁。共享锁允许多个客户端同时获得锁,适用于读多写少的场景。

五、适用场景

  • Redis 分布式锁:适用于对性能要求较高、对可靠性要求相对较低的场景,例如缓存更新、分布式任务调度等。
  • Zookeeper 分布式锁:适用于对可靠性要求较高、对性能要求相对较低的场景,例如分布式事务、分布式配置管理等。

6,什么是缓存雪崩,缓存穿透,缓存击穿, 你怎么解决?

一、缓存雪崩

定义
缓存雪崩是指缓存中大量的数据在同一时间过期,或者缓存服务出现故障,导致大量的请求直接访问数据库,给数据库造成巨大的压力,甚至可能导致数据库宕机。

解决方法

  1. 缓存数据的过期时间设置为随机值,避免大量缓存同时过期。
  2. 搭建缓存高可用集群,当一个缓存节点出现故障时,其他节点可以继续提供服务。
  3. 增加数据库的缓存层,如使用数据库连接池等技术,减轻数据库的压力。
  4. 进行限流和降级处理,当系统面临巨大压力时,可以限制部分请求或者返回降级数据,保证系统的核心功能正常运行。

二、缓存穿透

定义
缓存穿透是指查询一个不存在的数据,由于缓存中没有该数据,每次查询都要穿透到数据库,导致数据库承受大量无效的查询压力。如果被恶意攻击者利用,可能会对数据库造成严重的影响。

解决方法

  1. 对于不存在的数据,也在缓存中存储一个空值或者默认值,并设置较短的过期时间。这样下次查询相同的数据时,就可以直接从缓存中返回,而不会穿透到数据库。
  2. 使用布隆过滤器,在查询数据之前,先通过布隆过滤器判断数据是否可能存在。如果布隆过滤器判断数据不存在,就直接返回,避免穿透到数据库。
  3. 加强对数据库的安全防护,防止恶意攻击。

三、缓存击穿

定义
缓存击穿是指一个非常热点的数据,在缓存过期的瞬间,同时有大量的请求来访问这个数据,导致这些请求直接穿透到数据库,给数据库带来巨大压力。

解决方法

  1. 加互斥锁,当第一个请求发现缓存过期时,对该数据加锁,其他请求等待。第一个请求从数据库中查询数据并更新缓存后,其他请求可以直接从缓存中获取数据。
  2. 热点数据永不过期,对于热点数据,可以设置永不过期,或者在缓存即将过期时,提前更新缓存。

7,缓存跟DB的一致性问题怎么产生?

缓存和数据库的一致性问题通常是由于数据在缓存和数据库中的不同步引起的。以下是一些可能导致一致性问题的情况以及解决方法:

一、产生一致性问题的情况

  1. 先更新数据库,后更新缓存:

    • 如果在更新数据库后,更新缓存的过程中出现问题,比如网络故障或者缓存服务故障,就会导致缓存中的数据与数据库中的数据不一致。
    • 即使更新缓存成功,如果有多个应用实例同时更新数据库和缓存,也可能出现缓存数据不一致的情况。例如,一个实例更新了数据库,还没来得及更新缓存,另一个实例读取了旧的缓存数据并更新了数据库,然后第一个实例更新缓存,就会导致缓存中的数据与数据库中的数据不一致。
  2. 先更新缓存,后更新数据库:

    • 如果在更新缓存后,更新数据库的过程中出现问题,同样会导致缓存中的数据与数据库中的数据不一致。
    • 而且,如果有多个应用实例同时更新缓存和数据库,也可能出现缓存数据不一致的情况。例如,一个实例更新了缓存,还没来得及更新数据库,另一个实例读取了新的缓存数据并更新了数据库,然后第一个实例更新数据库,就会导致数据库中的数据与缓存中的数据不一致。
  3. 先删除缓存,后更新数据库:

    • 如果在删除缓存后,更新数据库的过程中出现问题,那么在缓存被删除后,下一次读取数据时,会从数据库中读取旧的数据并更新到缓存中,导致缓存中的数据与数据库中的数据不一致。
  4. 先更新数据库,后删除缓存:

    • 如果在更新数据库后,删除缓存的过程中出现问题,那么缓存中的数据仍然是旧的数据,与数据库中的数据不一致。
    • 但是,这种方式相对来说出现一致性问题的概率较小,因为即使删除缓存失败,下一次读取数据时,会从数据库中读取新的数据并更新到缓存中,最终可以保证缓存中的数据与数据库中的数据一致。

二、解决一致性问题的方法

  1. 延迟双删策略:

    • 先更新数据库。
    • 然后删除缓存。
    • 等待一段时间(比如几百毫秒),再次删除缓存。这样可以确保在更新数据库后,即使有其他应用实例读取了旧的缓存数据并更新到缓存中,也可以通过第二次删除缓存来保证缓存中的数据与数据库中的数据一致。
  2. 使用消息队列:

    • 当更新数据库时,将更新操作发送到消息队列中。
    • 消费消息队列中的消息,进行缓存的更新或删除操作。这样可以确保缓存的更新或删除操作与数据库的更新操作在同一个事务中,从而保证数据的一致性。
  3. 读取数据库时更新缓存:

    • 当读取数据时,如果缓存中不存在数据,从数据库中读取数据并更新到缓存中。
    • 如果缓存中的数据已经过期,从数据库中读取新的数据并更新到缓存中。这样可以确保缓存中的数据始终与数据库中的数据一致。
  4. 监控和报警:

    • 对缓存和数据库中的数据进行监控,及时发现数据不一致的情况。
    • 当发现数据不一致时,发出报警,以便及时进行处理。

8,说下动态代理的理解?

动态代理是一种在运行时动态地创建代理对象的技术。代理对象可以在不修改原始对象代码的情况下,对原始对象的方法进行增强、拦截或修改。

一、实现方式

在 Java 中,动态代理主要有两种实现方式:基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。

1,JDK 动态代理

  • 要求被代理的对象必须实现一个或多个接口。

  • 通过实现InvocationHandler接口,并重写invoke方法来定义代理对象的行为。在invoke方法中,可以在调用被代理对象的方法前后添加额外的逻辑。

  • 使用Proxy.newProxyInstance方法创建代理对象。

     interface TargetInterface {
         void doSomething();
     }
    
     class TargetObject implements TargetInterface {
         @Override
         public void doSomething() {
             System.out.println("Target object is doing something.");
         }
     }
    
     class ProxyHandler implements InvocationHandler {
         private Object target;
    
         public ProxyHandler(Object target) {
             this.target = target;
         }
    
         @Override
         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
             System.out.println("Before calling method.");
             Object result = method.invoke(target, args);
             System.out.println("After calling method.");
             return result;
         }
     }
    
     public class Main {
         public static void main(String[] args) {
             TargetInterface targetObject = new TargetObject();
             TargetInterface proxyObject = (TargetInterface) Proxy.newProxyInstance(
                     targetObject.getClass().getClassLoader(),
                     targetObject.getClass().getInterfaces(),
                     new ProxyHandler(targetObject));
             proxyObject.doSomething();
         }
     }
    

2,CGLIB 动态代理

  • 可以代理没有实现接口的类。

  • 通过继承被代理对象的类,并重写需要增强的方法来实现代理。

  • 使用Enhancer类来创建代理对象。

     class TargetClass {
         public void doSomething() {
             System.out.println("Target class is doing something.");
         }
     }
    
     class CglibProxyInterceptor implements MethodInterceptor {
         @Override
         public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
             System.out.println("Before calling method.");
             Object result = proxy.invokeSuper(obj, args);
             System.out.println("After calling method.");
             return result;
         }
     }
    
     public class Main {
         public static void main(String[] args) {
             Enhancer enhancer = new Enhancer();
             enhancer.setSuperclass(TargetClass.class);
             enhancer.setCallback(new CglibProxyInterceptor());
             TargetClass proxyObject = (TargetClass) enhancer.create();
             proxyObject.doSomething();
         }
     }
    

二、应用场景

  1. 日志记录:可以在方法执行前后记录日志,方便调试和性能分析。
  2. 事务管理:在方法执行前开启事务,在方法执行后提交或回滚事务。
  3. 权限控制:检查用户是否具有执行某个方法的权限。
  4. 性能监控:统计方法的执行时间,以便进行性能优化。

三、优点

  1. 灵活性高:可以在不修改原始代码的情况下,对方法进行增强和修改。
  2. 解耦性好:代理对象和被代理对象之间通过接口进行交互,降低了它们之间的耦合度。
  3. 易于维护:如果需要修改代理逻辑,只需要修改代理对象的代码,而不会影响到被代理对象。

四、缺点

  1. 性能开销:由于需要在运行时动态创建代理对象,会有一定的性能开销。
  2. 复杂性增加:动态代理的实现相对复杂,需要对反射和字节码操作有一定的了解。

9,项目中有用过redis 吗? 用在哪里?

Redis 主要被用在以下几个方面:

一、缓存

  1. 缓存数据库查询结果:对于一些频繁查询且数据变化不频繁的数据,如用户信息、商品信息等,可以将查询结果缓存到 Redis 中。这样,下次查询时可以直接从 Redis 中获取数据,大大提高查询速度,减轻数据库的压力。
  2. 缓存页面片段:对于一些动态生成但内容相对固定的页面片段,如热门商品列表、推荐文章等,可以将其缓存到 Redis 中。当用户请求这些页面时,可以直接从 Redis 中获取页面片段,快速组装页面并返回给用户。

二、分布式锁

在分布式系统中,为了保证数据的一致性和避免并发问题,可以使用 Redis 实现分布式锁。例如,在多个实例同时处理一个任务时,只有获得锁的实例才能执行任务,其他实例等待锁释放后再尝试获取锁。

三、消息队列

Redis 的列表数据结构可以用作简单的消息队列。生产者将消息推入列表,消费者从列表中弹出消息进行处理。虽然 Redis 作为消息队列的功能相对简单,但对于一些轻量级的消息传递场景非常实用。

四、计数器和限速器

  1. 计数器:可以使用 Redis 的原子操作来实现计数器功能,如统计网站的访问次数、用户的操作次数等。
  2. 限速器:通过 Redis 的过期时间和原子操作,可以实现限速功能,例如限制用户在一定时间内的请求次数,防止恶意攻击或过度使用资源。

五、会话存储

在分布式系统中,可以将用户会话信息存储在 Redis 中,实现会话共享。这样,当用户在不同的服务器节点之间切换时,仍然可以保持会话状态的一致性。

10,redis 怎么做缓存设计?

一、确定缓存数据

  1. 分析业务需求,确定哪些数据适合缓存。一般来说,频繁读取且相对静态的数据适合缓存,比如用户信息、商品详情、配置参数等。
  2. 考虑数据的时效性,对于时效性要求高的数据,需要设置合理的缓存过期时间,以保证数据的新鲜度。

二、缓存策略选择

  1. 全量缓存:将数据库中的所有数据一次性缓存到 Redis 中。这种策略适用于数据量较小且数据变化不频繁的情况。但在数据量大时,可能会占用大量内存,并且在数据更新时需要重新加载整个数据集。
  2. 增量缓存:只缓存部分关键数据或最近访问的数据。当有新的数据被访问时,再将其缓存到 Redis 中。这种策略可以节省内存,但需要有合理的缓存更新机制,以保证缓存中的数据是最新的。

三、缓存更新机制

  1. 主动更新:在数据发生变化时,主动更新 Redis 中的缓存数据。可以通过数据库的触发器、消息队列或者在业务代码中手动触发缓存更新。
  2. 被动更新:当客户端请求数据时,如果发现缓存中数据过期或不存在,再从数据库中读取数据并更新缓存。这种方式相对简单,但可能会导致缓存穿透和数据不一致的问题,需要采取相应的措施进行处理。

四、缓存淘汰策略

  1. 当 Redis 内存不足时,需要选择合适的缓存淘汰策略。Redis 提供了多种淘汰策略,如 LRU(最近最少使用)、LFU(最不经常使用)、随机淘汰等。
  2. 根据业务需求和数据访问模式选择合适的淘汰策略。例如,如果数据的访问模式比较随机,可以选择随机淘汰策略;如果数据的访问模式符合最近最少使用原则,可以选择 LRU 策略。

五、缓存一致性保证

  1. 为了保证缓存和数据库中的数据一致性,可以采用先更新数据库,再删除缓存的策略。这样,当有数据更新时,先更新数据库,然后删除对应的缓存数据,下次读取数据时会从数据库中重新加载最新数据到缓存中。
  2. 可以使用消息队列或者定时任务来异步地更新缓存,以减少对业务的影响。同时,需要考虑在缓存更新过程中可能出现的问题,如网络故障、缓存服务故障等,采取相应的容错措施。

六、缓存监控和优化

  1. 对 Redis 的使用情况进行监控,包括内存使用情况、缓存命中率、请求响应时间等指标。通过监控可以及时发现问题,并进行优化调整。
  2. 根据监控数据调整缓存策略和参数,如缓存过期时间、缓存淘汰策略等。同时,可以对 Redis 的配置进行优化,以提高性能和稳定性。

11, Redis内存满了,如何处理?

一、增加内存容量

如果可能的话,可以考虑增加 Redis 服务器的内存容量。这是最直接的方法,但可能需要一定的成本投入。

二、数据淘汰策略

  1. Redis 提供了多种数据淘汰策略,可以根据实际情况进行选择。例如:

    • LRU(Least Recently Used):最近最少使用算法,淘汰最近最少使用的键值对。
    • LFU(Least Frequently Used):最不经常使用算法,淘汰使用频率最低的键值对。
    • volatile-lru:在设置了过期时间的键值对中,使用 LRU 算法进行淘汰。
    • volatile-lfu:在设置了过期时间的键值对中,使用 LFU 算法进行淘汰。
    • allkeys-lru:在所有的键值对中,使用 LRU 算法进行淘汰。
    • allkeys-lfu:在所有的键值对中,使用 LFU 算法进行淘汰。
    • volatile-random:在设置了过期时间的键值对中,随机淘汰。
    • allkeys-random:在所有的键值对中,随机淘汰。
    • volatile-ttl:淘汰剩余生存时间(TTL)最短的键值对。
  2. 可以通过修改 Redis 的配置文件来设置数据淘汰策略,例如:

    maxmemory-policy volatile-lru

三、数据压缩

  1. 对于一些占用内存较大的数据,可以考虑使用数据压缩算法来减少内存占用。例如,可以使用 Redis 的 ziplist 数据结构来存储一些小的列表或哈希表,ziplist 会对数据进行压缩存储。

  2. 对于字符串类型的数据,可以考虑使用 Redis 的字符串压缩功能。可以通过修改 Redis 的配置文件来开启字符串压缩功能,例如:

    set maxmemory 100mb set maxmemory-samples 5 set maxmemory-policy volatile-lru set lazyfree-lazy-eviction no set lazyfree-lazy-expire no set lazyfree-lazy-server-del no set active-defrag no set activedefrag-ignore-bytes 100mb set activedefrag-threshold-lower 10 set activedefrag-threshold-upper 100 set activedefrag-cycle-min 25 set activedefrag-cycle-max 75 set activedefrag-cycle-min-usec 5000 set activedefrag-cycle-max-usec 10000 set activedefrag-cycle-pct-change 10

四、数据清理

  1. 手动清理一些不必要的数据。可以根据业务需求,删除一些过期的数据、临时数据或者不再使用的数据。
  2. 可以使用 Redis 的命令来删除数据,例如:
    • DEL:删除指定的键值对。
    • FLUSHALL:删除所有的键值对。
    • FLUSHDB:删除当前数据库中的所有键值对。

五、数据分片

  1. 如果数据量非常大,可以考虑使用 Redis 的数据分片功能。将数据分散存储在多个 Redis 实例中,每个实例只存储一部分数据。这样可以有效地利用多个服务器的内存资源,提高系统的存储容量。
  2. 可以使用 Redis 的 Cluster 模式或者客户端分片来实现数据分片。