分布式阻塞锁

 

写个分布式阻塞锁吧...



背景

当多个线程会抢占同一资源时,如果为了保证结果的正确需要做排它操作,一般会使用jdk提供的synchronized关键字或Lock类来做加锁操作。但是他们只提供同一jvm内的互斥,对于分布式的情况下的java应用并不能达到期望的效果。我们希望能够设计实现一个能够在多jvm下强占资源的锁。

方案

目前系统上已经有一个能够正常工作的分布式锁,使用mysql作为单点,当申请资源失败时直接通知应用,核心功能可以用伪码表示为

try{

mysqlConnection.insert(resource);

app.doExecute();
}catch(ResourceExistsException exp){

throw exp;
}finally{

mysqlConnection.delete(resource);
}
如果在应用层对于申请资源失败的异常进行捕获并不断重试,就是最简单的阻塞策略的实现,然而如果资源抢占比较激烈,所有的线程都在不断的重试,会浪费大量的cpu。

另外一个存在的风险是,当某个节点获取锁成功之后宕机,由于mysql没有超时机制锁并不会被释放,会导致这个资源永远被占有,需要人工介入。对于这个问题可以通过获取锁之前检验锁的创建时间来解决

Date insertTime = mysqlConnection.select(resource);
if(now - insertTime > 10s){

mysqlConnection.del(resource, insertTime);
}
注意当过期时删除锁还需要加上insertTime,否则可能会出现多个线程同时获得锁成功的情况。

当然如果使用具有expire语义的单点控制器,如redis,就可以避免这个问题。实际上我的实现也是使用了redis。

实现

单点的set与delete

redis通过set nx指令提供类似于cas(null, val)的语义,通过ex设置过期时间。当某线程设置的锁过期之后,为了防止调用del删除调其他线程设置的值,每次set时会生成一个uuid作为val。删除时使用watch机制,保证原子性的情况下执行以下的操作:[a]如果当前key的val等同于当前线程申请资源时生成的uuid。[b]删除key。

等待线程的处理

每个jvm下会维护一个key为资源唯一标识,值为等待该资源线程的队列的一个哈希表。获取资源的线程通过set操作的返回值来判断自己是否可以继续运行,如果被阻塞则查询获取资源的阻塞队列进行入队操作,同时使当前线程进入休眠。

等待线程的恢复

通知开启键空间通知,可以令redis发布对指定规则的键进行指定操作的消息。每个jvm会订阅资源主键的del和expired事件,当收到消息时通过资源键去查询对应队列中是否有等待的线程,如果有让队首线程出队尝试set,成功则恢复,失败将其重新入队。

消息丢失补偿

假定某一时间redis到应用主机网络异常,一个资源键的删除通知没有成功发送到任一主机,那么到下一次同一资源申请set之前所有主机上阻塞在此资源的线程都无法恢复。为了解决这个问题会启用一个后台进程,每隔10秒钟扫描所有队列检测队首等待时间是否过长,如果过长则尝试让其set。

模块划分



  • DistributedLock:暴露给应用程序的api,如lock,unLock,tryLock等。
  • CentralController:单点控制器,为DistributedLock提供单点上set和delete的支持。

锁申请流程



遗留问题与后续计划

  • 增加本地线程可以不通过redis通知来强占锁的策略。
  • 当redis主备切换时,由于写盘无法同步会导致刚刚申请的资源键丢失。
  • 提供redis集群的支持,同时如果使用集群可以用类似paxos的机制解决上面的问题。
  • 提供可重入的支持。
  • 提供读写锁的支持。

仓库地址

https://github.com/ShikiRyougi/cactus


    关注 远虑与近忧的矛盾螺旋


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册