Envío de orden de idempotencia de interfaz

Tabla de contenido

1. ¿Qué es la idempotencia?

2. ¿Qué situaciones hay que prevenir?

3. ¿Bajo qué circunstancias se requiere la idempotencia?

4. Soluciones idempotentes

1. Mecanismo de fichas

2. Varios mecanismos de bloqueo.

(1) Bloqueo pesimista de la base de datos

(2) Bloqueo optimista de la base de datos

(3) Bloqueo distribuido de capa empresarial

3. Varias limitaciones únicas

(1) Restricciones únicas de la base de datos

(2) redis configurado para evitar la redundancia

4. Reloj antipesado

5. Identificación única de solicitud global

Caso: envío de pedido


1. ¿Qué es la idempotencia?

La idempotencia de la interfaz significa que los resultados de una solicitud o múltiples solicitudes iniciadas por el usuario para la misma operación son consistentes y no habrá efectos secundarios debido a múltiples clics; por ejemplo, en el escenario de pago, el usuario compra el producto y el pago se deduce con éxito, pero la devolución Cuando se genera el resultado, la red es anormal. En este momento, el dinero se ha deducido. El usuario vuelve a hacer clic en el botón y se realizará la segunda deducción. El resultado es devuelto con éxito. Cuando el usuario verifica el saldo, descubre que se ha deducido más dinero y el registro de transacciones se ha convertido en dos. . Esto no garantiza la idempotencia de la interfaz.

2. ¿Qué situaciones hay que prevenir?

  • El usuario hace clic en el botón varias veces
  • La página del usuario regresa y se envía nuevamente.
  • Los microservicios se llaman entre sí y la solicitud falla debido a problemas de red. fingir desencadena el mecanismo de reintento
  • Otras condiciones comerciales

3. ¿Bajo qué circunstancias se requiere la idempotencia?

Tomando SQL como ejemplo, algunas operaciones son naturalmente idempotentes.
SELECT * FROM table WHER id=? no cambiará el estado sin importar cuántas veces se ejecute. Es naturalmente idempotente .
ACTUALIZAR tab1 SET col1=1 WHERE col2=2 , el estado es consistente sin importar cuántas veces se ejecute con éxito, y también es una operación idempotente.
eliminar del usuario donde ID de usuario = 1 , múltiples operaciones, el resultado es el mismo, es idempotente
insert into user(userid,name) value(1,'a') Si el ID de usuario es la única clave principal, es decir, si repite lo anterior, solo se insertará un dato del usuario, que es idempotente .
ACTUALIZAR tab1 SET col1=col1+1 DONDE col2=2 , el resultado cambiará cada vez que se ejecute, no es idempotente .
insertar en el usuario (ID de usuario, nombre) valores (1, 'a') Si el ID de usuario no es la clave principal, se puede repetir. Si el negocio anterior se opera varias veces, se agregarán varios datos, lo cual no es idempotente .

4. Soluciones idempotentes

1. Mecanismo de fichas _

  1. El servidor proporciona una interfaz para enviar tokens . Cuando analizamos el negocio, qué negocios tienen problemas idempotentes, primero debemos obtener el token antes de ejecutar el negocio, y el servidor guardará el token para redis .
  2. Luego, cuando llame a la solicitud de la interfaz comercial, lleve el token , generalmente en el encabezado de la solicitud.
  3. El servidor determina si el token existe en redis , si existe , indica la primera solicitud , luego elimina el token y continúa ejecutando el negocio.
  4. Si se determina que el token no existe en redis , significa que la operación se repite y la marca repetida se devuelve directamente al cliente , lo que garantiza que el código comercial no se ejecutará repetidamente.

 A continuación, considere si eliminar el token primero o ejecutar el negocio primero después de enviar el pedido.

analizar:

Eliminar el token más tarde

①  La eliminación posterior puede hacer que el negocio se procese con éxito, pero el servicio se interrumpe, se agota el tiempo de espera y el token no se elimina . Otros continúan reintentando, lo que hace que el negocio se ejecute dos veces.

② Durante la ejecución del negocio, debido a que el token no se ha eliminado, si el cliente presiona dos veces para enviar la orden, esta se ejecutará dos veces.

Eliminar el token primero

① El formato del código es aproximadamente el siguiente

if(serverToken == redisToken) {

    del(serverToken)
    执行业务
}

② Si se considera el extremo, aún es posible que los clientes hagan clic continuamente para enviar órdenes y, al mismo tiempo, realicen múltiples juicios y ejecuten múltiples transacciones.

Pero es obvio que la segunda opción es menos probable y menos peligrosa, por lo que elegimos eliminar el token primero y luego considerar la optimización.

Podemos agrupar los tres pasos para determinar si el token existe en redis y eliminar el token en redis, es decir, la adquisición, comparación y eliminación del token deben ser atómicas.

(1) Si estas dos operaciones de redis.get(token) , token.equals y redis.del(token) no son atómicas, puede resultar en que, en condiciones de alta concurrencia, ambas obtengan los mismos datos y el juicio sea exitoso. y el negocio continúa ejecutándose al mismo tiempo.
(2) Puede usar el script lua en redis para completar esta operación
if 
    redis.call('get', KEYS[1]) == ARGV[1] 
then 
    return redis.call('del', KEYS[1]) 
else 
    return 0 
end

2. Varios mecanismos de bloqueo.

(1) Bloqueo pesimista de la base de datos

seleccione * de xxxx donde id = 1 para actualizar;
Los bloqueos pesimistas se utilizan generalmente junto con las transacciones. El tiempo de bloqueo de datos puede ser muy largo y debe seleccionarse de acuerdo con la situación real. Otra cosa a tener en cuenta es que el campo de identificación debe ser una clave principal o un índice único; de lo contrario, puede provocar el bloqueo de la tabla, lo que será muy problemático de solucionar.

(2) Bloqueo optimista de la base de datos

Este método es adecuado para escenarios más nuevos,
actualizar t_goods establecer recuento = recuento -1, versión = versión + 1 donde good_id = 2 y versión = 1
Según la versión , es decir, antes de operar el inventario, primero obtenga el número de versión del producto actual y luego traiga este número de versión cuando lo opere . Vamos a solucionarlo. Cuando operamos el inventario por primera vez, obtuvimos la versión 1 , y cuando llamamos al servicio de inventario, la versión se convirtió en 2 ; pero hubo un problema al regresar al servicio de pedidos . El servicio de pedidos inició otro llamada al servicio de inventario. Cuando el servicio de pedido pasó como La versión sigue siendo 1. Cuando la instrucción SQL anterior se ejecuta nuevamente, no se ejecutará; debido a que la versión ha cambiado a 2 , la condición donde no se cumplirá. Esto garantiza que no importa cuántas veces se llame, solo se procesará una vez. El bloqueo optimista se utiliza principalmente para solucionar el problema de más lecturas y menos escrituras.

(3) Bloqueo distribuido de capa empresarial

Si varias máquinas pueden procesar los mismos datos al mismo tiempo, por ejemplo, las tareas programadas de varias máquinas han recibido los mismos datos para su procesamiento, podemos agregar un bloqueo distribuido, bloquear los datos y liberar el bloqueo una vez que se completa el procesamiento. Para obtener el bloqueo, primero debe determinar si los datos han sido procesados.

3. Varias limitaciones únicas

(1) Restricciones únicas de la base de datos

Los datos deben insertarse según un índice único, como por ejemplo el número de pedido, es imposible insertar dos registros para el mismo pedido. Prevenimos la duplicación a nivel de base de datos. Este mecanismo aprovecha la característica de restricción única de clave principal de la base de datos y resuelve el problema idempotente en el escenario de inserción. Sin embargo, el requisito de clave primaria no es una clave primaria incrementada automáticamente, por lo que la empresa necesita generar una clave primaria única a nivel mundial. Si se trata de un escenario de subbase de datos y subtabla, las reglas de enrutamiento deben garantizar que la misma solicitud llegue a la misma base de datos y a la misma tabla. De lo contrario, la restricción de la clave primaria de la base de datos no tendrá ningún efecto porque las claves primarias de diferentes bases de datos y las tablas no están relacionadas.

(2) redis configurar anti-reparación

Es necesario procesar una gran cantidad de datos y solo se pueden procesar una vez. Por ejemplo, podemos calcular el MD5 de los datos y ponerlo en el conjunto de redis . Cada vez que se procesan los datos, primero verifique si el MD5 ya existe. Si existe, no será procesado.

4. Reloj antipesado

Utilice el número de pedido orderNo como índice único de la tabla de deduplicación, inserte el índice único en la tabla de deduplicación y luego realice operaciones comerciales, y estarán en la misma transacción. Esto garantiza que cuando se repita una solicitud, la solicitud fallará porque la tabla de deduplicación tiene una restricción única, evitando así problemas de idempotencia. Lo que se debe tener en cuenta aquí es que la tabla de deduplicación y la tabla de negocios deben estar en la misma base de datos, lo que garantiza que en la misma transacción, incluso si la operación comercial falla, los datos de la tabla de deduplicación se revertirán. Esto garantiza muy bien la coherencia de los datos.
La protección de Redis mencionada antes también cuenta.

5. Identificación única de solicitud global

Al llamar a la interfaz, se genera una ID única y Redis guarda los datos en la colección (deduplicación). Si existe, se ha procesado. Puede utilizar nginx para establecer una ID única para cada solicitud;
proxy_set_header X-Request-Id $request_id;

Caso: envío de pedido

① Llevar información relevante que el pedido debe enviarse en el código de la página.
    <form action="http://order.gulimall.com/submitOrder" method="post">
        <input type="hidden" name="addrId" id="addrIdInput"/>
        <input type="hidden" name="payPrice" id="payPriceInput"/>
        <input type="hidden" name="orderToken" th:value="${orderConfirmData.Token}"/>
        <button class="tijiao" type="submit">提交订单</button>
    </form>

② Valor de configuración

    function getFare(addrId) {
        $("#addrIdInput").val(addrId)
        $.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" + addrId, function (data) {
            console.log(data);
            $("#fare").text(data.data.fare)
            var total = [[${orderConfirmData.total}]]
            // 设置应付价格
            var payPrice = data.data.fare * 1 + total * 1
            $("#payPriceEle").text(payPrice)
            $("#payPriceInput").val(payPrice)
            $("#recieveAddressEle").text(data.data.address.provice + "" + data.data.address.detailAddress);
            $("#recieverEle").text(data.data.address.name)
        })
    }

③ Primero, garantice la unicidad del pedido en el nivel de la base de datos y agregue restricciones de unicidad al número de pedido.

④ Interfaz de pedidos de fondo

    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo) {
        // 下单,创建订单,校验令牌,检验价格,锁库存
        SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
        System.out.println("订单提价的数据。。。" + vo);

        if(responseVo.getCode() == 0) {
            // 成功显示支付页面
            return "pay";
        } else {
            // 失败返回订单确认页面
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }

⑤ Asegúrese de que la obtención de tokens de redis, la comparación y la eliminación de tokens sean atómicas.

    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        submitVoThreadLocal.set(vo);
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
        String orderToken = vo.getOrderToken();
        // 成功返回1  失败返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 保证原子性
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()), orderToken);
        if(result == 0L) {
            // 验证失败
            response.setCode(1);
            return response;
        } else {
            // 下单,创建订单,校验令牌,检验价格,锁库存
            // 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            if(Math.abs(payAmount.subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
                // 金额对比成功后保存订单
                saveOrder(order);
            } else {
                response.setCode(2); // 金额对比失败
                return response;
            }

        }

        // 逻辑如下,但是其实我们需要保证对比和删除是原子性的,因此使用redisTemplate的脚本
//        if(orderToken != null && orderToken.equals(redisToken) ) {
//            // 检验成功
//            redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
//        } else {
//            // 失败
//        }
        response.setCode(1);
        return response;
    }

Supongo que te gusta

Origin blog.csdn.net/m0_62946761/article/details/132767531
Recomendado
Clasificación