分布式锁的概念
在我们进行单机应用开发,涉汲并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题。
但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题。
那么就需要一种更加高级的锁机制,来处理这种跨机器的进程之间的数据同步问题——这就是分布式锁。
分布式锁常见的实现方式
1. 使用Redis或者MemCache缓存实现:高效,但不稳定。
2. 使用zookeeper实现:三者中最为稳定的一种方式。
3. 数据库的乐观锁和悲观锁实现:性能最低。
Zookeeper分布式锁原理
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点。
1. 客户端获取锁时,在lock节点下创建临时顺序节点。
那么问题来了,为什么需要临时顺序节点呢?
首先说一下,为什么需要“临时”?
假设,client1在服务端创建了一个节点,拿到了锁。然后,如果client1创建的是一个持久化节点,client1发生故障挂了,就无法删除掉客户端的节点,也就无法释放锁,导致其他节点无法获取到锁。如果这是一个临时节点,即使服务挂掉,到了时间也能被自动删除,从而把锁释放。
其次,为什么需要“顺序”?
比如3个client都同时去竞争锁,然后创建了3个节点。决定谁拿到锁,就取决于谁的节点序号最小。因此节点设置成“顺序”,是用于决定谁拿到了锁。
2. 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
3. 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
4. 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
总结
当多个服务去竞争锁时,每个服务就会分别在zookeeper服务端创建子节点,然后判断谁的子节点序号最小,序号最小者,则抢锁成功。当锁被释放后,就会重复刚刚的步骤。
Curator实现分布式锁API
在Curator中有五种锁方案:
- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
- InterProcessMutex:分布式可重入排它锁
- InterProcessReadWriteLock: 分布式读写锁
- InterProcessMultilock: 将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
什么是可重入?什么是非可重入?
可重入,是指某个线程已经获得资源并上了锁,如果还需要使用这个资源,再次上锁使用即可,无需把锁释放。重入了几次,将来释放锁就要几次。
非可重入,就是使用完资源之后,必须马上释放锁,并重新和其他线程竞争锁。
案例:模拟12306售票
假设有三款旅游APP都有提供卖票服务,但是真正出票的地方都是从12306来的,相当于是做一个“代理商”的角色。那么这三款APP,都需要调用12306提供的服务接口,来完成“购票”操作。
那么“锁”应该加在三个旅游APP,还是12306集群上?
答案应该是加在12306的集群上,因为让三个APP访问同一个zookeeper服务器是非常不现实的。资源在谁手上,锁就加在哪里。
代码实战
加锁前
public class Ticket12306 implements Runnable{
// 车票数量
private int tickets = 20;
@Override
public void run() {
// 减少票数
while(true){
if(tickets > 0){
System.out.println(Thread.currentThread() + ":" + tickets);
tickets--;
}else {
break;
}
}
}
public static void main(String[] args) {
Ticket12306 ticket = new Ticket12306();
Thread t1 = new Thread(ticket, "A");
Thread t2 = new Thread(ticket, "B");
Thread t3 = new Thread(ticket, "C");
t1.start();
t2.start();
t3.start();
}
}
在未加锁的情况下,出现了多个线程抢到同一张票的情况,这明显不符合我们的预期。
加锁后
public class Ticket12306 implements Runnable{
// 车票数量
private int tickets = 20;
public static final String ZOOKEEPER_SERVER_IP = "192.168.59.128:2181";
private InterProcessMutex lock;
public Ticket12306(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(ZOOKEEPER_SERVER_IP)
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
.build();
// 启动客户端
client.start();
// 创建锁,获取锁创建的子节点暂时存储在/lock下
this.lock = new InterProcessMutex(client, "/lock");
}
@Override
public void run() {
while(true){
try{
// 获取锁 每过三秒钟尝试去获取一次锁
lock.acquire(3, TimeUnit.SECONDS);
// 减少票数
if(tickets > 0){
System.out.println(Thread.currentThread() + ":" + tickets);
tickets--;
}else {
break;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
// 释放锁
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Ticket12306 ticket = new Ticket12306();
Thread t1 = new Thread(ticket, "A");
Thread t2 = new Thread(ticket, "B");
Thread t3 = new Thread(ticket, "C");
t1.start();
t2.start();
t3.start();
}
}
加锁后,没有了多个线程买到同一张票的情况出现,而且每个线程买到票的机会都非常平均。
出现这个异常信息,是因为有的线程抢不到锁报的异常,可以不用理会。
在服务端中,我们也可以看到,我们原本没有/lock节点,程序启动之后会自动帮我们创建。程序结束之后,/lock节点过一会也没了,说明/lock是一个临时节点。