25.分布式幂等性设计

分布式幂等性设计

1. 基于本地消息的最终一致性方案设计

  • 采用BASE原理实现,保障事务的最终一致性
  • 设计的过程中是否采用最终一致性需要根据业务来进行评估
  • 基于本地消息的方式,将本事务外的操作,记录在消息表中
    • 举例:下订单-支付,这就是两个事务
  • 其他事务提供操作接口(支付成功后如果直接调用订单接口)
  • 定时轮询的方式将未执行的消息发送给操作接口
  • 操作接口返回失败记录失败的标识,需要设置retry次数
  • 超过retry的次数后不再进行消息发送并记录我们的失败状态
  • 重试后没有成功的就可以通过人工补偿

在这里插入图片描述

这个图例的理解:

  • 业务表/消息表:支付的业务和支付成功的通知
    • 支付相关信息记录到业务表
    • 将需要通知订单的消息记录到消息表中
    • 定时任务通过定时轮询消息表来获取哪个订单需要发送确认消息
    • 定时任务通过接口修改业务表状态
      • 如果成功更新消息表状态
      • 失败就不断轮询并记录轮询次数,超次数后标记并不再调用
  • 业务表:这就是订单信息
    • 记录订单业务数据
    • 支付成功后订单状态
  • 订单的支付状态在一段时间不一致,但最终一致

基于本地消息的特点

  • 优点:
    • 将事务拆分没有同时操作两个数据库,每一步之操作自己的数据库,保证事务完整性
    • 避免了分布式事务,实现最终一致性
  • 缺点:
    • 要注意重试时的幂等性操作

2. 基于本地消息的最终一致性代码实现

支付的消息表

CREATE TABLE `pay_msg` (
  `id` int(11) NOT NULL,
  `order_id` int(11) NOT NULL,
  `status` int(11) NOT NULL DEFAULT '0',
  `fail_count` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

支付的信息表

CREATE TABLE `user_account` (
  `id` int(11) NOT NULL,
  `username` varchar(255) NOT NULL,
  `account` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单信息表

CREATE TABLE `order_info` (
  `id` int(11) NOT NULL,
  `order_status` int(11) NOT NULL,
  `order_amount` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

POJO代码

package com.icodingedu.pojo;

import lombok.Data;

@Data
public class PayMsg {
    private int id;
    private int order_id;
    private int status;
    private int fail_count;
}
package com.icodingedu.pojo;

import lombok.Data;

@Data
public class UserAccount {
    private int id;
    private String username;
    private int account;
}

package com.icodingedu.pojo;

import lombok.Data;

@Data
public class OrderInfo {
    private int id;
    private int order_status;
    private int order_amount;
}

Mapper代码

package com.icodingedu.mapper.db195;

import com.icodingedu.pojo.PayMsg;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface PayMsgMapper {
    int insertPayMs(PayMsg payMsg);
    int updatePayMsg(PayMsg payMsg);
    List<PayMsg> queryNoSend();
    PayMsg queryForId(int order_id);
}
package com.icodingedu.mapper.db195;

import com.icodingedu.pojo.UserAccount;
import org.springframework.stereotype.Repository;

@Repository
public interface UserAccountMapper {
    int updateUserAccount(UserAccount userAccount);
    UserAccount queryForId(int id);
}
package com.icodingedu.mapper.db197;

import com.icodingedu.pojo.OrderInfo;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderInfoMapper {
    int updateOrderInfo(OrderInfo orderInfo);
    OrderInfo queryForId(int id);
}

xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.icodingedu.mapper.db195.PayMsgMapper">
    <insert id="insertPayMs" parameterType="com.icodingedu.pojo.PayMsg">
        insert into pay_msg(id,order_id,status,fail_count) values(#{id},#{order_id},#{status},#{fail_count})
    </insert>
    <update id="updatePayMsg" parameterType="com.icodingedu.pojo.PayMsg">
        update pay_msg set status=#{status},fail_count=#{fail_count} where id=#{id}
    </update>
    <select id="queryNoSend" resultType="com.icodingedu.pojo.PayMsg">
        select * from pay_msg where status=0
    </select>
    <select id="queryForId" resultType="com.icodingedu.pojo.PayMsg">
        select * from pay_msg where order_id=#{order_id}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.icodingedu.mapper.db195.UserAccountMapper">
    <update id="updateUserAccount" parameterType="com.icodingedu.pojo.UserAccount">
        update user_account set account=#{account} where id=#{id}
    </update>
    <select id="queryForId" resultType="com.icodingedu.pojo.UserAccount">
        select * from user_account where id=#{id}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.icodingedu.mapper.db197.OrderInfoMapper">
    <update id="updateOrderInfo" parameterType="com.icodingedu.pojo.OrderInfo">
        update order_info set order_status=#{order_status},order_amount=#{order_amount} where id=#{id}
    </update>
    <select id="queryForId" resultType="com.icodingedu.pojo.OrderInfo">
        select * from order_info where id=#{id}
    </select>
</mapper>

service

package com.icodingedu.service;

import com.icodingedu.mapper.db197.OrderInfoMapper;
import com.icodingedu.pojo.OrderInfo;
import com.mysql.cj.x.protobuf.MysqlxCrud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    @Autowired
    OrderInfoMapper orderInfoMapper;

    /**
     *
     * @param order_id
     * @return:0-更新成功,1-订单不存在
     */
    public int handleOrder(int order_id){
        OrderInfo orderInfo = orderInfoMapper.queryForId(order_id);
        if(orderInfo==null){
            return 1;
        }
        orderInfo.setOrder_status(1);
        orderInfoMapper.updateOrderInfo(orderInfo);
        return 0;
    }
}
package com.icodingedu.service;

import com.icodingedu.mapper.db195.PayMsgMapper;
import com.icodingedu.pojo.PayMsg;
import jdk.internal.dynalink.linker.LinkerServices;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderNotifyService {

    @Autowired
    PayMsgMapper payMsgMapper;

    @Scheduled(cron = "0/5 * * * * ?")
    public void orderNotify() throws Exception{
        System.out.println("进入cron************");
        List<PayMsg> payMsgList = payMsgMapper.queryNoSend();
        if(payMsgList==null||payMsgList.size()==0){
            return;
        }
        for (PayMsg payMsg: payMsgList) {
            int order_id = payMsg.getOrder_id();
            //http://localhost:8080/handleorder?id=2001
            CloseableHttpClient httpClient = HttpClientBuilder.create().build();
            HttpGet httpGet = new HttpGet("http://localhost:8080/handleorder?id="+order_id);
            CloseableHttpResponse httpResponse = httpClient.execute(httpGet);
            String response = EntityUtils.toString(httpResponse.getEntity());
            System.out.println("************调用结果:"+response);
            if("success".equals(response)){
                payMsg.setStatus(1);
            }else{
                int count = payMsg.getFail_count();
                payMsg.setFail_count(count+1);
                if(count+1>5){
                    payMsg.setStatus(2);
                }
            }
            payMsgMapper.updatePayMsg(payMsg);
        }
    }
}
package com.icodingedu.service;

import com.icodingedu.mapper.db195.PayMsgMapper;
import com.icodingedu.mapper.db195.UserAccountMapper;
import com.icodingedu.pojo.PayMsg;
import com.icodingedu.pojo.UserAccount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PaymentService {

    @Autowired
    UserAccountMapper userAccountMapper;

    @Autowired
    PayMsgMapper payMsgMapper;
    /**
     *
     * @param uid
     * @param order_id
     * @param amount
     * @return:0-成功,1-用户不存在,2-余额不足
     */
    @Transactional(transactionManager = "tm195")
    public int payment(int uid,int order_id,int amount){
        UserAccount userAccount = userAccountMapper.queryForId(uid);
        if(userAccount==null){
            return 1;
        }
        int account = userAccount.getAccount();
        if(account<amount){
            return 2;
        }
        userAccount.setAccount(account-amount);
        userAccountMapper.updateUserAccount(userAccount);

        PayMsg payMsg = new PayMsg();
        payMsg.setId(1001);
        payMsg.setOrder_id(order_id);
        payMsg.setStatus(0);//0-未发送,1-发送成功,2-超次数
        payMsg.setFail_count(0);
        payMsgMapper.insertPayMs(payMsg);
        return 0;
    }
}

controller

package com.icodingedu.controller;

import com.icodingedu.service.MsgSendService;
import com.icodingedu.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PaymentController {
    @Autowired
    PaymentService paymentService;

    @GetMapping("/payment")
    @ResponseBody
    public String payment(int uid,int orderid,int amount){
        int status = paymentService.payment(uid,orderid,amount);
        return "支付成功: "+status;
    }
}
package com.icodingedu.controller;

import com.icodingedu.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class OrderController {

    @Autowired
    OrderService orderService;

    @GetMapping("/handleorder")
    @ResponseBody
    public String orderSet(int id){
        try {
            int flag = orderService.handleOrder(id);
            if (flag == 0) {
                return "success";
            } else {
                return "fail";
            }
        }catch (Exception ex){
            ex.printStackTrace();
            return "fail";
        }
    }
}

Application

package com.icodingedu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class TccTransactionApplication {

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

}

3. 基于消息队列MQ的最终一致性方案设计

  • 原理和流程类似我们的本本地消息
  • 不同点
    • 本地消息表改为MQ
    • 定时任务的职责直接由MQ的消费者来担任了

在这里插入图片描述

基于MQ实现最终一致性的问题分析

  • 不依赖于定时任务的周期验证,基于MQ更高效、更可靠
  • 适合自己企业内部系统调用
  • 不同企业的系统之间无法基于MQ,本地消息更适合(第三方应用一般都是通过网络访问)

具体逻辑实现

  • 扣减余额后就发送订单更新消息通知给MQ
  • 消费端接收消息成功后更新订单状态并返回ACK给MQ
  • 如果消费端消费失败则NACK重回队列(回到队首)并记录消费失败次数,超过这个次数就ACK到MQ,然后人工在消息记录里进行补偿

在这里插入图片描述

4. 基于消息队列MQ的最终一致性代码实现

POM依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

配置发送和接收

spring:
  rabbitmq:
    host: 39.100.17.31
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    connection-timeout: 10000
    listener:
      simple:
        concurrency: 1
        max-concurrency: 1
        auto-startup: true
        prefetch: 1
        acknowledge-mode: manual

POJO

package com.icodingedu.pojo;

import lombok.Data;

@Data
public class ReceiveMsg {
    private int id;
    private int order_id;
    private int fail_count;
}

Mapper

package com.icodingedu.mapper.db197;

import com.icodingedu.pojo.ReceiveMsg;
import org.springframework.stereotype.Repository;

@Repository
public interface ReceiveMsgMapper {
    int insert(ReceiveMsg receiveMsg);
    int update(ReceiveMsg receiveMsg);
    ReceiveMsg queryForOrderId(int order_id);
}

xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.icodingedu.mapper.db197.ReceiveMsgMapper">
    <insert id="insert" parameterType="com.icodingedu.pojo.ReceiveMsg">
        insert into receive_msg(id,order_id,fail_count) values(#{id},#{order_id},#{fail_count})
    </insert>
    <update id="update" parameterType="com.icodingedu.pojo.ReceiveMsg">
        update receive_msg set fail_count=#{fail_count} where id=#{id}
    </update>
    <select id="queryForOrderId" resultType="com.icodingedu.pojo.ReceiveMsg">
        select * from receive_msg where order_id=#{order_id}
    </select>
</mapper>

controller

package com.icodingedu.controller;

import com.icodingedu.service.MsgSendService;
import com.icodingedu.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PaymentController {
    @Autowired
    PaymentService paymentService;

    @Autowired
    MsgSendService msgSendService;

    @GetMapping("/payment")
    @ResponseBody
    public String payment(int uid,int orderid,int amount){
        int status = paymentService.payment(uid,orderid,amount);
        msgSendService.sendMessage("msg1001",String.valueOf(orderid));
        return "支付成功: "+status;
    }
}

service

package com.icodingedu.service;

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MsgSendService {
    @Autowired
    RabbitTemplate rabbitTemplate;

    public void sendMessage(String mid,String msg){
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(mid);
        rabbitTemplate.convertAndSend("order-exchange","receive.info",msg,correlationData);
    }
}
package com.icodingedu.service;

import com.icodingedu.mapper.db197.ReceiveMsgMapper;
import com.icodingedu.pojo.ReceiveMsg;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class OrderReceiveService {

    @Autowired
    OrderService orderService;

    @Autowired
    ReceiveSendService receiveSendService;

    @Autowired
    ReceiveMsgMapper receiveMsgMapper;

    @RabbitListener(queues = "receive-queue")
    @RabbitHandler
    public void onOrderMessage(@Payload String orderid, @Headers Map<String,Object> headers, Channel channel) throws Exception{
        System.out.println("*********消息收到,开始消费*********");
        System.out.println("OrderID :"+orderid);
        Long deliverTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);

        try{
            //订单状态发生了改变
            int flag = orderService.handleOrder(Integer.valueOf(orderid));
            if(flag==0){
                channel.basicAck(deliverTag,false);
                //通知发送端我接收到消息了
                receiveSendService.sendMessage("send1001",orderid);
            }else{
                ReceiveMsg receiveMsg = receiveMsgMapper.queryForOrderId(Integer.valueOf(orderid));
                if(receiveMsg==null){
                    receiveMsg = new ReceiveMsg();
                    receiveMsg.setId(3001);
                    receiveMsg.setFail_count(1);
                    receiveMsg.setOrder_id(Integer.valueOf(orderid));
                    receiveMsgMapper.insert(receiveMsg);
                    channel.basicNack(deliverTag,false,true);
                }else{
                    int fail_count = receiveMsg.getFail_count();
                    if(fail_count>5){
                        channel.basicAck(deliverTag,false);
                    }else{
                        receiveMsg.setFail_count(fail_count+1);
                        receiveMsgMapper.update(receiveMsg);
                        channel.basicNack(deliverTag,false,true);
                    }
                }
            }
        }catch (Exception ex){
            ex.printStackTrace();
            ReceiveMsg receiveMsg = receiveMsgMapper.queryForOrderId(Integer.valueOf(orderid));
            if(receiveMsg==null){
                receiveMsg = new ReceiveMsg();
                receiveMsg.setId(3001);
                receiveMsg.setFail_count(1);
                receiveMsg.setOrder_id(Integer.valueOf(orderid));
                receiveMsgMapper.insert(receiveMsg);
                channel.basicNack(deliverTag,false,true);
            }else{
                int fail_count = receiveMsg.getFail_count();
                if(fail_count>5){
                    channel.basicAck(deliverTag,false);
                }else{
                    receiveMsg.setFail_count(fail_count+1);
                    receiveMsgMapper.update(receiveMsg);
                    channel.basicNack(deliverTag,false,true);
                }
            }
        }
    }
}
package com.icodingedu.service;

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ReceiveSendService {

    @Autowired
    RabbitTemplate rabbitTemplate;

    public void sendMessage(String mid,String msg){
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(mid);
        rabbitTemplate.convertAndSend("order-exchange","send.confirm",msg,correlationData);
    }
}
package com.icodingedu.service;

import com.icodingedu.mapper.db195.PayMsgMapper;
import com.icodingedu.pojo.PayMsg;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class SendReceiveService {

    @Autowired
    PayMsgMapper payMsgMapper;

    @RabbitListener(queues = "sendconfirm-queue")
    @RabbitHandler
    public void onOrderMessage(@Payload String orderid, @Headers Map<String,Object> headers, Channel channel) throws Exception{
        System.out.println("===========接收发送确认的消息============");
        System.out.println("=====orderid "+orderid);
        Long deliverTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
        PayMsg payMsg = payMsgMapper.queryForId(Integer.valueOf(orderid));
        if(payMsg!=null){
            payMsg.setStatus(1);
            payMsgMapper.updatePayMsg(payMsg);
            channel.basicAck(deliverTag,false);
        }
    }
}

5. 接口幂等性涉及的相关问题

经常遇到数据重复的问题

  • 表单录入如何防止重复提交
  • 微服务架构中,客户端重试如何防止重复提交

幂等性:f(f(x)) = f(x)

幂等元素运行多次,还等于它原来运算的结果

什么情况下需要幂等性

重复提交、接口重试、前端业务操作抖动

并不是所有的业务都需要幂等性,要根据实际业务确定是否需要幂等性

6. 保证幂等性的策略分析

保证幂等性的核心思想:通过一个唯一序号保证幂等(如果一个业务操作我们赋予它一个唯一业务id,如果这个业务操作内容一样,那么这个业务id就不变,这个时候有多个一模一样的业务id到后台,就可以进行判断了)

非并发情况下只需要查询这个业务单号是否有操作过,如果没有执行即可

并发情况整个过程用加锁来实现

7. 业务操作的幂等性分析

  • Select:查询操作不需要考虑幂等,对数据不会产生任何影响,天然幂等
  • Delete:删除操作,第一次修改成功后就不会有任何返回,后续啊不再执行了,也就不用考虑幂等了,但有一个问题,删除操作无论使用什么条件最后都转换到唯一id进行删除
  • Update:这个就需要考虑幂等
    • 如果更新你的工资,20k,30k,update table set salary=30 where id=1001,这就不需要幂等
    • 如果是自增方式:update table set salary+10 where id=1001,需要设计幂等
  • Insert:由于是新增操作没有唯一单号,就需要通过token的形式使用幂等
  • 混合操作:如果这一组业务有一个唯一单号,就可以使用这个单号进行锁操作,如果没有就需要增肌token进行幂等设计

8. 幂等性的具体设计分析

  • 修改操作的幂等设计

    • 修改数据前一定是先查询并获得数据了
    • 获得的数据里要加上版本号version、update_time
    • 修改的时候使用这个version作为条件,如果条件不符更新肯定不成功
    • 更新同时变更version
    • 实际上就是使用了乐观锁和update行锁实现幂等
  • insert操作的幂等性设计

    • 根据唯一单号进行设计:比如限购,一个用于只能购买一个(uid+pid)给这个组合设置唯一索引
    • 没有唯一单号:用户提交数据或form表单事重复提交导致的数据重复录入
      • 通过token机制来解决
      • 在用户打开表单的同时生成一个唯一序列的token,提交数据时将token传递给后台
      • 对这个token进行加锁控制,未获得锁的操作全部结束业务
      • 为了保证其他重复提交不获得锁,可以不手动释放锁,待其自动超时释放
  • 混合操作方式
    Delete:删除操作,第一次修改成功后就不会有任何返回,后续啊不再执行了,也就不用考虑幂等了,但有一个问题,删除操作无论使用什么条件最后都转换到唯一id进行删除

  • Update:这个就需要考虑幂等

    • 如果更新你的工资,20k,30k,update table set salary=30 where id=1001,这就不需要幂等
    • 如果是自增方式:update table set salary+10 where id=1001,需要设计幂等
  • Insert:由于是新增操作没有唯一单号,就需要通过token的形式使用幂等

  • 混合操作:如果这一组业务有一个唯一单号,就可以使用这个单号进行锁操作,如果没有就需要增肌token进行幂等设计

8. 幂等性的具体设计分析

  • 修改操作的幂等设计

    • 修改数据前一定是先查询并获得数据了
    • 获得的数据里要加上版本号version、update_time
    • 修改的时候使用这个version作为条件,如果条件不符更新肯定不成功
    • 更新同时变更version
    • 实际上就是使用了乐观锁和update行锁实现幂等
  • insert操作的幂等性设计

    • 根据唯一单号进行设计:比如限购,一个用于只能购买一个(uid+pid)给这个组合设置唯一索引
    • 没有唯一单号:用户提交数据或form表单事重复提交导致的数据重复录入
      • 通过token机制来解决
      • 在用户打开表单的同时生成一个唯一序列的token,提交数据时将token传递给后台
      • 对这个token进行加锁控制,未获得锁的操作全部结束业务
      • 为了保证其他重复提交不获得锁,可以不手动释放锁,待其自动超时释放
  • 混合操作方式

    • 一整套混合业务操作可以统一使用token机制来加锁进行重复提交限制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值