当前位置: 首页 > news >正文

《Redis》缓存与分布式锁

文章目录

  • 缓存概念
  • 缓存的更新策略
    • 1.定期生成
    • 2.实时生成
  • 缓存预热,穿透,雪崩,击穿
    • 缓存预热
    • 缓存穿透
    • 缓存雪崩
    • 缓存击穿
  • Redis典型应用——分布式锁


缓存概念

举个例⼦:
⽐如我需要去⾼铁站坐⾼铁. 我们知道坐⾼铁是需要反复刷⾝份证的 (进⼊⾼铁站, 检票, 上⻋,
乘⻋过程中, 出站…).
正常来说, 我的⾝份证是放在⽪箱⾥的(⽪箱的存储空间⼤, ⾜够能装). 但是每次刷⾝份证都需
要开⼀次⽪箱找⾝份证, 就⾮常不⽅便.
因此我就可以把⾝份证先放到⾐服⼝袋⾥. ⼝袋虽然空间⼩, 但是访问速度⽐⽪箱快很多.
这样的话每次刷⾝份证我只需要从⼝袋⾥掏⾝份证就⾏了, 就不必开⽪箱了.
此时 “⼝袋” 就是 “⽪箱” 的缓存. 使⽤缓存能够⼤⼤提⾼访问效率.

缓存的更新策略

1.定期生成

每隔⼀定的周期(⽐如⼀天/⼀周/⼀个⽉), 对于访问的数据频次进⾏统计. 挑选出访问频次最⾼的前 N%
的数据.

🥇 以搜索引擎为例. ⽤⼾在搜索引擎中会输⼊⼀个 “查询词”, 有些词是属于⾼频的, ⼤家都爱搜(鲜花, 蛋糕, 同城交友,不孕不育…). 有些词就属于低频的, ⼤家很少搜. 搜索引擎的服务器会把哪个⽤⼾什么时间搜了啥词, 都通过⽇志的⽅式记录的明明⽩⽩.
然后 每隔⼀段时间对这期间的搜索结果进⾏统计 (⽇志的数量可能⾮常巨⼤, 这个统计的过程可能 需要使⽤ hadoop 或者 spark等⽅式完成). 从⽽就可以得到 “⾼频词表” .

这种做法实时性较低. 对于⼀些突然情况应对的并不好.

⽐如春节期间, “春晚” 这样的词就会成为⾮常⾼频的词. ⽽平时则很少会有⼈搜索 “春晚”

在这里插入图片描述

2.实时生成

先给缓存设定容量上限(可以通过 Redis 配置⽂件的 maxmemory 参数设定).
接下来把⽤⼾每次查询:
• 如果在 Redis 查到了, 就直接返回.
• 如果 Redis 中不存在, 就从数据库查, 把查到的结果同时也写⼊ Redis.
如果缓存已经满了(达到上限), 就触发缓存淘汰策略, 把⼀些 “相对不那么热⻔” 的数据淘汰掉.
按照上述过程, 持续⼀段时间之后 Redis 内部的数据⾃然就是 “热⻔数据” 了

通用的内存淘汰策略有几种:

  • 1)FIFO:把缓存中存在时间最久的 (也就是先来的数据) 淘汰掉
  • 2)LRU (Least Recently Used) 淘汰最久未使⽤的
  • 3)LFU (Least Frequently Used) 淘汰访问次数最少的
    记录每个 key 最近⼀段时间的访问次数. 把访问次数最少的淘汰掉
  • 4)Random 随机淘汰
    从所有的 key 中抽取幸运⼉被随机淘汰掉。

理解上述⼏种淘汰策略:
想象你是个皇帝, 有后宫佳丽三千. 虽然你是 “真⻰天⼦”, 但是经常宠幸的妃⼦也就那么寥寥数
⼈(精⼒有限).
后宫佳丽三千, 相当于数据库中的全量数据. 经常宠幸的妃⼦相当于热点数据, 是放在缓存中
的.
今年选秀的⼀批新的⼩主, 其中有⼀个被你看上了. 宠信新⼈, ⾃然就需要有旧⼈被冷落. 到底
谁是要被冷落的⼈呢?
• FIFO: 皇后是最先受宠的. 现在已经年⽼⾊衰了. 皇后失宠.
• LRU: 统计最近宠幸时间. 皇后(⼀周前), 熹妃(昨天), 安答应(两周前), 华妃(⼀个⽉前). 华妃
失宠.
• LFU: 统计最近⼀个⽉的宠幸次数, 皇后(3次), 熹妃(15次), 安答应(1次), 华妃(10次). 安答应
失宠.
• Random: 随机挑⼀个妃⼦失宠.

这⾥的淘汰策略, 我们可以⾃⼰实现. 当然 Redis 也提供了内置的淘汰策略, 也可以供我们直接使⽤.

在这里插入图片描述
整体来说 Redis 提供的策略和我们上述介绍的通⽤策略是基本⼀致的. 只不过 Redis 这⾥会针对 “过期
key” 和 “全部 key” 做分别处理。

缓存预热,穿透,雪崩,击穿

缓存预热

使⽤ Redis 作为 MySQL 的缓存的时候, 当 Redis 刚刚启动, 或者 Redis ⼤批 key 失效之后, 此时由于
Redis ⾃⾝相当于是空着的, 没啥缓存数据, 那么 MySQL 就可能直接被访问到, 从⽽造成较⼤的压⼒.
因此就需要提前把热点数据准备好, 直接写⼊到 Redis 中. 使 Redis 可以尽快为 MySQL 撑起保护伞.

然后使用缓存更新策略,随着时间的推移,旧的热点数据逐渐被替代成新的热点数据,就能帮助MySQL减轻很大的负担。

缓存穿透

访问的 key 在 Redis 和 数据库中都不存在. 此时这样的 key 不会被放到缓存上, 后续如果仍然在访问该key, 依然会访问到数据库.这就会导致数据库承担的请求太多, 压⼒很⼤.
这种情况称为缓存穿透。

为何会产生这种情况?

原因可能有⼏种:
• 业务设计不合理. ⽐如缺少必要的参数校验环节, 导致⾮法的 key 也被进⾏查询了.
• 开发/运维误操作. 不⼩⼼把部分数据从数据库上误删了.
• ⿊客恶意攻击.

如何解决?

针对要查询的参数进⾏严格的合法性校验. ⽐如要查询的 key 是⽤⼾的⼿机号, 那么就需要校验当前
key 是否满⾜⼀个合法的⼿机号的格式.
• 针对数据库上也不存在的 key , 也存储到 Redis 中, ⽐如 value 就随便设成⼀个 “”. 避免后续频繁访
问数据库.(降低问题的严重性)
• 使⽤布隆过滤器先判定 key 是否存在, 再真正查询。
虽然布隆过滤器存在误判,但是误判的概率比较小,就算误判了,也还有上面几种补救措施。

经过上面几层防护,非法的key的请求到达MySQL进行查询的次数就会大大减少。

缓存雪崩

短时间内⼤量的 key 在缓存上失效, 导致数据库压⼒骤增, 甚⾄直接宕机。
本来 Redis 是 MySQL 的⼀个护盾, 帮 MySQL 抵挡了很多外部的压⼒. ⼀旦护盾突然失效了, MySQL
⾃⾝承担的压⼒骤增, 就可能直接崩溃.

为什么会产生缓存雪崩呢?

⼤规模 key 失效, 可能性主要有两种:
• Redis 挂了.
• Redis 上的⼤量的 key 同时过期.
为啥会出现⼤量的 key 同时过期?
这种和可能是短时间内在 Redis 上缓存了⼤量的 key, 并且设定了相同的过期时间
(给Redis作为缓存的时候,有时候为了考虑时效性,就会设置过期时间,这和Redis内存淘汰机制是配合使用的)

解决办法:
部署⾼可⽤的 Redis 集群, 并且完善监控报警体系.
不给 key 设置过期时间 或者 设置过期时间的时候添加随机时间因⼦.

缓存击穿

相当于缓存雪崩的特殊情况. 针对热点 key , 突然过期了, 导致⼤量的请求直接访问到数据库上, 甚⾄引
起数据库宕机.

如何解决?
• 基于统计的⽅式发现热点 key, 并设置永不过期.
• 进⾏必要的服务降级. 例如访问数据库的时候使⽤分布式锁, 限制同时请求数据库的并发数.

Redis典型应用——分布式锁

什么是分布式锁?
在⼀个分布式的系统中, 也会涉及到多个节点访问同⼀个公共资源的情况. 此时就需要通过 锁 来做互斥控制, 避免出现类似于 “线程安全” 的问题.
⽽ java 的 synchronized 或者 C++ 的 std::mutex, 这样的锁都是只能在当前进程中⽣效, 在分布式的这
种多个进程多个主机的场景下就⽆能为⼒了.
此时就需要使⽤到分布式锁.

本质上就是使⽤⼀个公共的服务器, 来记录 加锁状态.
这个公共的服务器可以是 Redis, 也可以是其他组件(⽐如 MySQL 或者 ZooKeeper 等), 还可以
是我们⾃⼰写的⼀个服务.

在这里插入图片描述

分布式锁的实现方式:
在这里插入图片描述
Redis 中提供了 set nx 操作, 正好适合这个场景. 即: key 不存在就设置, 存在则直接失败.


引入过期时间

当 服务器1 加锁之后, 开始处理买票的过程中, 如果 服务器1 意外宕机了, 就会导致解锁操作 (删除该
key) 不能执⾏. 就可能引起其他服务器始终⽆法获取到锁的情况.
为了解决这个问题, 可以在设置 key 的同时引⼊过期时间. 即这个锁最多持有多久, 就应该被释放.

ps:对于进程内的锁来说(这里假设C++的实现方式),可以将释放锁的操作用一个智能指针管理,析构的时候就会自动释放锁了。(RAII)

Redis则支持 set ex nx,在设置锁的同时把过期时间设置了。

注意! 此处的过期时间只能使⽤⼀个命令的⽅式设置. 如果分开多个操作, ⽐如
set nx
expire
由于 Redis 的多个指令之间不存在关 联, 并且即使使⽤了事务也不能保证这两个操作都⼀定成功, 因此就可能出现 setnx 成功, 但是expire 失败的情况. 此时仍然会出现⽆法正确释放锁的问题。
所以务必要用set ex nx的方式来操作!!!


引入校验ID

对于 Redis 中写⼊的加锁键值对, 其他的节点也是可以删除的.
⽐如 服务器1 写⼊⼀个 “001”: 1 这样的键值对, 服务器2 是完全可以把 “001” 给删除掉的.
当然, 服务器2 不会进⾏这样的 “恶意删除” 操作, 不过不能保证因为⼀些 bug 导致 服务器2 把锁误删除.
为了解决上述问题, 我们可以引⼊⼀个校验 id.
⽐如可以把设置的键值对的值, 不再是简单的设为⼀个 1, ⽽是设成服务器的编号. 形如 “001”: “服务器1”.
这样就可以在删除 key (解锁)的时候, 先校验当前删除 key 的服务器是否是当初加锁的服务器, 如果是,
才能真正删除; 不是, 则不能删除.

上面的逻辑用伪代码的描述如下:

String key = [要加锁的资源 id];
String serverId = [服务器的编号];
// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");
// 执⾏各种业务逻辑, ⽐如修改数据库数据. 
doSomeThing();
// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配. 
if (redis.get(key) == serverId) {redis.del(key);
}

引入lua脚本

但是很明显, 上面的解锁逻辑是两步操作 “get” 和 “del”, 这样做并不是原子的,就容易出问题。

一个服务器内部,是有很多线程的,这就有可能同时存在两个以上的线程都在执行get,del操作。

在这里插入图片描述

Redis使用事务能解决上述问题,虽然Redis弱事务,但是能够避免插队。
但实际中更广泛使用的解决方案是lua脚本。

Lua 的语法类似于 JS, 是⼀个动态弱类型的语⾔. Lua 的解释器⼀般使⽤ C 语⾔实现. Lua 语法
简单精炼, 执⾏速度快, 解释器也⽐较轻量(Lua 解释器的可执⾏程序体积只有 200KB 左右).
因此 Lua 经常作为其他程序内部嵌⼊的脚本语⾔. Redis 本⾝就⽀持 Lua 作为内嵌脚本.
很多程序都⽀持内嵌脚本, ⽐如 MySQL 8 ⽀持 JS 作为内嵌脚本, ⽐如 Vim ⽀持 VimScript
和 Python 作为内嵌脚本… 通过内嵌脚本来实现更复杂的功能, 提供更强的扩展性.
Lua 除了和 Redis 搭伙之外, 在很多场景也会作为内嵌脚本. ⽐如在游戏开发领域常常作为
编写逻辑的语⾔. (⽐如魔兽世界, ⼤话西游等
在这里插入图片描述
redis执行lua脚本的过程,就是原子的,相当于一条命令一样(不过lua中可以写多条命令)。


引入watch dog(看门狗)解决key的过期问题。

上述⽅案仍然存在⼀个重要问题. 当我们设置了 key 过期时间之后 (⽐如 10s), 仍然存在⼀定的可能性,
当任务还没执⾏完, key 就先过期了. 这就导致锁提前失效.
把这个过期时间设置的⾜够⻓, ⽐如 30s, 是否能解决这个问题呢? 很明显, 设置多⻓时间合适, 是⽆⽌
境的. 即使设置再⻓, 也不能完全保证就没有提前失效的情况.
⽽且如果设置的太⻓了, 万⼀对应的服务器挂了, 此时其他服务器也不能及时的获取到锁.
因此相⽐于设置⼀个固定的⻓时间, 不如动态的调整时间更合适.
所谓 watch dog, 本质上是加锁的服务器上的⼀个单独的线程, 通过这个线程来对锁过期时间进⾏ “续
约”.
注意, 这个线程是业务服务器上的, 不是 Redis 服务器的

举个具体的例⼦:
初始情况下设置过期时间为 10s. 同时设定看⻔狗线程每隔 3s 检测⼀次.
那么当 3s 时间到的时候, 看⻔狗就会判定当前任务是否完成.
• 如果任务已经完成, 则直接通过 lua 脚本的⽅式, 释放锁(删除 key).
• 如果任务未完成, 则把过期时间重写设置为 10s. (即 “续约”)
这样就不担⼼锁提前失效的问题了. ⽽且另⼀⽅⾯, 如果该服务器挂了, 看⻔狗线程也就随之挂了, 此时
⽆⼈续约, 这个 key ⾃然就可以迅速过期, 让其他服务器能够获取到锁了.


引入redlock解决下面的问题

在这里插入图片描述
我们引⼊⼀组 Redis 节点. 其中每⼀组 Redis 节点都包含⼀个主节点和若⼲从节点. 并且组和组之间存
储的数据都是⼀致的, 相互之间是 “备份” 关系(⽽并⾮是数据集合的⼀部分, 这点有别于 Redis cluster).
加锁的时候, 按照⼀定的顺序, 写多个 master 节点. 在写锁的时候需要设定操作的 “超时时间”. ⽐如
50ms. 即如果 setnx 操作超过了 50ms 还没有成功, 就视为加锁失败.

在这里插入图片描述

在这里插入图片描述

http://www.dtcms.com/a/268269.html

相关文章:

  • usrp b210 亚克力外壳
  • 【机器学习笔记Ⅰ】11 多项式回归
  • hp开关机
  • CICD[导航]、docker+gitlab+harbor+jenkins从安装到部署
  • vuex 和持久化 vuex-persistedstate
  • macOS虚拟机登录AppleID,全版本,成功率高
  • 深度学习篇---简单果实分类网络
  • 群晖 DS3617xs DSM 6.1.7 解决 PhotoStation 安装失败问题 PHP7.0
  • 企业智脑:智能营销新纪元——自动化品牌建设与智能化营销的技术革命
  • Unreal Engine 5中的AI知识
  • Yocto项目:嵌入式Linux开发的“万能烹饪手册”
  • 力扣 hot100 Day36
  • C++学习之STL学习:list的模拟实现
  • 【CSS-16】深入理解CSS Transform:从基础到高级应用
  • 条件渲染 v-show与v-if
  • 《自然》发布机器人技术路线图
  • 铸造软件交付的“自动驾驶”系统——AI大模型如何引爆DevOps革命
  • 分布式压测
  • Linux驱动学习day18(I2C设备ap3216c驱动编写)
  • Mybatis----留言板
  • python实战项目81:ZeoDB多线程数据爬取程序(最新稳定好用)
  • Node中Unexpected end of form 错误
  • 【大模型入门】访问GPT_API实战案例
  • 从LLM和MCP的协同过程看如何做优化
  • webUI平替应用,安装简单,功能齐全
  • 基于Java+springboot 的车险理赔信息管理系统
  • 基于udev规则固定相机名称
  • 计算机网络:(七)网络层(上)网络层中重要的概念与网际协议 IP
  • 深度学习图像分类数据集—濒危动物识别分类
  • 如何将 Java 项目打包为可执行 JAR 文件