ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

电商支付与风控面试实录:从Spring Boot、MyBatis、Redis、Kafka到微服务与安全的全栈技术实战解析

电商支付与风控面试实录:从Spring Boot、MyBatis、Redis、Kafka到微服务与安全的全栈技术实战解析 电商支付与风控面试实录从Spring Boot、MyBatis、Redis、Kafka到微服务与安全的全栈技术实战解析面试开场面试官一脸严肃翻着简历“谢飞机是吧坐。今天我们聊一下电商业务场景你之前说做过电商项目那我们从基础开始。总共三轮每轮几个问题你尽量讲清楚。”谢飞机紧张地搓了搓手“好的面试官我尽量。”第一轮核心语言与数据持久化面试官抬眼“你简历写的是Java 8和11都用过那你说说Java 8和Java 11的主要区别有哪些”谢飞机眼睛一亮“这个我熟Java 8有Lambda表达式写集合操作就像流水线一下就把循环写完了。还有新的时间API再也不怕SimpleDateFormat线程不安全了。Java 11嘛……它也是LTS支持用var声明变量就是那个‘自动类型推断’别的……嗯……好像还有HttpClient我记不太清了。”面试官点点头“基本方向没错Java 8还有Stream、Optional、CompletableFuture这些Java 11在模块化、ZGC垃圾收集器上也有改进。那Java 8和11的长期支持版本选择你们项目怎么考虑的”谢飞机“我们用的Java 8因为公司老项目都是它稳定不搬。”面试官不置可否继续问“那Spring Boot的核心启动原理是什么SpringBootApplication这个注解是不是就相当于一个‘启动开关’”谢飞机搓搓手指“这个我会。启动类上有一个main方法然后调用SpringApplication.run就把Spring容器拉起来了。SpringBootApplication是个组合注解里面包含Configuration、EnableAutoConfiguration和ComponentScan。自动配置会把我们需要的Bean自动装进去比如数据库连接池、RedisTemplate这些。”面试官难得露出一丝笑意“嗯说得不错。那接着问数据库这块MyBatis里#{}和${}有什么区别你在写SQL的时候怎么选”谢飞机赶紧答“#{}是预编译会生成一个问号占位符值由PreparedStatement来设置能防止SQL注入。${}就是直接字符串拼进去比较危险所以一般传排序字段、表名这种没法用占位符的地方才用。我们公司都规定能用#{}就不要用${}。”面试官“很好。那你说下Flyway和HikariCP这两个东西在Spring Boot启动时是怎么配合的比如你要执行数据库迁移脚本连接池和Flyway谁先谁后”谢飞机愣了一下“呃……应该是先跑Flyway因为脚本要把表建好后面程序才用连接池好像……不对”他挠挠头“也可能是先建立连接池再跑Flyway这个我记不清了反正他们都能配。”面试官轻轻摇头“Flyway执行数据库迁移需要一个数据源而Spring Boot自动配置是先通过DataSource的自动配置创建HikariCP连接池然后再把连接池交给Flyway去执行迁移脚本。顺序反了可不行。你回去要再看下Spring Boot自动配置的加载顺序。”谢飞机低头“哦……好的。”第二轮缓存、消息队列与秒杀场景面试官话锋一转“我们这有一个电商订单系统到了大促时热点商品的详情页会被疯狂请求。假设有人恶意刷不存在的商品ID导致请求全部穿透到数据库你怎么解决”谢飞机“这个我懂用‘蓝布过滤器’”他看面试官表情不对忙改口“哦不是布隆过滤器。把所有存在的商品ID都放进布隆过滤器来了一个请求通过布隆过滤器判断这个ID是否存在不存在就直接返回‘商品不存在’不打数据库。如果ID存在再走缓存和数据库。”面试官追问“布隆过滤器原理是什么为什么说它有一定误判率”谢飞机磕磕绊绊“它……就是一个很长的二进制位数组还要好几个哈希函数。把一个ID经过这些哈希函数算出来映射到几个位置上把它们置成1。查询时看几个位置是不是都为1如果有一个是0那这个ID一定不存在如果都是1那就可能存在。因为哈希碰撞所以误判。”面试官“好那如果缓存和数据库数据不一致比如商家改了商品价格你在项目里怎么保证缓存和数据库的一致性”谢飞机“我们用的Cache Aside Pattern先更新数据库然后再删除缓存。这样下次读的时候缓存没有就到数据库拿再回填缓存。”面试官“那如果删除缓存失败了呢”谢飞机“呃……那就……重试或者等下再删我们当时好像没处理……”面试官“生产环境这个问题很常见。通常可以引入可靠消息队列或者订阅数据库binlog来异步删除缓存还要考虑延迟双删。你这样可不行。”谢飞机冷汗直冒“明白明白我后来研究过。”面试官继续“秒杀场景下你用什么来削峰Kafka消息会丢失吗怎么保证消息不丢和消费者处理顺序”谢飞机说到这个有点兴奋“我们用Kafka秒杀请求先写到Kafka里下游服务再慢慢消费这样数据库不会被打爆。消息不丢的话生产者要设置acksall消费者处理完再手动提交offset。顺序性方面比如同一个用户或者同一个订单的消息可以按订单号作为key这样Kafka会把它分布到同一个分区里面。”面试官“那如果一个分区被多个消费者同时消费顺序还能保证吗”谢飞机迟疑“不能……会乱。我记着Kafka是一个消费者线程在处理一个分区的消息如果换成多线程并发处理同分区的消息就肯定乱了。”面试官凝视他“那你刚才说‘同一个分区’其实没问题——一个分区只会被一个消费者组内的一个消费者实例消费。如果你的消费者内部搞了多线程并行那才会乱。看来你把原理搞混了。那重复消息你们怎么处理”谢飞机“这个我会消费的时候要用幂等。我们订单表设置了唯一订单号重复插入会被数据库主动拒绝还有状态机比如支付收到重复消息看到订单已经是‘已支付’就直接忽略。”面试官点头“这个回答还可以。”第三轮微服务、注册中心与安全面试官“你们的电商是微服务架构吧注册中心用的什么Eureka和Consul有什么区别”谢飞机“我们项目用的是Eureka。Eureka是AP模型Consul是CP模型。Eureka每个节点都是对等的如果网络分区它保留的可用节点还是能接收请求但是数据可能不一致。Consul用Raft算法选主强一致性但是等选举的时候可能暂时不可用。”面试官“不错。那服务间调用你用的OpenFeign如果下游接口超时了你的Feign超时时间怎么配置配合Resilience4j怎么去熔断和降级”谢飞机“Feign超时有连接超时和读取超时在application.yml里配置。Resilience4j有CircuitBreaker注解能做个熔断器出现异常多了就打开熔断快速失败还能配一个fallback方法返回默认值。”面试官“超时和重试最容易导致雪崩。你觉得设置超时、重试次数、熔断触发比例这些参数时要注意什么”谢飞机支支吾吾“就是……超时时间不能太大不然请求堆积重试次数也不能太多不然下游更惨熔断阈值……嗯……我们当时就是抄的网上配置没细调……”面试官深吸一口气“希望你不是上线后才抄的。”谢飞机尴尬地笑了笑。面试官继续“电商系统用户登录鉴权你用JWT还是OAuth2两者什么区别”谢飞机眼睛一亮“都是常用的JWT就是那种一串token带上用户信息服务端不用存session无状态。OAuth2是一个授权框架不是一种具体的token格式它规定客户端、用户、授权服务器、资源服务器三方联动拿到Access Token再去调API。比如微信登录就是OAuth2授权码模式。我们自己的后台API用的是JWT在Spring Security里写了个过滤器解析请求头的Authorization验签然后设置SecurityContext。”面试官“那么支付平台回调呢支付回调如果被人伪造或者网络重发你怎么保证安全”谢飞机这下积极“回调地址如果被攻击别人假装支付平台发一个成功通知那就亏大了。所以要做验签支付平台用自己的私钥签名我们平台拿公钥验签还要再验证一下订单号和金额是否一致。防重放可以用nonce或者时间戳而且回调处理要用订单状态机状态从‘待支付’改成‘已支付’不能重复改。”面试官“嗯你这个回答有点东西。最后一个问题整体上你觉得你的项目里最难的部分是什么”谢飞机想了想“最难的是……我讲不明白的部分。”面试官“……”面试官合上笔记本淡淡说“谢先生你有一些知识掌握得还不错但也有很多细节是含糊的尤其是缓存一致性、Kafka顺序和微服务调度的边界情况。项目里对复杂问题不能只靠‘记得’去支撑。这样吧今天的面试先到这里你先回去等通知。”谢飞机站起来鞠了一躬“谢谢面试官我一定等通知。这次回去我好好看文档”起身时把椅子碰了一下他赶紧扶住灰溜溜地走出了会议室。附录面试问题详细答案与业务场景技术解析以下将面试中的每个问题展开结合电商/支付/秒杀等业务场景从原理到落地方案给想学技术的小白做一个完整的梳理。第一轮详解核心语言与数据持久化1. Java 8 与 Java 11 的主要区别业务场景大厂内部在选型时通常会选择 LTS长期支持版本。Java 8 和 Java 11 是两个关键的分水岭。Java 8 的重要特性Lambda 表达式用lambda - 表达式替代匿名内部类配合Stream API可以写出非常简洁的集合处理代码。Stream API支持惰性求值、管道操作、并行流如list.stream().filter(x - x.getPrice() 10).collect(...)。新的日期时间 APILocalDate、LocalDateTime、Instant、Duration、Period解决了旧Date/Calendar的线程不安全、设计混乱问题。Optional 容器避免NullPointerException。接口默认方法与静态方法接口中可以有default方法允许在不破坏实现类的情况下扩展接口。CompletableFuture异步编程支持回调、编排多个异步任务。Metaspace 替代 PermGen方法区不再产生永久代内存溢出。Java 11 的重要特性LTS基于 Java 9 和 Java 10引入了模块化系统JPMS、局部变量类型推断var。Java 11 新增了标准HttpClient支持 HTTP/2 和 WebSocket。String、Collection、Optional 等类增加了不少便捷方法比如String.isBlank()、String.lines()、String.strip()。引入了低停顿垃圾收集器ZGC适合大堆内存场景。JEP 系列比如 动态类文件常量、Epsilon 无操作 GC 等。面试答题建议回答时提到“LTS”“Lambda/Stream”“var/HttpClient/ZGC”再说明项目选型考虑稳定性和生态支持。2. Spring Boot 启动原理与 SpringBootApplication业务场景一个电商服务启动时需要自动配置数据库、Redis、消息队列、Web 容器等。Spring Boot 的自动配置就是为了减少手动写一堆xxx.xml配置文件。启动流程简述执行SpringApplication.run(Application.class, args)。创建SpringApplication实例确定应用类型Servlet/Reactive。加载spring.factories中的ApplicationContextInitializer和ApplicationListener。准备环境变量Environment。创建并刷新ApplicationContext对于 Web 应用创建ServletWebServerApplicationContext。在刷新过程中执行自动配置EnableAutoConfiguration通过AutoConfigurationImportSelector加载所有自动配置类。启动内嵌 Tomcat / Jetty / Undertow完成服务注册。SpringBootApplication 是一个组合注解包含三个核心注解SpringBootConfiguration其实就是Configuration标识这是一个配置类。EnableAutoConfiguration开启自动配置将需要装配的 Bean 按条件加载。ComponentScan扫描启动类所在包及其子包包括Component、Service、Repository、Controller等。常见自动配置类举例DataSourceAutoConfiguration自动装配DataSource连接池。RedisAutoConfiguration自动装配RedisTemplate、StringRedisTemplate。KafkaAutoConfiguration自动装配KafkaTemplate、ConsumerFactory。WebMvcAutoConfiguration自动装配 Spring MVC 相关组件。面试中可以补充Spring Boot 的条件装配ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty让配置只有在满足条件时才生效。3. MyBatis 中#{}与${}的区别业务场景在订单查询中经常写SELECT * FROM t_order WHERE user_id #{userId}如果用${userId}当传入1 or 11时SQL 会变成SELECT * FROM t_order WHERE user_id 1 or 11导致全表数据被查询出来严重时还可以拼接DROP TABLE等破坏性语句。核心区别#{}MyBatis 会将其解析为一个 JDBC 预编译占位符?再通过PreparedStatement的setXxx()方法传入参数。这个过程是类型安全的能够防止 SQL 注入。${}MyBatis 直接进行字符串替换生成的 SQL 是拼接后的内容。若参数是用户可控输入会产生注入漏洞。什么场景下必须使用${}动态排序ORDER BY ${orderByColumn} ${orderDir}但必须对传入的列名/排序方向做白名单校验。动态表名SELECT * FROM ${tableName}例如按月分表order_202506。数据库关键字或动态 SQL 片段。工程规范95% 以上场景用#{}不可避免用${}时先做枚举校验或正则校验。4. Flyway 和 HikariCP 在 Spring Boot 中的协作业务场景商品系统要做数据库表结构版本管理。开发人员写好V1__create_goods.sql提交后其他人员migrate就能自动建表、加字段。Flyway 就是数据库迁移工具。HikariCP是 Spring Boot 2.x 默认的 JDBC 连接池特点是高性能、轻量级。Spring Boot 自动配置的技术细节DataSourceAutoConfiguration首先创建DataSource默认使用 HikariCP如果 classpath 中存在 HikariCP。FlywayAutoConfiguration在DataSource创建之后执行。它接收这个DataSource作为参数创建Flyway对象。应用启动时Flyway 通过连接池获取数据库连接执行db/migration目录下的迁移脚本并维护flyway_schema_history表记录版本。所以顺序是先有连接池再跑 Flyway 迁移。如果反过来Flyway 没有数据库连接池可用就无法连接数据库执行 SQL。在实际生产中连接池管理的是运行期连接Flyway 是启动期一次性迁移两者职责不同。常用配置spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle5 spring.datasource.hikari.connection-timeout30000 spring.flyway.enabledtrue spring.flyway.locationsclasspath:db/migration spring.flyway.baseline-on-migratetrue这里注意原文中“谢飞机说先跑Flyway再建连接池”是错误的面试官已经纠正。第二轮详解缓存、消息队列与秒杀削峰1. 缓存穿透与布隆过滤器业务场景商品详情页缓存 Redis查询顺序为请求到达先查 Redis。如果 Redis 没有查询 MySQL。如果 MySQL 有回填 Redis 并返回。攻击者循环请求不存在的商品 ID比如负数、UUIDRedis 和 MySQL 都没有于是每次请求都打到数据库最终数据库崩溃。这就是缓存穿透。解决方案布隆过滤器系统启动时把存在的商品 ID 全部放入布隆过滤器。请求到达时先判断 ID 是否在过滤器中。若过滤器说“不存在”则直接返回“商品不存在”若过滤器说“可能存在”则继续查 Redis/DB。缓存空值对于查询结果为空的数据Redis 中设置一个短 TTL 的空值比如 2 分钟。这样下次相同请求不会打到 DB。缺点是无法拦截大量不同的非法 ID。参数校验如 ID 必须为正整数、长度限制等。布隆过滤器原理初始化一个长度为 m 的 bit 数组初始全 0。插入一个元素时通过 k 个哈希函数计算出 k 个位置将 bit 置 1。查询一个元素时计算 k 个位置如果任意一位为 0说明元素肯定不存在如果都为 1说明元素可能存在。由于哈希碰撞某个不存在的元素可能和已存在的元素映射到相同 bit 位置所以布隆过滤器宁可错杀判断为可能存在也不会漏报判断为不存在就确定不存在。它的优点是节省内存适合海量数据判断。删除元素困难因为不能简单地把 bit 置 0否则会影响其他元素。工程落地GoogleGuava BloomFilter或者 Redis 的bitmap自己实现也可以用 Redis 4.0 的插件RedisBloom。2. 缓存与数据库一致性方案业务场景商家修改商品价格MySQL 中price改成了 99但 Redis 缓存中还是旧值 109用户在页面上看到不对。这个问题是分布式系统的经典难题。Cache Aside Pattern最常用的模式读先读缓存命中则返回未命中则读 DB回填缓存返回。写先更新 DB然后删除缓存。为什么是“更新 DB 后删缓存”而不是“更新 DB 后写缓存”删除缓存比更新缓存更简单且不会产生并发写旧值的问题。如果写缓存两个并发请求可能一个写新值、一个写旧值导致缓存长期为脏数据。删除缓存后下一次读请求会重新加载最新 DB 值。“先更新 DB再删除缓存”潜在的失败场景更新 DB 成功了删除缓存失败。怎么处理方案一发送一条延迟消息到消息队列消费者异步重试删除。方案二启动一个订阅 Binlog 的组件如 Canal监听数据库变更异步刷新缓存/删除缓存。方案三引入本地消息表 定时任务保证最终一致。“延迟双删”策略先删除缓存。更新数据库。休眠一小段时间如 500ms再次删除缓存。这样做的目的是删除掉在更新 DB 期间可能由于旧数据回填造成的缓存脏数据。但延迟时间不好确定实际并发高时依然有边界问题。最终一致性的最佳实践还是Binlog 订阅 重试。面试回答建议先答 Cache Aside 模式再说删除缓存失败可以用 MQ 重试或 Canal最后强调做不到强一致只能做到最终一致。3. Kafka 消息不丢失与顺序保证业务场景秒杀系统的下单指令通过 Kafka 异步传递给订单服务。如果消息丢了用户秒杀成功但订单没建出来这是重大事故。Kafka 消息不丢失需要从三个端分别保障生产者端设置acksall或-1表示分区副本全部写入成功后才返回成功。设置retries0允许临时失败重试。设置enable.idempotencetrue实现生产者幂等避免重试造成重复消息。Broker 端replication.factor3副本数至少 3。min.insync.replicas2至少两个副本同步成功才确认。消费者端关闭自动提交enable.auto.commitfalse。在消息处理成功后再手动提交 offset。如果业务处理失败可以重试或打入死信队列不要直接提交。Kafka 的顺序保证核心概念是分区Kafka 只保证单个分区内的消息是有序的。如果业务要求同一订单的消息按顺序处理可以让生产者将orderId作为 key。Kafka 根据 key 哈希选择分区同一个 key 的消息会进入同一个分区从而保证分区内顺序。一个分区在同一消费者组内只会被一个消费者实例拉取正常情况下该消费者按 offset 顺序消费所以顺序不会乱。如果消费者内部使用多线程并行处理同一个分区的消息就会乱序。解决办法是内部用单线程或再按业务 key 做内部队列保证同一个 key 的并发串行。若一个分区下有多个消费者组不同消费者组相互独立不影响顺序。注意不能为了保证全局顺序而让整个 topic 只有一个分区那样会严重限制吞吐量。实际业务中只要求按 key 有序。4. 秒杀削峰与消费幂等业务场景一个商品库存 100 件瞬时 10 万用户点击秒杀。如果 10 万请求直接打订单服务、扣减库存数据库会瞬间卡死。所以需要“前端削峰、异步处理”。经典秒杀架构前端静态化秒杀页面加验证码/滑块/答题延缓请求。使用 Redis 进行库存预扣减Lua脚本原子性判断库存并减库存比如if (stock 0) then stock stock - 1; return 1; else return 0; end。通过 Redis 预扣成功后将“秒杀成功事件”发送到 Kafka。订单服务从 Kafka 异步消费消息创建订单再扣减数据库中的库存。如果数据库扣减失败要发送补偿消息回滚 Redis 库存。为什么要异步Kafka 将瞬时高并发请求变成可平滑处理的队列下游服务按自己的处理能力消费不会被打爆。消费者如何处理重复消息幂等性因为 Kafka 可能“消息被投递多次”比如消费者处理成功后提交 offset 前宕机重启后重新拉取到同一条消息所以消费逻辑必须幂等。常见幂等实现使用数据库唯一约束比如order_sn唯一索引。重复插入时违反唯一约束捕获异常直接返回成功。使用状态机处理前先查询order.status只有“待支付”才允许修改为“已支付”否则忽略。使用 RedisSETNX通过SETNX order_id_{id} 1判断是否已处理过。第三轮详解微服务、注册中心与调用链1. Eureka 与 Consul 的区别业务场景微服务架构中商品服务、订单服务、支付服务互相调用。服务实例的 IP 地址会动态变化所以需要注册中心。注册中心维护服务实例列表并提供健康检查、发现服务的能力。EurekaNetflix 开源属于AP 模型优先保证可用性牺牲一致性。节点之间是对等的没有主从。注册信息在各个节点之间异步同步。如果网络分区Eureka 中可能出现某个节点数据与其他节点不一致但所有节点都还能读写。Eureka 有自我保护机制当短时间内丢失大量心跳时不会立即剔除服务而是保留实例防止因网络抖动导致误杀。客户端内置负载均衡通过 Ribbon / Spring Cloud LoadBalancer可以从注册中心拉取实例列表然后本地负载均衡。ConsulHashiCorp 开源属于CP 模型优先保证强一致性牺牲部分可用性。使用 Raft 一致性算法选举出 Leader 节点。读写由 Leader 处理其他 Follower 同步日志。如果 Leader 宕机集群会进入选举状态此时部分请求可能失败直到选出新 Leader。Consul 不仅是注册中心还提供健康检查、KV 存储、多数据中心、Service Mesh 等功能。支持 DNS 和 HTTP 接口服务发现不一定要引入 SDK。选型建议如果你需要一个高可用的注册中心且能容忍短暂的不一致可以用 Eureka 或 NacosNacos 同时支持 AP/CP。如果对数据一致性要求很高比如配置发布类场景可以用 Consul。国内很多互联网公司采用 Nacos因为它在 Spring Cloud Alibaba 生态中非常流行同时支持注册中心和配置中心。2. OpenFeign 超时配置与 Resilience4j 熔断降级业务场景订单服务调用用户服务获取收货地址。用户服务负载较高响应变慢。如果订单服务没有设置超时和熔断会导致线程池被占用最终系统崩溃这就是“雪崩效应”。OpenFeign 超时配置在application.yml中可以配置连接超时和读取超时feign: client: config: default: connectTimeout: 2000 readTimeout: 3000connectTimeout建立 TCP 连接的最长等待时间。readTimeout服务端响应数据的最长等待时间。如果同时使用了 RibbonSpring Cloud 旧版还需要配置 Ribbon 的超时取二者较小值。Resilience4j 核心组件CircuitBreaker熔断器状态有 CLOSED关闭正常调用、OPEN打开直接拒绝、HALF_OPEN半开试探性放行少量请求。当失败率达到阈值时熔断器打开快速失败不请求下游服务。RateLimiter限流器限制每秒请求数。Bulkhead舱壁隔离限制某个服务的最大并发线程数或信号量防止一个下游服务拖垮整个系统。Retry重试在遇到某些异常时自动重试。Timelimiter调用超时控制。示例CircuitBreaker(name userService, fallbackMethod getUserFallback) public User getUser(Long userId) { return userClient.getUser(userId); } public User getUserFallback(Long userId, Throwable throwable) { return new User(); // 默认值 }配合使用时注意超时时间不宜过长否则请求堆积。重试次数不宜过多否则会放大流量。熔断的失败率阈值和滑动窗口大小要根据业务压测结果动态调整。熔断降级不能仅仅是返回空数据还要有日志、监控Micrometer Prometheus Grafana并在恢复后自动半开试探。3. JWT 与 OAuth2 的区别与集成业务场景电商系统有两个场景用户使用手机号验证码登录 APP随后请求 API。需要一种令牌机制让服务端识别用户身份。→ 使用JWT是常见方案。微信小程序/公众号授权登录需要用户授权后小程序拿到code后端再请求微信授权服务器换取openid和access_token。→ 使用OAuth2授权码模式。JWTJSON Web Token详解JWT 是一种开放的 Token 格式RFC 7519由三部分组成Header头部、Payload载荷、Signature签名。Header 常包含alg算法和typ类型。Payload 包含用户信息如userId、username、exp过期时间等。Signature 是对 Header 和 Payload 的签名防止内容被篡改。服务端不需要保存 Session所以 JWT 天然支持无状态适合水平扩展。JWT 的缺点无法主动吊销。一旦签发在过期前始终有效。解决增加 Redis 黑名单。Payload 不能放敏感信息因为 Base64 可以解码。Token 体积比普通 Session ID 大。OAuth2 详解OAuth2 是一个授权框架解决“第三方应用如何获得用户授权登录”的问题。核心角色Resource Owner用户Client第三方应用Authorization Server授权服务器Resource Server资源服务器常见模式授权码模式Authorization Code流程为 用户登录授权服务器 → 客户端拿到 code → 用 code 换 token → 通过 token 请求资源服务器。密码模式Password客户端直接拿用户名密码换 token仅适合信任应用。客户端凭证模式Client Credentials适合服务间调用。隐式模式Implicit已不推荐。OAuth2 的 Access Token 可以是一个不透明的字符串也可以是 JWT两者不冲突。在 Spring Security 中集成 JWT用户认证成功时生成 JWT返回给前端。前端每次请求在Authorization头带上Bearer token。后端写一个过滤器如OncePerRequestFilter提取 Authorization 头。解析 JWT校验签名和过期时间。从 JWT 中获取用户 ID。构建Authentication对象如UsernamePasswordAuthenticationToken存入SecurityContextHolder。再通过 Spring Security 的授权配置决定哪些路径需要认证。总结区别JWT 是一个令牌标准/格式OAuth2 是一个授权协议。OAuth2 可以使用 JWT 作为 Access Token 的载体也可以使用不透明字符串。通常 SSO、开放平台、第三方登录用 OAuth2前后端分离应用的单体系统内部认证用 JWT。4. 支付回调安全与幂等处理业务场景用户支付成功后微信/支付宝向商户服务器发送异步回调告知“支付成功”。商户收到回调后需要把订单状态从“待支付”改为“已支付”然后给用户发货。支付回调面临的风险攻击者伪造回调谎称支付成功。回调被网络窃听/篡改。支付系统因网络重试发送了多次重复回调。多个线程同时处理同一笔订单导致状态重复更新或竞争。安全方案HTTPS保证传输层的加密防止明文被截获。单向/双向证书商用支付平台通常要求商户上传证书进行双向 TLS 认证极大提高安全性。验签支付平台使用私钥对回调参数如appid、orderId、amount、tradeNo进行签名。商户使用支付平台公钥验签。验签失败直接拒绝。验签算法常见为 RSA/SHA256withRSA。校验业务参数商户订单号orderId必须是本系统产生的订单。回调金额amount必须等于订单金额。支付状态必须是“成功”。商户号/应用 ID 要匹配。防重放回调信息中包含时间戳超过一定时间范围拒绝。或者使用nonce随机数商户缓存已处理过的 nonce重复请求拒绝。更常见的方法是在处理逻辑上做幂等因为支付平台自身会重试。幂等处理与状态机订单状态设计为待支付 → 已支付 → 已发货 → 已完成同时存在已取消、退款中、已退款等状态。当支付回调到达时通过分布式锁或数据库锁锁住订单。查询当前订单状态。只有当前状态是“待支付”时才允许修改为“已支付”。如果已经是“已支付”说明重复回调直接返回成功给支付平台避免它再次重试。使用数据库乐观锁/版本号UPDATE t_order SET status已支付, versionversion1 WHERE order_id? AND version?。更新行数为 0 则说明状态已变化幂等成功。数据库唯一流水号支付回调里带有支付平台流水号tradeNo在t_pay_notify表中以tradeNo为唯一索引。重复回调插入时会冲突直接说明已经处理过。这也是一种幂等方案。最终建议流程1. HTTPS收单 2. 验签 3. 校验订单号和金额 4. 加锁分布式锁/乐观锁 5. 读取当前订单状态符合“待支付”才更新 6. 更新订单状态写入支付流水 7. 返回“SUCCESS”给支付平台结束语面试官让谢飞机“回去等通知”不是讽刺而是真实面试中常见的收尾话术。作为技术人不管基础是好是坏都要记住面试中简单问题要答得完整复杂问题要能讲清出边界和工程取舍。这篇文章涉及的Java SE8/11Spring Boot 自动配置MyBatis 占位符Flyway / HikariCPRedis 缓存穿透/缓存一致性Kafka 可靠性/顺序/幂等Eureka / ConsulOpenFeign / Resilience4jSpring Security / JWT / OAuth2支付回调安全正是互联网大厂 Java 求职者最常被问到的技术栈。建议读者照着这个场景自己模拟一遍把每一个问题都写成一张卡片不断追问“为什么”直到能像真正的架构师一样讲清楚为止。希望下次谢飞机再面试时已经所有细节都搞明白了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进