Knowledge note
正在加载知识笔记
正在加载知识笔记
Knowledge note
本文与
Questions.md的Q01至Q50一一对应。参考回答用于组织表达,不建议逐字背诵;面试时应结合自己实际阅读、调试和改造过的内容作答。
参考回答
这是一个拼团营销服务,位于商品展示、交易订单和支付系统之间。它提供营销试算、开团或参团锁单、支付后结算、成团通知、退款和超时补偿等能力,同时支持按渠道配置活动、按人群标签控制可见与参与范围。
技术上使用 Spring Boot、MyBatis、MySQL、Redis/Redisson 和 RabbitMQ,按 DDD 拆成 api、domain、infrastructure、trigger、app、types 六个模块。核心亮点不是页面或普通 CRUD,而是通过规则树、责任链和策略模式组织复杂规则,用 Redis 控制队伍名额,并通过本地消息表、MQ 和定时任务处理最终一致性。
考察重点:能否在两分钟内讲清业务目标、核心流程和技术亮点,而不是背技术栈。
常见追问与简答
回答提醒:不要声称项目已经承载百万 QPS、完成分库分表或使用了 Seata,当前代码没有这些实现。
参考回答
它解决的是“商品在特定渠道和活动下,哪些用户能以什么拼团价格购买,以及拼团队伍如何推进”的问题。它管理活动、优惠规则、队伍进度和营销订单,但不应该承担完整商品库存、支付扣款、物流履约等职责。
商品信息在当前示例中由本地 sku 表提供,生产环境可以通过商品端口查询;支付由外部系统完成,本项目只接收 outTradeNo 和支付时间进行结算;普通订单系统负责主订单生命周期,本项目维护的是拼团营销侧订单与队伍状态,并在成团或退款时通知上游。
考察重点:是否有系统边界和上下游意识。
常见追问与简答
回答提醒:要明确“队伍库存”和“商品库存”不是一回事。
参考回答
用户进入商品页后,前端调用首页营销接口。系统根据渠道、商品和活动配置并发加载活动与 SKU,执行降级、灰度、人群标签和优惠计算,返回拼团价、可参加的队伍及活动统计。
用户下单时调用锁单接口。teamId 为空就开新团,不为空就加入已有队伍;锁单前校验活动有效性、参与次数和队伍名额,成功后写队伍主单和用户明细。外部支付成功后调用结算接口,系统把明细改为已支付并增加队伍完成数。达到目标人数时更新成团状态、写 notify_task,再通过 HTTP 或 MQ 通知上游。
如果用户主动退款,或定时任务发现未支付订单超时,则进入退款责任链,根据订单和队伍状态选择退款策略。数据库状态更新后发布退款消息,由消费者幂等恢复 Redis 队伍名额。
考察重点:能否串起正向交易、异步通知和逆向退款。
常见追问与简答
回答提醒:成团依据是支付完成数,不是单纯锁单数。
参考回答
锁单请求中 teamId 为空表示开团。仓储层生成 8 位数字 teamId,插入一条 group_buy_order,初始 lock_count 为 1、complete_count 为 0,然后为发起人插入 group_buy_order_list。
teamId 不为空表示参团。规则链先用 Redis 抢占该队伍名额,数据库再通过 updateAddLockCount 对已有队伍增加锁单数;随后同样写用户订单明细。参团比开团多了队伍是否已满和并发名额抢占逻辑。
考察重点:是否理解一张队伍主表对应多条用户明细。
常见追问与简答
+1 把创建者算进去。RandomStringUtils.randomNumeric(8),生产中更适合全局唯一 ID 方案并处理碰撞。回答提醒:随机数字并不天然保证唯一,真正兜底依赖 team_id 唯一键和异常处理。
source、channel、goodsId、activityId、teamId、outTradeNo 分别表示什么参考回答
source 和 channel 共同描述业务来源与渠道,用于匹配 sc_sku_activity、做灰度或黑名单控制;goodsId 标识商品;activityId 标识拼团活动;teamId 标识具体拼团队伍;outTradeNo 是上游交易系统传入的外部单号,用来关联支付、退款并承担幂等键的业务意图。
这些 ID 分属不同边界:商品、活动、队伍、营销明细和外部交易不能混用。一个活动可以有多个队伍,一个队伍可以有多条用户订单明细。
考察重点:是否理解关键标识及表之间的基数关系。
常见追问与简答
orderId 还要 outTradeNo?答:orderId 是营销系统内部单号,outTradeNo 用于和上游订单或支付系统对账、幂等。回答提醒:源码注释中 source/channel 的中文表述有交叉,面试时应强调它们组合成渠道维度即可。
参考回答
用户明细 group_buy_order_list 的状态由 TradeOrderStatusEnumVO 表示:CREATE(0) 初始锁定、COMPLETE(1) 支付完成、CLOSE(2) 用户退单。队伍主单 group_buy_order 由 GroupBuyOrderEnumVO 表示:PROGRESS(0) 拼单中、COMPLETE(1) 完成、FAIL(2) 失败、COMPLETE_FAIL(3) 已完成但含退单。
两套状态必须分开,因为队伍和成员不是同一个生命周期。某个用户可能已支付,但队伍仍在拼单;也可能队伍已经成团,之后某个成员退款,队伍变为“完成含退单”。
考察重点:能否区分成员状态和聚合状态。
常见追问与简答
lock_count 和 complete_count 有何区别?答:前者统计已占名额的锁单,后者统计已完成支付的成员。COMPLETE_FAIL?答:保留“曾经成团但发生退款”的业务事实,便于后续履约和审计判断。回答提醒:不要把 group_buy_order.status=1 说成某一位用户支付成功,它表示整个队伍完成。
参考回答
api 定义对外契约和 DTO;trigger 接收 HTTP、定时任务、MQ 等输入并调用领域服务;domain 保存活动、交易、标签等核心模型和规则;infrastructure 实现数据库、Redis、MQ、HTTP 等技术细节;app 负责 Spring Boot 启动、配置和资源装配;types 放公共枚举、异常和常量。
这样拆分的价值是让业务规则不被 Web、MyBatis 或 MQ 绑死。新增一种触发方式主要改 trigger,替换持久化方式主要改 infrastructure,领域逻辑可以保持稳定。
考察重点:能否讲出模块职责和变化隔离,而不是只背 DDD 名词。
常见追问与简答
回答提醒:多模块本身不等于 DDD,关键还要看领域模型和依赖方向。
参考回答
以锁单为例,请求进入 MarketTradeController,Controller 把 DTO 转成 UserEntity、PayActivityEntity、PayDiscountEntity 后调用 ITradeLockOrderService。TradeLockOrderService 执行规则链并组装 GroupBuyOrderAggregate,然后通过 domain 中的 ITradeRepository 调用仓储。
Spring 注入 infrastructure 中的 TradeRepository 实现,它把领域对象转换为 GroupBuyOrder、GroupBuyOrderList PO,再调用 MyBatis DAO。DAO 方法由 app 模块资源目录下的 Mapper XML 执行 SQL,最终访问 MySQL。
考察重点:是否真正沿源码走过一条调用链。
常见追问与简答
回答提醒:回答时最好选一条具体链路,不要只说 Controller-Service-DAO 三层套话。
参考回答
领域层需要表达“我要查询活动、锁定订单、结算队伍”,但不应该知道这些能力由 MyBatis、Redis 还是远程服务实现。因此 domain 定义 IActivityRepository、ITradeRepository、ITagRepository 等业务语义接口,infrastructure 提供实现。
依赖方向因此从 infrastructure 指向 domain,符合依赖倒置。领域服务可针对接口测试,未来更换数据库或把 SKU 查询改成 RPC 时,不必改核心规则。
考察重点:依赖倒置和端口适配器思想。
常见追问与简答
ITradePort 又是什么?答:它抽象领域向外部系统发送通知的出口,具体 HTTP/MQ 选择由 infrastructure 的 TradePort 实现。回答提醒:不要把 Repository 简化成对每张表做一套 CRUD。
参考回答
DTO 是接口边界的数据结构,例如 LockMarketPayOrderRequestDTO;领域实体有业务身份和生命周期,例如 GroupBuyTeamEntity;值对象用属性表达概念且通常无独立身份,例如 NotifyConfigVO、GroupBuyProgressVO;聚合对象把一次业务操作中需要保持一致的实体和值对象组合起来,例如 GroupBuyOrderAggregate;PO 与数据库表字段对应,例如 GroupBuyOrder、GroupBuyOrderList。
分开这些对象可以避免接口字段、业务模型和表结构互相污染,也便于在边界处做校验和转换。
考察重点:对象建模能力与边界意识。
常见追问与简答
回答提醒:不要因为类名带 Entity 就机械判断,重点看它在业务中的身份和职责。
参考回答
交易域中主要有 GroupBuyOrderAggregate、GroupBuyTeamSettlementAggregate 和 GroupBuyRefundAggregate。它们分别承载锁单、支付结算和退款时需要一起处理的数据,例如用户、活动、优惠、队伍、支付或退款命令。
聚合定义业务一致性边界,Repository 以聚合为输入执行事务。例如锁单要在一个事务里创建或更新队伍,并插入用户明细;结算要更新明细、队伍计数,并在成团时写通知任务。聚合并不等于一张表,而是一次业务操作中必须保持规则一致的一组对象。
考察重点:聚合与事务一致性的关系。
常见追问与简答
notify_task,事务后再异步发送,形成最终一致性。回答提醒:不要说“一个聚合必须对应一张表”或“所有关联对象都应放入同一事务”。
参考回答
它们都是驱动业务执行的输入适配器,只是触发方式不同:Controller 响应同步请求,Job 响应时间事件,Listener 响应消息事件。trigger 负责协议解析、参数转换、异常映射和调用领域服务,不承载核心业务规则。
统一放在 trigger 可以让 domain 不依赖 Spring MVC、@Scheduled 或 RabbitMQ 注解,也便于以后增加 RPC、命令行或其他消息入口。
考察重点:输入适配器的统一抽象。
常见追问与简答
restoreTeamLockStock 完成业务。回答提醒:trigger 不是传统意义上只包含 Controller 的“表现层”。
参考回答
types 放跨模块共享的 Constants、ResponseCode、业务枚举、AppException 和事件基类,避免各模块重复定义。它应尽量稳定、轻量,不包含具体领域流程。
当前 types 的 POM 依赖了 spring-boot-starter-web 等较重依赖,公共模块容易被框架污染;部分枚举是交易域专属,也可以下沉到对应 domain 子域;MyBatis XML 当前放在 app,归入 infrastructure 会让资源归属更清晰。改进时要保持 api 契约兼容,并避免为了“纯洁”制造过多小模块。
考察重点:能否评价架构,而不是无条件赞美。
常见追问与简答
回答提醒:架构改进要说明收益和成本,不能只追求模块数量。
query_group_buy_market_config 接口内部经历了哪些步骤参考回答
请求先进入 MarketIndexController,限流注解按 userId 控制访问频率,Controller 再校验用户、来源、渠道和商品 ID。随后构造 MarketProductEntity 调用 indexMarketTrial,由规则树完成活动匹配、动态开关、人群和优惠试算。
得到试算结果后,Controller 根据 activityId 查询用户自己正在参与的队伍和随机可参与队伍,再统计活动的队伍数、成团数和参与人数,最后组装 GoodsMarketResponseDTO 返回商品价格、活动、队伍列表和统计信息。
考察重点:是否知道接口不仅计算价格,还查询队伍与统计数据。
常见追问与简答
queryGroupBuyMarketConfigFallBack,返回统一的限流响应码。回答提醒:不要只描述 Controller,要继续讲到领域规则树和仓储查询。
参考回答
RootNode 做入口参数校验;SwitchNode 读取 DCC,判断系统是否降级以及用户是否在灰度切量范围;MarketNode 并发加载活动优惠和 SKU,再按 marketPlan 选择优惠策略;TagNode 判断活动标签范围,设置是否可见和是否可参与;EndNode 把上下文中的活动、商品、优惠金额和权限组装成 TrialBalanceEntity。
当活动、商品或优惠数据缺失时,MarketNode 会路由到 ErrorNode,由它抛出无营销配置等业务异常。节点之间通过 DynamicContext 共享已加载数据和计算结果。
考察重点:节点职责是否单一,以及上下文如何在链路中传递。
常见追问与简答
MarketNode,因为它负责并发加载和优惠策略选择。DynamicContext?答:避免每个节点重复查询,也避免不断扩大方法参数。回答提醒:这条链更接近可路由的规则树,不只是固定 if-else 顺序链。
参考回答
营销规则通常会持续增加,例如开关、渠道、标签、会员等级、库存和优惠叠加。如果全部写进一个 Service,会形成大量嵌套判断,难以单测和调整顺序。拆成节点后,每个节点只处理一个规则,并根据上下文决定下一节点或异常节点。
新增规则时可以新增节点并调整工厂装配,而无需重写整个试算方法。代价是调用链更分散,调试时需要良好的日志、链路 ID 和节点级测试。
考察重点:是否理解模式解决的变化问题及其成本。
常见追问与简答
回答提醒:不要回答“用了设计模式所以代码高级”,要说明规则变化和可测试性。
参考回答
活动配置和 SKU 查询互不依赖,串行耗时约等于两次查询耗时之和,并行后接近较慢的一次。MarketNode 使用自定义线程池提交两个 FutureTask,分别查询活动优惠和 SKU,并以 5 秒超时读取结果,再写入 DynamicContext。
风险包括:Future#get 仍会阻塞请求线程;一个任务失败或超时时,另一个任务没有显式取消;线程池和数据库连接池容量若不匹配,可能把一次请求放大成多个排队任务;异常和超时也需要统一监控。仓库还提供了未启用的 MarketNode2CompletableFuture 示例,但当前生效的是 FutureTask 版本。
考察重点:并发查询的延迟收益、资源成本和超时治理。
常见追问与简答
CompletableFuture 编排或批量接口。回答提醒:异步不等于不阻塞,当前请求最终仍要等待两个结果。
参考回答
四个优惠实现都实现 IDiscountCalculateService,并通过 @Service("ZJ")、@Service("MJ") 等名称注册。Spring 将它们注入为 Map<String, IDiscountCalculateService>,MarketNode 用数据库中的 marketPlan 作为 key 选择策略。
新增一种优惠时,应新增实现类,最好继承 AbstractDiscountCalculateService 复用标签过滤,使用新的稳定编码注册 Bean,并补充表达式校验、单元测试和数据库配置。这样无需修改 MarketNode 的分支代码,符合开闭原则。
考察重点:策略注册、选择和扩展过程。
常见追问与简答
回答提醒:策略模式消除了算法分支,但不能省略配置校验和异常处理。
参考回答
ZJ 是直减,用原价减表达式金额,并保证最低支付 0.01;MJ 把表达式解析为 X,Y,原价满 X 才减 Y;ZK 用原价乘折扣比例;N 直接把表达式作为最终支付价。标签型优惠会先在抽象父类中判断用户是否属于指定人群,不命中则返回原价。
金额使用 BigDecimal 是为了避免 double 的二进制浮点误差。构造时应使用字符串或 BigDecimal.valueOf,并明确精度和舍入规则。当前 ZK 使用 setScale(0, RoundingMode.DOWN) 会舍弃小数位,若金额单位是元,这个规则值得重新确认。
考察重点:优惠算法、边界值和金额精度意识。
常见追问与简答
回答提醒:不要只说 BigDecimal 精确,还要提构造方式、scale 和 rounding mode。
visible 与 enable 有什么区别参考回答
人群限制有两层。优惠策略的 discountType=TAG 时,抽象优惠服务根据优惠 tagId 判断用户是否在 BitSet 中,不命中就不给优惠;活动本身的 tagId/tagScope 由 TagNode 处理,用于控制页面是否展示以及是否允许锁单。
visible 表示活动是否对用户可见,enable 表示即使看见是否能参与。tagScope 中 1 表示可见限制,2 表示参与限制;用户命中标签后可解除对应限制。若 Redis 中该标签 BitSet 不存在,当前仓储实现默认返回 true,这是一种偏可用性的放行策略,也可能带来权限放大的风险。
考察重点:优惠资格、活动展示和交易资格的区别。
常见追问与简答
回答提醒:要指出当前“不存在即放行”是实现选择,不是所有系统的标准答案。
参考回答
限流控制单位时间请求量,保护线程池、数据库和 Redis;降级开关在系统或下游异常时直接停止营销能力;灰度切量通过 userId hash 后两位和 cutRange 比较,只让一部分用户进入新链路。三者分别解决容量保护、故障止损和渐进发布。
首页接口使用 @RateLimiterAccessInterceptor 按用户限速,SwitchNode 再检查 downgradeSwitch 和 cutRange。DCC 通过 Redis topic 动态发布配置,因此可以不重启应用调整开关。合理顺序通常是入口先限流,再做全局降级和灰度判断,最后进入昂贵业务计算。
考察重点:稳定性手段的不同目标与执行顺序。
常见追问与简答
回答提醒:限流、熔断、降级不是同一个概念,当前项目主要展示限流和动态开关。
lock_market_pay_order 的锁单流程参考回答
Controller 先读取用户、渠道、商品、活动、外部单号、队伍和通知配置并校验。随后按 userId + outTradeNo 查询已有营销订单,若已存在初始订单则直接返回;参团时还会先查队伍进度,已满则拦截。
接着重新执行营销试算,确认价格和人群可见、参与资格,再构造用户、活动、优惠实体调用 TradeLockOrderService。领域服务执行活动可用性、参与次数、队伍库存占用三段责任链,组装 GroupBuyOrderAggregate。仓储事务内创建或更新队伍,并插入用户订单明细;若数据库失败,服务记录 Redis 恢复量,最后返回订单与价格信息。
考察重点:幂等检查、规则校验、Redis 名额和数据库事务的先后关系。
常见追问与简答
回答提醒:当前 Controller 对空 notifyConfigVO 的处理不够安全,回答时可主动指出。
outTradeNo 实现幂等?当前方案在并发下是否绝对可靠参考回答
接口先通过 userId + outTradeNo 查询 group_buy_order_list。若发现状态为 CREATE 的记录,就直接返回原订单结果,避免客户端重试重复锁单。支付结算更新 SQL 也带 status=0 条件,使重复结算不能再次更新。
但“先查后写”不是并发原子操作。当前初始化 SQL 没有给 out_trade_no 或 (user_id,out_trade_no) 建唯一索引,两个并发请求可能都查不到后再分别插入。因此不能说绝对可靠。生产上应增加符合分片路由的唯一约束,采用插入竞争或幂等记录,并把重复键转换为查询已有结果。
考察重点:应用层幂等与数据库强约束的区别。
常见追问与简答
outTradeNo,还要兼顾未来分库分表路由键。回答提醒:表字段注释写“唯一幂等键”不代表数据库已经建立唯一索引。
参考回答
ActivityUsabilityRuleFilter 查询活动并校验生效状态和起止时间,同时把活动写入上下文;UserTakeLimitRuleFilter 查询用户在活动中的参与次数,与 takeLimitCount 比较;TeamStockOccupyRuleFilter 只在参团时抢占 Redis 队伍名额,并返回失败恢复 key。
顺序上,后续节点依赖前面加载的活动和计数;同时先做活动、次数等确定性校验,再做会改变 Redis 状态的库存抢占,可以减少无效占用和补偿操作。
考察重点:节点依赖、校验成本和副作用顺序。
常见追问与简答
teamId 为空时直接返回,不抢占已有队伍库存。回答提醒:责任链顺序属于业务正确性的一部分,不只是代码排版。
参考回答
队伍 key 格式为 group_buy_market_team_stock_key_{activityId}_{teamId}。抢占时先读取 recovery key 的恢复量,然后对库存 key 原子 incr,并加 1 把开团用户算入队伍。若结果超过 target + recoveryCount,立即 decr 并返回失败。
未超限时,再为本次序号创建 teamStockKey_{occupy} 的 SET NX 锁,并设置为活动有效时间加 60 分钟。这个二级 key 用于避免同一占位序号被重复接受。成功后才进入数据库锁单。
考察重点:原子计数、目标比较、二级防重 key 和 TTL。
常见追问与简答
incr 原子是否代表整个算法原子?答:不是,读取恢复量、incr、比较、decr、setNx 是多条命令,存在中间失败窗口。回答提醒:不要把当前实现说成完整的 Redis Lua 原子扣减,它没有使用 Lua。
参考回答
TradeLockOrderService 在规则链成功后调用仓储锁单。如果数据库操作抛异常,会调用 recoveryTeamStock(recoveryTeamStockKey, validTime),对恢复 key 自增。后续抢占计算允许上限为 target + recoveryCount,等价于把失败占用释放出来。
这种方式没有直接修改原抢占计数,而是用补偿量修正可用额度,降低并发回滚时对主计数的干扰。不过它依赖恢复 key 与主 key 生命周期一致、补偿不重复,并需要监控两个计数的差异。当前 validTime 参数在恢复方法中没有实际设置过期,也值得完善。
考察重点:补偿思路和失败窗口分析。
常见追问与简答
decr?答:直接回退在复杂并发和重试下可能误减别人的占位,补偿量更容易保留操作事实,但实现也必须幂等。teamId,当前代码不会生成 recovery key,因此不走该补偿。回答提醒:这是一种补偿设计,不等于 Redis 和 MySQL 已经组成强一致事务。
参考回答
Redis 位于前置高并发路径,用原子自增快速筛掉超过目标人数的请求,减少数据库压力。数据库执行 update group_buy_order set lock_count = lock_count + 1 where team_id = ? and lock_count < target_count,更新行数不是 1 就认为队伍已满。
因此 Redis 是性能层和第一道闸门,数据库条件更新是最终数据层防线。即使 Redis 数据丢失或短暂不一致,数据库也不应让 lock_count 超过 target_count。两个层次之后还要保证用户明细插入与队伍计数在同一事务中。
考察重点:缓存防并发和数据库最终约束的分工。
常见追问与简答
回答提醒:当前数据库条件更新保护的是队伍计数,不代表所有幂等问题都已解决。
参考回答
当前 SQL 对 group_buy_order.team_id、group_buy_order_list.order_id 建了唯一索引,并对 (user_id,activity_id) 建了普通索引。代码生成了 biz_id=activityId_userId_takeCount,字段注释也把 out_trade_no 当作幂等键,但初始化 SQL 没有给 biz_id 或 out_trade_no 建唯一索引。
因此随机 orderId 冲突能被唯一键发现,但用户并发参与次数和外部单号并发重复写仍有缺口。建议增加 (user_id,out_trade_no) 唯一键以及符合业务规则的 biz_id 唯一键;notify_task.uuid 当前索引名叫 uq_uuid,但 SQL 实际只是普通 KEY,也应改为真正 UNIQUE。
考察重点:会不会核对真实 DDL,而不是只相信注释和异常捕获。
常见追问与简答
(user_id,activity_id) 普通索引仍有用?答:参与次数和活动内用户订单查询可以利用前缀匹配,但它不能防重复。回答提醒:DuplicateKeyException 只有命中真实唯一约束才会发生。
参考回答
一次锁单至少改变队伍主单和用户明细。开团时要同时插入 group_buy_order 与 group_buy_order_list;参团时要同时增加队伍 lock_count 与插入明细。如果一项成功另一项失败,队伍统计和实际成员会不一致,因此 TradeRepository#lockMarketPayOrder 使用数据库事务。
条件更新把业务前置条件放进 SQL,例如 lock_count < target_count,可避免“先查未满、并发后再更新”造成超员。事务只覆盖 MySQL,Redis 抢占发生在事务外,所以失败时还需要补偿,而不能误认为一个 @Transactional 包住了全部资源。
考察重点:数据库原子性、条件更新和跨资源一致性。
常见追问与简答
回答提醒:Spring 本地事务不能自动回滚 Redis 操作或已发送 MQ 消息。
参考回答
先通过压测观察接口 P99、Redis 热 key、数据库热点行锁、线程池队列、连接池等待和失败补偿量。当前方案已有入口限流、Redis 前置抢占和数据库条件更新,但 Redis 算法由多条命令组成,且每个成功请求仍会竞争同一队伍数据库行。
改进顺序可以是:用 Lua 原子完成名额判断和占位;补齐外部单号与业务 ID 唯一索引;按活动或队伍做限流和排队;将活动/SKU 静态数据稳定缓存;对超热点队伍采用消息串行化或分段名额方案;建立 Redis 与 MySQL 对账补偿。只有量级足够时才考虑拆库或更复杂架构。
考察重点:能否从正确性、容量、热点和可观测性分层改进。
常见追问与简答
回答提醒:回答应给出渐进方案,不要一上来就说分库分表、微服务拆分。
settlement_market_pay_order 的支付结算流程参考回答
上游支付成功后传入来源、渠道、用户、外部单号和支付时间。Controller 校验后构造 TradePaySuccessEntity,调用 TradeSettlementOrderService。服务先执行结算责任链,检查渠道黑名单、外部单号、订单状态和支付有效期,并得到队伍上下文。
仓储事务内把用户明细从 CREATE 更新为 COMPLETE,增加队伍 complete_count。如果判断本次支付使队伍达到目标人数,就把队伍状态改为完成,查询所有已支付外部单号,并在同一事务写入 notify_task。事务完成后,线程池立即尝试投递通知;失败任务由定时任务补偿。
考察重点:支付幂等、成团判断、事务内消息记录和事务后投递。
常见追问与简答
status=0,第二次更新行为 0 并抛业务异常,不会重复增加完成数。回答提醒:本项目只处理支付结果,不执行真实扣款。
参考回答
SCRuleFilter 根据 DCC 的 scBlacklist 拦截已下线的来源渠道;OutTradeNoRuleFilter 查询营销订单,拒绝不存在或已关闭的外部单号,并把订单放入上下文;SettableRuleFilter 根据订单的 teamId 查询队伍,校验支付时间早于队伍有效结束时间;EndRuleFilter 把队伍 ID、活动、目标数、完成数、状态和通知配置封装为返回对象。
这条链从入口策略、订单合法性、时间合法性到数据组装逐步推进,后续节点依赖前面加载的数据。
考察重点:每个节点的输入、输出和顺序依赖。
常见追问与简答
status=0 的更新会失败,从数据层保证不能重复推进。回答提醒:规则链校验和数据库条件更新共同保证结算,不能只讲其中一层。
参考回答
仓储先拿到结算前查询的 GroupBuyTeamEntity,完成明细更新和 complete_count + 1 后,用 targetCount - completeCount == 1 判断本次是否为最后一人。如果是,就条件更新队伍状态为完成并写通知任务。
风险在于 completeCount 是事务前读取的快照。比如目标 3 人、当前完成 1 人,两个成员并发结算都看到 1,各自判断差值为 2,于是都不进入成团分支,但数据库最后可能已经累加到 3。代码注释也承认这个问题。可通过队伍行锁、按 teamId 分布式串行、更新后重新读取,或一条条件 SQL 在 complete_count=target_count 时竞争更新成团状态,并配套补偿扫描解决。
考察重点:是否能构造具体并发时序,而不只说“加锁”。
常见追问与简答
synchronized 不够?答:多实例部署时只能约束单 JVM,不能保护跨实例并发。回答提醒:当前代码并没有完整解决这处并发风险,要如实说明。
notify_task,而不是在数据库事务里直接调用外部接口参考回答
外部 HTTP 或 MQ 与 MySQL 不在同一个本地事务中。如果在事务内直接调用,外部调用慢会拉长数据库锁时间;外部成功但数据库回滚,或数据库提交后应用宕机,都可能造成两边不一致。
项目在成团事务内写 notify_task,保证“业务状态完成”和“存在待通知记录”原子提交。事务后立即投递只是提高时效,失败后仍可从任务表扫描重试,这就是本地消息表的最终一致性方案。
考察重点:跨资源事务的失败窗口和本地消息表原理。
常见追问与简答
回答提醒:最终一致性不是没有一致性,而是允许短暂延迟并通过重试收敛。
参考回答
开团锁单时把 notifyType 和 HTTP URL 保存到队伍主单;MQ routing key 使用系统配置。成团或退款时生成 NotifyTaskEntity,TradePort 先按任务 uuid 获取 Redisson 锁,HTTP 类型调用 GroupBuyNotifyService,MQ 类型通过 RabbitTemplate 发布消息。
TradeTaskService 根据结果更新任务状态:成功为 1,失败时先标记重试 2,次数超过阈值后标记失败 3。GroupBuyNotifyJob 每天扫描状态 0、2 的任务,每次最多 50 条继续执行。当前没有指数退避、下一次执行时间、发布确认或死信队列的完整配置,生产上应补齐。
考察重点:通知路由、抢占执行、状态机和补偿机制。
常见追问与简答
回答提醒:代码只显式设置了消息 delivery mode 为 persistent,不能夸大为完整可靠消息方案。
参考回答
退款请求进入 TradeRefundOrderService 后执行 TradeRefundRuleFilterFactory 组装的链。DataNodeFilter 按用户和外部单号加载营销订单,再按 teamId 加载队伍,写入动态上下文;UniqueRefundNodeFilter 发现明细已经是 CLOSE 时直接返回重复退款结果;RefundOrderNodeFilter 根据队伍状态和用户订单状态选择 IRefundOrderStrategy 并执行。
责任链把通用步骤和状态分派拆开,具体策略只处理对应的数据库变更和退款通知。数据库更新 SQL 同样带原状态条件,避免重复推进。
考察重点:退款幂等、上下文传递和策略分派。
常见追问与简答
DataNodeFilter 缺少明确空值校验,可能空指针;应转换为可识别的订单不存在业务码。RefundTypeEnumVO 会抛出不支持的状态组合异常。回答提醒:应用层重复判断和数据库条件更新都需要,不能只靠一次查询。
参考回答
未支付未成团对应队伍 PROGRESS、明细 CREATE,unpaid2RefundStrategy 把明细关闭并减少队伍 lock_count;已支付未成团对应 PROGRESS + COMPLETE,paid2RefundStrategy 同时减少 lock_count 和 complete_count;已支付已成团对应队伍 COMPLETE/COMPLETE_FAIL + COMPLETE,paidTeam2RefundStrategy 减少两个计数,并根据是否还剩已支付成员把队伍改成 COMPLETE_FAIL 或 FAIL。
前两类退款发送 MQ 后,消费者恢复 Redis 参团名额;已成团退款策略认为队伍阶段已经结束,不再恢复可继续参团的 Redis 名额。三类策略都通过本地通知任务记录后续消息。
考察重点:状态组合、计数变化和是否恢复名额。
常见追问与简答
回答提醒:退款策略处理的是营销状态和通知,不代表已调用真实支付渠道退钱。
参考回答
TimeoutRefundJob 每分钟获取 Redisson 分布式锁,查询状态为 0、没有支付时间且当前时间超过结束时间的明细,逐条构造退款命令并复用正常退款服务。数据库事务关闭订单、减少队伍计数并写退款 MQ 任务。
任务投递后,RefundSuccessTopicListener 消费退款消息,解析 TeamRefundSuccess,再由对应退款策略执行 reverseStock。未成团退款通过 recovery key 恢复名额,并用 refund_lock_{orderId} 防止 MQ 重复消费造成重复恢复;处理失败抛异常,让消息具备重试机会。
考察重点:扫描补偿、复用领域逻辑、MQ 至少一次和消费幂等。
常见追问与简答
回答提醒:最终一致性收敛依赖任务表、MQ 重试和幂等三者,不是只靠定时任务。
参考回答
配置侧有 sku 商品表、sc_sku_activity 渠道商品活动关联、group_buy_activity 活动表和 group_buy_discount 优惠表。标签侧有 crowd_tags、crowd_tags_detail 和 crowd_tags_job。
交易侧 group_buy_order 是队伍主单,一个 team_id 对应一个队伍;group_buy_order_list 是成员明细,一个队伍对应多条用户订单。成团或退款产生 notify_task,通过 team_id、activity_id 和 uuid 关联业务事件。表间主要由业务 ID 关联,DDL 没有显式外键,便于写入和拆分,但一致性需要应用和对账任务维护。
考察重点:配置、标签和交易三组表,以及一对多关系。
常见追问与简答
回答提醒:没有外键不代表没有数据关系。
group_buy_order 与 group_buy_order_list 为什么不能合成一张表参考回答
队伍主单保存目标人数、锁单数、支付完成数、队伍状态、有效期和通知配置,这些属性对整个 team 只有一份。成员明细保存用户、内部订单号、外部单号、商品、成交价格和个人状态,一个队伍有多条。
如果合成一张表,队伍统计和配置会在每个成员行重复,更新成团状态需要修改多行,也难以通过一行条件更新保护队伍计数。拆分后主表承担队伍聚合和并发控制,明细表承担成员生命周期与对账。
考察重点:数据范式、聚合建模和并发更新。
常见追问与简答
回答提醒:拆表不仅是“数据量大”,更重要的是生命周期和更新频率不同。
参考回答
值得肯定的有:商品、活动、优惠、队伍等业务 ID 使用唯一索引;sc_sku_activity 对 (source,channel,goods_id) 建联合唯一索引,符合查询条件;明细表对 (user_id,activity_id) 建索引,服务参与次数查询。
改进点包括:out_trade_no、biz_id 缺少唯一约束;notify_task.uuid 的索引名称虽为 uq_uuid,实际不是 UNIQUE;待通知查询应考虑 (notify_status, next_notify_time) 索引;超时订单扫描应围绕 (status,out_trade_time,end_time) 设计索引;常用的 team_id 明细查询也应确认索引。新增索引前要结合基数、执行计划和写放大评估。
考察重点:能否从真实 SQL、查询条件和幂等语义分析索引。
常见追问与简答
回答提醒:不要脱离 Mapper SQL只背最左匹配原则。
参考回答
Redis 用于活动/优惠缓存、DCC 动态配置发布与注册、队伍名额原子计数、失败恢复量、通知和定时任务分布式锁、退款恢复幂等,以及人群标签 BitSet。它既是缓存,也是并发协调和配置基础设施。
AbstractRepository#getFromCacheOrDb 使用 Cache-Aside:先读缓存,未命中则查数据库并回填;DCC 的 cacheSwitch 可动态关闭缓存。当前实现没有完整处理空值缓存、热点互斥加载、随机 TTL 和配置更新后的删缓存流程,存在穿透、击穿和旧数据风险。
考察重点:不同 Redis 数据结构与使用场景,以及 Cache-Aside 的一致性问题。
常见追问与简答
回答提醒:Redis 在这里不只是缓存,不能用一套缓存降级结论覆盖所有用途。
参考回答
BitSet 用一个 bit 表示用户是否属于标签,判断只需读取一个位,空间和查询效率通常优于存储大量字符串集合。标签任务把用户写入明细表,同时用 MD5(userId) mod Integer.MAX_VALUE 得到 bit 下标;试算时用相同算法查询。
风险是哈希取模可能碰撞,两个用户映射到同一位会误判;下标上限接近 21 亿,Redis 位图会按最高位扩容,稀疏写入可能占用很大空间;标签删除和重建也需治理。生产中可按稳定数值用户 ID 分片位图、使用 RoaringBitmap,或根据是否允许误判选择 Bloom Filter,并做版本化重建。
考察重点:位图复杂度、哈希碰撞和稀疏空间问题。
常见追问与简答
回答提醒:BitSet 不是用户数多少就只占多少 bit,最高偏移决定底层空间。
参考回答
配置中使用 topic exchange group_buy_market_exchange,成团 routing key 为 topic.team_success,退款为 topic.team_refund,分别绑定对应队列。两个 Listener 通过 @RabbitListener、@QueueBinding 声明 exchange、queue 和 key;发送端 EventPublisher 用 RabbitTemplate.convertAndSend 并把消息 delivery mode 设为 persistent。
持久消息只是可靠性的一个环节。还应确保交换机和队列持久化,配置 publisher confirm/return,消费者手动或正确自动 ACK,设置重试、死信队列和告警。开发配置还要使用 AMQP 端口 5672,15672 是管理后台端口。
考察重点:RabbitMQ 路由关系和完整可靠性链路。
常见追问与简答
* 和 #,比 direct 更适合主题分类扩展。回答提醒:不要把 RabbitMQ 管理端口 15672 配成应用连接端口。
参考回答
应用多实例部署时,每个实例都会触发 @Scheduled。GroupBuyNotifyJob 和 TimeoutRefundJob 通过固定 Redisson key 抢锁,只让一个实例执行本轮任务,避免同一批数据被同时扫描和处理。
当前通知任务每天零点执行一次且只查 50 条,补偿时效和积压处理能力有限;超时退款每分钟逐条串行处理,大批积压时单轮可能超过锁租期;任务没有分片、游标、稳定排序、失败隔离和执行进度。生产上可增加 next_execute_time、分页抢占状态、分片调度、单条幂等和任务监控,或使用成熟调度平台。
考察重点:多实例调度、锁租期、积压和幂等。
常见追问与简答
回答提醒:分布式锁只解决同一时刻的竞争,不负责失败恢复和任务不丢。
参考回答
开发配置中核心线程 20、最大线程 50、队列 5000,拒绝策略为 CallerRunsPolicy。该线程池既用于 MarketNode 并发加载,也用于结算和退款后的即时通知。大队列能缓冲突发任务,但会让延迟和内存占用变得隐蔽;CallerRunsPolicy 会让提交任务的 HTTP 或结算线程自己执行,形成反压,同时也会拉长接口响应。
生产上应按任务性质拆分线程池:营销查询是请求关键路径,通知是可重试异步任务,隔离后不会互相拖垮。还应设置有业务含义的线程名、MDC/TraceId 传递、队列和拒绝次数指标、优雅关闭,并让线程数与数据库连接池、下游 QPS 一起容量规划。
考察重点:线程数、队列、拒绝策略、资源隔离和可观测性。
常见追问与简答
回答提醒:线程池参数没有通用最优值,必须结合任务类型、耗时和下游容量压测。
参考回答
第一是数据幂等约束:out_trade_no、biz_id 和 notify_task.uuid 没有真正的唯一索引,应用层“先查后写”在并发下可能重复。第二是结算成团判断使用更新前的 completeCount,两个成员并发支付可能都不触发成团,需要原子状态竞争或补偿扫描。
第三是退款库存恢复锁的过期时间写成 30 * 24 * 60 * 60 * 1000L,却传入 TimeUnit.MINUTES,实际远超注释中的 30 天,应改为 30, TimeUnit.DAYS 或等价正确单位。之后再处理通知任务每天只扫 50 条、缺少发布确认/DLQ、空通知配置可能 NPE,以及开发环境 RabbitMQ 端口使用 15672 等问题。
考察重点:能否给出源码证据、正确性影响和修复优先级。
常见追问与简答
回答提醒:代码评审应指出具体位置和影响,不要只说“代码不规范、日志不完善”。
参考回答
先区分读多的营销试算和写多的锁单。试算侧把活动、优惠、SKU 和标签结果预热到 Redis,本地缓存承接热点只读配置,设置版本和主动失效;入口按活动、用户、渠道做限流,静态展示数据可进一步前置。线程池、连接池和下游都按压测结果设置容量与隔离。
锁单侧用 Lua 原子抢占名额,幂等唯一键做数据库兜底;对极热点队伍可按 teamId 进入有序消息或单写者模型,避免热点行竞争。数据量增长后再按用户或活动设计分库分表,同时为 teamId 查询建立路由映射。通知侧可把本地消息表升级为 Outbox + CDC 投递,并完善确认、重试、死信和对账。
整个演进必须配套多级降级、容量压测、热点监控、链路追踪和故障演练,逐步验证,而不是一次性堆中间件。
考察重点:读写分治、热点治理、幂等一致性和渐进演进。
常见追问与简答
回答提醒:十万 QPS 是目标场景,不是当前项目已经达到的性能指标。
参考回答
先确认业务事实:队伍是否真的已达到目标人数,而不是只有该用户支付。然后用 userId + outTradeNo 查 group_buy_order_list 的状态和支付时间,再按 teamId 查 group_buy_order 的 lock_count、complete_count、target_count 和状态。
如果明细仍是 CREATE,沿 TraceId 和结算日志检查上游回调是否到达、参数是否合法、渠道是否被 DCC 拦截、支付时间是否超期、更新行数是否为 0。如果明细已完成但队伍计数少,检查事务回滚和重复回调;如果完成数已经等于目标但队伍仍为 PROGRESS,重点怀疑并发结算使用旧 completeCount 的问题,并通过修复后的补偿任务收敛。
若队伍已 COMPLETE 但上游仍显示未成团,则检查 notify_task 是否生成、状态和次数、HTTP 返回、RabbitMQ 发布与消费、队列积压及定时补偿。最后核对 Redis 名额只是锁单维度,不要把它当作支付完成的唯一依据。
考察重点:按数据状态分层定位,而不是上来就重启或改数据。
常见追问与简答
回答提醒:排查顺序应是上游回调、用户明细、队伍聚合、通知下游四层。
参考回答
单元测试覆盖四种优惠的阈值、金额下限、舍入和标签命中,覆盖各规则节点与退款状态组合;仓储集成测试使用独立 MySQL、Redis、RabbitMQ 环境验证真实 SQL、事务、缓存和消息。接口测试覆盖试算、开团、参团、结算、重复结算、主动退款和超时退款。
并发测试最关键:目标 N 人时同时发起 N+M 次参团,验证不超员;同一 outTradeNo 并发锁单只生成一条;最后两人并发结算最终必须成团且通知任务唯一;MQ 重复投递退款消息只能恢复一次名额。还要测试 Redis/MQ 短暂不可用、应用在事务提交后宕机等恢复场景。
证明掌握项目不能只展示文档。我会画出三条时序图,使用断点和 SQL 跑通完整链路,补充上述自动化测试,并亲自修复一个可验证问题,例如补唯一索引、原子成团判断或退款锁 TTL,再用测试说明修复前后的行为。
考察重点:测试分层、并发不变量、故障恢复和真实动手能力。
常见追问与简答
回答提醒:现有 Maven 配置默认跳过测试,真正建设测试体系时还要调整 CI,使测试被实际执行。
知识笔记会随着实践和认知变化持续更新,不代表最终结论。