現在、ほとんどの大規模な Web サイトとアプリケーションが分散方式で展開されています。分散シナリオにおけるデータの一貫性の問題は常に重要なテーマです。分散 CAP 理論は、「いかなる分散システムも、一貫性 (Consistency)、可用性 (Availability)、および分割許容度 (Partition torrent) を同時に満たすことはできません。したがって、多くのシステムは同時に 2 つを満たすことしかできません。」と教えてくれます。設計の初めにこれら 3 つのうちのいずれかを選択する必要があります。インターネット分野のほとんどのシナリオでは、システムの高い可用性と引き換えに強力な一貫性を犠牲にする必要がありますが、多くの場合、システムは、最終時間がユーザーの許容範囲内である限り、「最終的な一貫性」を確保するだけで済みます。
多くのシナリオでは、データの最終的な一貫性を確保するために、分散トランザクション、分散ロックなど、データをサポートするための多くの技術ソリューションが必要です。場合によっては、メソッドが同時に同じスレッドによってのみ実行できるようにする必要があります。スタンドアロン環境では、Java は実際には同時処理に関連する API を多数提供しますが、これらの API は分散シナリオでは役に立ちません。つまり、単純な Java API は分散ロック機能を提供できません。したがって、現在、分散ロックを実装するためのソリューションが多数あります。
分散ロックの実装には、現在次のソリューションが一般的に使用されています:
分散ロックはデータベースに基づいて実装されます。 分散ロックはキャッシュ (redis、memcached、tair) に基づいて実装されます。 分散ロックは 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には一意の制約があるため、複数のリクエストが同時にデータベースに送信された場合、そうであれば、データベースは 1 つの操作のみが成功することを保証し、操作が成功したスレッドがメソッドのロックを取得し、メソッド本体のコンテンツを実行できると考えることができます。
メソッドの実行後、ロックを解放したい場合は、次の SQL を実行する必要があります:
delete from methodLock where method_name ='method_name'
上記の単純な実装には次の問題があります:
1. このロックはデータベースの可用性に大きく依存します。データベースはシングルポイントであるため、データベースがハングアップすると、ビジネス システムは使用できなくなります。
2. このロックには有効期限がありません。ロック解除操作が失敗すると、ロック レコードはデータベースに残り、他のスレッドはロックを取得できなくなります。
3. データ挿入操作が失敗するとエラーが直接報告されるため、このロックは非ブロッキングのみにすることができます。ロックを取得していないスレッドはキューに入りません。ロックを再度取得したい場合は、ロック取得操作を再度トリガーする必要があります。
4. このロックは再入不可です。ロックを解放するまで、同じスレッドは再度ロックを取得できません。データ内のデータがすでに存在しているためです。
もちろん、上記の問題を解決する他の方法もあります。
データベースは単一ポイントですか? 2 つのデータベースを作成し、両方向でデータを同期します。障害が発生したら、すぐにスタンバイ データベースに切り替えます。
有効期限はありませんか?スケジュールされたタスクを実行し、一定の間隔でデータベース内のタイムアウト データをクリーンアップするだけです。
ノンブロッキング?挿入が成功するまで 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连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
ノンブロッキングロック?ブロック ロックは、Zookeeper を使用して実現できます。クライアントは、ZK で順次ノードを作成し、そのノードにリスナーをバインドできます。ノードが変更されると、Zookeeper がクライアントに通知し、クライアントは作成したノードが現在のものであるかどうかを確認できます。すべてのノードの中で最も小さいシーケンス番号である場合、ロックを取得しており、ビジネス ロジックを実行できます。
再入場は禁止ですか? Zookeeper を使用すると、クライアントがノードを作成するときに、次回ロックを取得するときに、現在のクライアントのホスト情報とスレッド情報がノードに直接書き込まれます。現時点で最小のノード内のデータを比較するだけです。自分の情報と同じ場合は直接ロックを取得し、異なる場合は一時シーケンスノードを作成してキューに参加します。
単一の質問ですか? Zookeeper を使用すると、単一点の問題を効果的に解決できます。ZK は、クラスター内のマシンの半分以上が存続している限り、外部サービスを提供できます。
再入可能なロック サービスをカプセル化する、Zookeeper のサードパーティ ライブラリである Curator クライアントを直接使用できます。
public boolean tryLock(long timeout, TimeUnit Unit) throws InterruptedException {
try {
} return interProcessMutex.acquire(timeout, Unit);
} catch (Exception e) {
e.printStackTrace();
}
return true ;
}
public boolean lock() {
try {
} interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} 最後に {
executorService. sched ule( new Cleaner(client, path),layTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
Curator が提供する InterProcessMutex は分散ロックの実装です。取得メソッドのユーザーはロックを取得し、解放メソッドはロックを解放するために使用されます。
ZK を使用して実装された分散ロックは、この記事の冒頭で述べた分散ロックに対するすべての期待を完全に満たしているようです。しかし、実際にはそうではありません。Zookeeper が実装する分散ロックには、パフォーマンスがキャッシュ サービスほど高くない可能性があるという欠点があります。ロックの作成と解放のプロセス中は常に、ロック機能を実装するために一時ノードを動的に作成および破棄する必要があるためです。 ZK でのノードの作成と削除はリーダー サーバーを通じてのみ実行でき、データはすべてのフォロワー マシンと共有されません。
概要
Zookeeper を使用して分散ロックを実装する利点
シングルポイント問題、非リエントラント問題、ノンブロッキング問題、およびロックを解放できない問題を効果的に解決します。実装は比較的簡単です。
Zookeeper を使用して分散ロックを実装するデメリット
パフォーマンスは、キャッシュを使用して分散ロックを実装するほど良くありません。 ZK の原則を理解する必要があります。
3 つのソリューションの比較
理解しやすさの観点から (低位から高位へ)
データベース > キャッシュ > Zookeeper
実装の複雑さの観点から (低位から高位へ)
Zookeeper >= キャッシュ > データベース
パフォーマンスの観点から (高から低の順)
キャッシュ > Zookeeper >= データベース
信頼性の観点から (高から低の順)
Zookeeper > キャッシュ > データベース