什么是接口幂等性
接口幂等性,简单来讲就是指一个接口调用一次和调用N次的效果是同样的,不会产生其余的反作用。
注意:
这里的效果同样和返回结果同样的区别,好比咱们都知道查询接口具备自然的幂等性,可是屡次调用查询接口的过程当中,若是有其余操做对查询的数据进行了新增、修改、删除操做,那么查询接口的返回结果就会不一直,可是这并不能说明该查询接口不具备幂等性。java
场景说明
典型场景,对指定订单发起一笔付款交易,不管交易接口调用一次仍是N次,都只能扣用户帐户一次钱。
通常最简单的幂等处理就是经过订单状态来进行控制,伪代码以下:web
begin:
根据订单号查询订单状态;
if(待支付){
扣款操做;
更新订单状态为已支付;
}
end;
代码实战
一、订单表说明微信
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`goods_code` varchar(255) DEFAULT NULL COMMENT '商品编码',
`goods_num` int(10) DEFAULT NULL COMMENT '商品数量',
`money` decimal(20,0) DEFAULT NULL COMMENT '总金额',
`status` tinyint(1) DEFAULT NULL COMMENT '订单状态(0未支付,1支付)',
`account` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '帐户',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
添加一条测试订单数据:订单id是1,状态0的未支付订单。并发
INSERT INTO `order`.`tb_order`(`id`, `goods_code`, `goods_num`, `money`, `status`, `account`) VALUES (1, 'wsj0001', 5, 3000, 0, 'laowan');
二、新建Order订单工程,实现订单支付接口app
/**
* @program: order
* @description: 订单接口实现
* @author: wanli
* @create: 2020-09-21 16:11
**/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
OrderMapper orderMapper;
/**
* 订单支付接口
* @param orderId
* @return
* @throws InterruptedException
*/
@Override
public String payOrder(String orderId) throws InterruptedException {
//一、查询订单
Order order = new Order();
order.setId(Long.parseLong(orderId));
order = orderMapper.selectByPrimaryKey(order);
//为模拟并发,暂定100ms
Thread.sleep(100);
//二、根据订单状态判断,是否进行支付
if(order.getStatus().equals(0)){
log.info("进行支付,并更新订单状态");
order.setStatus(1);
orderMapper.updateByPrimaryKeySelective(order);
return "支付完成";
}else{
log.info("已支付");
return "已支付";
}
}
}
说明:
这个应该是不少人的常见代码,先查询订单状态,判断订单状态是不是未支付状态,是则进行支付。ide
三、采用JMeter模拟并发支付
高并发


模拟每秒50个并发,执行结果为:

能够发现,在高并发的状况下,出现了大量重复支付的状况。
四、优化:经过for update添加悲观锁
4.一、OrderMapper.xml中添加selectForUpdate测试
<select id="selectForUpdate" resultType="com.laowan.order.model.Order">
select * from tb_order t where t.id = #{orderId} for update
</select>
4.二、OrderMapper.xml中添加selectForUpdate优化
public interface OrderMapper extends BaseMapper<Order> {
Order selectForUpdate(Long orderId);
}
4.三、修改支付订单方法ui
/**
* 订单支付接口
* @param orderId
* @return
* @throws InterruptedException
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String payOrder(String orderId) throws InterruptedException {
//一、查询订单
Order order = new Order();
order.setId(Long.parseLong(orderId));
//经过
order = orderMapper.selectForUpdate(Long.parseLong(orderId));
//为模拟并发,暂定100ms
Thread.sleep(100);
//二、根据订单状态判断,是否进行支付
if(order.getStatus().equals(0)){
log.info("进行支付,并更新订单状态");
order.setStatus(1);
orderMapper.updateByPrimaryKeySelective(order);
return "支付完成";
}else{
log.info("已支付");
return "已支付";
}
}
说明:
for update必定要配合事务使用,否则执行完查询语句后,会自动释放锁。
给方法添加 @Transactional事务注解后,只有方法执行完毕才会自动释放锁。
4.四、再次使用JMeter压测

订单支付接口在高并发屡次调用的状况下,仍然只支付了一次,接口的幂等性获得保证。
总结:
一、不能简单的经过订单状态来控制接口的幂等性,高并发状况下,多个线程同时查到未支付状态的订单,就容易出现重复支付的状况。
二、经过for update对订单记录加上排他锁,使的该订单记录不能被其余线程查询到,也不能进行其余修改操做。只有当事务提交,释放锁后,该订单才能被其余线程操做,从而保证接口的幂等性。
三、必定要给订单号加上索引,让查询和修改操做只添加行锁,避免锁表。(Innodb引擎中,查询和更新操做若是不走索引,就会进行全表锁定。本例中因为订单号是主键,因此不用加)。
四、@Transactional注解不但能控制一个方法中的多个数据修改操做的原子性,也能控制锁的释放。好比本例中,尽管只有一个修改操做,可是必定要添加@Transactional注解让方法执行完后自动释放锁。
固然实现接口幂等性还有不少其余的方法,这里只是说明一个最典型的错误场景,并经过简单的添加悲观锁,实现接口的幂等性控制。
更多精彩,关注我吧。

本文分享自微信公众号 - 跟着老万学java(douzhe_2019)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。