分布式锁的实现

300 阅读5分钟

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

锁具有的条件

  • 互斥性:同一时刻,某个方法只能有一个线程获得锁
  • 不能死锁:客户端即使发生崩溃,锁也能自己释放
  • 锁的高可用:锁不能随便挂掉
  • 只有锁的持有者才能释放锁

实现的选择

Redis

思路

  • 使用sexnx命令设置kv,如果当前key不存在,则拿到锁,否则获取不到锁。满足了互斥性。
  • 通过expire命令,给锁设置过期时间,若客户端出错,保证锁可以自己释放。
  • 设置value值为当前操作人本身,释放锁的时候去判断是否为当前锁持有线程。
  • 逻辑代码超时导致key删除,需要监听key超时时间

代码如下

public class RedisLock {

    private static final String LOCK_SUCCESS = "OK";

    /**
     * NX -- Only set the key if it does not already exist
     */
    private static final String NX = "NX";

    /**
     * expire time units: EX = seconds
     */
    private static final String EX = "EX";

    /**
     * 是否监听key是否过期
     */
    protected static volatile boolean isOpenExpirationRenewal = true;

    /**
     * 加锁
     *
     * @param lockKey 锁key
     * @param time    锁的时间
     * @return
     */
    public static void lock(Jedis jedis, String lockKey, long time) {
        while (true) {
            // 获取当前线程id,并设置value
            String threadName = Thread.currentThread().getName();
            // 加锁并设置锁的过期时间
            String result = jedis.set(lockKey, threadName, NX, EX, time);
            if (LOCK_SUCCESS.equals(result)) {
                System.out.println("线程:" + Thread.currentThread().getName() + "获取锁成功!时间:" + LocalTime.now());
                scheduleExpirationRenewal(jedis, lockKey);
                break;
            }
            System.out.println("线程:" + Thread.currentThread().getName() + "获取锁失败,休眠2秒!时间:" + LocalTime.now());
            //休眠2秒
            sleepBySecond(2);
        }
    }

    private static void sleepBySecond(long time) {
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void unlock(Jedis jedis, String lockKey) {
        // 判断当前key是否存在,如果存在则释放key
        String threadName = Thread.currentThread().getName();
        // 使用lua脚本进行原子删除操作
        String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) " +
                "else " +
                "return 0 " +
                "end";
        jedis.eval(checkAndDelScript, 1, lockKey, threadName);
        System.out.println("线程:" + Thread.currentThread().getName() + "释放锁成功");
    }

    /**
     * 开启定时刷新
     */
    protected static void scheduleExpirationRenewal(Jedis jedis, String lockKey) {
        String threadName = Thread.currentThread().getName();
        Thread renewalThread = new Thread(new ExpirationRenewal(jedis, lockKey, threadName));
        renewalThread.start();
    }


    /**
     * 刷新key的过期时间
     */
    private static class ExpirationRenewal implements Runnable {

        private Jedis jedis;

        private String lockKey;

        private String threadName;

        public ExpirationRenewal(Jedis jedis, String lockKey, String threadName) {
            this.jedis = jedis;
            this.lockKey = lockKey;
            this.threadName = threadName;
        }

        @Override
        public void run() {
            while (isOpenExpirationRenewal) {
                System.out.println("执行延迟失效时间中...");

                String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 end";
                jedis.eval(checkAndExpireScript, 1, lockKey, threadName, "5");

                //休眠30秒
                sleepBySecond(30);
            }
        }
    }

}

测试类

//定义线程池
ExecutorService pool = Executors.newFixedThreadPool(10);

//添加10个线程获取锁
for (int i = 0; i < 10; i++) {
    pool.submit(() -> {
        try {
            Jedis jedis = new Jedis("192.168.126.20", 6379, 10000 * 5);
            String lockKey = "doSomeThing";
            RedisLock.lock(jedis, lockKey, 3);
            System.out.println(Thread.currentThread().getName() + "开始执行业务");
            // do something
            TimeUnit.SECONDS.toSeconds(5L);
            System.out.println(Thread.currentThread().getName() + "业务执行完毕");
            RedisLock.unlock(jedis, lockKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}
pool.shutdown();

Zookeeper

思路

  • 使用临时节点,保证不会死锁
  • 使用有序节点,每个节点始终监听上个节点,判断节点变化
  • zookeeper集群保证高可用

基于zkclient实现

public class ZKLock implements Watcher {

    private ZooKeeper zooKeeper;

    private CountDownLatch countDownLatch;
    
    public static final String ROOT_PATH = "/lock";

    /**
     * 当前等待节点
     */
    private String waitLockNode;

    /**
     * 锁的前缀
     */
    private final String preLockNode = "/lock_";

    /**
     * 当前节点
     */
    private String currentLockNode;

    public ZKLock(ZooKeeper zooKeeper) {
        this.zooKeeper = zooKeeper;
    }

    /**
     * 加锁
     */
    public boolean lock() {
        if (tryLock()) {
            System.out.println("线程" + Thread.currentThread().getName() + "获取锁" + currentLockNode + "成功");
            return true;
        }
        return waitLock();
    }

    /**
     * 如果不是当前锁,则等待锁的释放
     */
    public boolean waitLock() {
        // 判断等待锁的节点是否存在
        String currentWaitLock = ROOT_PATH + this.waitLockNode;
        try {
            Stat stat = zooKeeper.exists(currentWaitLock, this);
            if (stat != null) {
                // 监听的节点存在,阻断线程的执行
                System.out.println("线程" + Thread.currentThread().getName() + "加锁失败,等待" + currentWaitLock);
                // 设置计数器,使用计数器阻塞线程
                countDownLatch = new CountDownLatch(1);
                countDownLatch.await();
                System.out.println("线程" + Thread.currentThread().getName() + "加锁成功,锁" + currentWaitLock + "已经释放");
                return true;
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 尝试给指定路径加锁
     *
     * @throws KeeperException
     * @throws InterruptedException
     */
    private boolean tryLock() {
        // 给当前线程创建锁,并赋值给当前节点,采用有序临时节点
        try {
            this.currentLockNode = zooKeeper.create(ROOT_PATH + preLockNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // 获取锁路径下的所有节点,并排序
            List<String> children = zooKeeper.getChildren(ROOT_PATH, false);
            if (!CollectionUtils.isEmpty(children)) {
                // 存在子节点,进行排序
                Collections.sort(children);
                // 判断是否是最小子节点
                String minNode = children.get(0);
                if ((ROOT_PATH + "/" + minNode).equals(this.currentLockNode)) {
                    // 属于当前线程,放过继续执行
                    return true;
                }
                // 获取当前节点的上个节点,并对其进行监听
                String currentLockNodeNode = this.currentLockNode.substring(this.currentLockNode.lastIndexOf("/") + 1);
                int preNodeIndex = Collections.binarySearch(children, currentLockNodeNode) - 1;
                this.waitLockNode = "/" + children.get(preNodeIndex);
                System.out.println("线程" + Thread.currentThread().getName() + "等待锁:" + ROOT_PATH + this.waitLockNode);
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 释放锁
     */
    public void unlock() {
        try {
            Stat stat = zooKeeper.exists(this.currentLockNode, null);
            if (stat != null) {
                // 删除当前节点
                System.out.println(Thread.currentThread().getName() + "开始释放锁:" + currentLockNode);
                zooKeeper.delete(this.currentLockNode, -1);
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void process(WatchedEvent event) {
        // 监听根节点下是否有目录删除
        if (countDownLatch != null && event.getType().equals(Event.EventType.NodeDeleted)) {
            // 计数器减一,恢复线程操作
            countDownLatch.countDown();
        }
    }
}

基于curator实现

使用curator封装好的分布式锁结合注解AOP来实现

注入CuratorFramework对象到Spring容器

@Component
public class ZKConfig {

    @Bean
    public CuratorFramework curatorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                .connectString("192.168.126.30:2181")
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(5000)
                .retryPolicy(retryPolicy)
                .build();
        curatorFramework.start();
        return curatorFramework;
    }

自定义注解,使用该注解标记某方法使用分布式锁

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    String lockPath();
}

编写AOP,对@DistributedLock注解方法进行切面

@Aspect
@Component
public class DistributedLockAspect implements ApplicationContextAware {

    private ApplicationContext context;

    private CuratorFramework curatorFramework;

    private static final String lockRootPath = "/lock";

    @Pointcut("@annotation(com.sun.zk.lock.DistributedLock)")
    public void methodAspect() {

    }

    @Around(value = "execution(* com.sun.zk.client..*(..)) && @annotation(DistributedLock)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取分布式锁注解DistributedLock的锁的路径
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = joinPoint.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        String lockPath = distributedLock.lockPath();
        // 锁的路径
        lockPath = lockRootPath + lockPath;
        // 创建分布式可重入互斥锁
        InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, lockPath);
        try {
            // 获取到互斥锁为true
            boolean acquire = interProcessMutex.acquire(1000, TimeUnit.SECONDS);
            System.out.println("线程" + Thread.currentThread().getName() + "获取锁:" + acquire);
            if (acquire) {
                return joinPoint.proceed();
            }
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            interProcessMutex.release();
            System.out.println("线程" + Thread.currentThread().getName() + "释放锁");
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
        curatorFramework = context.getBean(CuratorFramework.class);
    }

    @PreDestroy
    public void destroy(){
        CloseableUtils.closeQuietly(curatorFramework);
    }
}