分布式目录path锁实现方案

什么要实现分布式path锁(服务的多实例及并发下触发path不一致的场景,包括同级目录下不能重名)

分布式目录path锁实现方案

什么并发场景会触发path不一致

(1)更新并发冲突(两个用户同时更新的时候)

比如存在目录/d1,/d1/d2_1,/d1/d2_2,/d1/d2_3

用户同时修改目录d1的名称

目前的逻辑:通过/d1前缀匹配获取d1的所有子目录,假设两者几乎同步,然后修改上诉子目录的path,假设甲修改为d1_甲,已修改为d1_已

最后可能出现的现象:/d1_甲, /d1_甲/d2_1, /d1_已/d2_2, /d1_已/d2_3

现在的path是有问题的

 

(2)更新插入并发冲突

比如存在目录/d1,/d1/d2_1,/d1/d2_2,/d1/d2_3

用户甲修改d1为d1_甲,已用户在/d1/d2_3下添加新目录d3_1

可能会出现的现象/d1_甲, /d1_甲/d2_1, /d1_甲/d2_2, /d1_甲/d2_3, /d1/d2_3/d3_1

这样的最后一个path是有问题的

 

(3)并发插入目录冲突

比如用户甲,已同时在/dir1 下插入新目录dir2

两种几乎同时判断db是否重名(同时通过,因为dir2还没入库),出现path重名

 

 

解决思路:

(1)分布式悲观锁,假设上面的树是测试计划plan1的树(testplanId:plan-001,全局唯一)

  只有对上面的树操作->获取锁plan-001->成功->更新

 但是上述思路会造成大量的伪冲突,比如说同时修改dir2_1和dir3_5是不会冲突的

 

(2)分布式乐观锁

 什么情况下会冲突,当两个节点的更新修改的节点有重合部分(对应1),或者插入更新节点有重合部分(对应2)同一个父测试套节点的更新才会用冲突(对应3)

下面设置一个结构体来实现:借用读写锁的思路

我们给path节点状态标识:重入或者独占

rule1:若当前节点为无状态,也就是重入数为0,而且也不是独占状态,则改节点为无状态,无状态节点可独占,可重入(重入数+1)

rule2:若当前path节点状态为重入,则该节点可继续重入,重入数+1,但是不可独占

rule3:若当前path节点状态为独占,则改节点不可独占,也不可重入

可以用数据结构表示为

type Path struct {

    Path string        //路径

    ReentrantNum //重入数

    IsSync               //是否被独占

}

下面我们对节点做独占或者重入表示为获取节点资源

1)更新场景:当我们要对dir3_1节点做更新操作,path为:/dir1/dir2_1/dir3_1

     获取节点资源:

     尝试对当前节点path占用/dir1/dir2_1/dir3_1(防止1,2,3),对父亲节点path占用/dir1/dir2_1(防止2,3),对其他节点/dir1重入(防止伪冲突,也防止了1和3)

     有一个节点资源获取失败,报冲突,释放以获取资源,都成功了,执行更新操作

2)插入场景:当我们要在dir3_1节点下插入新节点,path为/dir1/dir2_1/dir3_1

     获取节点资源:

     尝试对父亲节点/dir1/dir2_1/dir3_1独占,对其他节点/dir1/dir2_1, /dir1重入

     有一个节点资源获取失败,报冲突,释放以获取资源,都成功了,执行插入操作

 

因为服务是多实例部署的,因此需要保证上述操作的进程并发安全性,原子性

实现上述方案可用的工具(借助第三方存储redis,zookeeper)

要借助第三方存储实现的功能:

(1)获取节点资源,也就是执行rule1,rule2,rule3时需要保证并发安全性(reids或者zookeeper提供上述原子性操作,或者通过分布式锁来实现)

(2)要考虑服务重启(宕机带来的问题),比如某个服务实例获取节点资源(重入/dir1),然后服务挂了,宕机了,要保证之前重入的资源要释放掉

 

 

 

 

 

 

 

 

--------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

zookeeper实现方案:

zookeeper的数据模型也是树,完美切合path场景,而且具有同级目录不能同名的优点,一次只能创建一级目录

 

1.测试套及目录同级目录不能同名以及并发下导致path不一致的现象

 

(1)针对修改的场景:

比如dir3: 当前path:/dir1/dir2/dir3, 把dir3修改成dir3_new

 

尝试创建目录/dir1,/dir1/dir2,/dir1/dir2/dir3(最大深度为15(O(1)时间操作))

都能成功,抢占锁资源成功

 

 

失败,释放掉获取的锁资源(也就是被别人占用了),提示并发修改冲突

 

获取资源成功的场景:判断同级目录重名->执行修改操作->释放锁资源(删除目录)

 

 

当然上诉还需要加一个CAS(Compare and Swap逻辑),因为存在可能通过id获取到path时后一瞬间,另外一个用户更新成功了,因此获取到锁资源后,判断path是否在获取锁资源过程中被修改,如果修改了,释放资源,获取新path,获取资源,判断path是否修改

(显然这是一个while(true)循环,直到成功,可以设置尝试次数)

 

 

(2)针对插入的场景,同修改

比如在dir2,path为/dir1/dir2 添加dir3,处理流程如同修改

 

 

上述方案会造成一点伪冲突现象(插入的冲突应该是互不影响的,但是按照上述方案确冲突了,应该只考虑插入和更新的相互影响,更新和更新的相互影响,优化方案后续完善)

 

考虑不同测试计划(不同分支下一定不会相互影响),所有要加前缀路径作为命名空间,而且是统一的命名规范,用来实现分段锁逻辑,比如

/project1(testcenterdev)/master/dirlock(代码创建) 后面才是具体的路径(只是示例,后面具体讨论)

 

 

 

2.测试计划在一个项目下(工作区下)不能同名,用例库分支名(一个用例库不能同名),这个只要做同级目录不能同名的分布式锁

原理如同1,但是命名空间不一样

如测试计划:

/testplan/planlock  后面加工作区下

 

比如修改测试计划plan1,工作区为testcenterdev

代码流程:zookeeper尝试创建目录/testplan/planlock/testcenterdev(乐观式控制并发)->判断重名(数据库层面)->测试套修改逻辑->创建测试计划->释放锁资源删除目录

 

 

 

 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 redis实现方案

 整体方案(1) 采用哈希表(hash)数据结构,map[string][string]value(其中value值在优化时会有作用),最外层的key是命名空间,用于区分设计计划,用例库等不同服务及不同测试计划和不同用例库(实现分段锁逻辑),采用命令HSETNX:key值不存在才创建

 

 为什么命名空间包含测试计划Id,因为不同测试计划下是不会相互影响的,就是为了减少冲突,把锁资源分段

 (1)命名空间规划(只是实例,具体怎么命名待讨论)

 比如测试计划(id为testcenter-1200)的测试套的修改

 testplan_suit_update_testcenter-1200(测试计划id)

 

 比如测试计划(项目Id也就是工作区为testcenterdev)自身名称的修改,第二级key为工作区(testcenterdev)

 testplan_plan_update

 

 

 (2)针对修改场景,实现方案类似zookeeper,测试计划id为testplan_01

 比如dir3: 当前path:/dir1/dir2/dir3, 把dir3修改成dir3_new(最多15层,最多发送15次,优化考虑一次发送15次请求,待研究)

 1)HSETNX testplan_suit_update_testplan_01 /dir1  1     

 2)HSETNX testplan_suit_update_testplan_01 /dir1/dir2 1

 3)HSETNX testplan_suit_update_testplan_01 /dir1/dir2/dir3 1

 如果都设置成功了,意味着获取资源成功,其中某一步失败了,要把获取的资源释放掉

 比如说1),2)成功了,但是3)失败了,提示并发修改冲突, 执行HDEL testplan_suit_update_testplan_01 /dir1 /dir1/dir2 释放资源

 

 同理,这里也要加一个CAS

 

 

 获取资源成功后->判断重名校验(走db)—> 修改->释放资源

 

(3)插入场景类似修改

 

 

(4)测试计划(工作区为testcenterdev)自身名称修改(会伴随测试套更新走1逻辑)

测试计划自身逻辑为

1)HSETNX testplan_plan_update testcenterdev 1

成功后—>重名校验->走测试套更新逻辑->修改->释放资源

 

 

 

 

 

 

 

 整体方案(2)不推荐(HGETALL会拿到大量数据,造成一定的阻塞,rula脚本通过)

 采用哈希表,通过rula脚本及命令HGETALL获取所有的key,value,在通过path前缀匹配来判断资源冲突(rula脚本是为了实现操作的并发下安全,不一定能行,只是一个实现思路,待讨论并且性能上会带有一定的阻塞)

 

 

 

 

 

 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 优化伪冲突

 按照上诉redis的方案1,会有伪冲突

 比如:

 插入和插入的伪冲突:

 比如在/dir1/dir2  和/dir1/dir3下同时插入新目录,这是不会相互影响的,但是按照上述方案冲突了

 

 更新和更新的伪冲突:

 比如在/dir1/dir2/dir3 的dir3修改为dir3_new, 在/dir1/dir2/dir4/dir5 的dir5修改为dir5_new ,这是不会相互影响的,按照上述方案冲突了

 

 

 为了解决这个问题引入两个概念:

 独占节点:当节点为独占时,不可重入,不可在被占用

 可重入节点:当节点为重入时,可重入,不可独占

 

 

 value值作用及含义:

 0:表示没有占用

 -1:表示独占

 >1:表示重入了多少次

 

 优化redis方案1

 当把/dir1/dir2/dir3 的dir3修改为dir3_new

 需要把当前节点设置为独占/dir1/dir2/dir3(并发写冲突), 把父亲节点设置为独占(同级目录不能重名),剩下的节点设置为重入(避免伪冲突)

 

 因此释放资源就不是上述的直接删除节点了,独占的可以删除,重入的是把value值减1,当value值为0时可以删除

 

 

 

 上诉优化方案意味着要保证判断节点是否存在,对节点的value值做处理及判断等,这些操作也是要保证并发安全性的(redis没有提供复杂的原子性命令,或许可以通过rula脚本,pipline或者事务实现,下面介绍第三中实现)

 具体采用哪一种需要一起讨论下,下面介绍第三种实现

 

 

 

 对一个节点做是否存在,对其value值做操作,删除key节点(避免key值太多,当value为0时是可以删除的)等一系列操作,需要先获取改节点权限(也就是锁)

 这个而通过zookeeper的临时节点实现(和会话绑定,会话关闭则节点自动删掉,避免死锁),也可以通过redis实现(设置超时,避免服务崩溃导致的死锁),下面介绍redis的实现方案

 类似上面redis方案1

 采用set数据结构(当然也可以采用hash)

 Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。

 利用setnx命令实现分布式锁

 

 为了区分与上面资源容器的区别,命名空间需要好好设计

如果要修改测试计划(plan-01)下,/plan1/suit1/suit2 的suit2改为suit2_new 

要把/plan1/suit1/suit2,/plan1/suit1的节点设置为独占,把/plan1/suit1/suit2节点的重入数+1

设置/plan1/suit1/suit2节点状态之前,需要获取设置该节点的锁

 (1)命名空间(举列子,后面需要好好设计)

 Setnx testplan_suit_common_lock_plan-01 /plan1/suit1/suit2

 设置成功了表示获取到了锁,尝试修改节点状态

 

 (2)冲突情况

 当节点为独占时, 在对该节点独占或者重入则冲突,提示同时修改冲突

 当节点为重入时,在对该节点独占则冲突,对该节点重入则value值+1

 

 

 

 

 上述方案如何避免某个实例宕机(导致资源锁没释放)或者redis某个实例宕机,重新选举master(使得某个获取锁的写数据没有同步到slave节点等问题)

 需要在讨论下

 

 

 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 redis实现方案总结:

 1.redis分布式锁+资源状态标记实现(需要考虑某个实例宕机导致的死锁问题)

 2.redis lua脚本+资源状态标记实现

 3.redis事务+watch+资源状态标记实现(待讨论,redis的事务有坑,而且能否实现需求有待讨论研究)

 

分布式目录path锁实现方案

 

综上所述:也就两种方案

(1)(分支锁(测试计划级别的锁),会有伪冲突)

当某个分支下的目录并发更新或者插入,都悲观的认为会有冲突

redis 的setNx + 超时机制 + 客户端(随机码)来实现

链接:https://baijiahao.baidu.com/s?id=1623086259657780069&wfr=spider&for=pc

(2)redis分布式锁(lua脚本)+资源状态标记实现(需要考虑某个实例宕机导致的锁资源不释放问题),但是目前没法解决服务宕机问题(或者服务重新部署带来的问题)

 场景分析:修改dir3_1

独占节点dir3_1,重入节点dir2_1和dir1,为啥dir2_1和dir1需要重入,就是为了实现dir3_1 和dir1的并发修改冲突,但是dir3_1和dir3_4的并发修改不冲突

 

 

 

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 zookeeper实现方案总结(通过临时节点解决实例宕机导致的锁资源不释放的问题)(推荐)

 资源标记(重入+独占)+分布式锁(对资源操作的并发安全性)+临时节点实现

 zookeeper临时节点特性:会话关闭,节点就删掉(可以用这个特性实现资源释放及防止宕机)

 

 

 

 testplanId:plan-0001

 suitId:suit-001,name:dir3  修改为dir2_new

 通过suitId获取path,当前path:/dir1/dir2/dir3, 把dir3修改成dir3_new

 按照上述可以避免伪冲突的情况,需要把节点/dir1/dir2/dir3独占,把节点/dir1/dir2独占,把节点/dir1重入

 考虑命名空间:节点后可以添加测试计划ID,变成/dir1/dir2/dir3_plan-0001独占,把节点/dir1/dir2_plan-0001独占,把节点/dir1_plan-0001重入

 

 

 如何用zookeeper实现重入和独占逻辑(zookeeper节点名称不支持/,可以把节点处理下,比如_dir1_dir2_dir3_plan-0001)

 /testcenter/testplan/suitnode/sync

 用于保存独占节点

 

 /testcenter/testplan/suitnode/reentrant

 用于保存重入几点

 

 独占场景:对节点_dir1_dir2_dir3_plan-0001做独占:

 (1)判断节点是否重入,重入了报冲突(判断/testcenter/testplan/suitnode/reentrant/_dir1_dir2_dir3_plan-0001是否存在,不存在或者子节点的数量为0表示没有重入)

     获取子节点数量 ls /testcenter/testplan/suitnode/reentrant/_dir1_dir2_dir3_plan-0001 返回的是列表,判断列表长度

 (2)create -e /testcenter/testplan/suitnode/sync/_dir1_dir2_dir3_plan-0001

 直接在/testcenter/testplan/suitnode/sync节点下创建临时节点_dir1_dir2_dir3_plan-0001,创建成功则成功独占,失败意味着已经被其他用户独占

 

 重入场景:这个比较复杂和麻烦,应为节点可以被多个客户端重入,对节点_dir1_dir2_dir3_plan-0001重入,这里需要用到临时顺序节点,重入编号让zookeeper给我们生成

 当然也可以使用临时节点,自己生成重入编号(保证不会重复并全局唯一就行)

 (1)判断该节点是否已经被独占,如果已经被独占,冲突,如果

 (2)判断目录是否存在/testcenter/testplan/suitnode/reentrant/_dir1_dir2_dir3_plan-0001,不存在则创建

 (3)创建重入编号 create -es /testcenter/testplan/suitnode/reentrant_dir1_dir2_dir3_plan-0001/(改命令会自动创建一个节点/testcenter/testplan/suitnode/reentrant_dir1_dir2_dir3_plan-0001/00001)

    

 

 当然上诉方案有两个问题:

 

 (1)定时删除 重入节点/testcenter/testplan/suitnode/reentrant/_dir1_dir2_dir3_plan-0001什么时候可以删除,防止节点数过多(凌晨启动定时任务,当/testcenter/testplan/suitnode/reentrant/下面的节点的子接点数为空,删除)

  ls /testcenter/testplan/suitnode/reentrant 获取到子节点列表 [_dir1_dir2_plan-0001, _dir1_dir2_plan-0002]

  遍历上述列表:

  获取节点锁(_dir1_dir2_plan-0001)->判断该节点是否存在(stat /testcenter/testplan/suitnode/reentrant/_dir1_dir2_plan-0001)->判断该节点下是否存在子节点(ls /testcenter/testplan/suitnode/reentrant/_dir1_dir2_plan-0001 返回的列表)->判断列表长度是否为0->是->删除该节点->释放锁节点

 

  

 (2)需要分布式锁

 保证重入场景(1)(2)(3)的原子性也就是并发安全性,对资源节点的操作是并发安全的,因此还需要加分布式锁(也通过zookeeper实现)

 也就是前面说的获取节点锁,下面说的是用临时节点实现的非阻塞的场景,也能减少节点数量

 在目录/testcenter/testplan/suitlock下创建临时节点

 获取节点锁(_dir1_dir2_plan-0001): create -e /testcenter/testplan/suitlock/_dir1_dir2_plan-0001 成功则获取锁,失败可以尝试一定的次数+时间(比如0.5s试一次,尝试4次),如果失败返回并发冲突

 

 当然获取锁的流程需要加上CAS操作 获取成功->通过suitId获取path->判断path是否变更(有可能在获取_dir1_dir2_plan-0001操作权限的一瞬间另外一个用户更新完毕了)->变更(/dir1/dir2_new)->释放锁资源_dir1_dir2_plan-0001(delete)->尝试获取_dir1_dir2_new_plan-0001的操作权限(原始节点)

 显然上面是一个while(true),也就是CAS逻辑,可以设置最大尝试次数

 

 

 

 1.测试套更新场景描述:

 实现思路:对当前path及父path独占(如果是顶层节点,则对当前path独占),其他节点重入

 testplanId:p-1

 testsuitId:s-1

 name:suit3

 path:/suit1/suit2/suit3(通过testsuitId获取)

 按上述规则转换:_suit1_suit2_suit3_p-1

 将上诉测试套name改成suit3_new

 

 代码流程:获取节点_suit1_suit2_suit3_p-1锁->尝试对该节点独占->获取_suit1_suit2_p-1节点锁->尝试对该节点独占->获取_suit1_p-1节点锁->尝试对该节点重入

 如果都成功了,成功获取资源,进行更新操作

 

 

2.测试套插入场景描述:

 类似更新:

 实现思路,对父亲节点独占,其他节点重入

 

 

 以上就能实现测试套所有插入修改导致的并发一致性问题

 

 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 下面讨论测试计划同一个项目下不能重名

 这边简单通过分布式锁实现(类似上面的测试套分布式锁,只是区分开会好点,可扩展,原理是一样的,只是在不同的目录下,也就是命名空间不同)

 不同的是没有CAS操作,英文工作区ID获取项目ID不会变

 租户为:root(目前是没有多租户的,加进去可扩展)

 项目ID为:testcenterdev

 

 根目录为:/testcenter/testplan/planlock

 获取锁操作:create -e /testcenter/testplan/planlock/root_testcenterdev ,失败可以尝试一定的次数+时间(比如0.5s试一次,尝试4次),如果失败返回并发冲突

 

 3.新建测试计划场景

 租户为:root

 项目ID为:testcenterdev

 获取计划锁(create -e /testcenter/testplan/planlock/root_testcenterdev)->成功则进行插入

 

 

 4.修过测试计划名称(需要递归修过测试计划所在树的path)

 租户为:root

 项目ID为:testcenterdev

 测试计划名称:plan1 修过为plan_new

 

 获取计划锁(create -e /testcenter/testplan/planlock/root_testcenterdev)->1测试套更新场景(修过顶层虚拟套)->都成功了则更新测试计划的名称

 (这里有个问题,就是path更新成功了,但是测试计划更新接口失败了,还有一个回退操作)

 如何解决这个问题:

 (1)目前测试计划这边name path并没有多大的作用->转成id path(转成id path后几乎解决了所有问题,只需要考虑测试计划锁)

 (2)获取像用例库一样,测试计划对应虚拟套(顶层套)的为path"",而不是/+测试计划名称(这个改动意味着所有通过path做递归查询的接口需要加测试计划ID做过滤)

 (3)保持不变,需要考虑回退操作

 

 

 

---------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

特别注意:不能使用池子,但是要控制并发的数量(防止并发量太大把zookeeper的连接用完了)

这个实现后面再完善,可以先不考虑

因为上述方案都是用临时节点实现的,通过会话的关闭来实现锁的释放或者重入次数的释放,所有每一个请求如果涉及到上诉对zookeeper的操作,应该调用一个接口获取一个zookeeper会话

这个接口要实现的功能是:(比如)

上一篇:zookeeper


下一篇:Linux 文件与目录相关指令