[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-896":3,"consumer-news-interaction-896":41,"consumer-news-related-896":44},{"detail":4,"item":36},{"card":5,"schemaVersion":23,"fields":24,"content":30},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":15,"href":16,"sourceName":12,"meta":17,"metrics":20,"tags":21,"resolved":22},"NEWS_ARTICLE:896","news","NEWS_ARTICLE",896,"资讯","如何设计对外API接口","博客园","详解生产级对外 API 完整设计方案，围绕易用、安全、健壮黄金三角，覆盖 RESTful 规范、统一响应、签名验签防重放、AOP 注解实现分布式幂等、Redis 令牌桶限流、版本兼容、监控文档。附带可直接复用 Java 核心代码，梳理面试高频技术难点，搭配模拟面试场景，帮你避开线上坑点，快速掌握开放","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202608\u002F739056-20260831134132446-55355388.png","","\u002Fnews\u002F896",[18,19],"2026","综合技术",{},[19],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":25,"categoryName":19,"summary":13,"description":13,"publishTime":26,"updateTime":27,"sourceUrl":28,"language":29},"Rain的Java大神实战圈","2026-08-31T13:44","2026-09-02T20:37:54","https:\u002F\u002Fwww.cnblogs.com\u002Fzrui-xyu\u002Fp\u002F22772864","中文",{"format":31,"policy":32,"normalized":22,"html":33,"text":34,"wordCount":35,"hasBody":22},"HTML","NEWS_CONTENT_V1","\u003Ch2>如何设计对外API接口\u003C\u002Fh2>\n\u003Ch3>先明确 API 设计的 \"黄金三角\" 原则\u003C\u002Fh3>\n\u003Cp>这是所有设计的基础，偏离了这些原则，API 就会变成 \"没人愿意用的鸡肋\" 👇\u003C\u002Fp>\n\u003Cp>\u003Cimg alt=\"image\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202608\u002F739056-20260831134132446-55355388.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Ch3>接口命名与规范 ✅\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>坚决使用 RESTful 风格\u003C\u002Fstrong>，这是业界事实上的标准，能极大降低沟通成本：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>资源用\u003Cstrong>名词复数\u003C\u002Fstrong>：\u003Ccode>\u002Fusers\u003C\u002Fcode>、\u003Ccode>\u002Forders\u003C\u002Fcode>、\u003Ccode>\u002Fproducts\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>HTTP 动词表示操作： \n  \u003Cul>\n   \u003Cli>GET：查询资源\u003C\u002Fli>\n   \u003Cli>POST：创建资源\u003C\u002Fli>\n   \u003Cli>PUT：全量更新资源\u003C\u002Fli>\n   \u003Cli>PATCH：部分更新资源\u003C\u002Fli>\n   \u003Cli>DELETE：删除资源\u003C\u002Fli>\n  \u003C\u002Ful>\u003C\u002Fli>\n \u003Cli>禁止使用动词：❌ \u003Ccode>\u002FgetUser\u003C\u002Fcode>、✅ \u003Ccode>\u002Fusers\u002F{id}\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>路径分隔用\u003Cstrong>短横线\u003C\u002Fstrong>：❌ \u003Ccode>\u002FuserInfo\u003C\u002Fcode>、✅ \u003Ccode>\u002Fuser-info\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>统一小写，避免大小写敏感问题\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>请求与响应设计 📦\u003C\u002Fh3>\n\u003Cp>这是最容易踩坑的地方，\u003Cstrong>统一的格式\u003C\u002Fstrong>比什么都重要！\u003C\u002Fp>\n\u003Ch4>1. 统一响应体结构\u003C\u002Fh4>\n\u003Cpre>\u003Ccode>{\n  \"code\": 200,          \u002F\u002F 业务状态码，≠HTTP状态码\n  \"message\": \"success\", \u002F\u002F 提示信息\n  \"data\": {},           \u002F\u002F 业务数据\n  \"timestamp\": 1717888888888 \u002F\u002F 服务器时间戳\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>2. 分页查询标准设计\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>所有列表接口必须支持分页\u003C\u002Fstrong>，防止大数据量拖垮服务：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>GET \u002Fusers?pageNum=1&amp;pageSize=20&amp;sort=createTime,desc\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>响应示例：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n  \"code\": 200,\n  \"message\": \"success\",\n  \"data\": {\n    \"total\": 100,\n    \"list\": [...],\n    \"pageNum\": 1,\n    \"pageSize\": 20,\n    \"pages\": 5\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>3. 关键注意事项 ⚠️\u003C\u002Fh4>\n\u003Cul>\n \u003Cli>\u003Cstrong>参数校验\u003C\u002Fstrong>：所有入参必须在 Controller 层做校验（使用 JSR-380 规范：\u003Ccode>@NotNull\u003C\u002Fcode>、\u003Ccode>@Size\u003C\u002Fcode>等）\u003C\u002Fli>\n \u003Cli>\u003Cstrong>敏感字段脱敏\u003C\u002Fstrong>：手机号、身份证、银行卡号等必须返回脱敏后的数据\u003C\u002Fli>\n \u003Cli>\u003Cstrong>禁止返回内部异常栈\u003C\u002Fstrong>：生产环境异常信息会泄露系统架构，给黑客可乘之机\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>安全设计 🛡️\u003C\u002Fh3>\n\u003Cp>对外 API 的安全是\u003Cstrong>生命线\u003C\u002Fstrong>，没有安全的 API 就是在裸奔！\u003C\u002Fp>\n\u003Cp>\u003Cimg alt=\"image\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202608\u002F739056-20260831134149754-169393492.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Ch4>核心安全措施：\u003C\u002Fh4>\n\u003Col>\n \u003Cli>\u003Cstrong>强制 HTTPS\u003C\u002Fstrong>：所有对外接口必须使用 HTTPS，禁止 HTTP 明文传输\u003C\u002Fli>\n \u003Cli>\u003Cstrong>签名机制\u003C\u002Fstrong>：请求参数 + 时间戳 + 随机数 + 密钥生成签名，防止参数被篡改\u003C\u002Fli>\n \u003Cli>\u003Cstrong>防重放攻击\u003C\u002Fstrong>：签名有效期一般设为 5-15 分钟，配合 nonce 随机数去重\u003C\u002Fli>\n \u003Cli>\u003Cstrong>身份认证\u003C\u002Fstrong>：优先使用 JWT 无状态认证，涉及高敏感操作时增加二次验证\u003C\u002Fli>\n \u003Cli>\u003Cstrong>权限控制\u003C\u002Fstrong>：基于 RBAC 模型，细粒度控制每个接口的访问权限\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>性能与可用性设计 🚀\u003C\u002Fh3>\n\u003Cp>好的 API 不仅能用，还要\u003Cstrong>好用、耐用\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003Cstrong>问题\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>解决方案\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>实现要点\u003C\u002Fstrong>\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>高并发\u003C\u002Ftd>\n   \u003Ctd>限流\u003C\u002Ftd>\n   \u003Ctd>令牌桶算法，基于 Redis 实现分布式限流\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>服务雪崩\u003C\u002Ftd>\n   \u003Ctd>熔断降级\u003C\u002Ftd>\n   \u003Ctd>使用 Sentinel\u002FHystrix，非核心接口直接降级\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>重复提交\u003C\u002Ftd>\n   \u003Ctd>幂等性\u003C\u002Ftd>\n   \u003Ctd>唯一请求 ID + 防重表，写操作必须保证幂等\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>慢查询\u003C\u002Ftd>\n   \u003Ctd>缓存\u003C\u002Ftd>\n   \u003Ctd>热点数据放入 Redis，设置合理的过期时间\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>大数据量\u003C\u002Ftd>\n   \u003Ctd>分批处理\u003C\u002Ftd>\n   \u003Ctd>导出、批量查询接口必须分批，避免 OOM\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>幂等性设计是重中之重\u003C\u002Fstrong>：所有写操作（POST\u002FPUT\u002FDELETE）都必须保证幂等，防止重复下单、重复扣款等严重业务问题。\u003C\u002Fp>\n\u003Ch3>版本管理 🔄\u003C\u002Fh3>\n\u003Cp>API 一旦发布就不能随意修改，\u003Cstrong>版本控制\u003C\u002Fstrong>是保证兼容性的关键：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>推荐 URL 路径版本\u003C\u002Fstrong>：\u003Ccode>\u002Fapi\u002Fv1\u002Fusers\u003C\u002Fcode>、\u003Ccode>\u002Fapi\u002Fv2\u002Fusers\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>不推荐：参数版本（\u003Ccode>?version=1\u003C\u002Fcode>）、请求头版本\u003C\u002Fli>\n \u003Cli>旧版本至少保留\u003Cstrong>3-6 个月\u003C\u002Fstrong>，给调用方足够的迁移时间\u003C\u002Fli>\n \u003Cli>废弃的接口在响应头中添加\u003Ccode>Deprecation\u003C\u002Fcode>和\u003Ccode>Sunset\u003C\u002Fcode>字段提示\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>文档与监控 📊\u003C\u002Fh3>\n\u003Cul>\n \u003Cli>\u003Cp>\u003Cstrong>接口文档\u003C\u002Fstrong>：强制使用 Swagger\u002FOpenAPI 3.0，文档必须包含：\u003C\u002Fp>\n  \u003Cul>\n   \u003Cli>接口功能说明\u003C\u002Fli>\n   \u003Cli>请求参数、响应字段说明\u003C\u002Fli>\n   \u003Cli>成功 \u002F 失败示例\u003C\u002Fli>\n   \u003Cli>错误码对照表\u003C\u002Fli>\n  \u003C\u002Ful>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>监控告警\u003C\u002Fstrong>：监控以下核心指标：\u003C\u002Fp>\n  \u003Cul>\n   \u003Cli>QPS、响应时间、错误率\u003C\u002Fli>\n   \u003Cli>慢接口 TOP10\u003C\u002Fli>\n   \u003Cli>异常调用次数\u003C\u002Fli>\n   \u003Cli>限流熔断触发次数\u003C\u002Fli>\n  \u003C\u002Ful>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>最后总结 ✨\u003C\u002Fh3>\n\u003Cp>好的对外 API 接口应该像一个 \"\u003Cstrong>优雅的服务员\u003C\u002Fstrong>\"：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>说话清晰（命名规范）\u003C\u002Fli>\n \u003Cli>态度友好（响应统一）\u003C\u002Fli>\n \u003Cli>安全可靠（防护到位）\u003C\u002Fli>\n \u003Cli>反应迅速（性能优秀）\u003C\u002Fli>\n \u003Cli>与时俱进（版本迭代）\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>它不仅是系统之间的桥梁，更是技术团队的 \"脸面\"。一个设计糟糕的 API 会让调用方痛苦不堪，而一个设计优秀的 API 则会大大提升开发效率和系统稳定性。\u003C\u002Fp>\n\u003Ch3>核心代码实现与技术亮点 ✨\u003C\u002Fh3>\n\u003Cp>我会展示对外 API 设计中\u003Cstrong>最核心、最有技术含量\u003C\u002Fstrong>的 5 个模块代码，全部是生产环境可用的标准实现。\u003C\u002Fp>\n\u003Ch4>1. 统一响应体 + 全局异常处理（基础中的基础）\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：泛型封装 + 全局异常拦截，彻底消灭零散的响应构造代码，统一所有异常的返回格式\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 1. 通用响应体\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class ApiResponse&lt;T&gt; {\n    private int code;\n    private String message;\n    private T data;\n    private long timestamp;\n\n    \u002F\u002F 静态工厂方法，业务代码只需调用这几个方法\n    public static &lt;T&gt; ApiResponse&lt;T&gt; success(T data) {\n        return new ApiResponse&lt;&gt;(200, \"success\", data, System.currentTimeMillis());\n    }\n\n    public static &lt;T&gt; ApiResponse&lt;T&gt; fail(int code, String message) {\n        return new ApiResponse&lt;&gt;(code, message, null, System.currentTimeMillis());\n    }\n}\n\n\u002F\u002F 2. 全局异常处理器\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n    \u002F\u002F 业务异常\n    @ExceptionHandler(BusinessException.class)\n    public ApiResponse&lt;Void&gt; handleBusinessException(BusinessException e) {\n        return ApiResponse.fail(e.getCode(), e.getMessage());\n    }\n\n    \u002F\u002F 参数校验异常\n    @ExceptionHandler(MethodArgumentNotValidException.class)\n    public ApiResponse&lt;Void&gt; handleValidationException(MethodArgumentNotValidException e) {\n        String errorMsg = e.getBindingResult().getFieldErrors().stream()\n                .map(FieldError::getDefaultMessage)\n                .collect(Collectors.joining(\", \"));\n        return ApiResponse.fail(400, \"参数错误：\" + errorMsg);\n    }\n\n    \u002F\u002F 兜底异常（禁止返回异常栈）\n    @ExceptionHandler(Exception.class)\n    public ApiResponse&lt;Void&gt; handleException(Exception e) {\n        log.error(\"系统异常\", e); \u002F\u002F 内部记录完整日志\n        return ApiResponse.fail(500, \"系统繁忙，请稍后重试\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>2. 签名验签工具类（安全核心）\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：支持防篡改 + 防重放攻击，生产环境标准实现\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@Component\npublic class SignUtils {\n    private static final String SIGN_SECRET = \"your-secret-key\"; \u002F\u002F 从配置中心读取\n    private static final long SIGN_EXPIRE_TIME = 5 * 60 * 1000; \u002F\u002F 签名有效期5分钟\n\n    \u002F\u002F 生成签名\n    public String generateSign(Map&lt;String, String&gt; params, String timestamp, String nonce) {\n        \u002F\u002F 1. 参数按字典序排序\n        String sortedParams = params.entrySet().stream()\n                .sorted(Map.Entry.comparingByKey())\n                .map(entry -&gt; entry.getKey() + \"=\" + entry.getValue())\n                .collect(Collectors.joining(\"&amp;\"));\n\n        \u002F\u002F 2. 拼接签名串：sortedParams + timestamp + nonce + secret\n        String signStr = sortedParams + timestamp + nonce + SIGN_SECRET;\n\n        \u002F\u002F 3. MD5加密并转大写\n        return DigestUtils.md5DigestAsHex(signStr.getBytes()).toUpperCase();\n    }\n\n    \u002F\u002F 验证签名+防重放\n    public boolean verifySign(Map&lt;String, String&gt; params, String timestamp, String nonce, String sign) {\n        \u002F\u002F 1. 检查时间戳是否过期\n        long requestTime = Long.parseLong(timestamp);\n        if (System.currentTimeMillis() - requestTime &gt; SIGN_EXPIRE_TIME) {\n            return false;\n        }\n\n        \u002F\u002F 2. 检查nonce是否已存在（防重放）\n        String redisKey = \"nonce:\" + nonce;\n        if (redisTemplate.hasKey(redisKey)) {\n            return false;\n        }\n\n        \u002F\u002F 3. 验证签名\n        String serverSign = generateSign(params, timestamp, nonce);\n        if (!serverSign.equals(sign)) {\n            return false;\n        }\n\n        \u002F\u002F 4. 存入Redis，有效期同签名有效期\n        redisTemplate.opsForValue().set(redisKey, \"1\", SIGN_EXPIRE_TIME, TimeUnit.MILLISECONDS);\n        return true;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>3. 幂等性注解 + AOP 实现（无侵入设计）\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：注解驱动 + AOP，业务代码零侵入，支持自定义幂等有效期\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 1. 幂等性注解\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Idempotent {\n    long expireTime() default 10 * 60 * 1000; \u002F\u002F 默认10分钟\n}\n\n\u002F\u002F 2. AOP切面实现\n@Aspect\n@Component\npublic class IdempotentAspect {\n    @Autowired\n    private RedisTemplate&lt;String, String&gt; redisTemplate;\n\n    @Around(\"@annotation(idempotent)\")\n    public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable {\n        \u002F\u002F 1. 获取唯一请求ID（建议从请求头传递，前端生成）\n        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();\n        String requestId = request.getHeader(\"X-Request-Id\");\n        if (StringUtils.isEmpty(requestId)) {\n            throw new BusinessException(400, \"缺少请求唯一标识\");\n        }\n\n        \u002F\u002F 2. Redis原子操作判断是否已执行\n        String redisKey = \"idempotent:\" + requestId;\n        Boolean success = redisTemplate.opsForValue()\n                .setIfAbsent(redisKey, \"1\", idempotent.expireTime(), TimeUnit.MILLISECONDS);\n\n        if (Boolean.FALSE.equals(success)) {\n            throw new BusinessException(409, \"请勿重复提交\");\n        }\n\n        \u002F\u002F 3. 执行原方法\n        try {\n            return point.proceed();\n        } catch (Exception e) {\n            \u002F\u002F 异常时删除key，允许重试\n            redisTemplate.delete(redisKey);\n            throw e;\n        }\n    }\n}\n\n\u002F\u002F 3. 业务使用示例\n@PostMapping(\"\u002Forders\")\n@Idempotent(expireTime = 30 * 60 * 1000) \u002F\u002F 订单30分钟内不允许重复提交\npublic ApiResponse&lt;OrderVO&gt; createOrder(@RequestBody @Valid CreateOrderRequest request) {\n    Order order = orderService.createOrder(request);\n    return ApiResponse.success(convert(order));\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>4. 分布式限流注解 + AOP 实现\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：基于 Redis 令牌桶算法，支持自定义限流规则，集群环境有效\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 1. 限流注解\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface RateLimit {\n    int limit() default 100; \u002F\u002F 每秒允许的请求数\n    int burst() default 200; \u002F\u002F 突发流量上限\n}\n\n\u002F\u002F 2. AOP切面实现\n@Aspect\n@Component\npublic class RateLimitAspect {\n    @Autowired\n    private RedisTemplate&lt;String, String&gt; redisTemplate;\n    private static final String LIMIT_SCRIPT = \"local key = KEYS[1] \" +\n            \"local limit = tonumber(ARGV[1]) \" +\n            \"local burst = tonumber(ARGV[2]) \" +\n            \"local now = tonumber(ARGV[3]) \" +\n            \"local rate = limit \u002F 1000 \" +\n            \"local last_time = tonumber(redis.call('hget', key, 'last_time') or now) \" +\n            \"local tokens = tonumber(redis.call('hget', key, 'tokens') or burst) \" +\n            \"tokens = math.min(burst, tokens + (now - last_time) * rate) \" +\n            \"if tokens &gt;= 1 then \" +\n            \"    redis.call('hset', key, 'tokens', tokens - 1) \" +\n            \"    redis.call('hset', key, 'last_time', now) \" +\n            \"    return 1 \" +\n            \"else \" +\n            \"    return 0 \" +\n            \"end\";\n\n    @Around(\"@annotation(rateLimit)\")\n    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {\n        String key = \"rate_limit:\" + point.getSignature().toShortString();\n        List&lt;String&gt; keys = Collections.singletonList(key);\n        Object[] args = {rateLimit.limit(), rateLimit.burst(), System.currentTimeMillis()};\n\n        Long result = (Long) redisTemplate.execute(new DefaultRedisScript&lt;&gt;(LIMIT_SCRIPT, Long.class), keys, args);\n\n        if (result == 0) {\n            throw new BusinessException(429, \"请求过于频繁，请稍后重试\");\n        }\n\n        return point.proceed();\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>技术难点与解决方案 🧩\u003C\u002Fh3>\n\u003Cp>我整理了对外 API 设计中\u003Cstrong>最容易被面试官追问\u003C\u002Fstrong>的 5 个技术难点，以及对应的生产级解决方案：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003Cstrong>技术难点\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>具体挑战\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>最优解决方案\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>注意事项\u003C\u002Fstrong>\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>分布式幂等性保证 ⚠️\u003C\u002Ftd>\n   \u003Ctd>1. 并发场景下重复请求2. 网络超时导致的重试3. 业务回滚后允许重试\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>Redis 防重表 + 唯一请求 ID\u003C\u002Fstrong>（推荐方案）备选：- 数据库唯一索引- 状态机控制\u003C\u002Ftd>\n   \u003Ctd>1. 所有写操作必须加幂等2. 请求 ID 必须由前端生成3. 异常时要删除 Redis Key\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>防重放攻击 🛡️\u003C\u002Ftd>\n   \u003Ctd>1. 中间人截取请求重复发送2. 签名被破解后批量攻击\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>签名 + 时间戳 + nonce 随机数\u003C\u002Fstrong>1. 签名有效期 5-15 分钟2. nonce 存入 Redis 去重3. 关键接口增加 IP 白名单\u003C\u002Ftd>\n   \u003Ctd>1. 禁止使用固定密钥2. 密钥定期轮换3. 监控异常签名请求\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>大流量限流熔断 🚀\u003C\u002Ftd>\n   \u003Ctd>1. 突发流量打垮服务2. 下游服务故障导致雪崩\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>Redis 分布式限流 + Sentinel 熔断\u003C\u002Fstrong>1. 限流：令牌桶算法2. 熔断：慢调用比例 + 异常比例3. 降级：返回默认值或缓存数据\u003C\u002Ftd>\n   \u003Ctd>1. 限流阈值要压测得出2. 熔断后要有告警3. 核心接口不能降级\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>敏感数据脱敏 🔒\u003C\u002Ftd>\n   \u003Ctd>1. 不同字段脱敏规则不同2. 业务代码零散处理容易遗漏\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>Jackson 自定义序列化器 + 注解\u003C\u002Fstrong>\u003Ccode>java@Sensitive(SensitiveType.PHONE)private String phone;\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>1. 日志也要做脱敏2. 内部接口同样需要脱敏3. 禁止返回完整身份证号\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>API 版本兼容 🔄\u003C\u002Ftd>\n   \u003Ctd>1. 旧版本无法立即下线2. 多个版本并行维护成本高\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>URL 路径版本 + 灰度发布\u003C\u002Fstrong>1. 版本号使用 v1、v2 整数2. 旧版本保留 3-6 个月3. 废弃接口添加告警\u003C\u002Ftd>\n   \u003Ctd>1. 版本内保证向下兼容2. 禁止在旧版本上新增功能3. 监控旧版本调用量\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch3>补充：面试加分项 🌟\u003C\u002Fh3>\n\u003Cp>如果面试官继续追问，可以补充这两点：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>灰度发布\u003C\u002Fstrong>：通过网关根据用户 ID 或流量比例切流到新版本 API，降低发布风险\u003C\u002Fli>\n \u003Cli>\u003Cstrong>API 网关\u003C\u002Fstrong>：将认证、限流、日志、监控等通用逻辑下沉到网关层，业务服务只关注核心逻辑\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>真实面试模拟\u003C\u002Fh2>\n\u003Ch3>真实面试模拟\u003C\u002Fh3>\n\u003Ch4>面试官 😊：\u003C\u002Fh4>\n\u003Cp>欢迎，咱们直入正题。今天这道场景设计题很常见：“\u003Cstrong>如果让你从零设计一套对外的API接口，你会怎么考虑\u003C\u002Fstrong>？” 就从你踩过的坑，想到哪说到哪。\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>好的面试官，对外API就是一份合同，我会把它当成“法务条文”来设计。核心抓住七个点：\u003Cstrong>接口契约、统一响应、安全三板斧、版本管理、流量控制与幂等、监控文档、异步处理\u003C\u002Fstrong>。我按一条请求的生命周期展开说吧。\u003C\u002Fp>\n\u003Ch4>面试官 👂：\u003C\u002Fh4>\n\u003Cp>行，先从最直观的开始。\u003Cstrong>你怎么定接口规范，让调用方一眼就能看懂\u003C\u002Fstrong>？\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>我严格遵循 \u003Cstrong>RESTful 风格\u003C\u002Fstrong>，做到“望文生义”：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>URL 只用名词复数，层级不超过两层，如 \u003Ccode>\u002Fapi\u002Fv1\u002Forders\u002F{orderId}\u002Fitems\u003C\u002Fcode>，一看就知道在操作订单下的商品。\u003C\u002Fli>\n \u003Cli>HTTP 方法语义固定：\u003Ccode>GET\u003C\u002Fcode>查、\u003Ccode>POST\u003C\u002Fcode>创、\u003Ccode>PUT\u003C\u002Fcode>全量改、\u003Ccode>PATCH\u003C\u002Fcode>部分改、\u003Ccode>DELETE\u003C\u002Fcode>删。\u003C\u002Fli>\n \u003Cli>状态码精打细磨：\u003Ccode>200\u002F201\u003C\u002Fcode>成功，\u003Ccode>400\u003C\u002Fcode>参数错，\u003Ccode>401\u003C\u002Fcode>没认证，\u003Ccode>403\u003C\u002Fcode>没权限，\u003Ccode>404\u003C\u002Fcode>不存在，\u003Ccode>409\u003C\u002Fcode>冲突，\u003Ccode>429\u003C\u002Fcode>限流，\u003Ccode>500\u003C\u002Fcode>系统异常。保证调用方只抓状态码就能判断大类。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>面试官 💬：\u003C\u002Fh4>\n\u003Cp>不错，那\u003Cstrong>返回体结构你怎么统一？\u003C\u002Fstrong> 我不希望每个接口格式五花八门。\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>我强制所有接口都套一层\u003Cstrong>统一响应信封\u003C\u002Fstrong>，长这样：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n  \"code\": 200,\n  \"message\": \"success\",\n  \"data\": { ... },\n  \"traceId\": \"a1b2c3d4-xxxx\"\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n \u003Cli>\u003Ccode>code\u003C\u002Fcode> 是业务错误码，能唯一定位问题。\u003C\u002Fli>\n \u003Cli>\u003Ccode>traceId\u003C\u002Fcode> 必须全链路透传，调用方查日志时直接甩这个ID过来，省得扯皮。\u003C\u002Fli>\n \u003Cli>错误时 \u003Ccode>data\u003C\u002Fcode> 为空，\u003Ccode>message\u003C\u002Fcode> 说人话，但\u003Cstrong>绝不暴露\u003C\u002Fstrong>数据库或堆栈信息。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>面试官 🛡️：\u003C\u002Fh4>\n\u003Cp>统一格式是基本功。接下来是重头戏：\u003Cstrong>对外接口的安全你怎么落地\u003C\u002Fstrong>？ 真怕被人一晚上打穿。\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>这块必须上\u003Cstrong>\u003Cstrong>安全三板斧\u003C\u002Fstrong>\u003C\u002Fstrong>，我画个时序图更清晰：\u003C\u002Fp>\n\u003Cp>\u003Cimg alt=\"image\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202608\u002F739056-20260831134217432-2074051782.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Ch4>具体落地：\u003C\u002Fh4>\n\u003Cul>\n \u003Cli>\u003Cstrong>身份+防篡改\u003C\u002Fstrong>：给每个合作方发 \u003Ccode>AppKey\u003C\u002Fcode> + \u003Ccode>AppSecret\u003C\u002Fcode>，要求请求头带上 \u003Cstrong>HMAC-SHA256\u003C\u002Fstrong> 签名。把 \u003Ccode>timestamp\u003C\u002Fcode>、\u003Ccode>nonce\u003C\u002Fcode>（随机数）、请求体拼起来算签，服务端重算对比。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>防重放\u003C\u002Fstrong>：用 \u003Ccode>timestamp\u003C\u002Fcode> 和 \u003Ccode>nonce\u003C\u002Fcode> 联合约束。\u003Ccode>timestamp\u003C\u002Fcode> 偏离服务端时间超过5分钟直接拒掉；\u003Ccode>nonce\u003C\u002Fcode> 存入 Redis，有效期5分钟，出现重复直接视为重放攻击。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>传输\u003C\u002Fstrong>：全站HTTPS，HSTS强制。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>面试官 🤔：\u003C\u002Fh4>\n\u003Cp>挺严密。那我再挑个刺儿：\u003Cstrong>如果调用方坚持自己生成幂等key，不信任你的，怎么防止key冲突？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>这是个常见的信任问题。我会在调用方传来的key前面\u003Cstrong>强制加上AppKey前缀\u003C\u002Fstrong>，变成 \u003Ccode>app_123:order_submit:20260609\u003C\u002Fcode>。这样既隔离了不同租户，也避免恶意伪造撞库。同时限制key总长度，防止撑爆Redis。\u003C\u002Fp>\n\u003Ch4>面试官 🔄：\u003C\u002Fh4>\n\u003Cp>安全过关。接下来，\u003Cstrong>你怎么做版本管理，避免升级时被调用方骂？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>很简单，\u003Cstrong>永不原地修改\u003C\u002Fstrong>。URL路径显式带版本号，如 \u003Ccode>\u002Fapi\u002Fv1\u002F\u003C\u002Fcode>。一旦有破坏性变更，直接发 \u003Ccode>\u002Fapi\u002Fv2\u002F\u003C\u002Fcode>，老版本 v1 保持可用至少一个季度，并提前发邮件定好\u003Cstrong>日落时间\u003C\u002Fstrong>。最忌讳的就是偷偷改字段类型，那是在制造线上事故💥。\u003C\u002Fp>\n\u003Ch4>面试官 ⚡：\u003C\u002Fh4>\n\u003Cp>线上突发流量怎么扛？比如双十一，下游慢怎么办？\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>这要靠\u003Cstrong>限流+降级+幂等\u003C\u002Fstrong>组合拳。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>限流\u003C\u002Fstrong>：网关层按 \u003Ccode>AppKey\u003C\u002Fcode> 用令牌桶，每个key 100QPS。超了直接回 \u003Ccode>429\u003C\u002Fcode>，并带上 \u003Ccode>Retry-After\u003C\u002Fcode> 头。达到80%阈值就告警，准备扩容。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>熔断降级\u003C\u002Fstrong>：内部服务慢了，熔断器打开，直接返回缓存降级数据，不死扛导致雪崩。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>写操作幂等\u003C\u002Fstrong>：所有创建、扣款接口，\u003Cstrong>必须\u003C\u002Fstrong>要求调用方传 \u003Ccode>Idempotent-Key\u003C\u002Fcode>。我用 Redis 的 \u003Ccode>setnx\u003C\u002Fcode> 锁住这个key，处理成功就把结果缓存起来，重复请求直接返回缓存结果。这样就算网超重试，也只成功一次。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode>客户端重试\n              │\n              ▼\n  ┌──────────────────────┐\n  │ 带 Idempotent-Key 请求│\n  └──────────┬───────────┘\n             ▼\n      ┌──────────────┐\n      │ Redis 存在key？│\n      └──┬───────┬───┘\n     存在 │       │ 不存在\n         ▼       ▼\n   返回缓存结果  执行业务\n                 │\n                 ▼\n          结果缓存到Redis(锁定15天)\n                 │\n                 ▼\n            返回结果\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>面试官 👨‍🏫：\u003C\u002Fh4>\n\u003Cp>嗯，这个图把幂等逻辑讲得很透。\u003C\u002Fp>\n\u003Ch4>面试官 📊：\u003C\u002Fh4>\n\u003Cp>接口上线后就是运营了。\u003Cstrong>你怎么让调用方用得爽，自己排查快？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>两手抓：\u003Cstrong>监控与文档\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>可观测性\u003C\u002Fstrong>：QPS、延迟p99、错误率全上Grafana大盘，错误率突增即时钉钉\u002F飞书告警。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>在线文档\u003C\u002Fstrong>：用OpenAPI 3.0自动生成Swagger页面，调用方能在网页上直接试参数。文档里必须写清错误码、限流规则、幂等key用法，最好提供 \u003Cstrong>Java\u002FPython SDK\u003C\u002Fstrong>，帮他们封装好签名、重试、幂等注入，十行代码搞定接入，对接方自然粘性高 🤝。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>面试官 ⏳：\u003C\u002Fh4>\n\u003Cp>最后一个场景，\u003Cstrong>如果接口需要导出大报表，耗时很长，怎么设计交互？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>绝不同步阻塞。接口立即返回 \u003Ccode>202 Accepted\u003C\u002Fcode>，body里给一个 \u003Ccode>taskId\u003C\u002Fcode>。调用方用 \u003Ccode>\u002Fapi\u002Fv1\u002Ftasks\u002F{taskId}\u003C\u002Fcode> 轮询进度。如果甲方有回调能力，我们支持提前注册 webhook，任务一完成主动通知。体验拉满，还不占用连接数。\u003C\u002Fp>\n\u003Ch4>面试官 🧐：\u003C\u002Fh4>\n\u003Cp>整体设计没问题，看得出工程经验。不过我好奇具体落地，\u003Cstrong>能不能挑几个核心代码片段，展示下你的技术亮点？另外，总结一下这种对外API设计的几个硬骨头和你的解法\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch4>候选人 🧑‍💻：\u003C\u002Fh4>\n\u003Cp>没问题，我挑三段最能体现亮点的代码，然后梳理一张难点攻克表。\u003C\u002Fp>\n\u003Ch3>💻 核心代码 &amp; 技术亮点\u003C\u002Fh3>\n\u003Ch4>① 统一响应信封 + 全局异常翻译\u003C\u002Fh4>\n\u003Cp>不写死，用枚举管理错误码，并借助 \u003Ccode>@RestControllerAdvice\u003C\u002Fcode> 把异常自动翻译成标准信封，避免 try-catch 满天飞。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 统一响应体\n@Data\n@AllArgsConstructor\npublic class ApiResponse&lt;T&gt; {\n    private int code;\n    private String message;\n    private T data;\n    private String traceId;\n\n    public static &lt;T&gt; ApiResponse&lt;T&gt; success(T data) {\n        return new ApiResponse&lt;&gt;(200, \"success\", data, MDC.get(\"traceId\"));\n    }\n\n    public static ApiResponse&lt;Void&gt; error(ErrorCode errorCode) {\n        return new ApiResponse&lt;&gt;(errorCode.getCode(), errorCode.getMessage(), null, MDC.get(\"traceId\"));\n    }\n}\n\n\u002F\u002F 全局异常处理，任何未捕获异常都会变成标准格式\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n    @ExceptionHandler(BizException.class)\n    public ApiResponse&lt;Void&gt; handleBizException(BizException e) {\n        return ApiResponse.error(e.getErrorCode());\n    }\n\n    @ExceptionHandler(Exception.class)\n    public ApiResponse&lt;Void&gt; handleException(Exception e) {\n        log.error(\"系统异常\", e);\n        return ApiResponse.error(ErrorCode.SYSTEM_ERROR);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>🌟 亮点：利用 \u003Ccode>MDC\u003C\u002Fcode> 透传 \u003Ccode>traceId\u003C\u002Fcode>，响应里自动带出，配合日志系统，一条链路不漏。\u003C\u002Fp>\n\u003Ch4>② 签名验证拦截器 (HMAC-SHA256 + 防重放)\u003C\u002Fh4>\n\u003Cp>这是安全核心，抽成独立拦截器，不侵入业务代码。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@Component\npublic class SignatureInterceptor implements HandlerInterceptor {\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {\n        String appKey = request.getHeader(\"X-AppKey\");\n        String timestamp = request.getHeader(\"X-Timestamp\");\n        String nonce = request.getHeader(\"X-Nonce\");\n        String clientSign = request.getHeader(\"X-Sign\");\n\n        \u002F\u002F 1. 时间戳偏移校验 (5分钟窗口)\n        if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) &gt; 300_000) {\n            throw new BizException(ErrorCode.TIMESTAMP_EXPIRED);\n        }\n\n        \u002F\u002F 2. nonce防重放 Redis setnx 原子排他\n        String nonceKey = \"nonce:\" + appKey + \":\" + nonce;\n        Boolean locked = redisTemplate.opsForValue().setIfAbsent(nonceKey, \"1\", 5, TimeUnit.MINUTES);\n        if (Boolean.FALSE.equals(locked)) {\n            throw new BizException(ErrorCode.REPLAY_ATTACK);\n        }\n\n        \u002F\u002F 3. 重算签名 (参数+body)\n        String body = getRequestBody(request); \u002F\u002F 缓存body的wrapper\n        String sign = HmacUtils.hmacSha256Hex(appSecretMap.get(appKey),\n                appKey + timestamp + nonce + body);\n        if (!sign.equals(clientSign)) {\n            throw new BizException(ErrorCode.SIGN_INVALID);\n        }\n        return true;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>🌟 亮点：\u003Ccode>setIfAbsent\u003C\u002Fcode> 天然原子性，既防重放又避免锁竞争；签名材料包含 body，防请求体篡改。\u003C\u002Fp>\n\u003Ch4>③ 幂等注解 + AOP（切面自动处理）\u003C\u002Fh4>\n\u003Cp>不想每个方法都写重复逻辑，用声明式注解让业务方一个 \u003Ccode>@Idempotent\u003C\u002Fcode> 搞定。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Idempotent {\n    String keyPrefix() default \"\";   \u002F\u002F 业务前缀\n    long lockTime() default 15;      \u002F\u002F 结果缓存天数\n}\n\n@Aspect\n@Component\npublic class IdempotentAspect {\n\n    @Around(\"@annotation(idempotent)\")\n    public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable {\n        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(\n                RequestContextHolder.getRequestAttributes())).getRequest();\n        String idempotentKey = request.getHeader(\"Idempotent-Key\");\n        if (StringUtils.isBlank(idempotentKey)) {\n            throw new BizException(ErrorCode.MISSING_IDEMPOTENT_KEY);\n        }\n\n        String finalKey = request.getHeader(\"X-AppKey\") + \":\" + idempotent.keyPrefix() + \":\" + idempotentKey;\n        String result = redisTemplate.opsForValue().get(finalKey);\n        if (result != null) {\n            return JSON.parseObject(result, ApiResponse.class); \u002F\u002F 直接返回缓存结果\n        }\n\n        Object proceed = point.proceed();\n        redisTemplate.opsForValue().set(finalKey, JSON.toJSONString(proceed),\n                idempotent.lockTime(), TimeUnit.DAYS);\n        return proceed;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>🌟 亮点：AOP 零侵入，缓存完整响应对象，真正“第一次干活，后续全部秒返回”；key 自动注入租户前缀，隔离安全。\u003C\u002Fp>\n\u003Ch3>🧗 技术难点 &amp; 解决方案\u003C\u002Fh3>\n\u003Cp>对外 API 不是“能用就行”，这些硬骨头啃不掉就会天天救火 🚒。\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003Cstrong>技术难点\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>核心风险\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>解决方案\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>落地关键点\u003C\u002Fstrong>\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>高并发幂等性\u003C\u002Ftd>\n   \u003Ctd>网络重试导致重复扣款\u002F重复下单\u003C\u002Ftd>\n   \u003Ctd>基于Redis原子\u003Ccode>SETNX\u003C\u002Fcode> + 结果缓存\u003C\u002Ftd>\n   \u003Ctd>强制要求\u003Ccode>Idempotent-Key\u003C\u002Fcode>，键名包含租户前缀防冲突\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>防重放与防篡改\u003C\u002Ftd>\n   \u003Ctd>攻击者抓包重放，数据被改\u003C\u002Ftd>\n   \u003Ctd>HMAC签名 + 时间窗口 + nonce一次性令牌\u003C\u002Ftd>\n   \u003Ctd>签名包含请求体、5分钟窗口、nonce Redis暂存\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>版本无缝升级\u003C\u002Ftd>\n   \u003Ctd>老接口废弃导致调用方瘫痪\u003C\u002Ftd>\n   \u003Ctd>URL路径版本号 + 日落期通知\u003C\u002Ftd>\n   \u003Ctd>v1\u002Fv2并存，至少保留一个季度，提前邮件周知\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>突发流量雪崩\u003C\u002Ftd>\n   \u003Ctd>双十一流量打垮下游，级联故障\u003C\u002Ftd>\n   \u003Ctd>网关令牌桶限流 + 业务熔断降级\u003C\u002Ftd>\n   \u003Ctd>超过阈值返回429，预警告警，降级返回兜底数据\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>大任务阻塞连接\u003C\u002Ftd>\n   \u003Ctd>导出报表等长耗时导致HTTP超时\u003C\u002Ftd>\n   \u003Ctd>异步化：202 Accepted + 轮询\u002FWebhook\u003C\u002Ftd>\n   \u003Ctd>立即返回taskId，后台处理，支持回调URL主动通知\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>调用方接入痛苦\u003C\u002Ftd>\n   \u003Ctd>签名、幂等、重试逻辑复杂导致弃用\u003C\u002Ftd>\n   \u003Ctd>提供多语言SDK封装 + 在线交互式文档\u003C\u002Ftd>\n   \u003Ctd>OpenAPI生成文档，SDK仅10行代码完成完整调用\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>问题定位困难\u003C\u002Ftd>\n   \u003Ctd>跨系统调用链断裂，扯皮耗时\u003C\u002Ftd>\n   \u003Ctd>全链路traceId + 统一错误码字典\u003C\u002Ftd>\n   \u003Ctd>网关注入traceId，所有响应和日志携带，Grafana串联\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>最后再附一张\u003Cstrong>防御体系全景图\u003C\u002Fstrong>，方便记忆：\u003C\u002Fp>\n\u003Cp>\u003Cimg alt=\"image\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202608\u002F739056-20260831134247638-376969225.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Cp>这个图把最核心的几道防线串起来了，每一层都在保护后面的系统 🤺。\u003C\u002Fp>\n\u003Ch4>面试官 🚀：\u003C\u002Fh4>\n\u003Cp>非常好，从代码到难点体系都讲得很实，看得出你是真刀真枪在线上扛过的。这一题咱们就完美收尾，准备进入下个领域。\u003C\u002Fp>\n\u003Chr>\n\u003Cp>公众号“Rain的Java大神之路”\u003Cbr>\n  个人博客“www.javadashen.com”\u003C\u002Fp>","如何设计对外API接口 先明确 API 设计的 \"黄金三角\" 原则 这是所有设计的基础，偏离了这些原则，API 就会变成 \"没人愿意用的鸡肋\" 👇 接口命名与规范 ✅ 坚决使用 RESTful 风格，这是业界事实上的标准，能极大降低沟通成本： 资源用名词复数：\u002Fusers、\u002Forders、\u002Fproducts HTTP 动词表示操作： GET：查询资源 POST：创建资源 PUT：全量更新资源 PATCH：部分更新资源 DELETE：删除资源 禁止使用动词：❌ \u002FgetUser、✅ \u002Fusers\u002F{id} 路径分隔用短横线：❌ \u002FuserInfo、✅ \u002Fuser-info 统一小写，避免大小写敏感问题 请求与响应设计 📦 这是最容易踩坑的地方，统一的格式比什么都重要！ 1. 统一响应体结构 { \"code\": 200, \u002F\u002F 业务状态码，≠HTTP状态码 \"message\": \"success\", \u002F\u002F 提示信息 \"data\": {}, \u002F\u002F 业务数据 \"timestamp\": 1717888888888 \u002F\u002F 服务器时间戳 } 2. 分页查询标准设计 所有列表接口必须支持分页，防止大数据量拖垮服务： GET \u002Fusers?pageNum=1&pageSize=20&sort=createTime,desc 响应示例： { \"code\": 200, \"message\": \"success\", \"data\": { \"total\": 100, \"list\": [...], \"pageNum\": 1, \"pageSize\": 20, \"pages\": 5 } } 3. 关键注意事项 ⚠️ 参数校验：所有入参必须在 Controller 层做校验（使用 JSR-380 规范：@NotNull、@Size等） 敏感字段脱敏：手机号、身份证、银行卡号等必须返回脱敏后的数据 禁止返回内部异常栈：生产环境异常信息会泄露系统架构，给黑客可乘之机 安全设计 🛡️ 对外 API 的安全是生命线，没有安全的 API 就是在裸奔！ 核心安全措施： 强制 HTTPS：所有对外接口必须使用 HTTPS，禁止 HTTP 明文传输 签名机制：请求参数 + 时间戳 + 随机数 + 密钥生成签名，防止参数被篡改 防重放攻击：签名有效期一般设为 5-15 分钟，配合 nonce 随机数去重 身份认证：优先使用 JWT 无状态认证，涉及高敏感操作时增加二次验证 权限控制：基于 RBAC 模型，细粒度控制每个接口的访问权限 性能与可用性设计 🚀 好的 API 不仅能用，还要好用、耐用： 问题 解决方案 实现要点 高并发 限流 令牌桶算法，基于 Redis 实现分布式限流 服务雪崩 熔断降级 使用 Sentinel\u002FHystrix，非核心接口直接降级 重复提交 幂等性 唯一请求 ID + 防重表，写操作必须保证幂等 慢查询 缓存 热点数据放入 Redis，设置合理的过期时间 大数据量 分批处理 导出、批量查询接口必须分批，避免 OOM 幂等性设计是重中之重：所有写操作（POST\u002FPUT\u002FDELETE）都必须保证幂等，防止重复下单、重复扣款等严重业务问题。 版本管理 🔄 API 一旦发布就不能随意修改，版本控制是保证兼容性的关键： 推荐 URL 路径版本：\u002Fapi\u002Fv1\u002Fusers、\u002Fapi\u002Fv2\u002Fusers 不推荐：参数版本（?version=1）、请求头版本 旧版本至少保留3-6 个月，给调用方足够的迁移时间 废弃的接口在响应头中添加Deprecation和Sunset字段提示 文档与监控 📊 接口文档：强制使用 Swagger\u002FOpenAPI 3.0，文档必须包含： 接口功能说明 请求参数、响应字段说明 成功 \u002F 失败示例 错误码对照表 监控告警：监控以下核心指标： QPS、响应时间、错误率 慢接口 TOP10 异常调用次数 限流熔断触发次数 最后总结 ✨ 好的对外 API 接口应该像一个 \"优雅的服务员\"： 说话清晰（命名规范） 态度友好（响应统一） 安全可靠（防护到位） 反应迅速（性能优秀） 与时俱进（版本迭代） 它不仅是系统之间的桥梁，更是技术团队的 \"脸面\"。一个设计糟糕的 API 会让调用方痛苦不堪，而一个设计优秀的 API 则会大大提升开发效率和系统稳定性。 核心代码实现与技术亮点 ✨ 我会展示对外 API 设计中最核心、最有技术含量的 5 个模块代码，全部是生产环境可用的标准实现。 1. 统一响应体 + 全局异常处理（基础中的基础） 技术亮点：泛型封装 + 全局异常拦截，彻底消灭零散的响应构造代码，统一所有异常的返回格式 \u002F\u002F 1. 通用响应体 @Data @AllArgsConstructor @NoArgsConstructor public class ApiResponse\u003CT> { private int code; private String message; private T data; private long timestamp; \u002F\u002F 静态工厂方法，业务代码只需调用这几个方法 public static \u003CT> ApiResponse\u003CT> success(T data) { return new ApiResponse\u003C>(200, \"success\", data, System.currentTimeMillis()); } public static \u003CT> ApiResponse\u003CT> fail(int code, String message) { return new ApiResponse\u003C>(code, message, null, System.currentTimeMillis()); } } \u002F\u002F 2. 全局异常处理器 @RestControllerAdvice public class GlobalExceptionHandler { \u002F\u002F 业务异常 @ExceptionHandler(BusinessException.class) public ApiResponse\u003CVoid> handleBusinessException(BusinessException e) { return ApiResponse.fail(e.getCode(), e.getMessage()); } \u002F\u002F 参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public ApiResponse\u003CVoid> handleValidationException(MethodArgumentNotValidException e) { String errorMsg = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(\", \")); return ApiResponse.fail(400, \"参数错误：\" + errorMsg); } \u002F\u002F 兜底异常（禁止返回异常栈） @ExceptionHandler(Exception.class) public ApiResponse\u003CVoid> handleException(Exception e) { log.error(\"系统异常\", e); \u002F\u002F 内部记录完整日志 return ApiResponse.fail(500, \"系统繁忙，请稍后重试\"); } } 2. 签名验签工具类（安全核心） 技术亮点：支持防篡改 + 防重放攻击，生产环境标准实现 @Component public class SignUtils { private static final String SIGN_SECRET = \"your-secret-key\"; \u002F\u002F 从配置中心读取 private static final long SIGN_EXPIRE_TIME = 5 * 60 * 1000; \u002F\u002F 签名有效期5分钟 \u002F\u002F 生成签名 public String generateSign(Map\u003CString, String> params, String timestamp, String nonce) { \u002F\u002F 1. 参数按字典序排序 String sortedParams = params.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(entry -> entry.getKey() + \"=\" + entry.getValue()) .collect(Collectors.joining(\"&\")); \u002F\u002F 2. 拼接签名串：sortedParams + timestamp + nonce + secret String signStr = sortedParams + timestamp + nonce + SIGN_SECRET; \u002F\u002F 3. MD5加密并转大写 return DigestUtils.md5DigestAsHex(signStr.getBytes()).toUpperCase(); } \u002F\u002F 验证签名+防重放 public boolean verifySign(Map\u003CString, String> params, String timestamp, String nonce, String sign) { \u002F\u002F 1. 检查时间戳是否过期 long requestTime = Long.parseLong(timestamp); if (System.currentTimeMillis() - requestTime > SIGN_EXPIRE_TIME) { return false; } \u002F\u002F 2. 检查nonce是否已存在（防重放） String redisKey = \"nonce:\" + nonce; if (redisTemplate.hasKey(redisKey)) { return false; } \u002F\u002F 3. 验证签名 String serverSign = generateSign(params, timestamp, nonce); if (!serverSign.equals(sign)) { return false; } \u002F\u002F 4. 存入Redis，有效期同签名有效期 redisTemplate.opsForValue().set(redisKey, \"1\", SIGN_EXPIRE_TIME, TimeUnit.MILLISECONDS); return true; } } 3. 幂等性注解 + AOP 实现（无侵入设计） 技术亮点：注解驱动 + AOP，业务代码零侵入，支持自定义幂等有效期 \u002F\u002F 1. 幂等性注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { long expireTime() default 10 * 60 * 1000; \u002F\u002F 默认10分钟 } \u002F\u002F 2. AOP切面实现 @Aspect @Component public class IdempotentAspect { @Autowired private RedisTemplate\u003CString, String> redisTemplate; @Around(\"@annotation(idempotent)\") public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable { \u002F\u002F 1. 获取唯一请求ID（建议从请求头传递，前端生成） HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String requestId = request.getHeader(\"X-Request-Id\"); if (StringUtils.isEmpty(requestId)) { throw new BusinessException(400, \"缺少请求唯一标识\"); } \u002F\u002F 2. Redis原子操作判断是否已执行 String redisKey = \"idempotent:\" + requestId; Boolean success = redisTemplate.opsForValue() .setIfAbsent(redisKey, \"1\", idempotent.expireTime(), TimeUnit.MILLISECONDS); if (Boolean.FALSE.equals(success)) { throw new BusinessException(409, \"请勿重复提交\"); } \u002F\u002F 3. 执行原方法 try { return point.proceed(); } catch (Exception e) { \u002F\u002F 异常时删除key，允许重试 redisTemplate.delete(redisKey); throw e; } } } \u002F\u002F 3. 业务使用示例 @PostMapping(\"\u002Forders\") @Idempotent(expireTime = 30 * 60 * 1000) \u002F\u002F 订单30分钟内不允许重复提交 public ApiResponse\u003COrderVO> createOrder(@RequestBody @Valid CreateOrderRequest request) { Order order = orderService.createOrder(request); return ApiResponse.success(convert(order)); } 4. 分布式限流注解 + AOP 实现 技术亮点：基于 Redis 令牌桶算法，支持自定义限流规则，集群环境有效 \u002F\u002F 1. 限流注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int limit() default 100; \u002F\u002F 每秒允许的请求数 int burst() default 200; \u002F\u002F 突发流量上限 } \u002F\u002F 2. AOP切面实现 @Aspect @Component public class RateLimitAspect { @Autowired private RedisTemplate\u003CString, String> redisTemplate; private static final String LIMIT_SCRIPT = \"local key = KEYS[1] \" + \"local limit = tonumber(ARGV[1]) \" + \"local burst = tonumber(ARGV[2]) \" + \"local now = tonumber(ARGV[3]) \" + \"local rate = limit \u002F 1000 \" + \"local last_time = tonumber(redis.call('hget', key, 'last_time') or now) \" + \"local tokens = tonumber(redis.call('hget', key, 'tokens') or burst) \" + \"tokens = math.min(burst, tokens + (now - last_time) * rate) \" + \"if tokens >= 1 then \" + \" redis.call('hset', key, 'tokens', tokens - 1) \" + \" redis.call('hset', key, 'last_time', now) \" + \" return 1 \" + \"else \" + \" return 0 \" + \"end\"; @Around(\"@annotation(rateLimit)\") public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable { String key = \"rate_limit:\" + point.getSignature().toShortString(); List\u003CString> keys = Collections.singletonList(key); Object[] args = {rateLimit.limit(), rateLimit.burst(), System.currentTimeMillis()}; Long result = (Long) redisTemplate.execute(new DefaultRedisScript\u003C>(LIMIT_SCRIPT, Long.class), keys, args); if (result == 0) { throw new BusinessException(429, \"请求过于频繁，请稍后重试\"); } return point.proceed(); } } 技术难点与解决方案 🧩 我整理了对外 API 设计中最容易被面试官追问的 5 个技术难点，以及对应的生产级解决方案： 技术难点 具体挑战 最优解决方案 注意事项 分布式幂等性保证 ⚠️ 1. 并发场景下重复请求2. 网络超时导致的重试3. 业务回滚后允许重试 Redis 防重表 + 唯一请求 ID（推荐方案）备选：- 数据库唯一索引- 状态机控制 1. 所有写操作必须加幂等2. 请求 ID 必须由前端生成3. 异常时要删除 Redis Key 防重放攻击 🛡️ 1. 中间人截取请求重复发送2. 签名被破解后批量攻击 签名 + 时间戳 + nonce 随机数1. 签名有效期 5-15 分钟2. nonce 存入 Redis 去重3. 关键接口增加 IP 白名单 1. 禁止使用固定密钥2. 密钥定期轮换3. 监控异常签名请求 大流量限流熔断 🚀 1. 突发流量打垮服务2. 下游服务故障导致雪崩 Redis 分布式限流 + Sentinel 熔断1. 限流：令牌桶算法2. 熔断：慢调用比例 + 异常比例3. 降级：返回默认值或缓存数据 1. 限流阈值要压测得出2. 熔断后要有告警3. 核心接口不能降级 敏感数据脱敏 🔒 1. 不同字段脱敏规则不同2. 业务代码零散处理容易遗漏 Jackson 自定义序列化器 + 注解java@Sensitive(SensitiveType.PHONE)private String phone; 1. 日志也要做脱敏2. 内部接口同样需要脱敏3. 禁止返回完整身份证号 API 版本兼容 🔄 1. 旧版本无法立即下线2. 多个版本并行维护成本高 URL 路径版本 + 灰度发布1. 版本号使用 v1、v2 整数2. 旧版本保留 3-6 个月3. 废弃接口添加告警 1. 版本内保证向下兼容2. 禁止在旧版本上新增功能3. 监控旧版本调用量 补充：面试加分项 🌟 如果面试官继续追问，可以补充这两点： 灰度发布：通过网关根据用户 ID 或流量比例切流到新版本 API，降低发布风险 API 网关：将认证、限流、日志、监控等通用逻辑下沉到网关层，业务服务只关注核心逻辑 真实面试模拟 真实面试模拟 面试官 😊： 欢迎，咱们直入正题。今天这道场景设计题很常见：“如果让你从零设计一套对外的API接口，你会怎么考虑？” 就从你踩过的坑，想到哪说到哪。 候选人 🧑‍💻： 好的面试官，对外API就是一份合同，我会把它当成“法务条文”来设计。核心抓住七个点：接口契约、统一响应、安全三板斧、版本管理、流量控制与幂等、监控文档、异步处理。我按一条请求的生命周期展开说吧。 面试官 👂： 行，先从最直观的开始。你怎么定接口规范，让调用方一眼就能看懂？ 候选人 🧑‍💻： 我严格遵循 RESTful 风格，做到“望文生义”： URL 只用名词复数，层级不超过两层，如 \u002Fapi\u002Fv1\u002Forders\u002F{orderId}\u002Fitems，一看就知道在操作订单下的商品。 HTTP 方法语义固定：GET查、POST创、PUT全量改、PATCH部分改、DELETE删。 状态码精打细磨：200\u002F201成功，400参数错，401没认证，403没权限，404不存在，409冲突，429限流，500系统异常。保证调用方只抓状态码就能判断大类。 面试官 💬： 不错，那返回体结构你怎么统一？ 我不希望每个接口格式五花八门。 候选人 🧑‍💻： 我强制所有接口都套一层统一响应信封，长这样： { \"code\": 200, \"message\": \"success\", \"data\": { ... }, \"traceId\": \"a1b2c3d4-xxxx\" } code 是业务错误码，能唯一定位问题。 traceId 必须全链路透传，调用方查日志时直接甩这个ID过来，省得扯皮。 错误时 data 为空，message 说人话，但绝不暴露数据库或堆栈信息。 面试官 🛡️： 统一格式是基本功。接下来是重头戏：对外接口的安全你怎么落地？ 真怕被人一晚上打穿。 候选人 🧑‍💻： 这块必须上安全三板斧，我画个时序图更清晰： 具体落地： 身份+防篡改：给每个合作方发 AppKey + AppSecret，要求请求头带上 HMAC-SHA256 签名。把 timestamp、nonce（随机数）、请求体拼起来算签，服务端重算对比。 防重放：用 timestamp 和 nonce 联合约束。timestamp 偏离服务端时间超过5分钟直接拒掉；nonce 存入 Redis，有效期5分钟，出现重复直接视为重放攻击。 传输：全站HTTPS，HSTS强制。 面试官 🤔： 挺严密。那我再挑个刺儿：如果调用方坚持自己生成幂等key，不信任你的，怎么防止key冲突？ 候选人 🧑‍💻： 这是个常见的信任问题。我会在调用方传来的key前面强制加上AppKey前缀，变成 app_123:order_submit:20260609。这样既隔离了不同租户，也避免恶意伪造撞库。同时限制key总长度，防止撑爆Redis。 面试官 🔄： 安全过关。接下来，你怎么做版本管理，避免升级时被调用方骂？ 候选人 🧑‍💻： 很简单，永不原地修改。URL路径显式带版本号，如 \u002Fapi\u002Fv1\u002F。一旦有破坏性变更，直接发 \u002Fapi\u002Fv2\u002F，老版本 v1 保持可用至少一个季度，并提前发邮件定好日落时间。最忌讳的就是偷偷改字段类型，那是在制造线上事故💥。 面试官 ⚡： 线上突发流量怎么扛？比如双十一，下游慢怎么办？ 候选人 🧑‍💻： 这要靠限流+降级+幂等组合拳。 限流：网关层按 AppKey 用令牌桶，每个key 100QPS。超了直接回 429，并带上 Retry-After 头。达到80%阈值就告警，准备扩容。 熔断降级：内部服务慢了，熔断器打开，直接返回缓存降级数据，不死扛导致雪崩。 写操作幂等：所有创建、扣款接口，必须要求调用方传 Idempotent-Key。我用 Redis 的 setnx 锁住这个key，处理成功就把结果缓存起来，重复请求直接返回缓存结果。这样就算网超重试，也只成功一次。 客户端重试 │ ▼ ┌──────────────────────┐ │ 带 Idempotent-Key 请求│ └──────────┬───────────┘ ▼ ┌──────────────┐ │ Redis 存在key？│ └──┬───────┬───┘ 存在 │ │ 不存在 ▼ ▼ 返回缓存结果 执行业务 │ ▼ 结果缓存到Redis(锁定15天) │ ▼ 返回结果 面试官 👨‍🏫： 嗯，这个图把幂等逻辑讲得很透。 面试官 📊： 接口上线后就是运营了。你怎么让调用方用得爽，自己排查快？ 候选人 🧑‍💻： 两手抓：监控与文档。 可观测性：QPS、延迟p99、错误率全上Grafana大盘，错误率突增即时钉钉\u002F飞书告警。 在线文档：用OpenAPI 3.0自动生成Swagger页面，调用方能在网页上直接试参数。文档里必须写清错误码、限流规则、幂等key用法，最好提供 Java\u002FPython SDK，帮他们封装好签名、重试、幂等注入，十行代码搞定接入，对接方自然粘性高 🤝。 面试官 ⏳： 最后一个场景，如果接口需要导出大报表，耗时很长，怎么设计交互？ 候选人 🧑‍💻： 绝不同步阻塞。接口立即返回 202 Accepted，body里给一个 taskId。调用方用 \u002Fapi\u002Fv1\u002Ftasks\u002F{taskId} 轮询进度。如果甲方有回调能力，我们支持提前注册 webhook，任务一完成主动通知。体验拉满，还不占用连接数。 面试官 🧐： 整体设计没问题，看得出工程经验。不过我好奇具体落地，能不能挑几个核心代码片段，展示下你的技术亮点？另外，总结一下这种对外API设计的几个硬骨头和你的解法。 候选人 🧑‍💻： 没问题，我挑三段最能体现亮点的代码，然后梳理一张难点攻克表。 💻 核心代码 & 技术亮点 ① 统一响应信封 + 全局异常翻译 不写死，用枚举管理错误码，并借助 @RestControllerAdvice 把异常自动翻译成标准信封，避免 try-catch 满天飞。 \u002F\u002F 统一响应体 @Data @AllArgsConstructor public class ApiResponse\u003CT> { private int code; private String message; private T data; private String traceId; public static \u003CT> ApiResponse\u003CT> success(T data) { return new ApiResponse\u003C>(200, \"success\", data, MDC.get(\"traceId\")); } public static ApiResponse\u003CVoid> error(ErrorCode errorCode) { return new ApiResponse\u003C>(errorCode.getCode(), errorCode.getMessage(), null, MDC.get(\"traceId\")); } } \u002F\u002F 全局异常处理，任何未捕获异常都会变成标准格式 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public ApiResponse\u003CVoid> handleBizException(BizException e) { return ApiResponse.error(e.getErrorCode()); } @ExceptionHandler(Exception.class) public ApiResponse\u003CVoid> handleException(Exception e) { log.error(\"系统异常\", e); return ApiResponse.error(ErrorCode.SYSTEM_ERROR); } } 🌟 亮点：利用 MDC 透传 traceId，响应里自动带出，配合日志系统，一条链路不漏。 ② 签名验证拦截器 (HMAC-SHA256 + 防重放) 这是安全核心，抽成独立拦截器，不侵入业务代码。 @Component public class SignatureInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String appKey = request.getHeader(\"X-AppKey\"); String timestamp = request.getHeader(\"X-Timestamp\"); String nonce = request.getHeader(\"X-Nonce\"); String clientSign = request.getHeader(\"X-Sign\"); \u002F\u002F 1. 时间戳偏移校验 (5分钟窗口) if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 300_000) { throw new BizException(ErrorCode.TIMESTAMP_EXPIRED); } \u002F\u002F 2. nonce防重放 Redis setnx 原子排他 String nonceKey = \"nonce:\" + appKey + \":\" + nonce; Boolean locked = redisTemplate.opsForValue().setIfAbsent(nonceKey, \"1\", 5, TimeUnit.MINUTES); if (Boolean.FALSE.equals(locked)) { throw new BizException(ErrorCode.REPLAY_ATTACK); } \u002F\u002F 3. 重算签名 (参数+body) String body = getRequestBody(request); \u002F\u002F 缓存body的wrapper String sign = HmacUtils.hmacSha256Hex(appSecretMap.get(appKey), appKey + timestamp + nonce + body); if (!sign.equals(clientSign)) { throw new BizException(ErrorCode.SIGN_INVALID); } return true; } } 🌟 亮点：setIfAbsent 天然原子性，既防重放又避免锁竞争；签名材料包含 body，防请求体篡改。 ③ 幂等注解 + AOP（切面自动处理） 不想每个方法都写重复逻辑，用声明式注解让业务方一个 @Idempotent 搞定。 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { String keyPrefix() default \"\"; \u002F\u002F 业务前缀 long lockTime() default 15; \u002F\u002F 结果缓存天数 } @Aspect @Component public class IdempotentAspect { @Around(\"@annotation(idempotent)\") public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull( RequestContextHolder.getRequestAttributes())).getRequest(); String idempotentKey = request.getHeader(\"Idempotent-Key\"); if (StringUtils.isBlank(idempotentKey)) { throw new BizException(ErrorCode.MISSING_IDEMPOTENT_KEY); } String finalKey = request.getHeader(\"X-AppKey\") + \":\" + idempotent.keyPrefix() + \":\" + idempotentKey; String result = redisTemplate.opsForValue().get(finalKey); if (result != null) { return JSON.parseObject(result, ApiResponse.class); \u002F\u002F 直接返回缓存结果 } Object proceed = point.proceed(); redisTemplate.opsForValue().set(finalKey, JSON.toJSONString(proceed), idempotent.lockTime(), TimeUnit.DAYS); return proceed; } } 🌟 亮点：AOP 零侵入，缓存完整响应对象，真正“第一次干活，后续全部秒返回”；key 自动注入租户前缀，隔离安全。 🧗 技术难点 & 解决方案 对外 API 不是“能用就行”，这些硬骨头啃不掉就会天天救火 🚒。 技术难点 核心风险 解决方案 落地关键点 高并发幂等性 网络重试导致重复扣款\u002F重复下单 基于Redis原子SETNX + 结果缓存 强制要求Idempotent-Key，键名包含租户前缀防冲突 防重放与防篡改 攻击者抓包重放，数据被改 HMAC签名 + 时间窗口 + nonce一次性令牌 签名包含请求体、5分钟窗口、nonce Redis暂存 版本无缝升级 老接口废弃导致调用方瘫痪 URL路径版本号 + 日落期通知 v1\u002Fv2并存，至少保留一个季度，提前邮件周知 突发流量雪崩 双十一流量打垮下游，级联故障 网关令牌桶限流 + 业务熔断降级 超过阈值返回429，预警告警，降级返回兜底数据 大任务阻塞连接 导出报表等长耗时导致HTTP超时 异步化：202 Accepted + 轮询\u002FWebhook 立即返回taskId，后台处理，支持回调URL主动通知 调用方接入痛苦 签名、幂等、重试逻辑复杂导致弃用 提供多语言SDK封装 + 在线交互式文档 OpenAPI生成文档，SDK仅10行代码完成完整调用 问题定位困难 跨系统调用链断裂，扯皮耗时 全链路traceId + 统一错误码字典 网关注入traceId，所有响应和日志携带，Grafana串联 最后再附一张防御体系全景图，方便记忆： 这个图把最核心的几道防线串起来了，每一层都在保护后面的系统 🤺。 面试官 🚀： 非常好，从代码到难点体系都讲得很实，看得出你是真刀真枪在线上扛过的。这一题咱们就完美收尾，准备进入下个领域。 公众号“Rain的Java大神之路” 个人博客“www.javadashen.com”",13392,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":16,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":40},"2026 · 综合技术","#2563eb","16 \u002F 10",[19],{"targetType":8,"targetId":9,"likedByMe":42,"likeCount":43,"commentCount":43,"contentLikeCount":43,"contentCommentCount":43,"sourceLikeCount":43,"sourceCommentCount":43},false,0,[45,52,59,65,72,79,85,92],{"id":46,"kind":7,"title":47,"summary":48,"image":49,"href":50,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":51},"NEWS_ARTICLE:838","AI 学习笔记：LLM 的微调实验","title: LLM 的微调实验 author: 凌杰 date: 2026-08-21 tags: LoRA, LLaMA-Factory, Qwen categories: 人工智能 [!NOTE] 笔记说明 这篇笔记对应的是《[[关于 AI 的学习路线图]]》一文中所规划的第三个学习阶段。其中","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260902122904258-1285666483.png","\u002Fnews\u002F838",[19],{"id":53,"kind":7,"title":54,"summary":55,"image":56,"href":57,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":58},"NEWS_ARTICLE:843","介绍一下常用的Token鉴权方案","本文梳理 Session‑Cookie、JWT、OAuth2.0、SSO 主流 Token 鉴权方案，对比各方案适用场景。重点讲解生产级 JWT 双 Token 架构，给出 RS256 非对称加密、Redis 黑名单、网关统一鉴权等 Java 实战代码。剖析 JWT 注销、并发刷新、令牌泄露等落地痛","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260902114155245-1924311389.png","\u002Fnews\u002F843",[19],{"id":60,"kind":7,"title":61,"summary":62,"image":15,"href":63,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":64},"NEWS_ARTICLE:854","分治：序列分治（CDQ）与点分治","分治：序列分治（CDQ）与点分治 一、分治思想概述 分治（Divide and Conquer）是算法设计中最核心的思想之一。它的基本策略是： 分（Divide）：将原问题划分为规模更小的子问题。 治（Conquer）：递归地求解子问题（若子问题足够小则直接求解）。 合（Combine）：将子问题的","\u002Fnews\u002F854",[19],{"id":66,"kind":7,"title":67,"summary":68,"image":69,"href":70,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":71},"NEWS_ARTICLE:863","弱模型不能裸奔：Agent Harness 凭什么真实有效","Harness 不是给弱模型贴的创可贴。它是把工程纪律——验证、门禁、不变量、路由——变成架构里一等公民的方式。模型每半年换一代，今天省钱的 Flash 明天可能就过时了，但那套'默认怀疑、机器校验、按决策密度调度'的流程会留下来，并且越跑越值钱。\n弱模型不能裸奔。给它穿上 harness，便宜才真","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202608\u002F510-20260830181633785-1171146572.jpg","\u002Fnews\u002F863",[19],{"id":73,"kind":7,"title":74,"summary":75,"image":76,"href":77,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":78},"NEWS_ARTICLE:865","焕新鸿蒙应用权限管理方案，应用授权体验再升级","作为用户或应用开发者，或许经历过类似的体验场景：使用应用的过程中，触发应用某些功能会需要访问你的位置、麦克风、相机等常用权限，若为了保护隐私拒绝授权后，想要使用功能时，再次打开却找不到设置入口；或开启流程繁琐，需要经过频繁跳转和设置。这一问题不仅影响用户体验，还可能造成应用功能不可用、用户流失，也制","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F2396482\u002F202609\u002F2396482-20260901170248450-744043562.png","\u002Fnews\u002F865",[19],{"id":80,"kind":7,"title":81,"summary":82,"image":15,"href":83,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":84},"NEWS_ARTICLE:875","别急着翻译 SKILL.md：我做了一个专门拆解 Skill 设计的工具","别急着翻译 SKILL.md：我做了一个专门拆解 Skill 设计的工具 第一次打开一份复杂的 SKILL.md，很多人的反应都是从头往下读。 每句话似乎都认识，连在一起却不一定明白：为什么这里用了 MUST？为什么执行前要读取这些文件？状态由谁维护？AI 做到什么程度才算完成？如果两条指令发生冲突","\u002Fnews\u002F875",[19],{"id":86,"kind":7,"title":87,"summary":88,"image":89,"href":90,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":91},"NEWS_ARTICLE:879","高并发下的抢红包设计：微信红包背后的算法与温情","本文拆解微信拼手气红包高并发实现，详解核心**二倍均值算法**，解决红包分配公平性问题。架构上依靠 Redis+Lua 脚本实现原子抢红包，规避超发与重复抢夺；结合 MQ 异步落库、热点 key 拆分、多层限流、定时对账兜底，支撑百万 QPS。给出 Java、Lua 核心源码，梳理超发、缓存一致性、","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260901095345589-1940958188.png","\u002Fnews\u002F879",[19],{"id":93,"kind":7,"title":94,"summary":95,"image":15,"href":96,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":97},"NEWS_ARTICLE:906","上周热点回顾（8.24-8.30）","热点随笔： &#183; 从 PostgreSQL 到 Kubernetes：开源的护城河，从来不写在代码里 (张善友) &#183; 代码都能让AI写了，我还学个屁？ (佛祖让我来巡山) &#183; Vibe Coding 月提交量 29 亿次之后：GitHub 的危机、Azure 迁移，以及","\u002Fnews\u002F906",[19]]