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

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

Java后端技术全栈
Java后端技术全栈転載
2023-08-22 15:57:311357ブラウズ

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

昨日、私は ## をしていました。友人 # 模擬面接、インターフェイス冪等性を実装するにはどうすればよいですか?彼の答えの口調から、彼が 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 サイトの他の関連記事を参照してください。

声明:
この記事はJava后端技术全栈で複製されています。侵害がある場合は、admin@php.cn までご連絡ください。