文章目录
- 1.5.1 布隆过滤器BloomFilter
- 1.5.1.1 原理
- 1.5.1.2 使用场景
- 1.5.2 Redis分布式锁
- 1.5.2.1 使用案例分析
- 1.5.2.1.1 单机版没加锁
- 1.5.2.1.2 单节点Redis实现分布式锁
- 1.5.2.1.3 集群下的分布式及CAP
- 1.5.2.1.4 Redisson可靠分布式锁
- 1.5.2.1.5 Redis分布式锁-Redlock算法
- 1.5.2.1.5.1 多机案例
- 1.5.3 Redisson源码分析
- 1.5.3.1 单机案例
- 1.5.3.2 加锁及可重入原理
- 1.5.3.3 watchDog原理
- 1.5.3.4 互斥原理
- 1.5.3.5 解锁原理
- 1.5.3.6 案例代码解释加锁和可重入
- 1.5.4 Reids实战问题
- 1.5.4.1 缓存双写一致性之更新策略探讨
- 1.5.4.1.1 方案1:先更新数据库,再更新缓存
- 1.5.4.1.2 方案2:先删除缓存,再更新数据库
- 1.5.4.1.2.1 分布式锁防止缓存击穿
- 1.5.4.1.2.2 延时双删策略
- 1.5.4.1.3 方案3:先更新数据库,再删除缓存
- 1.5.4.1.4 总结对比
- 1.5.4.2 缓存预热+缓存雪崩+缓存击穿+缓存穿透
- 1.5.4.2.1 缓存雪崩
- 1.5.4.2.2 缓存穿透
- 1.5.4.2.3 缓存击透
- 案例:淘宝聚划算功能实现+防止缓存击穿
1.5.1 布隆过滤器BloomFilter
问题:现有50亿个电话号码,现有10万个电话号码,如何要快速准确的判断这些电话号码是否已经存在?
1、通过数据库查询-------实现快速有点难。
2、数据预放到内存集合中:50亿*8字节大约40G,内存太大了。
1.5.1.1 原理
实质就是一个大型位数组(初值0)和几个不同的无偏hash函数(无偏表示分布均匀)。
当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,
把它们置为 1(假定有两个变量都通过 3 个映射函数)。
若现在只存如obj1和obj2,如上图。
假设现在obj3,经过多个hash映射到1、3、8的位置,宏观上看我们知道obj3不存在,但是底层三个位置为1,最后给我的是true,故而只能确定为不一定存在
假设现在obj3,经过多个hash映射到1、3、4的位置,宏观上看我们知道obj4不存在,但是底层三个位置存在0,最后给我的是false,故而判定一定不存在
布隆过滤器误判率,为什么不要删除?(多个元素共享一个位置)
1.5.1.2 使用场景
解决缓存穿透的问题
黑名单
1.5.2 Redis分布式锁
1.5.2.1 使用案例分析
背景:秒杀,超卖现象分析
先总结:
- synchronized单机版OK,上分布式
- nginx分布式微服务单机锁不行/(ㄒoㄒ)/~~
- 取消单机锁,上redis分布式锁setnx
- 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
- 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
- 为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行
- 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
- redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现
1.5.2.1.1 单机版没加锁
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),
所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
1.5.2.1.2 单节点Redis实现分布式锁
分布式部署后,单机锁还是出现超卖现象,需要分布式锁
问题1:若服务异常,无法释放锁,需代码层面finally释放锁
问题2:服务宕机,未执行finally,需给锁添加超时时间
问题3:设置key+过期时间分开了,必须要合并成一行具备原子性
问题4:删除了别人的锁,需 删除时判断是否是自己的锁
当锁的粒度粗,两次请求使用同一把锁,若业务执行时间长,锁过期时间短
第一次请求锁过期,业务还未执行完,此时
第二个请到来获取到锁,最后被第一请求删锁,故删除时需判断是否是自己的锁
此处判断是否是自己的锁根据,锁持有值判断,两次请求值不一样
问题5:finally块的判断+del删除操作不是原子性的
解决方案:Redis调用Lua脚本通过eval命令保证代码执行的原子性
问题6:确保redisLock过期时间大于业务执行时间的问题(Redis分布式锁如何续期?)
1.5.2.1.3 集群下的分布式及CAP
确保redisLock过期时间大于业务执行时间的问题(Redis分布式锁如何续期?)
集群+CAP,redis对比zookeeper
Zookeeper集群的CAP:
1.5.2.1.4 Redisson可靠分布式锁
上述代码解决了续期问题(Redisson),
问题1: 但无锁时执行unlock,报错,需判断
尝试解锁,没锁当前线程节点id
1.5.2.1.5 Redis分布式锁-Redlock算法
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
Redis分布式锁比较正确的姿势是采用redisson这个客户端工具
1.5.2.1.5.1 多机案例
基于setnx的分布式锁有什么缺点?解决方案红锁
破坏了锁的排他特性,同一时间只能有一个建redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。危险的
Redlock:
redis之父提出了Redlock算法解决这个问题:Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。
设计理念:
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。
解决方案:采用3个说明
为什么是奇数? N = 2X + 1 (N是最终部署机器数,X是容错机器数)
那你知道redis之父设计的这个方案,还有bug吗?分布式难以避免的,系统时钟影响
如果线程1从3个实例获取到了锁。但是这3个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有3个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。
1.5.3 Redisson源码分析
使用:
1.5.3.1 单机案例
一般中小公司,不是高并发场景,是可以使用的。单机redis小业务也撑得住
加锁关键逻辑:
NX:等效setNx,key不存在才能加锁成功
解锁关键逻辑:
1.5.3.2 加锁及可重入原理
1.5.3.3 watchDog原理
看门狗是为了解决业务耗时,锁过期失效问题
场景:A、B线程执行同一业务方法,A加分布式锁,锁过期失效,A还没执行完,B获取到锁执行,可能会出现安全问题
解决方法:在获取锁成功后,给锁加一个watchdog后台线程,定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间
加锁成功触发监听执行如下方法,
这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
1.5.3.4 互斥原理
当其他线程进入临界资源,发现存在分布式锁,会执行到加锁lua脚本中
“return redis.call(‘pttl’, KEYS[1]);” // 如果已经锁定,但并非本线程,返回锁还有锁失效ttl |
---|
1.5.3.5 解锁原理
1.5.3.6 案例代码解释加锁和可重入
1.5.4 Reids实战问题
目的:最终一致性
1.5.4.1 缓存双写一致性之更新策略探讨
1.5.4.1.1 方案1:先更新数据库,再更新缓存
方案1:先更新数据库,再更新缓存
问题分析:更新db成功,更新缓存失败(缓存还是旧的),导致db和缓存数据不一致
1.5.4.1.2 方案2:先删除缓存,再更新数据库
方案2:先删除缓存,再更新数据库问题1:请求A删缓存成功,写db业务耗时,此时请求B查询数据,会出现两个问题:
请求B从mysql中获取旧值
请求B获取的旧值写回redis
请求A更新完成,但是缓存缓存中是旧数据
1.5.4.1.2.1 分布式锁防止缓存击穿
解决方案1:阿里内部缓存击穿的方案
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
1.5.4.1.2.2 延时双删策略
优化写法,做成组件,通过aop切入。
问题:
这个删除该休眠多久呢?
统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,
以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
思考:即使写在读上加时间,在高并发下还是不能得到保证的。分析:若读将旧值写入缓存,写更新完db还未删除缓存,由于高并发读在写更新完db和未删除缓存空隙读取就是脏数据。
问题:写操作在读操作基础上加时间,降低了吞吐
解决方案:延时删除采用异步线程
上述效果是mysql单机,如果mysql主从读写分离架构如何?
1.5.4.1.3 方案3:先更新数据库,再删除缓存
解决方案:canal+mq方案
- 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
- 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
- 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
- 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。
1.5.4.1.4 总结对比
方案2和方案3用那个?利弊如何
优先使用方案3:先更新数据库,再删除缓存。理由如下:
方案2难点:
- 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,严重导致打满mysql。(为防止缓存击穿,读需加分布式锁)
- 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置
方案3保证的是最终一致性
若要保证强一致:读写接口使用读写锁
落地方案推荐:方案3,使用canal+mq的异步删除
1.5.4.2 缓存预热+缓存雪崩+缓存击穿+缓存穿透
缓存预热:热点数据放入缓存、经常使用的静态数据放入缓存
1.5.4.2.1 缓存雪崩
其他解决方案:
1.5.4.2.2 缓存穿透
解决方案1:空对象缓存或者缺省值
存在问题:黑客或恶意攻击
大量不同id打击,也会造成数据压力
方案2:布隆过滤器
方案2.1:Google布隆过滤器Guava解决缓存穿透(缺点:单机)
方案2.2:Redis布隆过滤器解决缓存穿透
基于布隆过滤器的白名单架构:
黑名单架构:
1.5.4.2.3 缓存击透
热点key失效,导致大量请求打到mysql
方案1:加分布式锁(互斥独占锁防止击穿),获得锁的请求会将查询数据放入缓存,后续请求查缓存
方案2:对于访问频繁的热点key,干脆就不设置过期时间
案例:淘宝聚划算功能实现+防止缓存击穿
秒杀商品分页展示
分析过程:
redis数据类型选型:list
采用定时器将参与聚划算活动的特价商品新增进入redis中:
问题:QPS上1000后导致可怕的缓存击穿
解决方案:定时轮询,互斥更新,差异失效时间
定时任务修改
Controller修改: