redis面试题(二)附答案

news2024/11/16 12:51:00

书接上回,接着分享面试题,最近开发了几个小伙伴的项目,耽误更新了,来点干货,表示歉意。大家有需求也可以找小编。

2、缓存穿击

业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据【当前key是一个热点key**

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。直接访问数据库,在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题

图片

  • 解决方案

1、 互斥锁方案

保证同一时间只允许一个线程重建缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值【set(key,value,timeout)】。

图片

2、 永不过期

不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间

总结对比:

分布式互斥锁:这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。

永远不过期:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。

3、缓存雪崩

通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

那么,当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

  • 图片

图片

  • 可以看到,发生缓存雪崩有两个原因:

    • 大量数据同时过期;
    • Redis 故障宕机;
  • 解决方案

1、大量数据同时过期

针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  • 均匀设置过期时间;
  • 互斥锁;
  • 双 key 策略;
  • 后台更新缓存;

1. 均匀设置过期时间

如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。

2. 互斥锁

当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

3. 双 key 策略

我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。

当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。

4. 后台更新缓存

业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新

事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。

解决上面的问题的方式有两种。

第一种方式,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。

这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。

第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。

在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。

2、Redis 故障宕机

针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:

  • 服务熔断或请求限流机制;
  • 构建 Redis 缓存高可靠集群;

1. 服务熔断或请求限流机制

因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。

服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作

为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。

2. 构建 Redis 缓存高可靠集群

服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过主从节点的方式构建 Redis 缓存高可靠集群

如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

图片

十、Redis的集群

1、主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

作用:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从库之间采用的是读写分离的方式。

  • 读操作:主库、从库都可以接收;
  • 写操作:首先到主库执行,然后,主库将写操作同步给从库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHYRb8cv-1682503842298)(data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)]’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

  • 全量(同步)复制:比如第一次同步时
  • 增量(同步)复制:只会把主从库网络断连期间主库收到的命令,同步给从库

第一阶段:主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。

具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

第二阶段:主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。

具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。

第三个阶段:主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

Redis 为什么主从全量复制使用RDB而不使用AOF?

1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量复制的成本最低。

2、假设要使用AOF做全量复制,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。

Redis 为什么还会有从库的从库的设计?

一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件

如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量复制。fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。那么,有没有好的解决方法可以分担主库压力呢?

其实是有的,这就是“主 - 从 - 从”模式。

在刚才介绍的主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,我们可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上

简单来说,我们在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。然后,我们可以再选择一些从库(例如三分之一的从库),在这些从库上执行如下命令,让它们和刚才所选的从库,建立起主从关系。

replicaof 所选从库的IP 6379

这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LYfIP7xp-1682503842298)(data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)]’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

2、Redis哨兵机制

哨兵的作用:

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

十一、Redis实现分布式锁

方式一:可能会导致锁一直没有释放

//加锁:如果键存在则不操作,否则操作
Boolean setIfAbsent(K, V );
setnx(K,V);

//释放锁:
Boolean delete(K);
del(K);

方式二:可能出现上完锁后,服务器异常,不能设置过期时间(redis的命令不是原子操作)

//加锁:如果键存在则不操作,否则操作
Boolean setIfAbsent(K, V );
setnx(K,V);

//设置过期时间
Boolean expire(K key, final long timeout, final TimeUnit unit)
expire(K,Time);//单位:毫秒

//释放锁:
Boolean delete(K);
del(K);

方式四:上锁同时设置过期时间;可能会发生锁误删除。

//加锁并设置过期时间
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
setex(K,Time,V);//单位:毫秒

//释放锁:
Boolean delete(K);
del(K);

方式五:上锁同时设置过期时间;将值设置为uuid,在进行释放锁时,先判断当前的uuid和要释放锁的uuid,若一致进行释放,否则不释放。

//1.进入方案
String requestId = IdWorker.getIdStr();
Boolean ret = redisTemplate.opsForValue().setIfAbsent( "key",requestId,5L,TimeUnit.SECONDS);
//2.判断为true 表示加锁成功,false标识锁已经存在
if(ret){
    //3.执行业务

    //4.执行结束
    Object o = redisTemplate.opsForValue().get(key);
    if(o !=null){
        if(requestId.equals((String)o)){
            redisTemplate.delete(key);
        }
    }
}else{
    //5.做异常容错处理
    Long expire = redisTemplate.opsForValue().getOperations().getExpire(key);
    if(expire !=null && expire.intValue()<0){
        redisTemplate.delete(key);
    }
    //6.日志打印该模块未执行任务!
    log.info("模块:"+MODULE+",定时任务已在执行!");
}

十二、缓存和数据库双写不一致问题

1、缓存和数据库一致性定义

  • 缓存中有数据,且和数据库数据一致;
  • 缓存中无数据,数据库数据是最新的。

那么不符合这两种情况就属于缓存和数据库不一致的问题了。

当客户端发送一个数据修改的请求,我们不仅要修改数据库,还要一并操作(修改/删除)缓存。对数据库和缓存的操作又存在一个顺序的问题:到底是先操作数据库还是先操作缓存

下面我们以客户端向 MySQL 中删改数据为例来分析数据不一致的情况。

先不考虑并发问题,正常情况下,无论谁先谁后,都可以让两者保持一致,但现在我们需要重点考虑异常情况。

此时应用既要修改数据库也要修改缓存(删除和修改的影响是类似的,方便起见,下面只描述修改操作)的数据。

这里有两种场景我们分别来看下:

  • 先修改缓存,再修改数据库
  • 先修改数据库,再修改缓存

我们假设应用先修改缓存,再修改数据库。如果缓存修改成功,但是数据库操作失败,那么,应用再访问数据时,缓存中的值是正确的,但是一旦缓存中「数据失效」或者「缓存宕机」,然后,应用再访问数据库,此时数据库中的值为旧值,应用就访问到旧值了。

如果我们先更新数据库,再更新缓存中的值,是不是就可以解决这个问题呢?我们继续分析。

如果应用先完成了数据库的更新,但是,在更新缓存时失败了。那么,数据库中的值是新值,而缓存中的是旧值,这肯定是不一致的。

这个时候,如果有其他的并发请求来访问数据,按照正常的访问流程,就会先在缓存中查询,此时,就会读到旧值了。

好了,到这里,我们可以看到,在操作数据库和更新缓存值的过程中,无论这两个操作的执行顺序谁先谁后,只要「第二步的操作」失败了,就会导致客户端读取到旧值。

我们继续分析,除了「第二步操作失败」的问题,还有什么场景会影响数据一致性:并发问题

2、并发引发的一致性问题:

  • 先更新数据库,后更新缓存

假设我们采用「先更新数据库,后更新缓存」的方案,并且在两步都可以成功执行的前提下,如果存在并发,情况会是怎样的呢?

有线程 A 和线程 B 两个线程,需要更新「同一条」数据 x,可能会发生这样的场景:

  1. 线程 A 更新数据库(x = 1)
  2. 线程 B 更新数据库(x = 2)
  3. 线程 B 更新缓存(x = 2)
  4. 线程 A 更新缓存(x = 1)

最后我们发现,数据库中的 x 是2,而缓存中是1。显然是不一致的。

另外这种场景一般是不推荐使用的。因为某些业务因素,最后写到缓存中的值并不是和数据库是一致的,可能需要一系列计算得出的,最后才把这个值写到缓存中;如果此时有大量的对数据库进行写数据的请求,但是读请求并不多,那么此时如果每次写请求都更新一下缓存,那么性能损耗是非常大的。

比如现在数据库中 x = 1,此时我们有 10 个请求对其每次加一的操作。但是这期间并没有读操作进来,如果用了先更新数据库的办法,那么此时就会有10个请求对缓存进行更新,会有大量的冷数据产生。

至于「先更新缓存,后更新数据库」这种情况和上述问题的是一致的,我们就不在继续讨论。

不管是先修改缓存还是后修改缓存,这样不仅对缓存的利用率不高,还浪费了机器性能。所以此时我们需要考虑另外一种方案:删除缓存

  • 先删除缓存,后更新数据库

假设有两个线程:线程A(更新 x ),线程B(读取 x )。可能会发生如下场景:

  1. 线程 A 先删除缓存中的 x ,然后去数据库进行更新操作;
  2. 线程 B 此时来读取 x,发现数据不在缓存,查询数据库并补录到缓存中;
  3. 而此时线程 A 的事务还未提交。

图片
这个时候「先删除缓存,后更新数据库」仍会产生数据库与缓存的不一致问题。

  • 先更新数据库,后删除缓存

我们还用两个线程:线程 A(更新 x ),线程B(读取 x )举例。

  1. 线程 A 要把数据 x 的值从 1更新为 2,首先先成功更新了数据库;
  2. 线程 B 需要读取 x 的值,但线程 A 还没有把新的值更新到缓存中;
  3. 这个时候线程 B 读到的还是旧数据 1;

图片
不过,这种情况发生的概率很小,线程 A 会很快删除缓存中值。这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。

由此,我们可以采用这种方案,来尽量避免数据库和缓存在并发情况下的一致性问题。

下面,我们继续分析「第二步操作失败」,我们该如何处理?

3、如何保证双写一致性

如何保证「第二步操作失败」的双写一致?

前面我们分析到,无论是「更新缓存」还是「删除缓存」,只要第二步发生失败,那么就会导致数据库和缓存不一致。

这里的关键在于如何保证第二步执行成功

首先,介绍一种方法:「基于消息队列的重试机制」。

基于消息队列的重试机制

具体来说,就是把操作缓存,或者操作数据库的请求暂存到队列中。通过消费队列来重新处理这些请求。

流程如下:

  1. 请求 A 先对数据库进行更新操作;
  2. 在对 Redis 进行删除操作的时候发现删除失败;
  3. 此时将 对 Redis 的删除操作 作为消息体发送到消息队列中;
  4. 系统接收到消息队列发送的消息,再次对 Redis 进行删除操作。

图片消息队列的两个特性满足了我们重试的需求:

  • 保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心);
  • 保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)。

引入队列带来的问题:

  • 业务代码造成大量的侵入,同时增加了维护成本;
  • 写队列时也会存在失败的问题。

对于这两个问题,第一个,我们在项目中一般都会用到消息队列,维护成本并没有新增很多。而且对于同时写队列和缓存都失败的概率还是很小的。

如果是实在不想在应用中使用队列重试的,目前也有比较流行的解决方案:订阅数据库变更日志,再操作缓存。我们对 MySQL 数据库进行更新操作后,在 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 MySQL 数据库的 binlog 日志对缓存进行操作。

订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal

大概流程如下:

  1. 系统修改数据库,生成 binlog 日志;
  2. canal 订阅这个日志,获取具体的操作数据,投递给消息队列;
  3. 通过消息队列,删除缓存中的数据。

总结:推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来保证数据库和缓存一致性。

4、如何保证并发场景下的数据一致性

我们前面分析,在并发场景下,「先删除缓存,再更新数据库」,由于存在网络延迟等,可能会存在数据不一致问题。我再把上面的图贴过来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4xZgD27B-1682503842299)(data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)]’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
这个问题的核心在于:缓存都被种了「旧值」。

解决这种问题,最有效的办法就是,把缓存删掉。但是,不能立即删,而是需要「延迟删」,这就是业界给出的方案:缓存延迟双删策略

图片
在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。

之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。

但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢?
线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。这需要在实际业务运行时估算。

除此之外,其实还有一种场景也会出现不一致问题:如果数据库采用读写分离架构,主从同步之间也会有时间差,也可能会导致不一致:

  1. 线程 A 更新主库 x = 2(原值 x = 1);
  2. 线程 A 删除缓存;
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 x = 1);
  4. 从库「同步」完成(主从库 x = 2);
  5. 线程 B 将「旧值」写入缓存(x = 1)。

最终缓存中的 x 是旧值 1,而主从库最终值是新值 2。发生了数据不一致问题。

针对该问题的解决办法是,对于线程 B 的这种查询操作,可以强制将其指向主库进行查询,也可以使用上述「延迟删除」策略解决。

采用这种方案,也只是尽可能保证一致性而已,极端情况下,还是有可能发生不一致。

所以实际使用中,还是建议采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。

5、总结

图片

  1. 系统引入缓存提高应用性能问题
  2. 引入缓存后,需要考虑缓存和数据库双写一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」
  3. 不管哪种方案,只要第二步操作失败,都无法保证数据的一致性,针对这类问题,可以通过消息队列重试解决
  4. 「更新数据库 + 更新缓存」方案,在「并发」场景下无法保证缓存和数据一致性,且存在「缓存资源浪费」和「机器性能浪费」的情况发生,一般不建议使用
  5. 在「更新数据库 + 删除缓存」的方案中,「先删除缓存,再更新数据库」在「并发」场景下依旧有数据不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估,所以推荐用「先更新数据库,再删除缓存」的方案
  6. 在「先更新数据库,再删除缓存」方案下,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据一致性
  7. 在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「强制读主库」或者「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/465064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

好程序员:前端JavaScript全解析——Canvas绘制形状(上)

●今天&#xff0c;我们来通过 canvas 提供的方法开绘制一些简单的形状绘制矩形 绘制基础矩形。下面一起看看好程序员老师的讲解吧~ ●语法 : 工具箱.rect( 矩形起点 x 轴坐标, 矩形起点 y 轴坐标, 矩形宽度, 矩形高度 ) // 0. 获取到页面上的 canvas 标签元素节点 const canva…

Camtasia2023简体中文标准版免费更新下载

Camtasia专业的 屏幕录制和视频剪辑软件3000多万专业人士在全球范围内使用Camtasia展示产品&#xff0c;教授课程&#xff0c;培训他人&#xff0c;以更快的速度和更吸引人的方式进行沟通和屏幕分享。使您在Windows和Mac上进行录屏和剪辑创作专业外观的视频变得更为简单。 Camt…

Vue3 element-plus el-select 无法选中,又不报错

html 结构 <el-form :model"conditionForm"ref"conditionForm"label-width"100px" class"demo-ruleForm"><el-selectv-model"conditionForm.personnel"multipleplaceholder"Select"style"width: 2…

知网导入EndNote

首先进入知网&#xff0c;搜索你想要找的期刊论文。 选择EndNote 点击导出 浏览器自动下载以txt为后缀的文件 导入到EndNote中

【C++】异常,你了解了吗?

在之前的C语言处理错误时&#xff0c;会通过assert和错误码的方式来解决&#xff0c;这导致了发生错误就会直接把程序关闭&#xff0c;或者当调用链较长时&#xff0c;就会一层一层的去确定错误码&#xff0c;降低效率&#xff0c;所以c针对处理错误&#xff0c;出现了异常&…

ChatGPT写小论文

ChatGPT写小论文 只是个人对写小论文心得?从知乎,知网自己总结的,有问题,可以留个言我改一下 文章目录 ChatGPT写小论文-1.写论文模仿实战(狗头)0.论文组成1.好论文前提:2.标题3.摘要4.关键词5.概述6.实验数据、公式或者设计7.结论&#xff0c;思考8.参考文献 0.模仿1.喂大纲…

【云原生】Dockerfile制作WordPress镜像,实现Compose + K8s编排部署

文章目录 &#x1f479; 关于作者前言环境准备目录结构 dockerfile制作镜像yum 脚本Dockerfile-mariadb 镜像Dockerfile-service 镜像docker compose 编排 K8s部署svcdeploy ✊ 最后 &#x1f479; 关于作者 大家好&#xff0c;我是秋意零。 &#x1f608; CSDN作者主页 &…

lambda的toMap是不是要注意点,线上事故

异常回顾 先看代码&#xff1a; dbTaxiDrivers.ifPresent((drivers) -> { map.putAll(drivers.stream() .collect(Collectors.toMap(TaxiDriverInfo::getOperationId, item -> item))); }); 相信很多为了减少2层for循环&#xff0c…

✨✨✨ ❃ ♕ ꕥ Xpath解析html获取表情符号,丰富你的文章 ꧁ ꧂꧁ ꧂

✨✨✨ ❃ ♕ ꕥ Xpath解析html获取表情符号&#xff0c;丰富你的文章 ꧁ ꧂꧁ ꧂ 1. 推荐几个好玩的表情符号网站2. xpath解析html获取表情3. xpath解析html源码3.1 parse_li.py3.2 symbol2.html 参考 1. 推荐几个好玩的表情符号网站 &#x1f495; &#x1f9da; &#x1f6…

让科幻照进现实,未来出行革命

在“新四化”&#xff08;电动化、网联化、智能化、共享化&#xff09;和“碳中和、碳达峰”双碳目标下&#xff0c;中国汽车产业正经历着前所未有之大变局。作为“智能化”核心之一的自动驾驶&#xff0c;在监管、技术和商业化方面持续积累、不断完善&#xff0c;即将迈入发展…

案例1:Java超市管理系统设计与实现开题报告

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

浏览器【控制台】的小妙招-dom复制

了解前端开发的朋友应该对浏览器的控制台非常熟悉&#xff0c;毕竟日常里除了wife就是跟浏览器相处的最久了。 1、唤出控制台 打开一个网页&#xff1a; 按下键盘【F12】或者鼠标在网页任意位置【右键- 检查】&#xff0c;即可唤出浏览器的【控制台】 2、日常开发使用之【复制…

QuickTime Player + BlackHole解决Mac不能录内部声音

背景 在用mac录制屏幕时&#xff0c;发现不能录入电脑内的声音。 App Store中有一些收费的屏幕录制软件&#xff08;也需要安装特定的虚拟声卡&#xff09;。 现在实现完全免费的屏幕录制&#xff0c;QuickTime Player BlackHole组合&#xff0c;QuickTime Player是mac自带…

Android 12系统源码_CarService(一)CarService的基本架构

前言 1、Google官网上是这样介绍汽车架构的。 Car App&#xff1a;包括OEM和第三方开发的AppCar API&#xff1a;内有包含 CarSensorManager 在内的 API。位于 /platform/packages/services/Car/car-lib。CarService&#xff1a;系统中与车相关的服务&#xff0c;位于 /platfo…

报错记录:构造方法获取不了@Value的值,问题刨析与解决方案

报错记录&#xff1a;构造方法获取不了Value的值&#xff0c;问题刨析与解决方案 有时我们需要在构造函数中初始化属性&#xff0c;之前老的项目是用的I/O流来获取配置文件的值&#xff0c;目前配置转为线上&#xff0c;使用Apollo来获取值&#xff0c;由于获取Apollo的值被封装…

浅述 国产仪器 1761程控模块电源

1761程控模块电源是在自动测试环境中提供偏置功率和对部件或最终产品提供激励的理想设备&#xff0c;是测试系统必备的测试仪器。适用于研发、设计、生产制造等自动测试领域。 1761程控模块电源为用户选配电源提供了灵活性&#xff0c;根据需要可选购1&#xff5e;8种&#xff…

6.登录token

登录时生产token和refreshtoken&#xff0c;请求时带上token&#xff0c;后台校验&#xff0c;通过的话则进行处理&#xff0c;否则返回错误信息&#xff08;token失效过期等等&#xff09;&#xff0c;校验不通过会调用刷新token的接口&#xff0c;重新获取token&#xff0c;如…

< 封装公共导出模块:配合element实现提示 >

封装公共导出模块 &#x1f449; 前言&#x1f449; 一、原理&#x1f449; 二、实现案例&#x1f449; 三、效果演示往期内容 &#x1f4a8; &#x1f449; 前言 在 Vue elementUi 开发中&#xff0c;我们偶尔会遇到需要导出的列表&#xff0c;或者指定位置的导出内容。在一…

神经网络基础-手写数字识别

手写数字识别神经网络 基本原理 图像本质上被认为是一个矩阵&#xff0c;每个像素点都是一个对应的像素值&#xff0c;相当于在多维数据上进行相关的归类或者其他操作。 线性函数 线性函数的一个从输入到输出的映射&#xff0c;用于给目标一个每个类别对应的得分。 图像 ( …

leetcode刷题(9)二叉树(3)

各位朋友们&#xff0c;提前祝大家五一劳动节快乐啊&#xff01;&#xff01;&#xff01;今天我为大家分享的是关于leetcode刷题二叉树相关的第三篇我文章&#xff0c;让我们一起来看看吧。 文章目录 1.二叉树的层序遍历题目要求做题思路代码实现 2.从前序与中序遍历序列构造二…