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

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

相关推荐
装不满的克莱因瓶2 分钟前
【SpringAI Alibaba】ReactAgent结构化输出OutputType与OutputSchema实战
java·人工智能·ai·springai·springaialibaba
Moment14 分钟前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
秋天的怀念_David36 分钟前
算法一千零一夜 · 第二十八夜 注册
java·程序人生·算法
Java码农也是农40 分钟前
Spring 事务详解:@Transactional 如何生效、何时回滚、传播行为怎么选
java·spring boot·spring·事务
学长毕设1 小时前
基于SpringBoot的游乐园管理系统-附源码
java·spring boot·后端
橙序员小站2 小时前
Windows 安装包反复报错?一次排查补上了 5 个 CI 检查点
后端
陈随易2 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
SimonKing2 小时前
震惊:怎么回事,opencode一轮对话耗尽免费额度
java·后端·程序员
装不满的克莱因瓶2 小时前
【SpringAI Alibaba】ReactAgent响应SSE实时流式传输案例实战
java·人工智能·ai·sse·springai
杨凯凡2 小时前
Spring Boot 文件上传怎么做才安全?流式落盘、路径校验与数据库一致性实战
java·spring boot·文件上传·后端安全·数据库一致性