程序员子龙(Java面试 + Java学习) 程序员子龙(Java面试 + Java学习)
首页
学习指南
工具
开源项目
技术书籍

程序员子龙

Java 开发从业者
首页
学习指南
工具
开源项目
技术书籍
  • 基础

  • JVM

  • Spring

  • 并发编程

  • Mybatis

  • 网络编程

  • 数据库

  • 缓存

    • Redis

      • Redis基础知识
      • redis底层数据结构
      • 发布和订阅
      • 分分钟搞懂布隆过滤器,亿级数据过滤算法你值得拥有!
      • 缓存和数据库一致性解决方案
      • 详解redis的bitmap
      • 面试常问使用缓存出现的问题
        • 缓存和数据库一致性解决方案
        • 需求起因
        • 更新缓存
        • 缓存与数据库双写不一致
        • 缓存更新的设计模式
        • 缓存雪崩
        • 缓存穿透
        • 缓存击穿
      • Redis hot key 发现以及解决办法
      • Redis实现排行榜功能实战
      • Redis 管道技术——Pipeline
      • 11、RedisTemplate使用最详解(一)--- opsForValue()
      • RedisTemplate使用最详解(二)--- opsForList()
      • RedisTemplate使用最详解(三)--- opsForHash()
      • RedisTemplate使用最详解(四)--- opsForSet()
      • RedisTemplate使用最详解(五)--- opsForZSet()
      • Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)
      • 建议收藏!看完全面掌握,最详细的Redis总结
      • Redis分布式锁-这一篇就够了
      • 《进大厂系列》系列-Redis常见面试题
    • 本地缓存

  • 设计模式

  • 分布式

  • 高并发

  • SpringBoot

  • SpringCloudAlibaba

  • Nginx

  • 面试

  • 生产问题

  • 系统设计

  • 消息中间件

  • Java
  • 缓存
  • Redis
程序员子龙
2024-01-29
目录

面试常问使用缓存出现的问题

# 缓存和数据库一致性解决方案

# 需求起因

在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。

img

这个业务场景,主要是解决读数据从Redis缓存 (opens new window),一般都是按照下图的流程来进行业务操作。

img

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

# 更新缓存

当我们对数据进行修改的时候,到底是先删缓存,还是先写数据库?

1、先更新缓存,再更新 DB

这个方案一般不考虑。原因是更新缓存成功,更新数据库出现异常了, 导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。

2、先更新 DB,再更新缓存

这种方案会出现的问题:数据库更新成功了,缓存更新失败,同样会出现数据不一致问题

3、先删除缓存,后更新 DB

该方案也会出问题,具体出现的原因如下。

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;

此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到Redis 中;

但是此时请求 A 并没有更新成功,或者事务还未提交,请求 B 去数据库查询得到旧值;

img

4、先更新 DB,后删除缓存

这种方式,被称为 Cache Aside Pattern,读的时候,先读缓存,缓存没有的 话,就读数据库 (opens new window),然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。

最经典的缓存+数据库读写的模式

为什么是删除缓存,而不是更新缓存?

  • 懒加载

更新缓存成本大,但是缓存利用率低,比如:一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 (opens new window) 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。

  • 并发问题:

同时有请求 A 和请求 B 进行更新操作,那么会出现

(1)线程 A 更新了数据库

(2)线程 B 更新了数据库

(3)线程 B 更新了缓存

(4)线程 A 更新了缓存

img

这就出现请求 A 更新缓存应该比请求 B 更新缓存早才对,但是因为网络等原因,B 却比 A 更早更新了缓存。这就导致了脏数据,因此不考虑。

Cache Aside Pattern 的缺陷:

缺陷1:首次请求数据一定不在 cache 的问题

解决办法:可以将热点数据可以提前放入cache 中。

缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。

解决办法:

  • 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
  • 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。

缺陷3:数据不一致

理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多!

(1)缓存刚好失效

(2)请求 A 查询数据库,得一个旧值

(3)请求 B 将新值写入数据库

(4)请求 B 删除缓存

(5)请求 A 将查到的旧值写入缓存

解决方案:异步更新缓存(基于订阅binlog的同步机制)

技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

  • 读Redis:热数据基本都在Redis
  • 写MySQL:增删改都是操作MySQL
  • 更新Redis数据:订阅MySQL的binlog日志,来更新到Redis

1)把全量数据写入到缓存中

2)订阅binlog日志,推送到消息队列,消费端更新缓存

可以使用canal(阿里的一款开源框架),对MySQL的binlog进行订阅。

# 缓存与数据库双写不一致

# 缓存更新的设计模式

SoR(system-of-record):记录系统,或者可以叫做数据源,即实际存储原始数据的系统。

Cache:缓存,是SoR的快照数据,Cache的访问速度比SoR要快,放入Cache的目的是提升访问速度,减少回源到SoR的次数。

回源:即回到数据源头获取数据,Cache没有命中时,需要从SoR读取数据,这叫做回源。

1、Cache Aside Pattern 旁路缓存

失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功 后,放到缓存中。

命中:应用程序从 cache 中取数据,取到后返回。

更新:先把数据存到数据库中,成功后,再让缓存失效。

这种模式下,没有了删除 cache 数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来不会存在后续的查询操作一直都在取老的数据。

Cache-As-SoR

Cache-As-SoR即把Cache看作为SoR,所有操作都是对Cache进行,然后Cache再委托给SoR进行真实的读/写。即业务代码中只看到Cache的操作,看不到关于SoR相关的代码。有三种实现:read-through、write-through、write-behind。

  • Read-Through

Read-Through,业务代码首先调用Cache,如果Cache不命中由Cache回源到SoR,而不是业务代码(即由Cache读SoR)。使用Read-Through模式,需要配置一个CacheLoader组件用来回源到SoR加载源数据。

  • Write-Through

Write-Through,被称为穿透写模式/直写模式——业务代码首先调用Cache写(新增/修改)数据,然后由Cache负责写缓存和写SoR,而不是由业务代码。使用Write-Through模式需要配置一个CacheWriter组件用来回写SoR。

  • Write-Behind

Write-Behind,也叫Write-Back,我们称之为回写模式。不同于Write-Through是同步写SoR和Cache,Write-Behind是异步写。异步之后可以实现批量写、合并写、延时和限流。

# 缓存雪崩

缓存雪崩指的是在某一个时刻大流量怼到系统, 这时候系统出现了大量的 key同时失效, 这样导致了大量的请求到了数据库层, 导致数据库奔溃从而导致整个系统雪崩的现象。

解决方案:

预防和解决缓存雪崩问题,可以从以下三个方面进行着手。

1)保证缓存层服务高可用性。和飞机都有多个引擎一样,如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务, Redis Sentinel 和 Redis Cluster 都实现了高可用。

2)依赖隔离组件为后端限流并降级。无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可 用,可能会造成线程全部阻塞(hang)在这个资源上,造成整个系统不可用。降级机制在高并发系统中是非常普遍的。

3)提前演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

4)将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一 个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 缓存雪崩和缓存击穿的区别在于缓存击穿针对某一 key 缓存,缓存雪崩则是很多 key。

# 缓存穿透

缓存穿透更多的是一种恶意访问, 黑客故意大量访问一个 redis 里面没有, 数据库也没有的数据, 这样同样会导致大量请求落到数据库, 所以访问数据库加锁是必要的。 但是这里又有一个问题, 恶意访问会占用 redis 的连接资源, 所以这里需要使用拦截手段, 把请求拦截在 redis 之外, 比如用布隆过滤器, 比如用本地缓存都可以有效拦截对 redis 的恶意访问。

解决方案:

1、使用布隆过滤器提前拦截

2、如果是 redis 没有, 数据库也没有的情况, 可以把一个 null 字符串存储到 redis 并且存一份到本地缓存, 存本地缓存的目的也是为了减少对redis 的访问压力。

3、参数校验,对接口入参参数进行校验

# 缓存击穿

在某一个时刻大并发下请求某一个 key, 而这个 key 恰好在这个时候失效了, 这时候大量的请求会怼到数据库从而导致系统奔溃。

解决方案:

缓存要做预热, 且缓存的失效时间要大于业务生命周期时间, 比如一个秒杀业务, 1 小时内秒完, 那么这个 key 的失效时间要大于 1 小时。

请求数据库的逻辑需要加锁, 避免大量请求落到数据库层。 可能这个锁的逻辑块永远不会执行, 因为缓存是存在在 redis 的, 但是代码要有健壮性考虑。

使用互斥锁(mutex key)

业界比较常用的做法,是使用 mutex。简单地来说,就是在缓存失效的时候 (判断拿出来的值为空),不是立即去 load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key (opens new window),当操作返回成功时,再进行 load db 的操作并回设缓存;否则,就重试整个 get 缓存的方法。

伪代码:

       public String get(key) {
            String value = redis.get(key);
            if (value == null) { //代表缓存值过期
                //设置 3min 的超时,防止 del 操作失败的时候,下次缓存过期一直不能 load  db
                //代表设置成功
                if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
                    value = db.get(key);
                    redis.set(key, value, expire_secs);
                    redis.del(key_mutex);
                } else {
                    //这个时候代表同时候的其他线程已经 load db 并回设到缓存了,这时候重试获取缓存值即可
                    sleep(50);
                    get(key); //重试
                }
            } else {
                return value;
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

永远不过期

这里的“永远不过期”包含两层意思:

(1) 从 redis 上看,确实没有设置过期时间,这就保证了,不会出现热点key 过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期 从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

上次更新: 2024/03/11, 15:54:57
详解redis的bitmap
Redis hot key 发现以及解决办法

← 详解redis的bitmap Redis hot key 发现以及解决办法→

最近更新
01
一个注解,优雅的实现接口幂等性
11-17
02
MySQL事务(超详细!!!)
10-14
03
阿里二面:Kafka中如何保证消息的顺序性?这周被问到两次了
10-09
更多文章>
Theme by Vdoing | Copyright © 2024-2024

    辽ICP备2023001503号-2

  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式