高级JAVA面试题详解(二)——Redis(分布式锁、重入锁、缓存数据一致性、缓存穿透/击穿/雪崩)

Redis详解 上篇

redis分布式锁如何实现

使用set(String key, String value, String nxxx, String expx, long time)方法;
方法参数详解:

  • key 锁的key值不做过多解释
  • value 很多同学会问弄个锁还要value干啥?value可以去控制谁能来解锁,或者用于重入锁来比对是否可重入。
  • nxxx 只能取NX或者XX:
    • NX 当Key不存在时set成功
    • XX 当key存在时set
  • pxex 只能取EX或者PX:代表数据过期时间的单位,用于描述time字段
    • EX单位秒
    • PX单位毫秒
  • time 过期时间,单位是expx所代表的单位

方法返回值详解: 返回字符串“OK” 代表成功。否则为插入失败

光知道这个方法是没用的,因为没有处理当锁被占用时的场景。
还需要设置锁自旋与超时。
自旋相当于多久尝试再次获取锁,
超时相当于自旋多久后依然未成功获取到锁则丢弃

设置一个超时时间假设timeout = 10000毫秒 每次自旋间隔为100毫秒
每次自旋timeout-=100; 当timeout<=0时结束自旋。代码如下

int timeout = 10000;
long expires = 60000L;
while(timeout >= 0) {
	if ("OK".equalsIgnoreCase(jedis.set(lockKey, lockValue, "NX", "PX", expires))) {
		return true;
	}
	timeout -= 100;
	Thread.sleep(100L);
}

扩展篇(韭菜课堂开课啦!)

很多博客里面有写分布式锁使用setnx + expire方法其实是错误的,因为无法保证整个操作的原子性。
如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁(一直不会被释放和过期)。
错误案例代码:

if (1L == jedis.setnx(lockKey, lockValue)) {
	// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
	jedis.expire(lockKey, expireTime);
}

如何实现重入锁呢?

首先我们来看看什么是重入锁

重入锁相当于给锁配了一把钥匙,任何持有对应钥匙的线程可在锁尚未释放的情况下开锁然后再次加锁。

知道重入锁的意思之后 根据上面的set方法是不是很容易就想到了解决方案?
思路:上面所说的set方法的value值可以设置为requestId(UUID或其他唯一标识),将requestId存入局部线程变量(ThreadLocal),当发现被锁时从ThreadLocal取出requestId(如果存在)尝试和锁的value值比较,如果相等,则返回true,可以重新设置失效时间。
是不是觉得到这里就完成了重入锁?然而并没有!
重入锁必须记录重复获取锁的次数。释放锁不能直接删除,因为锁是可重入的,如果锁进入了多次,在内层的某个业务执行结束出来就直接释放锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。

如何做到缓存和数据库的数据一致性呢

数据一致性这一块一定是在发生写的操作的情况下才需要去保证的,如果全是读自然不可能出现不一致的情况:
方案:先修改数据库再删除Redis缓存,且缓存一般都要设置失效时间,避免在极端情况下不一致的问题。

为什么要选择删除而不是直接修改? 为什么要把删除放在数据库写入的后面而不是前面?

  • 选择删除而不是直接修改
    如果在高并发情况下线程A执行写操作,成功更新数据库;
    这时候线程B同样执行和线程A一样的操作,但是在线程A执行更新缓存的过程中,线程B更新了新的数据库数据到缓存中;
    线程A在线程B全部操作完成以后才将相对老的数据又更新到了缓存中;当出现这种情况就导致缓存和数据库数据不一致的问题。
  • 什么要把删除放在数据库写入的后面而不是前面
    因为如果先删除在还没有写入数据库成功前有一个线程去查询就会把旧数据更新到缓存中出现数据库数据不一致的问题。

缓存穿透、缓存击穿、缓存雪崩解决方案

缓存穿透

  • 名词解释:用户想要查询一个key的数据,发现redis内存没有,于是查数据库,数据库也没有。当大量的请求去查这个key的时候就会对持久层造成很大的压力,这就是缓存穿透
  • 解决方案:
    1、缓存空对象,数据库不存在也到redis里保存一下,并设置过期时间。
    2、布隆过滤器,对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中(或者自己定义校验规则),就直接丢弃,不再对持久层查询。

缓存击穿

  • 名词解释:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,导致数据库压力暴增。
  • 解决方案:
    1、热点key永不失效
    2、互斥锁,查询缓存如果不存在就去获取锁,如果获取到了就去查数据库,查到的结果写入缓存,如果没有获取到锁就在一定时间后再去获取缓存。

缓存雪崩

  • 名词解释:缓存在短时间内大量失效或者redis突然挂了,导致数据库查询压力暴增,造成存储层也会挂掉的情况。
  • 解决方案:
    1、redis高可用 既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
    2、互斥锁 (同上)
    3、设置不同的过期时间,让缓存失效的时间点尽量均匀。

小伙子你终于聊到了redis集群,那么咱么来聊聊redis集群相关的问题吧,面试问问题嘛都是一路顺着问问到你不会或者面试官不会了就换一个方向。所以基本你的回答意味着接下来的问题可能从你的回答中出发。
集群的我下一篇再整理吧。

发布了18 篇原创文章 · 获赞 45 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/zhibo_lv/article/details/105203257