分布式锁的应用
分布式锁服务宕机, ZooKeeper 一般是以集群部署, 如果出现 ZooKeeper 宕机, 那么只要当前正常的服务器超过集群的半数, 依然可以正常提供服务
持有锁资源服务器宕机, 假如一台服务器获取锁之后就宕机了, 那么就会导致其他服务器无法再获取该锁. 就会造成 死锁 问题, 在 Curator 中, 锁的信息都是保存在临时节点上, 如果持有锁资源的服务器宕机, 那么 ZooKeeper 就会移除它的信息, 这时其他服务器就能进行获取锁操作。
当然了分布式锁还可以基于redis实现,其string类型的 setnx key value命令 结合expire命令。
前面的准备工作:
package Lock; import java.util.concurrent.TimeUnit; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.recipes.locks.InterProcessLock; import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex; import org.apache.curator.retry.ExponentialBackoffRetry; import org.apache.curator.utils.CloseableUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class DistributedLockDemo { // ZooKeeper 锁节点路径, 分布式锁的相关操作都是在这个节点上进行 private final String lockPath = "/distributed-lock"; // ZooKeeper 服务地址, 单机格式为:(127.0.0.1:2181), // 集群格式为:(127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183) private String connectString; // Curator 客户端重试策略 private RetryPolicy retry; // Curator 客户端对象 private CuratorFramework client; // client2 用户模拟其他客户端 private CuratorFramework client2; // 初始化资源 @Before public void init() throws Exception { // 设置 ZooKeeper 服务地址为本机的 2181 端口 connectString = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"; // 重试策略 // 初始休眠时间为 1000ms, 最大重试次数为 3 retry = new ExponentialBackoffRetry(1000, 3); // 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为链接超时时间 client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry); client2 = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry); // 创建会话 client.start(); client2.start(); } // 释放资源 @After public void close() { CloseableUtils.closeQuietly(client); } }
zookeper的实现主要有下面四类类:
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
1.共享锁,不可重入--- InterProcessSemaphoreMutex
InterProcessSemaphoreMutex是一种不可重入的互斥锁,也就意味着即使是同一个线程也无法在持有锁的情况下再次获得锁,所以需要注意,不可重入的锁很容易在一些情况导致死锁。
@Test public void sharedLock() throws Exception { // 创建共享锁 final InterProcessLock lock = new InterProcessSemaphoreMutex(client, lockPath); // lock2 用于模拟其他客户端 final InterProcessLock lock2 = new InterProcessSemaphoreMutex(client2, lockPath); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { lock.acquire(); System.out.println("1获取锁==============="); // 测试锁重入 Thread.sleep(5 * 1000); lock.release(); System.out.println("1释放锁==============="); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { lock2.acquire(); System.out.println("2获取锁==============="); Thread.sleep(5 * 1000); lock2.release(); System.out.println("2释放锁==============="); } catch (Exception e) { e.printStackTrace(); } } }).start(); Thread.sleep(20 * 1000); }
2.共享可重入锁--- InterProcessMutex
此锁可以重入,但是重入几次需要释放几次。
@Test public void sharedReentrantLock() throws Exception { // 创建共享锁 final InterProcessLock lock = new InterProcessMutex(client, lockPath); // lock2 用于模拟其他客户端 final InterProcessLock lock2 = new InterProcessMutex(client2, lockPath); final CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { lock.acquire(); System.out.println("1获取锁==============="); // 测试锁重入 lock.acquire(); System.out.println("1再次获取锁==============="); Thread.sleep(5 * 1000); lock.release(); System.out.println("1释放锁==============="); lock.release(); System.out.println("1再次释放锁==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { lock2.acquire(); System.out.println("2获取锁==============="); // 测试锁重入 lock2.acquire(); System.out.println("2再次获取锁==============="); Thread.sleep(5 * 1000); lock2.release(); System.out.println("2释放锁==============="); lock2.release(); System.out.println("2再次释放锁==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); countDownLatch.await(); }
结果:
1获取锁===============
1再次获取锁===============
1释放锁===============
1再次释放锁===============
2获取锁===============
2再次获取锁===============
2释放锁===============
2再次释放锁===============
原理:
InterProcessMutex通过在zookeeper的某路径节点下创建临时序列节点来实现分布式锁,即每个线程(跨进程的线程)获取同一把锁前,都需要在同样的路径下创建一个节点,节点名字由uuid + 递增序列组成。而通过对比自身的序列数是否在所有子节点的第一位,来判断是否成功获取到了锁。当获取锁失败时,它会添加watcher来监听前一个节点的变动情况,然后进行等待状态。直到watcher的事件生效将自己唤醒,或者超时时间异常返回。
3.共享可重入读写锁
读锁和读锁不互斥,只要有写锁就互斥。
@Test public void sharedReentrantReadWriteLock() throws Exception { // 创建共享可重入读写锁 final InterProcessReadWriteLock locl1 = new InterProcessReadWriteLock(client, lockPath); // lock2 用于模拟其他客户端 final InterProcessReadWriteLock lock2 = new InterProcessReadWriteLock(client2, lockPath); // 获取读写锁(使用 InterProcessMutex 实现, 所以是可以重入的) final InterProcessLock readLock = locl1.readLock(); final InterProcessLock readLockw = lock2.readLock(); final CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { readLock.acquire(); System.out.println("1获取读锁==============="); // 测试锁重入 readLock.acquire(); System.out.println("1再次获取读锁==============="); Thread.sleep(5 * 1000); readLock.release(); System.out.println("1释放读锁==============="); readLock.release(); System.out.println("1再次释放读锁==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { Thread.sleep(500); readLockw.acquire(); System.out.println("2获取读锁==============="); // 测试锁重入 readLockw.acquire(); System.out.println("2再次获取读锁=============="); Thread.sleep(5 * 1000); readLockw.release(); System.out.println("2释放读锁==============="); readLockw.release(); System.out.println("2再次释放读锁==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); countDownLatch.await(); }
结果:
1获取读锁===============
1再次获取读锁===============
2获取读锁===============
2再次获取读锁==============
1释放读锁===============
2释放读锁===============
1再次释放读锁===============
2再次释放读锁===============
4. 共享信号量
@Test public void semaphore() throws Exception { // 创建一个信号量, Curator 以公平锁的方式进行实现 final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 1); final CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { // 获取一个许可 Lease lease = semaphore.acquire(); logger.info("1获取读信号量==============="); Thread.sleep(5 * 1000); semaphore.returnLease(lease); logger.info("1释放读信号量==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { // 获取一个许可 Lease lease = semaphore.acquire(); logger.info("2获取读信号量==============="); Thread.sleep(5 * 1000); semaphore.returnLease(lease); logger.info("2释放读信号量==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); countDownLatch.await(); }
结果:
09:39:26 [Lock.DistributedLockDemo]-[INFO] 2获取读信号量===============
09:39:32 [Lock.DistributedLockDemo]-[INFO] 2释放读信号量===============
09:39:32 [Lock.DistributedLockDemo]-[INFO] 1获取读信号量===============
09:39:37 [Lock.DistributedLockDemo]-[INFO] 1释放读信号量===============
当然可以一次获取多个信号量:
@Test public void semaphore() throws Exception { // 创建一个信号量, Curator 以公平锁的方式进行实现 final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, lockPath, 3); final CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { // 获取2个许可 Collection<Lease> acquire = semaphore.acquire(2); logger.info("1获取读信号量==============="); Thread.sleep(5 * 1000); semaphore.returnAll(acquire); logger.info("1释放读信号量==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { // 获取1个许可 Collection<Lease> acquire = semaphore.acquire(1); logger.info("2获取读信号量==============="); Thread.sleep(5 * 1000); semaphore.returnAll(acquire); logger.info("2释放读信号量==============="); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); countDownLatch.await(); }
结果:
09:46:53 [Lock.DistributedLockDemo]-[INFO] 2获取读信号量===============
09:46:53 [Lock.DistributedLockDemo]-[INFO] 1获取读信号量===============
09:46:58 [Lock.DistributedLockDemo]-[INFO] 2释放读信号量===============
09:46:58 [Lock.DistributedLockDemo]-[INFO] 1释放读信号量===============
5.多重共享锁
@Test public void multiLock() throws Exception { // 可重入锁 final InterProcessLock interProcessLock1 = new InterProcessMutex(client, lockPath); // 不可重入锁 final InterProcessLock interProcessLock2 = new InterProcessSemaphoreMutex(client2, lockPath); // 创建多重锁对象 final InterProcessLock lock = new InterProcessMultiLock(Arrays.asList(interProcessLock1, interProcessLock2)); final CountDownLatch countDownLatch = new CountDownLatch(1); new Thread(new Runnable() { @Override public void run() { // 获取锁对象 try { // 获取参数集合中的所有锁 lock.acquire(); // 因为存在一个不可重入锁, 所以整个 InterProcessMultiLock 不可重入 System.out.println(lock.acquire(2, TimeUnit.SECONDS)); // interProcessLock1 是可重入锁, 所以可以继续获取锁 System.out.println(interProcessLock1.acquire(2, TimeUnit.SECONDS)); // interProcessLock2 是不可重入锁, 所以获取锁失败 System.out.println(interProcessLock2.acquire(2, TimeUnit.SECONDS)); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } } }).start(); countDownLatch.await(); }
结果:
false
true
false