古き良き皆さん、これはJava ResearchInstituteです。
今日は4つの議論する
分散ロックの実装を:1.分散ロックを実装2. MySQLのを通じて配布ロックを実装する
のRedis経由する分散ロックを実装3.
分散ロックを実装4.飼育係を通じて
etcdて
1.分散ロックとは何ですか?
共有リソースに一度に1つのスレッドのみがアクセスできるようにするにはどうすればよいですか?
これは非常に単純であると思われるかもしれません。jvmでは、同期またはReentrantLockを使用して非常に簡単に実現できます。
確かに、単一のJVMでは実際には問題はありません。
ただし、通常、システムはクラスターにデプロイされます。現時点では、クラスター内の各ノードはjvm環境であるため、同期またはReentrantLockを使用して共有リソースアクセスの問題を解決することはできません。
現時点では分散ロックが必要です。分散ロックは、分散環境の共有リソースへの順次アクセスの問題を解決します。同時に、クラスター内のすべてのノードの1つのスレッドのみが共有リソースにアクセスできます。
2.分散ロックの機能
分散ロックユーザーは別のマシンに配置されています。ロックが正常に取得されると、共有リソースを操作できます
。分散ロックを取得できるのは、すべてのマシンの1人のユーザーだけです。
ロックには再入可能機能があります。つまり、1人のユーザーが分散ロックを複数回取得する
プロセスでは、タイムアウト機能を指定できます。指定された時間内にロックを取得しようとします。タイムアウト期間の後、ロックが取得されていない場合、取得は
デッドロックの防止に失敗します。例:マシンが取得します。ロックがロックされた後、ロックが解除される前にマシンAがハングアップし、ロックが解除されないため、ロックはマシンAによって占有されています。この場合、分散ロックは自動的に解決される必要があります。解決策:ロックが保持されている場合保留タイムアウト期間を追加できます。この時間の後、ロックは自動的に解放されます。この時点で、他のマシンがロックを取得する機会があります。
分散ロックの4つの実装を見てみましょう。
3.方法1:データベース方式
3.1原則
ロック取得プロセス
クラスター環境にn個のシステムがあり、各システムにjvmがあり、各jvmに分散ロックを取得するためのm個のスレッドがある場合、同時に分散を取得するためのn * m個のスレッドが存在する可能性があります現時点では、分散ロックのプレッシャーは比較的大きく、各jvmの複数のスレッドが同時にロックを取得することは無意味です。各jvmにローカルロックを追加して、分散ロックを取得できます。 JVMローカルロックを取得する前に、ローカルロックが正常に取得された後、分散ロックの取得を試みることができます。現時点では、nシステムの最大nスレッドが分散ロックの取得を試みます。ロックを取得する手順は、主に2つの手順です。
1、先尝试获取jvm本地锁
2、jvm本地锁获取成功之后尝试获取分布式锁
残業時間
ロックを取得するときは、ロックを取得するための最大待機時間を渡すことができます。指定された時間内に複数回ロックを取得してみてください。取得に失敗した後、しばらくスリープしてから、時間がなくなるまで取得を試みます。
ロックの有効期間
ロックを取得するときに有効期間を指定する必要があります。有効期間とは、ユーザーがロックを取得してからロックを使用する期間のことですが、なぜ有効期間が必要なのですか。
有効期限がない場合、ユーザーが正常に取得すると、システムが突然シャットダウンし、ロックを解除できなくなり、他のスレッドはロックを取得できなくなります。
したがって、有効期間が必要です。有効期間が過ぎると、ロックは無効になり、他のスレッドがロックの取得を試みることができます。
閉じ込める
ロック更新とは何ですか?
例:ユーザーがロックを取得した場合、指定された有効期間は5分ですが、5分後、ユーザーは作業を終了せず、しばらく使用を続けたい場合、寿命延長機能を使用してロックの有効期間を遅らせることができます。
サブスレッドを開始して、ライフの更新操作を自動的に完了することができます。たとえば、元の有効期間は5分、4分間使用した場合、更新期間は2分、有効期間は7分です。これは比較的簡単で、好きなようにプレイできます。
3.2、sqlを準備する
create table t_lock(
lock_key varchar(32) PRIMARY KEY NOT NULL COMMENT '锁唯一标志',
request_id varchar(64) NOT NULL DEFAULT '' COMMENT '用来标识请求对象的',
lock_count INT NOT NULL DEFAULT 0 COMMENT '当前上锁次数',
timeout BIGINT NOT NULL DEFAULT 0 COMMENT '锁超时时间',
version INT NOT NULL DEFAULT 0 COMMENT '版本号,每次更新+1'
)COMMENT '锁信息表';
注:テーブルにはバージョン番号フィールドがあります。バージョン番号は主に、同時条件下で更新されたデータの正確性を確保するために、楽観的なロック方法でデータを更新するために使用されます。
3.3、ツールコードをロックする
コードは比較的シンプルで、主にロックを取得するためのロック方法とロックを解除するためのロック解除方法を見ていきますが、コメントはより詳細で、読んで理解することができます。
コードの重要なポイントは、バージョン番号を比較し、casメソッドを使用してデータを更新し、同時条件下で更新されたデータの正確性を確認することです。
このコードは、ロックを取得および解放する操作を実装します。ライフを更新する操作は実装されていません。実装を試みることができます。
package lock;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class DbLockUtil {
//将requestid保存在该变量中
static ThreadLocal<String> requestIdTL = new ThreadLocal<>();
//jvm锁:当多个线程并发获取分布式锁时,需要先获取jvm锁,jvm锁获取成功,则尝试获取分布式锁
static Map<String, ReentrantLock> jvmLockMap = new ConcurrentHashMap<>();
/**
* 获取当前线程requestid
*
* @return
*/
public static String getRequestId() {
String requestId = requestIdTL.get();
if (requestId == null || "".equals(requestId)) {
requestId = UUID.randomUUID().toString();
requestIdTL.set(requestId);
}
log.info("requestId:{}", requestId);
return requestId;
}
/**
* 获取锁
*
* @param lockKey 锁key
* @param lockTimeOut(毫秒) 持有锁的有效时间,防止死锁
* @param getTimeOut(毫秒) 获取锁的超时时间,这个时间内获取不到将重试
* @return
*/
public static boolean lock(String lockKey, long lockTimeOut, int getTimeOut) throws Exception {
log.info("start");
boolean lockResult = false;
/**
* 单个jvm中可能有多个线程并发获取一个锁
* 此时我们只允许一个线程去获取分布式锁
* 所以如果同一个jvm中有多个线程尝试获取分布式锁,需要先获取jvm中的锁
*/
ReentrantLock jvmLock = new ReentrantLock();
ReentrantLock oldJvmLock = jvmLockMap.putIfAbsent(lockKey, jvmLock);
oldJvmLock = oldJvmLock != null ? oldJvmLock : jvmLock;
boolean jvmLockSuccess = oldJvmLock.tryLock(getTimeOut, TimeUnit.MILLISECONDS);
//jvm锁获取失败,则直接失败
if (!jvmLockSuccess) {
return lockResult;
} else {
//jvm锁获取成功,则继续尝试获取分布式锁
try {
String request_id = getRequestId();
long startTime = System.currentTimeMillis();
//循环尝试获取锁
while (true) {
//通过lockKey获取db中的记录
LockModel lockModel = DbLockUtil.get(lockKey);
if (Objects.isNull(lockModel)) {
//记录不存在,则先插入一条
DbLockUtil.insert(LockModel.builder().lock_key(lockKey).request_id("").lock_count(0).timeout(0L).version(0).build());
} else {
//获取请求id,稍后请求id会放入ThreadLocal中
String requestId = lockModel.getRequest_id();
//如果requestId为空字符,表示锁未被占用
if ("".equals(requestId)) {
lockModel.setRequest_id(request_id);
lockModel.setLock_count(1);
lockModel.setTimeout(System.currentTimeMillis() + lockTimeOut);
//并发情况下,采用cas方式更新记录
if (DbLockUtil.update(lockModel) == 1) {
lockResult = true;
break;
}
} else if (request_id.equals(requestId)) {
//如果requestId和表中request_id一样表示锁被当前线程持有者,此时需要加重入锁
lockModel.setTimeout(System.currentTimeMillis() + lockTimeOut);
lockModel.setLock_count(lockModel.getLock_count() + 1);
if (DbLockUtil.update(lockModel) == 1) {
lockResult = true;
break;
}
} else {
//锁不是自己的,并且已经超时了,则重置锁,继续重试
if (lockModel.getTimeout() < System.currentTimeMillis()) {
DbLockUtil.resetLock(lockModel);
} else {
//如果未超时,休眠100毫秒,继续重试
if (startTime + getTimeOut > System.currentTimeMillis()) {
TimeUnit.MILLISECONDS.sleep(100);
} else {
break;
}
}
}
}
}
} finally {
//释放jvm锁,将其从map中异常
jvmLock.unlock();
jvmLockMap.remove(lockKey);
}
}
log.info("end");
return lockResult;
}
/**
* 释放锁
*
* @param lock_key
* @throws Exception
*/
private static void unlock(String lock_key) throws Exception {
//获取当前线程requestId
String requestId = getRequestId();
LockModel lockModel = DbLockUtil.get(lock_key);
//当前线程requestId和库中request_id一致 && lock_count>0,表示可以释放锁
if (Objects.nonNull(lockModel) && requestId.equals(lockModel.getRequest_id()) && lockModel.getLock_count() > 0) {
if (lockModel.getLock_count() == 1) {
//重置锁
resetLock(lockModel);
} else {
lockModel.setLock_count(lockModel.getLock_count() - 1);
DbLockUtil.update(lockModel);
}
}
}
/**
* 重置锁
*
* @param lockModel
* @return
* @throws Exception
*/
private static int resetLock(LockModel lockModel) throws Exception {
lockModel.setRequest_id("");
lockModel.setLock_count(0);
lockModel.setTimeout(0L);
return DbLockUtil.update(lockModel);
}
/**
* 更新lockModel信息,内部采用乐观锁来更新
*
* @param lockModel
* @return
* @throws Exception
*/
private static int update(LockModel lockModel) throws Exception {
return exec(conn -> {
String sql = "UPDATE t_lock SET request_id = ?,lock_count = ?,timeout = ?,version = version + 1 WHERE lock_key = ? AND version = ?";
PreparedStatement ps = conn.prepareStatement(sql);
int colIndex = 1;
ps.setString(colIndex++, lockModel.getRequest_id());
ps.setInt(colIndex++, lockModel.getLock_count());
ps.setLong(colIndex++, lockModel.getTimeout());
ps.setString(colIndex++, lockModel.getLock_key());
ps.setInt(colIndex++, lockModel.getVersion());
return ps.executeUpdate();
});
}
private static LockModel get(String lock_key) throws Exception {
return exec(conn -> {
String sql = "select * from t_lock t WHERE t.lock_key=?";
PreparedStatement ps = conn.prepareStatement(sql);
int colIndex = 1;
ps.setString(colIndex++, lock_key);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return LockModel.builder().
lock_key(lock_key).
request_id(rs.getString("request_id")).
lock_count(rs.getInt("lock_count")).
timeout(rs.getLong("timeout")).
version(rs.getInt("version")).build();
}
return null;
});
}
private static int insert(LockModel lockModel) throws Exception {
return exec(conn -> {
String sql = "insert into t_lock (lock_key, request_id, lock_count, timeout, version) VALUES (?,?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
int colIndex = 1;
ps.setString(colIndex++, lockModel.getLock_key());
ps.setString(colIndex++, lockModel.getRequest_id());
ps.setInt(colIndex++, lockModel.getLock_count());
ps.setLong(colIndex++, lockModel.getTimeout());
ps.setInt(colIndex++, lockModel.getVersion());
return ps.executeUpdate();
});
}
private static <T> T exec(SqlExec<T> sqlExec) throws Exception {
Connection conn = getConn();
try {
return sqlExec.exec(conn);
} finally {
closeConn(conn);
}
}
@FunctionalInterface
public interface SqlExec<T> {
T exec(Connection conn) throws Exception;
}
@Getter
@Setter
@Builder
public static class LockModel {
private String lock_key;
private String request_id;
private Integer lock_count;
private Long timeout;
private Integer version;
}
private static final String url = "jdbc:mysql://localhost:3306/dlock?useSSL=false"; //数据库地址
private static final String username = ""; //数据库用户名
private static final String password = ""; //数据库密码
private static final String driver = "com.mysql.jdbc.Driver"; //mysql驱动
/**
* 连接数据库
*
* @return
*/
private static Connection getConn() {
Connection conn = null;
try {
Class.forName(driver); //加载数据库驱动
try {
conn = DriverManager.getConnection(url, username, password); //连接数据库
} catch (SQLException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭数据库链接
*
* @return
*/
private static void closeConn(Connection conn) {
if (conn != null) {
try {
conn.close(); //关闭数据库链接
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
4.方法2:Redisメソッド
4.1。使用されるいくつかのコマンド
-
setnxのコマンド形式:SETNXキー値。「SETif Not eXists」(存在しない場合はSET)の略です。キーが存在しない場合にのみ、キーの値がvalueに設定されます。キーがすでに存在する場合、SETNXコマンドは何もしません。コマンドは、設定が成功した場合は1を返し、設定が失敗した場合は0を返します。 -
getset
コマンド形式:GETSETキー値、キーの値をvalueに設定し、設定される前のキーの古い値を返します。戻り値:キーに古い値がない場合、つまり、設定される前にキーが存在しない場合、コマンドはnilを返します。キーは存在するが文字列タイプではない場合、コマンドはエラーを返します。 - Expire
コマンドの形式:EXPIREキー秒、使用:特定のキーの存続時間を設定します。キーの有効期限が切れると(存続時間は0)、自動的に削除されます。戻り値:正常に設定された場合は1を返します。キーが存在しない場合、またはキーの有効期間を設定できない場合(たとえば、2.1.3より前のバージョンのRedisでキーの有効期間を更新しようとした場合)、0を返します。
- del
コマンドの形式:DELキー[キー…]、使用:1つ以上の指定されたキーを削除します。存在しないキーは無視されます。戻り値:削除されたキーの数。
4.2。原則
プロセス分析については、最初に図の左側にあるプロセスを見てください。
1. Aがロックキーを取得しようとし、setnx(lockkey、currenttime + timeout)コマンドを使用してロックキーの値を現在の時刻+ロックタイムアウト時刻に設定します
。2 。戻り値が1の場合、redisサーバーがまだ存在していることを意味します。ロックキーがない場合、つまり他のユーザーがロックを所有していない場合、Aはロックを正常に取得できます。3。
関連するビジネス実行を実行する前に、expire(lockkey)を実行し、ロックキーの有効期間を設定してデッドロックを防止します。有効期間が設定されていない場合、 、ロックキーは常にredisに存在します。他のユーザーがロックを取得しようとすると、setnx(lockkey、currenttime + timeout)の
実行時にロックが正常に取得されません。4。関連ビジネスを実行し
ます。5。ロックを解放します。Aは関連ビジネスを完了します。その後、所有しているロックを解放する必要があります。つまり、redis、del(lockkey)でロックの内容を削除すると、ユーザーはロックの新しい値をリセットできます。
右側のプロセスを見てください
6. Aがsetnx(lockkey、currenttime + timeout)コマンドを使用してロックキーを正常に設定できない場合、ロックの取得が失敗したと直接結論付けることはできません。ロックを設定するときにロックタイムアウトタイムアウトを設定したため、現在の時間がredisより大きい場合ストレージキーがlockkeyの値である場合、前の所有者のロックを使用する権利が失効し、Aがロックを強制的に所有できると見なすことができます。具体的な決定プロセスは次のとおりです
。7。Aはget(lockkey)を介してredisを取得します。のストレージキー値は、ロックキーの値です。つまり、ロックを取得する相対時間lockvalueA
8、lockvalueA!= null && currenttime> lockvalue、Aは、現在の時刻がロック設定よりも大きい場合、現在の時刻をロック設定時刻と比較します。時間が重要です。つまり、ロックを取得できるかどうかをさらに判断できます。そうでない場合は、ロックがまだ占有されており、Aがまだロックを取得できず、ロックの取得が失敗します
。9。手順4の戻り結果がtrueになったら、getSetを使用して新しい結果を設定します。分散環境では、ここに入ると、別のプロセスがロックを取得して値を変更する可能性があるため、タイムアウトして、判断のために古い値lockvalueBを返します。古い値と戻り値のみが一貫しており、中間がそうではないことを示します。他のプロセスはこのロックを取得します
10、lockvalueB == null || lockvalueA == lockvalueB、判断:lockvalueBがnullの場合、ロックは解放されており、プロセスはこの時点でロックを取得できます。古い値は返されたlockvalueBと一致しています。これは、途中の他のプロセスによってロックが取得されず、ロックを取得できることを意味します。そうしないと、ロックを取得できず、ロックの取得に失敗します。
4.3、コード
それをみんなに任せて、次を達成するために上記のプロセスに従ってください。
5.方法3:動物園の飼育係
5.1。原則
動物園の飼育係とは何ですか?これは、可用性の高い構成センターとして使用できるオープンソースのミドルウェアです。簡単に理解してください。一部のユーザーデータを保存するために使用できます。
zookeeperには3つの重要な特性があり、これら2つの機能を実現するために、ロックオフ
ボタンが分散されています。
最初の機能:ノードは自然に整然としています
zookeeperに保存されるデータはツリー構造であり、ツリーの下に多くのノードを作成でき、ユーザーデータをノードに保存できます。
各ノードの下に子ノードを作成する場合、選択した作成タイプが順序付きタイプである限り、このノードは、クライアントによって指定されたノード名の後に単調に増加するシーケンス番号を自動的に追加します。ポイントは、子ノードが同時に作成される場合です。 、複数の子ノードの順序を保証することもできます。
たとえば、次のように、/ lock / lock1の下に4つの順序付けられた子ノードを同時に作成します。
クライアントは、作成されたノードのシーケンス番号が最小かどうかを判断でき、子ノードの中で番号が最小の場合、ロックは正常に取得されます。
2番目の機能:一時ノード
zookeeperを操作するには、クライアントはzookeeperとの接続を確立する必要があります。クライアントがzookeeperで作成するように要求するノードのタイプが一時ノードである場合、クライアントとzookeeperの間の接続が切断されると、作成される一時ノードは自動的にzookeeperになります。削除します。
これにより、複数の機能によるデッドロックを防ぐことができます。たとえば、ロックの取得後にクライアントが電話を切ると、ノードが自動的に削除され、他のロック取得者がロックを取得する機会が与えられます。
3番目の機能:リスナー
クライアントはリスナーをノードに追加できます。ノード情報が変更されると、zookeeperがクライアントに通知します。たとえば、ノードデータが変更されたり、ノードが削除されたりすると、クライアントに通知されます。
この機能は特に優れています。この特別な機能かっこいい、次のノードは自分の前のノードを監視するだけで済みます。前のノードが削除されると、動物園の飼育係はリスナーに通知します。リスナーは自分が作成したノード番号が最小かどうかを判断できます。最小の場合は取得します。ロックは成功します。これは上記のデータベースおよびredisメソッドよりも優れています。dbおよびredisメソッドはスピンする必要があり(取得が失敗し、しばらくスリープし、試行を繰り返します)、ロックが解除されたときにzookeeperをスピンする必要はありません。 Zookeeperはウェイターに通知します。
5.2、コード
原則を理解することに焦点を当てます。コードはオンラインで見つけることができます。他にもあるので、ここでは投稿しません。
6.方法4:etcd
Etcdとzookeeperは同様の機能を持ち、高可用性構成センターとしても使用できますが、etcdはGo言語に基づいており、分散ロックの実装にも使用できます。実装の原則はzookeeperに似ているため、ここでは詳しく説明しません。
7.まとめ
この記事では、主に分散ロックを実装する4つの方法を紹介します。誰もが、それぞれの方法の原則を理解することに集中する必要があります。
dbとredisの原理は似ています。内部取得に失敗した場合は、スピン方式を使用してロックの取得を再試行する必要がありますが、動物園の飼育係は監視方式を使用します。
redisとzookeeperの2つの方法がより多く使用され、redisはパフォーマンスが優れており、redisはより多くの同時実行で使用できます。
設計には別のポイントがあります。ロックを取得するとき、最初にjvmでロックを取得し、次に分散ロックを取得しようとする2つのステップに分割されます。
8.その他のインタビューの質問
- ステーションBで推奨されるビデオは何ですか?
- 古典的なインタビューの質問:equalsメソッドを書き直すときにhashCodeメソッドを書き直す必要があるのはなぜですか?
- 古典的なインタビューの質問:HashMap 16のデフォルトの容量はなぜですか?
- 古典的なインタビューの質問:ArraylistとLinkedlistの違いは何ですか???
- 古典的なインタビューの質問:NoClassDefFoundErrorとClassNotFoundExceptionの違いは何ですか?
- 古典的なインタビューの質問:Throwable、Exception、Error、RuntimeExceptionの違いは何ですか?
- 古典的なインタビューの質問:試行と最終的にリターンがある場合、コードはどのように実行されますか????
- MySQLが優れているかどうかにかかわらず、何億ものデータに直面しているので、見てみましょう。!
- 古典的なインタビューの質問:ThreadLocalシリアルガン!!
- 古典的なインタビューの質問:強い引用、柔らかい引用、弱い引用、誤った引用の違いは何ですか?
- インタビュアー:スレッドにはいくつの状態がありますか?それらはどのように相互に変換しますか?
・END・
QRコードをスキャン+フォローしてください