之前在做elastic分布式定时器时用到过zookeeper,但实际上并没有直接使用。我们项目使用rocketmq来分发消息,然后服务器是分布式的,那么可能就会出现不同的服务器更新相同公司的数据从而导致并发问题。如果是单体架构,那么使用锁就行了,但是分布式服务器就需要使用分布式的锁了。主管钦定我去用zookeeper做一个分布式锁。于是我就开始在网上查阅资料。
首先是zookeeper,总结起来就是一个分布式的类文件系统,数据格式像文件夹一样
然后是通知或监听功能,在文件发生变化的时候通知相应的客户端。主要就是这两点。
当然还有分布式的集群功能,选举啊,备份啊之类的,跟我们的实现没什么关系。
zookeeper的分布式锁原理则跟上面两点有关系
参考资料:https://blog.csdn.net/qiangcuo6087/article/details/79067136
其实就是客户端在某个文件下创建一个同名的节点,那么会自动在后面加上序号,然后客户端查询该节点是否是所有节点中序号最小的,如果是最小的,那么视为拿到了锁,可以执行业务代码。(判断序号是否最小只要获得该文件下的所有节点列表,第一个就是,因为查询出的位置是不变的),如果不是最小的,那么就监控自己前一位的节点。
当业务结束,释放锁的时候,客户端删除自己的那个节点,然后该节点后面的节点会触发删除的监听事件,拿到锁,以此类推。
原理基本就是这样,具体实现我参考了另一篇文章 利用ZooKeeper简单实现分布式锁
但是注意,这篇文章的作者犯了一个致命的错误,实际测试的时候根本行不通,我在文章下的评论中有提到。所以我在他的基础上稍微改变了下。
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DistributedLock implements Lock,Watcher {
Logger logger = LoggerFactory.getLogger(this.getClass());
private ZooKeeper zk;
private String root = "/locks";//根
private String lockName = "";//竞争资源的标志
private String waitNode;//等待前一个锁
private String myZnode;//当前锁
private CountDownLatch latch;//计数器
private CountDownLatch connectedSignal=new CountDownLatch(1);
String splitStr = "_lock_";
private int sessionTimeout = 1000;
/**
* 创建分布式锁,使用前请确认config配置的zookeeper服务可用
* @param config 192.168.1.127:2181
* @param lockName 竞争资源标志,lockName中不能包含单词_lock_
*/
public DistributedLock(String config, String lockName){
this.lockName = lockName;
// 创建一个与服务器的连接
try {
//创建zookeeper连接,注意this就是zookeeper的监听器watcher
zk = new ZooKeeper(config, sessionTimeout, this);
connectedSignal.await();
Stat stat = zk.exists(root, false);//false的意思是不监听节点
if(stat == null){
// 创建根节点
zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
} catch (IOException e) {
throw new LockException(e);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
/**
* zookeeper节点的监视器
*/
@Override
public void process(WatchedEvent event) {
if(connectedSignal != null && event.getState()==KeeperState.SyncConnected){
connectedSignal.countDown();//建立zookeeper连接的消息
connectedSignal = null;
return;
}
//删除节点的类型
if(event.getType() == EventType.NodeDeleted) {
//其他线程放弃锁的标志
try {
if(this.latch != null) {
this.latch.countDown();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void lock() {
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);
}
}
@Override
public boolean tryLock() {
try {
if(lockName.contains(splitStr))
throw new LockException("lockName can not contains \\u000B");
//创建临时子节点
myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("Thread " + Thread.currentThread().getId()+ " 创建 "+ myZnode );
//是否获得锁
if(getLock()) {
return true;
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
public boolean getLock() throws KeeperException, InterruptedException {
//取出所有子节点
List<String> subNodes = zk.getChildren(root, false);
//取出所有lockName的锁
List<String> lockObjNodes = new ArrayList<String>();
for (String node : subNodes) {
String _node = node.split(splitStr)[0];
if(_node.equals(lockName)){
lockObjNodes.add(node);
}
}
Collections.sort(lockObjNodes);
if(myZnode.equals(root+"/"+lockObjNodes.get(0))){
//如果是最小的节点,则表示取得锁
System.out.println(myZnode + "==" + lockObjNodes.get(0));
return true;
}
//如果不是最小的节点,找到比自己小1的节点
String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);//找到前一个子节点
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
try {
if(this.tryLock()){
return true;
}
return waitForLock(waitNode,time);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
Stat stat = zk.exists(root + "/" + lower,true);//同时注册监听。
//判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
if(stat != null){
System.out.println("Thread " + Thread.currentThread().getId() + " 等待 " + root + "/" + lower);
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);//可能因为并发或者其他原因导致监听丢失导致死锁,因此采用轮询的方式确定上个节点是否存在
while(this.latch.getCount() > 0) {
Stat stat2 = zk.exists(root + "/" + lower,true);
if(stat2 == null) {
this.latch.countDown();
}else {
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
}
}
// this.latch.await();
this.latch = null;
}
return true;
}
@Override
public void unlock() {
try {
System.out.println("Thread " + Thread.currentThread().getId() + "释放了锁 " + myZnode);
zk.delete(myZnode,-1);
myZnode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
this.lock();
}
@Override
public Condition newCondition() {
return null;
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e){
super(e);
}
public LockException(Exception e){
super(e);
}
}
volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
int all = 10;
CountDownLatch latch = new CountDownLatch(all);//活学活用,使用计数器来计算整个多进程的运行时间
Runnable runnable = new Runnable() {
public void run() {
DistributedLock lock = null;
try {
lock = new DistributedLock("127.0.0.1:2181", "test1");
lock.lock();
num++;
System.out.println("Thread " + Thread.currentThread().getId() + "正在运行");
} finally {
if (lock != null) {
lock.unlock();
latch.countDown();
}
}
}
};
Date begin = new Date();
for (int i = 0; i < all; i++) {
if(i == 0) {
new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
//当所有的进程都运行完成后计算运行时间
System.out.println("花费时间:"+(new Date().getTime() - begin.getTime()));
System.out.println("执行个数:"+num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();;
}
Thread t = new Thread(runnable);
t.start();
}
}
}
还是用到了很多新东西的,比如Lock接口和CountDownLatch类等都没有用过,之前都是用synchronized
代码可以直接执行,值得注意得有这么几点,new ZooKeeper(config, sessionTimeout, this)中this其实指的就是监听器,zookeeper.exists(root, false)后面的boolean值就是是否监听节点的事件,其他getData的方法类似。并且监听方法只执行一次,修改后再修改就不会触发监听了,然后最关键的就是process方法,里面就是监听到事件后的方法。
这算是一个简单的demo,之后我想再弄个zookeeper连接池之类的优化一下,如果每个进程都要创建关闭连接在我看来太不经济了。或者是把锁,计时器,上一个锁放在一个Map<Thread ,LockData>中进行管理,这样就不用每次使用都创建一个实例了。这些都是可以考虑的方向。
测试发现,connectedSignal还是很有必要的,当我把进程开到一百个时,就会报下面的错误
其实就是zookeeper在建立连接之前调用了方法(如exist),所以需要connectedSignal来阻塞一下,直到收到连接建立的消息。
果然zookeeper连接数多的时候有问题,一直重连
2018-08-27 11:03:05 5018 [Thread-872-SendThread(127.0.0.1:2181)] WARN org.apache.zookeeper.ClientCnxn - Session 0x0 for server 127.0.0.1/127.0.0.1:2181, unexpected error, closing socket connection and attempting reconnect
但是zookeeper并不能像datasource连接池一样重复利用,因为每个zookeeper连接都有一个实例化的watcher,并且我没有找到方法去改变zookeeper的监听器,所以只好限制zookeeper的连接数
import java.io.IOException;
import java.util.concurrent.Semaphore;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class ZookeeperPool {
private static final int maxSize = 30;
private static int nowSize = 0;
private static Semaphore available = new Semaphore(maxSize);
public static ZooKeeper getZookeeper(String config, int sessionTimeout, Watcher watcher) throws IOException, InterruptedException {
available.acquire();
return new ZooKeeper(config, sessionTimeout, watcher);
}
public static void releaseZookeeper(ZooKeeper zk) throws InterruptedException {
available.release();
zk.close();
}
}
将zookeeper的新建和关闭都使用ZookeeperPool方法来调用,这样就能限制zookeeper连接数了
优化了waitForLock等待锁的方法,防止监听丢失导致死锁
最近线上发现zookeeper锁失效了,不停地在本地模拟和测试,查看代码和算法的漏洞,花了我快一周的时间,都没办法重现问题,终于在今天连接到线上的zookeeper发现了问题所在,lock节点的编号似乎达到了某个极限,所以后面新增的节点的编号数会比前面新增的节点编号数要小,导致在getLock方法里新增的节点直接变成了序号最小的节点,所有的线程都能够直接拿到锁。我怀疑是因为编号数过大的时候出现的,因为之前都没有出现过这个问题。在重建zookeeper之后暂时解决了这个问题。之后我们可能会换成redis锁。
一定要注意!!zookeeper的锁节点编号数,不要超过某个值,定时重建或清空数据
还有查找一些线上问题的时候,一定要连接线上的环境,不要自己在那里盲目地猜测,用线下的环境测试