同一个requestId换了参数,为什么不能直接返回旧结果?

创建客户成功了,响应却没送到页面。用户重试时沿用requestId,服务端返回上次结果,这条路径没有问题。可如果用户顺手改了姓名,再用同一个requestId提交,继续返回旧结果就会制造另一种误解:页面显示成功,刚改的姓名根本没保存。

本文补的是重复请求里的"输入绑定"这一层。下面的Java程序用内存Map演示判定规则,不提供跨进程、多实例或持久化幂等保证。

请求号要对应一份业务输入

可以先把已经完成的请求分成两类:相同范围、相同请求号、相同输入,返回原结果;相同范围、相同请求号、不同输入,拒绝复用。

这里的范围包含经过认证的用户和操作类型。Alice创建客户用的r1,不应与Bob创建客户的r1相撞,也不应复用成Alice删除客户的结果。

java 复制代码
record Key(String user, String operation, String requestId) {}
record Entry(String fingerprint, String result) {}

user 在业务接口中来自可信身份上下文,不接受前端随意指定;演示程序里的alice、bob是教学夹具。

不直接对随手序列化的JSON求摘要

字段顺序变化、空白差异、默认值填充,都可能让原始JSON不同,而业务输入相同。先固定规范化规则,再计算稳定摘要。

本例只收email和name两个字段:邮箱去首尾空白并转小写,姓名只去首尾空白。这个邮箱规则是示例选择,不是适用于所有邮箱业务的普遍保证。字段顺序固定,并采用长度前缀,避免简单分隔符拼接产生歧义;字符串前面的v1标记规范化规则版本。

计算摘要是为了识别输入,不是为了加密存储个人信息,也不替代访问控制。若数据库还保留原始输入,仍要按其敏感程度处理。

已完成请求的处理分支

java 复制代码
Entry prior = done.get(key);
if (prior != null) {
    if (!prior.fingerprint().equals(fp)) {
        throw new IllegalArgumentException("REQUEST_KEY_REUSED_WITH_DIFFERENT_INPUT");
    }
    return prior.result();
}

发现输入不同后,不要悄悄覆盖旧摘要,也不要替用户自动换号重做。给出明确的冲突结果,等用户决定是否发起新操作。新请求号只代表新的逻辑请求,业务唯一性依然可能拒绝它。

本地实际结果是:同号同内容返回customer-1;同号改姓名被拒绝,写入次数仍是1;不同用户同号独立处理;新请求号进入新的处理分支。完整程序中这些结果都有断言。

哪些能力不在这张Map里

synchronized 仅串行化当前Java对象的方法调用,程序退出后Map就没了。真实系统至少还需要持久化的范围唯一约束、处理中/成功/失败状态、原结果保存及恢复规则。业务写入与请求记录如何一起提交,应结合事务边界设计。

特别要检查"业务已提交、结果记录没保存"这段窗口。不能因为查不到成功记录就断定从未执行过;也不能用一段内存锁代码宣称端到端Exactly Once。

如果沿用现有requestId实现,可以先只补本文这层:保存规范化输入摘要,对同号改参数加一个拒绝测试,再逐项检查持久化与并发。一次修改解决一个已知缺口,比重新堆一套名词更容易核对。

运行完整例子

下面的类只用JDK标准库,不需要Maven。保存为 ReplayLab.java,从该文件所在目录运行:

shell 复制代码
javac -encoding UTF-8 -d out ReplayLab.java
java -cp out demo.ReplayLab

预期:

text 复制代码
sameInput=customer-1; changedInput=rejected; separateUser=customer-2; newKey=customer-3; checks=6
java 复制代码
package demo;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;

public class ReplayLab {
    record Key(String user,String operation,String requestId) {}
    record Entry(String fingerprint,String result) {}
    final Map<Key,Entry> done=new HashMap<>();
    int writes;
    static String fingerprint(String email,String name) throws Exception {
        // 按示例业务规则:邮箱去首尾空白并小写,姓名去首尾空白;保留姓名大小写。
        String a=email.strip().toLowerCase(Locale.ROOT),b=name.strip();
        // 长度前缀避免字段拼接歧义;v1固定规范化规则的版本。
        String canonical="v1|"+a.length()+":"+a+"|"+b.length()+":"+b;
        return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(canonical.getBytes(StandardCharsets.UTF_8)));
    }
    synchronized String submit(Key key,String email,String name) throws Exception {
        String fp=fingerprint(email,name); Entry prior=done.get(key);
        if(prior!=null){
            if(!prior.fingerprint().equals(fp))throw new IllegalArgumentException("REQUEST_KEY_REUSED_WITH_DIFFERENT_INPUT");
            return prior.result();
        }
        String result="customer-"+(++writes);done.put(key,new Entry(fp,result));return result;
    }
    static void eq(Object e,Object a){if(!e.equals(a))throw new AssertionError(e+" != "+a);}
    public static void main(String[] args) throws Exception {
        var lab=new ReplayLab();var key=new Key("alice","create-customer","r1");
        eq("customer-1",lab.submit(key,"A@example.com","Ada"));
        eq("customer-1",lab.submit(key," a@example.com ","Ada"));
        boolean conflict=false;try{lab.submit(key,"a@example.com","Bob");}catch(IllegalArgumentException e){conflict=true;}
        eq(true,conflict);eq(1,lab.writes);
        eq("customer-2",lab.submit(new Key("bob","create-customer","r1"),"b@example.com","Bob"));
        eq("customer-3",lab.submit(new Key("alice","create-customer","r2"),"c@example.com","Cat"));
        System.out.println("sameInput=customer-1; changedInput=rejected; separateUser=customer-2; newKey=customer-3; checks=6");
    }
}

你们对"同一个请求号改了参数"现在怎么处理?如果直接返回旧结果,页面有没有办法知道这次的新内容没有生效?

相关推荐
MayBaymax1 小时前
Elasticsearch 原理与用法
java·elasticsearch
hsfxuebao1 小时前
Harness工程
后端·架构
马剑威(威哥爱编程)1 小时前
【AI全栈后端12-02】Spring Boot 跑通第一个 AI 对话接口:HR 政策问答机器人实战
java·人工智能·spring boot·机器人
暗夜行者之光1 小时前
LangGraph 实战:构建“自我修正”的代码生成 Agent
后端
IT枫斗者枫哥1 小时前
AI返回合法JSON,字段就可信吗?给抽取结果补一道业务校验
java·人工智能·后端
天天被压力1 小时前
【别再到处找免费股票数据API了:官方204个接口,32篇一次讲透 #06】Python实时行情总报错?五档盘口+逐笔一次跑通
java·人工智能·python
溪语流沙1 小时前
Django + Vue电商项目第005讲:后端骨架|Django初始化、配置分层与DRF接入
vue.js·后端·python·django
益达是我啊1 小时前
前端工程师的 Java 后端 30 天快速入门(1)
后端
今年下半年1 小时前
【网站连通性检测】java项目telnet、curl命令的使用
java·curl·telnet