在一个分布式系统中,如何保证一个操作,同一时间只有一个线程可以执行,这就是分布式锁的使用场景,同一时间,只有一个线程可以获得锁的使用权。
如何实现一个分布式锁?
实现一个分布式锁,可以有以下3种方法。
一、基于数据库实现分布式锁。
1、在MySQL中,使用悲观锁"select from t where id = for update"可以对行数据进行加锁,来实现分布式锁。
2、同一个时间内,只会有一个线程加锁成功,其他线程必须等待。
3、数据库要保证是全局的,每一把锁所对应的行数据也是唯一的。
优点:
1、实现简单,方便。
缺点:
1、基于数据库的悲观锁,性能比较差。
2、等待中的线程是自旋的或者等待状态,需要等待持有锁的线程处理完,多个线程再一起去竞争同一把锁。
3、异常无法处理,当持有锁的线程还没有释放锁,意外退出,锁资源将无法释放,应用程序将无法继续进行。
二、基于redis实现的分布式锁。
1、使用redis的setnx命令,可以模拟分布式锁,setnx保证操作一个key值,如果没有则返回true,如果存在则返回false。
2、为了解决程序意外退出导致无法释放锁资源,需要给key增加一个超时时间。
3、redis提供了带参数的命令,可以保证设置key值和设置超时时间这2个操作的原子性
SET key value [EX seconds] [PX milliseconds] [NX|XX]
4、假如有如下情况:
A获取了锁,设置了超时时间为10秒,但是A执行了15秒,在10秒的时候锁失效了。
B在11秒的时候获取了锁,执行了5秒,那么A在15秒的时候会误删掉B的锁。
为了解决以上问题,每个加锁的线程都需要设置自己的value值,当删除的时候也要校验是自己的锁,才可以删除。
优点:
1、实现简单,方便。
2、基于redis的高性能,效率高。
缺点:
1、无法解决超时锁失效问题,逻辑处理的时间超过了设置的超时时间,那么这个时候就会导致,另一个也可以拿到锁继续执行。
为了解决这个问题,一般的方法是启动一个守护线程,时刻监控失效时间,当锁时间超过一定的执行时间比例之后,自动续约一定的时间,当然这个总时间是有最大阈值限制的。
2、等待中的线程是自旋的后者等待状态,需要等待持有锁的线程处理完,多个线程再一起去竞争同一把锁。
三、zookeeper实现分布式锁
方式1:悲观锁
1、使用临时节点实现分布式锁,第一个创建临时节点成功的线程,获取锁成功。
2、其他创建临时节点的线程会失败,那么监听锁的临时节点。
3、当锁释放的时候,删除临时节点,会通知到监听的线程,收到通知的线程继续尝试创建临时锁节点,谁创建成功谁获得锁。
优点:
解决了锁失效问题,通知机制可以完美解决,即使创建临时节点的线程挂掉,临时节点会自动删除。
缺点:
1、当有大量线程,等待锁资源的时候,锁资源释放会涉及到大量的通知,并且大量的线程需要一起竞争锁资源。
方式2:乐观锁
1、在一个目录下,各个线程创建顺序的临时节点,节点编号1、2、3、4、5等。
2、目录下创建的节点最小的线程获取锁。
3、等待锁资源的线程,不再一起全部监听锁节点,而是只监听比自己小的上一个节点。
4、当监听的比自己小的锁节点被删除后,继续改为监听上一个比自己小的节点。
4、当锁释放的时候,只需要通知监听锁节点的一个获几个线程,避免了大量的通知。
zookeeper实现的乐观锁,是比较合理的分布式锁方式,感兴趣的朋友可以用代码实现一下。