数据过期删除策略
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用
惰性删除
惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
优点 :对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。
定期删除
定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
定期清理有两种模式:
SLOW模式是定时任务,执行频率可以通过修改配置文件redis.conf 来调整。
FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
面试回答模板
Redis的数据过期策略有哪些 ?
背熟以下答案,大概用时1min。
在redis中提供了两种数据过期删除策略,第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们再检查其是否过期,如果过期,我们就删掉它,反之返回该key。
第二种是定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key。
定期删除存在两种模式:
SLOW模式是定时任务,任务执行频率可以通过修改配置文件redis.conf 来调整。
FAST模式执行频率不固定,但两次之间的间隔和每次耗时都有固定值。
Redis的过期删除策略实际是惰性删除和定期删除两种策略配合使用的。
分布式锁
Redis
实现分布式锁主要利用setnx
命令,是SET if not exists
(如果不存在,则设置,否则啥也不做)的简写。
setnx命令
获取锁
# 添加锁,NX是互斥、EX是设置超时时间
SET key value NX EX 10
# 或者直接添加锁
SET key value
- key:加锁的锁名;
- value:能够唯一标识锁的随机字符串;
- NX:只有当 key 对应的值不存在的时候才能 SET 成功;
- EX:过期时间设置(秒为单位)EX 10 标示这个锁有一个 10 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
一定要保证设置指定 key 的值和过期时间是一个原子操作! 不然的话,依然可能会出现锁无法被释放的问题。
释放锁
# 释放锁,删除即可
DEL key
Lua脚本
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
优缺点
使用setnx
获取锁,lua脚本释放锁的这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。
你或许在想:如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了。为此,出现了redisson
基于redisson看门狗机制(锁的监控和续期,推荐)
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗)
redis加锁时,如果不自己定义参数中的leaseTime(锁自动释放时间),或者定义的leaseTime为-1,那么就会开启看门狗机制。(不定义的话,这个线程默认的leaseTime就是30s)
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit);
看门狗机制就是当一个线程A加锁成功,且达到看门狗机制的触发条件时,会额外开一个线程,一般命名为Watch dog。
这个线程每隔(leaseTime/3,也就是默认为10s)的时间就为A续期时间,即重置过期时间。
在该锁有效期间,如果有其他线程想要试图加锁,那么就会不断while循环重试。
而采用了看门狗机制的锁的释放,是需要手动完成的,释放完成后还需要通知看门狗线程。
需要注意的是,加锁、设置过期时间等操作都是基于lua脚本完成的,因为能保证操作的原子性
代码模板
public void redisLock() throws InterruptedException{
RLock lock = redissonClient.getLock("heimalock");
try{
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
if(isLock){
System.out.println("执行业务……");
}
}finally{
lock.unlock();
}
}
可重入性
锁的可重入性指的是同一个线程在持有锁的情况下,能够多次获取该锁而不会发生死锁或阻塞的情况。
可重入锁的一个典型应用场景是在一个方法中调用另一个加锁的方法,譬如以下代码。
如果方法A在获取锁后调用了方法B,而方法B也需要获取同一个锁,那么如果锁是可重入的,方法B可以直接获取到锁,而不会因为锁已被方法A持有而发生死锁。
public void add1(){
RLock lock = redissonClient.getLock(“heimalock");
boolean isLock = lock.tryLock();
//执行业务
add2();
//释放锁
lock.unlock();
}
public void add2(){
RLock lock = redissonClient.getLock(“heimalock");
boolean isLock = lock.tryLock();
//执行业务
//释放锁
lock.unlock();
}
基于redisson实现的分布式锁是拥有可重入性的。
其中的KEY一般称为大键,可以根据自己的业务来命名。
field一般称为小键,是当前线程的唯一标识。
value记录着当前线程重入的次数。
主从一致性
对于多线程的主从一致性,实际上redisson没办法很好地保障。
像是原本如下运行,一个主库,两个从库,且java应用程序对主库进行加锁。
因为某种原因,主库短暂宕机,此时根据哨兵模式选举了下面的从库为新的主库。
此时又有个新的java应用来对这个新的主库进行加锁。那么就会出现两个线程同时持有一把锁的情况。
redlock红锁
为此,redisson提出了一个叫RedLock,红锁。
RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n / 2 + 1),避免在一个redis实例上加锁。即,在超过一半的实例上进行加锁。
其实就是让客户端向 Redis 集群中的实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
但是这种机制实现复杂,性能较差,运维起来也很繁琐。所以实际开发中很少用。
AP思想和CP思想
综上,对于多线程的主从一致性,实际上redis没办法很好地保障。因为redis本身是AP思想。
AP思想(可用性与分区容忍):
AP思想注重系统的可用性和分区容忍性,即在遇到网络分区或节点故障的情况下,系统仍然能够提供服务并保持可用性。这意味着系统可以容忍一定程度的数据不一致或冲突,以保证用户的访问体验。
如果想要强一致性,建议使用CP思想的zookeeper。
CP思想(一致性与分区容忍):
CP思想注重系统的一致性和分区容忍性,即在分布式环境中保持数据的一致性,并且即使发生网络分区,系统也能保持一致性。这种设计思想追求强一致性,确保所有节点中的数据都是一致的,但可能会导致一些节点不可用。
面试回答模板
Redis分布式锁如何实现 ?
候选人:嗯,在redis中提供了一个命令setnx(SET if not exists)
由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的
面试官:好的,那你如何控制Redis实现分布式锁有效时长呢?
候选人:嗯,的确,redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
面试官:好的,redisson实现的分布式锁是可重入的吗?
候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数
面试官:redisson实现的分布式锁能解决主从一致性的问题吗
候选人:这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
面试官:好的,如果业务非要保证数据的强一致性,这个该怎么解决呢?
**候选人:**嗯~,redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。