4

缓存面试解析:穿透、击穿、雪崩,一致性、分布式锁、Redis过期,海量数据查找 - 努力...

 1 year ago
source link: https://www.cnblogs.com/guoxiaoyu/p/17615706.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

为什么使用缓存

  1. 在程序内部使用缓存,比如使用map等数据结构作为内部缓存,可以快速获取对象。通过将经常使用的数据存储在缓存中,可以减少对数据库的频繁访问,从而提高系统的响应速度和性能。缓存可以将数据保存在内存中,读取速度更快,能够大大缩短数据访问的时间,提升用户体验。

  2. 在业界中,通常在数据库之前添加一层Redis缓存,这样可以避免数据库的性能被大量的请求耗费。当有大量的并发请求时,数据库可能会成为瓶颈,而使用缓存可以有效地缓解数据库的压力。Redis作为一种高效的缓存解决方案,可以将热门数据存储在内存中,以快速响应用户的请求。这种缓存层的引入不仅可以提高系统的性能和吞吐量,还可以提高系统的可靠性和稳定性,因为即使数据库出现故障,缓存仍然可以提供部分服务。

  3. 缓存还可以减少网络传输的负载,特别是在分布式系统中。通过将计算结果或频繁访问的数据缓存起来,可以避免重复计算和重复访问数据库,节省了网络带宽和服务器的资源消耗。这对于海量数据的查找和计算密集型任务尤为重要,可以大大提升系统的效率和可扩展性。

    总之,使用缓存可以优化系统的性能、提高响应速度、降低数据库负载、节省网络传输和服务器资源,从而提升用户体验和系统的可靠性。

缓存穿透、击穿、雪崩

缓存穿透:

缓存穿透指的是当一个请求查询的数据不在缓存中,也不在数据库中,导致每次请求都直接访问数据库,增加了数据库的负载。这可能是由于恶意攻击或者异常情况导致的。为了解决缓存穿透问题,可以采取以下措施:

  • 在缓存中存储一个空值或者默认值,且设置成一定过期时间,以避免重复的无效查询,但是这种方案有缺陷就是redis会多出无用的key,浪费内存资源;
  • 使用布隆过滤器等技术来过滤掉无效的请求,将可能不存在的数据快速过滤掉,布隆过滤器可以有效防止不存在的key进入业务调用数据库,但是需要提前将数据库数据预热到布隆过滤器中,并且他也有一种缺陷就是由于他的数据结构和算法导致无法删除热键,只能新增;

缓存击穿指的是当某个热点数据过期或者被删除时,大量的请求同时涌入,导致数据库负载过高。这通常发生在高并发环境下。为了避免缓存击穿问题,可以采取以下措施:

  • 第一种就是将热点数据永久缓存进redis,并另起一个线程定时的去更新这个热点数据,那么就热点数据永远不会失效,但是缺陷是在定时任务启动前可能存在数据错误的情况;
  • 第二种情况那么就是加锁,使用互斥锁或者分布式锁来保护对数据库的访问,确保只有一个请求能够重新加载数据到缓存中。但是这种虽然解决了数据库问题,但同时也带来了性能下降;

缓存雪崩指的是当缓存中大量的数据同时过期时,导致大量的请求直接访问数据库,造成数据库负载过高。这通常是由于缓存服务器故障、网络故障或者缓存数据过期时间设置不合理等原因导致的。为了避免缓存雪崩问题,可以采取以下措施:

  • 就是在给缓存数据设置过期时间的时候请加一个随机值使用不同的过期时间来分散缓存的失效时间,避免大量数据同时过期。
  • 使用热点数据预加载技术,在缓存数据即将过期之前,提前加载数据到缓存中,确保数据的可用性。

如何保证缓存与数据库之间的数据一致性

保证缓存与数据库之间的强一致性是一个相对复杂的问题。尽管没有绝对的解决方案,但可以采取一些策略来尽可能地提高数据一致性。以下是几种常见的策略:

第一种就是先删除缓存还是先写数据库,这两种都一样,我就说下先删除缓存带来的问题,先删除缓存确实可以在写完数据库后后续的操作都会更新缓存值,但是扛不住并发高,如果删除完缓存后还没来得及写入又被另一个线程读取了旧值更新缓存,那么这缓存白删除了,

第二种就是先写数据库呢?如果数据库写完后,一是在删除缓存之前的读操作读取的仍然是旧值,二是,如果写操作完成后,缓存删除操作由于网络原因丢失了怎么办,以后读取操作都是旧值了;

第三种也就是业界最常用的延时双删;但同时他也无法一定保证数据的一致性

  • 在操作数据库之前先删除缓存:首先,你需要先删除缓存中对应的数据,确保下一次读取请求不会命中旧的缓存数据。
  • 更新数据库:然后,你可以更新数据库中的数据,确保数据库中的数据是最新的。
  • 再次删除缓存:最后,在延时之后,再次删除缓存中的数据。这样可以确保在延时结束后,读操作仍然可以从缓存中获取最新的数据。

如果写操作很频繁,那么缺陷就很明显:很容易产生脏数据并且也无法满足缓存与数据库之间的一致性;

第四种:引入MQ,当我们有两个消费者的时候,一个消费者只管消息的数据库操作,一个消费者只管消息的缓存操作,这样可以确保操作是原子操作。确保了不会删除缓存失败的问题。

但是以上四种都无法保证缓存与数据库之间的强一致性,只能保证数据库与缓存之间的最终一致性;

如何设计分布式锁?如何对锁性能进行优化?

首先分布式锁主要应用场景就是应对多节点部署下如何控制资源的并发保护,那么单纯的jvm锁已经无法满足需求,所以引入了分布式锁,那么常见的有数据库、zookeeper、redis;通常分布式锁的要求的就是性能高、与业务无关;设计分布式锁时,常见的选择是使用Redis作为分布式锁的存储介质。下面将介绍如何设计分布式锁,并对锁性能进行优化。

首先,我们需要掌握Redis的基本命令:

  • SETNX:设置键值对,如果键不存在则返回1,如果键已存在则返回0。

  • EXPIRE:设置键的过期时间。

  • GETSET:先获取旧值,然后将新值设置进去;如果键不存在,则返回null。

  • DEL:删除一个键。

    接下来,我们将讨论几种常见的分布式锁设计方式:

  1. 使用SETNX和DEL操作:在当前业务执行完毕后,使用DEL操作删除锁。但是如果获取锁的进程执行失败,它将永远不会主动解锁,导致锁被死锁。
  2. 使用SETNX和EXPIRE操作:这是最常见的分布式锁设计方式。但是存在一个问题,如果在设置过期时间之前节点挂掉,其他服务将永远无法获取到锁,因为SETNX和EXPIRE不是原子操作。
  3. 使用SETNX和GETSET操作:在设置锁时,将过期时间作为值存储在Redis中。当其他线程争取锁失败时,可以通过GETSET操作检查当前锁是否已经失效。如果锁已失效,则可以使用自己的过期时间来替换旧的值,并与之前的过期时间进行比较,以确定是否成功获取锁。下面给出伪代码示例:
public boolean tryLock(RedisConnection conn) {
    long nowTime = System.currentTimeMillis();
    long expireTime = nowTime + 1000;
    if (conn.SETNX("mykey", expireTime) == 1) {
        conn.EXPIRE("mykey", 1000);
        return true;
    } else {
        long oldValue = conn.get("mykey");
        if (oldValue != null && oldValue < nowTime) {
            long currentValue = conn.GETSET("mykey", expireTime);
            if (oldValue == currentValue) {
                conn.EXPIRE("mykey", 1000);
                return true;
            }
            return false;
        }
        return false;
    }
}

上述代码实现了一种比较高效的分布式锁。然而,上述优化的根本问题在于SETNX和EXPIRE两个指令无法保证原子性。为此,Redis 2.6版本引入了执行Lua脚本的功能,通过Lua脚本可以保证原子性。Redission工具就是基于此原理提供的分布式锁工具。

如何设置过期时间,实现原理是什么?

redis有两种命令可以进行对key设置过期时间:expire和setex。这两种命令都可以用来给key设置过期时间。

实现过期时间的原理可以分为两个部分。

首先是主动删除。Redis会有一个定时任务,定期检查数据库中的key是否已经过期。如果发现某个key已经过期,那么Redis会直接将其删除。

其次是被动删除。当应用程序尝试获取一个已经设置了过期时间的key时,Redis会检查该key是否已经过期。如果已经过期,Redis会在返回结果之前将该key删除。

这样,通过主动删除和被动删除的组合,Redis实现了对key的过期时间的管理。这种混合实现的方式可以保证Redis中的数据始终是最新的,并且不会出现过期的数据。

需要注意的是,Redis并不会为每个key都启动一个单独的定时任务去检查过期时间。相反,Redis会根据实际情况动态调整定时任务的执行频率,以提高性能和效率。这种设计可以有效地减少对系统资源的占用,提高Redis的性能和稳定性。

海量数据下,如何快速查找一条记录?

当前这道题目考验的是对redis整体的理解,所以也要全方位考虑,可以考虑以下优化策略:

  • 使用布隆过滤器:布隆过滤器是一种概率型数据结构,可以用于判断某个元素是否存在于集合中。在海量数据下,可以先使用布隆过滤器将不存在的key过滤掉,这样可以减少部分请求,提高查询效率。

  • 合理选择存储结构:在缓存记录时,可以考虑使用适合的存储结构。如果存储的是大对象,使用key+value(json)形式,那么key可能会很大,不建议使用。而如果使用hash结构存储,可以充分利用Redis的哈希表特性,提高存储效率。此外,可以根据实际情况选择其他存储结构,如列表、有序集合等。

  • 查询优化:如果Redis是集群部署的,数据根据槽位进行分配。如果我们自己对key进行了定位,可以直接访问对应的Redis节点,而不需要通过集群路由。这样可以减少Redis集群的机器计算,提高查询性能。

本文提供了一些保证数据一致性和设计分布式锁的策略。这些策略可以在实际应用中帮助开发人员解决相关的问题,确保系统的数据一致性和并发访问的正确性。同时,通过合理地使用缓存和分布式锁,可以提高系统的性能和可靠性。希望对你在面对Redis相关面试题时有所帮助!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK