架构设计 | 接口幂等性原则,防重复提交Token管理-阿里云开发者社区

开发者社区> 知了一笑> 正文

架构设计 | 接口幂等性原则,防重复提交Token管理

简介:
+关注继续查看

一、幂等性概念

1、幂等简介

编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。就是说,一次和多次请求某一个资源会产生同样的作用影响。

2、HTTP请求

遵循Http协议的请求,越来越强调Rest请求风格,可以更好的规范和理解接口的设计。

GET:用于获取资源,不应有副作用,所以是幂等的;

POST:用于创建资源,重复提交POST请求可能产生两个不同的资源,有副作用不满足幂等性;

PUT:用于更新操作,重复提交PUT请求只会对其URL中指定的资源有副作用,满足幂等性;

DELETE:用于删除资源,有副作用,但它应该满足幂等性;

HEAD:和GET本质是一样的,但HEAD不含有呈现数据,仅是HTTP头信息,没有副作用,满足幂等性;

OPTIONS:用于获取当前URL所支持的请求方法,满足幂等性;

二、场景业务分析

1、订单支付

03_1

实际开发中,经常会面对订单支付问题,基本流程如下:

  • 客户端发起订单支付请求 ;
  • 支付前系统本地相关业务处理 ;
  • 请求第三方支付服务执行扣款;
  • 第三方支付返回处理结果;
  • 本地服务基于支付结果响应客户端;

该业务流程中要处理相当复杂的问题,比如事务,分布式事务,接口延迟超时,客户端重复提交等等,这里只基于幂等接口角度来看该流程,其他问题后续再聊。

2、幂等接口

当上述流程的支付请求有明确结果的时候:失败或成功,这样业务流程都好处理,但是例如支付场景如果请求超时,如何判断服务的结果状态:客户端请求超时,本地服务超时,请求支付超时,支付回调超时,客户端响应超时等等。

这就需要设计流程化的状态管理。

3、基础操作案例

模拟管理上述流程,设计幂等接口:

表结构设计

CREATE TABLE `dp_order_state` (
    `order_id` BIGINT (20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
    `token_id` VARCHAR (50) DEFAULT NULL COMMENT '防重复提交',
    `state` INT (1) DEFAULT '1' COMMENT '1创建订单,2本地业务,3支付业务',
    PRIMARY KEY (`order_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '订单状态表';

CREATE TABLE `dp_state_record` (
    `id` INT (11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `order_id` BIGINT (20) NOT NULL COMMENT '订单id',
    `state_dec` VARCHAR (50) DEFAULT NULL COMMENT '状态描述',
    PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '状态记录表';

模拟业务流程

将订单创建,本地业务,支付业务,分开分段管理提交。分阶段测试异常熔断的业务。

@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderStateMapper orderStateMapper ;
    @Resource
    private StateRecordMapper stateRecordMapper ;

    @Override
    public OrderState queryOrder(OrderState orderState) {
        Map<String,Object> paramMap = new HashMap<>() ;
        paramMap.put("order_id",orderState.getOrderId());
        List<OrderState> orderStateList = orderStateMapper.selectByMap(paramMap);
        if (orderStateList != null && orderStateList.size()>0){
            return orderStateList.get(0) ;
        }
        return null ;
    }

    @Override
    public boolean createOrder(OrderState orderState) {
        int saveRes = orderStateMapper.insert(orderState);
        if (saveRes > 0){
            saveStateRecord(orderState.getOrderId(),"订单创建成功");
        }
        return saveRes > 0 ;
    }

    @Override
    public boolean localBiz(OrderState orderState) {
        orderState.setState(2);
        int updateRes = orderStateMapper.updateState(orderState) ;
        if (updateRes > 0){
            saveStateRecord(orderState.getOrderId(),"本地业务成功");
        }
        return updateRes > 0;
    }

    @Override
    public boolean paymentBiz(OrderState orderState) {
        orderState.setState(3);
        int updateRes = orderStateMapper.updateState(orderState) ;
        if (updateRes > 0){
            saveStateRecord(orderState.getOrderId(),"支付业务成功");
        }
        return updateRes > 0;
    }

    private void saveStateRecord (Long orderId,String stateDec){
        StateRecord stateRecord = new StateRecord() ;
        stateRecord.setOrderId(orderId);
        stateRecord.setStateDec(stateDec);
        stateRecordMapper.insert(stateRecord) ;
    }
}

测试接口

根据订单状态,分段补偿执行未完成的业务,如果该订单已经完成,多次提交不影响最终结果。

@Api(value = "OrderController")
@RestController
public class OrderController {

    @Resource
    private OrderService orderService ;

    @PostMapping("/submitOrder")
    public String submitOrder (OrderState orderState){
        OrderState orderState01 = orderService.queryOrder(orderState) ;
        if (orderState01 == null){
            // 正常业务流程
            orderService.createOrder(orderState) ;
            orderService.localBiz(orderState) ;
            orderService.paymentBiz(orderState) ;
        } else {
            switch (orderState01.getState()){
                case 1:
                    // 订单创建成功:后推执行本地和支付业务
                    orderService.localBiz(orderState01) ;
                    orderService.paymentBiz(orderState01) ;
                    break ;
                case 2:
                    // 订单本地业务成功:后推执行支付业务
                    orderService.paymentBiz(orderState01) ;
                    break ;
                default:
                    break ;
            }
        }
        return "success" ;
    }
}

絮叨一句:实际开发中,该流程是不会由页面多次提交完成,订单是不能重复提交的,下面会演示如何控制,这里业务是执行后推到完成,也可能业务向前清理,把整个流程置为失败,这里涉及关键状态判断,要选取一个状态作为成功或失败的标识,判断后续操作流程。在分布式系统中这种复杂流程最难处理的是分布式事务,最终一致性问题,后续再聊。

三、接口重复提交

1、表单重复提交

在实际情况中,接口如果处理时间过长,用户可能会点击多次提交按钮,导致数据重复。

常见的一个解决方案:在表单提交中隐藏一个token_id参数,一起提交到接口服务中,数据库存储订单和关联的tokenId,如果多次提交,直接返回页面提示信息即可。

2、演示案例

订单关联Token查询

@Service
public class OrderServiceImpl implements OrderService {
    @Override
    public Boolean queryToken(OrderState orderState) {
        Map<String,Object> paramMap = new HashMap<>() ;
        paramMap.put("order_id",orderState.getOrderId());
        paramMap.put("token_id",orderState.getTokenId());
        List<OrderState> orderStateList = orderStateMapper.selectByMap(paramMap);
        return orderStateList.size() > 0 ;
    }
}

测试接口

@RestController
public class OrderController {
    @Resource
    private OrderService orderService ;

    @PostMapping("/repeatSub")
    public String repeatSub (OrderState orderState){
        boolean flag = orderService.queryToken(orderState) ;
        if (flag){
            return "请勿重复提交订单" ;
        }
        return "success" ;
    }
}

四、源代码地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
面象对象设计6大原则之四:接口隔离原则
接口隔离原则(ISP),The Interface Segregation Principle 定义 客户端不需要强迫依赖那些它们不需要的接口。
783 0
安全管理最佳实践系列:给ECS实例配置一个RAM角色身份(使用动态STS-Token访问云服务API)
如果你的应用程序部署在ECS实例中,你可能会苦恼于应用程序的AK配置安全问题及管理难的问题。为此阿里云提供了给ECS实例配置RAM角色的解决方案,使得运行ECS实例中的应用程序将不再需要配置AK,而只需要从ECS Metadata服务中获取StsToken就可以访问阿里云服务API,让你不再担心应用程序的AK配置安全问题和难管理的问题。
6693 0
接口设计的核心原则:功能抽象
1,抽象就是概念化的过程,明确功能/数据的外延(适用范围,对象)和内涵(属性,特征) “外延就是这个集合的全部元素,而内涵就是这个集合全部元素共有的属性。” 举个例子:flash_read(int chip, int addr, int len, char *data) chip决定了接口的使用范围,每个id代表一个约定的访问对象。
590 0
《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——2.10 初始化缓冲区管理结构
本节书摘来自华章计算机《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》一书中的第2章,第2.10节,作者:新设计团队著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1128 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10547 0
架构设计 | 分布式系统调度,Zookeeper集群化管理
一、框架简介 1、基础简介 Zookeeper基于观察者模式设计的组件,主要应用于分布式系统架构中的,统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等场景。 Linux下Zookeeper单节点安装 SpringBoot整合Zookeeper中间件 2、集群选举 Zookeeper集群基于半数机制,集群中半数以上机器存活,集群处于可用状态。
739 0
+关注
知了一笑
公众号:知了一笑
198
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载