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

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

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

  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 API
SpringCloud&Gateway网关限流
SpringCloud&Gateway网关限流
1878 7
|
前端开发 NoSQL JavaScript
常见接口和服务幂等性问题及解决方案
常见接口和服务幂等性问题及解决方案
1212 0
|
SQL 关系型数据库 数据库
学习分布式事务Seata看这一篇就够了,建议收藏
学习分布式事务Seata看这一篇就够了,建议收藏
26242 2
|
开发工具 git
Git - Smart Checkout、Force Checkout 区别
Git - Smart Checkout、Force Checkout 区别
3185 0
Git - Smart Checkout、Force Checkout 区别
|
8月前
|
存储 监控 Java
Java实现接口幂等性:程序员的“后悔药”
接口幂等性就像是给系统穿了件"防重复甲",让它在面对:用户疯狂点击、网络抽风重试、系统自动重试等这些情况时,都能淡定地说:"老弟,这个请求我已经处理过了,结果在这,拿去吧!"
394 4
|
人工智能 Java Serverless
【MCP教程系列】搭建基于 Spring AI 的 SSE 模式 MCP 服务并自定义部署至阿里云百炼
本文详细介绍了如何基于Spring AI搭建支持SSE模式的MCP服务,并成功集成至阿里云百炼大模型平台。通过四个步骤实现从零到Agent的构建,包括项目创建、工具开发、服务测试与部署。文章还提供了具体代码示例和操作截图,帮助读者快速上手。最终,将自定义SSE MCP服务集成到百炼平台,完成智能体应用的创建与测试。适合希望了解SSE实时交互及大模型集成的开发者参考。
15697 60
|
9月前
|
算法 NoSQL Java
拒绝服务雪崩!4种经典限流算法图文详解(附Java实战代码)
限流是保护系统的“保险丝”,防止突发流量导致服务雪崩。常见算法有:固定窗口(简单但有突刺)、滑动窗口(精准平滑)、漏桶(恒定处理速率)和令牌桶(允许突发,最常用)。单机限流可用计数器或Guava,分布式场景则依赖Redis实现全局控制。
1371 9
|
10月前
|
监控 关系型数据库 MySQL
MySQL 联合索引
联合索引是MySQL中提升多列查询性能的关键技术,由多个列组成,遵循最左前缀原则。合理设计索引顺序可显著加速查询,避免全表扫描。需结合实际查询需求创建,避免冗余,配合EXPLAIN分析执行计划,优化数据库性能。(238字)
|
11月前
|
安全 Java Unix
UUID v7 一文详解
UUID v7是RFC 9562定义的新型有序UUID,结合时间戳与随机数,兼具全局唯一性、时间有序性和隐私安全,适用于数据库主键与分布式系统,显著提升索引性能与系统效率。
|
存储 缓存 安全
如何保证接口幂等性,幂等性到底是干什么的
本文介绍了幂等性原则及其在程序中的应用。首先定义了幂等性,即无论执行多少次,结果不变的特性,并区分了幂等与非幂等操作。接着详细探讨了实现幂等性的策略,如使用唯一标识符、幂等性标记字段、乐观锁版本控制等。最后,通过Java示例展示了如何实现无状态幂等操作,并强调了幂等性在分布式系统和高并发场景下的重要性。
1673 0