接口幂等性设计的简单思考

Java编程之路 专栏收录该内容
119 篇文章 14 订阅

幂等性场景

  • 接口重试:服务A调用服务B,B由于某些原因未在指定时间内返回响应,出于容错性考虑服务A进行多次重试,服务B接口未做幂等性设置,影响业务数据;
  • 表单重复提交:用户注册接口,用户很激动,瞬时点击多次造成表单重复提交,造成同一用户注册多次;
  • 请求重发:网络抖动引发的nginx重发请求,造成重复调用;
  • 消息重复消费:例如kafka的"再均衡"造成消息重复消费,影响业务数据;

幂等性含义

从数学的角度来看,幂等性表示为f(f(x))=f(x),即x经过同一函数多次计算和一次计算的效果相同,从程序开发角度而言表示一个接口经过多次调用和调用一次的效果一致,这个一致具体表现为 返回值 || 数据影响 一致,例如查询操作"select code,name from table where id=1",此sql执行多次返回结果集总是一致。

解决方案

宏观上来看,业界比较多的就是使用token机制或者唯一业务单号来实现,微观上来说,我们可以细分出以下几种情况:

  1. select和delete操作:这一类操作具有天然幂等性,select多次结果总是一致,delete第一次执行后继续再执行也不会对数据有影响;
  2. insert操作:如果有唯一业务单号例如userCode或多个业务字段联合唯一,则可通过数据库层面的唯一/联合唯一索引来限制重复数据;
  3. update操作:通过数据库层面乐观锁(增加版本号或修改时间字段,修改时带上这个字段"update table set stock=#{stock},version=#{version}+1 where id=#{id} and version=#{version}"行级锁还行)或者悲观锁("update table set stock=#{stock} for update"锁整表比较糟糕)实现,
  4. 混合类型操作(同时包含增删改等):使用token机制,接口A请求前先调用接口B(比如可以在进入页面时候预先请求获取token)获取一次性token并放在redis/jvm内存中,请求接口B时将token放在request的header或作为入参传递给B接口,B接口根据这个token来判断是否为重复请求,伪代码如下:

@RestController
public class MidengController {

    private static final Logger logger = LoggerFactory.getLogger(MidengController.class);

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @GetMapping("/get-token")
    public String getToken(HttpServletRequest request) {
        String token = RandomStringUtils.randomAlphabetic(16).toLowerCase();
        // String sessionId = request.getSession().getId();
        String sessionId = "admin";
        redisTemplate.opsForValue().set(sessionId, token, 600, TimeUnit.SECONDS);
        return token;
    }

    @GetMapping("/check-token")
    public String checkToken(HttpServletRequest request, String token) {
        if (StringUtils.isEmpty(token)) {
            return "token为空,非法请求!";
        }
        //  String sessionId = request.getSession().getId();
        String sessionId = "admin";
        Boolean exist = redisTemplate.delete(sessionId);
        if (exist) {
            return "success!";
        }
        return "token无效!";
    }
}

先调用get-token接口获取一个token,再配合jmeter压测模拟瞬时10个重复请求:

压测结果显示只有一个返回"success",其余均为"token无效"

思考题:上述解决方案比较适合有前后端交互的场景,如果单纯只是多个服务间的接口调用,如何设计接口幂等性比较好?


场景1:系统A将业务需求及功能需求数据推送给系统B,系统B进行后续处理,即系统A->系统B,对于推送接口就需要幂等性处理,否则可能导致系统A推送过来的同一份业务需求在系统B中被处理多次导致重复 

解决方式:将接口post的入参json进行MD5算法处理得到md5值并存放到数据库中,根据md5判定同一份数据是否被快速重复提交多次,伪代码如下

    @Around("pointCut()")
    public Object doBefore(ProceedingJoinPoint point) throws Throwable {
        PushRecord pushRecord = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(PushRecord.class);
        if (pushRecord == null) {
            return point.proceed();
        }
        String source = pushRecord.source();
        if (StringUtils.isEmpty(source)) {
            return point.proceed();
        }
        Object[] args = point.getArgs();
        if (args != null && args.length > 0) {
            String recordContent = JSON.toJSONString(args[0]);
            String md5 = DigestUtils.md5Hex(source + recordContent);
            boolean lock = false;
            try {
                lock = redisDistributeLockUtil.getLock(md5, md5, 3000);
                if (lock) {
                    DemandRecord record = new DemandRecord();
                    record.setMd5(md5);
                    if (demandRecordService.count(record) > 0) {
                        logger.error("该数据已推送过,无需重复推送");
                        return new ResultResponse().data("该数据已推送过,无需重复推送");
                    } else {
                        addItplatformPushRecord(recordContent, source, md5);
                    }
                } else {
                    return new ResultResponse().data("未抢到锁");
                }
            } finally {
                if (lock) {
                    redisDistributeLockUtil.releaseLock(md5, md5);
                }
            }
        }
        return point.proceed();
    }

@Component
public class RedisDistributeLockUtil {
    private static final Long SUCCESS = 1L;

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    public boolean getLock(String lockKey, String value, int expireTime) {
        boolean ret = false;
        try {
            String script = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then " +
                    "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                    "return redis.call('expire',KEYS[1],ARGV[2]) " +
                    "else " +
                    "return 0 " +
                    "end " +
                    "end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            Object execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, expireTime);
            return SUCCESS.equals(execute);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    public boolean releaseLock(String lockKey, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) " +
                "else return 0 " +
                "end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Object execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value);
        return SUCCESS.equals(execute);
    }
}

  • 3
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页

打赏

饭一碗

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值