本文源码: GitHub·点这里 || GitEE·点这里
编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。就是说,一次和多次请求某一个资源会产生同样的作用影响。
遵循Http协议的请求,越来越强调Rest请求风格,可以更好的规范和理解接口的设计。
GET:用于获取资源,不应有副作用,所以是幂等的;
POST:用于创建资源,重复提交POST请求可能产生两个不同的资源,有副作用不满足幂等性;
PUT:用于更新操作,重复提交PUT请求只会对其URL中指定的资源有副作用,满足幂等性;
DELETE:用于删除资源,有副作用,但它应该满足幂等性;
HEAD:和GET本质是一样的,但HEAD不含有呈现数据,仅是HTTP头信息,没有副作用,满足幂等性;
OPTIONS:用于获取当前URL所支持的请求方法,满足幂等性;
实际开发中,经常会面对订单支付问题,基本流程如下:
该业务流程中要处理相当复杂的问题,比如事务,分布式事务,接口延迟超时,客户端重复提交等等,这里只基于幂等接口角度来看该流程,其他问题后续再聊。
当上述流程的支付请求有明确结果的时候:失败或成功,这样业务流程都好处理,但是例如支付场景如果请求超时,如何判断服务的结果状态:客户端请求超时,本地服务超时,请求支付超时,支付回调超时,客户端响应超时等等。
这就需要设计流程化的状态管理。
模拟管理上述流程,设计幂等接口:
表结构设计
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" ;
}
}
絮叨一句
:实际开发中,该流程是不会由页面多次提交完成,订单是不能重复提交的,下面会演示如何控制,这里业务是执行后推到完成,也可能业务向前清理,把整个流程置为失败,这里涉及关键状态判断,要选取一个状态作为成功或失败的标识,判断后续操作流程。在分布式系统中这种复杂流程最难处理的是分布式事务,最终一致性问题,后续再聊。
在实际情况中,接口如果处理时间过长,用户可能会点击多次提交按钮,导致数据重复。
常见的一个解决方案:在表单提交中隐藏一个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" ;
}
}
GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。