目次
7. テスト検証 " >7. テスト検証
ホームページ Java &#&面接の質問 インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

Aug 22, 2023 pm 03:57 PM
Javaの面接の質問

#皆さん、こんにちは。私はティアン兄弟です。

昨日、私は ## をしていました。友人 # 模擬面接、インターフェイス冪等性を実装するにはどうすればよいですか?彼の答えの口調から、彼が 8 部構成のエッセイを暗記していることがわかります。 そこで、インターフェースの冪等実装を誰もが簡単に体験できるように、Tian 兄弟 は今日この記事を手配しました

この記事には 9 つの主要な内容があります:

1. 電力コンセプト等価性 インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

# 冪等性は、平たく言えばインターフェイスです。同じリクエストを複数回開始する場合は、その操作が 1 回しか実行できないことを確認する必要があります。たとえば、 :
  • #注文インターフェイス、注文は複数回作成できません
  • 支払いインターフェイス、同じ注文の支払いは 1 回のみ引き落とし可能
  • Alipay コールバック インターフェイス、複数のコールバックが存在する可能性があり、繰り返されるコールバックは処理する必要があります
  • 通常のフォーム送信インターフェイス、ネットワーク タイムアウトやその他の理由により、クリックすることしかできません複数回送信しても成功できるのは 1 回だけです。お待​​ちください

2. 一般的な解決策

  1. 一意のインデックス -- 新しいダーティ データを防止します
  2. トークン メカニズム -- ページの繰り返し送信を防止します
  3. 悲観的ロック-- データ取得時にロックします(テーブルまたは行をロック)
  4. #楽観的ロック--バージョン番号に基づいて実装され、データが更新された時点でデータを検証します
  5. 分散ロック -- redis (jedis、redisson) または Zookeeper の実装
  6. ステート マシン -- 状態変更、データ更新時の状態の決定

3. この記事の実装 この記事では 2 番目の方法を使用して実装します。

Redis トークン

メカニズムは、インターフェイスの冪等性チェックを実現します。 <h2 data-tool="mdnice编辑器" style="margin-top: 30px;margin-bottom: 15px;font-weight: bold;border-bottom: 2px solid rgb(239, 112, 96);font-size: 1.3em;"> <span style="display: none;"></span><span style="display: inline-block;background: rgb(239, 112, 96);color: rgb(255, 255, 255);padding: 3px 10px 1px;border-top-right-radius: 3px;border-top-left-radius: 3px;margin-right: 3px;">4. 実装のアイデア</span><span style="display: inline-block;vertical-align: bottom;border-bottom: 36px solid #efebe9;border-right: 20px solid transparent;"> </span> </h2> <p data-tool="mdnice编辑器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;margin-top: 1px;margin-bottom: 1px;">冪等性を確保する必要があるリクエストごとに一意の識別子を作成します <code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">token 、まず token を取得し、この token を redis に保存します。インターフェイスをリクエストするときは、この token をヘッダーに置くか、インターフェイスをリクエストするためのリクエスト パラメーターとして入れます。バックエンド インターフェイスは、この token:

が Redis に存在するかどうかを判断します。
  • 存在する場合は、ビジネス ロジックを通常どおり処理し、この token を redis から削除します。その後、繰り返しのリクエストである場合は、token が既に削除されているため、削除されました。検証に合格できず、操作を繰り返さないでくださいを返しますプロンプト
  • これが存在しない場合は、パラメータが不正であるか、リクエストが無効であることを意味します。繰り返し、プロンプトを返すだけです

5. プロジェクトの紹介

  • Spring Boot
  • Redis
  • ##@ApiIdempotentアノテーション インターセプターリクエストをインターセプト
  • @ControllerAdviceグローバル例外処理
  • #ストレス テスト ツール:
  • Jmeter
  • 注:

この記事はべき等性のコア実装に焦点を当てています。Spring Boot が Redis、ServerResponse、ResponseCode をどのように統合するか、およびその他の詳細については範囲を超えています。この記事の内容。

6. コードの実装

1, maven依存関係

<!-- Redis-Jedis -->
<dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.9.0</version>
</dependency>

<!--lombok 本文用到@Slf4j注解, 也可不引用, 自定义log即可-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
</dependency>

2. JedisUtil

@Component
@Slf4j
public class JedisUtil {

    @Autowired
    private JedisPool jedisPool;

    private Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * 设值
     *
     * @param key
     * @param value
     * @return
     */
    public String set(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} error", key, value, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 设值
     *
     * @param key
     * @param value
     * @param expireTime 过期时间, 单位: s
     * @return
     */
    public String set(String key, String value, int expireTime) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.setex(key, expireTime, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 取值
     *
     * @param key
     * @return
     */
    public String get(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 删除key
     *
     * @param key
     * @return
     */
    public Long del(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.del(key.getBytes());
        } catch (Exception e) {
            log.error("del key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 判断key是否存在
     *
     * @param key
     * @return
     */
    public Boolean exists(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.exists(key.getBytes());
        } catch (Exception e) {
            log.error("exists key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 设值key过期时间
     *
     * @param key
     * @param expireTime 过期时间, 单位: s
     * @return
     */
    public Long expire(String key, int expireTime) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.expire(key.getBytes(), expireTime);
        } catch (Exception e) {
            log.error("expire key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    /**
     * 获取剩余时间
     *
     * @param key
     * @return
     */
    public Long ttl(String key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            return jedis.ttl(key);
        } catch (Exception e) {
            log.error("ttl key:{} error", key, e);
            return null;
        } finally {
            close(jedis);
        }
    }

    private void close(Jedis jedis) {
        if (null != jedis) {
            jedis.close();
        }
    }

}

3. カスタム アノテーション @ApiIdempotent

/**
 * 在需要保证 接口幂等性 的Controller的方法上使用此注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}

4. ApiIdempotentInterceptor インターセプター

/**
 * 接口幂等性拦截器
 */
public class ApiIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
        if (methodAnnotation != null) {
            check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
        }

        return true;
    }

    private void check(HttpServletRequest request) {
        tokenService.checkToken(request);
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

5、TokenServiceImpl

@Service
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private JedisUtil jedisUtil;

    @Override
    public ServerResponse createToken() {
        String str = RandomUtil.UUID32();
        StrBuilder token = new StrBuilder();
        token.append(Constant.Redis.TOKEN_PREFIX).append(str);

        jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE);

        return ServerResponse.success(token.toString());
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {// header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.isBlank(token)) {// parameter中也不存在token
                throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
            }
        }

        if (!jedisUtil.exists(token)) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        }

        Long del = jedisUtil.del(token);
        if (del <= 0) {
            throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
    }

}

6、TestApplication

@SpringBootApplication
@MapperScan("com.wangzaiplus.test.mapper")
public class TestApplication  extends WebMvcConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

    /**
     * 跨域
     * @return
     */
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 接口幂等性拦截器
        registry.addInterceptor(apiIdempotentInterceptor());
        super.addInterceptors(registry);
    }

    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor() {
        return new ApiIdempotentInterceptor();
    }

}

さて、上記はコードの実装部分です。それを確認してください。

7. テスト検証

tokenTokenController# のコントローラーを取得します。 ##:

@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private TokenService tokenService;

    @GetMapping
    public ServerResponse token() {
        return tokenService.createToken();
    }

}

TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响:

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Autowired
    private TestService testService;

    @ApiIdempotent
    @PostMapping("testIdempotence")
    public ServerResponse testIdempotence() {
        return testService.testIdempotence();
    }

}

获取token

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

查看Redis

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

测试接口安全性: 利用Jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。
インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为abcd

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。

8. 注意事項 (非常に重要)

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。
# # 上の図では、削除が成功したかどうかを確認せずにトークンを直接削除することはできません。複数のスレッドが同時に 46 行目に達する可能性があり、この時点ではトークンが削除されていないため、同時実行性のセキュリティの問題が発生します。

jedisUtil.del(token) の削除結果を確認せずに直接解放すると、実際の削除操作が 1 つだけであっても、重複送信の問題が依然として発生します。 、以下を 1 回再現します。

コードを少し変更します:

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。
もう一度リクエストします

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。
コンソールをもう一度見てください

インタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。
実際に削除されるのは 1 つのトークンだけですが、削除結果の検証が行われていないため、同時実行性の問題が依然として残っているため、検証が必要です。

9. 概要

実際、考え方は非常にシンプルで、各リクエストが一意であることが保証されています。これにより、interceptor アノテーション を介して 冪等性プロパティ が保証され、リクエストごとに繰り返しコードを記述する必要がなくなります。実際、Spring AOP## を使用して実装することもできます。 #。

わかりました。今日はここで共有します。 ##################################

以上がインタビュアー: 支払いインターフェースでは、同じ注文に対する繰り返しの支払いに対して、金額を差し引くことができるのは 1 回だけです。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

インタビュアー: Spring Aop の共通アノテーションと実行シーケンス インタビュアー: Spring Aop の共通アノテーションと実行シーケンス Aug 15, 2023 pm 04:32 PM

Spring について知っている必要があるので、Aop のすべての通知の順序について話しましょう。Spring Boot または Spring Boot 2 は AOP の実行順序にどのように影響しますか? AOP で遭遇した落とし穴について教えてください。

特定のグループへのインタビュー: オンラインで OOM に遭遇した場合、どのようにトラブルシューティングを行うべきですか?の解き方?どのようなオプションがありますか? 特定のグループへのインタビュー: オンラインで OOM に遭遇した場合、どのようにトラブルシューティングを行うべきですか?の解き方?どのようなオプションがありますか? Aug 23, 2023 pm 02:34 PM

OOM は、プログラムに脆弱性があることを意味します。これは、コードまたは JVM パラメータ設定が原因である可能性があります。この記事では、Java プロセスが OOM をトリガーした場合のトラブルシューティング方法について読者に説明します。

Ele.me の筆記試験問題は簡単そうに見えますが、多くの人が困惑します。 Ele.me の筆記試験問題は簡単そうに見えますが、多くの人が困惑します。 Aug 24, 2023 pm 03:29 PM

多くの企業の筆記試験の問題には落とし穴があり、うっかり陥る可能性がありますので、甘く見ないでください。サイクルに関するこの種の筆記試験問題に遭遇した場合は、冷静に考えて段階的に解答することをお勧めします。

面接での 5 つの質問。すべてに正解できる人は 10% 未満です。 (答え付き) 面接での 5 つの質問。すべてに正解できる人は 10% 未満です。 (答え付き) Aug 23, 2023 pm 02:49 PM

この記事では、Java String クラスに関する 5 つの面接の質問を取り上げます。私は面接プロセス中にこれら 5 つの質問のうちのいくつかを個人的に経験しました。この記事は、これらの質問に対する答えがなぜこのようになるのかを理解するのに役立ちます。

先週、XX保険の面接を受けましたが、とても良かったです。 ! ! 先週、XX保険の面接を受けましたが、とても良かったです。 ! ! Aug 25, 2023 pm 03:44 PM

「先週、グループの友人が平安保険の面接に行きました。結果は少し残念でした。非常に残念ですが、落ち込まないでほしいと思います。あなたが言ったように、基本的には、ここで出た質問はすべて解決しました」面接は面接の質問を暗記すれば解けますので、頑張ってください!

初心者も BAT 面接官と競争できる: CAS 初心者も BAT 面接官と競争できる: CAS Aug 24, 2023 pm 03:09 PM

Java並行プログラミングシリーズの番外編「C A S (Compare and swap)」は、絵と文章でわかりやすく、インタビュアーと夢中で会話できるスタイルを保っています。

ほぼすべての Java インタビューで聞かれる質問: ArrayList と LinkedList の違いについての話 ほぼすべての Java インタビューで聞かれる質問: ArrayList と LinkedList の違いについての話 Jul 26, 2023 pm 03:11 PM

Java のデータ構造がインタビューの焦点です。Java のインタビューに参加したことのある人なら誰でも、ある程度の経験があるはずです。面接官がこのような質問をするとき、単に「使い方を知っている」というレベルにとどまるのではなく、Java で一般的に使用されるデータ型の基礎となる構造を学習したかどうかを確認したいことがよくあります。

インタビュアー: クラスロードプロセスについて教えてください (10 個の図) インタビュアー: クラスロードプロセスについて教えてください (10 個の図) Aug 23, 2023 pm 03:05 PM

クラスを使用する場合は、ClassLoader を通じてクラスをメモリにロードする必要があります。

See all articles