Java高频面试之Redis篇

本文围绕Java高频面试题,详细讨论了集合、异常处理、并发编程、SSM框架、MySQL、Redis的使用场景、Redis功能、缓存一致性、数据持久化、分布式锁、淘汰策略以及常见性能问题和解决方案,适合面试准备和技术学习者参考。
摘要由CSDN通过智能技术生成

有需要互关的小伙伴,关注一下,有关必回关,争取今年认证早日拿到博客专家

Java高频面试之总纲篇

Java高频面试之集合篇

Java高频面试之异常篇

Java高频面试之并发篇

Java高频面试之SSM篇

Java高频面试之Mysql篇

Java高频面试之Redis篇

Java高频面试之消息队列与分布式篇

50道SQL面试题

奇奇怪怪的面试题

五花八门的内存溢出

谈下你对 Redis 的了解?

Remote Dictionary Server 远程字典服务

  1. 高性能:redis是一个内存数据库,基于内存的读写
  2. 丰富的数据结构
  3. 支持持久化
  4. 支持集群

Redis 一般都有哪些使用场景?

  • 缓存(减少数据库压力)

    1. 缓存热点数据
    2. 缓存一些计算结果
  • 数据共享

    1. 分布式锁
    2. 分布式计数器
    3. 分布式session
  • redis自身的特性

    1.sortedset的排行榜

public class LeaderboardService {
    private final RedisTemplate<String, String> redisTemplate;

    public LeaderboardService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 添加玩家到排行榜
    public void addPlayerToLeaderboard(String player, double score) {
        redisTemplate.opsForZSet().add("leaderboard", player, score);
    }

    // 获取排行榜中前 N 名玩家 /rɪˈvɜːs/
    public Set<ZSetOperations.TypedTuple<String>> getLeaderboard(int topN) {
        return redisTemplate.opsForZSet().reverseRangeWithScores("leaderboard", 0, topN - 1);
    }

    // 更新玩家在排行榜中的分数
    public void updatePlayerScore(String player, double score) {
        redisTemplate.opsForZSet().incrementScore("leaderboard", player, score);
    }

    // 从排行榜中移除玩家
    public void removePlayerFromLeaderboard(String player) {
        redisTemplate.opsForZSet().remove("leaderboard", player);
    }
}
  1. 利用set的pop()和randomMembers抽奖
/**
 * 一等奖1个,二等奖2个,三等奖3个,参与奖100个
 */
public class LotteryService {
    private final RedisTemplate<String, String> redisTemplate;
    private final JdbcTemplate jdbcTemplate;
    private static final String SPOP_USER_SETS = "pop:user:set";

    public LotteryService(JdbcTemplate jdbcTemplate, RedisTemplate<String, String> redisTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.redisTemplate = redisTemplate;
    }

    public void initUserSet(String prize) {
        String sql = "select id from user";
        List<Map<String, Object>> userIds = jdbcTemplate.queryForList(sql);
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        String[] ids = userIds.stream().map(item -> String.valueOf(item.get("id").toString())).toArray(String[]::new);
        ops.add(SPOP_USER_SETS, ids);
    }

    public void drawAllPrize() {
        List<String> firstPrize = drawPrize(1);
        System.out.println("一等奖:" + firstPrize.get(0));
        List<String> secondPrize = drawPrize(2);
        System.out.println("二等奖:" + String.join(",", secondPrize));
        List<String> thirdPrize = drawPrize(3);
        System.out.println("三等奖:" + String.join(",", thirdPrize));
        List<String> participationPrize = drawPrize(100);
        System.out.println("参与奖:" + String.join(",", participationPrize));
    }

    /**
     * 一个人最多获取一次奖品
     * @param count
     * @return
     */
    public List<String> drawPrize(int count) {
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        // Remove and return count random members from set at key.
        return ops.pop("prize_pool", count);
    }

    /**
     * draw 抽取
     * 允许一个人多次抽取奖品
     * @param count
     * @return
     */
    public List<String> drawPrize4duplicate(int count) {
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        // Get count random elements from set at key.
        return ops.randomMembers("prize_pool", count);
    }

}
  1. 统计

    基于String的统计

    无法去重,准确

public class Stats4String {
    private RedisTemplate<String, String> redisTemplate;

    public Stats4String(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 比如点赞/收藏
    public void incrementCount(String key) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        ops.increment(key);
    }

    // 比如取消点赞/取消收藏
    public void decrementCount(String key) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        ops.decrement(key);
    }

    // 获取统计数量
    public Long getCount(String key) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        String countStr = ops.get(key);

        if (countStr != null) {
            return Long.parseLong(countStr);
        } else {
            return 0L;
        }
    }

    // 清空统计数据
    public void clearCount(String key) {
        redisTemplate.delete(key);
    }
}

基于HyperLogLog的统计

去重,不可减,不准确,节省内存

public class Stats4HyperLogLog {
    private RedisTemplate<String, String> redisTemplate;

    public Stats4HyperLogLog(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 添加元素到 HyperLogLog
    public void addElement(String key, String... elements) {
        HyperLogLogOperations<String, String> ops = redisTemplate.opsForHyperLogLog();
        ops.add(key, elements);
    }

    // 获取 HyperLogLog 的基数估计值
    public long getApproximateCount(String key) {
        HyperLogLogOperations<String, String> ops = redisTemplate.opsForHyperLogLog();
        return ops.size(key);
    }

    // 合并多个 HyperLogLog
    public void mergeHyperLogLogs(String destinationKey, String... sourceKeys) {
        HyperLogLogOperations<String, String> ops = redisTemplate.opsForHyperLogLog();
        ops.union(destinationKey, sourceKeys);
    }

    // 清空 HyperLogLog
    public void clearHyperLogLog(String key) {
        redisTemplate.delete(key);
    }
}

基于Set的统计

public class SetStats4Set {
    private final RedisTemplate<String, String> redisTemplate;

    public SetStats4Set(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 例如点赞
    public void add(String key, String... elements) {
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        ops.add(key, elements);
    }

    // 例如取消点赞
    public void remove(String key, Object... elements) {
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        ops.remove(key, elements);
    }

    // 例如点赞数
    public long getDistinctCount(String key) {
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        Long size = ops.size(key);
        return size != null ? size : 0L;
    }

    // 例如点赞的人
    public Set<String> members(String key) {
        SetOperations<String, String> ops = redisTemplate.opsForSet();
        return ops.members(key);
    }

    // 清空集合
    public void clearSet(String key) {
        redisTemplate.delete(key);
    }
}
  1. 社交模型
  2. 消息队列
  3. 发布/订阅
  4. 地理位置和地理信息

Redis 有哪些常见的功能?

Redis 支持的数据类型有哪些?

  1. 字符串(String):字符串是 Redis 最基本的数据类型,可以存储任何类型的数据,例如文本、数字等。

  2. 哈希(Hash):哈希是一个键值对的集合,类似于其他编程语言中的字典或映射。在 Redis 中,哈希用于存储对象,每个对象都有一个唯一的键,对应多个字段和值。

  3. 列表(List):列表是一个按照插入顺序排序的字符串集合。Redis 的列表是一个双向链表,可以在列表的两端进行元素的插入和删除操作。

  4. 集合(Set):集合是一个无序的、不重复的字符串集合。Redis 的集合可以进行交集、并集、差集等集合操作,还可以判断一个元素是否存在于集合中。

  5. 有序集合(Sorted Set):有序集合是一个有序的、不重复的字符串集合,每个成员都关联着一个分数,通过分数进行排序。可以用于实现排行榜、优先级队列等场景。

  6. 位图(Bitmap):位图是 Redis 特有的一种数据结构,可以对比特位进行操作,支持位操作的逻辑运算。

  7. HyperLogLog:HyperLogLog 是一种用于进行基数(不重复元素)估计的数据结构,用于统计大规模数据的去重数目。

  8. 地理空间索引(Geospatial Index):Redis 支持地理空间数据的存储和查询,可以存储地理位置信息,并支持附近点的搜索和距离计算。

Redis 为什么这么快?

  1. 内存存储:Redis 将数据存储在内存中,而不是在磁盘上。相对于磁盘访问,内存访问速度更快,因为内存的读写延迟更低。这使得 Redis 能够以非常高的速度进行数据读写操作。
  2. 单线程模型:Redis 使用单线程模型来处理客户端请求。这意味着 Redis 在任意给定时间内只会处理一个请求,避免了多线程之间的竞争和同步开销。通过避免线程切换和上下文切换的开销,Redis 可以更高效地处理请求,提高整体性能。
  3. 非阻塞 I/O:Redis 使用非阻塞的 I/O 模型,通过使用事件驱动的方式来处理网络请求。它利用操作系统提供的 epoll、kqueue、IOCP 等机制,实现了高效的事件多路复用,可以同时处理多个连接,避免了线程和连接之间的阻塞,提高了并发性能。
  4. 精简的数据结构:Redis 的数据结构经过精心设计,具有高效的实现方式。例如,Redis 使用哈希表来实现字典和哈希数据类型,使用跳表(Skip List)来实现有序集合,这些数据结构在查找、插入和删除操作上都具有较高的性能。
  5. 优化的网络协议:Redis 使用自己的协议进行客户端与服务器之间的通信,该协议基于 TCP 连接,并且是基于文本的协议。这种简单的协议设计使得数据传输更加高效,并且能够减少网络带宽的消耗。
  6. 内部优化策略:Redis 在内部实现中采用了多种优化策略,如对象共享、内存池、事件合并等,以提高内存利用率和减少内存碎片。此外,Redis 还针对不同的使用场景提供了一系列的配置选项和优化参数,可以根据具体需求进行调整,以获得更好的性能。

综合上述因素,Redis 在数据存储、读写操作和网络通信等方面做出了高效的设计和优化,从而实现了快速的响应和高性能

什么是缓存穿透?怎么解决?

缓存中没有,数据库中也没有

缓存穿透是指在使用缓存系统时,恶意或非法的请求导致缓存无法命中,并且每次请求都会直接访问后端存储系统,对系统造成了极大的压力和性能问题。

常见的缓存穿透场景是当一个请求查询一个不存在的数据时,由于缓存中没有该数据的记录,请求会直接访问后端数据库,但由于数据不存在,后端数据库也无法返回结果,这样的请求会一直穿透缓存直达数据库。

为了解决缓存穿透问题,可以采取以下几种策略:

  1. 布隆过滤器(Bloom Filter):布隆过滤器是一种数据结构,用于快速检测某个元素是否存在于一个集合中。可以将所有可能存在的数据的特征值添加到布隆过滤器中,请求首先经过布隆过滤器进行检测,如果检测结果为不存在,则可以直接返回结果,避免了对后端存储系统的访问。
  2. 缓存空对象(Cache Null Object):针对查询结果为空的情况,可以将空对象(如空字符串或特殊对象)缓存起来,设置一个较短的过期时间。当查询请求命中空对象时,可以直接返回缓存中的空结果,避免对后端存储系统的频繁访问。
  3. 数据预热(Cache Pre-warming):在系统启动时,或者在缓存失效之前,预先将热门数据加载到缓存中,确保热门数据在缓存中存在,减少缓存穿透的可能性。
  4. 异常请求过滤:在应用层面对请求进行过滤,排除掉一些恶意或非法的请求。例如,可以根据请求参数的合法性进行校验,对于非法的请求直接进行拦截和过滤,避免对后端存储系统的无效访问。
  5. 限流和熔断:采用限流和熔断机制,对频繁发起的请求进行控制和限制。可以设置请求频率的阈值,超过阈值则拒绝请求或返回错误信息,从而保护后端存储系统免受大量无效请求的影响。

综合运用上述策略,可以有效地解决缓存穿透问题,减轻对后端存储系统的负载压力,提高系统的性能和稳定性。

什么是缓存击穿?如何解决?

数据库有,缓存中没有

缓存击穿是指在使用缓存系统时,某个热门数据过期或被删除后,恰好有大量的并发请求同时访问该数据,导致这些请求都无法命中缓存,直接访问后端存储系统,对后端系统造成巨大压力,可能引发系统崩溃或性能下降的问题。

解决方案:

  1. 设置热门数据的永不过期策略:对于一些非常热门的数据,可以设置缓存的过期时间为永不过期(或设置一个较长的过期时间),确保热门数据在缓存中一直存在,避免因过期而导致的击穿问题。但这种方式需要注意数据的更新机制,以保证数据的实时性。
  2. 使用互斥锁(Mutex Lock)或分布式锁(Distributed Lock):在数据过期后,当有一个请求发现缓存失效时,可以尝试获取一个互斥锁或分布式锁。如果获取成功,该请求可以访问后端存储系统,并将结果写入缓存;如果获取失败,说明已有其他请求正在处理数据,可以等待或直接返回之前的结果,避免多个请求同时访问后端存储系统。
  3. 引入热点数据预加载机制:在系统启动时或非高峰期,可以预先将一些热点数据加载到缓存中,确保热门数据一直存在于缓存中,从而减少因数据过期而引发的击穿问题。

什么是缓存雪崩?该如何解决?

大量缓存同时过期或者失效

缓存雪崩是指在使用缓存系统时,缓存中大量的数据同时过期或失效,导致大量的请求直接访问后端存储系统,使得后端系统承受巨大的压力,甚至引发系统崩溃的现象。

解决缓存雪崩问题的方法如下:

  1. 设置合理的缓存过期时间:合理设置缓存数据的过期时间,并且将过期时间分散开来,避免大量数据在同一时间点过期。可以通过在每个缓存数据的过期时间上增加一个随机值或在定期时间上增加一个随机偏移量来实现。
  2. 使用缓存预热机制:在系统启动或低峰期,预先将部分或全部热门数据加载到缓存中,以确保缓存中的数据一直存在,即使发生缓存失效也能保持一定的正常访问量。
  3. 引入多级缓存架构:通过使用多级缓存架构,例如将缓存分为本地缓存和分布式缓存,本地缓存作为第一级缓存存在于应用程序内部,分布式缓存作为第二级缓存,可以减轻单一缓存系统故障对整个系统的影响。
  4. 缓存数据永不过期策略:对于一些热门数据,可以设置缓存的过期时间为永不过期,或者设置一个很长的过期时间,确保这些热门数据一直存在于缓存中。
  5. 使用限流和熔断机制:通过限制并发请求的数量,例如使用限流算法或熔断器,可以避免大量请求同时访问后端存储系统,减轻后端系统的压力。
  6. 数据异步更新:当缓存数据失效时,将请求缓存和数据更新的操作进行异步化处理。例如,在缓存失效的情况下,通过后台任务异步更新数据并回填到缓存中,期间继续使用旧数据,避免大量请求同时直接访问后端存储系统。
  7. 高可用架构:在缓存层面使用高可用架构,如使用缓存集群、主从复制、分布式缓存等技术,确保缓存系统的高可用性和容错性,减少缓存故障对整个系统的影响。

综合运用上述解决方案,可以有效地预防和应对缓存雪崩问题,保障系统的稳定性和性能。

怎么保证缓存和数据库数据的一致性?

先更新数据库再删除缓存,两步操作,防止第二步失败,所以对第二步加个重试(失败一定次数,告诉定时任务啥的)

利用阿里的开源组件Canal

Redis 持久化有几种方式?

  1. RDB
  2. AOF

Redis 怎么实现分布式锁?

Redis 淘汰策略有哪些?

  1. noeviction 不淘汰,数据满了再写就报错。

  2. volatile-ttl 在设置了过期时间的key中选,越早过期的越先被删除。

  3. volatile-random(X2) 随机

  4. LRU算法(X2) 最近最不长使用了(访问时间维度)

    LRU 策略的核心思想:如果一个数据刚刚被访问,那么这个数据肯定是热数据,还会被再次访问。

    按照这个核心思想,Redis 中的 LRU 策略,会在每个数据对应的 RedisObject 结构体中设置一个 lru 字段,用来记录数据的访问时间戳。在进行数据淘汰时,LRU 策略会在候选数据集中淘汰掉 lru 字段值最小的数据(也就是访问时间最久的数据)。

    class LRU4LinkedHashMap {
        public static Map<String, Object> map = new LinkedHashMap<>();
    
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            putValue("k1", "v1");
            putValue("k2", "v2");
            putValue("k3", "v3");
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                sb.append(entry.getKey());
            }
            System.out.println(sb);
            sb = new StringBuilder();
            putValue("k2", "v22");
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                sb.append(entry.getKey());
            }
            System.out.println(sb);
            sb = new StringBuilder();
            getValue("k1");
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                System.out.print(entry.getKey());
            }
            System.out.println(sb);
    
        }
    
        private static void putValue(String key, Object value) {
            if (map.containsKey(key)) {
                map.remove(key);
            }
            map.put(key, value);
        }
    
        private static Object getValue(String key) {
            Object value = map.get(key);
            if (value != null) {
                map.remove(key);
                map.put(key, value);
            }
            return value;
        }
    
    }
    
  5. lfu(X2) 使用频率最少的(访问频率维度)

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

  1. 内存不足:当 Redis 实例使用的内存超过可用内存时,性能可能会下降或出现崩溃。解决方案包括:
    • 监控 Redis 实例的内存使用情况,并及时扩容内存。
    • 配置 Redis 的最大内存限制,使用 maxmemory 参数控制内存使用量,并采用合适的策略(例如使用 LRU 策略)来处理超出内存限制的情况。
  2. 持久化操作的性能问题:Redis 支持持久化数据到磁盘,以便在重启后恢复数据。但是,持久化操作(如 RDB 快照和 AOF 日志)可能会对性能产生影响。解决方案包括:
    • 针对持久化操作的频率和性能要求进行调优,选择合适的持久化方式(RDB 或 AOF)。
    • 对于 AOF 日志,可以使用合适的 fsync 策略(如 everysec)来平衡数据安全和性能之间的权衡。
  3. 频繁的键过期操作:如果 Redis 实例中有大量的键需要过期处理,可能会对性能造成负面影响。解决方案包括:
    • 避免使用大量的键过期操作,或者减少键过期的时间间隔。
    • 如果需要对大量的键进行过期处理,可以使用 Redis 的持久化功能,将过期键保存到磁盘上的 RDB 或 AOF 文件中,以减轻内存压力。
  4. 频繁的数据更新操作:如果 Redis 实例中有大量的写操作(如 SET、INCRBY 等),可能会导致性能瓶颈。解决方案包括:
    • 使用批量操作来减少单个操作的数量,例如使用 MSET、HSET 等进行批量设置。
    • 针对频繁更新的键,可以考虑使用 Redis 的数据结构(如 Hash、List)来存储和更新数据,以减少键的数量。
  5. 高并发访问:当 Redis 实例面临高并发的读写访问时,可能会导致性能下降。解决方案包括:
    • 使用 Redis 集群或主从复制来实现读写分离,将读操作分散到多个实例,减轻单个实例的负载。
    • 使用连接池管理 Redis 客户端连接

Java高频面试之总纲篇

Java高频面试之集合篇

Java高频面试之异常篇

Java高频面试之并发篇

Java高频面试之SSM篇

Java高频面试之Mysql篇

Java高频面试之Redis篇

Java高频面试之消息队列与分布式篇

50道SQL面试题

奇奇怪怪的面试题

五花八门的内存溢出

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值