1. 架构设计类
Q1:你们的多租户是如何实现数据隔离的?
标准回答
我们采用的是 基于 tenant_id 的行级数据隔离方案。
具体实现分为四层:
第一,数据库层面
所有业务表统一增加 tenant_id 字段,作为租户标识。
第二,ORM 层自动隔离
使用 MyBatis-Plus 的 TenantLineInnerInterceptor,在 SQL 执行前自动为业务 SQL 拼接 tenant_id = 当前租户ID 条件。
第三,租户上下文传递
用户登录后会确定当前租户,请求进入系统后,从 Sa-Token 的 Session 中获取租户 ID,写入 ThreadLocal,供多租户插件读取。
第四,白名单机制
像 sys_dict、sys_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 的原因
-
上手快,API 直观
-
认证、授权、踢人下线、Session 管理这些功能都够用
-
性能更轻量
-
中文文档好,团队开发效率更高
一句话总结
如果是权限模型非常复杂、和 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:工作流引擎是如何实现的?
标准回答
我们实现的是一个 轻量级审批流程引擎,支持固定审批、角色审批、上级审批、会签、或签等场景。
核心模型
-
流程定义
-
流程实例
-
审批任务
-
节点流转规则
执行流程
-
用户发起流程
-
创建流程实例
-
根据流程定义生成首个审批任务
-
审批人处理任务
-
根据通过/驳回结果流转下一节点
-
所有节点结束后流程完成
面试亮点
你可以强调:
这个引擎虽然比不上 Activiti、Flowable 那么重,但对我们当前业务足够,而且结构可控、可定制性强。
Q10:智能客服的 AI 集成是如何实现的?
标准回答
我们是按 规则匹配 → 知识库检索 → 大模型生成 的三级降级链路设计的。
流程
-
用户提问
-
先走关键词自动回复规则
-
未命中则去 Elasticsearch 检索知识库
-
仍然没有结果,再调用 AI 大模型生成答案
-
最终记录聊天日志,供后续优化
为什么这样做
因为直接调用大模型成本高、响应慢,不适合所有请求。
先用规则和知识库可以提高响应速度,降低模型调用成本。
优化点
-
高频问题缓存到 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 攻击?
标准回答
主要有四种方式:
-
CSRF Token
-
SameSite Cookie
-
校验 Referer/Origin
-
双重 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 机制一起做防护。
所以这个问题我一般不会一刀切回答,而是结合实际鉴权方式去看。