接口幂等性

    技术2022-07-11  127

    定义

    幂等性: f(f(x)) = f(x) 幂等元素运行多次,还等于它原来的运算结果 在系统中,一个接口运行多次,与运行一次的效果是一致的

    适合场景 

    重复提交、接口重试、前端操作抖动等业务场景:用户多次点击提交订单,后台应只生成一个订单业务场景:支付时,由于网络问题重发,应该只扣一次钱并不是所有的接口都要求幂等性,要根据业务而定,主要是支付接口和下单接口  

    核心思想

    通过唯一 的业务单号保证幂等非并发情况下,查询业务单号有没有操作过,没有则执行操作并发的情况下,整个操作过程加锁Select操作:不会对业务数据有影响,天然幂等Delete操作:第一次已经删除, 第二次也不会有影响Update操作:更新操作传入数据版本号,通过乐观锁实现幂等性Insert操作:此时没有唯一-业务单号, 使用Token保证幂等混合操作:找到操作的唯一业务单号,有则可使用分布式锁,没有可以通过Token保证幂等

     

    Delete操作的幂等性

         根据唯一业务号去删除(也是天然幂等性)

    第一次删除时,已将数据删除第二次再次执行时,由于找不到记录,所以返回的结果是0,对业务数据没有影响。可在删除前进行数据的查询。

          删除操作没有唯一业务号, 则要看具体的业务需求

     例如:删除所有审核未通过的商品。 执行第二次删除操作,新的未审核通过的商品要不要删除? 根据业务需求而定

    Update操作的幂等性

         根据唯一业务号去更新数据的情况

    用户查询出要修改的数据,系统将数据返回页面,将数据版本号放入隐藏域用户修改数据,点击提交,将版本号- -同提交给后台后台使用版本号作为更新条件update set version =version+ 1 ,XXx= ${xxx} where id=xXxand version = ${version}

          更新操作没有唯一业务号,可使用Token机制

    Insert操作的幂等性

    有唯一业务号的Insert操作,例如:秒杀,商品ID+ 用户ID

    可通过分布式锁,保证接口幂等业务执行完成后,不进行锁释放,让其过期自动释放下面的注册以用户的名字作为唯一业务号 @Configuration public class ZkConfig { @Bean(initMethod="start",destroyMethod = "close") public CuratorFramework getCuratorFramework() { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy); return client; } } @Autowired private CuratorFramework zkClient; public int insertUser(User user) throws Exception { InterProcessMutex lock = new InterProcessMutex(zkClient, "/"+user.getUsername()); boolean isLock = lock.acquire(30, TimeUnit.SECONDS); if (isLock){ return userMapper.insertSelective(user); } return 0;

     

    没有唯一业务号的Insert操作,比如:收货地址

    使用Token机制,保证幂等性进入到注册页时,后台统一生成Token,返回前台隐藏域中使用Token获取分布式锁,完成Insert操作执行成功后,不释放锁,等待过期自动释放 public int insertUser(User user, String token) throws Exception { InterProcessMutex lock = new InterProcessMutex(zkClient, "/"+token); boolean isLock = lock.acquire(30, TimeUnit.SECONDS); if (isLock){ return userMapper.insertSelective(user); } return 0; }

     

     

    项目支付接口的使用 

    1.生成Token返回给前端

    @ApiOperation(value = "获取订单token", notes = "获取订单token", httpMethod = "POST") @PostMapping("/getOrderToken") public IMOOCJSONResult getOrderToken(HttpSession session){ String token = UUID.randomUUID().toString(); redisOperator.set("Order TOKEN"+session.getId(),token,30); return IMOOCJSONResult.ok(token); }

    2.订单的BO中加入redis属性

    3.用户下单:注意两点①第一个次用户进入订单逻辑后,删除redis,②加入分布式锁防止多个用户获得token

    public IMOOCJSONResult create(@RequestBody SubmitOrderBO submitOrderBO, HttpServletRequest request, HttpServletResponse response) { String orderTokenKey = "ORDER_TOKEN" + request.getSession().getId(); String lockKey = "LOCK_KEY"+request.getSession().getId(); RLock lock = redissonClient.getLock(lockKey); lock.lock(5, TimeUnit.SECONDS); try { String orderToken = redisOperator.get(orderTokenKey); if (StringUtils.isBlank(orderToken)) { throw new RuntimeException("orderToken不存在"); } boolean equals = orderToken.equals(submitOrderBO.getToken()); if (!equals) { throw new RuntimeException("orderTokenwrong"); } redisOperator.del(orderToken); }finally { lock.unlock(); }

    4.前端加载订单页面时,添加获取后端提取后端token的方法  

     

     

    Processed: 0.011, SQL: 9