
此仅供个人学习使用AI模拟面试回答一. 认证系统开发基于 Spring Security 的 JWT 双令牌认证系统采用 RS256 签名 Redis 刷新令牌白名单实现 15 分钟访问令牌 7 天刷新令牌的安全会话管理支持即时令牌撤销兼顾高安全与高性能。 标签认证双令牌模式1.第一问你简历里写的是“开发基于 Spring Security 的 JWT 双令牌认证系统采用 RS256 签名 Redis 刷新令牌白名单实现 15 分钟访问令牌 7 天刷新令牌的安全会话管理支持即时令牌撤销兼顾高安全与高性能。”问答下你为什么要用“JWT 双令牌 RS256 Redis 白名单”这套方案如果只用单 JWT或者用 HS256不行吗你从“登录、鉴权、刷新、退出登录/强制下线”这 4 个环节把你的设计思路完整讲一遍。答“我选择JWT 双令牌 RS256 Redis 白名单本质上是在安全性、可撤销性和性能之间做平衡。如果只用单 JWT服务端虽然校验快但它是天然无状态的一旦签发出去在不过期之前服务端通常没法主动让它失效所以被盗后风险窗口完全取决于 token TTL。我这里把 token 拆成了两类access token用来做日常接口鉴权过期时间短比如 15 分钟refresh token用来换新 access token过期时间长比如 7 天。这样用户不用频繁登录同时又把 access token 的风险窗口压短了。鉴权时我让大部分请求只校验access token的签名和过期时间不查 Redis这样接口访问是无状态的性能高。而refresh token我会带上jti并把它作为白名单记录存到 Redis比如按userId tokenId维度存并设置和 refresh token 一致的 TTL。当用户刷新 token 时服务端不仅要校验 JWT 本身合法还要校验它是不是 refresh 类型、再检查 Redis 白名单里这个jti还在不在。校验通过后我会签发一对新的 access/refresh token同时把旧 refresh token 的白名单删掉再把新的存进去这其实就是 refresh token 轮换。退出登录或者重置密码时我能做的是立即撤销 refresh token。这意味着攻击者之后不能再用旧 refresh token 续期了。但已经签发出去的access token因为是无状态 JWT通常不能做到秒级立刻失效只能等它自然过期。所以我把 access token 设计成 15 分钟就是把这段风险窗口控制住。这也是为什么我说它兼顾安全和性能高频请求只验 access token不查 Redis只有刷新、登出、强制下线这类低频但安全敏感的操作才依赖 Redis 做可撤销控制。”2.第二问你简历里写了Redis 刷新令牌白名单问你的refresh token在刷新时有没有做“轮换rotation”也就是用户拿旧refresh token换新 token 成功之后旧的refresh token还能不能再用为什么要这么设计如果不做轮换会有什么安全风险答“我的 refresh token 做了 rotation。用户拿旧 refresh token 刷新成功后旧 refresh token 会立刻失效不能再次使用。这样设计的核心目的是防止 refresh token 被盗后被反复重放。因为 refresh token 的生命周期通常比 access token 长如果不做轮换那么攻击者一旦拿到一个还没过期的 refresh token就可以不断换出新的 access token风险窗口会非常大。所以我的做法是刷新请求进来后先校验 refresh token 的签名、过期时间和 token 类型再根据其中的jti去 Redis 白名单判断它当前是否仍然有效。只有都通过才签发新的一对 access/refresh token。签发成功后立即删除旧 refresh token 对应的白名单记录再把新的 refresh token 写入 Redis。这样旧 token 就不能再重复刷新了。这个设计的价值在于服务端可以把 refresh token 从‘长期可重复使用的凭证’变成‘一次性、可轮换的会话续期凭证’安全性会明显更高。”3.第三问问你为什么把 Redis 设计成“刷新令牌白名单”而不是把access token也放进 Redis 每次鉴权都查一遍这样不是更安全吗你为什么还说自己的方案兼顾了高性能答“我没有把access token也放到 Redis 里每次鉴权都查主要是因为access token是高频使用的。业务接口的每一次请求几乎都会带access token如果我在每次请求时都额外查一次 Redis那么整个认证链路就从原来的本地验签变成了‘验签 远程缓存访问’这会增加网络 IO、Redis 压力和整体延迟在高并发下代价比较明显。所以我的设计是把access token作为短生命周期、无状态的 JWT 来用。它在每次请求时仍然会被鉴权但方式是服务端直接校验它的签名、过期时间和声明字段这一步不依赖 Redis性能更高。而refresh token是低频使用的只在登录续期、退出登录、强制下线这类场景才会参与所以我把 Redis 白名单放在refresh token这一层用来实现可撤销和会话控制。这样做的权衡是高频请求走无状态 JWT 校验保证性能低频但安全敏感的续期操作走 Redis 白名单保证可控性。当然它不是绝对最安全的因为如果access token已经泄露在它自然过期前通常还可以继续用所以我的项目里把access tokenTTL 设得比较短比如 15 分钟而refresh token则通过 Redis 白名单和轮换机制来控制长期会话风险。这就是我说它兼顾高性能和高安全的原因本质上是分层控制而不是把所有 token 都做成强状态化。”4.第四问问你说支持“即时令牌撤销”但实际上access token不能立刻失效那你这句话会不会有点夸大如果面试官当场质疑你“这不算真正的即时撤销”你怎么解释才能既诚实又不把自己说崩答“如果严格定义‘即时令牌撤销’我的实现并不是让access token秒级失效而是立即撤销refresh token阻断会话继续续期。因为access token在我的设计里是短生命周期、无状态的 JWT服务端平时只做验签和过期校验不维护它的中心状态所以一旦签发出去默认不能像 Session 一样随时主动作废。所以我这里的处理方式是把access token的 TTL 设得比较短比如 15 分钟把泄露后的可利用窗口压缩到较小范围同时把refresh token放进 Redis 白名单并支持登出时撤销单个 refresh token、重置密码时撤销该用户全部 refresh token。这样做的效果是攻击者即使暂时持有一个还没过期的access token最多也只能使用到它自然过期但他已经无法再用refresh token继续续期整个会话会在短时间内被彻底收敛。所以更准确地说我的项目支持的是刷新令牌的即时撤销以及会话续期能力的即时切断。如果是更高安全场景还可以进一步缩短 access token TTL或者给 access token 增加黑名单/版本号机制但代价是每次鉴权性能会下降。”二. 计数和点赞系统1计数笔记维度 (点赞收藏) 与用户维度 (关注取关) 以 Redis 作为底层存储系统采用定制化 Redis SDS 二进制紧凑计数使用 Lua 脚本进行原子更新并实现了采样一致性校验与自愈重建。 标签定制化 Redis SDS2点赞采用异步写 写聚合 Kafka 异步写 写聚合的形式应对高并发写场景。采用位图的结构高效实现幂等和判重。读取遇到异常或缺失时基于位图做按需重建保证最终一致。并用 Kafka 做 “灾难回放” 的兜底操作。 标签分片位图 计数重建策略1.第一问问你简历里写了“点赞系统采用异步写 写聚合 Kafka 异步写聚合应对高并发写场景”。你现在给我讲清楚这条写链路答“一个用户对一篇帖子点了赞”之后这个请求从进入接口开始到最终计数对外可读为止中间经过了哪些存储层、哪些关键步骤当用户点赞一篇文章时请求线程不会直接去改“最终点赞数”而是先把这个用户映射到固定分片算出chunk和分片内bit offset然后对对应的位图 key 执行 Lua 脚本。这个 Lua 的职责是做原子状态切换比如从 0 变 1 才算真正点赞成功如果本来就是 1说明重复点赞这次就不会继续往后发事件所以幂等性是在位图层就卡住了。只有位图状态真的发生变化后系统才会构造一个计数事件里面带上entityType、entityId、metric、idx、userId、delta这些信息发到 Kafka。这里异步写的核心目的是把行为确认和计数折叠解耦请求线程只需要尽快完成事实层写入不必同步承担热点计数更新的压力。Kafka 消费到事件后也不是立刻直接改最终快照而是先把增量写进 Redis 的聚合桶也就是一个Hash结构key 类似agg:v1:{etype}:{eid}field 是指标下标value 是累计 delta。这样同一秒内大量的1/-1可以先在聚合桶里合并。随后定时任务按秒扫描这些聚合桶再用 Lua 脚本把聚合后的增量一次性折叠到cnt:v1:{etype}:{eid}这个 SDS 快照里所以“写聚合”解决的是高频小写导致的 Redis IO 放大问题。客户端后续查点赞数时读的是cnt:v1:{etype}:{eid}这个 SDS 快照不是直接扫位图。如果发现这个快照缺失、损坏或者长度不符合 schema系统会触发基于位图分片的扫描重建把各分片BITCOUNT求和后回写新的 SDS。为了避免多个请求同时重建还会加分布式锁为了防止热点实体反复重建还做了限流和指数退避。所以整体上这是一个“位图做事实层Kafka聚合桶做异步折叠SDS 做读快照异常时按位图自愈重建”的设计。2.第二问为什么要位图分片分片大小怎么选这里把位图按用户 ID 分片当前实现里CHUNK_SIZE 32768也就是每片 32768 bit约4KB。这样做的好处是单个 bitmap key 大小稳定不容易膨胀GETBIT/SETBIT/BITCOUNT都只打在小块上时延更稳重建时可以按分片 pipeline 做BITCOUNT成本可控为什么选 32768太小key 太多管理和扫描成本高太大单 key 又会变热、变重4KB/片是一个比较均衡的工程取值。3.第三问问如果Lua已经把位图状态改成功了但Kafka发送事件失败了这次点赞要不要回滚你怎么保证最终计数不乱答如果Lua已经把位图状态改成功了但Kafka发送失败这种情况一般不建议回滚位图。因为位图已经代表“事实状态”变更了回滚会把“用户已经点了赞”这件事改掉反而更容易引入不一致。这个场景里我们接受的是最终一致性核心思路是先保证事实层成功再尽量把增量事件可靠投递出去如果投递失败后面可以通过补偿、重试或者定时对账把计数补回来。在我们这个项目里点赞成功后会先修改位图再发CounterEvent到 KafkaKafka 事件消费后会进入聚合桶最后由定时任务折叠到 SDS 快照。所以真正要保证的是“位图状态”和“计数快照”最终一致而不是要求一次请求内强事务一致。面试时你可以补一句如果 Kafka 发送失败系统会记录日志并依赖重试/补偿机制必要时也可以通过位图扫描重建快照来纠正最终计数。4.第四问问扫描重建为什么要按分片做BITCOUNT而不是直接存一个总计数然后每次回放事件答这套方案里SDS只是读快照真正的事实层是分片位图。之所以重建时按分片做BITCOUNT求和而不是只维护一个总计数再去回放事件是因为总计数一旦出错没法从“结果值”反推回真实状态而位图里的每一位代表一个用户的真实点赞态重建时直接扫位图就能把当前事实重新计算出来。另外按分片统计能避免单个大 key 扫描太慢也方便并行处理重建成本更可控。总之BITCOUNT扫分片是“从事实恢复快照”而不是“从事件日志补历史”这样更稳也更适合自愈。三. 搜索系统基于 Elasticsearch 构建内容搜索与联想建议功能支持关键词检索标签过滤采用 search_after 游标分页保证深分页稳定性。同时通过 function_score 融合 BM25 相关性与点赞等业务权重优化排序保证结果的相关性并使用 ES 的 completion suggester 实现低延迟前缀联想。1.第一问问你简历里写了这句“基于 Elasticsearch 构建内容搜索与联想建议功能支持关键词检索、标签过滤采用search_after游标分页保证深分页稳定性同时通过function_score融合 BM25 相关性与点赞等业务权重优化排序并使用 ES 的completion suggester实现低延迟前缀联想。”如果一篇知识帖从“发布成功”到“最终能被用户搜索到”中间完整链路是怎样的答用户发帖后业务先把变更写入 outboxcanal/消息消费端监听到变更事件后拿到帖子id和操作类型调用搜索索引服务做upsert或soft delete。索引服务会回表补齐帖子详情、作者信息、标签、图片、统计数据等构建一份面向搜索的 ES 文档。应用启动时还会先确保索引和 mapping 存在必要时回灌历史数据。用户搜索时不直接查数据库而是用 ES 做全文检索和相关性排序multi_match召回标题和正文function_score结合点赞、浏览等业务权重排序深分页用search_after联想建议则单独用completion suggester做低延迟前缀补全。2.第二问问为什么这里不直接用offset/page翻页而要用search_after答offset/page在 ES 深分页时性能差因为它本质上还是要先跳过前面很多命中结果页数越深协调和内存开销越大。search_after的思路不是按页号翻而是把上一页最后一条文档的排序值作为游标下一页直接从这个排序边界之后继续查所以更适合深分页。但search_after必须配合稳定、确定性的排序使用否则如果前面的排序字段重复就可能出现翻页重复或漏掉数据。因此通常要设计多字段排序并加一个唯一兜底字段作为 tie-breaker。在这个项目里更合理的说法是游标不能只存content_id因为分页边界不是由 id 定义的而是由整组排序值定义的。比如前面如果先按相关性、发布时间、点赞数排序那么仅有 id 根本无法唯一确定“上一页最后一条在排序结果里的位置”所以要把整组 sort 值一起带上。3.第三问问为什么不直接查 MySQL而要单独引入 ES答因为 MySQL 更擅长事务型查询、精确过滤和关系建模但不擅长中文全文检索、相关性排序、前缀联想和深分页。比如用户搜一个自然语言关键词时我希望标题命中和正文命中能有不同权重还希望结果能按文本相关性排序这类能力 ES 原生支持得更好。再比如搜索联想和高亮片段放在数据库里做成本会很高效果也一般。所以我把 MySQL 作为事实数据源把 ES 作为搜索场景下的读模型两者各司其职。4.第四问问为什么要维护一份 ES 读模型而不是搜索时临时回表拼数据答因为搜索结果页不仅要“搜到”还要“直接展示”。结果页通常需要标题、摘要、封面、标签、作者信息、互动计数这些内容如果每次搜索完再回 MySQL 拼装一是链路会变长二是性能不稳定三是排序和高亮都不容易统一处理。维护 ES 读模型的好处是把搜索需要的字段预先物化进去查询时一次命中就能拿到足够多的展示数据。这样搜索层就从“临时拼装”变成“直接读取搜索优化后的文档”。5.第五问问为什么你这里选择outbox Kafka而不是业务代码里直接双写 MySQL 和 ES答答因为双写最怕的是一致性问题。比如 MySQL 成功了、ES 失败了或者反过来主链路里很难优雅处理而且会让业务事务变得又长又脆弱。outbox的优势在于先保证主业务事务落库成功再用异步消息去驱动搜索索引更新把一致性问题从“强一致双写”变成“可重试的最终一致”。对搜索这种旁路能力来说这是更工程化、更稳定的方案。四.用户关系系统实现关注功能采用一主多从 事件驱动模型。粉丝表计数系统列表缓存都作为关注表的伪从。关注事件发生时在同一事务中插入关注表和 Outbox 表使用 Canal 订阅 Outbox 表的 binlog并将变更事件发布到 Kafka 异步更新其他数据源。 标签Outbox 事件驱动模式用户关系1.第一问问你这个关注系统的完整链路是怎样的答一次 A 关注 B 的请求进来后我会先做用户级限流避免短时间内频繁关注/取关把数据库、缓存和消息链路打爆。通过限流后同步写入following主表同时把这次领域事件写入Outbox设计目标是两者同事务提交这样主事实和事件源保持一致。事务提交成功后Canal 订阅outbox的 binlog只捕获已经提交成功的变更再把事件转成 Kafka 消息。下游消费者收到消息后做幂等校验然后异步维护follower伪从表、关注/粉丝列表缓存以及关注数和粉丝数等衍生数据。整个系统里following是主事实其他数据更多是为读优化服务的派生视图。2.第二问问为什么要做“一主多从”而不是一张关系表加几个索引答核心不是单纯“查得更快”而是把“主事实”和“读模型”拆开。following解决的是“我关注了谁”它是强业务语义、必须尽快准确的主数据follower解决的是“谁关注了我”本质上是反向读模型可以接受异步维护和短暂延迟。这样做的好处是主写链路可以更短只保证关键事实先落地而粉丝侧列表、计数、缓存这些展示型数据走异步链路不阻塞主请求。再往后扩容时也可以按不同访问方向分别做热点隔离和缓存优化。3.第三问问为什么Outbox一定要和following放在同一个事务里答这是为了解决双写一致性问题。关注主表和事件如果不在一个事务里就会出现两种最危险的情况第一种是主表写成功了但事件没发出去下游永远收不到变更导致粉丝表、计数、缓存长期不一致第二种是事件发出去了但主表最后回滚了下游却已经更新了粉丝列表和计数形成幽灵数据。把following和Outbox放在同一事务里本质上就是把“写业务数据”和“生产事件”绑定成一个原子操作要么一起成功要么一起失败。这样 Canal 只会订阅到真正提交成功的 outbox 记录。4.第四问问Canal 和 Kafka 在这个方案里分别解决什么问题答Canal 主要解决“如何稳定地把数据库提交后的事实变成事件”这个问题它通过订阅 MySQL binlog 来拿到已提交事务的变更避免业务代码直接在事务里同步发 MQ。Kafka 解决的是“如何把这些事件高吞吐、可缓冲、可解耦地分发给下游”这个问题它让下游多个消费者可以独立消费也能容忍短时间消费堆积。简单说Canal 更像数据库变更捕获器Kafka 更像事件总线和削峰缓冲层。两者配合后主请求只负责落主事实衍生数据维护交给异步链路。5.第五问问既然已经有Outbox Kafka了为什么消费端还要做幂等答因为这条链路天然更接近“至少一次投递”而不是“恰好一次”。比如 Canal 重启、位点回退、Kafka 消费者 rebalance、消费成功但还没提交位点就宕机都会让同一条事件被再次投递。如果不做幂等最直接的问题就是计数被重复加一、缓存被重复改写严重时会把粉丝数和关注数算错。我的做法是给每条关系事件构造一个去重键把事件类型、用户对和关系 id 组合起来先做一次原子去重再执行异步更新逻辑。这样即使消息重复投递下游也只会生效一次。6.第六问问为什么你这里还要做关注操作限流答因为关注/取关虽然看起来只是一个小动作但它会联动数据库写入、Outbox 写入、binlog 捕获、Kafka 投递、下游缓存刷新和计数更新实际上是一条完整的写扩散链路。如果不做限流恶意脚本或异常用户行为很容易把整条链路打爆。令牌桶的好处是既允许短时突发又能把长期流量平滑下来比较适合这种用户行为型接口。这样既保护系统也避免单个用户对热门账号形成放大冲击。7.第七问问如果面试官追问“这个系统是强一致还是最终一致”你怎么回答答我会说这是一个“主事实强一致、衍生视图最终一致”的设计。关注关系本身以主表为准所以像“是否已关注”这种核心判断必须尽快准确而粉丝侧视图、列表缓存、计数缓存这些更多是读优化和展示数据可以接受秒级甚至分钟级延迟。这样分层后系统既不会为了所有数据都强一致而把写链路拖得很重也不会让业务关键判断依赖异步链路。这个回答通常比单纯说“最终一致”更完整