ICode9

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

缓存的设计

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

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


引子

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

  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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

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

ICode9版权所有