Maison  >  Article  >  Java  >  Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

Java后端技术全栈
Java后端技术全栈avant
2023-08-22 15:57:311176parcourir

Bonjour à tous, je suis frère Tian

Hier, alors que je faisais une simulation d'interview pour un ami, comment mettre en œuvre l'idempotence d'interface ? Le ton de sa réponse montre qu'il est en train de mémoriser un essai à huit pattes.

Donc, afin de permettre à chacun de découvrir facilement l'implémentation idempotente de l'interface, frère Tian

a organisé cet article aujourd'hui .

Cet article a un total de neuf contenus principaux :

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

1. Le concept d'idempotence L'imdempotence, en termes simples, est une interface qui initie plusieurs fois la même requête . Il faut s'assurer que l'opération ne peut être exécutée qu'une seule fois, par exemple :

  • Interface de commande, les commandes ne peuvent pas être créées plusieurs fois
  • Interface de paiement, les paiements répétés pour la même commande ne peuvent être déduits qu'une seule fois
  • Interface de rappel Alipay, il peut y avoir plusieurs rappels, les rappels répétés doivent être traités
  • Interface de soumission de formulaire ordinaire, cliquez sur Soumettre plusieurs fois en raison d'un délai d'attente du réseau et d'autres raisons, ne peut réussir qu'une seule fois et ainsi de suite

2. Solutions communes

  1. Index unique - empêcher nouvelles données sales
  2. mécanisme de jeton - pour empêcher les soumissions répétées de pages
  3. verrouillage pessimiste - verrouillage lors de l'acquisition de données (verrouillage de la table ou de la ligne de verrouillage)
  4. verrouillage optimiste - implémenté en fonction du numéro de version, lorsque mise à jour des données Vérifier les données à un moment donné
  5. Verrouillage distribué - implémentation de redis (jedis, redisson) ou zookeeper
  6. machine d'état - changement d'état, juger de l'état lors de la mise à jour des données

3. Implémentation de cet article

Cet article utilise la deuxième façon de l'implémenter, c'est-à-dire d'implémenter la vérification de l'idempotence de l'interface via le mécanisme Redis + token.

4. Idées de mise en œuvre

Créez un identifiant unique pour chaque demande qui doit garantir l'idempotencetoken, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token :

  • S'il existe, traitez la logique métier normalement et supprimez-la de redisjeton, alors, s'il s'agit d'une requête répétée, en raison de jeton a été supprimé, il ne peut pas passer la vérification et renvoie Veuillez ne pas répéter l'opérationPrompttoken, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示
  • 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可

五、项目简介

  • Spring Boot
  • Redis
  • @ApiIdempotent注解 + 拦截器对请求进行拦截
  • @ControllerAdvice全局异常处理
  • 压测工具: Jmeter
S'il n'existe pas, cela signifie que le paramètre est illégal ou qu'il s'agit d'une requête répétée, renvoyez simplement le prompt

5. Introduction au projet

🎜Spring Boot🎜🎜🎜🎜Redis🎜🎜🎜🎜@ApiIdempotent annotation + intercepteur intercepte les requêtes 🎜🎜🎜🎜@ControllerAdviceGestion globale des exceptions🎜🎜🎜🎜Outil de test de stress : Jmeter🎜🎜🎜🎜Explication :🎜🎜🎜Cet article se concentre sur l'implémentation de base de l'idempotence, sur la façon dont Spring Boot Les détails de l'intégration de redis, ServerResponse, ResponseCode et d'autres détails dépassent le cadre de cet article.🎜

六、代码实现

1、maven依赖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();
    }

}

好了,以上便是代码的实现部分,下面我们就来验证一下。

七、测试验证

获取token的控制器TokenController

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

    @Autowired
    private TokenService tokenService;

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

}

2、@RestController @RequestMapping("/test") @Slf4j public class TestController { @Autowired private TestService testService; @ApiIdempotent @PostMapping("testIdempotence") public ServerResponse testIdempotence() { return testService.testIdempotence(); } }🎜3、自定义注解ApiIdempotentInterceptor  拦截器🎜rrreee🎜5、TokenServiceImpl🎜rrreee🎜6、<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);famille de polices : 'Operator Mono', Consolas, Monaco, Menlo, monospace;saut de mot : break-all;couleur : rgb(239, 112, 96);">TestApplication 🎜rrreee🎜好了,以上便是代码的实现部分,下面我们就来验证一下。🎜🎜🎜🎜🎜七、测试验证🎜🎜 🎜🎜🎜获取token的控制器TokenController :🎜
@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

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

查看Redis

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

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

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?
Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

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

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

8. Notes (très important)

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

Dans l'image ci-dessus, vous ne pouvez pas simplement supprimer le jeton directement sans vérifier si la suppression est réussie, car il y aura des problèmes de sécurité de concurrence, car, Il est possible que plusieurs threads puissent atteindre la ligne 46 en même temps, le jeton n'a pas été supprimé, donc l'exécution continue si le résultat de la suppression de jedisUtil.del(token) n'est pas vérifié et directement publié, le problème de la répétition. la soumission aura toujours lieu, même s'il n'y a en réalité qu'une véritable opération de suppression, reproduisez-la ci-dessous.

Modifiez légèrement le code :

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

Demande à nouveau

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

Regardez à nouveau la console

Intervieweur : Dans l'interface de paiement, l'argent ne peut être déduit qu'une seule fois pour les paiements répétés pour la même commande. Comment faire ?

Bien qu'un seul des jetons soit réellement supprimé, le résultat de la suppression n'étant pas vérifié, il reste toujours Les problèmes de concurrence doivent donc être vérifiés

9. Résumé

En fait, l'idée est très simple, c'est-à-dire que chaque demande est garantie unique, donc 保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用Spring AOP réalisée.

D'accord, je vais le partager ici aujourd'hui.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer