1、幂等性概念
一、幂等简介
编程中一个幂等操做的特色是其任意屡次执行所产生的影响均与一次执行的影响相同。就是说,一次和屡次请求某一个资源会产生一样的做用影响。java
二、HTTP请求
遵循Http协议的请求,愈来愈强调Rest请求风格,能够更好的规范和理解接口的设计。git
GET:用于获取资源,不该有反作用,因此是幂等的;github
POST:用于建立资源,重复提交POST请求可能产生两个不一样的资源,有反作用不知足幂等性;sql
PUT:用于更新操做,重复提交PUT请求只会对其URL中指定的资源有反作用,知足幂等性;数据库
DELETE:用于删除资源,有反作用,但它应该知足幂等性;编程
HEAD:和GET本质是同样的,但HEAD不含有呈现数据,仅是HTTP头信息,没有反作用,知足幂等性;app
OPTIONS:用于获取当前URL所支持的请求方法,知足幂等性;分布式
2、场景业务分析
一、订单支付
实际开发中,常常会面对订单支付问题,基本流程以下:ide
- 客户端发起订单支付请求 ;
- 支付前系统本地相关业务处理 ;
- 请求第三方支付服务执行扣款;
- 第三方支付返回处理结果;
- 本地服务基于支付结果响应客户端;
该业务流程中要处理至关复杂的问题,好比事务,分布式事务,接口延迟超时,客户端重复提交等等,这里只基于幂等接口角度来看该流程,其余问题后续再聊。测试
二、幂等接口
当上述流程的支付请求有明确结果的时候:失败或成功,这样业务流程都好处理,可是例如支付场景若是请求超时,如何判断服务的结果状态:客户端请求超时,本地服务超时,请求支付超时,支付回调超时,客户端响应超时等等。
这就须要设计流程化的状态管理。
三、基础操做案例
模拟管理上述流程,设计幂等接口:
表结构设计
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" ; } }
絮叨一句
:实际开发中,该流程是不会由页面屡次提交完成,订单是不能重复提交的,下面会演示如何控制,这里业务是执行后推到完成,也可能业务向前清理,把整个流程置为失败,这里涉及关键状态判断,要选取一个状态做为成功或失败的标识,判断后续操做流程。在分布式系统中这种复杂流程最难处理的是分布式事务,最终一致性问题,后续再聊。
3、接口重复提交
一、表单重复提交
在实际状况中,接口若是处理时间过长,用户可能会点击屡次提交按钮,致使数据重复。
常见的一个解决方案:在表单提交中隐藏一个token_id参数,一块儿提交到接口服务中,数据库存储订单和关联的tokenId,若是屡次提交,直接返回页面提示信息便可。
二、演示案例
订单关联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" ; } }
4、源代码地址
GitHub·地址 https://github.com/cicadasmile/data-manage-parent GitEE·地址 https://gitee.com/cicadasmile/data-manage-parent