同一个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");
    }
}

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

相关推荐
Pniubi7 小时前
HashMap的扩容和树化边界
java·开发语言
Mikko77 小时前
用 Java 复刻了高中时在塞班上玩的「熔岩矿车」:纯 Swing、零依赖、没有一张图片,一个 jar 双击就能玩
java·游戏开发·swing
知了学历孙老师7 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
Java爱好狂.7 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
秋饼8 小时前
LLM API密钥动态轮换实战:Spring Vault + Spring AI + 多Provider路由
java·ai·技术分享·后端开发
马良神笔9 小时前
2026 年Rag最近技术前沿总结
后端·python·django
程序猿DD9 小时前
同样10美元,OpenDesign和Command Code的DeepSeek V4.1 Flash额度到底有多少?
后端
福兮说10 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
高频因子挖掘机10 小时前
A股量化从日线升级到5分钟线:数据量增加后最先暴露哪些工程问题?
后端·github·api
高频因子挖掘机10 小时前
历史行情返回顺序变了,为什么按行号计算指标很危险?
后端·github·api