为什么需要分布式锁?
如下图所示 ,当nignx 并发量到达10w ,tomcat1,tomcat2,tomcat3 都是订单服务,在如此高的并发量下面, 如果不做分布式锁,那么后台生成的订单编号必定重复。如果不是分布式的话,那么只要加锁就行, 可是分布式的话,不是同一个jvm那么 加sync 锁也是不行的.
那么我们如何去解决呢?看下图
一、通过临时文件建立分布式锁
当这三个tomcat 去创建订单的时候先去zookeeper 的某个文件夹下加把锁(创建个文件),当某个tomcat 申请锁成功后(创建某个文件成功) ,另外两个tomcat 就等待(同一个目录下不能创建同样名字的文件)。
我用个类比的方法说一遍。 比如这三个tomcat 生成订单号的时候, 都要去/lock目录下生成createOrderId 这个名字的文件夹,当tomcat2 创建文件夹成功了,tomcat1,和tomcat2 就不能创建createOrderId这个名字的文件夹了,因为相同目录下文件夹的名字不能重复。这样创建不了文件夹的就当做没有获取到锁。当创建成功文件夹的时候。我们通过get 文件名的命令可以回去到很多信息有一个就是sessionid ,根据session 可以判断是和创建文件的客户端断开连接了,如果断开则销毁临时文件(下图中ephemeralOwner 即临时文件所有者就是sessionid)。这样我们可以通过关闭客户端来释放锁。
1)加锁
lock()是获取锁的方法,先是试着获取锁trylock()如果成功打印信息,否则等待着释放锁再去获取锁
@Override
public void lock() {
if(tryLock()) {
logger.info(Thread.currentThread().getName() + "-->获取锁成功");
} else {
waitforlock();
lock();
}
}
protected abstract boolean tryLock();
protected abstract void waitforlock();
/**
释放锁
*/
@Override
public void unlock() {
client.close();
}
2)创建临时文件,注册监听事件
client.createEphemeral(path);就是去创建临时节点成功就返回true,否者获取锁失败waitforlock()就是监听需要创建的文件夹,
向client 注册监听事件.如果当前路劲是存在的,则阻塞在这边。当文件状态是删除的,触发监听事件,所有的countDownLauch全部放行。并且这时候取消监听事件 waitforlock()的方法全部走完,再走上图代码段中的else 里面的lock()方法继续去竞争锁
public class ZkLockImpl extends ZkAbstractLock {
private CountDownLatch cdl = null;
/**
* 这个就是去创建znode,如果能成功就返回true
*/
@Override
protected boolean tryLock() {
try {
client.createEphemeral(path);
return true;
} catch (ZkException e) {
return false;
}
}
/**
* waitforlock要监控前面的那个线程的/lock节点
* 要知道/lock节点有没有删除
*/
@Override
protected void waitforlock() {
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
if (cdl != null) {
cdl.countDown();
}
}
};
//这里注册path的事件
client.subscribeDataChanges(path, iZkDataListener);
if (client.exists(path)) {
cdl = new CountDownLatch(1);
try {
//这里是要等待,只有当前面的线程释放了锁,也就是说前面的线程的zookeeper会话失效以后
//才不需要等待
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
client.unsubscribeDataChanges(path, iZkDataListener);
}
}
下面是 运用发射枪(countDownLauch)模拟101 个并发
public class OrderService implements Runnable {
Logger logger = LoggerFactory.getLogger(getClass());
private OrderNumFactory onf = new OrderNumFactory();
private static Integer count = 101;
private static CountDownLatch cdl = new CountDownLatch(count);
private Lock lock = new ZkImproveLockImpl();
@Override
public void run() {
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
createOrderNum();
}
public void createOrderNum() {
lock.lock();
String orderNum = onf.createOrderNum();
logger.info(Thread.currentThread().getName()+ "创建了订单号:[" + orderNum + "]!");
lock.unlock();
}
public static void main(String[] args) {
for (int i = 0; i < count; i++) {
new Thread(new OrderService()).start();
cdl.countDown();
}
}
}
二、利用临时顺序文件来创建分布式锁
先在实现类中创建一个永久文件以及三个变量
private String currentPath;
private String beforePath;
private CountDownLatch cdl;
public ZkImproveLockImpl() {
if(!this.client.exists(path)) {
this.client.createPersistent(path,"xx");
}
}
代码逻辑
当trylock 的时候看看此类的当前路径(currentPath)存在不存在。 如果不存在,就在这个永久节点下创建一个临时顺序节点,若存在,再去判断这个永久节点下第一个顺序节点是不是自己,如果是则获取锁,执行业务,最终关掉客户端 当客户端关闭,临时文件也会消失。 如果不是则获取当前文件(currentPath)的前面一个文件, 并且去监听这个文件,当他被删除的时候,解开countDownLatch 再去获取锁 执行业务,关闭连接,取消监听。
@Override
protected boolean tryLock() {
if(currentPath == null || currentPath.length() <= 0) {
currentPath = this.client.createEphemeralSequential(path + "/","aa");
}
List<String> childrens = this.client.getChildren(path);
Collections.sort(childrens);
//这个就是判断当前用户创建的临时节点的名称是否跟孩子里面最新的那个相等,如果相等就代表可以获得锁
if(currentPath.equals(path + "/" + childrens.get(0))) {
return true;
} else {
//如果不能获取到这把锁,那么必须要获取到前面那个节点,要注册对前面那个节点的事件监控
System.out.println(currentPath.substring(5));
int i = Collections.binarySearch(childrens,currentPath.substring(6));
System.out.println(i);
beforePath = path + "/" + childrens.get(i - 1);
}
return false;
}
@Override
protected void waitforlock() {
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
if(cdl != null) {
cdl.countDown();
}
}
};
client.subscribeDataChanges(beforePath,iZkDataListener);
if(client.exists(beforePath)) {
cdl = new CountDownLatch(1);
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
client.unsubscribeDataChanges(beforePath,iZkDataListener);
}
当然apache里面有netflix 贡献出来的curator项目将这些复杂的部分封装起来了,但是原理我们还是要好好看看的。