最近看了一遍,分布式锁的实现,以为都看懂了。上手还是遇到一些问题,所以写下这篇博客,作为参考,废话不多说,先上一篇代码:
OrderCodeGenerator :
/** * orderCode 生成类 这个是我们要操作的目标对象 * Created by Dell on 2018/5/15. */ public class OrderCodeGenerator { private int i = 1; public String getOrderCode() { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss " + (i++)); String s = simpleDateFormat.format(new Date()); System.out.println(s); return s; } }
模拟数据层,生成以时间+序号 的订单 编号
OrderCodeServiceImpl:
/** * 业务实现层 * Created by Dell on 2018/5/15. */ public class OrderCodeServiceImpl { final static OrderCodeGenerator ocg = new OrderCodeGenerator(); private CountDownLatch cdl; public OrderCodeServiceImpl(int size) { cdl = new CountDownLatch(size); } //自定义锁的实现 Lock lock = new DistributedLock("/test"); public void createOrder() { new Thread(new Runnable() { public void run() { try { lock.lock(); cdl.await();//阻塞等待,所有线程创建完毕。模拟并发情况 ocg.getOrderCode(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }).start(); } /** * 计数递减的方法 */ public void countDown() { this.cdl.countDown(); } }业务层,调用数据层生成订单编号,其中 final关键字是必须的,这里模拟多个服务间共有变量,cdl.await(),阻塞线程,模拟并发请求。size标识并发数,从上游传过来 ;cdl.countDown()没生成一个线程,计数减一。
DistributedLock:
package com.wera.chapter1; import org.I0Itec.zkclient.IZkDataListener; import org.I0Itec.zkclient.ZkClient; import java.util.Collections; 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; /** * Created by Dell on 2018/5/15. */ public class DistributedLock implements Lock { private ZkClient zkClient; private String parentPath = ""; private ThreadLocal<String> currentPath = new ThreadLocal<String>(); private ThreadLocal<String> beforePath = new ThreadLocal<String>(); CountDownLatch cdl = new CountDownLatch(1); public DistributedLock(String parentPath) { this.parentPath = parentPath; //建立链接 zkClient = new ZkClient("localhost:2181"); //序列话接口 zkClient.setZkSerializer(new MySerializer()); if (!zkClient.exists(this.parentPath)) { zkClient.createPersistent(this.parentPath); } } public void lock() { if (!tryLock()) {//未获取到监听 waitForLock(); //注册监听,阻塞自己 lock();//递归调用自身 } } public void lockInterruptibly() throws InterruptedException { } //注册监听 监听成功就阻塞自己 public void waitForLock() { IZkDataListener listener = new IZkDataListener() { public void handleDataChange(String s, Object o) throws Exception { System.out.println("节点 " + s + " 的数据发生了变化" + o); } public void handleDataDeleted(String s) throws Exception { System.out.println("节点 " + s + " 被删除"); cdl.countDown(); } }; zkClient.subscribeDataChanges(beforePath.get(), listener); if (zkClient.exists(beforePath.get())) { try { cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } } zkClient.unsubscribeDataChanges(beforePath.get(), listener); } /** * 临时顺序节点有一下两个特性: * 1)断开链接时候,节点删除 * 2)它是有序递增的 * 我们为每个线程建立一个节点,建立节点成功就表示拿到了锁lock,并用TheadLocal接收,同时返回当前 节点的名称 * 父节点下的所有子节点可以排序,确保最小的节点为当前节点即可。如果当前节点不是最小节点,我们只关注当前节点的前一个节点, * 对它加监听,当前 节点 删除时唤醒线程 ; * 前节点依然存在的时候阻塞线程。 * 释放锁的过程就是删除前一节点 * 有几种情况: * 1)子节点列表为空,表示第一个拿到锁,同时创建节点,这个时候,当前 节点等同于前节点。 * 2)子节点列表不为空,但是当前节点为空,表示当前 线程并没有创建节点,有其他的线程创建过节点,这个时候创建本线程的节点,递归 继续 * 3)子节点列表不为空且长度等于1,当前节点也不为空.这个时候就说明了只有当前线程创建了节点。所以这个 时候也是拿到了锁。这个时候前节点等同于当前节点 * 4)子节点列表不为空且长度也大于1,当前节点不为空,并且当前节点不再列表的首位,需要继续监听,阻塞,这个时候前节点不等同于当前节点 * 5)子节点列表不为空且长度大于1,当前节点不为空,并且 当前节点再列表的首位,说明拿到了锁。这个时候前节点等同于当前节点 * @return */ public boolean tryLock() { boolean isback = false; List<String> children = zkClient.getChildren(parentPath); if (null == children && children.size() == 0 ) { //当前未创建节点 ,创建节点,于是获得了锁 currentPath.set(zkClient.createEphemeralSequential(this.parentPath+"/","aa")); beforePath.set(currentPath.get()); //这一种情况只有一个 isback = true; } else { if (null == currentPath.get()) { //本线程创建一个节点 currentPath.set(zkClient.createEphemeralSequential(this.parentPath+"/","aa")); return tryLock(); }else { //该线程已经创建过节点了。 if (children.size() == 1) { //当前线程创建了一个节点,并且该父节点下的子节点数是1,也就是说只有当前线程创建了一个节点,所以它也拿到了锁 isback = true; beforePath.set(currentPath.get());//这一种情况只有一个 }else { Collections.sort(children); String curent = currentPath.get().substring(this.parentPath.length() + 1); int currentIndex = children.indexOf(curent); if (currentIndex > 0) { beforePath.set(this.parentPath + "/" + children.get(currentIndex - 1)); isback = false; }else { isback = true; beforePath.set(currentPath.get());//这一种情况只有一个 } } } } return isback; } public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } public void unlock() { zkClient.delete(beforePath.get()); } public Condition newCondition() { return null; } }
自定义锁的实现,实现逻辑是利用zookeeper特性,如分布式,同一父节点下的节点唯一(不可重复),详细请参见 注释。
OrderCodeTest:
/** * Created by Dell on 2018/5/15. */ public class OrderCodeTest { public static void main(String[] args) throws InterruptedException { int size = 20; OrderCodeServiceImpl orderCodeService = new OrderCodeServiceImpl(size); for (int i = 0; i < size; i++) { orderCodeService.createOrder();//调用业务层的逻辑 orderCodeService.countDown();//调用一次计数减一次 } } }
程序入口,size=20,模拟20个请求 。同时调用计数器,当计数为0,模拟并发请求情景。
控制台打印结果如下:
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
2018-05-15 19:09:17 1
节点 /test/0000002810 被删除
2018-05-15 19:09:17 2
节点 /test/0000002811 被删除
2018-05-15 19:09:18 3
2018-05-15 19:09:18 4
节点 /test/0000002812 被删除
2018-05-15 19:09:18 5
节点 /test/0000002814 被删除
2018-05-15 19:09:18 6
2018-05-15 19:09:18 7
节点 /test/0000002815 被删除
节点 /test/0000002816 被删除
2018-05-15 19:09:18 8
2018-05-15 19:09:18 9
节点 /test/0000002817 被删除
2018-05-15 19:09:18 10
2018-05-15 19:09:18 11
2018-05-15 19:09:18 12
节点 /test/0000002820 被删除
2018-05-15 19:09:18 13
2018-05-15 19:09:19 14
节点 /test/0000002822 被删除
2018-05-15 19:09:19 15
节点 /test/0000002823 被删除
2018-05-15 19:09:19 16
节点 /test/0000002824 被删除
节点 /test/0000002825 被删除
2018-05-15 19:09:19 17
2018-05-15 19:09:19 18
节点 /test/0000002826 被删除
2018-05-15 19:09:19 19
2018-05-15 19:09:19 20
节点 /test/0000002828 被删除
Disconnected from the target VM, address: '127.0.0.1:52753', transport: 'socket'
点击查看gitHub地址,如果打不开请 复制以下地址浏览器中打开:
https://github.com/wera0817/learnroot