Zookeeper的分布式锁例子(Curator)

环境

  • Ubuntu 22.04
  • Zookeeper 3.7.1
  • JDK 17.0.3.1
  • IntelliJ IDEA 2022.1.3
  • Curator 5.4.0

准备

创建Maven项目 test1217

打开 pom.xml 文件,添加如下依赖:

        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.4.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.4.0</version>
        </dependency>

并发冲突

考虑一个应用场景:N个进程同时运行,并发访问某资源。

为了简单起见,用并发线程来模拟多进程。用整数10代表10个资源项。

如果不考虑并发冲突问题,代码如下:

package pkg4;

public class Test1 {
    private static int res = 10;

    public static void foo() {
        while (res > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + res);
            res--;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                foo();
            }).start();
        }
    }
}

运行结果如下:

Thread-2 : 10
Thread-0 : 10
Thread-1 : 10
Thread-2 : 7
Thread-1 : 6
Thread-0 : 5
Thread-2 : 4
Thread-1 : 4
Thread-0 : 2
Thread-2 : 1
Thread-0 : 1
Thread-1 : 1

Process finished with exit code 0

该程序模拟了3个客户端应用,并发访问资源。具体逻辑为:打印现有的资源数量,并消耗一个资源。

代码逻辑里没有做并发控制,从打印结果可见,多个客户端可能会获取到同一份资源,造成错误。

具体问题包括:

  • res > 0 在并发情况下,即使判断还有资源,后面打印资源数量以及消耗资源时,可能已经是0了。
  • res-- 不是原子操作,在并发情况下,可能会产生“字撕裂”的问题(如果没记错可以使用 volatile 关键字解决)。

synchronized关键字

要解决并发冲突,就要引入同步机制。最简单的办法是用 synchronized 关键字修饰 foo() 方法,但是这样做太暴力了,因为 while 循环是在 foo() 方法里面的,这种做法会导致只能有一个客户端获取所有资源。

因此,需要把“访问资源”的逻辑提出来,单独创建一个 synchronized 方法,如下:

package pkg4;

public class Test2 {
    private static int res = 10;

    public synchronized static boolean bar() {
        if (res > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + res);
            res--;

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return true;
        } else {
            return false;
        }
    }
    public static void foo() {
        while (true) {
            boolean succ = bar();

            if (!succ) break;

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                foo();
            }).start();
        }
    }
}

运行结果如下:

Thread-0 : 10
Thread-2 : 9
Thread-1 : 8
Thread-2 : 7
Thread-0 : 6
Thread-2 : 5
Thread-1 : 4
Thread-2 : 3
Thread-0 : 2
Thread-1 : 1

这回没有问题,多个客户端无序的交替获取资源。

注意:循环里面在调用 bar()方法之后,最好sleep一会儿,给别的客户端机会,否则貌似当前客户端会继续获取下一个资源,别的客户端抢占不过它。

Zookeeper分布式锁(Curator)

使用Curator的锁机制,如下:

package pkg4;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

import java.util.concurrent.TimeUnit;

public class Test3 {
    private static int res = 10;

    public static boolean bar() {
        if (res > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + res);
            res--;

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return true;
        } else {
            return false;
        }
    }
    public static void foo() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);

        CuratorFramework zkClient = CuratorFrameworkFactory.builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(60 * 1000)
                .connectionTimeoutMs(15 * 1000)
                .retryPolicy(retryPolicy)
                .namespace("test1217")
                .build();

        zkClient.start();

        try {
            InterProcessMutex lock = new InterProcessMutex(zkClient,"/myresource");

            while (true) {
                boolean gotLock = false;
                try {
                    gotLock = lock.acquire(3, TimeUnit.SECONDS);
                } catch (Exception e) {
                    gotLock = false;
                }

                if (gotLock) {
                    try {
                        boolean succ = bar();
                        if (!succ) break;

                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } finally {
                        try {
                            lock.release();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        } finally {
            zkClient.close();
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                foo();
            }).start();
        }
    }
}

运行结果如下:

Thread-1 : 10
Thread-0 : 9
Thread-2 : 8
Thread-1 : 7
Thread-0 : 6
Thread-2 : 5
Thread-1 : 4
Thread-0 : 3
Thread-2 : 2
Thread-1 : 1

可以看到3个客户端是严格交替获取锁,这是因为客户端一旦释放锁,其它正在等待的客户端就会立即获取锁,而本客户端再等待锁时,其顺序号是最大的,所以最晚获取锁。

前面用 synchronized 方法时,客户端交替是无序的,因为线程调度是无序的。

特别需要注意的是,3个客户端各自拥有自己的 zkClientlock 实例,客户端之间没有任何关联,这是为了模拟客户端进程之间没有任何关联。客户端各自独立访问Zookeeper。

main() 方法和 bar() 方法都没有变化( bar() 方法去掉了 synchronized 关键字)。只有 foo() 方法有修改。其实修改也很简单直接,就是在调用 bar() 的前后添加了获取锁和释放锁的操作,只不过加了一些 try...catch...finally ,显得代码多了一些。

此外,在程序运行期间,可以查看 /test1217/myresource 节点,比如:

ls /test1217/myresource
[_c_b6ad09df-597c-418f-bace-619a9a16369d-lock-0000000004, _c_c4a832f5-fb5c-432d-bf8f-bd7010b8571d-lock-0000000003, _c_d0e6779f-9d87-41ad-a66b-4b8eeab18bae-lock-0000000000]
get /test1217/myresource/_c_d0e6779f-9d87-41ad-a66b-4b8eeab18bae-lock-0000000000
127.0.1.1

可见,有3个顺序节点。

程序运行结束后,过一会儿再看,就会报错说 Node does not exist: /test1217/myresource ,可见是临时节点。

注:要想查看节点,最好把sleep时间设置大一些,或者设置断点,否则来不及看。

其它

我在网上看到一些Curator分布式锁的示例代码,比如:

package com.itheima.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

import java.util.concurrent.TimeUnit;

public class Ticket12306 implements Runnable{

    private int tickets = 10;//数据库的票数

    private InterProcessMutex lock ;

    public Ticket12306(){
        //重试策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        //2.第二种方式
        //CuratorFrameworkFactory.builder();
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("192.168.149.135:2181")
                .sessionTimeoutMs(60 * 1000)
                .connectionTimeoutMs(15 * 1000)
                .retryPolicy(retryPolicy)
                .build();

        //开启连接
        client.start();

        lock = new InterProcessMutex(client,"/lock");
    }

    @Override
    public void run() {
        while(true){
            //获取锁
            try {
                lock.acquire(3, TimeUnit.SECONDS);
                if(tickets > 0){

                    System.out.println(Thread.currentThread()+":"+tickets);
                    Thread.sleep(100);
                    tickets--;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //释放锁
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
package com.itheima.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class LockTest {
    public static void main(String[] args) {
        Ticket12306 ticket12306 = new Ticket12306();

        //创建客户端
        Thread t1 = new Thread(ticket12306,"携程");
        Thread t2 = new Thread(ticket12306,"飞猪");

        t1.start();
        t2.start();
    }
}

想法和疑问:

  • 没判断锁是否获取成功。
  • 没有关闭Zookeeper client连接。
  • 多个客户端使用同一个Zookeeper client和lock的实例,没有模拟“分布进程”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值