1. インターフェースの冪等性とは何ですか?
HTTP/1.1 では、冪等性が定義されており、リソースに対する 1 つまたは複数のリクエストは、リソース自体に対して同じ結果をもたらす必要があります (ネットワーク タイムアウトなどの問題を除く)。つまり、最初のリクエストはリソースに副作用をもたらします。後続のリクエストはリソースに副作用を及ぼさなくなります。
簡単に言うと、複数の実行はリソース自体に 1 回の実行と同じ影響を与えます。
2. インターフェイスの冪等性を実装する必要があるのはなぜですか?
通常の状況では、インターフェイスが呼び出されると、情報は正常に返され、繰り返し送信されることはありませんが、次のような状況が発生した場合に問題が発生する可能性があります。
- フロントエンドでのフォームの繰り返しの送信: 一部のフォームに入力すると、ユーザーは送信を完了しますが、多くの場合、ネットワークの変動により、ユーザーは送信の成功に時間内に応答せず、送信が完了したとユーザーに思わせます。が失敗し、送信ボタンをクリックし続けると、このような現象が発生します。フォーム要求を繰り返し送信してください。
- ユーザーが悪意を持って不正行為を行う: たとえば、ユーザー投票機能を実装する場合、ユーザーが繰り返し投票を送信すると、インターフェイスがユーザーによって繰り返し送信された投票情報を受信することになり、投票結果が重大な影響を与える可能性があります。事実と矛盾します。
- インターフェイスのタイムアウトと繰り返しの送信: 多くの場合、HTTP クライアント ツールは、特にサードパーティがインターフェイスを呼び出す場合に、デフォルトでタイムアウト再試行メカニズムを有効にします。ネットワークの変動やタイムアウトなどによるリクエストの失敗を防ぐために、再試行メカニズムが有効になります。が追加されるため、1 つのリクエストに対して複数の送信が行われることになります。
- メッセージの繰り返し消費: MQ メッセージ ミドルウェアを使用する場合、メッセージ ミドルウェアでエラーが発生し、消費情報が時間内に送信されないと、繰り返し消費が発生します。
べき等性を使用する最大の利点は、インターフェースがべき等な動作を保証し、システム内の再試行などによって引き起こされる未知の問題を回避できることです。
3. インターフェースの冪等性を実現する方法
解決策: 重複防止トークン
プログラムの説明:
クライアントが継続的にクリックしたり、呼び出し元がタイムアウトして再試行したりする状況 (注文の送信など) では、トークン メカニズムを使用して、繰り返しの送信を防ぐことができます。
簡単に言うと、インターフェイスを呼び出すとき、呼び出し元は最初にバックエンドからグローバル ID (トークン) を要求し、このグローバル ID を要求とともに送信します (トークンをヘッダーに入れるのが最善です)。このTokenをKeyとして、ユーザー情報をValueとしてRedisに送信し、Key値の内容検証を行い、Keyが存在し、Valueが一致する場合には削除コマンドが実行され、以降のビジネスロジックが正常に実行されます。対応するキーがない場合、または値が一致しない場合は、冪等な操作を保証するためにエラー メッセージが繰り返し返されます。
適用可能な操作:
- 挿入操作
- 更新操作
- 削除操作
主なプロセス:
-
① サーバーはトークンを取得するためのインターフェースを提供し、トークンはシリアル番号、配布 ID、または UUID 文字列になります。
-
② クライアントはトークンを取得するためにインターフェースを呼び出しますが、このときサーバーはトークン文字列を生成します。
-
③ 次に、トークンを Redis キーとして使用して、文字列を Redis データベースに保存します (有効期限に注意してください)。
-
④ トークンをクライアントに返却し、クライアントがトークンを取得したら、フォームの隠しフィールドに保存します。
-
⑤ クライアントはフォームを実行して送信すると、トークンをヘッダーに格納し、ビジネスリクエストの実行時にヘッダーを保持します。
-
⑥ リクエストを受信したサーバーはヘッダーからトークンを取得し、そのトークンに基づいて Redis を検索してキーが存在するかどうかを確認します。
-
⑦ サーバーは、Redis にキーが存在するかどうかを判断し、存在する場合はキーを削除し、ビジネス ロジックを通常どおり実行します。存在しない場合は、例外がスローされ、繰り返し送信されるとエラー メッセージが返されます。
例: 重複防止トークン
SpringBoot プロジェクトを作成し、Redis 依存関係を導入する
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
トークンツールクラス:
@Slf4j
@Component
public class TokenUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 存入 Redis 的 Token 键的前缀
*/
private static final String REPEAT_TOKEN_PREFIX = "repeat_token:";
public String createToken(String value) {
// 实例化生成 ID 工具对象
String token = UUID.randomUUID().toString();
// 设置存入 Redis 的 Key
String key = REPEAT_TOKEN_PREFIX + token;
// 存储 Token 到 Redis,且设置过期时间为5分钟
redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
// 返回 Token
return token;
}
public boolean validToken(String token, String value) {
// 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value
String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 根据 Key 前缀拼接 Key
String key = REPEAT_TOKEN_PREFIX + token;
// 执行 Lua 脚本
Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
// 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,如果结果不为空和0,则验证通过
if (result != null && result != 0L) {
log.info("验证 token={},key={},value={} 成功", token, key, value);
return true;
}
log.info("验证 token={},key={},value={} 失败", token, key, value);
return false;
}
}
トークンコントローラー:
@RestController
public class TokenController {
@Autowired
private TokenUtil tokenUtil;
@GetMapping("/getToken")
public String getToken(String userId) {
return tokenUtil.createToken(userId);
}
@GetMapping("/test")
public String test(@RequestHeader("token") String token, String userId) {
boolean result = tokenUtil.validToken(token, userId);
if (result) {
// TODO 进行业务处理
return "正常调用";
}
return "重复调用";
}
}
郵便配達員のテスト
まずトークンを取得します
次に、インターフェース呼び出しを行います
4. 最適化
プロジェクト内に実際の冪等性を検証する必要があるインターフェイスが 10 個ある場合、このコードは
boolean result = tokenUtil.validToken(token, userId);
if (result) {
// TODO 进行业务处理
return "正常调用";
}
10回も書かないといけないの?? ? 明らかにDRY原則に違反しています。
解決策: カスタム アノテーション + AOP