面试题:对Redis分布式锁有了解吗?在开发中遇到过哪些问题,怎么解决的?
前言
今天来分享一道比较好的面试题,“对Redis分布式锁有了解吗?在开发中遇到过哪些问题,怎么解决的?”对于这个问题,我们一起看看考察点和比较好的回答吧!
考察点
Redis作为企业级开发中经常使用的缓存中间件,几乎每个程序员都接触过或者听说过,这个问题就是面试官想考察我们对Reids分布式锁没有深刻的认识,以及日常开发中是否善于积累,认真思考。
回答
下面我从3个点方面来回答:
-
分布式锁的应用场景。
-
通过多线程并发案例说下Redis分布式锁可能遇到的问题。
-
使用Redission解决分布式锁出现的问题。
分布式锁的应用场景
-
防止缓存穿透,当redis中热点数据过期的时候,大量的线程访问数据库,可能导致数据库崩溃。
-
防止秒杀超卖,在秒杀场景中,库存数量同步给redis后,直接对redis 数据进行扣减,存在诸多原子性的安全问题。
-
双写一致性,缓存的数据被修改,导致数据库和缓存数据不一致。
-
接口幂等性,由于网络波动或者快速点击导致发出多次请求。
多线程并发案例说明
在分布式秒杀环境下,订单服务从库存中心拿库存数据,如果库存总数大于0,则进行库存扣减,并创建订单服务。订单服务负责创建订单,库存服务负责扣减库存。代码如下:
@Service
public class RedisServiceImpl implements RedisService{
@Resource
public RedisTemplate redisTemplate;
@Override
public void redisLock(){
Integer stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
if (stock>0){
int resultStock = stock-1;
redisTemplate.opsForValue().set("stock",resultStock+"");
System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
}else{
System.out.println("库存不足,抢购失败!");
}
}
}
当多个线程访问的时候会出现问题,不满足原子性。所以可以加单体锁,例如我们常用的Synchronized,ReentrantLock可以保证原子性安全,但是在分布式系统下,特别是集群部署的时候,这种单体锁根本锁不住!所以我们使用分布式锁,其核心逻辑是对共享资源保证互斥性,当获取锁失败的时候,获取锁的线程进行自旋,执行完毕之后删除分布式锁。Redis实现分布式锁的核心在于SETNX命令,如果键不存在,则将键设置为给定值,在这种情况下它等于SET;当键已存在时,不存在任何操作;成功时返回1,失败返回0。
//分布式锁的实现
@Override
public void redisLock2() {
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (lock){
int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
if (stock>0){
int resultStock = stock-1;
redisTemplate.opsForValue().set("stock",resultStock+"");
System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
}else{
System.out.println("库存不足,抢购失败!");
}
//删除锁
redisTemplate.delete("lock");
}else{
//没有得到锁,就自旋获取锁。
redisLock2();
}
}
上面这段代码就是说明分布式锁的实现,其核心逻辑就是对公共资源互斥,获取到锁执行完毕之后删除锁,没有获取到锁就自旋。
Redis分布式锁可能遇到的问题
死锁:如果某个线程在执行锁逻辑过程中宕机,导致没有删除锁,锁会一直得不到释放,其他线程会无法获取锁,造成死锁的情况。解决办法:①添加过期时间 ②原子性的添加过期时间
<------添加过期时间,避免死锁问题的发生----->
@Override
public void redisLock3() {
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
redisTemplate.expire("lock",30, TimeUnit.SECONDS);
if (lock){
int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
if (stock>0){
int resultStock = stock-1;
redisTemplate.opsForValue().set("stock",resultStock+"");
System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
}else{
System.out.println("库存不足,抢购失败!");
}
//删除锁
redisTemplate.delete("lock");
}else{
//没有得到锁,就自旋获取锁。
redisLock2();
}
}
<------原子性的设置过期时间,避免死锁问题的发生----->
//分布式锁的实现,添加原子性的过期时间,避免死锁问题的发生
@Override
public void redisLock4() {
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1",30,TimeUnit.SECONDS);
//redisTemplate.expire("lock",30, TimeUnit.SECONDS);
if (lock){
int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
if (stock>0){
int resultStock = stock-1;
redisTemplate.opsForValue().set("stock",resultStock+"");
System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
}else{
System.out.println("库存不足,抢购失败!");
}
//删除锁
redisTemplate.delete("lock");
}else{
//没有得到锁,就自旋获取锁。
redisLock2();
}
}
锁不住:
①如果某个线程在执行过程中,出现卡顿或者某个数据库查询很慢超过了锁的释放时间,锁会被释放,其他线程会获取到锁。解决办法:使用Redission解决 ②删除别人锁,当前线程A进入后由于超时,有其他线程B进入,此时redis中的锁是线程B的,而原来的线程A接着执行,线程A会删除掉别人的锁。解决办法:1.通过给当前线程绑定一个局部变量uuid,由于线程是和局部变量绑定之后,我们在删除之前判断一下,当前这把锁是自己的才进行删除。2.虽然方法1可行,但是存在获取uuid,比较uuid,删除锁这三个操作并不是原子性的操作,还是存在着安全性问题,所以引入lua表达式来进行处理。
使用uuid
//分布式锁的实现,使用uuid,避免被其他人删锁
@Override
public void redisLock5() {
String uuidLock = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuidLock,30,TimeUnit.SECONDS);
//redisTemplate.expire("lock",30, TimeUnit.SECONDS);
if (lock){
int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
if (stock>0){
int resultStock = stock-1;
redisTemplate.opsForValue().set("stock",resultStock+"");
System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
}else{
System.out.println("库存不足,抢购失败!");
}
//删除锁
String redisLock = redisTemplate.opsForValue().get("lock").toString();
if (uuidLock.equals(redisLock)){
redisTemplate.delete("lock");
}
}else{
//没有得到锁,就自旋获取锁。
redisLock2();
}
}
但是上面这个存在一些问题,获取uuid,比较uuid,删除锁不符合原子性!使用lua表达式:
//分布式锁的实现,使用lua表达式,避免被其他人删锁
@Override
public void redisLock6() {
String uuidLock = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuidLock,30,TimeUnit.SECONDS);
//redisTemplate.expire("lock",30, TimeUnit.SECONDS);
if (lock){
int stock = Integer.parseInt((String) redisTemplate.opsForValue().get("stock"));
if (stock>0){
int resultStock = stock-1;
redisTemplate.opsForValue().set("stock",resultStock+"");
System.out.println("库存充足,抢购成功!库存剩余:"+resultStock);
}else{
System.out.println("库存不足,抢购失败!");
}
String lua="if redis.call('get',KEYS[1]==ARGV[1]) then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(lua,Long.class), Arrays.asList("lock"),uuidLock);
}else{
//没有得到锁,就自旋获取锁。
redisLock2();
}
}
过期时间不符合业务需求:
在开发中更多的时候是不知道程序具体执行多久的,所以对于设置锁的过期时间上不能很精确,所以会出现安全问题。解决方法:使用Redission,Redission是分布式的Redis锁的解决技术,提供了简单高效的命令供我们使用。可以很好的解决Redis分布式锁的各种问题。Redission是一个可重入锁(ReentrantLock),同时具有看门狗机制,可以对线程获取锁的情况进行很好的控制,监视线程的运行情况,进行时间续期等。使用起来也很简单:
引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
配置类:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置类
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.150.101:6379")
.setPassword("123321");
// 创建客户端
return Redisson.create(config);
}
}
使用:
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
redission的特性:
redission会默认给我们设置过期时间。
redission能够自动续期
redission不会已发死锁问题(引入看门狗机制,会监控锁的状态,如果某一个线程宕机了,不会在进行续期,导致死锁问题)
redission不会删除别人的锁,会通过抛出异常的方式解决。
redission的使用很简单。
以上就是我对于这个问题的理解。
总结
这个问题主要是考察求职者对Redis分布式锁的理解能力。在实际应用中,Redis分布式锁非常实用,也是最常用的功能,对于这部分内容如果理解不够深刻,很容易写出低效代码。希望读完这篇文章你有所收获。欢迎转发,关注微信公众号:程序员的故事,了解更多精彩面试题。