《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 的服务器是否是当初加锁的服务器, 如果是,

才能真正删除; 不是, 则不能删除.

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

c 复制代码
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 还没有成功, 就视为加锁失败.

相关推荐
jakeswang1 小时前
应用缓存不止是Redis!——亿级流量系统架构设计系列
redis·分布式·后端·缓存
.Shu.3 小时前
Redis zset 渐进式rehash 实现原理、触发条件、执行流程以及数据一致性保障机制【分步源码解析】
数据库·redis·缓存
君不见,青丝成雪3 小时前
大数据技术栈 —— Redis与Kafka
数据库·redis·kafka
悟能不能悟3 小时前
排查Redis数据倾斜引发的性能瓶颈
java·数据库·redis
切糕师学AI3 小时前
.net core web程序如何设置redis预热?
redis·.netcore
不久之3 小时前
大数据服务完全分布式部署- 其他组件(阿里云版)
分布式·阿里云·云计算
Mi_Manchikkk4 小时前
Java高级面试实战:Spring Boot微服务与Redis缓存整合案例解析
java·spring boot·redis·缓存·微服务·面试
Direction_Wind4 小时前
粗粮厂的基于spark的通用olap之间的同步工具项目
大数据·分布式·spark
xiao-xiang4 小时前
redis-集成prometheus监控(k8s)
数据库·redis·kubernetes·k8s·grafana·prometheus
柳贯一(逆流河版)17 小时前
Spring 三级缓存:破解循环依赖的底层密码
java·spring·缓存·bean的循环依赖