一、基于key/value实现

我们在构建分布式系统的时候,经常需要控制对共享资源的互斥访问。这个时候我们就涉及到分布式锁(也称为全局锁)的实现,基于目前的各种工具,我们已经有了大量的实现方式,比如:基于Redis的实现、基于Zookeeper的实现。本文将介绍一种基于Consul 的Key/Value存储来实现分布式锁以及信号量的方法。

分布式锁实现

基于Consul的分布式锁主要利用Key/Value存储API中的acquire和release操作来实现。acquire和release操作是类似Check-And-Set的操作:

- acquire操作只有当锁不存在持有者时才会返回true,并且set设置的Value值,同时执行操作的session会持有对该Key的锁,否则就返回false

- release操作则是使用指定的session来释放某个Key的锁,如果指定的session无效,那么会返回false,否则就会set设置Value值,并返回true

具体实现中主要使用了这几个Key/Value的API:

- create session:https://www.consul.io/api/session.html#session_create

- delete session:https://www.consul.io/api/session.html#delete-session

- KV acquire/release:https://www.consul.io/api/kv.html#create-update-key

基本流程

具体实现

public class Lock {

private static final String prefix = "lock/";  // 同步锁参数前缀

private ConsulClient consulClient;

private String sessionName;

private String sessionId = null;

private String lockKey;

/**

*

* @param consulClient

* @param sessionName   同步锁的session名称

* @param lockKey       同步锁在consul的KV存储中的Key路径,会自动增加prefix前缀,方便归类查询

*/

public Lock(ConsulClient consulClient, String sessionName, String lockKey) {

this.consulClient = consulClient;

this.sessionName = sessionName;

this.lockKey = prefix + lockKey;

}

/**

* 获取同步锁

*

* @param block     是否阻塞,直到获取到锁为止

* @return

*/

public Boolean lock(boolean block) {

if (sessionId != null) {

throw new RuntimeException(sessionId + " - Already locked!");

}

sessionId = createSession(sessionName);

while(true) {

PutParams putParams = new PutParams();

putParams.setAcquireSession(sessionId);

if(consulClient.setKVValue(lockKey, "lock:" + LocalDateTime.now(), putParams).getValue()) {

return true;

} else if(block) {

continue;

} else {

return false;

}

}

}

/**

* 释放同步锁

*

* @return

*/

public Boolean unlock() {

PutParams putParams = new PutParams();

putParams.setReleaseSession(sessionId);

boolean result = consulClient.setKVValue(lockKey, "unlock:" + LocalDateTime.now(), putParams).getValue();

consulClient.sessionDestroy(sessionId, null);

return result;

}

/**

* 创建session

* @param sessionName

* @return

*/

private String createSession(String sessionName) {

NewSession newSession = new NewSession();

newSession.setName(sessionName);

return consulClient.sessionCreate(newSession, null).getValue();

}

}

单元测试

下面单元测试的逻辑:通过线程的方式来模拟不同的分布式服务来竞争锁。多个处理线程同时以阻塞方式来申请分布式锁,当处理线程获得锁之后,Sleep一段随机事件,以模拟处理业务逻辑,处理完毕之后释放锁。

public class TestLock {

private Logger logger = Logger.getLogger(getClass());

@Test

public void testLock() throws Exception  {

new Thread(new LockRunner(1)).start();

new Thread(new LockRunner(2)).start();

new Thread(new LockRunner(3)).start();

new Thread(new LockRunner(4)).start();

new Thread(new LockRunner(5)).start();

Thread.sleep(200000L);

}

class LockRunner implements Runnable {

private Logger logger = Logger.getLogger(getClass());

private int flag;

public LockRunner(int flag) {

this.flag = flag;

}

@Override

public void run() {

Lock lock = new Lock(new ConsulClient(), "lock-session", "lock-key");

try {

if (lock.lock(true)) {

logger.info("Thread " + flag + " start!");

Thread.sleep(new Random().nextInt(3000L));

logger.info("Thread " + flag + " end!");

}

} catch (Exception e) {

e.printStackTrace();

} finally {

lock.unlock();

}

}

}

}

单元测试执行结果如下:

2017-04-12 21:28:09,698 INFO  [Thread-0] LockRunner - Thread 1 start!

2017-04-12 21:28:12,717 INFO  [Thread-0] LockRunner - Thread 1 end!

2017-04-12 21:28:13,219 INFO  [Thread-2] LockRunner - Thread 3 start!

2017-04-12 21:28:15,672 INFO  [Thread-2] LockRunner - Thread 3 end!

2017-04-12 21:28:15,735 INFO  [Thread-1] LockRunner - Thread 2 start!

2017-04-12 21:28:17,788 INFO  [Thread-1] LockRunner - Thread 2 end!

2017-04-12 21:28:18,249 INFO  [Thread-4] LockRunner - Thread 5 start!

2017-04-12 21:28:19,573 INFO  [Thread-4] LockRunner - Thread 5 end!

2017-04-12 21:28:19,757 INFO  [Thread-3] LockRunner - Thread 4 start!

2017-04-12 21:28:21,353 INFO  [Thread-3] LockRunner - Thread 4 end!

从测试结果我们可以看到,通过分布式锁的形式来控制并发时,多个同步操作只会有一个操作能够被执行,其他操作只有在等锁释放之后才有机会去执行,所以通过这样的分布式锁,我们可以控制共享资源同时只能被一个操作进行执行,以保障数据处理时的分布式并发问题。

优化建议

本文我们实现了基于Consul的简单分布式锁,但是在实际运行时,可能会因为各种各样的意外情况导致unlock操作没有得到正确地执行,从而使得分布式锁无法释放。所以为了更完善的使用分布式锁,我们还必须实现对锁的超时清理等控制,保证即使出现了未正常解锁的情况下也能自动修复,以提升系统的健壮性。那么如何实现呢?请持续关注我的后续分解!

参考文档

Key/Value的API:https://www.consul.io/api/kv.html

二、基于consul分布式信号量实现

在上面《基于Consul的分布式锁实现》中我们介绍如何基于Consul的KV存储来实现分布式互斥锁。本文将继续讨论基于Consul的分布式锁实现。信号量是我们在实现并发控制时会经常使用的手段,主要用来限制同时并发线程或进程的数量,比如:Zuul默认情况下就使用信号量来限制每个路由的并发数,以实现不同路由间的资源隔离。

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端,确认这些信号量VI引用的是初始创建的信号量。如在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

实现思路

- 信号量存储:semaphore/key

- acquired操作:

- 创建session

- 锁定key竞争者:semaphore/key/session

- 查询信号量:semaphore/key/.lock,可以获得如下内容(如果是第一次创建信号量,将获取不到,这个时候就直接创建)

- 如果持有者已达上限,返回false,如果阻塞模式,就继续尝试acquired操作

- 如果持有者未达上限,更新semaphore/key/.lock的内容,将当前线程的sessionId加入到holders中。注意:更新的时候需要设置cas,它的值是“查询信号量”步骤获得的“ModifyIndex”值,该值用于保证更新操作的基础没有被其他竞争者更新。如果更新成功,就开始执行具体逻辑。如果没有更新成功,说明有其他竞争者抢占了资源,返回false,阻塞模式下继续尝试acquired操作

- release操作:

- 从semaphore/key/.lock的holders中移除当前sessionId

- 删除semaphore/key/session

- 删除当前的session

流程图

代码实现

public class Semaphore {

private Logger logger = Logger.getLogger(getClass());

private static final String prefix = "semaphore/";  // 信号量参数前缀

private ConsulClient consulClient;

private int limit;

private String keyPath;

private String sessionId = null;

private boolean acquired = false;

/**

*

* @param consulClient consul客户端实例

* @param limit 信号量上限值

* @param keyPath 信号量在consul中存储的参数路径

*/

public Semaphore(ConsulClient consulClient, int limit, String keyPath) {

this.consulClient = consulClient;

this.limit = limit;

this.keyPath = prefix + keyPath;

}

/**

* acquired信号量

*

* @param block 是否阻塞。如果为true,那么一直尝试,直到获取到该资源为止。

* @return

* @throws IOException

*/

public Boolean acquired(boolean block) throws IOException {

if(acquired) {

logger.error(sessionId + " - Already acquired");

throw new RuntimeException(sessionId + " - Already acquired");

}

// create session

clearSession();

this.sessionId = createSessionId("semaphore");

logger.debug("Create session : " + sessionId);

// add contender entry

String contenderKey = keyPath + "/" + sessionId;

logger.debug("contenderKey : " + contenderKey);

PutParams putParams = new PutParams();

putParams.setAcquireSession(sessionId);

Boolean b = consulClient.setKVValue(contenderKey, "", putParams).getValue();

if(!b) {

logger.error("Failed to add contender entry : " + contenderKey + ", " + sessionId);

throw new RuntimeException("Failed to add contender entry : " + contenderKey + ", " + sessionId);

}

while(true) {

// try to take the semaphore

String lockKey = keyPath + "/.lock";

String lockKeyValue;

GetValue lockKeyContent = consulClient.getKVValue(lockKey).getValue();

if (lockKeyContent != null) {

// lock值转换

lockKeyValue = lockKeyContent.getValue();

BASE64Decoder decoder = new BASE64Decoder();

byte[] v = decoder.decodeBuffer(lockKeyValue);

String lockKeyValueDecode = new String(v);

logger.debug("lockKey=" + lockKey + ", lockKeyValueDecode=" + lockKeyValueDecode);

Gson gson = new Gson();

ContenderValue contenderValue = gson.fromJson(lockKeyValueDecode, ContenderValue.class);

// 当前信号量已满

if(contenderValue.getLimit() == contenderValue.getHolders().size()) {

logger.debug("Semaphore limited " + contenderValue.getLimit() + ", waiting...");

if(block) {

// 如果是阻塞模式,再尝试

try {

Thread.sleep(100L);

} catch (InterruptedException e) {

}

continue;

}

// 非阻塞模式,直接返回没有获取到信号量

return false;

}

// 信号量增加

contenderValue.getHolders().add(sessionId);

putParams = new PutParams();

putParams.setCas(lockKeyContent.getModifyIndex());

boolean c = consulClient.setKVValue(lockKey, contenderValue.toString(), putParams).getValue();

if(c) {

acquired = true;

return true;

}

else

continue;

} else {

// 当前信号量还没有,所以创建一个,并马上抢占一个资源

ContenderValue contenderValue = new ContenderValue();

contenderValue.setLimit(limit);

contenderValue.getHolders().add(sessionId);

putParams = new PutParams();

putParams.setCas(0L);

boolean c = consulClient.setKVValue(lockKey, contenderValue.toString(), putParams).getValue();

if (c) {

acquired = true;

return true;

}

continue;

}

}

}

/**

* 创建sessionId

* @param sessionName

* @return

*/

public String createSessionId(String sessionName) {

NewSession newSession = new NewSession();

newSession.setName(sessionName);

return consulClient.sessionCreate(newSession, null).getValue();

}

/**

* 释放session、并从lock中移除当前的sessionId

* @throws IOException

*/

public void release() throws IOException {

if(this.acquired) {

// remove session from lock

while(true) {

String contenderKey = keyPath + "/" + sessionId;

String lockKey = keyPath + "/.lock";

String lockKeyValue;

GetValue lockKeyContent = consulClient.getKVValue(lockKey).getValue();

if (lockKeyContent != null) {

// lock值转换

lockKeyValue = lockKeyContent.getValue();

BASE64Decoder decoder = new BASE64Decoder();

byte[] v = decoder.decodeBuffer(lockKeyValue);

String lockKeyValueDecode = new String(v);

Gson gson = new Gson();

ContenderValue contenderValue = gson.fromJson(lockKeyValueDecode, ContenderValue.class);

contenderValue.getHolders().remove(sessionId);

PutParams putParams = new PutParams();

putParams.setCas(lockKeyContent.getModifyIndex());

consulClient.deleteKVValue(contenderKey);

boolean c = consulClient.setKVValue(lockKey, contenderValue.toString(), putParams).getValue();

if(c) {

break;

}

}

}

// remove session key

}

this.acquired = false;

clearSession();

}

public void clearSession() {

if(sessionId != null) {

consulClient.sessionDestroy(sessionId, null);

sessionId = null;

}

}

class ContenderValue implements Serializable {

private Integer limit;

private List<String> holders = new ArrayList<>();

public Integer getLimit() {

return limit;

}

public void setLimit(Integer limit) {

this.limit = limit;

}

public List<String> getHolders() {

return holders;

}

public void setHolders(List<String> holders) {

this.holders = holders;

}

@Override

public String toString() {

return new Gson().toJson(this);

}

}

}

单元测试

下面单元测试的逻辑:通过线程的方式来模拟不同的分布式服务来获取信号量执行业务逻辑。由于信号量与简单的分布式互斥锁有所不同,它不是只限定一个线程可以操作,而是可以控制多个线程的并发,所以通过下面的单元测试,我们设置信号量为3,然后同时启动15个线程来竞争的情况,来观察分布式信号量实现的结果如何。

public class TestLock {

private Logger logger = Logger.getLogger(getClass());

@Test

public void testSemaphore() throws Exception {

new Thread(new SemaphoreRunner(1)).start();

new Thread(new SemaphoreRunner(2)).start();

new Thread(new SemaphoreRunner(3)).start();

new Thread(new SemaphoreRunner(4)).start();

new Thread(new SemaphoreRunner(5)).start();

new Thread(new SemaphoreRunner(6)).start();

new Thread(new SemaphoreRunner(7)).start();

new Thread(new SemaphoreRunner(8)).start();

new Thread(new SemaphoreRunner(9)).start();

new Thread(new SemaphoreRunner(10)).start();

Thread.sleep(1000000L);

}

}

public class SemaphoreRunner implements Runnable {

private Logger logger = Logger.getLogger(getClass());

private int flag;

public SemaphoreRunner(int flag) {

this.flag = flag;

}

@Override

public void run() {

Semaphore semaphore = new Semaphore(new ConsulClient(), 3, "mg-init");

try {

if (semaphore.acquired(true)) {

// 获取到信号量,执行业务逻辑

logger.info("Thread " + flag + " start!");

Thread.sleep(new Random().nextInt(10000));

logger.info("Thread " + flag + " end!");

}

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

// 信号量释放、Session锁释放、Session删除

semaphore.release();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

执行结果:

INFO  [Thread-6] SemaphoreRunner - Thread 7 start!

INFO  [Thread-2] SemaphoreRunner - Thread 3 start!

INFO  [Thread-7] SemaphoreRunner - Thread 8 start!

INFO  [Thread-2] SemaphoreRunner - Thread 3 end!

INFO  [Thread-5] SemaphoreRunner - Thread 6 start!

INFO  [Thread-6] SemaphoreRunner - Thread 7 end!

INFO  [Thread-9] SemaphoreRunner - Thread 10 start!

INFO  [Thread-5] SemaphoreRunner - Thread 6 end!

INFO  [Thread-1] SemaphoreRunner - Thread 2 start!

INFO  [Thread-7] SemaphoreRunner - Thread 8 end!

INFO  [Thread-10] SemaphoreRunner - Thread 11 start!

INFO  [Thread-10] SemaphoreRunner - Thread 11 end!

INFO  [Thread-12] SemaphoreRunner - Thread 13 start!

INFO  [Thread-1] SemaphoreRunner - Thread 2 end!

INFO  [Thread-3] SemaphoreRunner - Thread 4 start!

INFO  [Thread-9] SemaphoreRunner - Thread 10 end!

INFO  [Thread-0] SemaphoreRunner - Thread 1 start!

INFO  [Thread-3] SemaphoreRunner - Thread 4 end!

INFO  [Thread-14] SemaphoreRunner - Thread 15 start!

INFO  [Thread-12] SemaphoreRunner - Thread 13 end!

INFO  [Thread-0] SemaphoreRunner - Thread 1 end!

INFO  [Thread-13] SemaphoreRunner - Thread 14 start!

INFO  [Thread-11] SemaphoreRunner - Thread 12 start!

INFO  [Thread-13] SemaphoreRunner - Thread 14 end!

INFO  [Thread-4] SemaphoreRunner - Thread 5 start!

INFO  [Thread-4] SemaphoreRunner - Thread 5 end!

INFO  [Thread-8] SemaphoreRunner - Thread 9 start!

INFO  [Thread-11] SemaphoreRunner - Thread 12 end!

INFO  [Thread-14] SemaphoreRunner - Thread 15 end!

INFO  [Thread-8] SemaphoreRunner - Thread 9 end!

从测试结果,我们可以发现当信号量持有者数量达到信号量上限3的时候,其他竞争者就开始进行等待了,只有当某个持有者释放信号量之后,才会有新的线程变成持有者,从而开始执行自己的业务逻辑。所以,分布式信号量可以帮助我们有效的控制同时操作某个共享资源的并发数。

优化建议与参考文档

同前文一样,这里只是做了简单的实现。线上应用还必须加入TTL的session清理以及对.lock资源中的无效holder进行清理的机制。

参考文档:

https://www.consul.io/docs/guides/semaphore.html

转自:http://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247483857&idx=1&sn=495c0faad9bc237132aca49e722022ec&chksm=9bd0ac49aca7255fec67f9364fab63638b30e7a69fc0771f5977a6cc9a38856879b64832bc67&scene=21#wechat_redirect

 

服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁的更多相关文章

  1. python与consul 实现gRPC服务注册-发现

    背景 通过对gRPC的介绍我们知道,当正常启动服务后,我们只需要知道ip,port就可以进行gRPC的连接.可以想到,这种方式并不适合用于线上环境,因为这样直连的话就失去了扩展性,当需要多机部署的时候 ...

  2. Consul 多数据中心下的服务注册发现与配置共享

    1. Consul简介   Consul是HashiCorp公司推出的开源软件,它提供了一套分布式高可用可横向扩展的解决方案,能为微服务提供服务治理.健康检查.配置共享等能力.   Eurake2.x ...

  3. 基于docker,consul,consul-template, registrator, nginx服务注册发现集群

      介绍 该工程主要实现服务的自动注册发现,从而达到提高运维效率,做到服务的自动发现和动态扩展. 服务注册发现 服务启动后自动被发现 动态变更负载均衡 自动伸缩 工具 1.Registrator 这是 ...

  4. 服务注册发现与注册中心对比-Eureka,Consul,Zookeeper,Nacos对比

    服务注册发现与注册中心对比-Eureka,Consul,Zookeeper,Nacos对比 注册中心简介 流程和原理 基础流程 核心功能 1.Eureka.Consul.Zookeeper三者异同点 ...

  5. 服务注册发现consul之二:在Spring Cloud中使用Consul实现服务的注册和发现

    首先安装consul环境,参照之前的文章:<服务注册发现consul之一:consul介绍及安装>中的第一节介绍. Spring Cloud使用Consul的服务与发现 1.导入依赖pri ...

  6. 服务注册发现、配置中心集一体的 Spring Cloud Consul

    前面讲了 Eureka 和 Spring Cloud Config,今天介绍一个全能选手 「Consul」.它是 HashiCorp 公司推出,用于提供服务发现和服务配置的工具.用 go 语言开发,具 ...

  7. spring cloud微服务快速教程之(七) Spring Cloud Alibaba--nacos(一)、服务注册发现

    0.前言 什么是Spring Cloud Alibaba? Spring Cloud Alibaba 是阿里开源的,致力于提供微服务开发的一站式解决方案.此项目包含开发分布式应用微服务的必需组件,方便 ...

  8. spring-cloud-consul 服务注册发现与配置

    下面是 Spring Cloud 支持的服务发现软件以及特性对比(Eureka 已停止更新,取而代之的是 Consul): Feature euerka Consul zookeeper etcd 服 ...

  9. CoSky 高性能 服务注册/发现 & 配置中心

    CoSky 基于 Redis 的服务治理平台(服务注册/发现 & 配置中心) Consul + Sky = CoSky CoSky 是一个轻量级.低成本的服务注册.服务发现. 配置服务 SDK ...

随机推荐

  1. 给Linux服务器设置共享文件目录

    通过samba软件 :yum install samba 修改配置文件:vi /etc/samba/smb.conf 添加 [WORKSPACE] comment = workspace       ...

  2. FZU软工第十一次作业-软件产品案例分析

    目录 前言: 第一部分.调研,评测: 1.1.初次感觉: 1.2.企业号bug: 1.3.你觉得为什么这个产品组的人没有发现这些bug: 1.4.假设你们团队需要开发这套系统,需要注意哪些方面: 2. ...

  3. 计算x

    如果x的x次幂结果为10(参见[图1.png]),你能计算出x的近似值吗? 显然,这个值是介于2和3之间的一个数字. 请把x的值计算到小数后6位(四舍五入),并填写这个小数值. 注意:只填写一个小数, ...

  4. python筛选特定文件的信息按照格式输出到txt

    最近搞数据库,为了把图片文件的信息导入数据库表中,我开始研究python列出图片文件,其中发现因为IE临时文件里有非常多的不需要的图片,就需要筛选掉一些文件. 最终用python输出了所有需要的图片文 ...

  5. mysql之主从配置实现

    我使用的是两台centos7虚拟机来做实验的,主服务器ip为192.168.2.128,从服务器ip为192.168.2.130 安装mysql就不用说了吧,不对,我们需要安装的是mariadb,命令 ...

  6. mysql再探

    select子句及其顺序 select from where group by having order by limit 创建表 create table student(id int not nu ...

  7. windows知识

    文章目录 系统 修改远程桌面的端口号 IE选项中reset web setting不可用(灰色)的解决办法 重装系统后,修改默认程序安装目录.我的文档.桌面 路径 Keep network addre ...

  8. Java数组逆序排列

    //逆序排列原理 /* A: 数组逆序原理* a: 题目分析* 通过观察发现,本题目要实现原数组元素倒序存放操作.即原数组存储元素为{12,69,852,25,89,588},逆序后为原数组存储元素变 ...

  9. 使用nexus 管理pip 私有包

    nexus 已经支持了对于python pip 包的管理(支持group,host,proxy) 这个是一个简单的使用docker 运行的demo,同时集成了s3 存储,以及 一个为了测试简单的自定义 ...

  10. openresty 编译ngx_pagespeed 模块-docker 构建

    ngx_pagespeed 是一个很不错的web 优化模块,我们通过简单的配置就可以对于web页面的加载有很大的提升 ngx_pagespeed 依赖psol 模块 Dockerfile   # Do ...