1. 架构设计类

Q1:你们的多租户是如何实现数据隔离的?

标准回答

我们采用的是 基于 tenant_id 的行级数据隔离方案

具体实现分为四层:

第一,数据库层面
所有业务表统一增加 tenant_id 字段,作为租户标识。

第二,ORM 层自动隔离
使用 MyBatis-Plus 的 TenantLineInnerInterceptor,在 SQL 执行前自动为业务 SQL 拼接 tenant_id = 当前租户ID 条件。

第三,租户上下文传递
用户登录后会确定当前租户,请求进入系统后,从 Sa-Token 的 Session 中获取租户 ID,写入 ThreadLocal,供多租户插件读取。

第四,白名单机制
sys_dictsys_config 这种全局共享表不需要租户隔离,我们通过白名单排除。

优势

  • 成本低,共享一套库表结构

  • 扩展方便,新增租户不需要建新库新表

  • 结合索引后性能可控

风险与应对

  • 数据量大后单表压力会增大,可以进一步做分库分表

  • 如果代码绕过插件,可能有越权风险,所以我们会结合代码审查、测试和 SQL 规范保障安全

Q2:你们的接口加密方案是怎么设计的?为什么这样设计?

标准回答

我们采用的是 RSA + AES-GCM 混合加密 + HMAC-SHA256 签名 的方案。

设计思路

因为:

  • RSA 安全性高,但性能差,适合做密钥交换

  • AES-GCM 加密性能高,适合业务数据加密,而且自带完整性校验

  • HMAC-SHA256 用来做签名,保证请求没被篡改

  • 再结合 时间戳 + Nonce 防止重放攻击

Q3:如果租户数量增长到 10 万+,架构如何扩展?

标准回答

如果租户规模从当前增长到 10 万以上,我会从 数据库、应用、缓存、搜索、监控 五个层面扩展。

1)数据库层
  • tenant_id 做分库分表

  • 大租户独立分库,小租户共享分片

  • 主从复制做读写分离

  • 历史数据归档,做冷热分离

2)应用层
  • 按业务域拆分微服务,比如用户、订单、工单、支付独立

  • 引入服务治理和限流熔断

3)缓存层
  • Redis 集群

  • 本地缓存 + 分布式缓存两级结构

  • 对热点租户做缓存预热

4)搜索层
  • Elasticsearch 集群化部署

  • 大租户独立索引,小租户共享索引

  • 查询高峰支持降级策略

5)可观测性
  • Prometheus + Grafana 做指标监控

  • ELK 做日志分析

  • SkyWalking 做链路追踪

总结一句

也就是说,当前架构适合中小规模租户;当规模继续扩大时,核心思路就是 分片、拆服务、做缓存、做治理、增强监控

2. 技术实现类

Q4:MyBatis-Plus 的多租户插件是如何工作的?

标准回答

它本质上是 SQL 拦截 + SQL 重写

工作流程是:

  • 在 MyBatis 执行 SQL 前拦截 SQL

  • 使用 JSqlParser 解析 SQL

  • 识别表名、WHERE 条件

  • 自动追加 tenant_id = 当前租户ID

  • 把重写后的 SQL 再交给 MyBatis 执行

面试加分点

它对普通单表查询支持很好,但对于复杂 JOIN、子查询、手写 SQL,仍然需要重点验证和测试,避免租户条件遗漏。

Q5:Sa-Token 和 Spring Security 有什么区别?为什么选 Sa-Token?

两者都能做认证授权,但定位不同。

Spring Security 更偏企业级全能框架,功能非常全面,但配置复杂、学习成本高。
Sa-Token 更轻量,API 简单,适合中小型项目快速落地。

我们选择 Sa-Token 的原因

  1. 上手快,API 直观

  2. 认证、授权、踢人下线、Session 管理这些功能都够用

  3. 性能更轻量

  4. 中文文档好,团队开发效率更高

一句话总结

如果是权限模型非常复杂、和 Spring 生态深度绑定的场景,Spring Security 更合适;
如果是追求开发效率和易维护性,Sa-Token 更适合。

Q6:Redis 在项目中的应用场景有哪些?

标准回答

我们项目里 Redis 主要有 6 个应用场景:

1. Session 存储

用于保存 Sa-Token 的会话数据,支持分布式部署。

2. 缓存热点数据

比如用户信息、角色权限、字典数据、配置项。

3. 分布式锁

使用 Redisson,避免并发重复执行,比如支付回调幂等、定时任务防重跑。

4. 限流

INCR + EXPIRE 实现接口限流和登录失败次数限制。

5. 防重放

保存请求 Nonce,防止重复请求。

6. 轻量异步队列

比如发短信、发邮件等低复杂度异步任务。

面试加一句

如果业务再复杂一些,我会把 Redis List 这种轻量队列升级成 RabbitMQ 或 Kafka,提高可靠性和可观测性。

Q7:Elasticsearch 在项目中如何使用?

标准回答

主要用在两个场景:

1. 全文搜索

比如工单、知识库、操作日志检索。

实现方式是:

  • 用 Canal 监听 MySQL binlog

  • 增量同步到 Elasticsearch

  • 按租户维度建立索引或在文档中加 tenantId

  • 查询时带租户过滤条件,避免跨租户搜索

2. 日志分析

通过 ELK 收集业务日志和系统日志,配合 Kibana 做可视化和排障。

优化经验

  • 合理控制分片数

  • 使用 filter 替代部分 query,提高缓存命中率

  • 避免深分页

  • 做冷热索引分离

3. 业务逻辑类

Q8:支付计费模块是如何设计的?

标准回答

支付计费模块主要包括:

  • 订单

  • 支付

  • 账单

  • 发票

核心流程

1. 订单

用户选择套餐后生成订单,状态初始为待支付。

2. 支付

接入支付宝、微信等渠道。
后端通过策略模式统一封装不同支付方式。

3. 回调

支付成功后,支付平台会异步通知系统。
系统验证签名后,更新订单状态,并延长租户服务有效期。

4. 账单与发票

系统按月生成账单,租户可申请发票,管理员审核后开票。

关键设计点

  • 幂等性:支付回调可能多次触发,用分布式锁 + 状态判断防止重复处理

  • 事务一致性:订单、账单、租户状态更新放在同一事务

  • 异步解耦:通知类操作走消息队列

Q9:工作流引擎是如何实现的?

标准回答

我们实现的是一个 轻量级审批流程引擎,支持固定审批、角色审批、上级审批、会签、或签等场景。

核心模型

  • 流程定义

  • 流程实例

  • 审批任务

  • 节点流转规则

执行流程

  1. 用户发起流程

  2. 创建流程实例

  3. 根据流程定义生成首个审批任务

  4. 审批人处理任务

  5. 根据通过/驳回结果流转下一节点

  6. 所有节点结束后流程完成

面试亮点

你可以强调:

这个引擎虽然比不上 Activiti、Flowable 那么重,但对我们当前业务足够,而且结构可控、可定制性强。

Q10:智能客服的 AI 集成是如何实现的?

标准回答

我们是按 规则匹配 → 知识库检索 → 大模型生成 的三级降级链路设计的。

流程

  1. 用户提问

  2. 先走关键词自动回复规则

  3. 未命中则去 Elasticsearch 检索知识库

  4. 仍然没有结果,再调用 AI 大模型生成答案

  5. 最终记录聊天日志,供后续优化

为什么这样做

因为直接调用大模型成本高、响应慢,不适合所有请求。
先用规则和知识库可以提高响应速度,降低模型调用成本。

优化点

  • 高频问题缓存到 Redis

  • AI 响应通过 WebSocket 流式返回

  • 设置调用限额,超限自动降级

4. 性能优化类

Q11:项目中做过哪些性能优化?

标准回答

我主要做了四类性能优化:

1. 数据库优化
  • 给高频查询字段加索引

  • 使用复合索引优化多条件查询

  • 避免 select *

  • 使用 EXPLAIN 分析慢 SQL

  • 深分页改成游标分页

2. 缓存优化
  • 本地缓存 + Redis 两级缓存

  • 缓存预热

  • 布隆过滤器防穿透

  • 随机过期时间防雪崩

  • 分布式锁防击穿

3. 接口优化
  • 异步化耗时任务

  • 批量插入、批量更新

  • 接口合并,减少前后端交互次数

4. 前端优化
  • 路由懒加载

  • 大列表虚拟滚动

  • 搜索框防抖

  • 静态资源走 CDN

结果表达

经过优化后,系统接口平均响应时间从 500ms 降到 200ms 左右,并发承载能力明显提升。

Q12:如何解决 N+1 查询问题?

标准回答

N+1 查询指的是先查一次主表,再循环查 N 次关联表,导致 SQL 次数过多。

解决方案

方案一:JOIN 查询

适合一对一、一对多关联字段展示。

方案二:批量查询 + 内存组装

先一次查主表,再提取关联 ID 批量查询,从内存 Map 中回填。

方案三:合理封装 ResultMap

适用于 MyBatis 场景,但要避免再次触发隐式多次查询。

总结一句

我的优先级一般是:
能 JOIN 就 JOIN;不适合 JOIN 就批量查;尽量避免在循环里查数据库。

5. 安全相关类

Q13:如何防止 SQL 注入?

标准回答

我们是多层防护:

1. 预编译

MyBatis 使用 #{},底层是 PreparedStatement,这是最核心的防线。

2. 参数校验

对排序字段、关键字等用户输入做白名单校验。

3. 风险拦截

对明显的恶意关键字做安全拦截和审计记录。

4. 数据库最小权限

业务账号只给必要权限,不给高危 DDL 权限。

面试注意

不要把“正则匹配关键字”说成主要防护手段。
主要防护永远是预编译和参数化查询。

Q14:如何防止 XSS 攻击?

标准回答

XSS 的核心防护思路是:输入过滤 + 输出编码 + 浏览器策略限制

具体做法
  • 服务端对富文本外的普通内容做 HTML 转义

  • 前端默认用文本渲染,避免直接使用 v-html

  • 必须渲染富文本时,使用 DOMPurify 这类库过滤

  • 配置 CSP 限制脚本来源

  • Cookie 设置 HttpOnly

总结一句

XSS 防护不能只靠后端,也不能只靠前端,必须前后端一起做。

Q15:如何防止 CSRF 攻击?

标准回答

主要有四种方式:

  1. CSRF Token

  2. SameSite Cookie

  3. 校验 Referer/Origin

  4. 双重 Cookie 校验

实际项目回答建议

如果系统主要是前后端分离 + Token 鉴权,一般 CSRF 风险会比传统 Cookie Session 模式低很多;但如果仍然依赖 Cookie,我们会结合 SameSite 和 CSRF Token 一起防护。

1. 请你简单介绍一下这个项目

这个项目是一个 SaaS 多租户管理平台,主要面向企业客户,核心功能包括租户管理、用户权限、工单流程、支付计费,还有智能客服等模块。
比如在多租户这块,我们用的是基于 tenant_id 的行级隔离;安全上做了接口加密、防重放、防 SQL 注入这些;性能上引入了 Redis 缓存、分布式锁和 Elasticsearch 搜索。
整体来说,这个项目更偏企业级后台系统,重点在于 隔离性、安全性、可扩展性和性能稳定性

2. 你们的多租户是怎么实现的?

我们采用的是 基于 tenant_id 的行级隔离方案
简单来说,就是所有业务表都会带一个 tenant_id 字段,代表这条数据属于哪个租户。
然后在 ORM 层,我们用了 MyBatis-Plus 的多租户插件,它会在 SQL 执行前自动帮我们拼上 tenant_id = 当前租户ID 这个条件。当前租户 ID 一般是在用户登录后确定的,请求进来后从 Session 里取出来,放到 ThreadLocal 里,插件再去读。
优点:实现成本比较低,所有租户共享一套库表结构,扩展方便。
缺点:随着数据量增大,单库单表压力会越来越大,所以后面如果租户规模再上去,就要考虑分库分表了。

3. MyBatis-Plus 的多租户插件原理是什么?

它本质上就是 SQL 拦截和重写
在 MyBatis 执行 SQL 之前,这个插件会先把 SQL 拦截下来,然后解析 SQL 结构,再自动往里面加租户条件,比如 tenant_id = 1
最后把改写后的 SQL 再交给 MyBatis 去执行。
这种方式对普通 CRUD 很方便,开发时不用每条 SQL 都手写租户条件。
但是如果是特别复杂的 JOIN、子查询,或者手写 SQL,就要格外注意,因为这类场景更容易出现租户条件遗漏的问题。

4. 如果面试官问:为什么你们不用一租户一库,而是用 tenant_id?

这个主要是结合业务阶段做的选择。
因为我们当时租户规模还没有大到必须一租户一库的程度,所以用 tenant_id 这种共享库表的方式,成本更低,开发效率更高,维护也更简单
如果一开始就一租户一库,虽然隔离性会更强,但数据库资源成本、运维复杂度、扩容成本都会上去。
所以我们当时更倾向于先采用行级隔离方案,等租户规模或者大客户数量上来之后,再逐步演进到分库甚至独立库。

5. 你们的接口加密方案是怎么做的?

我们做的是 RSA + AES-GCM + HMAC-SHA256 的混合方案。设计思路其实很明确,就是兼顾 安全性和性能
RSA 主要用来做密钥交换,因为它安全性高,但是性能比较差,不适合加密大量业务数据;真正的数据内容,我们用 AES-GCM 来加密,因为它效率高,而且还能同时做完整性校验。
在请求防篡改这块,我们又加了一层 HMAC 签名,签名内容里会带上时间戳、Nonce、请求路径、请求体摘要这些信息。服务端收到请求之后,会先验签,再解密,再执行业务。
这样既能防止数据在传输过程中被篡改,也能防止重放攻击。

6. 怎么防重放攻击?

我们主要用了两个手段:时间戳 + Nonce
时间戳这块,如果请求超过我们设置的有效窗口,比如 5 分钟,就直接拒绝。
Nonce 就是每次请求带一个唯一随机串,服务端会把它存到 Redis 里,并设置一个短过期时间。
如果发现同一个 Nonce 被重复使用,就说明这个请求可能是重放的,直接拦截掉。
这样实现起来比较简单,效果也比较好。

7. Sa-Token 和 Spring Security 有什么区别?为什么选 Sa-Token?

两者都能做认证授权,但使用体验差别还是挺大的。
Spring Security 更偏重型框架,功能很全,适合权限模型特别复杂的大型系统,但是它的配置和理解成本比较高。
Sa-Token 相对来说更轻量,API 也更直观,比如登录直接 StpUtil.login(),做权限校验也有注解支持。
我们当时这个项目更强调开发效率和落地速度,而且权限模型没有复杂到必须上 Spring Security 的程度,所以最后选了 Sa-Token。
它对我们这种中后台项目来说,功能是够用的,而且团队上手也更快。

8. Redis 在你们项目里主要用在哪些地方?

Redis 在我们项目里用得挺多的,主要有这么几个场景。
第一是 Session 存储,因为我们系统是分布式部署,所以登录态不能只放单机内存里。
第二是 缓存热点数据,比如用户信息、角色权限、字典数据这些,减少数据库压力。
第三是 分布式锁,比如支付回调、定时任务防重复执行。
第四是 限流,像登录失败次数限制、接口频率控制这些。
第五是 防重放攻击,把请求的 Nonce 存进去做校验。
还有一些简单的异步场景,我们也用过 Redis 做轻量级消息队列。
所以它在我们项目里既承担了缓存作用,也承担了一部分并发控制和安全防护能力。

9. Elasticsearch 在项目中怎么用?

Elasticsearch 主要有两个用途。
一个是做 全文搜索,比如搜索工单、知识库、日志这些内容;

全文搜索这块,我们的数据原本是在 MySQL 里,然后通过 Canal 监听 binlog,把变更同步到 ES。查询时会带上租户过滤条件,保证不同租户之间的数据不会串。

一个是做 日志分析

日志分析这块就是常见的 ELK 方案,用 Logstash 收日志,写到 Elasticsearch,再用 Kibana 做查询和可视化。
这样一来,业务搜索体验会比 MySQL 的模糊查询好很多,排查问题也更方便。

10. 支付模块是怎么设计的?

支付模块我们主要拆成了 订单、支付、账单、发票 这几个部分。
业务流程大概是:用户先选择套餐创建订单,然后选择支付方式,比如支付宝或者微信;后端调用对应支付渠道接口,生成支付二维码或者支付链接;支付成功之后,支付平台会异步回调我们系统;我们校验签名,更新订单状态,同时延长租户服务有效期。
这里面最关键的是两个点:
一个是 幂等性,因为支付回调有可能重复通知;另一个是 事务一致性,因为订单状态、账单状态、租户有效期这些要一起更新。
所以我们会用分布式锁状态判断保证幂等,再通过事务保证数据一致。

11. 支付回调怎么保证幂等?

支付回调幂等我一般会从两层保证。
第一层是 业务状态判断,比如订单如果已经是“已支付”状态了,那后面的重复回调就直接忽略。
第二层是 分布式锁,比如基于订单号加锁,避免并发情况下多个线程同时处理同一个回调。
这样就算支付平台因为网络原因多次通知,我们这边也只会真正处理一次。
这个点在支付场景里很关键,不然很容易出现重复记账或者重复延长服务时间的问题。

12. 工作流引擎是怎么实现的?

我们做的是一个轻量级审批引擎,不是直接上 Activiti 或 Flowable。
核心模型主要有三个:流程定义、流程实例、审批任务
流程定义描述有哪些节点、每个节点的审批规则;流程实例表示某个用户实际发起的一次流程;审批任务则对应流程中的每一个审批动作。
用户发起流程之后,系统会根据流程定义生成首个审批任务;审批人处理完后,再根据规则流转到下一节点;全部节点走完,流程就结束。
我们支持固定审批人、角色审批、上级审批、会签、或签这些常见场景。
这种方式的好处是实现可控、业务适配性强,虽然没有通用流程引擎那么重,但对当前场景已经足够了。

13. 智能客服的 AI 是怎么接入的?

我们不是用户一提问就直接调大模型,而是做了一个 三级处理链路
第一层是规则匹配,比如关键词自动回复;第二层是知识库检索,用 Elasticsearch 从知识库里找最相关的内容;第三层才是调用大模型生成答案。
这样设计的原因主要有两个:
一个是 降成本,因为并不是所有问题都值得直接调模型;
另一个是 提升响应速度,规则和知识库检索通常比大模型更快。
如果检索到知识库内容,我们还会把检索结果作为上下文传给模型,这样回答会更贴近业务。

14. 你在项目里做过哪些性能优化?

我主要做过数据库、缓存、接口和前端这几方面的优化。
数据库层面,重点是加索引、优化 SQL、避免深分页;
缓存层面,用了本地缓存加 Redis 的多级缓存,还针对缓存穿透、击穿、雪崩做了处理;
接口层面,把一些耗时操作异步化,比如短信、邮件通知;
前端层面做了懒加载、虚拟滚动、防抖节流这些。
我自己的思路一般是先定位瓶颈,再分层优化,而不是一上来就盲目加缓存或者加机器。

15. 什么是 N+1 查询?你怎么解决?

N+1 查询就是先查一次主表,然后再循环查 N 次关联表,导致数据库访问次数特别多。
比如查用户列表后,又对每个用户单独查部门信息,这就是典型的 N+1。
我一般有两种解决方式:
第一种是直接用 JOIN,一次把数据查出来;
第二种是先批量查主表,再把关联 ID 收集起来一次性批量查询,然后在内存里组装。
实际项目里我会根据场景选,如果 JOIN 不复杂,我优先 JOIN;如果数据量大或者关系复杂,就批量查再组装。
原则就是一句话:尽量不要在循环里查数据库

16. 如何防止 SQL 注入?

最核心的方式其实就一个:参数化查询,也就是预编译
在 MyBatis 里就是优先用 #{},不要乱用 ${}
除了这个之外,我们还会做输入参数校验,比如排序字段、查询条件做白名单限制。
再进一步的话,也会在安全层面对明显恶意输入做拦截和审计。
另外数据库账号本身也遵循最小权限原则,避免即使发生问题也造成更严重的后果。
所以 SQL 注入防护不能只靠某一层,而是要从代码、参数、数据库权限几个层面一起控制。

17. 如何防止 XSS?

XSS 我理解核心就是一句话:不要让不可信输入被浏览器当成脚本执行
所以我们会从两个方向处理。
一个是输入端做转义或者过滤,尤其是普通文本场景,不允许直接存原始 HTML;
另一个是输出端尽量按纯文本渲染,不直接使用 v-html 这种高风险方式。
如果确实要展示富文本,就会用像 DOMPurify 这类库先做清洗。
另外还会结合 CSP、HttpOnly Cookie 这些浏览器安全策略一起做。
这个问题本质上不能只靠后端,也不能只靠前端,必须两边一起配合。

18. 如何防止 CSRF?

这个要看系统认证方式。
如果是传统的 Cookie + Session 模式,那 CSRF 风险会比较明显,一般会通过 CSRF Token + SameSite Cookie + Referer/Origin 校验 来防。
如果是前后端分离、Token 放在请求头里的方式,相对来说风险会小很多,因为浏览器不会自动帮你带这个 Token。
不过如果系统里仍然有依赖 Cookie 的地方,我们还是会配合 SameSite 和 Token 机制一起做防护。
所以这个问题我一般不会一刀切回答,而是结合实际鉴权方式去看。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐