“分布式锁介绍*分布式锁方案对比*zk分布式锁实现方式*zk分布式锁效果展示*curator框架介绍*源码分析”
找到新工作之后,好久没更新公众号了。最近正好在公司分享了"基于zookeeper的分布式锁"这个课题,而分享这个课题也是因为在学习zk阶段我发现老师们的实现多多少少有点问题,我想借这个机会推动自己去找到一个zk分布式锁的成熟实现,很幸运,我找到了curator框架。
01
—
分布式锁介绍
分布式锁即在分布式环境下锁定共享资源,让请求处理串行化。(只有获取到锁的线程才能去访问共享资源)
简单来说,单机环境下我们用sync,lock这些单机锁,对应到分布式环境我们就需要使用分布式锁。
02
—
分布式锁方案对比
在这里主要谈各方案的优缺点:
先提出一个定理:FLP定理(不存在完全满足一致性的异步算法)证明了在异步分布式系统中,如果可能发生故障,就无法达成一致。
mysql:一般公司会用mysql的binlog做异步的主从同步,这也就直接导致其一致性,但由于数据库很少会挂掉,可以视作一个稳定的数据源。就单数据原来说,由于mysql的实现是通过表锁+forUpdate,这种方式直接决定了高并发环境的吞吐量无法支持。
redis:一般公司也是集群且异步主从同步,这也就直接导致其一致性,而且redis作为数据源并不稳定,所以这种情况下使用redis作锁是不明智的。单机redis可以保证数据一致但无法高可用,这才企业级应用中发生故障是不可想象的,而有一个备用方案,可以在单机redis锁故障后切换为mysql作锁(但此时接口吞吐量也会受影响),这个叫做锁降级。
zk:一般公司也是集群且使用zk自身的同步主从同步,且zk的设计初衷也是为了在分布式环境下作协调处理。唯一的问题是公司zk集群一般用来给服务注册或者配置发现,如果在其中加入zk锁的使用会给主机造成不小的压力,影响其原有功能的性能,所以一般建议单独一套zk集群作分布式锁较好。
etcd:同zk,企业级应用首选,但对资源和设备要求高。
03
—
zk分布式锁实现方式
以两个线程AB简单说一下过程:
线程A先开始获取锁:A去zk中创建一个临时有序节点,节点路径:
/lock/000001,然后再向zk请求/lock下所有的子节点,因为序号是有序递增的,其判断自己是否是最小子节点,当然这里只有A,所以A认为自己获取了锁。
A解锁前,B也来获取锁:同样是先去zk中创建一个临时有序节点,节点路径:/lock/000002,然后再向zk请求/lock下所有的子节点,判断自己是否是最小子节点,因为A的存在B不是最小,于是B发起watch请求,watchA:/lock/000001,并进入阻塞状态。
A解锁后,zk发送事件给B,B被唤醒,继续上述判断,直到获取锁。
分布式锁的主要实现思路就是:监控其他客户端的状态,来判断自己是否可以获得锁。采用临时顺序节点的原因:
1.Zk服务器维护了客户端会话的有效性,当会话消失的时候,其会话所创建的临时性节点都会被删除,通过这一特点,可以通过watch临时节点来监控其他客户端的情况,方便自己做出相应动作。
2.因为zk对写操作是顺序性的,所以并发创建节点都会有一个唯一确定的序号,当前锁是公平锁的一种实现,所以依靠这种顺序性可以很好的解释一节点序列小的获取到锁并且可以采用watch自己前一个节点来避免惊群现象。
04
—
zk分布式锁效果展示
这里我使用,Jmeter+Nginx的方式模拟真实场景的并发,并模拟5种生产环境中可能出现的场景以观察zk的状态。
1.正常情况:无超卖现象
2.模拟场景1:服务端短暂的丢失连接,然后恢复,无超卖现象。
3.模拟场景2:无异常现象,无超卖现象。
4.模拟场景3:集群陷入不可用状态,无超卖现象。
5.模拟场景4:出现超卖现象。
6.模拟场景5:不设置超时则阻塞,设置则会按时删除锁。
package cn.pw.studyJavaDemo.ZKLOCK.controller;import cn.pw.studyJavaDemo.ZKLOCK.ZKLock;import cn.pw.studyJavaDemo.ZKLOCK.dao.RecordDao;import cn.pw.studyJavaDemo.ZKLOCK.dao.StockDao;import cn.pw.studyJavaDemo.ZKLOCK.entity.Record;import cn.pw.studyJavaDemo.ZKLOCK.entity.Stock;import lombok.extern.log4j.Log4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("pwZkLock")@Log4jpublic class SeckillController { @Autowired RecordDao recordDao; @Autowired StockDao stockDao; private static final Integer STOCK_ID = 1; private static ZKLock zkLock = new ZKLock("139.224.119.73:2181,139.224.119.73:2182,139.224.119.73:2183,139.224.119.73:2184","/lockPath"); /** * 模拟秒杀的并发场景 * @return true/false * @desc */ @RequestMapping(value = "seckKill",method = RequestMethod.GET) public String seckKill(){ try { //1.加锁 zkLock.lock(); //debug锁重入 /*zkLock.lock();*/// zkLock.unlock();// zkLock.unlock(); //2.查询库存 Stock stock = stockDao.getOne(STOCK_ID); //3.判断库存 if(stock.getStockNum() > 0){ Record record = new Record(); record.setStockRecord(stock.getStockNum()); //保存记录 recordDao.save(record); stock.setStockNum(stock.getStockNum()-1); //库存减1 stockDao.save(stock); return "获取库存成功"; }else { return "商品卖完了"; } }catch (Exception e){ throw new RuntimeException(e); } finally { //6.解锁 zkLock.unlock(); } }}
05
—
curator框架介绍
可参照这篇文章:https://www.jianshu.com/p/db65b64f38aa
依赖注入:
<dependency> <groupId>org.apache.curatorgroupId> <artifactId>curator-recipesartifactId> <version>2.12.0version> dependency>
装配:
private CuratorFramework client; private InterProcessLock lock; public ZKLock(String zkAddress,String lockPath){ //1.Connect to zk client = CuratorFrameworkFactory.newClient(zkAddress, new RetryNTimes(5,5000) ); client.start(); if(client.getState() == CuratorFrameworkState.STARTED){ log.info("zk client start successfully! zkAddress={},lockPath={}",zkAddress,lockPath); }else { throw new RuntimeException("zk client start failed!"); } this.lock = defaultLock(lockPath); } private InterProcessLock defaultLock(String lockPath) { return new InterProcessMutex(client,lockPath); }
private static ZKLock zkLock = new ZKLock("139.224.119.73:2181,139.224.119.73:2182,139.224.119.73:2183,139.224.119.73:2184","/lockPath");
06
—
源码分析
加解锁的方法入口在:InterProcessLock这个类
一共4种锁实现:
1.InterProcessMutex:分布式可重入排他锁。(同时也是公平锁)
2.InterProcessSemaphoreMutex:基于信号量实现的分布式排他锁
3.InterProcessMultiLock:将多个锁作为单个实体管理的容器
4.InterProcessReadWriteLock:分布式读写锁
这里我只暂时讲InterProcessMutex这个:
------------------------------------------------------------------------------
acquire():
获取锁主要分4个大块:
获取当前线程
检测锁重入性
threaData是Map,lockData是锁信息对象。这里是锁在重入时进行自选计数,计数存在lockCount里。
尝试获取锁
尝试获取锁是获取锁的核心,这里要细说一下。
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception{ //startMillis当前系统时间 final long startMillis = System.currentTimeMillis(); //如果有锁失效时间则将传进来的时间转化为毫秒 final Long millisToWait = (unit != null) ? unit.toMillis(time) : null; //创建节点并设置节点里的数据,这里为null final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes; //重试次数,客户端设置了重试机制时生效 int retryCount = 0; //创建的临时节点的路径 String ourPath = null; //是否持有锁 boolean hasTheLock = false; //尝试获取锁的过程是否完成 boolean isDone = false; while ( !isDone ) { isDone = true; try { //创建临时序列节点 ourPath = driver.createsTheLock(client, path, localLockNodeBytes); //判断是否获取锁 hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath); } catch ( KeeperException.NoNodeException e ) { // gets thrown by StandardLockInternalsDriver when it can't find the lock node // this can happen when the session expires, etc. So, if the retry allows, just try it all again //如果出现session过期这里看是否启动重试机制, if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ) { isDone = false; } else { //不需要重试,直接抛出异常 throw e; } } } if ( hasTheLock ) { //持有锁则返回持有锁的路径 return ourPath; } //否则返回null return null; }
再说一下driver.createsTheLock:
@Override public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception { String ourPath; if ( lockNodeBytes != null ) { //有数据则创建有数据的临时顺序节点 ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes); } else { //没有则创建无数据的临时顺序节点 ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path); } return ourPath;
实际路径:
String adjustPath(String path) throws Exception { if (this.doProtected) { PathAndNode pathAndNode = ZKPaths.getPathAndNode(path); String name = getProtectedPrefix(this.protectedId) + pathAndNode.getNode(); //实际路径是:/我们给出的根路径/uuid+lock+zk生产的递增序列 path = ZKPaths.makePath(pathAndNode.getPath(), name); } return path; }
发出请求:
public ReplyHeader submitRequest(RequestHeader h, Record request, Record response, WatchRegistration watchRegistration) throws InterruptedException { ReplyHeader r = new ReplyHeader(); ClientCnxn.Packet packet = this.queuePacket(h, r, request, response, (AsyncCallback)null, (String)null, (String)null, (Object)null, watchRegistration); synchronized(packet) { while(!packet.finished) { packet.wait(); } return r; } }
再说下internalLockLoop();
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception{ //是否持有锁 boolean haveTheLock = false; //是否需要删除锁 boolean doDelete = false; try { if ( revocable.get() != null ) { //如果需要锁撤回则加锁测回的watch client.getData().usingWatcher(revocableWatcher).forPath(ourPath); } while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) { //访问zk获得根节点所有的子节点list List children = getSortedChildren(); //获得当前临时顺序节点的路径 String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash //判断是否获得锁 PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases); if ( predicateResults.getsTheLock() ) { //获得锁 haveTheLock = true; } else { //获取前一个节点的路径 String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch(); synchronized(this) { try { // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak //wath前面一个节点 client.getData().usingWatcher(watcher).forPath(previousSequencePath); if ( millisToWait != null ) { //如果有获取锁的限定时间这里判断一下 millisToWait -= (System.currentTimeMillis() - startMillis); startMillis = System.currentTimeMillis(); if ( millisToWait <= 0 ) { //超时了,要删除这个临时节点 doDelete = true; // timed out - delete our node break; } //没有超时,当前线程阻塞指定时间 wait(millisToWait); } else { //没有锁的限定时间则无限阻塞等待唤醒 wait(); } } catch ( KeeperException.NoNodeException e ) { //这里是防止在watch前一个节点时,前面一个节点被删除, //因为抛出异常会试synchronized锁失效,这里catch住当前线程还是锁的持有者 //当前线程会继续循环查看自己是否时分布式锁的持有者 // it has been deleted (i.e. lock released). Try to acquire again } } } } } catch ( Exception e ) { //阻断自己的线程 ThreadUtils.checkInterrupted(e); //删除临时节点 doDelete = true; //抛出异常 throw e; } finally { if ( doDelete ) { //删除临时节点 deleteOurPath(ourPath); } } return haveTheLock; } private void deleteOurPath(String ourPath) throws Exception{ try { client.delete().guaranteed().forPath(ourPath); } catch ( KeeperException.NoNodeException e ) { // ignore - already deleted (possibly expired session, etc.) } } private synchronized void notifyFromWatcher(){//唤醒wait中的线程 notifyAll(); }
4.保存锁数据
当前线程为key,锁数据为value。
------------------------------------------------------------------------------
release():
@Override public void release() throws Exception { /* Note on concurrency: a given lockData instance can be only acted on by a single thread so locking isn't necessary */ Thread currentThread = Thread.currentThread(); LockData lockData = threadData.get(currentThread); if ( lockData == null ) { throw new IllegalMonitorStateException("You do not own the lock: " + basePath); } int newLockCount = lockData.lockCount.decrementAndGet(); if ( newLockCount > 0 ) { return; } if ( newLockCount < 0 ) { throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath); } try { internals.releaseLock(lockData.lockPath); } finally { threadData.remove(currentThread); } }
release()比较简单这里只说大概4步:
获取当前线程
检测锁的重入性
释放锁
移除存储的锁数据
其实如果自己实现zk分布式锁就会发现并没有那么简单,会有很多问题需要考虑,比如watch前面节点时前置节点已经被删除,抛出异常如何处理等等。另外zk锁在公司不要连公司里的服务注册中心的zk使用哦,频繁的读写会增加leader的压力,搞的出问题不要怪我哦。