zookeeper分布式锁从零实践与源码解析

现今服务大都是集群部署,使用操作系统锁或基于AQS的JUC并发工具,不能满足`跨JVM`线程同步的需求。本文从中间件安装开始,以抢购商品为例,实现一个`zk`分布式锁。
## 一、实现方式对比
分布式锁常见实现有如下三种:
- 数据库,基于唯一索引
- `redis`,基于原子命令与`lua`脚本
- `zookeeper`,基于临时顺序节点
我们选择zk实现的原因如下:
- `mysql`实现锁依赖IO,性能过低
- `redis`主从部署存在数据同步问题
- 实现`redLock`资源占用高、性能较低
- `zk`本身具有分布式一致性,`ZAB`协议请查阅参考资料
- `zk`实现较为简洁
- 生产环境本身已有`zk`集群
## 二、业务场景描述
假设我们要抢购一部新发行的手机,限制条件如下:
- 商品库存总量为`10`台
- 并发量大约为`10000`
- 不能超卖
## 三、中间件安装
- `docker`安装:
参考:[容器运行时docker与服务编排](https://juejin.cn/post/7083742011576025118)
- `zookeeper`安装
```
docker run ‐‐privileged=true ‐d ‐‐name zookeeper ‐p 8001:2181 ‐p 8002:8080 ‐d zookeeper:latest
```
这里宿主机端口映射,可自定义。
- 运行`docker ps`查看运行状态
![1650421646(1).png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f38600395144e5fa6d8b2ac8bb8d3ba~tplv-k3u1fbpfcp-watermark.image?)
- 防火墙开放端口
```
firewall‐cmd ‐‐zone=public ‐‐add‐port=2181/tcp ‐‐permanent
firewall‐cmd ‐‐zone=public ‐‐add‐port=8080/tcp ‐‐permanent
firewall‐cmd ‐‐reload
```
- 验证zookeeper
![1650421825(1).png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cff46a94400b4d0a8d1de03ad2480f70~tplv-k3u1fbpfcp-watermark.image?)
有返回状态即可。
## 四、代码实现
这里我们使用`curator`客户端进行实现。
- 引入依赖
```
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.0</version>
</dependency>
```
- 核心代码实现
```
@Component("zk")
public class FlashSaleHandler {

    // 手机销售总量为10
    public static AtomicInteger storageCount = new AtomicInteger(10);

    public static final String SERVER_ADDR = "192.168.16.16:8001";

    public Boolean flashSale() {

        // 重试策略
        RetryPolicy policy = new ExponentialBackoffRetry(2000, 3);

        // 创建客户端
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .namespace(String.valueOf(new Random().nextInt()))
                .connectString(SERVER_ADDR)
                .retryPolicy(policy)
                .build();
                
        // 连接zk服务端
        client.start();

        // 构建锁对象
        final InterProcessMutex mutex = new InterProcessMutex(client, "/locks/iphone13");

        // 秒杀逻辑
        try {
            // 获取锁
            mutex.acquire();
            // 秒杀减库存
            if (storageCount.get() > 0) {
                System.out.println(Thread.currentThread().getName()
                        + "成功抢购,剩余库存" + storageCount.decrementAndGet());
                return true;
            } else {
                // 库存不足
                System.out.println("sold out...");
                return false;
            }
        } catch (Exception exception) {
            // 这里我们直接忽略
            System.out.println("just ignore");
        } finally {
            // 释放锁
            try {
                mutex.release();
            } catch (Exception exception) {
                // 这里我们直接忽略
                System.out.println("just ignore");
            }
        }
        return false;
    }
}
```
## 五、测试
测试线程池配置:
```
@Configuration
public class ThreadPoolCreator {

    /**
     * 核心线程数
     */
    private static int corePoolSize = Runtime.getRuntime().availableProcessors() * 3;

    /**
     * 最大线程数 避免内存交换 设置为核心核心线程数
     */
    private static int maximumPoolSize = corePoolSize;

    /**
     * 最大空闲时间
     */
    private static long keepAliveTime = 3;

    /**
     * 最大空闲时间单位
     */
    private static TimeUnit unit = TimeUnit.MINUTES;

    /**
     * 使用有界队列,避免内存溢出
     */
    private static BlockingQueue<Runnable> workQueue = new LinkedBlockingDeque<>(10000);

    /**
     * 线程工厂,这里我们使用可命名的线程工厂,方便业务区分以及生产问题排查。
     */
    private static ThreadFactory threadFactory = new ThreadFactoryBuilder()
            .setNameFormat("flashSale-%d").build();

    /**
     * 拒绝策略 根据业务选择或者自定义
     */
    private static RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(){
        return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime, unit,
                workQueue,
                threadFactory,
                handler);
    }
}
```
测试逻辑:
```
...
@Resource
private FlashSaleHandler zk;


@Test
public void testFlashSale(){
    List<CompletableFuture<Boolean>> futures = new ArrayList<>(10000);
    List<Boolean> results = new ArrayList<>(10000);
    for (int i=0; i<10000; i++){
        CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(zk::flashSale,
                threadPoolExecutor);
        futures.add(future);
    }
    CompletableFuture
            .allOf(futures.toArray(new CompletableFuture[futures.size()]))
            .whenComplete((v, t)-> futures.forEach(eachFuture -> {
                results.add(eachFuture.getNow(null));
            })).join();
    // 最终抢购成功数量为10
    Assertions.assertEquals(results.stream()
            .filter(result -> result == true).collect(Collectors.toList()).size(), 10);
}
```
运行单元测试,查看zookeeper节点情况与执行结果:

![db63303f2145c7f0fd89548a9af764a.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ddb09546c29343b687906a9f0714b9eb~tplv-k3u1fbpfcp-watermark.image?)

![1650429987(1).png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f597aa417e84ec3a74faff2728d7644~tplv-k3u1fbpfcp-watermark.image?)

## 六、源码解析
本节我们从`mutex.acquire()`获取锁方法开始。
```
public void acquire() throws Exception {
    if (!this.internalLock(-1L, (TimeUnit)null)) {
        throw new IOException("Lost connection while trying to acquire lock: " + this.basePath);
    }
}
```
我们进入`internalLock(...)`方法
```
private boolean internalLock(long time, TimeUnit unit) throws Exception {
    // 获取当前线程
    Thread currentThread = Thread.currentThread();
    // 获取当前线程的lockData
    InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
    if (lockData != null) {
        // lockData不为空,说明当前线程已经加过锁,这里我们重入即可。
        lockData.lockCount.incrementAndGet();
        return true;
    } else {
        // lockData不为空,尝试枷锁,这里会返回一个临时顺序节点,我们这里是/locks/iphone13/...
        String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());
        if (lockPath != null) {
            // 加锁成功,构建lockData对象
            InterProcessMutex.LockData newLockData = new InterProcessMutex.LockData(currentThread, lockPath);
            this.threadData.put(currentThread, newLockData);
            return true;
        } else {
            return false;
        }
    }
}
```
上面我们发现一直在操作一个`lockData`对象,我们来看看`lockData`中的内容:
```
private static class LockData {
    final Thread owningThread;
    final String lockPath;
    final AtomicInteger lockCount;
    ...
}
```
我们可以看到该类主要包含三个属性:
- `owningThread`  代表锁所属的线程
- `lockPath`   表示临时顺序节点的路径
- `lockCount` 用于锁重入和解锁的计数器
我们接着看加锁方法`attemptLock(...)`:
```
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
    ...
    while(!isDone) {
        isDone = true;

        try {
            // 创建锁
            ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);
            // 多个线程抢锁阻塞等待
            hasTheLock = this.internalLockLoop(startMillis, millisToWait, ourPath);
        } catch (NoNodeException var14) {
            if (!this.client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper())) {
                throw var14;
            }

            isDone = false;
        }
    }

    return hasTheLock ? ourPath : null;
}
```
先看下创建锁的方法`createsTheLock(...)`:
```
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
    String ourPath;
    // 默认是null,这里我们跳过
    if (lockNodeBytes != null) {
        ourPath = (String)((ACLBackgroundPathAndBytesable)client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).forPath(path, lockNodeBytes);
    } else {
        // 主要执行逻辑,创建节点
        ourPath = (String)((ACLBackgroundPathAndBytesable)client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).forPath(path);
    }
    return ourPath;
}
```
这里有一个节点类型的枚举:
```
PERSISTENT(0, false, false, false, false),
PERSISTENT_SEQUENTIAL(2, false, true, false, false),
EPHEMERAL(1, true, false, false, false),
EPHEMERAL_SEQUENTIAL(3, true, true, false, false),
CONTAINER(4, false, false, true, false),
PERSISTENT_WITH_TTL(5, false, false, false, true),
PERSISTENT_SEQUENTIAL_WITH_TTL(6, false, true, false, true);
```
我们需要创建的是`EPHEMERAL_SEQUENTIAL`临时顺序节点。

接下来我们看看抢锁的方法`internalLockLoop(...)`:
```
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
    boolean haveTheLock = false;
    boolean doDelete = false;
    try {
        ...
        while(this.client.getState() == CuratorFrameworkState.STARTED && !haveTheLock) {
            // 获取节点/lock/iphone13下面的临时顺序节点并按编号升序排列
            List<String> children = this.getSortedChildren();
            // 获取当前线程创建的临时顺序节点名称
            String sequenceNodeName = ourPath.substring(this.basePath.length() + 1);
            // 尝试获取锁,判断当前节点的编号,如果是第一个则抢占锁
            PredicateResults predicateResults = this.driver.getsTheLock(this.client, children, sequenceNodeName, this.maxLeases);
            // 当前客户端获取到锁,直接返回true
            if (predicateResults.getsTheLock()) {
                haveTheLock = true;
            } else {
                // 当前没抢到锁,根据排序结果则监听上一个节点
                String previousSequencePath = this.basePath + "/" + predicateResults.getPathToWatch();
                synchronized(this) {
                    try {
               // 开启监听器,监听上一个节点
              ((BackgroundPathable)this.client.getData()
              .usingWatcher(this.watcher)).forPath(previousSequencePath);
                        if (millisToWait == null) {
                            // 阻塞等待
                            this.wait();
                        } else {
                            ...
                        }
           ...
    return haveTheLock;
}
```
这里我们看下监听器`this.watcher`的初始化创建:
```
private final Watcher watcher = new Watcher() {
    public void process(WatchedEvent event) {
        // 通知所有wait的节点
        LockInternals.this.client.postSafeNotify(LockInternals.this);
    }
};
```

加锁流程分析完后,我们看下如何解锁:
```
public void release() throws Exception {
    Thread currentThread = Thread.currentThread();
    // 获取之前创建的lockData对象
    InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
    if (lockData == null) {
        // 未获取到监视器锁异常
        throw new IllegalMonitorStateException("You do not own the lock: " + this.basePath);
    } else {
        // 不管是否是重入锁,lockCount - 1
        int newLockCount = lockData.lockCount.decrementAndGet();
        if (newLockCount <= 0) {
            if (newLockCount < 0) {
                throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + this.basePath);
            } else {
                try {
                    // 重入次数为0,直接释放锁并发送解锁事件
                    this.internals.releaseLock(lockData.lockPath);
                } finally {
                    // 从concurrentHashMap中删除
                    this.threadData.remove(currentThread);
                }
            }
        }
    }
}
```
总体流程图如下:


![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dcb1b3ca6f914163bd8ec777c6903497~tplv-k3u1fbpfcp-watermark.image?)


## 七、总结
zookeeper也并非是分布式一致性的银弹,心跳维持在网络抖动等场景,节点会因为超时未发送心跳出现假性死亡。在生产中,建议根据自己的业务需求,灵活合理地选择合适的分布式锁方案。

## 八、参考资料
- [Redis、ZK分布式锁的前世今生](https://juejin.cn/post/7038473714970656775)
- [分布式共识算法ZAB](https://zhuanlan.zhihu.com/p/443717026)
- [容器运行时docker与服务编排](https://juejin.cn/post/7083742011576025118)


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值