使用Redis缓存存在的问题
使用redis缓存的初衷:缓解数据库压力,提高性能。
1、缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
2、缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
3、缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
案例:
优惠券秒杀问题
超卖问题:多线程存在安全问题
如下图所示
当线程1查询完库存大于0 还未扣减库存的时候,线程2也查询库存大于0 然后一起扣减 就会出现库存为负数的情况。
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
-
悲观锁:添加同步锁,让线程串行执行
优点:简单粗暴
缺点:性能一般 -
乐观锁:不加锁,在更新时判断是否有其它线程在修改
优点:性能好
缺点:存在成功率低的问题
一人一单问题
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
流程图变更如下
可通过加锁解决(单机情况):
同一个用户一把锁 ,注意:userId.toString() 每次都会new出来,需要使用userId.toString().intern(),才能锁住。
在大多数情况下 都是集群部署 通过负载均衡
通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
每个服务都会有单独的JVM,JVM中的锁监视器只能锁本JVM。导致两个服务没有被同一把锁锁住。
解决方法
分布式锁(经典加一层)
分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:
这里将使用Redis进行分布式锁
Redis中SetNX命令 SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
故可以利用它来实现锁的效果。
误删锁问题
当线程1获取到锁之后,业务阻塞,造成锁的超时释放,此时线程2进来便可以拿到锁,然后线程1继续执行,就可以把线程2的锁删除掉。
原来业务流程:
解决方法:在删除锁之前先看看锁是不是自己的。
改进Redis的分布式锁
但是上面误删问题仍可能在极端情况下发生;
例如:让线程1验证完是自己的锁的时候,被阻塞(垃圾回收机制),此时超时释放锁,然后线程2便进入了锁,线程1唤醒后,执行删除锁的业务,便也造成误删问题。
解决方法:只需要保证 验证锁是否是自己的和删除锁是一个原子性的操作即可。
这里我们将使用Lua脚本。
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
一人一单问题总结
-
基于Redis的分布式锁实现思路:
利用set nx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁 -
特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性
基于setnx实现的分布式锁存在下面的问题:
Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
- 不可重入Redis分布式锁:
原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
缺陷:不可重入、无法重试、锁超时失效 - 可重入的Redis分布式锁:
原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
缺陷:redis宕机引起锁失效问题 - Redisson的multiLock:
原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
缺陷:运维成本高、实现复杂