通俗易懂地介绍分布式锁实现

11次阅读

共计 3085 个字符,预计需要花费 8 分钟才能阅读完成。

文章来源:www.liangsonghua.me
作者介绍:京东资深工程师 - 梁松华,长期关注稳定性保障、敏捷开发、JAVA 高级、微服务架构

一般情况下我们会通过下面的方法进行资源的一致性保护

// THIS CODE IS BROKEN
function writeData(filename, data) {var lock = lockService.acquireLock(filename);
    if (!lock) {throw 'Failed to acquire lock';}

    try {var file = storage.readFile(filename);
        var updated = updateContents(file, data);
        storage.writeFile(filename, updated);
    } finally {lock.release();
    }
}

但是很遗憾的是,上面这段代码是不安全的,比如客户端 client- 1 获取锁后由于执行垃圾回收 GC 导致一段时间的停顿(stop-the-word GC pause)或者其他长时间阻塞操作,此时锁过期了,其他客户如 client- 2 会获得锁,当 client- 1 恢复后就会出现 client-1client- 2 同时处理获得锁的状态

我们可能会想到通过令牌或者叫版本号的方式,然而在使用 Redis 作为锁服务时并不能解决上述的问题。不管我们怎么修改 Redlock 生成 token 的算法,使用 unique random 随机数是不安全的,使用引用计数也是不安全的,一个 redis node 服务可能会出宕机,多个 redis node 服务可能会出现同步异常(go out of sync)。Redlock 锁会失效的根本原因是 Redis 使用 getimeofday 作为 key 缓存失效时间而不是监视器(monitonic lock),服务器的时钟出现异常回退无法百分百避免,ntp 分布式时间服务也是个难点

分布式锁实现需要考虑锁的排它性和不能释放它人的锁,作者不推荐使用 Redlock 算法,推荐使用 zookeeper 或者数据库事务(个人不推荐:for update 性能太差了)

补充:使用 zookeeper 实现分布式锁

可以通过客户端尝试创建节点路径,成功就获得锁,但是性能较差。更好的方式是利用 zookeeper 有序临时节点,最小序列获得锁,其他节点 lock 时需要阻塞等待前一个节点 (比自身序列小的最大那个) 释放锁(countDownLatch.wait()),当触发 watch 事件时将计数器减一(countDownLatch.countDown()), 然后此时最小序列节点将会获得锁。可以利用 Curator 简化操作,示例如下

public static void main(String[] args) throws Exception {
            // 重试策略
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

            // 创建工厂连接
            final CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(connetString)
                    .sessionTimeoutMs(sessionTimeOut).retryPolicy(retryPolicy).build();

            curatorFramework.start();

            // 创建分布式可重入排他锁,监听客户端为 curatorFramework,锁的根节点为 /locks
            final InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/lock");
            final CountDownLatch countDownLatch = new CountDownLatch(1);

            for (int i = 0; i < 100; i++) {new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {countDownLatch.await();
                            // 加锁
                            mutex.acquire();
                            process();} catch (Exception e) {e.printStackTrace();
                        }finally {
                            try {
                                // 释放锁
                                mutex.release();
                                System.out.println(Thread.currentThread().getName() + ": release lock");
                            } catch (Exception e) {e.printStackTrace();
                            }
                        }
                    }
                },"Thread" + i).start();}

            Thread.sleep(100);
            countDownLatch.countDown();}
    }

补充:redis 实现分布式锁

public enum  FreeLockUtil {
    instance;

    public static FreeLockUtil getInstance()
    {return instance;}

    @Autowired
    @Qualifier("jimClient")
    private Cluster jimClient;

    @Autowired
    private TdeUtil tdeUtil;

    private String scriptHash;

    @PostConstruct
    public void init() {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        scriptHash = jimClient.scriptLoad(script);
    }

    /** 
    * @Description: 没有获得锁时会返回空 
    * @Param: [key] 
    * @return: java.lang.String 
    * @Author: Pidan
    */
    public String lock(String lockKey)
    {String token = tdeUtil.random();
        // 不要将 set 和 expire 分开
        Boolean lockRes = jimClient.set(lockKey, token, 1L,TimeUnit.MINUTES, false);
        return lockRes?token:null;
    }
    /** 
    * @Description: 类似 CAS 版本号
    * @Param: [key, value] 
    * @return: void 
    * @Author: Pidan
    */
    public void unlock(String lockKey,String token)
    {
        // 不要在客户端使用 get-if-equals-del
      jimClient.evalsha(scriptHash, Collections.singletonList(lockKey),Collections.singletonList(token),true);
    }
}

不管是基于 Redis 或者是 Zookeeper 实现分布式锁都有各点的优缺点,Redis 的高并发是 Zookeeper 无法比拟的,但是 Redis 缓存的内存大小如果不足的话极有可能会导致信息丢失,反观使用 Zookeeper 实现分布式锁,会导致性能开销比较高,因为需要动态创建删除临时节点,频繁操作磁盘读写,不过它的可靠性更高

文章来源:www.liangsonghua.me
作者介绍:京东资深工程师 - 梁松华,长期关注稳定性保障、敏捷开发、JAVA 高级、微服务架构

正文完
 0