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

一、幂等性概念

1、幂等简介

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

2、HTTP请求

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

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

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

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

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

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

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

二、场景业务分析

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" ;

    }

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342