重复点击提交、产生多笔数据、保持数据只操作一次---->接口幂等性校验

简介: 重复点击提交、产生多笔数据、保持数据只操作一次---->接口幂等性校验

一、工作真实场景 + 常出现场景

  1. 真实场景:在一次工作中进行成品出库创建成品出库单时,手抖了一下,重复点击了两次确定(提交表单)。结果很神奇的发现居然产生了两笔一模一样的数据(流水号都一样),当时就很懵逼,稍作思考,想想应该是在同一时刻创建了两个出库单。感觉很有意思(因为之前没有遇到过,写代码的时候也没有考虑到这个问题的发生),后面换了一种方式复现场景:提交表单时多次点击Enter按钮,还是会产生多笔重复数据。
  2. 常见场景:
    订单接口, 不能多次创建订单
    支付接口, 重复支付同一笔订单只能扣一次钱
    支付宝回调接口, 可能会多次回调, 必须处理重复回调
    普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
    等等

二、工作解决方案 + 百度解决方案

  1. 工作:
    (1) 在对应业务接口上加上@Transactional,后面发现不能要(只是为了在遇到错误时之前对数据库操作做一个回滚),因为在做相同数据校验的时候去数据库是查不到同比数据的,此时所有代码没走完,事务未提交。所以只能拿掉事务。
    (2)在对应的业务代码块加上synchronized (this) {}对业务代码进行代码块的同步锁,锁里面做唯一性校验(查询数据库是否有相同的流水号[流水号是按照英文前缀加上时间戳实现],有的话直接抛异常)。后面发现如果在极端条件下这个判断还是有一定缺陷(时间戳只是精确到秒)。
@Override
    public JsonData saveOrUpdate(OutboundProductReq outboundProductReq) {
        // 出库单不能为空
        if (StringUtils.isBlank(outboundProductReq.getOutboundName())) {
            return JsonData.buildResult(BizCodeEnum.STORE_PRODUCT_OUTBOUND_NAME_EMPTY);
        }
        OutboundProductDO outboundProductDO;
        if (outboundProductReq.getId() != null && outboundProductReq.getId() > 0) {
            outboundProductDO = outboundProductMapper.selectById(outboundProductReq.getId());
            Integer outboundCount = outboundProductDO.getOutboundCount();
            // 校验成品库存
            JsonData data = checkProductStock(outboundProductReq.getProductId(), outboundProductReq.getOutboundCount(), outboundCount);
            if (data.getCode() == 0) {
                // 修改
                if (outboundProductDO.getOutboundState() == 0) {
                    BeanUtils.copyProperties(outboundProductReq, outboundProductDO);
                    outboundProductMapper.updateById(outboundProductDO);
                    // 修改库存详情表
                    updateTODetailProduct(outboundProductDO);
                    // 修改成品冻结数
                    StockProductDO stockProductDO = stockProductMapper.selectOne(new QueryWrapper<StockProductDO>().eq("product_id", outboundProductDO.getProductId()));
                    stockProductDO.setFreezeCount(stockProductDO.getFreezeCount() - outboundCount + outboundProductReq.getOutboundCount());
                    stockProductMapper.updateById(stockProductDO);
                } else {
                    return JsonData.buildResult(BizCodeEnum.STORE_PRODUCT_NOT_MODIFY);
                }
            } else {
                return data;
            }
        } else {
            // 校验成品库存
            JsonData data = checkProductStock(outboundProductReq.getProductId(), outboundProductReq.getOutboundCount());
            if (data.getCode() == 0) {
                // 新增
                synchronized (this) {
                    // 问题:连续点击速度很快时,会出现两条重复记录 解决方案:将以下操作抽取出来做方法,在方法上加事务和锁。
                    if (outboundProductMapper.selectOne(new QueryWrapper<OutboundProductDO>()
                            .eq("serial_code", CommonUtil.getCurrentSerialNumber("SN_OUT"))) != null) {
                        return JsonData.buildResult(BizCodeEnum.STORE_PRODUCT_OUTBOUND_SAME);
                    }
                    outboundProductDO = new OutboundProductDO();
                    BeanUtils.copyProperties(outboundProductReq, outboundProductDO);
                    outboundProductDO.setOutboundId(UUID.randomUUID().toString());
                    outboundProductDO.setOutboundState(0);
                    outboundProductDO.setOutboundType(0);
                    outboundProductDO.setSerialCode(CommonUtil.getCurrentSerialNumber("SN_OUT"));
                    outboundProductMapper.insert(outboundProductDO);
                    // 冻结成品库存
                    StockProductDO stockProductDO = stockProductMapper.selectOne(new QueryWrapper<StockProductDO>()
                            .eq("product_id", outboundProductDO.getProductId()));
                    stockProductDO.setFreezeCount(outboundProductReq.getOutboundCount() + stockProductDO.getFreezeCount());
                    stockProductMapper.updateById(stockProductDO);
                    // 保存到库存详情表
                    saveTODetailProduct(outboundProductDO, OpTypeEnum.OP_LOCK);
                }
            } else {
                return data;
            }
        }
        return JsonData.buildSuccess();
    }
  1. 唯一索引 – 防止新增脏数据
    token机制 – 防止页面重复提交
    悲观锁 – 获取数据的时候加锁(锁表或锁行)
    乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
    分布式锁 – redis(jedis、redisson)或zookeeper实现
    状态机 – 状态变更, 更新数据时判断状态

分享优秀博客

幂等校验

相关文章
|
NoSQL 关系型数据库 MySQL
接口防刷 && 接口幂等性问题
接口防刷 && 接口幂等性问题
240 0
|
12月前
|
消息中间件 存储 缓存
如果对方没做幂等!记一次生产订单重复的反思
公司旧系统中发现一个严重bug:用户支付一年服务费,系统却将有效期增加了两年。经分析,原因是消息队列(MQ)向第三方服务发送了两次消息,且该接口未实现幂等性控制。此问题可能导致财务损失和信誉受损。解决方案包括:生产者端通过请求频率限制、幂等键等防重措施;消费者端利用缓存和数据库确保幂等性;消息队列层配置去重功能、TTL和死信队列等。
128 0
|
设计模式 缓存 前端开发
什么是幂等性?四种接口幂等性方案详解!
本文深入分布式系统中的幂等性问题及其解决方案,涵盖数据库唯一主键、乐观锁、PRG模式和防重Token等方法,关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
什么是幂等性?四种接口幂等性方案详解!
|
监控 Java
压力测试Jmeter的简单使用,性能监控-堆内存与垃圾回收 -jvisualvm的使用
这篇文章介绍了如何使用JMeter进行压力测试,包括测试前的配置、测试执行和结果查看。同时,还探讨了性能监控工具jconsole和jvisualvm的使用,特别是jvisualvm,它可以监控内存泄露、跟踪垃圾回收、执行时内存和CPU分析以及线程分析等,文章还提供了使用这些工具的详细步骤和说明。
压力测试Jmeter的简单使用,性能监控-堆内存与垃圾回收 -jvisualvm的使用
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
3310 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
安全 网络安全 开发者
Docker学习笔记(一):Docker命令总结
本文介绍了Docker的基本概念、优点以及常用命令。Docker是一个开源的应用容器引擎,它通过容器打包应用程序及其依赖项,实现快速部署和迁移。主要优点包括轻量级、可移植性、易于管理、安全性和开源性。文章还区分了镜像和容器的概念,并提供了构建镜像、查看容器、运行容器、停止和删除容器等常用Docker命令的示例。
359 0
|
存储 Java 测试技术
阿里巴巴java开发手册
这篇文章是关于阿里巴巴Java开发手册的整理,内容包括编程规约、异常日志、单元测试、安全规约、MySQL数据库使用以及工程结构等方面的详细规范和建议,旨在帮助开发者编写更加规范、高效和安全的代码。
|
Java 数据库连接 网络安全
已解决 IDEA Maven 项目中 “Could not find artifact“ 问题的常见情况和解决方案
已解决 IDEA Maven 项目中 “Could not find artifact“ 问题的常见情况和解决方案
15758 0
|
缓存 运维 容灾
入行5年,谈谈我在阿里做测试开发的经验
作者在阿里一直从事测试开发相关工作,这几年学习很多、收获很多,作者希望给还在该方向摸爬滚打的同学一些启发和方向。