1、redis分布式锁相关的可以移步这篇文章redis做分布式锁实战案例详解_酒书的博客-CSDN博客
这里是对该篇文章的加深与补充
2.集群主从切换导致锁丢失问题:在redis主从架构中,写入都是写入到主redis中,主redis会同步数据到slave机器,比如一个A线程向redis实例中写入数据的时候来加了一个分布式锁,加锁后开始执行业务代码,这时如果主redis实例挂掉了,会选举出一个从redis实例成为master,如果刚刚加redis锁的key还没来得及同步到slave中,那么选举出来的新master中就没有这个key,这个时候线程B就可以进行加锁来获取分布式锁,然后执行业务代码,而这个时候线程A还没有执行结束,所以就会出现并发安全问题,这就是redis主从架构的分布式锁失效问题。
redis master收到数据之后就会返回ack ,而不是同步slave之后才返回ack,因此会有该问题。
解决方案1:使用zookeeper代替redis解决分布式锁失效问题
zookeeper也是一种k-v形式的存储中间件,它内部结构是树形的,zk集群中主节点叫做leader,从节点叫做follower,使用zk就能解决主从同步引起的分布式锁失效问题,这是因为zk保证的是一致性,leader收到请求后会同步数据给follower,收到半数上的follower ack之后leader才会给线程返回ack,即使这个时候leader挂了,已经同步到数据的follower由于数据最新必然会被选为新的leader,因此zk不存在集群分布式锁失效问题。
解决方案2:RedLock解决分布式锁失效问题
如果非要用redis来解决,那可以用RedLock,RedLock底层逻辑和zk很类似。
首先要有多个(最好是奇数个)对等的(没有主从关系)redis节点,当进行加锁时(比如是用setnx),则这个设置key-value的命令会发给每个redis节点执行,当且仅当客户端收到超过半数的节点写成功的消息时,才认为加锁成功,才开始执行业务代码。
下图中,Client 1向Redis 1/2/3三个结点去写key-value,假设当前处在Redis 1和Redis 2写入成功了,Redis 3还没有写入成功的状态,这个时候Client 1就已经认为加锁成功了,实际上已经可以执行业务代码了。此时,假设有一个Redis结点挂了(最坏的情况就是已经写入了key的一个结点挂了,如下图所示Redis 1挂了),这个时候假设Client 2也要尝试加锁,此时Redis 2由于已经被Client 1写过了,没法写入成功,但是Redis 3可以写入成功。此时只有1个结点能写入成功,所以认为加锁不成功,这样Client 2就不会开始错误的执行业务代码,也就不会出现并发安全问题。
当时其实RedLock也存在问题,比如说redis2挂了,redis2的slave成为新的master,然后client2加锁,这个时候redis3和redis2新的master都会返回写入成功,依然可以加锁,当然如果不搞从节点是可以的,那client2此时是无法加锁的。
redis实际应用中性能优化
1、我们在缓存秒杀商品的时候,比如秒杀商品product_iphone14pro_stock=1000,其实我们可以优化为将product_iphone14pro_stock 这个key给拆分为10个key,例如:
product_iphone14pro_stock_1=100
product_iphone14pro_stock_2=100
product_iphone14pro_stock_3=100
省略4、5、6、7、8、9
product_iphone14pro_stock_10=100
这样实现的话如果请求来了10个线程就不会出现并发问题,性能的话至少就提升了10倍,当然具体实现的话很多细节还是需要注意的。
2、我们用缓存的意义就是使那些热点访问的数据尽量待在缓存里,减少数据库的访问压力,而一些冷门的数据其实就没必要一直待在缓存里面。
因此1:在数据放入缓存的时候,尽量都加入一个缓存过期时间,这样一些冷门数据不会一直待在 缓存里面。
因此2:在查询缓存的时候,如果查询到了缓存,就给缓存的时间做一个超时的延期,这样不至于 热门访问的数据缓存过期。
上面的做法其实就是商品缓存数据冷热分离
3、缓存击穿(失效)
高并发情况下,热点数据缓存过期,大量请求直接打到数据库,导致数据库负载过大
解决方案:1、设置热点数据永不过期,缺点就是热点数据量大的话缓存量就比较大;
2、热点数据快过期时,通过另一个异步线程重新设置key,缺点就是需要另外的逻辑 去维护,会增加系统的复杂度
3、当缓存数据过期,重新从数据库加载数据到缓存的过程上互斥锁
4、缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和数据库都不会命中,通常出于容错的考虑,如果从数据库查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到数据库去查询,失去了缓存保护数据库的意义。造成缓存穿透的基本原因有两个:
第一:自身业务代码或者数据出现问题 第二:一些恶意攻击,爬虫等造成大量空命中
缓存穿透解决方案1:缓存空值,比如打到数据库也没有值,这个时候我将一个"{}"放到缓存里面,这样再次访问的时候就直接去缓存里面取"{}",当然设置缓存的时候记得要设置缓存过期的时间,因为如果遭到黑客攻击这个时候可能redis里面会被设置n 多个"{}",会占用我们的内存资源,设置短暂的过期时间使得黑客攻击后缓存的那些"{}"自动过期。
缓存穿透解决方案2:布隆过滤器
对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在,当说某个值不存在时,那就肯定不存在。
5、缓存雪崩
在高并发场景下,缓存同一时刻失效(缓存挂了或者大量的key设置了相同的过期时间),所有请求都会读取数据库,数据库负载过大或者崩掉
解决方案:1、缓存失效的时间设置随机数,避免同时失效
2、保证缓存服务高可用,比如使用sentinel或者cluster架构
3、依赖隔离组件为后端限流熔断并降级
6、突发性热点缓存重建导致系统压力暴增问题:
比如说某个冷门商品很少有人访问,可能就不会放到缓存里面,只有当被访问的时候才会被放到缓存里面,可是有一种情况就是某个大V直播间带货某平台某个冷门商品,导致大量流量一下子来到某商城访问某个冷门商品,由于缓存里面还没有数据,就会全部打到数据库,使得其压力增大。
解决方案:分布式锁来解决,第一个抢到锁的线程去查询数据库,然后写入缓存,后面线程进来 就可以直接去缓存里面获取值
7、 缓存与数据库双写不一致:在高并发下,同时操作数据库与缓存会存在数据不一致问题
有一种方案是写入数据库之后删除缓存,其实这样也有问题,就是读写不一致的情况
解决方案:
1、对于并发几率很小的数据几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间读取数据的时候更新缓存即可。
2、针对并发很高的情况且不能容忍缓存数据不一致的,可以通过加分布式读写锁保证并发读写或写写的时候按顺序排好队执行,读读的时候相当于无锁,提高了读的效率,推荐使用;
3、当然也可以用阿里开源的canal通过监听数据库的binlog日志及时去修改缓存,但是引入了新的中间件,增加了系统复杂度。
4、当然也可以采用延时双删策略:如下图在修改数据库数据之前,需要先删除一次redis缓存,此时是为了保证在数据库修改和redis缓存数据被删除的间隔时间内如果有其他线程来读取使得缓存中无数据,因为如果缓存中有数据的话必然是旧数据,和数据库数据不一致,避免读取缓存旧数据;第二次删除是在修改数据库之后,此时需要再次删除redis中的缓存数据,这一次是为了删除 第一次redis删除和数据库数据修改之间,如果有请求,那么旧数据又会重新缓存到redis中,然而数据在数据库中在接下来就会被修改,如果没有这一次删除,redis中则会存在数据库中旧的数据。
- 那么第二次为什么需要在数据库修改后延迟一定时间再删除redis呢?
- 为了等待之前的一次读取数据库,并等待其数据写入到缓存,最后删除这次脏数据,所以是一次数据从数据库中发到服务器+缓存写入的时间
但是延迟双删,所延迟的时间非常的难以确定,所以并不推荐延迟双删
根据综合考虑,即使先修改数据库,在删除缓存,有一定的时间会导致读取到旧数据,这通常是可以被忍受的。只要及时将缓存删除,其他线程就可以读取到最新的值。或者直接上分布式锁