ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

缓存的设计

2022-01-27 10:34:50  阅读:43  来源: 互联网

标签:缓存 数据库 更新 并发 线程 设计 数据


引子

面试时可能会被问到缓存设计相关的问题,如:

  1. 为什么你们系统需要用缓存?
  2. 使用缓存需要考虑哪些问题?
  3. 怎么保证数据库缓存一致性的?
  4. 缓存穿透?缓存击穿?缓存雪崩?

为什么你们系统需要用缓存?

高性能、高并发。

高性能

如果有这么个场景,一份数据需要聚合数据库的几个表的数据,并且需要还需要在代码中进行计算,可能获取这个数据的接口的性能就不会很高。每次获取都需要进行 Sql 查询和运算,是有很多重复计算的情况的,所以我们可以 空间换时间 ,将这个结果存起来,那么存到哪里呢,首选就是缓存了。
之后如果是获取相同结果,直接从缓存中获取,实现 高性能

高并发

数据库的并发一般都不会很高, Mysql 可能不到2000QPS就报警了。如果并发量很高,Mysql 肯定是扛不住的,所以引入缓存。高QPS获取的数据,可以理解为 热点数据,我们把 热点数据 存入 Redis,单节点 Redis 10W+并发应该无问题。
引入缓存也是对于 高并发 情况的一种应对方式。

使用缓存需要考虑哪些问题?

  • 数据不一致问题
  • 代码维护成本(解决上述不一致问题的努力)
  • 运维成本(维护缓存中间件的高可用)

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

这里我们需要分析自己的业务,是需要 强一致性 还是 最终一致性,可以区分使用不同的策略。

加入缓存后数据的读取姿势

  1. 先查缓存,缓存有数据,return
  2. 查询数据库,查询完数据之后,将数据回种到缓存

缓存的利用率

要想缓存利用率最大化,那就得让缓存中存储的尽量都是 热点数据

面试题:如何保证缓存中存储的是热点数据?

  1. 先查缓存,缓存有数据,return
  2. 查询数据库,查询完数据之后,将数据回种到缓存,并且增加超时时间

这样随着时间的推移,不常访问的数据会【淘汰掉】,剩下的是热点数据。
Redis 的淘汰策略 volatile-lru

先更新xx,后更新xx

在不考虑并发情况下,分析 2 种策略。

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

如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。
虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存「失效」,就会从数据库中读取到「旧值」,重建缓存也是这个旧值。
这时用户会发现自己之前修改的数据又「变回去」了,对业务造成影响。

2.先更新数据库,后更新缓存

如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。
之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。
这时用户会发现,自己刚刚修改了数据,但却看不到变更,一段时间过后,数据才变更过来,对业务也会有影响。

因为操作不具备 原子性,无论谁先谁后,都会出现不一致问题。

如果此时引入 并发,又会新增别的问题。

并发情况下

比如我们采用 【先更新数据库,后更新缓存】策略来推演 并发情况

设有 线程A 和 线程B 同时操作 【同一条数据】:

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

最终缓存中 X = 1,数据库中 X = 2,出现不一致问题。

无论使用哪种策略,并发情况 下,不一致问题更容易出现。

删除缓存

我们可以考虑不更新缓存,而是删除缓存。

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

如果有 2 个线程要并发「读写」数据,可能会发生以下场景:

  1. 线程 A 要更新 X = 2(原值 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。

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

依旧是 2 个线程并发「读写」数据:

  1. 缓存中 X 不存在(数据库 X = 1)
  2. 线程 A 读取数据库,得到旧值(X = 1)
  3. 线程 B 更新数据库(X = 2)
  4. 线程 B 删除缓存
  5. 线程 A 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。

其实概率「很低」,这是因为它必须满足 3 个条件:

缓存刚好已失效
读请求 + 写请求并发
更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)
仔细想一下,条件 3 发生的概率其实是非常低的。

因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。

这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

所以,我们应该采用这种方案,来操作数据库和缓存。

现在我们只要去保证 第二步操作【删除缓存】的成功性。

怎么保证两步操作都成功

重试

首先我们想到的简单办法,就是重试,重试去 【删除缓存】。
但是重试的话,需要考虑:

  • 重试次数 设置多大合适?
  • 重试会占用当前线程业务的资源
异步重试

我们一般考虑 异步重试 来解决,这里可以借用 MQ,将变更的需求生产到 消息队列,消费者消费消息,进行对应的【删除缓存】操作,保证最终的一致性。

还有一种方式,更新数据库之后不做任何操作。订阅数据库binlog,再删除缓存。

可使用 alibaba 开源的 canal

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

这里系统进一步解耦,但我们需要投入精力去维护 canal 的高可用和稳定性

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

主从库延迟

如果主从库延迟较大,即使我们 更新了数据库 并且 删除了缓存,若从库未能同步到主库的更新,这时候读从库数据回种到缓存,导致缓存是旧值,还是出现了不一致问题。

这里我们需要引入:缓存延迟双删策略

缓存延迟双删策略

比如说 可以生成一条【延时消息】,写到消息队列中,消费者延时【删除】缓存。
但是 延时 多久,这个我们很难去评估。很多时候是凭借经验去设置,比如1-5s,只能说是尽可能去保证一致性。极端情况下还是会出现不一致的情况。

没有银弹。

一定要强一致性吗?

考虑一致性问题的时候,我们需要考量,我们一定需要强一致性吗?

引入缓存的初衷,我们不就是为了 高性能高并发 吗?

也有一些方案是可以做 强一致性 的,比如说引入 分布式锁,更新数据库和更新缓存都成功之后才可访问此数据。

总结

  1. 引入缓存,提升系统 性能并发
  2. 考虑 一致性问题,最佳姿势 【更新数据库 + 删除缓存】
  3. 保证操作都成功,配合 消息队列订阅数据库变更日志
  4. 数据库【读写分离+主从库延迟】,借助 消息队列,考虑 延迟双删

标签:缓存,数据库,更新,并发,线程,设计,数据
来源: https://blog.csdn.net/qq_37421862/article/details/122696556

专注分享技术,共同学习,共同进步。侵权联系[admin#icode9.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有