mysql 行锁 可重入_利用Zookeeper实现 - 分布式锁

等待Watcher监听,继续进入步骤2

Zookeeper羊群效应改进前后Watcher监听图

cef69a5d1f321159086b8e54c41214f2.png

Zookeeper羊群效应改进前后

基于Curator客户端实现分布式锁

Apache Curator是一个Zookeeper的开源客户端,它提供了Zookeeper各种应用场景(Recipe,如共享锁服务、master选举、分布式计数器等)的抽象封装,接下来将利用Curator提供的类来实现分布式锁。

Curator提供的跟分布式锁相关的类有5个,分别是:

Shared Reentrant Lock 可重入锁

Shared Lock 共享不可重入锁

Shared Reentrant Read Write Lock 可重入读写锁

Shared Semaphore 信号量

Multi Shared Lock 多锁

关于错误处理:还是强烈推荐使用ConnectionStateListener处理连接状态的改变。当连接LOST时你不再拥有锁。

可重入锁

Shared Reentrant Lock,全局可重入锁,所有客户端都可以请求,同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。它是由类 InterProcessMutex 来实现,它的主要方法: // 构造方法

public InterProcessMutex(CuratorFramework client, String path)

public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)

// 通过acquire获得锁,并提供超时机制:

public void acquire() throws Exception

public boolean acquire(long time, TimeUnit unit) throws Exception

// 撤销锁

public void makeRevocable(RevocationListener listener)

public void makeRevocable(final RevocationListener listener, Executor executor)

定义一个 FakeLimitedResource 类来模拟一个共享资源,该资源一次只能被一个线程使用,直到使用结束,下一个线程才能使用,否则会抛出异常 public class FakeLimitedResource {

private final AtomicBoolean inUse = new AtomicBoolean(false);

// 模拟只能单线程操作的资源

public void use() throws InterruptedException {

if (!inUse.compareAndSet(false, true)) {

// 在正确使用锁的情况下,此异常不可能抛出

throw new IllegalStateException("Needs to be used by one client at a time");

}

try {

Thread.sleep((long) (100 * Math.random()));

} finally {

inUse.set(false);

}

}

}

下面的代码将创建 N 个线程来模拟分布式系统中的节点,系统将通过 InterProcessMutex 来控制对资源的同步使用;每个节点都将发起10次请求,完成 请求锁--访问资源--再次请求锁--释放锁--释放锁 的过程;客户端通过 acquire 请求锁,通过 release 释放锁,获得几把锁就要释放几把锁;这个共享资源一次只能被一个线程使用,如果控制同步失败,将抛异常。 public class SharedReentrantLockTest {

private static final String lockPath = "/testZK/sharedreentrantlock";

private static final Integer clientNums = 5;

final static FakeLimitedResource resource = new FakeLimitedResource(); // 共享的资源

private static CountDownLatch countDownLatch = new CountDownLatch(clientNums);

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < clientNums; i++) {

String clientName = "client#" + i;

new Thread(new Runnable() {

@Override

public void run() {

CuratorFramework client = ZKUtils.getClient();

client.start();

Random random = new Random();

try {

final InterProcessMutex lock = new InterProcessMutex(client, lockPath);

// 每个客户端请求10次共享资源

for (int j = 0; j < 10; j++) {

if (!lock.acquire(10, TimeUnit.SECONDS)) {

throw new IllegalStateException(j + ". " + clientName + " 不能得到互斥锁");

}

try {

System.out.println(j + ". " + clientName + " 已获取到互斥锁");

resource.use(); // 使用资源

if (!lock.acquire(10, TimeUnit.SECONDS)) {

throw new IllegalStateException(j + ". " + clientName + " 不能再次得到互斥锁");

}

System.out.println(j + ". " + clientName + " 已再次获取到互斥锁");

lock.release(); // 申请几次锁就要释放几次锁

} finally {

System.out.println(j + ". " + clientName + " 释放互斥锁");

lock.release(); // 总是在finally中释放

}

Thread.sleep(random.nextInt(100));

}

} catch (Throwable e) {

System.out.println(e.getMessage());

} finally {

CloseableUtils.closeQuietly(client);

System.out.println(clientName + " 客户端关闭!");

countDownLatch.countDown();

}

}

}).start();

}

countDownLatch.await();

System.out.println("结束!");

}

}

控制台打印日志,可以看到对资源的同步访问控制成功,并且锁是可重入的 0. client#3 已获取到互斥锁

0. client#3 已再次获取到互斥锁

0. client#3 释放互斥锁

0. client#1 已获取到互斥锁

0. client#1 已再次获取到互斥锁

0. client#1 释放互斥锁

0. client#2 已获取到互斥锁

0. client#2 已再次获取到互斥锁

0. client#2 释放互斥锁

0. client#0 已获取到互斥锁

0. client#0 已再次获取到互斥锁

0. client#0 释放互斥锁

0. client#4 已获取到互斥锁

0. client#4 已再次获取到互斥锁

0. client#4 释放互斥锁

1. client#1 已获取到互斥锁

1. client#1 已再次获取到互斥锁

1. client#1 释放互斥锁

2. client#1 已获取到互斥锁

2. client#1 已再次获取到互斥锁

2. client#1 释放互斥锁

1. client#4 已获取到互斥锁

1. client#4 已再次获取到互斥锁

1. client#4 释放互斥锁

1. client#3 已获取到互斥锁

1. client#3 已再次获取到互斥锁

1. client#3 释放互斥锁

1. client#2 已获取到互斥锁

1. client#2 已再次获取到互斥锁

1. client#2 释放互斥锁

2. client#4 已获取到互斥锁

2. client#4 已再次获取到互斥锁

2. client#4 释放互斥锁

....

....

client#2 客户端关闭!

9. client#0 已获取到互斥锁

9. client#0 已再次获取到互斥锁

9. client#0 释放互斥锁

9. client#3 已获取到互斥锁

9. client#3 已再次获取到互斥锁

9. client#3 释放互斥锁

client#0 客户端关闭!

8. client#4 已获取到互斥锁

8. client#4 已再次获取到互斥锁

8. client#4 释放互斥锁

9. client#4 已获取到互斥锁

9. client#4 已再次获取到互斥锁

9. client#4 释放互斥锁

client#3 客户端关闭!

client#4 客户端关闭!

结束!

同时在程序运行期间查看Zookeeper节点树,可以发现每一次请求的锁实际上对应一个临时顺序节点 [zk: localhost:2181(CONNECTED) 42] ls /testZK/sharedreentrantlock

[leases, _c_208d461b-716d-43ea-ac94-1d2be1206db3-lock-0000001659, locks, _c_64b19dba-3efa-46a6-9344-19a52e9e424f-lock-0000001658, _c_cee02916-d7d5-4186-8867-f921210b8815-lock-0000001657]

不可重入锁

Shared Lock 与 Shared Reentrant Lock 相似,但是不可重入。这个不可重入锁由类 InterProcessSemaphoreMutex 来实现,使用方法和上面的类类似。

将上面程序中的 InterProcessMutex 换成不可重入锁 InterProcessSemaphoreMutex,如果再运行上面的代码,结果就会发现线程被阻塞在第二个 acquire 上,直到超时,也就是此锁不是可重入的。

控制台输出日志 0. client#2 已获取到互斥锁

0. client#1 不能得到互斥锁

0. client#4 不能得到互斥锁

0. client#0 不能得到互斥锁

0. client#3 不能得到互斥锁

client#1 客户端关闭!

client#4 客户端关闭!

client#3 客户端关闭!

client#0 客户端关闭!

0. client#2 释放互斥锁

0. client#2 不能再次得到互斥锁

client#2 客户端关闭!

结束!

把第二个获取锁的代码注释,程序才能正常执行 0. client#1 已获取到互斥锁

0. client#1 释放互斥锁

0. client#2 已获取到互斥锁

0. client#2 释放互斥锁

0. client#0 已获取到互斥锁

0. client#0 释放互斥锁

0. client#4 已获取到互斥锁

0. client#4 释放互斥锁

0. client#3 已获取到互斥锁

0. client#3 释放互斥锁

1. client#1 已获取到互斥锁

1. client#1 释放互斥锁

1. client#2 已获取到互斥锁

1. client#2 释放互斥锁

....

....

9. client#4 已获取到互斥锁

9. client#4 释放互斥锁

9. client#0 已获取到互斥锁

client#2 客户端关闭!

9. client#0 释放互斥锁

9. client#1 已获取到互斥锁

client#0 客户端关闭!

client#4 客户端关闭!

9. client#1 释放互斥锁

9. client#3 已获取到互斥锁

client#1 客户端关闭!

9. client#3 释放互斥锁

client#3 客户端关闭!

结束!

可重入读写锁

Shared Reentrant Read Write Lock,可重入读写锁,一个读写锁管理一对相关的锁,一个负责读操作,另外一个负责写操作;读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞);此锁是可重入的;一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁,这也意味着写锁可以降级成读锁, 比如 请求写锁 --->读锁 ---->释放写锁;从读锁升级成写锁是不行的。

可重入读写锁主要由两个类实现:InterProcessReadWriteLock、InterProcessMutex,使用时首先创建一个 InterProcessReadWriteLock 实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是 InterProcessMutex。 public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < clientNums; i++) {

final String clientName = "client#" + i;

new Thread(new Runnable() {

@Override

public void run() {

CuratorFramework client = ZKUtils.getClient();

client.start();

final InterProcessReadWriteLock lock = new InterProcessReadWriteLock(client, lockPath);

final InterProcessMutex readLock = lock.readLock();

final InterProcessMutex writeLock = lock.writeLock();

try {

// 注意只能先得到写锁再得到读锁,不能反过来!!!

if (!writeLock.acquire(10, TimeUnit.SECONDS)) {

throw new IllegalStateException(clientName + " 不能得到写锁");

}

System.out.println(clientName + " 已得到写锁");

if (!readLock.acquire(10, TimeUnit.SECONDS)) {

throw new IllegalStateException(clientName + " 不能得到读锁");

}

System.out.println(clientName + " 已得到读锁");

try {

resource.use(); // 使用资源

} finally {

System.out.println(clientName + " 释放读写锁");

readLock.release();

writeLock.release();

}

} catch (Exception e) {

System.out.println(e.getMessage());

} finally {

CloseableUtils.closeQuietly(client);

countDownLatch.countDown();

}

}

}).start();

}

countDownLatch.await();

System.out.println("结束!");

}

}

控制台打印日志 client#1 已得到写锁

client#1 已得到读锁

client#1 释放读写锁

client#2 已得到写锁

client#2 已得到读锁

client#2 释放读写锁

client#0 已得到写锁

client#0 已得到读锁

client#0 释放读写锁

client#4 已得到写锁

client#4 已得到读锁

client#4 释放读写锁

client#3 已得到写锁

client#3 已得到读锁

client#3 释放读写锁

结束!

信号量

Shared Semaphore,一个计数的信号量类似JDK的 Semaphore,JDK中 Semaphore 维护的一组许可(permits),而Cubator中称之为租约(Lease)。有两种方式可以决定 semaphore 的最大租约数,第一种方式是由用户给定的 path 决定,第二种方式使用 SharedCountReader 类。如果不使用 SharedCountReader,没有内部代码检查进程是否假定有10个租约而进程B假定有20个租约。所以所有的实例必须使用相同的 numberOfLeases 值.

信号量主要实现类有: InterProcessSemaphoreV2 - 信号量实现类

Lease - 租约(单个信号)

SharedCountReader - 计数器,用于计算最大租约数量

调用 acquire 会返回一个租约对象,客户端必须在 finally 中 close 这些租约对象,否则这些租约会丢失掉。但是,如果客户端session由于某种原因比如crash丢掉,那么这些客户端持有的租约会自动close,这样其它客户端可以继续使用这些租约。租约还可以通过下面的方式返还: public void returnLease(Lease lease)

public void returnAll(Collection leases)

注意一次你可以请求多个租约,如果 Semaphore 当前的租约不够,则请求线程会被阻塞。同时还提供了超时的重载方法。 public Lease acquire() throws Exception

public Collection acquire(int qty) throws Exception

public Lease acquire(long time, TimeUnit unit) throws Exception

public Collection acquire(int qty, long time, TimeUnit unit) throws Exception

一个Demo程序如下 public class SharedSemaphoreTest {

private static final int MAX_LEASE = 10;

private static final String PATH = "/testZK/semaphore";

private static final FakeLimitedResource resource = new FakeLimitedResource();

public static void main(String[] args) throws Exception {

CuratorFramework client = ZKUtils.getClient();

client.start();

InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, PATH, MAX_LEASE);

Collection leases = semaphore.acquire(5);

System.out.println("获取租约数量:" + leases.size());

Lease lease = semaphore.acquire();

System.out.println("获取单个租约");

resource.use(); // 使用资源

// 再次申请获取5个leases,此时leases数量只剩4个,不够,将超时

Collection leases2 = semaphore.acquire(5, 10, TimeUnit.SECONDS);

System.out.println("获取租约,如果超时将为null:" + leases2);

System.out.println("释放租约");

semaphore.returnLease(lease);

// 再次申请获取5个,这次刚好够

leases2 = semaphore.acquire(5, 10, TimeUnit.SECONDS);

System.out.println("获取租约,如果超时将为null:" + leases2);

System.out.println("释放集合中的所有租约");

semaphore.returnAll(leases);

semaphore.returnAll(leases2);

client.close();

System.out.println("结束!");

}

}

控制台打印日志 获取租约数量:5

获取单个租约

获取租约,如果超时将为null:null

释放租约

获取租约,如果超时将为null:[org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@3108bc, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@370736d9, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@5f9d02cb, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@63753b6d, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@6b09bb57]

释放集合中的所有租约

结束!

注意:上面所讲的4种锁都是公平锁(fair)。从ZooKeeper的角度看,每个客户端都按照请求的顺序获得锁,相当公平。

多锁

Multi Shared Lock 是一个锁的容器。当调用 acquire,所有的锁都会被 acquire,如果请求失败,所有的锁都会被 release。同样调用 release 时所有的锁都被 release(失败被忽略)。基本上,它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。

主要涉及两个类: InterProcessMultiLock - 对所对象实现类

InterProcessLock - 分布式锁接口类

它的构造函数需要包含的锁的集合,或者一组 ZooKeeper 的 path,用法和 Shared Lock 相同 public InterProcessMultiLock(CuratorFramework client, List paths)

public InterProcessMultiLock(List locks)

一个Demo程序如下 public class MultiSharedLockTest {

private static final String lockPath1 = "/testZK/MSLock1";

private static final String lockPath2 = "/testZK/MSLock2";

private static final FakeLimitedResource resource = new FakeLimitedResource();

public static void main(String[] args) throws Exception {

CuratorFramework client = ZKUtils.getClient();

client.start();

InterProcessLock lock1 = new InterProcessMutex(client, lockPath1); // 可重入锁

InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2); // 不可重入锁

// 组锁,多锁

InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));

if (!lock.acquire(10, TimeUnit.SECONDS)) {

throw new IllegalStateException("不能获取多锁");

}

System.out.println("已获取多锁");

System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());

System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());

try {

resource.use(); // 资源操作

} finally {

System.out.println("释放多个锁");

lock.release(); // 释放多锁

}

System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());

System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());

client.close();

System.out.println("结束!");

}

} 推荐阅读

代码对比工具,我就用这6个

分享我常用的5个免费的在线 SQL 数据库环境,简直太方便了!

Spring Boot 三招组合拳,手把手教你打出优雅的后端接口

MySQL 5.7 vs 8.0,你选那个?网友:我继续原地踏步~ 最后,推荐给大家一个有趣有料的公众号:写代码的渣渣鹏,7年老程序员教你写bug,回复”面试“有惊喜哦

扫码关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值