Como garantir a idempotência da interface na ordem de negócios

Em nosso negócio de pedidos, como garantir a idempotência da interface

1. Mecanismo de token

1. O servidor fornece uma interface para envio de tokens. Quando analisamos o negócio, quais empresas têm problemas idempotentes, devemos obter o token antes de executar o negócio, e o servidor salvará o token no redis

2. Então, ao chamar a solicitação de interface de negócios, carregue o token e, geralmente, coloque-o no cabeçalho da solicitação

3. O servidor avalia se há redis no token. Existência significa a primeira solicitação e, em seguida, exclui o token e continua a executar o negócio

4. Se for julgado que o token não existe no redis, significa que a operação se repete, e a marca repetida é devolvida diretamente ao cliente, o que garante que o código de negócio não será executado repetidamente

Perigoso:

1. Exclua o token primeiro ou exclua o token depois:

Excluir primeiro pode fazer com que o negócio não seja realmente executado. A nova tentativa deve trazer o token anterior. Devido ao design anti-repetição, a solicitação não pode ser executada e, em
seguida, a exclusão pode fazer com que o negócio seja processado com sucesso, mas o serviço é interrompido, o tempo limite ocorre e ele não é excluído. Token, outros continuam tentando, fazendo com que o negócio seja executado duas vezes.
Finalmente planejamos excluir o token primeiro. Se a chamada comercial falhar, obtenha o token novamente. Solicite novamente
2 . A aquisição, comparação e exclusão do token devem ser atômicas

redis.get (token), token.equals, redis.del (token), se essas duas operações não forem atômicas, isso pode fazer com que, sob alta simultaneidade, todas obtenham os mesmos dados, o julgamento seja bem-sucedido e o negócio continue a executar simultaneamente
Você pode usar lua script para completar esta operação no redis
"if redis.call ('get', KEYS [1]) == ARGV [1] então retornar redis.call ('del', KEYS [1]) else return 0 end "

2. Vários mecanismos de bloqueio

1. Bloqueio de banco de dados pessimista,
selecione * de xxx onde id = 1 para atualização;

Bloqueie este registro ao consultar por atualização, outros precisam esperar

Os bloqueios pessimistas são geralmente usados ​​em conjunto com as transações. O tempo de bloqueio dos dados pode ser muito longo e deve ser selecionado de acordo com a situação real. Além disso, deve-se observar que o campo id deve ser uma chave primária ou um índice único, caso contrário pode causar o resultado da tabela de bloqueio e processá-lo. Será muito problemático

2. Bloqueio otimista do banco de dados
Este método é adequado para cenários de atualização

update t_goods set count = count - 1, version = version + 1 onde good_id = 2 e version = 1

De acordo com a versão da versão, ou seja, obtenha o número da versão do produto atual antes de operar o inventário de dados e, em seguida, traga o número da versão ao operar. Vamos resolver, quando operamos o inventário pela primeira vez, temos

Quando a versão é 1, a versão do serviço de estoque = 2 é chamada, mas há um problema de retorno ao serviço de pedidos. O serviço de pedidos chama o serviço de estoque novamente. Quando a versão passada pelo serviço de pedidos ainda for 1, execute o procedimento acima

A instrução sql não será executada porque a versão se tornou 2 e a condição where não foi estabelecida. Isso garante que não importa quantas vezes seja chamada, ela será processada apenas uma vez. O bloqueio otimista é usado principalmente para lidar com o problema de mais leituras e menos gravações.

3. Bloqueio distribuído da camada de negócios.
Se várias máquinas podem processar os mesmos dados ao mesmo tempo, por exemplo, várias máquinas obtêm os mesmos dados em uma tarefa cronometrada, podemos adicionar um bloqueio distribuído para bloquear esses dados e liberar o bloqueio após o processamento está concluído. Para adquirir o bloqueio, você deve primeiro determinar se os dados foram processados

3. Várias restrições exclusivas

1. A restrição única do banco de dados. Ao
inserir dados, eles devem ser inseridos de acordo com o índice único, como o número do pedido. É impossível inserir dois pedidos para o mesmo pedido. Evitamos duplicação no nível do banco de dados.

Este mecanismo tira proveito da restrição única da chave primária do banco de dados e resolve o problema idempotente no cenário de inserção, mas o requisito da chave primária não é uma chave primária auto-incrementada, então o negócio precisa gerar uma chave única globalmente chave primária

Se for um cenário de sub-banco de dados e subtabela, as regras de roteamento devem garantir que a mesma solicitação seja enviada ao mesmo banco de dados e à mesma tabela, caso contrário, a restrição da chave primária do banco de dados não será eficaz, porque as chaves primárias de diferentes bancos de dados e tabelas não estão relacionados

2. Redis definir anti-ponderação.
Muitos dados precisam ser processados ​​e só podem ser processados ​​uma vez. Por exemplo, podemos calcular o MD5 dos dados e colocá-los no redis.

definido, cada vez que os dados são processados, primeiro verifique se o MD5 já existe e não processe se ele existir

4. Mesa anti-peso

Use a tabela de pedidos orderNo como o índice exclusivo da tabela de eliminação de duplicação, insira o índice exclusivo na tabela de eliminação de duplicação e, em seguida, execute as operações de negócios e eles estarão na mesma transação, de modo que as solicitações repetidas sejam garantidas porque o - a tabela de duplicação é única

Restrições, resultando em falha de solicitação, evitando idempotência e outros problemas, a tabela de desduplicação e a tabela de negócios devem estar no mesmo banco de dados, de modo a garantir que na mesma transação, mesmo que a operação de negócios falhe, os dados na tabela de desduplicação ser devolvido Saia disso

Uma boa garantia de consistência de dados,

A prevenção de peso Redis também é um mecanismo de token

5. ID exclusivo de solicitação global

Quando a interface é chamada, um id único é gerado, o redis salva os dados na coleção (deduplicação) e é processado quando existe. Você pode usar o nginx para definir um id único para cada solicitação

proxy_set_header X-Request-Id $ Request_id。

O que demonstramos aqui é o mecanismo de token da seguinte forma

Insira a descrição da imagem aqui
Ao clicar para liquidar e ir para a página de liquidação do pedido, você deve gerar um token anti-repetido idempotente no back-end e enviá-lo de volta ao front-end e, em seguida, passá-lo para o back-end para verificação quando o o pedido é enviado.
Insira a descrição da imagem aqui

    /**
     * 去结算
     * 给订单确认ye返回数据
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    
    

        //要返回的大对象
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //在TreadLocal中获取用户
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        //Feign + 异步任务 需要共享RequestAttributes 每个任务都要setRequestAttributes()
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
    
    
            //每一个线程都要共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1 远程查询大对象的第一个属性 收货地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
            confirmVo.setAddress(address);
            //Feign在远程调用之前要构造请求,调用很多的拦截器
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
    
    
            //每一个线程都要共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2 远程查询大对象的第二个属性 所有购物项
            List<OrderItemVo> items = cartFeignService.currentUserItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(() -> {
    
    
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> ids = items.stream().map((item) -> {
    
    
                return item.getSkuId();
            }).collect(Collectors.toList());
            //远程查看库存
            R r = wareFeignService.getSkuHasStock(ids);
            List<SkuStockVo> skuStockVos = r.getData(new TypeReference<List<SkuStockVo>>() {
    
    
            });
            if (skuStockVos != null) {
    
    
                Map<Long, Boolean> booleanMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(booleanMap);
            }
        }, executor);

        //3 远程查询用户积分
        Integer integration = memberResVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4 其他的数据自动计算

        //TODO 5 防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        //给服务器一个 并指定过期时间
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId(), token, 30, TimeUnit.MINUTES);
        //给页面一个
        confirmVo.setOrderToken(token);

        CompletableFuture.allOf(getAddressFuture, cartFuture).get();

        return confirmVo;
    }

Quando clicamos em enviar um pedido, um pedido será gerado, e então seremos redirecionados para a página de pagamento.
Ao gerar pedidos, no futuro, para garantir a idempotência da interface para evitar o envio repetido de pedidos, precisamos passar o token anti-repetido para verificação. Aqui, ao verificar o token atômico, um script é usado para garantir que a comparação e exclusão são sexuais atômicas. Semelhante a um bloqueio distribuído, mas aqui
// 1, primeiro verifique se o token
// 0 falhou-1 foi bem-sucedido | não existe 0 existe para excluir? 1: 0
String script = "if redis.call ('get', KEYS [1]) == ARGV [1] then return redis.call ('del', KEYS [1]) else return 0 end";

/**
     * 提交订单 去支付
     *
     * @Transactional 是一种本地事物,在分布式系统中,只能控制住自己的回滚,控制不了其他服务的回滚
     * 分布式事物 最大的原因是 网络问题+分布式机器。
     * (isolation = Isolation.REPEATABLE_READ) MySql默认隔离级别 - 可重复读
     */
    //分布式事务 全局事务
    //@GlobalTransactional 不用
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    
    

        //共享前端页面传过来的vo
        confirmVoThreadLocal.set(vo);

        //要返回到大对象
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        //登录的用户
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
        responseVo.setCode(0);
        //1、首先验证令牌
        //0失败 - 1成功 | 不存在0 存在 删除?1:0
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();
        //原子验证令牌 和 删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()),
                orderToken);
        if (result == 0) {
    
    
            //令牌验证失败 非0失败码
            responseVo.setCode(1);
            return responseVo;
        } else {
    
    
            //令牌验证成功 -> 执行业务代码
            //下单 去创建订单 验证令牌 验证价格 锁库存
            //TODO 3 保存订单
            OrderCreatTo orderCreatTo = creatOrder();
            BigDecimal payAmount = orderCreatTo.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();
            //金额对比
            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
    
    
                //保存到数据库
                saveOrder(orderCreatTo);
                //库存锁定 只要有异常就回本订单数据
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(orderCreatTo.getOrder().getOrderSn());
                List<OrderItemVo> collect = orderCreatTo.getOrderItems().stream().map((item) -> {
    
    
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(collect);
                /**
                 * TODO 4 远程锁库存 非常严重
                 * 库存成功了,但是网络原因超时了,订单可以回滚,库存不能回滚
                 * 为了保证高并发,库存需要自己回滚。 这样可以采用发消息给库存服务
                 * 库存服务本身也可以使用自动解锁模式 使用消息队列完成  使用延时队列
                 */
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0) {
    
    
                    //锁成功
                    responseVo.setOrder(orderCreatTo.getOrder());
                    //TODO 5 远程扣减积分
                    //库存成功了,但是网络原因超时了,订单回滚,库存不回滚
//                    int i = 1 / 0;//模拟积分系统异常
                    //TODO 订单创建成功,发消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", orderCreatTo.getOrder());
                    return responseVo;
                } else {
    
    
                    //锁定失败
                    String msg1 = (String) r.get("msg");
                    throw new NoStockException(msg1);
                }
            } else {
    
    
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }

Acho que você gosta

Origin blog.csdn.net/u014496893/article/details/114186531
Recomendado
Clasificación