현재 거의 많은 대규모 웹사이트와 애플리케이션이 분산 방식으로 배포되고 있습니다. 분산 시나리오에서 데이터 일관성 문제는 항상 중요한 주제였습니다. 분산 CAP 이론은 "모든 분산 시스템은 일관성(Consistency), 가용성(Availability) 및 파티션 허용 오차(Partition Tolerance)를 동시에 충족할 수 없습니다. 동시에 두 가지만 충족할 수 있습니다."라고 말합니다. 디자인 초기에 이 세 가지 중 하나를 선택하는 것이 필요합니다. 인터넷 분야의 대부분의 시나리오에서는 높은 시스템 가용성을 위해 강력한 일관성을 희생해야 합니다. 시스템은 최종 시간이 사용자가 허용할 수 있는 범위 내에 있는 한 "최종 일관성"만 보장하면 되는 경우가 많습니다.
많은 시나리오에서 데이터의 궁극적인 일관성을 보장하려면 분산 트랜잭션, 분산 잠금 등과 같이 이를 지원하는 많은 기술 솔루션이 필요합니다. 때로는 동일한 스레드에서만 동시에 메서드를 실행할 수 있도록 해야 하는 경우도 있습니다. 독립형 환경에서 Java는 실제로 동시 처리와 관련된 많은 API를 제공하지만 이러한 API는 분산 시나리오에서는 쓸모가 없습니다. 즉, 단순 Java API는 분산 잠금 기능을 제공할 수 없습니다. 따라서 현재 분산 잠금 구현을 위한 많은 솔루션이 있습니다.
분산 잠금 구현을 위해 현재 일반적으로 사용되는 솔루션은 다음과 같습니다.
데이터베이스 기반 분산 잠금 구현 캐시 기반(redis, memcached, tair) 기반 분산 잠금 구현 on Zookeeper 분산 잠금 구현
이러한 구현 솔루션을 분석하기 전에 먼저 필요한 분산 잠금이 어떤 모습이어야 하는지 생각해 보겠습니다. (여기서는 메서드 잠금이 예로 사용되었으며 리소스 잠금도 동일합니다.)
은 분산 배포 애플리케이션 클러스터에서 동일한 메서드가 한 시스템에서만 사용될 수 있도록 보장할 수 있습니다. 동시에 하나의 스레드가 실행됩니다.
이 잠금이 재진입 잠금인 경우(교착 상태를 피하기 위해)
이 잠금이 차단 잠금인 경우 가장 좋습니다( 비즈니스 요구에 따라 원함)
고가용성 잠금 획득 및 잠금 해제 기능 보유
잠금 획득 및 잠금 해제 성능 더 좋음
데이터베이스 기반 분산 잠금 구현
데이터베이스 테이블 기반
분산 잠금을 구현하는 가장 간단한 방법이 달성될 수 있습니다. 잠금 테이블을 직접 생성한 후 테이블의 데이터를 조작합니다.
메서드나 리소스를 잠그고 싶을 때는 테이블에 레코드를 추가하고, 잠금을 해제하고 싶을 때는 해당 레코드를 삭제합니다.
다음과 같이 데이터베이스 테이블을 생성합니다.
CREATE TABLE `methodLock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', `desc` varchar(1024) NOT NULL DEFAULT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
메서드를 잠그려면 다음 SQL을 실행합니다.
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
우리는 method_name에 관심이 있으므로 고유성 제약 조건이 동시에 데이터베이스에 제출되면 데이터베이스는 하나의 작업만 성공할 수 있도록 보장합니다. 그러면 성공적인 작업을 수행한 스레드가 메서드 잠금을 획득했다고 생각할 수 있습니다. 메소드 본문 내용을 실행합니다.
메서드 실행 후 잠금을 해제하려면 다음 Sql을 실행해야 합니다.
delete from methodLock where method_name ='method_name'
위의 간단한 구현에는 다음과 같은 문제가 있습니다.
1. 이 잠금은 데이터베이스의 가용성에 따라 크게 달라집니다. 데이터베이스는 단일 지점이므로 데이터베이스가 중단되면 비즈니스 시스템을 사용할 수 없게 됩니다.
2. 이 잠금에는 만료 시간이 없습니다. 잠금 해제 작업이 실패하면 잠금 기록이 데이터베이스에 유지되고 다른 스레드는 더 이상 잠금을 얻을 수 없습니다.
3. 이 잠금은 비차단만 가능합니다. 데이터 삽입 작업이 실패하면 오류가 직접 보고되기 때문입니다. 잠금을 획득하지 못한 스레드는 대기열에 들어가지 않습니다. 잠금을 다시 획득하려면 잠금 획득 작업을 다시 트리거해야 합니다.
4. 이 잠금은 재진입이 불가능합니다. 잠금을 해제하기 전에는 동일한 스레드가 다시 잠금을 얻을 수 없습니다. 데이터 안의 데이터가 이미 존재하기 때문이다.
물론 위의 문제를 해결하는 다른 방법도 있을 수 있습니다.
데이터베이스가 단일 지점인가요? 두 개의 데이터베이스를 생성하고 양방향으로 데이터를 동기화합니다. 실패하면 신속하게 대기 데이터베이스로 전환하십시오.
유통기한은 없나요? 예약된 작업을 수행하고 특정 간격으로 데이터베이스의 시간 초과 데이터를 정리하세요.
논블로킹? 삽입이 성공할 때까지 while 루프를 만든 다음 성공을 반환합니다.
비재진입? 현재 잠금을 획득한 머신의 호스트 정보와 스레드 정보를 기록하는 필드를 데이터베이스 테이블에 추가한 후 다음에 잠금을 획득할 때 현재 머신의 호스트 정보와 스레드 정보를 먼저 쿼리합니다. 데이터베이스에서 직접 그 사람에게 잠금을 할당하면 됩니다.
데이터베이스 배타적 잠금 기반
데이터 테이블에 레코드를 추가하고 삭제하는 것 외에도 데이터에 내장된 잠금을 사용하여 분산 잠금을 구현할 수도 있습니다.
방금 생성한 데이터베이스 테이블을 계속 사용합니다. 분산 잠금은 데이터베이스에 대한 배타적 잠금을 통해 구현할 수 있습니다. MySql 기반 InnoDB 엔진은 다음과 같은 방법을 사용하여 잠금 작업을 구현할 수 있습니다.
public boolean lock(){ connection.setAutoCommit(false) while(true){ try{ result = select * from methodLock where method_name=xxx for update; if(result==null){ return true; } }catch(Exception e){ } sleep(1000); } return false; }
쿼리 문 다음에 업데이트를 위해 추가하면 데이터베이스는 쿼리 프로세스 중에 데이터베이스 테이블에 배타적 잠금을 추가합니다. 레코드에 배타적 잠금이 추가되면 다른 스레드는 더 이상 레코드에 배타적 잠금을 추가할 수 없습니다.
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){ connection.commit(); }
通过connection.commit()操作来释放锁。
这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。
阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。
但是还是无法直接解决数据库单点和可重入问题。
总结
总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优点
直接借助数据库,容易理解。
数据库实现分布式锁的缺点
会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。
操作数据库需要一定的开销,性能问题需要考虑。
基于缓存实现分布式锁
相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。
目前有很多成熟的缓存产品,包括Redis,memcached以及我们公司内部的Tair。
这里以Tair为例来分析下使用缓存实现分布式锁的方案。关于Redis和memcached在网络上有很多相关的文章,并且也有一些成熟的框架及算法可以直接使用。
基于Tair的实现分布式锁在内网中有很多相关文章,其中主要的实现方式是使用TairManager.put方法来实现。
public boolean trylock(String key) { ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0); if (ResultCode.SUCCESS.equals(code)) return true; else return false; } public boolean unlock(String key) { ldbTairManager.invalid(NAMESPACE, key); }
以上实现方式同样存在几个问题:
1、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在tair中,其他线程无法再获得到锁。
2、这把锁只能是非阻塞的,无论成功还是失败都直接返回。
3、这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的key在tair中已经存在。无法再执行put操作。
当然,同样有方式可以解决。
没有失效时间?tair的put方法支持传入失效时间,到达时间之后数据会自动删除。
非阻塞?while重复执行。
非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。
但是,失效时间我设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁同样存在
总结
可以使用缓存来代替数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。
使用缓存实现分布式锁的优点
性能好,实现起来较为方便。
使用缓存实现分布式锁的缺点
通过超时时间来控制锁的失效时间并不是十分的靠谱。
基于Zookeeper实现分布式锁
基于zookeeper临时有序节点可以实现的分布式锁。
大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
来看下Zookeeper能不能解决前面提到的问题。
锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
비차단 잠금? 클라이언트는 ZK에서 순차 노드를 생성하고 노드에 리스너를 바인딩할 수 있습니다. 노드가 변경되면 Zookeeper는 클라이언트에 알리고 클라이언트는 생성된 노드가 현재 노드인지 확인할 수 있습니다. 모든 노드 중 가장 작은 시퀀스 번호이면 잠금을 획득하고 비즈니스 로직을 실행할 수 있습니다.
재입장은 안되나요? Zookeeper를 사용하면 클라이언트가 노드를 생성할 때 다음 번에 잠금을 획득하려고 할 때 현재 클라이언트의 호스트 정보와 스레드 정보를 직접 기록하는 문제를 효과적으로 해결할 수 있습니다. 현재 가장 작은 노드입니다. 자신의 정보와 동일하면 바로 Lock을 획득하고, 다르면 임시 시퀀스 노드를 생성하여 대기열에 참여하게 됩니다.
한 가지 질문이 있으신가요? Zookeeper를 사용하면 단일 지점 문제를 효과적으로 해결할 수 있습니다. ZK는 클러스터에 있는 시스템의 절반 이상이 생존하는 한 외부 서비스를 제공할 수 있습니다.
재진입 잠금 서비스를 캡슐화하는 사육사 타사 라이브러리 큐레이터 클라이언트를 직접 사용할 수 있습니다.
public boolean tryLock(long timeout, TimeUnit 단위) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch(예외 e) {
e.printStackTrace();
}
return true;
}
public boolean Unlock() {
try {
} interProcessMutex.release();
} catch( Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), DelayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
Curator에서 제공하는 InterProcessMutex는 분산 잠금을 구현한 것입니다. 획득 방법은 사용자가 잠금을 획득하고 해제 방법은 잠금을 해제하는 데 사용됩니다.
ZK를 사용하여 구현된 분산 잠금은 이 글의 시작 부분에서 분산 잠금에 대한 우리의 모든 기대를 완벽하게 충족하는 것 같습니다. 그러나 실제로는 그렇지 않습니다. Zookeeper에서 구현하는 분산 잠금에는 실제로 캐시 서비스만큼 성능이 높지 않을 수 있다는 단점이 있습니다. 잠금 기능을 구현하려면 잠금을 생성하고 해제하는 과정에서 항상 임시 노드를 동적으로 생성하고 삭제해야 하기 때문입니다. ZK에서 노드 생성 및 삭제는 Leader 서버를 통해서만 수행할 수 있으며, 데이터는 모든 Follower 시스템에서 공유되지 않습니다.
요약
Zookeeper를 사용하여 분산 잠금을 구현하는 이점
단일 지점 문제, 비재진입 문제, 비차단 문제 및 잠금을 해제할 수 없는 문제를 효과적으로 해결합니다. 구현이 비교적 간단합니다.
Zookeeper를 사용하여 분산 잠금을 구현하는 경우의 단점
캐시를 사용하여 분산 잠금을 구현하는 것만큼 성능이 좋지 않습니다. ZK의 원리를 이해해야 합니다.
세 가지 솔루션 비교
이해의 용이성 관점에서(낮음부터 높음)
데이터베이스> 캐시> Zookeeper
구현부터 복잡성 관점(낮음에서 높음으로)
Zookeeper >= 캐시> 데이터베이스
성능 관점에서(높음에서 낮음으로)
캐시> Zookeeper >= 데이터베이스
신뢰성 관점에서(높음에서 낮음으로)
Zookeeper > Cache> 데이터베이스