SaaS-Admin-项目场景题
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 机制一起做防护。
所以这个问题我一般不会一刀切回答,而是结合实际鉴权方式去看。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)