【仅供内部供应商使用,不提供对外解答和培训】

一、使用场景

使用共享存储的场景下,集群多个节点访问同一个文件时,为了保证避免数据错误,需要加锁对进程访问文件进行控制。

二、文件锁方案

1、FileLock

对于File,java提供了FileLock用于直接锁定FileChannel的region,来实现文件锁的效果。

FileLock
    FileChannel channel = new FileOutputStream(LOG_FILE_NAME, true).getChannel();
    //第三个参数是文件锁是否共享,若系统不支持,将会采用排他锁
    FileLock fileLock = channel.lock(0L, Long.MAX_VALUE, true);
    fileLock = channel.lock();
    //read write file
    fileLock.release();

在我们的集群场景中,由于后端资源文件存储并非某个特定的存储介质,因此使用FileLock只能只能适用于使用系统FileSystem,而使用FileSystem的情况下的存储使用的是同步而非共享,因此不适合。

2、基于zookeeper的分布式锁

另外一种比较常用的方式是利用zookeeper的临时节点特性:客户端连接断开自动删除该客户端创建的临时节点,在zk的dataDir下的文件同名节点创建临时有序子节点,序号最前的获得读写该文件的锁。

具体的创建临时节点:

创建临时节点
private String createLockNode(ZkClient client, String path) throws LockException {
    // 创建临时循序节点
    try {
        return client.createEphemeralSequential(path, null);
    } catch (ZkNoNodeException e) {
        //
    }


    throw new LockException("Create ephemeral sequential node failed!");
}

监视前一节点的删除,立即抢占锁

监视前一节点
//被监视的节点
String watchPath = getWatchPath(getSortedChildren(), currentNode);
final CountDownLatch latch = new CountDownLatch(1);
//前一节点监听处理
final IZkDataListener previousNodeListener = new IZkDataListener() {
    public void handleDataDeleted(String dataPath) throws Exception {
        latch.countDown();//前一个删掉,等待锁
    }

    public void handleDataChange(String dataPath, Object data) throws Exception {
        //do nothing
    }
};


zkClient.subscribeDataChanges(watchPath, previousNodeListener);//监听前一个节点

 

优点是使用共享存储时,不需要考虑存储的具体实现,而是在平台的文件访问层面设置访问标志控制,速度快,无服务器和存储通信的网络延迟。这个方式不仅仅适用于共享存储方案,也适用于各自使用本机文件系统。

缺点是集群需要部署zookeeper,考虑到集群可能使用其它zk的功能也需要不是zk,可行性比较高。

3、基于抽象存储的文件锁

仿照zookeeper的思路,在共享存储中需要加锁的文件的同名目录下创建一个锁文件,另一个进程需要访问这个文件时,根据锁情况(读写)需要等待或者获得锁。

创建锁文件
private String createLockFile() {
    int seq = 0;
    //不存在先创建锁文件夹,rr为抽象存储
    if (!rr.exist(lockFilesPath)) {
        rr.createDirectory(lockFilesPath);
    }

    List<String> lockFiles = listLockFiles(lockFilesPath);
    try {

        if (lockFiles.size() > 0) {
            String last = lockFiles.get(lockFiles.size() - 1);
            seq = getSeq(last) + 1;
        }

        String lockFileName = lockFilesPath + lockPrefix + seq;
        if (rr.create(lockFileName)) {
            return lockFileName;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }


    return null;
}


1)例如当前节点机器对WEB-INF/reportlets/WorkBook13.cpt 文件加锁,则创建一个WEB-INF/reportlets/WorkBook13.cpt/的路径;

2)多个节点在该路径下创建锁文件如write-lock-01,read-lock-02, read-lock-03,最后的数字代表创建顺序,对于读锁,没有写锁时,共享锁,对于写锁,排在最前时获得锁;

3)等待锁,一直轮训当前的锁文件排序是否为最小,最小获得锁,超时删除锁文件,加锁失败;

这样做有两个问题:

1)当jvm进程在获得锁之后的过程中意外关闭,会导致锁文件没有及时删除,其它进程永远获取不到锁;

2)网络通信的延迟或者故障,导致创建或者删除锁文件感知有延迟带来的性能问题。

这个方案需要考虑在进程意外关闭后锁文件的管理,实时性也是老大难,保持怀疑。

4、基于Redis的分布式锁

和zookeeper类似,在redis设置一个访问标志来控制文件的访问,与利用zk的临时节点特性不一样之处在于,基于redis的SETNX(SET if Not Exist)命令来实现:

1)  最基本的做法,使用该命令实现的排他锁,setnx key若key不存在,返回1,表示获得锁;一旦key存在,其它设置该key的命令均返回0,即获取锁失败;

2)setnx可以设置超时时间,一旦这个kv超时就会被删除,让出锁,避免死锁;

3)全部为排他锁比较低效,读锁共享。

 

  • No labels