什么是接口幂等性
接口幂等性,简单来说就是指一个接口调用一次和调用N次的效果是一样的,不会产生其他的副作用。
注意:
这里的效果一样和返回结果一样的区别,比如我们都知道查询接口具有天然的幂等性,但是多次调用查询接口的过程中,如果有其他操作对查询的数据进行了新增、修改、删除操作,那么查询接口的返回结果就会不一直,但是这并不能说明该查询接口不具有幂等性。
场景说明
典型场景,对指定订单发起一笔付款交易,无论交易接口调用一次还是N次,都只能扣用户账户一次钱。
一般最简单的幂等处理就是通过订单状态来进行控制,伪代码如下:
begin: 根据订单号查询订单状态; if(待支付){ 扣款操作; 更新订单状态为已支付; } end;
代码实战
1、订单表说明
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');
2、新建Order订单工程,实现订单支付接口
/** * @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 { //1、查询订单 Order order = new Order(); order.setId(Long.parseLong(orderId)); order = orderMapper.selectByPrimaryKey(order); //为模拟并发,暂定100ms Thread.sleep(100); //2、根据订单状态判断,是否进行支付 if(order.getStatus().equals(0)){ log.info("进行支付,并更新订单状态"); order.setStatus(1); orderMapper.updateByPrimaryKeySelective(order); return "支付完成"; }else{ log.info("已支付"); return "已支付"; } } }
说明:
这个应该是很多人的常见代码,先查询订单状态,判断订单状态是否是未支付状态,是则进行支付。
3、采用JMeter模拟并发支付
模拟每秒50个并发,执行结果为:
可以发现,在高并发的情况下,出现了大量重复支付的情况。
4、优化:通过for update添加悲观锁
4.1、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.2、OrderMapper.xml中添加selectForUpdate
public interface OrderMapper extends BaseMapper<Order> { Order selectForUpdate(Long orderId); }
4.3、修改支付订单方法
/** * 订单支付接口 * @param orderId * @return * @throws InterruptedException */ @Transactional(rollbackFor = Exception.class) @Override public String payOrder(String orderId) throws InterruptedException { //1、查询订单 Order order = new Order(); order.setId(Long.parseLong(orderId)); //通过 order = orderMapper.selectForUpdate(Long.parseLong(orderId)); //为模拟并发,暂定100ms Thread.sleep(100); //2、根据订单状态判断,是否进行支付 if(order.getStatus().equals(0)){ log.info("进行支付,并更新订单状态"); order.setStatus(1); orderMapper.updateByPrimaryKeySelective(order); return "支付完成"; }else{ log.info("已支付"); return "已支付"; } }
说明:
for update一定要配合事务使用,不然执行完查询语句后,会自动释放锁。
给方法添加 @Transactional事务注解后,只有方法执行完毕才会自动释放锁。
4.4、再次使用JMeter压测
订单支付接口在高并发多次调用的情况下,仍然只支付了一次,接口的幂等性得到保证。
总结:
1、不能简单的通过订单状态来控制接口的幂等性,高并发情况下,多个线程同时查到未支付状态的订单,就容易出现重复支付的情况。
2、通过for update对订单记录加上排他锁,使的该订单记录不能被其他线程查询到,也不能进行其他修改操作。只有当事务提交,释放锁后,其他操作该订单的接口才能执行。
3、一定要给订单号加上索引,避免锁表。(Innodb引擎中,查询和更新操作如果不走索引,就会进行全表锁定。本例中由于订单号是主键,所以不用加)。
4、@Transactional注解不但能控制一个方法中的多个数据修改操作的原子性,也能控制锁的释放。比如本例中,尽管只有一个修改操作,但是一定要添加@Transactional注解让方法执行完后自动释放锁。
当然实现接口幂等性还有很多其他的方法,这里只是说明一个最典型的错误场景,并通过简单的添加悲观锁,实现接口的幂等性控制。