redis作为缓存和分布式锁的常见问题及解决方案

一.大纲

redis作为缓存和分布式锁的常见问题及解决方案_第1张图片

二.缓存

2.1 缓存穿透

案例:根据id查询文章
redis作为缓存和分布式锁的常见问题及解决方案_第2张图片
缓存穿透:当查询一个不存在的数据,mysql查询不到数据,也不会写入缓存,就会导致每次查询时候都会去查数据库。如果当黑客知道了请求的链路,一直用不存在的id去查询数据,就会可能导致数据库的压力增大,导致宕机。
解决方案

解决方案 描述 优点 缺点
缓存空数据 缓存空数据,查询返回的数据为空,也存在缓存中去 简单 1.当存在大量空数据的时候,会消耗内存;2.当原来的空数据,mysql数据库又突然有的时候,就会出现数据一致性问题。
布隆过滤器 布隆过滤器 占用内存少,没有多余的key 1.实现复杂 ;2.存在误判

redis作为缓存和分布式锁的常见问题及解决方案_第3张图片
布隆过滤器的实现

redis作为缓存和分布式锁的常见问题及解决方案_第4张图片

布隆过滤器的主要作用是检验一个元素是否在集合中存在。它的底层实现是先去初始化一个比较大的数组,里面存放的二进制0或1,在一开始都是0,当一个key来了之后经过3次hash计算,与数组的长度进行模取余,然后把数组中对应位置的0改为1,然后数组中的3个位置就能标明一个key的存在,查询的时候,需要匹配到对应的3个位置为1,就说明key存在。
布隆过滤器可能会存在误判,如下图所示:

redis作为缓存和分布式锁的常见问题及解决方案_第5张图片
误判率:数组越大,误判率越低,消耗内存越多;数组越小,误判率越高,消耗内存越小。
布隆过滤器有可能产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,这个误判是必然存在的,我们可以通过增加数组的长度,来减小误判率,但是会增加内存的消耗,一般5%的误判率一般的项目都能接受,不至于高并发下压倒数据库。

2.2 缓存击穿

缓存击穿:当给一个key设置了过期时间,当key过期的时候,此时恰好对应的key有大量的并发请求进来,这些请求有可能压垮数据库。
redis作为缓存和分布式锁的常见问题及解决方案_第6张图片
解决方案:
方案1:互斥锁,保证数据强一致,性能差。
redis作为缓存和分布式锁的常见问题及解决方案_第7张图片
如上图所示,使用互斥锁时,当线程1缓存未命中时,不会立即去load db,先使用如redis的setnx去设置一个互斥锁,当线程1获取锁成功时在进行load db的操作并设回缓存,如果在线程1重设数据的过程中,此时有其他线程也未中缓存,是会采用不断重试的方式访问缓存。

方案2:设置key逻辑过期,高可用,性能优,不能保证数据强一致性。

redis作为缓存和分布式锁的常见问题及解决方案_第8张图片
如上如所示:设置key逻辑过期的大概实现思路如下:
1.在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间;
2.当查询的时候,从redis取出数据后判断时间是否过期;
3.如果过期则开发另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新的。

2.3 缓存雪崩

缓存雪崩:当大量的key过期或者redis宕机的时候,导致大量的请求到达数据库,带来巨大压力。
redis作为缓存和分布式锁的常见问题及解决方案_第9张图片

redis作为缓存和分布式锁的常见问题及解决方案_第10张图片

解决方案
1.针对存在大量key过期的问题,我们可以给不同的key的TTL添加随机值;
2.利用Redis集群提高服务的高可用,利用redis的哨兵模式、集群模式等;
3.给缓存业务添加降级限流策略,如nginx或者spring clound gateway;
4.给业务添加多级缓存 ,如Guava 或Caffeine。

2.4 双写一致性

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。
当我们采取方案保持数据一致性的时候,需要考虑是要求一致性要求高,还是允许延迟一致。
redis作为缓存和分布式锁的常见问题及解决方案_第11张图片
读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间。
写操作:延迟双删
在这里插入图片描述
当发生数据修改的时候,我们需要思考一个问题,先操作数据库,在删除缓存;还是先删除缓存,在操作数据库,这两种方式都会出现什么问题?

情景一:先删除缓存,在操作数据库。
假如一开始mysql和redis中的数据都是10,现在需要将数据都更新为20。
redis作为缓存和分布式锁的常见问题及解决方案_第12张图片
如上图所示,正常执行流程如下:
1.线程1需要更新数据,执行删除缓存,更新数据库的数据为20;
2.线程2查询数据,未命中缓存,将数据库的数据20写入缓存;
3.此时mysql和redis数据都为20,情况正常。
异常情况流程如下图所示:

redis作为缓存和分布式锁的常见问题及解决方案_第13张图片
如上图所示,异常执行流程如下:
1.线程1删除缓存;
2.线程2查询缓存数据,未命中,将数据库10的数据写入到缓存,此时redis的数据为10;
3.线程1更新数据库的数据为20;
4.执行完之后redis的数据为10,mysql的数据为20,就出现了数据不一致性的问题。

情景二:先操作数据库,在操作缓存
redis作为缓存和分布式锁的常见问题及解决方案_第14张图片
正常情况如上图所示执行流程如下:
1.线程2更新数据库数据为20,并删除缓存;
2.线程1查询缓存未命中,查询数据库数据

redis作为缓存和分布式锁的常见问题及解决方案_第15张图片
如上图所示,异常情况执行流程如下:
1.假如key过期,线程1查询缓存未命中,查询数据库的数据10,并把数据写入到缓存;
2.线程2执行更新数据库数据为20,删除缓存;
3.执行完之后,数据库数据为20,缓存数据为10,导致数据的不一致。

解决方案
解决方案一:延迟双删
redis作为缓存和分布式锁的常见问题及解决方案_第16张图片

为什么要删除两次缓存:延时双删虽然不能完全杜绝脏数据,但是可以大大降低脏数据的出现。
为什么要延时:一般情况下,数据库是主从模式,需要等待主数据写到从数据库,所以需要延时,延时也有可能会导致脏数据的出现。
解决方案二:分布式锁
redis作为缓存和分布式锁的常见问题及解决方案_第17张图片

解决方案三: 使用共享锁和排他锁
一般情况下,存入缓存的数据都是读多写少的情况,如果是读少写多的情况,建议不适用缓存
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作;
排他锁:独占锁writeLock,加锁之后,阻塞其他线程读写操作。

2.5 持久化

redis数据持久化的方式有RDB和AOF,持久化的含义就是将数据写入到磁盘中去。
RDB(Redis Database Backup File):redis数据备份文件,也叫做redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中去。当redis实例故障重启后,从磁盘读取快照文件,恢复数据。
人工生成快照文件的命令如下

save # 由redis主进程来执行RDB,会阻塞所有命令;
bgsave # 开启子进程执行RDB,避免主进程收到影响,建议使用这种方式生成,不影响其他命令的执行。

redis内部有触发RDB的机制,可以在redis.conf文件中配置如下:

save 900 1  # 在900s内,如果有1个key被修改,则执行bgsave
save 300 10 # 在300s内,内部有10个key被修改,则执行bgsave

RDB的执行原理:
bgsave 开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件。
所有的进程不能直接操作物理内存,操作系统给每一个进程分配了一个虚拟内存,主进程只能操作虚拟内存,操作系统会维护一个虚拟内存和物理内存之间的映射表,叫做页表,记录虚拟地址与物理地址的映射关系。
redis作为缓存和分布式锁的常见问题及解决方案_第18张图片
但是存在一个问题,当子进程在生成rdb文件的同时,主进程在修改数据,就会导致脏数据出现,那么redis是怎么避免这种情况发生的呢?

fork底层采用的是copy-on-write技术:
1.当主进程执行读操作时,访问共享内存;
2.当主进程执行写操作时,则会拷贝一份数据,执行写操作,之后主进程读数据都从拷贝之后的数据副本中进行读数据。
redis作为缓存和分布式锁的常见问题及解决方案_第19张图片
AOF:全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看作是命令日志文件。
redis作为缓存和分布式锁的常见问题及解决方案_第20张图片
AOF默认是关闭的,需要修改redis.conf文件配置来开启AOF:

appendonly yes # 是否开启AOF功能,默认是no。
appendfilename "appendonly.aof" #用于执行AOF文件的名称。

AOF的命令记录的频率也可以通过redis.conf文件来配置

appendfsync always # 表示每执行一次写命令,立即记录到AOF。
appendfsync everysec # 写命令执行完先放入AOF缓冲区,然后表示每个1秒将缓冲区数据写到AOF文件中去,是默认方案。
appendfsync no # 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区数据写回磁盘。

以上三种方式的对比如下:

配置项 刷盘时机 优点 缺点
Always 同步刷盘,每执行一次写命令就刷 可靠性高,机会不丢失数据 性能影响大
everysec 每秒刷盘 性能始终 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

AOF因为是记录命令,AOF文件回避RDB文件大得多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最小的命令达到相同效果。

auto-aof-rewrite-percentagr 100 # AOF文件比上次文件增长超过多少百分比则触发重写。
auto-aof-rewrite-min-size 64mb # AOF文件体积最小多大以上才触发重写。

RDB与AOF对比
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。

RDB AOF
持久化方式 定时对整个内存做快照 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩、文件体积小 记录命令,文件体积很大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源,但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高场景
2.6 数据过期

假如redis的key过期之后,会立即删除吗?
Redis对数据设置数据的有效时间,数据过期后,就需要将数据从内存中删除掉,可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。redis的删除策略分为2中,惰性删除、定期删除。

2.6.1 惰性删除

设置该key的过期时间后,我们不去管他,只有当真正用它的时候采取检查是否过期,如果过期,我们就删掉他,反之,返回对应的key.
优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,如果一个key过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。

2.6.2 定期删除

每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中过期的key)。
定期清理有两种模式:
1.SLOW模式是定时任务,执行频率默认10hz,每次不超过25ms,可以通过修改配置文件redis.conf的hz选择来调整这个次数。
2.FAST模式执行频率不固定,但是两次间隔不低于2ms,每次耗时不超过1ms。
优点:可以通过限制删除操作执行的时常和频率来减少删除操作对cpu的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
Redis的过期策略:惰性删除+定期删除两种策略进行配合使用。

2.7 淘汰策略

问题如下:

  1. 假如缓存过多,内存是有限的,内存被占满了怎么办?
  2. 数据库有1000玩数据,Redis只能缓存20W数据,如何保证redis中的数据都是热点数据?
  3. Redis的内存用完了会发生什么?
    数据的淘汰策略:当redis的内部不够用时,此时在向redis中添加新的key,那么redis就会按照某一种规则将内存中的数据删掉,这种数据的删除规则被称之为内存的淘汰策略。
    Redis支持8中不同的策略来选择要删除的key:
    • noeviction:不淘汰任何的key,但是内存满时不允许写入新的数据,默认就是这种策略;
    • volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰;
    • allkeys-random:对所有的key,随机进行淘汰;
    • volatile-random:对设置有TTL的key,随机进行淘汰;
    • allkeys-lru:对所有的key,基于LRU算法进行淘汰;
    • volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰;
    • allkeys-lfu:对所有的key,基于LFU算法进行淘汰;
    • volatile-lfu:对所有设置了TTL的key,基于LFU算法进行淘汰。
    LRU算法:最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
    LFU算法:最少频率使用,会统计每个key的访问频率,值越少淘汰优先级越高。

淘汰策略的使用建议
• 优先使用allkeys-lru,充分利用LRU的算法有事,把最近最常访问的数据留在缓存中,如果也有有明显的冷热数据区分,建议使用;
• 如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用allkeys-random,随机选择淘汰;
• 如果业务中有置顶的需求,可以使用volalite-lru策略,同时置顶数据不设置过期时间,则这些数据一直不被删除,会淘汰其他设置过期时间的数据。
• 如果业务中有短时高频访问的数据,可以使用allkeys-lfu或volatile-lfu策略。

三.分布式锁

什么情况下会使用分布式锁呢?
集群情况下的定时任务、抢单、幂等性场景。
下面以一个抢优惠券的场景来引入为什么需要分布式锁。

redis作为缓存和分布式锁的常见问题及解决方案_第21张图片
执行抢券的基本流程,是先查询优惠券数量,判断库存是否充足,如果充足,扣减库存,如果不充足,抛出异常,结束即可。
如下图所示,假如有两个线程执行抢券流程:
redis作为缓存和分布式锁的常见问题及解决方案_第22张图片
正常情况执行流程如下:
1.线程1查询优惠券,库存是否充足,是,扣减库存,否,抛出异常;
2.线程2查询优惠券,库存是否充足,是,扣减库存,否,抛出异常。
上面是正常执行流程,但是会出现如下如所示的异常情况:
redis作为缓存和分布式锁的常见问题及解决方案_第23张图片
假如库存中优惠券数量只有1,异常执行情况如下:
1.线程1查询优惠券数量为1;
2.线程2查询优惠券数量为1;
3.线程2判断库存是否充足,充足,将库存减1,此时库存更改为0;
4.线程1因为前面查询优惠券数量也为1,判断是充足,也对库存进行减1操作,此时就会出现超卖,库存数量变为-1。
那我们应该怎么解决以上问题呢?
我们可以尝试加多线程中的本地锁,执行流程如下:
redis作为缓存和分布式锁的常见问题及解决方案_第24张图片
如上图所示的执行流程,就不会出现超卖的问题,因为每次执行都只有一个线程执行,如果项目是一个单体项目,只启动了一个服务器,那么上面的代码不会出现任何问题,因为在同一个JVM下面,但是当我们的项目进行集群部署的时候,这样加锁就会出现问题,因为上面的加锁方式是加的本地锁,只能解决同一个JVM下的线程互斥问题,针对以上情况,就需要考虑分布式锁了。如下图所示,同一套代码部署到了不同的服务器上,客户端发送的请求可以通过nginx负载均衡算法,将请求发送到不同的服务器上。
redis作为缓存和分布式锁的常见问题及解决方案_第25张图片
如下图所示,针对不同服务器,线程的执行流程如下:
redis作为缓存和分布式锁的常见问题及解决方案_第26张图片
集群部署下,加本地锁,执行抢券的异常流程如下:
1.8080服务器线程1获取本地锁成功,查询优惠券;
2.8081服务器线程1获取本地锁成功,查询优惠券;
3.8081服务器线程1判断库存充足,进行减1操作,此时库存为0;
4.8080服务器线程1由于前面查询优惠券是充足的,也对库存进行减1操作,此时库存数量为-1,此时就会出现超卖问题。

那如何解决上面在集群部署下,超卖的问题呢,于是就引入下面的分布式锁,使用多台服务器,也就是多个JVM竞争同一个锁,如下图所示:
redis作为缓存和分布式锁的常见问题及解决方案_第27张图片
如上图所示,加了一个分布式锁,多个服务器也就是多个JVM竞争同一把锁,此时执行业务的时候,只要一个线程获取锁成功执行业务,那么其他线程都会都会进行自旋获取锁,那么以上就解决了集群或者分布式部署下超卖的问题了。接下来将讲解redis实现的各种分布式锁及相关的应用场景。

Redis实现分布式锁主要利用Redis的setnx命令。setnx是set if not exists(如果不存在,则set)的简写。
• 获取锁:

SET lock value NX EX 10  添加锁,NX是互斥,EX是指设置超时时间,就是线程获取锁之后,执行业务的超时时间。

• 释放锁:

DEL key # 释放锁,删除即可

在这里我们思考一个问题,为什么需要加EX,设置超时时间?
如下图所示,线程的执行流程:
redis作为缓存和分布式锁的常见问题及解决方案_第28张图片
当线程获取锁成功,执行业务的过程中,假如出现执行业务时间很长,或者服务器宕机的时候,该线程一直没有释放锁,就会导致出现死锁的问题,其他线程一直获取不到该锁,执行不了业务,所以我们需要设置一个超时时间,当服务器宕机或者业务执行超过设置的时间,就自动释放锁,就不会出现死锁的问题。但是,上面也会有问题,如果业务没有执行完,已经到了设置超时时间节点就释放锁,其他线程也获取了锁,执行业务,就也会有可能出现多线程下数据的问题,那么如何解决这个问题呢?其实就是Redis实现分布式锁如何合理的控制锁的有效时长?
这里先给出答案,给锁续期那如何给锁续期呢?
给锁续期的含义是当一个线程获取了锁之后,会另外开启一个线程去监控业务的执行时间,如果业务执行时间过长,超过了设置的超时时间,且业务没有执行完,就增加该线程持有锁的时间。如果由我们自己去实现,就需要单独开启一个线程去监听业务了,比较复杂,下面讲一讲redisson实现的分布式锁的执行流程和原理。
redisson实现的分布式锁,如下图所示为redisson分布式锁的执行流程:
redis作为缓存和分布式锁的常见问题及解决方案_第29张图片
如上图所示,当线程获取锁之后,操作redis的同时,会另外开启一个线程去监听持有锁的线程,称为看门狗(Watch dog),由看门狗增加线程持有锁的时间,每隔(releaseTime/3)的时间做一次续期,releaseTime就是锁的过期时间,默认是30S,也就是每隔10s重置过期时间为30s,我们需要手动释放锁,释放锁的时候需要通知对应线程的看门狗,不需要在监听该线程了。
思考一个问题:假如现在有多个线程来竞争锁,那么redisson是怎么处理的呢?
如下图所示,当其他线程尝试获取锁的时候,会采用不断自旋的方式获取锁,如果其他线程一直没有释放锁,该线程也不会一直自旋,设置了一个阈值,当达到阈值的时候,就会获取锁失败。
redis作为缓存和分布式锁的常见问题及解决方案_第30张图片
redisson实现的分布式锁代码如下:

public void redisLock() throws InterruptedException{
    // 1.获取锁(重入锁),执行锁的名称
    RLock lock = redissonClient.getLock("lockName");
    // 2.尝试获取锁
    //  boolean isLock = lock.tryLock(10, 30,TimeUnit.SECONDS); # 这里有三个参数,第一个参数10代表尝试获取锁的时间,
    //  如果超过10就代表获取失败,第二个参数30代表锁的失效时间,第三个参数代表单位。当我们设置3个参数的时候,
    //  redisson就默认不会有看门狗去监听持有锁的线程,默认能够判断锁的失效时间,就不会给所续期了。
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
    // 3.判断是否获取锁成功
    if(isLock){
          try{
              System.out.printIn("执行业务");
       }  finally{
         // 5.释放锁
         lock.unlock;
       }
    }
   // 以上加锁,设置过期时间等操作都是基于lua脚本完成。
}

以上加锁,设置过期时间等操作都是基于lua脚本完成

思考下面一个问题,redisson实现的分布式锁是否可以重入吗?
答案是可以的。
redis作为缓存和分布式锁的常见问题及解决方案_第31张图片
如上图所示,一个线程执行了add1方法里面获取了heimalock的锁,然后又调用add2方法获取同一把锁,是可以获取成功的,是可以重入的,执行原理如下:
redis作为缓存和分布式锁的常见问题及解决方案_第32张图片
redisson实现的分布式锁可重入的原理是。利用如上图所示的hash结构记录线程id和重入次数,field记录线程的唯一标识,value记录重入的次数,只有当value值变为0之后,才会去释放锁。

redisson实现的分布式锁能够实现主从一致性吗?
假如搭建了redis的主从架构,如下图所示,主节点负责写数据,从节点负责读数据,但是主节点的数据修改之后要同步到从节点里面去,保证主从数据的同步。
redis作为缓存和分布式锁的常见问题及解决方案_第33张图片
如下图所示,当出现主节点向从节点同步数据的过程中,主节点突然宕机了,依据redis提供的哨兵模式(后续会讲解),会选择一个从节点作为主节点,假如此时有一个新的线程获取锁成功之后,会请求新的主节点,此时就会出现两个线程持有同一把锁,如果业务都在执行,此时就会出现脏数据,那遇到这种情况如何解决呢?
redis作为缓存和分布式锁的常见问题及解决方案_第34张图片
针对上面的问题,redisson提供了另外一个锁RedLock(红锁),红锁的特点是不能再一个redis实例上创建锁,应该在多个redis实例上创建锁(n/2 +1),即需要创建redis节点一半的锁,如下图所示,有3个redis节点,就需要创建2个锁。
redis作为缓存和分布式锁的常见问题及解决方案_第35张图片
虽然红锁解决了上面的问题,但是红锁也是有一定缺陷的,很少使用,因为加了红锁之后,实现起来很复杂,在高并发的情况下性能差,redis官方也不建议使用红锁来解决主从数据不一致性的问题。redis集群的思想是AP思想,保证高可用性,如果非要保证数据的强一致性,使用CP思想的zookeeper的分布式锁。

你可能感兴趣的:(Redis,缓存,redis,分布式)