面试题首页 > Java分布式面试题

分布式锁面试题

001有哪些方案实现分布式锁?

1)基于数据库做分布式锁--乐观锁(基于版本号)和悲观锁(基于排它锁) 
2)基于redis做分布式锁:setnx(key,当前时间+过期时间)和Redlock机制 
3)基于zookeeper做分布式锁:临时有序节点来实现的分布式锁,Curator 
4)基于 Consul 做分布式锁 

002MySQL如何做分布式锁?

基于数据库(MySQL)的分布式锁方案,一般分为3类:基于表记录、乐观锁和悲观锁。

003基于表记录的数据库锁的原理。

该方法是最简单的,就是直接创建一张锁表。当我们想要获得锁的时候,就可以在该锁表中增加一条记录,想要释放锁的时候就删除锁表的这条记录。
总结:
1.这种锁没有失效时间,一旦释放锁操作失败就会导致锁记录一直在数据库中,其它线程无法获得锁。这个缺陷也很好解决,比如可以做一个定时任务去定时清理。
2.这种锁的可靠性依赖于数据库。建议设置备库,避免单点,进一步提高可靠性。
3.这种锁是非阻塞的。因为插入数据失败之后会直接报错,想要获得锁就需要再次操作。如果需要阻塞式的,可以弄个for循环、while循环之类的,直至INSERT成功再返回。
4.这种锁是非可重入的。因为数据库中锁表的一份记录就是一把锁,想要实现可重入锁,可以在数据库中添加一些字段,比如获得锁的主机信息、线程信息等,那么在再次获得锁的时候可以先查询数据,如果当前的主机信息和线程信息等能被查到的话,可以直接把锁分配给它。

004分布式中乐观锁的原理。

乐观锁大多数是基于数据版本(version)的记录机制实现的。通过对数据库表添加一个 “version”字段来实现的。读数据时会将此版本号一同读出,之后更新数据时会对此版本号加1。在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行更新操作;如果版本号不一致,则执行不会更新。
当然借助更新时间戳(updated_at)也可以实现乐观锁,和采用version字段的方式相似:更新操作执行前先记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间戳相等。

005乐观锁的优点和缺点?

乐观锁的优点:由于在检测数据冲突时并不依赖数据库本身的锁机制,不会影响请求的性能,当产生并发且并发量较小的时候只有少部分请求会失败。
乐观锁的缺点:需要对表的设计增加额外的字段,增加了数据库的冗余。另外,当应用并发量高的时候,version值在频繁变化,会对数据库产生很大的写压力。并且也会导致大量请求失败,影响系统的可用性。所以数据库乐观锁比较适合并发量不高,并且写操作不频繁的场景。

006悲观锁的实现原理。

悲观锁是数据库中自带的。在查询语句后面增加FOR UPDATE,数据库会在查询过程中给数据库表增加悲观锁,也称排他锁。悲观锁就会比较悲观,总是假设最坏的情况,它认为数据的更新在大多数情况下是会产生冲突的。
在使用悲观锁的同时,我们需要注意一下锁的级别。MySQL InnoDB在加锁的时候,只有明确地指定主键(或索引)的才会执行行锁 (只锁住被选取的数据),否则将会执行表锁(将整个数据表单给锁住)。
在使用悲观锁时,我们必须关闭MySQL数据库的自动提交属性(参考下面的示例),因为MySQL默认使用autocommit(自动提交)模式。这样在使用FOR UPDATE获得锁之后可以执行相应的业务逻辑,执行完之后再使用COMMIT来释放锁。

007悲观锁的优点和缺点?

悲观锁优点:可以严格保证数据访问的安全。
悲观锁缺点:即每次请求都会额外产生加锁的开销且未获取到锁的请求将会阻塞等待锁的获取,在高并发环境下,容易造成大量请求阻塞,影响系统可用性。另外,悲观锁使用不当还可能产生死锁的情况。

008基于 ZooKeeper 的分布式锁实现原理是什么?

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,Zookeeper在本质上就像一个文件管理系统。其用类似文件路径的方式管理来监听多个节点(Znode),同时判断当前每个节点上机器的状态(是否宕机、是否断开连接等),从而达到分布式协同的操作。
ZooKeeper 可以根据有序节点+watch实现,实现思路,如:为每个线程生成一个有序的临时节点,为确保有序性,在排序一次全部节点,获取全部节点,每个线程判断自己是否最小,如果是的话,获得锁,执行操作,操作完删除自身节点。如果不是第一个的节点则监听它的前一个节点,当它的前一个节点被删除时,则它会获得锁,以此类推。

009ZooKeeper和Reids做分布式锁的区别?

1. Redis分布式锁需要不断去尝试获取锁,比较消耗性能。而ZooKeeper分布式锁,获取不到锁会注册个监听器,不需要不断主动尝试获取锁因此性能开销较小;
2. 如果是Redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而ZooKeeper的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁;

010Redis分布式锁之SETNX + EXPIRE

SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。
伪代码实现如下:假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值

if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁     
    expire(key_resource_id,100);//设置过期时间     
    try {         
        do something  //业务请求     
    }catch(){   
    }finally {        
        jedis.del(key_resource_id); //释放锁     
    }
}

缺点:setnx和expire两个命令分开了,不是原子操作。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就永远释放不了,别的线程永远获取不到锁啦。

011Redis分布式锁之SETNX + (系统时间+过期时间)

为了解决发生异常锁得不到释放的场景,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。加锁代码如下:

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);

// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}     
//其他情况,均返回加锁失败
return false;
}

缺点:
1.过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。
2.如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,因此某个客户端加的锁可能被别的客户端所覆盖。
3.该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。

012Redis分布式锁之SET扩展命令。

SET key value[EX seconds][PX milliseconds][NX|XX]
● EX seconds :设定key的过期时间,时间单位是秒。
● PX milliseconds: 设定key的过期时间,单位为毫秒。
● NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
● XX: 仅当key存在时设置值;

if(jedis.set(key_resource_id,lock_value,"NX","EX",100s)==1){//加锁  
    try{ 
        dosomething  
        //业务处理     
    }catch(){   
    }finally {        
        jedis.del(key_resource_id); //释放锁     
    } 
}

缺点:
问题一:锁过期释放了,业务还没执行完。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
问题二:锁被别的线程误删。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。

013Redis分布式锁之SET EX PX NX + 校验唯一随机值,再删除。

既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下:

if(jedis.set(key_resource_id,uni_request_id,"NX","EX",100s)==1){ 
    //加锁     
    try {         
        do something  //业务处理     
    }catch(){   
    }finally {        
        //判断是不是当前线程加的锁,是才释放        
        if (uni_request_id.equals(jedis.get(key_resource_id))) {         
            jedis.del(lockKey); //释放锁         
        }     
    } 
}

014Redis分布式锁之Redisson框架

方案四中还是可能存在锁过期释放,业务没执行完的问题。开源框架Redisson解决了这个问题。先来看下Redisson底层原理图:

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此Redisson解决了锁过期释放,业务没执行完问题。

015Redis多机版分布式锁之Redlock+Redisson

前面五种方案都是基于单机版的,其实Redis一般都是集群部署的。为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。

目录

返回顶部