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

【Redis】redis用作缓存和分布式锁

文章目录

  • 1. 缓存
    • 1.1 Redis作为缓存
    • 1.2 缓存更新、淘汰策略
    • 1.3 缓存预热、缓存穿透、缓存雪崩和缓存击穿
      • 1.3.1 缓存预热(preheating)
      • 1.3.2 缓存穿透(penetration)
      • 1.3.3 缓存雪崩(avalanche)
      • 1.3.4 缓存击穿(breakdown)
  • 2. 分布式锁
    • 2.1 什么是分布式锁?
    • 2.2 分布式锁的基础实现
    • 2.3 引入过期时间(防止“死锁”问题)
    • 2.4 引入校验id(防止锁误删问题)
    • 2.5 引入lua(解决原子性问题)
    • 2.6 引入watchdog(解决过期时间不足问题)
    • 2.7 引入Redlock算法(防止redis挂了)

在这里插入图片描述

1. 缓存

Redis最主要的三个功能

  • 存储数据(内存数据库)
  • 缓存
  • 消息队列

1.1 Redis作为缓存

在⼀个网站中,我们经常会使⽤关系型数据库(比如MySQL)来存储数据;关系型数据库虽然功能强大,但是有⼀个很大的缺陷,就是性能不高(换言之,进⾏⼀次查询操作消耗的系统资源较多)

硬件的访问速度通常是如下情况下:CPU寄存器 > 内存 > 硬盘 > ⽹络

为什么说关系型数据库性能不⾼?

  1. 数据库把数据存储在硬盘上,硬盘的IO速度并不快,尤其是随机访问.
  2. 如果查询不能命中索引,就需要进⾏表的遍历,这就会大大增加硬盘IO次数.
  3. 关系型数据库对于SQL的执行会做⼀系列的解析、校验、优化⼯作.
  4. 如果是⼀些复杂查询,⽐如联合查询,需要进⾏笛卡尔积操作,效率更是降低很多.

因此,如果访问数据库的并发量⽐较⾼,对于数据库的压⼒是很⼤的,很容易就会使数据库服务器宕机

如何让数据库能够承担更⼤的并发量呢?核心思路主要是两个:

  • 开源:引⼊更多的机器,部署更多的数据库实例,构成数据库集群.(主从复制,分库分表等…)
  • 节流:引⼊缓存,使⽤其他的⽅式保存经常访问的热点数据,从⽽降低直接访问数据库的请求数量

Redis 就是⼀个⽤来作为数据库缓存的常⻅⽅案,Redis访问速度⽐MySQL快很多,或者说处理同⼀个访问请求,Redis消耗的系统资源⽐MySQL少很多,因此Redis能⽀持的并发量更⼤.

  • Redis数据在内存中,访问内存⽐硬盘快很多.
  • Redis只是⽀持简单的key-value存储,不涉及复杂查询的那么多限制规则.

在这里插入图片描述

1.2 缓存更新、淘汰策略

redis作为缓存,一般存储的热点数据,那么如何知道哪些数据是热点数据呢?

  1. 定期生成

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

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

  1. 实时生成

先给缓存设定容量上限(可以通过Redis配置⽂件的maxmemory 参数设定).

接下来把用户每次查询:

  • 如果在Redis查到了,就直接返回.
  • 如果Redis中不存在,就从数据库查,把查到的结果同时也写⼊Redis.

如果缓存已经满了(达到上限),就触发缓存淘汰策略,把⼀些"相对不那么热门"的数据淘汰掉。照上述过程,持续⼀段时间之后Redis内部的数据⾃然就是"热门数据"了.

通⽤的淘汰策略主要有以下⼏种:

  • FIFO (First In First Out) 先进先出:把缓存中存在时间最久的(也就是先来的数据)淘汰掉
  • LRU(LeastRecentlyUsed)淘汰最久未使⽤的:记录每个key的最近访问时间,把最近访问时间最⽼的key淘汰掉
  • LFU(LeastFrequently Used)淘汰访问次数最少的:记录每个key最近⼀段时间的访问次数,把访问次数最少的淘汰掉
  • Random随机淘汰:从所有的key中抽取幸运⼉被随机淘汰掉

这⾥的淘汰策略,我们可以自己实现,当然Redis也提供了内置的淘汰策略,也可以供我们直接使用,下面罗列了一部分:
在这里插入图片描述

整体来说Redis提供的策略和我们上述介绍的通⽤策略是基本⼀致的,只不过Redis这⾥会针对"过期key" 和"全部key"做分别处理.

1.3 缓存预热、缓存穿透、缓存雪崩和缓存击穿

1.3.1 缓存预热(preheating)

使⽤Redis作为MySQL的缓存的时候,当Redis刚刚启动,或者Redis⼤批key失效之后,此时由于Redis 自身相当于是空着的,没啥缓存数据,那么MySQL就可能直接被访问到,从⽽造成较⼤的压⼒

因此就需要提前把热点数据准备好,直接写⼊到Redis中,使Redis可以尽快为MySQL撑起保护伞

热点数据可以基于之前介绍的统计的⽅式⽣成即可,这份热点数据不⼀定非得那么"准确",只要能帮助MySQL抵挡大部分请求即可,随着程序运⾏的推移,缓存的热点数据会逐渐自动调整,来更适应当前情况

1.3.2 缓存穿透(penetration)

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

为何产⽣?原因可能有几种:

  • 业务设计不合理,比如缺少必要的参数校验环节,导致非法的key也被进⾏查询了
  • 开发/运维误操作,不小心把部分数据从数据库上误删了
  • ⿊客恶意攻击

如何解决?

  • 针对要查询的参数进⾏严格的合法性校验
    • ⽐如要查询的key是⽤⼾的⼿机号,那么就需要校验当前key 是否满⾜⼀个合法的⼿机号的格式
  • 针对数据库上也不存在的key,也存储到Redis中
    • ⽐如value就随便设成⼀个"",避免后续频繁访问数据库.
  • 使⽤布隆过滤器先判定key是否存在,再真正查询

1.3.3 缓存雪崩(avalanche)

短时间内大量的key在缓存上失效,导致数据库压力骤增,甚⾄直接宕机,这种情况叫做缓存雪崩

本来Redis是MySQL的⼀个护盾,帮MySQL抵挡了很多外部的压⼒,⼀旦护盾突然失效了,MySQL⾃⾝承担的压⼒骤增,就可能直接崩溃.

产⽣大规模key失效,可能性主要有两种:

  • Redis挂了.
  • Redis上的大量的key同时过期

为啥会出现⼤量的key同时过期?这种可能是短时间内在Redis上缓存了⼤量的key,并且设定了相同的过期时间.

如何解决?

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

1.3.4 缓存击穿(breakdown)

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

如何解决?

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

2. 分布式锁

2.1 什么是分布式锁?

在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于"线程安全"的问题。

而java的synchronized或者C++的std::mutex,这样的锁都是只能在当前进程中⽣效;在分布式的这种多个进程多个主机的场景下就⽆能为力了,此时就需要使用到分布式锁

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

2.2 分布式锁的基础实现

思路非常简单,本质上就是通过⼀个键值对来标识锁的状态

举个例⼦:考虑买票的场景,现在⻋站提供了若⼲个⻋次,每个⻋次的票数都是固定的。现在存在多个服务器节点,都可能需要处理这个买票的逻辑:先查询指定⻋次的余票,如果余票>0,则设置余票值-=1

在这里插入图片描述
显然上述的场景是存在"线程安全"问题的,需要使用锁来控制。

所谓的分布式锁,就是一个/一组单的服务器程序,来给其它服务器提供“加锁”这样的服务。

redis是一种典型的可以用来实现分布式锁的方案,但不是唯一一种

在这里插入图片描述

在买票服务器进行买票操作的过程中,就需要先进行加锁

  • 加锁:往redis中设置一个特殊的key-value,完成买票操作,再把这个key-value删除掉
    • 使用 set nx命令设置,del命令删除
  • 其它服务器也想执行买票操作时,也去redis上尝试设置key-value,设置失败,则认为加锁失败(是阻塞/放弃,看具体的策略)
  • 此时,就可以解决线程安全的问题了

刚才买票场景,使用 mysql 的事务也可以批量执行 査询 + 修改 操作,但是分布式系统中,要访问的共享资源不一定是 mysq! 也可能是其他的存储介质没有事务,也可能是执行一段特定的操作,是通过统一的服务器完成执行动作

2.3 引入过期时间(防止“死锁”问题)

当服务器1加锁之后,开始处理买票的过程中,如果服务器1意外宕机了,就会导致解锁操作(删除该key) 不能执行,就可能引起其他服务器始终无法获取到锁的情况

为了解决这个问题,可以在设置key的同时引入过期时间,即这个锁最多持有多久,就应该被释放(使用set ex nx 命令)

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

2.4 引入校验id(防止锁误删问题)

对于Redis中写⼊的加锁键值对,其他的节点也是可以删除的.

比如服务器1写⼊⼀个"001":1这样的键值对,服务器2是完全可以把"001"给删除掉的。当然,服务器2不会进⾏这样的"恶意删除"操作,不过不能保证因为⼀些bug导致服务器2把锁误删除。

为了解决上述问题,我们可以引⼊⼀个校验id。
比如可以把设置的键值对的,不再是简单的设为⼀个1,⽽是设成服务器的编号,形如"001":"服务器1"

这样就可以在删除key(解锁)的时候,先校验当前删除key的服务器是否是当初加锁的服务器,如果是,才能真正删除;不是,则不能删除

在解锁的时候,要先进行id的校验,再执行del操作,但是这两步不是原子的,就可能出现问题
在这里插入图片描述

2.5 引入lua(解决原子性问题)

为了使解锁操作原子,可以使⽤Redis的Lua脚本功能

使⽤Lua脚本完成上述解锁功能

if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) 
else return 0 
end;

上述代码可以编写成⼀个.lua后缀的⽂件,由redis-cli 或者redis-plus-plus 或者jedis 等客⼾端加载,并发送给Redis服务器,由Redis服务器来执⾏这段逻辑;⼀个lua脚本会被Redis服务器以原⼦的⽅式来执⾏.

2.6 引入watchdog(解决过期时间不足问题)

上述⽅案仍然存在⼀个重要问题,当我们设置了key过期时间之后(⽐如10s),仍然存在⼀定的可能性,当任务还没执⾏完,key就先过期了,这就导致锁提前失效

  • 把这个过期时间设置的⾜够⻓,⽐如30s,是否能解决这个问题呢?很明显,设置多⻓时间合适,是⽆⽌境的,即使设置再⻓,也不能完全保证就没有提前失效的情况。
  • 如果设置的太⻓了,万⼀对应的服务器挂了,此时其他服务器也不能及时的获取到锁
  • 因此相⽐于设置⼀个固定的⻓时间,不如动态的调整时间更合适

所谓watchdog(看门狗),本质上是加锁的服务器上的一个单独的线程,通过这个线程来对锁过期时间进⾏"续约".

这样就不担心锁提前失效的问题了,而且另一方面,如果该服务器挂了,看门狗线程也就随之挂了,此时无人续约,这个key⾃然就可以迅速过期,让其他服务器能够获取到锁了

2.7 引入Redlock算法(防止redis挂了)

实践中的Redis⼀般是以集群的⽅式部署的(至少是主从的形式,⽽不是单机),那么就可能出现以下⽐较极端的大冤种情况:

  • 服务器1向master节点进行加锁操作,这个写⼊key的过程刚刚完成,master挂了,slave节点升级成了新的master节点。
  • 但是由于刚才写⼊的这个key尚未来得及同步给slave呢,此时就相当于服务器1的加锁操作形同虚设了,服务器2仍然可以进⾏加锁(即给新的master写⼊key,因为新的master不包含刚才的key)

为了解决这个问题,Redis的作者提出了Redlock算法

  • 我们引入一组Redis节点,其中每⼀组Redis节点都包含⼀个主节点和若⼲从节点,并且组和组之间存储的数据都是⼀致的,相互之间是"备份"关系(而并非是数据集合的⼀部分,这点有别于redis cluster).
  • 加锁的时候,按照⼀定的顺序,写多个master节点,在写锁的时候需要设定操作的"超时时间",⽐如50ms,即如果setnx操作超过了50ms还没有成功,就视为加锁失败.
  • 如果给某个节点加锁失败,就⽴即再尝试下⼀个节点,当加锁成功的节点数超过总节点数的⼀半,才视为加锁成功.

在这里插入图片描述
这样的话,即使有某些节点挂了,也不影响锁的正确性

同理,释放锁的时候,也需要把所有节点都进⾏解锁操作(即使是之前超时的节点,也要尝试解锁,尽量保证逻辑严密)

简而言之,Redlock算法的核心就是:加锁操作不能只写给⼀个Redis节点,而要写个多个!

分布式系统中任何⼀个节点都是不可靠的,最终的加锁成功结论是"少数服从多数的";由于⼀个分布式系统不⾄于⼤部分节点都同时出现故障,因此这样的可靠性要⽐单个节点来说靠谱不少

在这里插入图片描述

http://www.xdnf.cn/news/495631.html

相关文章:

  • 湖北理元理律师事务所:科学债务管理模型构建实录
  • 无法加载文件 E:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
  • 支持同步观看的媒体服务器GhostHub
  • 【Linux笔记】——线程互斥与互斥锁的封装
  • 使用 Python 连接 Oracle 23ai 数据库完整指南
  • 小黑独自咖啡厅享受思考心流:82. 删除排序链表中的重复元素 II
  • DAY28-类的定义和方法
  • 计算机视觉与深度学习 | LSTM应用于数据插值
  • 下集:一条打包到底的静态部署之路
  • JMeter 教程:编写 POST 请求脚本访问百度
  • SQL Server 与 Oracle 常用函数对照表
  • 二进制与十进制互转的方法
  • 使用Maven部署WebLogic应用
  • 信贷风控笔记6——风控常用指标(面试准备14)
  • MATLAB学习笔记(六):MATLAB数学建模
  • Uniapp、Flutter 和 React Native 全面对比
  • 分糖果--思维+while判断
  • 基于QT和FFmpeg实现自己的视频播放器FFMediaPlayer(一)——项目总览
  • 芯片生态链深度解析(二):基础设备篇——人类精密制造的“巅峰对决”
  • StarRocks MCP Server 开源发布:为 AI 应用提供强大分析中枢
  • gcc 源码目录文件夹功能简介
  • 从辅助到协作:GitHub Copilot的进化之路
  • 副业小程序YUERGS,从开发到变现
  • String的一些固定程序函数
  • 嵌入式学习笔记 - STM32 使用一个外部触发同时启动两个定时器
  • 谷歌浏览器(Google Chrome)136.0.7103.93便携增强版|Win中文|安装教程
  • 2.1.3
  • 【Linux网络】NAT和代理服务
  • AtCoder AT_abc406_c [ABC406C] ~
  • MySQL相关