記事ディレクトリ
マルチスレッド プログラミングでは、スレッド セーフが重要な概念であり、複数のスレッドが共有リソースにアクセスして変更するときの正確性と一貫性が関係します。スタンドアロン環境でも分散クラスター環境でも、スレッドの安全性の確保は開発者が注意を払う必要がある問題です。
スレッドセーフとは何ですか
スレッド セーフとは、プログラミング設計の用語です。これは、関数または関数ライブラリがマルチスレッド環境で呼び出されるときに、複数のスレッド間で共通の変数を正しく処理できるため、プログラム関数が正しく完了できることを意味します。
以上ウィキペディアより
スレッドセーフの重要性を説明する例を挙げてください。合計 10 席の映画を上映している映画館があるとします。スレッドセーフな保護手段がない場合、複数の人が同時に映画のチケットを購入しようと殺到すると、残りの座席が 0 より大きくなり、結果としてチケットが過剰に販売され、期待どおりにならない可能性があります。
サンプルコード:
public class MovieTicket {
private int availableTickets;
public MovieTicket(int totalTickets) {
this.availableTickets = totalTickets;
}
public void sellTickets(int numTickets, String user) {
if (numTickets > availableTickets) {
System.out.println("抱歉," + user + ",剩余票数不足!");
return;
}
// 模拟售票过程
// 例如查库 写库 调用远程服务等
try {
Thread.sleep(100); // 假设售票过程需要一定的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
availableTickets -= numTickets;
System.out.println(user + "购买了" + numTickets + "张票,剩余票数:" + availableTickets);
}
public int getTicketsAvailable() {
return availableTickets;
}
}
MovieTicket
このクラスは映画館の発券システムを表しますsellTicket()
このメソッドはチケットを販売するために使用され、呼び出しごとに残りのチケットの数が減り、チケット情報が出力されます。getTicketsAvailable()
このメソッドは残りの投票を取得するために使用されます
次に、 10 人のユーザーが同時に映画チケットを購入するシミュレーションをしてみましょう。
public class Test {
public static void main(String[] args) {
MovieTicket ticketCounter = new MovieTicket(10);
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread thread1 = new Thread(() -> ticketCounter.sellTickets(1, "User"+ finalI));
thread1.start();
}
}
}
実行結果は以下の通りです。
User7购买了1张票,剩余票数:3
User1购买了1张票,剩余票数:3
User8购买了1张票,剩余票数:3
User2购买了1张票,剩余票数:3
User9购买了1张票,剩余票数:3
User6购买了1张票,剩余票数:3
User4购买了1张票,剩余票数:3
User5购买了1张票,剩余票数:3
User0购买了1张票,剩余票数:3
User3购买了1张票,剩余票数:3
期待される結果は次のとおりです。
User0购买了1张票,剩余票数:9
User8购买了1张票,剩余票数:8
User7购买了1张票,剩余票数:7
User9购买了1张票,剩余票数:6
User6购买了1张票,剩余票数:5
User3购买了1张票,剩余票数:4
User5购买了1张票,剩余票数:3
User4购买了1张票,剩余票数:2
User2购买了1张票,剩余票数:1
User1购买了1张票,剩余票数:0
何が起こるか
複数のスレッドが共有リソースに対して同時に読み取りおよび書き込みを行うと、データ競合状態が発生し、データが汚染されたり、不確定な結果が生成されたりする可能性があります。
共有リソースには、カウンタ変数、配列、データベース内のレコード、またはその他のものが考えられます。
一般的な操作は次のとおりです。
- 動作の確認と動作(初期化)
- 読み取り-変更-書き込み操作(カウンターをインクリメント)
スレッドの安全性を確保する方法
スタンドアロン環境
1. ステートレスな設計
ステートレスなクラス、つまり、グローバル変数や共有状態を持たないクラスを設計します。共有リソースの競合を回避することで、スレッド間の競合や競合状態が軽減され、スレッドの安全性が確保されます。以下に例を示します。
サンプルコード:
public class ThreadSafeCalculator {
// 没有任何全局变量或共享状态
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
// 其他无状态的计算方法...
}
クラスをステートレスになるように設計することにより、各スレッドは独自のインスタンスを作成するか、同じインスタンスを共有してメソッドを独立して呼び出して計算を実行できます。共有状態がないため、スレッド間の競合や競合がなく、スレッドの安全性が確保されます。
ステートレス設計はすべてのシナリオに適しているわけではないことに注意してください。場合によっては、特定の機能を実装するために共有状態変数またはグローバル変数が実際に必要になる場合があります。この場合、共有リソースへのマルチスレッド アクセスの安全性を確保するには、適切な同期メカニズム (ロックの使用など) を採用する必要があります。
2.final キーワードを使用します (不変)
Java では、final 変数もスレッドセーフです。これは、あるオブジェクトの参照が割り当てられると、別のオブジェクトの参照を指すことができないためです。
コード例:
public class ThreadSafeCounter {
private final int limit = 100;
private final Object lock = new Object();
public void increment() {
}
public int getLimit() {
return limit;
}
}
final
キーワードは、変数参照が変更されないことを保証するだけであり、参照されるオブジェクトの内部状態の不変性を保証するものではないことに注意してください。参照オブジェクト自体が変更可能であり、複数のスレッドがそれを変更する場合でも、スレッドの安全性を確保するために追加の同期メカニズムが必要になります。
3. synchronized キーワードを使用する
synchronized キーワードを使用して、共有リソースのアクセス方法またはコード ブロックを変更し、同時に 1 つのスレッドのみが共有リソースにアクセスできるようにします。
synchronized キーワードを使用すると、複数のスレッドが共有リソースを同時に変更するのを防ぎ、データの一貫性と正確性を確保できます。スレッドがロックを取得すると、ロックが解放されるまで他のスレッドはブロックされます。
サンプルコード:
public synchronized void sellTickets(int numTickets, String user) {
// 线程安全的代码块
// ...
}
4. volatile キーワードを使用する
volatile キーワードは、共有変数を変更して、変数にアクセスするたびに、スレッドのローカル キャッシュを使用するのではなく、メイン メモリから最新の値が確実に読み取られるようにするために使用されます。複数のスレッド間の可視性は確保できますが、原子性と順序付けの問題は解決できません。したがって、volatile は、いくつかの単純な変数状態フラグやスイッチに適しています。
サンプルコード:
private volatile boolean flag = false;
public void setFlag(boolean value) {
flag = value;
}
public boolean getFlag() {
return flag;
}
5. java.util.concurrent.atomic パッケージのアトミック ラッパー クラスを使用します。
Java は、AtomicInteger、AtomicLong などの一連の Atomic クラスを提供します。これらはアトミック操作を提供し、読み取り操作と更新操作を 1 回の操作で完了できるため、スレッドの安全性が確保されます。Atomic クラスは、基礎となる CAS (比較および交換) 操作を使用して、アトミック性と可視性を確保します。
サンプルコード:
private AtomicInteger availableTickets = new AtomicInteger(10);
public void sellTickets(int numTickets, String user) {
int remainingTickets = availableTickets.getAndAdd(-numTickets);
// 线程安全的代码块
// ...
}
6. java.util.concurrent.locks パッケージのロックを使用する
ReentrantLock は Java によって提供される再入可能なロックであり、より高い柔軟性と拡張性を提供します。ロックを明示的に取得および解放することにより、1 つのスレッドのみが共有リソースにアクセスできるようにすることができます。synchronized キーワードと比較して、ReentrantLock は、割り込み可能なロック、公平なロックなど、より高度な機能を提供します。
サンプルコード:
private ReentrantLock lock = new ReentrantLock();
public void sellTickets(int numTickets, String user) {
lock.lock();
try {
// 线程安全的代码块
// ...
} finally {
lock.unlock();
}
}
7. スレッドセーフなコレクション クラスを使用する
Java は、ConcurrentHashMap、CopyOnWriteArrayList など、多くのスレッドセーフなデータ構造を提供します。これらのデータ構造は、スレッドセーフなアクセスおよび変更メカニズムを内部で実装しており、追加の同期手段を必要とせずにマルチスレッド環境で直接使用できます。
サンプルコード:
private Map<String, Integer> map = new ConcurrentHashMap<>();
public void updateMap(String key, int value) {
map.put(key, value);
}
8. ThreadLocal を使用する
ThreadLocal は、Java によって提供されるスレッド クロージャ メカニズムであり、各スレッドに独立した変数のコピー (時間のスペース) を提供できます。共有変数を ThreadLocal に保存することで、複数のスレッド間のデータ共有と競合を回避でき、スレッドの安全性が確保されます。
サンプルコード:
private ThreadLocal<Integer> threadLocalCount = ThreadLocal.withInitial(() -> 0);
public void incrementCount() {
int count = threadLocalCount.get();
threadLocalCount.set(count + 1);
}
クラスタ環境
クラスター環境では、スレッドの安全性を確保するために、より多くの要素と課題を考慮する必要があります。クラスターには複数のサーバーと複数のプロセス/スレッドが同時に実行されるため、スレッド セーフの維持はより複雑になります。以下に、クラスター環境でスレッドの安全性を確保するための一般的なシナリオをいくつか示します。
1. 分散ロック
- 分散ロックを使用して、複数のノード間で共有リソースへのアクセスを調整します。
- 一般的な分散ロックの実装には、データベース ベースのロック、キャッシュ ベースのロック (Redis ロックなど)、および ZooKeeper ベースのロックが含まれます。
- 共有リソースにアクセスする前に、ノードは分散ロックを取得して、1 つのノードだけがクリティカル セクション コードを実行できるようにする必要があります。
疑似コードの例:
// 加锁
if (acquireLock(key)) {
try {
// 执行操作
} finally {
// 释放锁
releaseLock(key);
}
}
参考
2. データのシャーディング、データの分割、分離
- 共有データをいくつかの部分に分割し、各部分を別のノードに割り当てて処理します。
- 各ノードは、自身に割り当てられたデータ フラグメントのみを担当し、複数のノードが同時に同じデータにアクセスすることを防ぎます。
- ハッシュベース、一貫性のあるハッシュ、範囲など、データの特性や負荷条件に応じて、適切なデータ断片化戦略を選択できます。
疑似コードの例:
// 获取数据分片的节点
Node node = getShardNode(key);
// 在指定节点上执行操作
result = node.processData(key, data);
3. シリアル化により同時実行性が回避される
- メッセージキューをデータ交換のミドルウェアとして利用し、共有リソースの操作を非同期メッセージの形式に変換します。
- 各ノードはメッセージ キューからメッセージを受信して処理し、各メッセージを 1 つのノードだけが処理するようにします。
- メッセージ キューは、信頼性の高いメッセージ配信メカニズムを提供し、メッセージ消費の順序を通じてデータの一貫性を確保できます。
- 何らかの戦略とビジネス設計を通じて同時実行を回避します。
// 发送消息到消息队列
queue.send(key,message);
// 在节点上异步消费消息
queue.consume(key,message -> {
// 处理消息
});
4. 分散アトミック操作
Redis は、クラスター環境で一般的な分散アトミック操作を実装できるアトミック コマンドをいくつか提供します。一般的に使用される Redis アトミック コマンドと例をいくつか示します。
1.SETNX(存在しない場合は設定)
指定されたキーが存在しない場合、キーの値が指定された値に設定され、操作はアトミックです。
// 设置键名为 "key" 的值为 "value",仅当该键不存在时
jedis.setnx("key", "value");
2. アトミックカウンター
Redis の INCR および DECR コマンドは、Redis に保存されている整数値に対してアトミックな操作を実行できます。
// 自增计数器
Long incrementedValue = jedis.incr("counter_key");
// 自减计数器
Long decrementedValue = jedis.decr("counter_key");
3. トランザクションのアトミック操作の組み合わせ
Redis は、複数の操作のアトミックな実行を実現できる MULTI/EXEC/WATCH コマンドの組み合わせを提供します。
// 监视键
jedis.watch("key");
// 开启事务
Transaction transaction = jedis.multi();
// 执行多个操作
transaction.set("key1", "value1");
transaction.set("key2", "value2");
// 提交事务
List<Object> results = transaction.exec();
4.luaスクリプト
Redis は、より複雑なアトミック操作を実装するために使用できる Lua スクリプト サポートを提供します。複数の Redis コマンドを Lua スクリプトに組み合わせることで、スクリプトの実行時に Redis はスクリプト全体をアトミック操作として実行し、実行中に他のコマンドによって中断されないようにします。
Redis Lua スクリプトを使用すると、スレッドの安全性を確保し、過剰販売の問題を回避できます。
-- Lua 脚本代码
local key = KEYS[1] -- 键名
local quantity = ARGV[1] -- 购买数量
local remaining = tonumber(redis.call('GET', key)) -- 获取当前剩余票数
if remaining and remaining >= tonumber(quantity) then
redis.call('DECRBY', key, quantity) -- 减少票数
return 1 -- 返回成功标志
else
return 0 -- 返回失败标志
end
この Lua スクリプトでは、まず指定されたキーの現在の残り投票を取得し、購入した数量に基づいて判断します。残りの投票が十分な場合は、RedisDECRBY
コマンドを使用して投票数をアトミックに減らし、成功フラグを返します。それ以外の場合は、失敗フラグを直接返します。
Java では、Jedis や Lettuce などの Redis クライアントを使用して Lua スクリプトを実行できます。以下は、Jedis を使用して Lua スクリプトを実行するためのサンプル コードです。
Jedis jedis = new Jedis("localhost", 6379);
String script = "local key = KEYS[1]\n" +
"local quantity = ARGV[1]\n" +
"local remaining = tonumber(redis.call('GET', key))\n" +
"if remaining and remaining >= tonumber(quantity) then\n" +
" redis.call('DECRBY', key, quantity)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
String key = "ticket";
String quantity = "2";
// 执行 Lua 脚本
Long result = (Long) jedis.eval(script, Collections.singletonList(key),Collections.singletonList(quantity));
if (result == 1) {
// 购票成功
System.out.println("购票成功");
} else {
// 购票失败
System.out.println("购票失败");
}
この Lua スクリプトを実行することで、分散環境におけるスレッドの安全性を確保し、映画チケットの過剰販売の問題を回避できます。複数のスレッドまたはノードが同時にスクリプトを実行する場合、Redis は Lua スクリプトのアトミック性を保証し、チケット購入操作の正確さと一貫性を保証します。
データベースにはさまざまな種類のアトミック操作があり、開発における一般的な例をいくつか次に示します。
アトミックカウンタ: 通常はカウンタの値を増減することによって、データベース内のカウンタに対してアトミック操作を実行します。
例: 記事の表のビューカウンタをインクリメントします。
UPDATE articles SET view_count = view_count + 1 WHERE id = 456;
5. アトミック操作 CAS + リトライ/フェイルファースト (一般的なソリューション - トークン制限保護)
クラスター環境でスレッドの安全性を確保するには、アトミック操作とトークン保護を組み合わせることが効果的なソリューションです。このスキームは、アトミック操作とトークン メカニズムを使用して、複数のスレッドまたはノード間の調整と相互排他を保証します。このスキームの詳細な説明とサンプル疑似コードは次のとおりです。
- アトミック操作: データベースまたは分散ストレージ システムによって提供されるアトミック操作を使用して、データの一貫性を確保します。これらのアトミック操作には、アトミックな追加、アトミックな更新、アトミックな削除などが含まれており、特定のビジネス要件に応じて適切なアトミックな操作を選択できます。
- トークン保護メカニズム: 一連の安全でない操作を実行する前に、トークン フェッチ操作を導入します。トークンの数は、クラスター内のリソースまたは運用能力に関連付けられます。各スレッドまたはノードが安全でない操作を実行する前に、トークン プールからトークンを取得する必要があります。トークンを取得するプロセスはスレッドセーフである必要があり、これはアトミック操作を使用して実現できます。
- 再試行/フェイルファースト: スレッドまたはノードがトークンを取得できない場合、つまりキー操作段階に入ることができない場合、再試行するか操作を放棄するかを選択できます。再試行メカニズムにより、スレッドは待機し、成功するまでトークンの取得を再度試行できます。フェイルファースト メカニズムは、リソースの無駄を避けるために操作をすぐに中止します。
以下は、アトミック操作とトークン保護のためのクラスター スレッドセーフ スキームを示す疑似コードの例です。
int maxRetries = 3;
int retryInterval = 100; // milliseconds
int currentRetry = 0;
boolean success = false;
while (!success && currentRetry < maxRetries) {
// 尝试获取令牌
if (threadSafeAcquireToken()) {
try {
// 执行一组不安全的操作
executeUnsafeOperations();
success = true;
} finally {
// 释放令牌
releaseToken();
}
} else {
// 没有获取到令牌,选择重试或者放弃操作
currentRetry++;
handleRetryOrFail();
// 拿不到令牌,等待一段时间后重试
Thread.sleep(retryInterval);
}
}
上記の映画チケットの販売を例に挙げると、このトークンは一般トークンでもビジネストークンでもよく、例えばここでのトークン制限は実際には映画1本あたり10人、つまり10トークンという制限になっています。
acquire_token.lua
トークンを取得するためのLua スクリプトを作成します。
local key = KEYS[1] -- 令牌池键名
local tokenCount = tonumber(ARGV[1]) -- 需要获取的令牌数量
local currentCount = tonumber(redis.call('GET', key)) -- 获取当前令牌数量
if currentCount and currentCount >= tokenCount then
redis.call('DECRBY', key, tokenCount) -- 减少令牌数量
return 1 -- 获取令牌成功
else
return 0 -- 获取令牌失败
end
release_token.lua
トークンを解放するためのLua スクリプトを作成する
local key = KEYS[1] -- 令牌池键名
local tokenCount = tonumber(ARGV[1]) -- 需要释放的令牌数量
redis.call('INCRBY', key, tokenCount) -- 增加令牌数量
Java で Jedis を使用して Lua スクリプトを実行します。
int maxRetries = 3; // 最大重试次数
int retryDelayMillis = 100; // 重试延迟时间
int retryCount = 0;
boolean acquiredToken = false;
// 获取令牌
while (!acquiredToken && retryCount < maxRetries) {
Long acquireResult = (Long) jedis.eval(acquireScript, Collections.singletonList(电影id), Collections.singletonList(String.valueOf(tokenCount)));
if (acquireResult == 1) {
acquiredToken = true;
} else {
retryCount++;
try {
Thread.sleep(retryDelayMillis);
} catch (InterruptedException e) {
}
}
}
// 处理业务
if (acquiredToken) {
try {
// 执行线程安全的操作 重点 重点 重点,这里是一大堆操作需要保证线程安全的
// 远程调用
// 写库
// ...
} finally {
// 释放令牌
jedis.eval(releaseScript, Collections.singletonList(电影id), Collections.singletonList(String.valueOf(tokenCount)));
}
} else {
// 重试次数超过阈值,执行其他处理逻辑或抛出异常
// ...
throw
}
要約する
上記の分類と分析を通じて、 Redisベースの一般的なトークン制限保護戦略に基づいた疑似コードを作成します。
public class RedisTokenProtection {
private final Jedis jedis;
private final String tokenPoolKey;
private final int maxRetries;
private final long retryInterval;
/**
* 构造函数
*
* @param jedisSupplier 提供 Jedis 实例的供应商
* @param tokenPoolKey 令牌池的键名
* @param maxRetries 最大重试次数
* @param retryInterval 重试间隔时间(毫秒)
*/
public RedisTokenProtection(Supplier<Jedis> jedisSupplier, String tokenPoolKey, int maxRetries, long retryInterval) {
this.jedis = jedisSupplier.get();
this.tokenPoolKey = tokenPoolKey;
this.maxRetries = maxRetries;
this.retryInterval = retryInterval;
}
/**
* 执行带有令牌保护的业务逻辑
*
* @param limitTokenCount 限制令牌数
* @param requestTokenKey 请求令牌的键名
* @param requestTokenCount 请求令牌的数量
* @param totalTimeout 总的执行超时时间(毫秒)
* @param supplier 提供业务逻辑的供应商
* @param <T> 返回值的类型
* @return 业务逻辑的返回值
* @throws TokenAcquisitionException 令牌获取异常
*/
public <T> T executeWithTokenProtection(int limitTokenCount, String requestTokenKey, int requestTokenCount, long totalTimeout, Supplier<T> supplier) throws TokenAcquisitionException {
long startTime = System.currentTimeMillis();
try {
// 尝试获取令牌
boolean acquiredToken = acquireToken(limitTokenCount, requestTokenKey, requestTokenCount);
if (acquiredToken) {
// 成功获取令牌后执行业务逻辑
return supplier.get();
}
throw new TokenAcquisitionException("Failed to acquire tokens.");
} catch (TokenAcquisitionException ex) {
throw ex;
} catch (Exception ex) {
long elapsedTime = System.currentTimeMillis() - startTime;
int retries = 0;
while (retries < maxRetries && elapsedTime < totalTimeout) {
try {
// 等待重试间隔
Thread.sleep(retryInterval);
boolean acquiredToken = acquireToken(limitTokenCount, requestTokenKey, requestTokenCount);
if (acquiredToken) {
// 成功获取令牌后执行业务逻辑
return supplier.get();
}
retries++;
elapsedTime = System.currentTimeMillis() - startTime;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
throw new TokenAcquisitionException("Failed to acquire tokens after retrying " + maxRetries + " times.");
} finally {
releaseToken(requestTokenKey, requestTokenCount);
}
}
// 获取令牌的逻辑,实现方法根据具体需求自行编写
// 必须是原子的
private boolean acquireToken(int limitTokenCount, String requestTokenKey, int requestTokenCount) {
String acquireTokenScript =
"local availableTokens = tonumber(redis.call('get', KEYS[1])) or 0\n" +
"if availableTokens >= tonumber(ARGV[1]) then\n" +
" redis.call('decrby', KEYS[1], ARGV[1])\n" +
" return true\n" +
"else\n" +
" return false\n" +
"end";
Object result = jedis.eval(acquireTokenScript, Collections.singletonList(requestTokenKey),
Collections.singletonList(String.valueOf(requestTokenCount)));
return (Boolean) result;
}
// 释放令牌的逻辑,实现方法根据具体需求自行编写
// 必须是原子的
private void releaseToken(String requestTokenKey, int requestTokenCount) {
String releaseTokenScript =
"redis.call('incrby', KEYS[1], ARGV[1])";
jedis.eval(releaseTokenScript, Collections.singletonList(requestTokenKey),
Collections.singletonList(String.valueOf(requestTokenCount)));
}
public class TokenAcquisitionException extends Exception {
public TokenAcquisitionException(String message) {
super(message);
}
}
}
呼び出し例:
public class Main {
public static void main(String[] args) {
// 创建 Jedis 实例的供应商
Supplier<Jedis> jedisSupplier = () -> {
// 这里创建和配置 Jedis 实例,例如连接到 Redis 服务器
return new Jedis("localhost");
};
// 创建 RedisTokenProtection 实例
RedisTokenProtection tokenProtection = new RedisTokenProtection(jedisSupplier, "token_pool:", 3, 1000);
try {
// 执行带有令牌保护的业务逻辑
String movieId = "亮剑";
boolean result = tokenProtection.executeWithTokenProtection(10, movieId, 1, 10000, () -> {
// 这里编写需要保护的线程不安全的业务逻辑
System.out.println("执行业务逻辑...");
// 假设这里有一段需要保护的代码
// ...
// 返回业务逻辑执行的结果
return true;
});
if (result) {
System.out.println("业务逻辑执行成功!");
} else {
System.out.println("业务逻辑执行失败!");
}
} catch (RedisTokenProtection.TokenAcquisitionException ex) {
System.out.println("获取令牌失败:" + ex.getMessage());
}
}
}
Spring AOPとカスタムアノテーションを組み合わせて利用すると、より便利にトークン保護の機能を実現できます。Spring AOP とカスタム アノテーションを使用してトークン保護を実装する方法を示すサンプル コードを次に示します。
まず、カスタム アノテーションを定義して、TokenProtected
トークン保護が必要なメソッドをマークします。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenProtected {
int limitTokenCount() default 1;
String requestTokenKey();
int requestTokenCount() default 1;
long totalTimeout() default 0;
}
TokenProtectionAspect
次に、 Spring AOP を使用してトークン保護ロジックを実装するアスペクト クラスを作成します。
@Aspect
@Component
public class TokenProtectionAspect {
private final RedisTokenProtection tokenProtection;
@Autowired
public TokenProtectionAspect(RedisTokenProtection tokenProtection) {
this.tokenProtection = tokenProtection;
}
@Pointcut("@annotation(com.example.TokenProtected)")
public void tokenProtectedMethod() {
}
@Around("tokenProtectedMethod() && @annotation(tokenProtected)")
public Object protectWithToken(ProceedingJoinPoint joinPoint, TokenProtected tokenProtected) throws Throwable {
int limitTokenCount = tokenProtected.limitTokenCount();
String requestTokenKey = tokenProtected.requestTokenKey();
int requestTokenCount = tokenProtected.requestTokenCount();
long totalTimeout = tokenProtected.totalTimeout();
Supplier<Object> supplier = () -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
return tokenProtection.executeWithTokenProtection(limitTokenCount, requestTokenKey, requestTokenCount, totalTimeout, supplier);
}
}
tokenProtectedMethod()
このアスペクト クラスでは、アノテーション付きメソッドと一致するポイントカットを定義しますTokenProtected
。このメソッドでは、アノテーションのパラメーターをprotectWithToken
取得し、トークン保護のロジックを実行するインスタンスを作成します。TokenProtected
RedisTokenProtection
最後に、これを使用するときは、トークン保護が必要なメソッドにアノテーションを追加し@TokenProtected
、対応するパラメーターを構成するだけです。
@Service
public class MyService {
@TokenProtected(limitTokenCount = 100, requestTokenKey = "myTokenKey", requestTokenCount = 1, totalTimeout = 5000)
public void protectedMethod() {
// 令牌保护的业务逻辑
}
}
上記の例では、protectedMethod
メソッドはトークン保護が必要であるとマークされており、関連するトークン パラメーターが提供されています。
上記の手順により、Spring AOP とカスタム アノテーションを使用して、便利で使いやすいトークン保護メカニズムを実装できます。アスペクト クラスは、@TokenProtected
アノテーション付きメソッドをインターセプトし、実行前と実行後にトークンを取得および解放します。