etcd 분산 잠금 : 최고의 달성 CP 분산 잠금

왜 CP 분산 잠금

분산 잠금 기능과 요구, 우리는 한 레디 스 잠금을 분산 : 간단한 버전 잠금 AOP와 레디 스에게의 실현을 분산 기반 전에 간단한을.

이미 장면 (주식 + 자동 갱신 + 재진입 분산 잠금)의 대부분을 만날 수있는 레디 스 분산 잠금의 현재 자체 개발, 생산 환경에 투입 될 수있다. 이 레디 스를 기반으로하기 때문에 그러나 독립형 환경을뿐만 아니라 높은 동시성 시나리오. 1, 레디 스 단일 클러스터로 변경 : 비즈니스 시나리오에 대한 접근의 확대와 함께, 레디 스 하나가 신뢰할 수없는되었다, 그 다음은 우리에게 오직 두 가지 선택을 제공합니다. 2 컨센서스 알고리즘에 기초하여 다른 실시 예를 사용한다.

당신은 선천성 결함을 가지고 시나리오 1, 레디 스 클러스터 마스터 노드가 다운 순간 간의 데이터의 일관성을 보장 할 수 없습니다, 마스터와 슬레이브 노드는 일치하지 않을 수 있습니다. 이 슬레이브 노드가 완료되지 아직 완전히 마스터 데이터 동기화를하기 전에, 슬레이브 노드 B에서 서비스가 성공적으로 같은이를 잠글 수있어, 마스터 노드와 마스터 노드에서 잠금을 얻기 위해 다운 될 서비스의 원인이됩니다.

합의 알고리즘을 기반으로 다른 구현에서, ZK 및 ectd은 좋은 선택이 될 것입니다. 그런 다음 밖으로 리드를 가져옵니다 ZK 이미 고려, 우리는이 떠오르는 스타를 ectd 선택했다.

분산 잠금 장면에서, 우리가 대신 가용성의 잠금, 잠금 일관성에 대한 자세한 우려하고 있기 때문에, AP 통신 CP 잠금 잠금보다 더 신뢰할 수있다.

디자인 아이디어

etcd 우리가 먼저 임대를 부여해야 다음 또한리스의 유효 시간을 설정, 임대의 개념을 도입했다. 우리는 효과적인 잠금으로 사용할 수있는 임대 시간의 유효 시간.

그런 다음 우리는 잠금 함수를 호출 etcd 지정된 임대에 지정된 lockName에 잠금 작업을 수행 할 수 있습니다. 다른 스레드가 잠금을 보유하지 않는 경우, 스레드가 직접 잠금을 보유 할 수 있습니다. 그렇지 않으면 기다릴 필요가있다. 여기에서 우리는 잠금 실패의 취득을 대기 경쟁 과정을 달성하기 위해 타임 아웃 시간 잠금 대기 시간을 설정할 수 있습니다. 물론, 네트워크 변동 및 기타 문제로 인해, 나는 최소한의 타임 아웃 시간은 500ms로 설정하는 것이 좋습니다 (또는 적절한 값을 생각 하는가).

그런 과정을 잠금 해제, 우리는 잠금 해제 작업 etcd을 포기하고 직접 운영 etcd 취소 사용. 왜 매개 변수를 우리가 결국 작업을 취소 수행하기 때문에, 잠금 단계 작업을, 우리는, 다중 필드를 유지하지 않으 lockKey 반환하고 두 번째의 잠금을 해제하기 위해 필요하기 때문에, 첫째, 잠금 해제 작업을 채택,하지만 작업은 임대 계약을 취소 할 수 없습니다 우리가 현재 잠금에 해당하는 임대를 설계하기 때문에 실패 이하의 모든 키는 상황이 존재하지 않는 잠금에서 추가 비즈니스 시나리오를 발표 할 예정이다.

또한, 갱신의 필요성을 결정하기 위해 정기적으로 스레드에 데몬 스레드를 엽니 다 임대를 부여 후, 만료되지 않습니다리스의 락의 취득을 대기, 그래서 우리는이 데몬 스레드에 대한 스레드를 설정해야합니다 동안 스레드를 보장하기 위해있다.

그리고 레디 스 분산 잠금 잠금 캐시의 유효 분산 유효 시간 레디 스 동일하지 않습니다, 그래서 당신은 잠금을 획득에 성공 후 갱신 데몬 스레드를 열 수 있으며, 유효 시간 etcd 분산 잠금 임대입니다 유효 시간, 임차가 만료 될 수 잠금을 획득하려고 대기하는 그래서 당신이 임대를 얻은 후 데몬 스레드를 열어야 할 필요가있다. 이것은 많은 복잡성을 추가합니다.

## 쓰기 모국어를 통해 이동 etcd 구체적인 실현은, 직접 응용 프로그램은 자바 프로그램에서 약간의 어려움이있을 것이다, 그래서 당신은 방법과 etcd 자바 프로그램의 코드를 사용할 수 있도록 우리는 클라이언트 etcd로 직접 사용 jetcd 서버 통신.

jetcd는 LeaseClient, 우리가 직접 임대를 부여 동작 기능을 완료하기 위해 보조금을 사용할 수 있습니다.

public LockLeaseData getLeaseData(String lockName, Long lockTime) {
    try {
        LockLeaseData lockLeaseData = new LockLeaseData();
        CompletableFuture<LeaseGrantResponse> leaseGrantResponseCompletableFuture = client.getLeaseClient().grant(lockTime);
        Long leaseId = leaseGrantResponseCompletableFuture.get(1, TimeUnit.SECONDS).getID();
        lockLeaseData.setLeaseId(leaseId);
        CpSurvivalClam cpSurvivalClam = new CpSurvivalClam(Thread.currentThread(), leaseId, lockName, lockTime, this);
        Thread survivalThread = threadFactoryManager.getThreadFactory().newThread(cpSurvivalClam);
        survivalThread.start();
        lockLeaseData.setCpSurvivalClam(cpSurvivalClam);
        lockLeaseData.setSurvivalThread(survivalThread);
        return lockLeaseData;
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        return null;
    }
}
复制代码

또한 우리가 임대를 얻을 후, 정기적으로 갱신에 CpSurvivalClam 보호자 스레드를 열어, 위에서 언급 한. 유일한 차이점은 keepAliveOnce에 etcd 변경 expandLockTime 작업 중 하나가 될 때 레디 스에서 CpSurvivalClam 구현하고 광범위하게 일치의 실현은 잠금 장치를 배포했습니다. expandLockTime 방법은 구체적으로 다음과 같이

/**
 * 重置锁的有效时间
 *
 * @param leaseId 锁的租约id
 * @return 是否成功重置
 */
public Boolean expandLockTime(Long leaseId) {
    try {
        CompletableFuture<LeaseKeepAliveResponse> leaseKeepAliveResponseCompletableFuture = client.getLeaseClient().keepAliveOnce(leaseId);
        leaseKeepAliveResponseCompletableFuture.get();
        return Boolean.TRUE;
    } catch (InterruptedException | ExecutionException e) {
        return Boolean.FALSE;
    }
}
复制代码

그런 다음 jetcd는 LockClient, 우리는 직접 잠금 기능을 사용할 수 있습니다 제공하며, leaseId lockName 우리가 lockKey에서 임대를 얻을 것이다, 통과시켰다. 또한, 잠금의 성공을 보장하기 위해, 임대가 만료되지 않았습니다. 우리는 판단하는 단계의 TimeToLive 작업을 추가할지 여부를 잠금 성공을 획득 한 후 아직 살아 임대. TTL이 0보다 큰 경우에는 잠금 오류가 있다고 판단된다.

/**
 * 在指定的租约上加锁,如果租约过期,则算加锁失败。
 *
 * @param leaseId  锁的租约Id
 * @param lockName 锁的名称
 * @param waitTime 加锁过程中的的等待时间,单位ms
 * @return 是否加锁成功
 */
public Boolean tryLock(Long leaseId, String lockName, Long waitTime) {
    try {
        CompletableFuture<LockResponse> lockResponseCompletableFuture = client.getLockClient().lock(ByteSequence.from(lockName, Charset.defaultCharset()), leaseId);
        long timeout = Math.max(500, waitTime);
        lockResponseCompletableFuture.get(timeout, TimeUnit.MILLISECONDS).getKey();
        CompletableFuture<LeaseTimeToLiveResponse> leaseTimeToLiveResponseCompletableFuture = client.getLeaseClient().timeToLive(leaseId, LeaseOption.DEFAULT);
        long ttl = leaseTimeToLiveResponseCompletableFuture.get(1, TimeUnit.SECONDS).getTTl();
        if (ttl > 0) {
            return Boolean.TRUE;
        } else {
            return Boolean.FALSE;
        }
    } catch (TimeoutException | InterruptedException | ExecutionException e) {
        return Boolean.FALSE;
    }
}
复制代码

잠금 해제 과정, 우리는 LeaseClient에서 작동 취소 직접 사용하는리스 임대를 취소하면서 잠금을 해제 할 수 있습니다.

/**
 * 取消租约,并释放锁
 *
 * @param leaseId 租约id
 * @return 是否成功释放
 */
public Boolean unLock(Long leaseId) {
    try {
        CompletableFuture<LeaseRevokeResponse> revokeResponseCompletableFuture = client.getLeaseClient().revoke(leaseId);
        revokeResponseCompletableFuture.get(1, TimeUnit.SECONDS);
        return Boolean.TRUE;
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        return Boolean.FALSE;
    }
}

复制代码

이어서 통합 CpLock 객체는 사용자가 잠금 해제 절차를 잊어 피하기 위해 유일한 방법을 실행 잠금 외부 노출을 해제하는 과정을 캡슐화한다.

public class CpLock {

    private String lockName;

    private LockEtcdClient lockEtcdClient;

    /**
     * 分布式锁的锁持有数
     */
    private volatile int state;

    private volatile transient Thread lockOwnerThread;

    /**
     * 当前线程拥有的lease对象
     */
    private FastThreadLocal<LockLeaseData> lockLeaseDataFastThreadLocal = new FastThreadLocal<>();
    /**
     * 锁自动释放时间,单位s,默认为30
     */
    private static Long LOCK_TIME = 30L;

    /**
     * 获取锁失败单次等待时间,单位ms,默认为300
     */
    private static Integer SLEEP_TIME_ONCE = 300;

    CpLock(String lockName, LockEtcdClient lockEtcdClient) {
        this.lockName = lockName;
        this.lockEtcdClient = lockEtcdClient;
    }

    private LockLeaseData getLockLeaseData(String lockName, long lockTime) {
        if (lockLeaseDataFastThreadLocal.get() != null) {
            return lockLeaseDataFastThreadLocal.get();
        } else {
            LockLeaseData lockLeaseData = lockEtcdClient.getLeaseData(lockName, lockTime);
            lockLeaseDataFastThreadLocal.set(lockLeaseData);
            return lockLeaseData;
        }
    }

    final Boolean tryLock(long waitTime) {
        final long startTime = System.currentTimeMillis();
        final long endTime = startTime + waitTime * 1000;
        final long lockTime = LOCK_TIME;
        final Thread current = Thread.currentThread();
        try {
            do {
                int c = this.getState();
                if (c == 0) {
                    LockLeaseData lockLeaseData = this.getLockLeaseData(lockName, lockTime);
                    if (Objects.isNull(lockLeaseData)) {
                        return Boolean.FALSE;
                    }
                    Long leaseId = lockLeaseData.getLeaseId();
                    if (lockEtcdClient.tryLock(leaseId, lockName, endTime - System.currentTimeMillis())) {
                        log.info("线程获取重入锁成功,cp锁的名称为{}", lockName);
                        this.setLockOwnerThread(current);
                        this.setState(c + 1);
                        return Boolean.TRUE;
                    }
                } else if (lockOwnerThread == Thread.currentThread()) {
                    if (c + 1 <= 0) {
                        throw new Error("Maximum lock count exceeded");
                    }
                    this.setState(c + 1);
                    log.info("线程重入锁成功,cp锁的名称为{},当前LockCount为{}", lockName, state);
                    return Boolean.TRUE;
                }
                int sleepTime = SLEEP_TIME_ONCE;
                if (waitTime > 0) {
                    log.info("线程暂时无法获得cp锁,当前已等待{}ms,本次将再等待{}ms,cp锁的名称为{}", System.currentTimeMillis() - startTime, sleepTime, lockName);
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {
                        log.info("线程等待过程中被中断,cp锁的名称为{}", lockName, e);
                    }
                }
            } while (System.currentTimeMillis() <= endTime);
            if (waitTime == 0) {
                log.info("线程获得cp锁失败,将放弃获取,cp锁的名称为{}", lockName);
            } else {
                log.info("线程获得cp锁失败,之前共等待{}ms,将放弃等待获取,cp锁的名称为{}", System.currentTimeMillis() - startTime, lockName);
            }
            this.stopKeepAlive();
            return Boolean.FALSE;
        } catch (Exception e) {
            log.error("execute error", e);
            this.stopKeepAlive();
            return Boolean.FALSE;
        }
    }

    /**
     * 停止续约,并将租约对象从线程中移除
     */
    private void stopKeepAlive() {
        LockLeaseData lockLeaseData = lockLeaseDataFastThreadLocal.get();
        if (Objects.nonNull(lockLeaseData)) {
            lockLeaseData.getCpSurvivalClam().stop();
            lockLeaseData.setCpSurvivalClam(null);
            lockLeaseData.getSurvivalThread().interrupt();
            lockLeaseData.setSurvivalThread(null);
        }
        lockLeaseDataFastThreadLocal.remove();
    }

    final void unLock() {
        if (lockOwnerThread == Thread.currentThread()) {
            int c = this.getState() - 1;
            if (c == 0) {
                this.setLockOwnerThread(null);
                this.setState(c);
                LockLeaseData lockLeaseData = lockLeaseDataFastThreadLocal.get();
                this.stopKeepAlive();
                //unLock操作必须在最后执行,避免其他线程获取到锁时的state等数据不正确
                lockEtcdClient.unLock(lockLeaseData.getLeaseId());
                log.info("重入锁LockCount-1,线程已成功释放锁,cp锁的名称为{}", lockName);
            } else {
                this.setState(c);
                log.info("重入锁LockCount-1,cp锁的名称为{},剩余LockCount为{}", lockName, c);
            }
        }
    }

    public <T> T execute(Supplier<T> supplier, int waitTime) {
        Boolean holdLock = Boolean.FALSE;
        Preconditions.checkArgument(waitTime >= 0, "waitTime必须为自然数");
        try {
            if (holdLock = this.tryLock(waitTime)) {
                return supplier.get();
            }
            return null;
        } catch (Exception e) {
            log.error("cpLock execute error", e);
            return null;
        } finally {
            if (holdLock) {
                this.unLock();
            }
        }
    }

    public <T> T execute(Supplier<T> supplier) {
        return this.execute(supplier, 0);
    }
}

复制代码

CpLock 및 이전 레디 스는 ApLock 잠금이 광범위하게 일치 달성 분산. 주요 차이점은 다음과 같습니다

우리가 데몬 스레드에서 부여하는 운용리스에서 열려, 그래서 경쟁 잠금 장치에 이상이 해제에 실패하고 이러한 시나리오를 잠글 수 있기 때문에 한, 우리는 데몬 스레드의 갱신을 중지해야합니다. 또한, 때문에 재진입 장면, 우리는 단지 국가의 경우 임대 잠금을 생성 경쟁 가고 싶어 제로이다. 그래서 다양한 상황을 판단하지 말고, 우리는 현재 스레드리스의 객체를 저장할 FastThreadLocal lockLeaseDataFastThreadLocal을 도입했습니다.

etcd 시나리오에서, 우리는 비의 상태에서의 대기 상태 논리를 etcd 0에 의해 이루어집니다 기다리는 동안 잠금을 취득을 대기하고있는 장면에서 잠금을 분산 2, 레디 스는 폴링에 의해 절전 모드로 수행됩니다 0 장면, 여전히 폴링 수면의 방법으로 달성하기 위해 기다리고. 이 제로로부터 비 - 제로 상태로 전환되므로 우리있는 waittime 값 endTime- 사용자 인 경우가 있기 때문에 -이 아니라 원래 수신있는 waittime 이상에 System.currentTimeMillis (). 이것은 우리의 기대에 대기 시간을 더 가까이 할 수 있습니다.

업데이트에 대한 설명

이 업데이트, 우리는 CP 분산 잠금 기반 etcd을 구현뿐만 아니라, 숨겨진 문제 레디 스 분산 잠금을 해결합니다.

잠금 해제 후 setState를 작동하기 전에, 동시성 시나리오 있도록 문제가 발생할 수 있습니다. 그리고, 스레드 B 스레드가 경쟁에 잠금을 획득하고, 각 로컬 및 상태 변수 C는 즉시 잠금을 해제하여 잠금을 획득 스레드 후 0이고, 다음을 수행 언록 상태 또는 1이고, B 실 성공적인 잠금의 상태는 다음 스레드가 실행 setState를 0으로한다 stete 여전히 1 C + 1로 리셋된다. 스레드 B가 록을 해제하는 경우, 이때, 작업이 stete-1은 -1 수행된다. 멀티 스레드 시나리오에서, 잠금 제어에 의한 분산 잠금, 우리는 작업이이 문제를 해결하기 위해 모든 지정 후 이동 잠금을 해제 할 필요가있는 동안이 문제는, 상태 값과 상태 값이 작업이 비동기 수정 획득에 주로 기인한다.

다음 단계

분산 CP 버전은 현재 구현을 잠 그려면,이 장면의 대부분을 만날 수있는 잠금 장치 (주식 + 자동으로 재진입 분산 잠금 + 비 재생), 그것은 생산 환경에 투입되었을 수 있습니다 배포되었습니다. 후속 계획, AP와 CP 잠금을 일부 사용 시나리오를 최적화 각 업데이트를 잠급니다. 공정 잠금 장치의 문제를 해결하고 루프 액세스 문제는 잠을 기다릴 필요가 잠금을 시도합니다.

공산당 분산 잠금 필요가 현지 잘못 생각하는 경우 현재는 소규모 시험을 수행 한 사용 시나리오, 많이 생각하지만, 또한 당신이 용서 희망합니다.

추천 도서

1, 레디 스 잠금을 분산 : 간단한 버전을 기반으로 분산 잠금 AOP와 레디 스 실현의
2, 레디 스 잠금 (B) 배포 : 여러 스레드가 잠겨 얻을 후 리드를 피하기 위해 갱신 잠금에 대한 지원, 잠금 시간 종료를
3, 레디 스 잠금을 분산 (c)는 : 때 잠금 재귀 교착 상태를 피하기 위해, 오목 로크를 지원

음, 우리는 다음 작별 인사 메시지를 논의하기 위해 환영합니다. 또한 엄지 손가락을 환영 -

추천

출처juejin.im/post/5d69dd446fb9a06aed713877