在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
与单机模式下的锁相比,不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。分布式场景下问题复杂性的根源之一主要就是需要考虑到网络的延时和不可靠性。
分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。
原理:上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 。
步骤:
1)每个zk客户端都在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。
2)判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
3)当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
4)取锁成功则执行代码,最后释放锁(删除该节点)。
引入相关POM依赖
com.101tec
zkclient
0.10
org.apache.zookeeper
zookeeper
3.4.8
package com.zpc.zklock;
/**
* 面向接口编程
*/
public interface Lock {
/**
* 获取锁
*/
public void getLock();
/**
* 释放锁
*/
public void unLock();
}
package com.zpc.zklock;
import org.I0Itec.zkclient.ZkClient;
import java.util.concurrent.CountDownLatch;
/**
* 公共部分抽象出来
*/
public abstract class ZkAbstractLock implements Lock {
private static final String CONNECTION_STRING = "127.0.0.1:2181";
/**
* 分布式环境下,每个应用节点都作为一个zk客户端
*/
protected ZkClient zkClient = new ZkClient(CONNECTION_STRING);
/**
* 在zookeeper中创建的节点名称
*/
protected static final String PATH = "/lock";
/**
* 计数器
*/
protected CountDownLatch countDownLatch;
@Override
public void getLock() {
if (tryLock()) {
System.out.println("--------获取锁成功--------");
} else {
System.out.println("--------等待锁--------");
waitLock();
System.out.println("-------等待线程唤醒--------");
getLock();
}
}
protected abstract void waitLock();
protected abstract boolean tryLock();
@Override
public void unLock() {
if (zkClient != null) {
zkClient.close();
}
System.out.println("--------释放锁成功--------");
}
}
package com.zpc.zklock;
import org.I0Itec.zkclient.IZkDataListener;
import java.util.concurrent.CountDownLatch;
public class ZkDistributeLock extends ZkAbstractLock {
@Override
protected void waitLock() {
//监听zookeeper的lock节点
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
if (countDownLatch != null) {
//线程唤醒
countDownLatch.countDown();
}
}
};
//让客户端监听节点信息
zkClient.subscribeDataChanges(PATH,iZkDataListener);
if (zkClient.exists(PATH)) {
countDownLatch = new CountDownLatch(1);
try {
//线程等待
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
//取消监听
zkClient.unsubscribeDataChanges(PATH,iZkDataListener);
}
/**
* 创建临时节点成功即认为获取锁成功
*
* @return
*/
@Override
protected boolean tryLock() {
try {
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
return false;
}
}
}
package com.zpc.zklock;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
/**
* 订单业务逻辑
*/
public class OrderService implements Runnable {
CountDownLatch latch;
/**
* 搞一个全局变量,以更好地演示效果
*/
private static int count=0;
Lock lock = new ZkDistributeLock();
public OrderService(CountDownLatch latch){
this.latch=latch;
}
@Override
public void run() {
try {
lock.getLock();
String orderId = getOrderId();
System.out.println("订单号:"+orderId);
for(int i=0;i<999999;i++){
//模拟耗时长的业务逻辑
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unLock();
latch.countDown();
}
}
public String getOrderId() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(new Date())+"-"+ (++count);
}
}
package com.zpc.zklock;
import java.util.concurrent.CountDownLatch;
/**
* 模拟分布式环境下应用程序生成订单号(通过加锁机制保证全局唯一)
*/
public class Client1 {
private static final int THREADNUM=100;
public static void main(String[] args) throws Exception {
//计数器,用来控制主线程挂起,所有线程执行完成后再执行主线程
CountDownLatch latch=new CountDownLatch(THREADNUM);
long start = System.currentTimeMillis();
//开启多个线程模拟分布式环境下多个不同的客户端去获取订单号
for (int i = 0; i < THREADNUM; i++) {
new Thread(new OrderService(latch)).start();
}
latch.await();
System.out.println("cost:"+(System.currentTimeMillis()-start));
}
}
package com.zpc.zklockv2;
import com.zpc.zklock.Lock;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
/**
* 订单业务逻辑
*/
public class OrderService implements Runnable {
CountDownLatch latch;
/**
* 搞一个全局变量,以更好地演示效果
*/
private static int count = 0;
Lock lock;
public OrderService(CountDownLatch latch, Lock lock) {
this.latch = latch;
this.lock = lock;
}
@Override
public void run() {
try {
lock.getLock();
//获得锁后的业务逻辑(通常是操作某个共享资源)
String orderId = getOrderId();
System.out.println("订单号:" + orderId);
for(int i=0;i<99999999;i++){
//模拟耗时的业务逻辑
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unLock();
latch.countDown();
}
}
public String getOrderId() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(new Date()) + "-" + (++count);
}
}
package com.zpc.zklockv2;
import com.zpc.zklock.Lock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 使用zookeeper实现分布式锁
* 上锁思路:创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平有序)。
* 步骤:
* 在zookeeper的/lock节点下创建一个有序临时节点(EPHEMERAL_SEQUENTIAL)。
* 判断当前客户端创建的节点序号是否最小,如果是最小则该客户端获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
* 当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
* 取锁成功则执行代码,最后释放锁(删除该节点)。
*/
public class ZkDistributedLock implements Lock, Watcher {
private ZooKeeper zk;
private String root = "/lock";//根
private String lockName;//竞争资源的标志,lockName中不能包含单词lock
private String waitNode;//等待前一个锁
private String myZnode;//当前锁
private CountDownLatch latch;//计数器
private int sessionTimeout = 12000;
private List exception = new ArrayList();
public ZkDistributedLock(String config, String lockName) {
this.lockName = lockName;
// 创建一个与服务器的连接
try {
zk = new ZooKeeper(config, sessionTimeout, this);
Stat stat = zk.exists(root, false);
if (stat == null) {
// 创建根节点(第一个连上服务器的客户端负责创建)
zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException e) {
e.printStackTrace();
exception.add(e);
} catch (KeeperException e) {
e.printStackTrace();
exception.add(e);
} catch (InterruptedException e) {
e.printStackTrace();
exception.add(e);
}
}
@Override
public void getLock() {
if (exception.size() > 0) {
System.out.println("=======exceptions==========" + exception);
throw new LockException(exception.get(0));
}
try {
if (this.tryLock()) {
System.out.println("Thread-" + Thread.currentThread().getId() + "-" + myZnode + " 获取锁成功");
return;
} else {
//等待锁
waitForLock(waitNode, sessionTimeout);
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
String splitStr = "_lock_";
if (lockName.contains(splitStr)) {
throw new LockException("lockName can not contains the word 'lock'");
}
//创建临时子节点
myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("=======================" + myZnode + " is created================== ");
//取出所有子节点
List subNodes = zk.getChildren(root, false);
//取出所有lockName的锁
List lockObjNodes = new ArrayList();
for (String node : subNodes) {
String _node = node.split(splitStr)[0];
if (_node.equals(lockName)) {
lockObjNodes.add(node);
}
}
System.out.println("排序前=====================" + lockObjNodes);
Collections.sort(lockObjNodes);
System.out.println("排序后=====================" + lockObjNodes);
System.out.println(myZnode + "==" + lockObjNodes.get(0));
if (myZnode.equals(root + "/" + lockObjNodes.get(0))) {
//如果是最小的节点,则表示取得锁
System.out.println("--------获取锁成功--------");
return true;
}
//如果不是最小的节点,找到比自己小1的节点
String myNodeSerialNum = myZnode.substring(myZnode.lastIndexOf("/") + 1);
System.out.println("myZnode.lastIndexOf=====" + myZnode.lastIndexOf("/"));
waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, myNodeSerialNum) - 1);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
//判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
Stat stat = zk.exists(root + "/" + waitNode, true);
if (stat != null) {
System.out.println("Thread-" + Thread.currentThread().getId() + " waiting for " + root + "/" + waitNode);
this.latch = new CountDownLatch(1);
//阻塞线程,直到latch=0
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
@Override
public void unLock() {
try {
System.out.println("unlock " + myZnode);
zk.delete(myZnode, -1);
myZnode = null;
if (zk != null) {
zk.close();
}
System.out.println("--------释放锁成功--------");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
if (this.latch != null) {
this.latch.countDown();
}
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
}
package com.zpc.zklockv2;
import java.util.concurrent.CountDownLatch;
public class Client2 {
private static final String CONNECTION_STRING = "127.0.0.1:2181";
private static final int THREADNUM = 100;
public static void main(String[] args) throws Exception{
//计数器,用来控制主线程挂起,所有线程执行完成后再执行主线程
CountDownLatch mylatch = new CountDownLatch(THREADNUM);
long start = System.currentTimeMillis();
//开启多个线程模拟分布式环境下多个不同的客户端去获取订单号
for (int i = 0; i < THREADNUM; i++) {
new Thread(new OrderService(mylatch,new ZkDistributedLock(CONNECTION_STRING, "zpcand"))).start();
}
mylatch.await();
System.out.println("cost:" + (System.currentTimeMillis() - start));
}
}
参考:
1.https://mp.weixin.qq.com/s?__biz=MzA4Njk2NDAzMA==&mid=2660209003&idx=1&sn=390a8d52f7aa404f0bf8e565c7d656c1&chksm=84bb933ab3cc1a2c64c8958de9737ab33d8a6bab8b6372cfc1db2b8e9b021aad7a2ecbf7aa39&scene=0#rd