【Redis】缓存+分布式锁

目录

缓存

Redis最主要的使用场景就是作为缓存

缓存的更新策略:

1.定期生成

2.实时生成

面试重点:

[缓存预热(Cache preheating):](#缓存预热(Cache preheating):)

[缓存穿透(Cache penetration)](#缓存穿透(Cache penetration))

缓存雪崩 (Cache avalanche)

缓存击穿 (Cache breakdown)

分布式锁

分布式锁的基本实现:

引入过期时间:

引入校验Id:

引入lua脚本:

[引入watch dog(看门狗)](#引入watch dog(看门狗))

引入Redlock算法:


缓存

核心思路就是把⼀些常见的数据放到触手可及(访问速度更快)的地方, 方便随时读取.

访问速度特别的快,但由于存储空间是有限的,所有我们只能存储"热点数据"(频繁访问的数据)在Redis中
俗称"二八定律":20%的热点数据,可以应对80%的应用场景,从而整体上的性能得到提升


Redis最主要的使用场景就是作为缓存

对应MySQL这种访问硬盘的数据库,Redis是内存数据库,Redis用做为MySQL的缓存

为什么MySQL性能不高?

1.首先数据存储在硬盘上,硬盘IO需要花费大量时间,尤其是随机访问;

2.如果查询中不能命中索引,那么将需要表的遍历,大大增加硬盘IO的次数

3.对应执行SQL需要一系列的解析,校验等等工作

4.如果是一些复杂查询,例如联合查询,笛卡尔积之类的操作效率会很低...............

如果数据库并发量特别高,那么它将会可能出现宕机(出现故障,罢工了,不做了)的情况

如何解决?

1.开源:增加多个设备,部署更多的数据库机器,构成数据库集群

2.节流:引入缓存,访问的时候尽量访问缓存内的数据,从而降低直接访问数据库的请求数量

实际开发中,往往是俩种方法结合起来使用

Redis为什么可以作为MySQL的缓存:

Redis访问速度比MySQL快很多,并且处理一个访问请求,Redis消耗的资源比MySQL少很多,因此Redis支持更大的并发量:

1.Redis数据在内存,并且访问速度比硬盘快很多

2.Redis只支持简单的KEY-VALUE 存储,不涉及很多复杂查询方法手段

业务服务器先查询 Redis, 看想要的数据是否在 Redis 中存在.

  • 如果已经在 Redis 中存在了, 就直接返回. 此时不必访问 MySQL 了.
  • 如果在 Redis 中不存在, 再查询 MySQL.

缓存的更新策略:

一个重要问题:那么多数据,哪些数据才是我们需要的热点数据呢?

1.定期生成

会将所有访问的数据,以日志的形式给记录下来,最后挑选出频率前%N的数据作为"热点数据";

此处的数据,可以按照每天/周/月去更新热点数据

2.实时生成

  • 如果在Redis查找到了,那么将直接返回
  • 如果在Redis没有查找到,那么从数据库查找,查找完并且写入Redis中

那么经过这样一段时间内,缓存的数据肯定会存储满,那么Redis也引入了对应的策略--------------内存淘汰机制:

通用的淘汰机制有以下机制:

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

在Redis中已经实现好了以上类似机制:

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

面试重点:

缓存预热(Cache preheating):

1.定期生成的数据这种情况不涉及缓存预热;

2.实时生成:
Redis服务器首次接入连接之后是没有数据的,此时所有的请求将在MySQL中(就怕此时MySQL没有抗住这么多请求挂了),那么随之时间的推移,Redis上的数据慢慢越来越多,MySQL承担的压力就会小很多;

缓存预热就是解决上述问题:
将定期生成和实时生成结合一下,先通过"离线"的方式,通过统计的途径,先找到一些热点数据(这些数据并一定精准,有就行)导入到Redis中,就能帮助MySQL承担很大的压力,随之时间的推移,热点数据会逐渐调整,来使用当前情况;


缓存穿透(Cache penetration)

在查询某个key的时候,在Redis中没有,在MySQL中也没有,那么这么key肯定不会更新到Redis中~
但是这个查询没有,接连着查询了多次,依然会给MySQL造成一些压力;

为什么出现这种情况?

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

解决:

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

缓存雪崩 (Cache avalanche)

在短时间内,Redis上大规模的key失效,导致缓存命中率陡然下降,让MySQL的压力迅游变大,导致宕机~

为何产生?

1.Redis挂了

2.Redis没有挂,但是可能之前给很多key设置的过期时间相同,一下子全过期自动删除了

解决:

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

缓存击穿 (Cache breakdown)

相当于缓存雪崩的特殊情况,针对Redis热点key全部过期了,导致大量的请求直接访问到MySQ中,引起数据库宕机

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

分布式锁

在一个分布式系统当中,会涉及到多个节点访问同一个公共资源的情况下,此时就需要通过"锁"来进行互斥,避免出现"线程安全"的问题;

像Java中的synchronized 这种锁只能在当前进程中生效,在分布式这种多个进程多个主机的场景下就不能使用了';

分布式锁的基本实现:

实现思路:通过设置一个键值对来标识锁的状态

举例买票的例子:

此时客户端1查询001车 还有1张车票 在即将执行1->0 过程前

客户端2也执行查询余票 发现还有一张,那么客户端2也执行1->0

那么这出现"超卖" 的情况,俩个人都买到票

加锁:

此时, 如果 买票服务器1 尝试买票, 就需要先访问 Redis, 在 Redis 上设置⼀个键值对. 比如 key 就是车次, value 随便设置个值 (比如 1).

如果发现操作成功,那么就可以视为当前没有节点对001车次加锁,那么就可以进行数据库的读写,操作完成之后,就可以吧这个key删除(相当于解锁)

如果服务器1在操作数据库时候,服务器2也想买票,那么它在给Redis新增key的时候,就会发现此时车次的key已经存在,就可以认为其他服务器此时正在持有锁,那么服务器2就可以等待或者放弃~

Redis中的setnx刚刚好就行对应此场景:当key不存在才可以创建成功,如果存在那么将失败 ~

此时如果某个服务器加锁成功,但意外发生了,服务器崩溃了没有执行解锁操作(删除操作),那么就导致其他服务器没有获取到锁;

引入过期时间:

可以设置key的过期时间,让这个锁持有多久,如果超过一定时间,那么将自动释放;

使用set ex nx 命令,在设置锁的同时加入过期时间;

注意这是一条命令

不能写成 setnx

expire

此时是俩条命令,就有可能造成原子性,仍然会出现无法正确释放锁的问题


所谓的加锁就是在Redis上执行setnx生成一个key
解锁就是删除这个key

那么有没有可能会出现服务器1执行了加锁操作,而服务器2执行了解锁?
那么是有可能的

引入校验Id:

需要引入一点校验机制:

1.给服务器编号,每个服务器有一个自己的身份标识

2.进行加锁的时候,设置key-value 对应要针对的哪个资源加锁(比如车次),value存储刚才服务器的编号 例如"001:1" ->"001:服务器1"

这样就可以在删除key的时候,校验当前的key是否是当初加锁的服务器,避免删除错误~


在解锁的时候:
1.先查询判断是否删除 2. 才进行删除

此时这个是俩步操作,并不是原子的,就有可能出现问题

引入lua脚本:

很多程序里面都支持内嵌脚本语音.Redis支持Lua语音作为内嵌脚本~

Lua 复制代码
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
1 2 3return 0
end;

过期时间续约问题可能存在,

在给key加锁的时候,设置它的过期时间,那么设置多久呢才是比较合适?

  • 设置时间短,那么可能在业务逻辑还没执行完,就释放锁了
  • 设置时间长,就可以导致锁释放不及时的可能

更好的是能够去进行"动态续约"

引入watch dog(看门狗)

本质上就是在加锁的服务器上的一个单独的线程,通过这个线程来对锁过期时间进行一个"续约"

❤ 举个具体的例子:

初始情况下设置过期时间为 10s. 同时设定看门狗线程每隔 3s 检测⼀次.

那么当 3s 时间到的时候, 看门狗就会判定当前任务是否完成.

  • 如果任务已经完成, 则直接通过 lua 脚本的方式, 释放锁(删除 key).
  • 如果任务未完成, 则把过期时间重写设置为 10s. (即 "续约")

使用Redis作为分布式锁,有没有可能Redis自身挂了?

那么是很有可能的!!!

Redis一般是以集群(至少也是主从的形式,而不是单机)的方式去部署的

服务器1向它的master节点就行加锁操作,这个数据刚写入主节点,主节点挂了,从节点就会新的主节点

由于主节点和从节点之间的数据是同步的,但是有延迟的

那么刚才写入的key还没有被新的主节点接收,此时相当于服务器1就没有成功进行加上锁

但是服务器2仍然可以进行去加上锁,

引入Redlock算法:

由于主节点和主节点之间数据都是一致的,相互是备份的关系

在进行加锁的时候,会按照一定的顺序,针对这组Redis都进行加锁

如果某个节点加不上锁,并没有关系,可能是Redis挂了,继续给下一个节点加锁即可

如果此时key加锁成功的节点个数占总的一半,那么可视为加锁成功

同时解锁的时候,会把上述的节点都需要解锁~

简而言之, Redlock 算法的核心就是, 加锁操作不能只写给⼀个 Redis 节点, 而要写个多个!! 分布式系统中任何⼀个节点都是不可靠的. 最终的加锁成功结论是 "少数服从多数的".

相关推荐
烛.照10318 分钟前
宝塔安装完redis 如何访问
linux·数据库·redis·缓存
优人ovo19 分钟前
Kafka的消息协议
分布式·网络协议·kafka
言之。3 小时前
【Spark速通】
大数据·分布式·spark
大秦王多鱼4 小时前
Kafka常见问题之 `javax.management.InstanceAlreadyExistsException`
运维·分布式·kafka·apache
_Eden_4 小时前
Redis学习之哨兵二
数据库·redis·学习
乙卯年QAQ6 小时前
【Hadoop】Hadoop 概述
大数据·hadoop·分布式
天选之子1236 小时前
spark运行流程
大数据·分布式·spark
想搞艺术的程序员6 小时前
Go优雅实现redis分布式锁
redis·分布式·golang
翻晒时光13 小时前
Spring Boot 集成 Redis 全解析
spring boot·redis
Neil Parker15 小时前
搭建Spark分布式集群
大数据·分布式·spark