接口幂等性设计

幂等性: 对于同一个操作发起一次请求或者多次请求,得到的结果都是一样的,不会因为请求多次而出现异常现象。

场景:

  • 用户多次请求,比如重复点击页面上的按钮
  • 网络异常,右移网络原因导致在一定时间内未返回调用成功的信息,触发了框架层的重试机制
  • 页面回退都再次提交的动作
  • 程序上的重试机制--对未及时响应的请求发起重试操作

Restful 请求方式的幂等性:

  • POST : 相当于新增,不具备幂等性
  • GET : 对资源的获取。在浏览器中通过地址进行访问,每次结果都是一样的,天然幂等
  • PUT : 将一个资源替换成另一个资源。这是非计算型的更新,无论更新多少次,结果都是一样的,天然幂等
  • DELETE : 无论删除多少次,都是一样的,是天然幂等

如何避免重复提交

1.利用全局唯一ID防止重复提交 

在向数据库新增一条记录时,有时会出现错误信息“result in duplicate entry for key primary”,原因是插入了相同的ID信息。

利用数据库的主键唯一特性,可以解决重复提交问题

流程:

  1. 搭建一个生成全局唯一ID的服务,可以参考雪花算法SnowFlow进行搭建
  2. 在订单确定页面中,调用全局唯一ID服务生成订单号
  3. 提交订单时带上订单号,请求到达订单订单系统的下单接口
  4. 订单系统在创建订单信息时,订单号使用前端传过来的订单号,然后直接将订单信息插入数据库
  5. 如果订单写入成功,则是第一次提交,返回下单成功;如果报ID冲突信息,则是重复提交。

 2.利用“Token+Redis”机制防止重复提交

流程:

  1. 订单系统提供一个发放Token的接口。这个Token是一个防重令牌,即一串唯一字符串(可以使用uuid)
  2. 在“订单确认页”中调用获取Token的接口,该接口向订单确认页返回Token,同时将Token写入Redis缓存中,并依据实际业务对其设置一定的有效期
  3. 用户在“订单确认页”中点击“提交订单”按钮时,将第2步Token以参数或者请求头的形式封装进订单信息,然后请求订单系统的下单接口
  4. 下单接口在收到提交下单的请求后,首先判断在Redis中是否存在当前传入的Token
  • 如果存在,则表示这是第1次请求,会删除这个Token,继续创建订单的其他业务
  • 如果不存在,则表示这不是第1此请求,而是重复的请求,会终止后面的业务逻辑

代码实现

生成Token

package com.wxclient.controller;


import com.fasterxml.uuid.Generators;
import com.yl.entitys.RespEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/system/idempotence")
public class IdempotenceController {

    @Autowired
    private RedisTemplate redisTemplate;

    public static final String USER_TOKEN_PREFIX = "idempotence:token:";
    /**
     * 参数token , 放入redis set数据结构,防止重复,返回给前端,做接口幂等性
     */
    @GetMapping("/{userId}")
    public RespEntity idempotence(@PathVariable Integer userId) {
        // 基于时间的UUID(全球唯一)
        UUID uuid = Generators.timeBasedGenerator().generate();
        // 将token 放入redis set中 ,5分钟的过期时间
        redisTemplate.opsForValue().set(USER_TOKEN_PREFIX+userId, uuid,5, TimeUnit.MINUTES);
        log.debug("【系统日志】产生的TOKEN->{}", uuid);
        return RespEntity.okData(uuid);
    }

}

 业务检验

	 /**
     * 添加公告信息
     */
    @ApiOperation(value = "添加公告信息")
    @PostMapping("/")
    public RespEntity add(@RequestBody YlNotice notice, HttpServletRequest httpServletRequest) {
        log.debug("【系统日志】添加公告信息---》");
        // 验证幂等性的标识
        String idempotence = httpServletRequest.getHeader("idempotence");
        // 用户的JWT信息
        String jwt = httpServletRequest.getHeader("jwt");

        JWT token = JWTUtil.parseToken(jwt);
        Integer userId = (Integer) token.getPayload("id");
        // 获取用户ID
        log.debug("【系统日志】用户:{}->",userId);
        log.debug("【幂等性】idempotence:{}->",idempotence);
        //获取redis中的令牌【令牌的对比和删除必须保证原子性】
        //LUA脚本  返回0表示校验令牌失败  1表示删除成功,校验令牌成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(USER_TOKEN_PREFIX + userId),
                idempotence);
        if (result == 1) {
            log.debug("【幂等性】OK:{}->",idempotence);
            log.debug("【系统日志】redis验证成功:{}->",idempotence);
            //令牌验证成功
            //去创建、下订单、验令牌、验价格、锁定库存...
            if (notice.getNoticeTitlet().isEmpty()){
                return new RespEntity(501, "公告标题不能为空", null);
            }
            if (notice.getNoticeContent().isEmpty()){
                return new RespEntity(501, "公告内容不能为空", null);
            }

            // 默认启用
            notice.setNoticeStates("y");
            LocalDateTime dateTime = LocalDateTime.now(); // 获取当前时间
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            notice.setNoticeTime(dateTime.format(formatter));
            noticeService.save(notice);
            return RespEntity.SUCCESS;
        } else {
            log.debug("【幂等性】ERROR:{}->",idempotence);
            log.debug("【系统日志】redis验证失败:{}->",idempotence);
            //令牌校验失败,返回失败信息
            return RespEntity.FAIL;
        }
    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值