面灵AI→

滴滴出行后端一面:RBAC 权限设计与联合索引拷打

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 请做一下自我介绍。
  2. 项目背景是什么?
  3. 这个项目你主要负责什么?
  4. 核心产出是什么?
  5. 遇到的最大挑战是什么?
  6. 这个挑战你是怎么解决的?
  7. RBAC 是什么?
  8. User、Role、Permission 之间是什么关系?
  9. 用户和角色、角色和权限是一对多还是多对多?
  10. 权限粒度怎么设计?
  11. 一次请求完整的权限校验流程是什么?
  12. RBAC 表结构怎么设计?
  13. 多角色、角色互斥怎么处理?
  14. 项目里有没有用 Redis?
  15. Redis 是做什么的?
  16. Redis 有哪些常见数据结构?
  17. MySQL 有哪些索引类型?
  18. 索引底层是什么数据结构?
  19. B+Tree 怎么理解?
  20. 什么是联合索引?
  21. 为什么联合索引的字段顺序重要?
  22. 最左前缀原则怎么理解?
  23. (A,B,C,D) 联合索引配合具体 WHERE 条件,哪些字段能用上索引?
  24. 什么情况下会索引失效?
  25. 手撕:长度为 K 的无重复子串数量。
  26. 手撕追问:左右指针什么时候移动?移动的条件是什么?

《参考解析》

B+Tree、联合索引与最左前缀

InnoDB 的索引底层是 B+Tree:非叶子节点只存键和子节点指针,所有数据都在叶子层,叶子之间用双向链表相连。这样做的好处是同样大小的页能存更多键、树高更低(三层就能索引上千万行),且范围查询只要定位到起点后顺着叶子链表扫即可。聚簇索引的叶子存整行数据,二级索引的叶子存主键值,所以走二级索引查非索引列要「回表」再查一次聚簇索引,这也是覆盖索引能显著提速的原因。

联合索引 (A,B,C,D) 是把四个列按顺序拼成一个键来排序,因此它只能被前缀使用:WHERE A=?、A=? AND B=?、A=? AND B=? AND C=? AND D=? 都能用;A=? AND C=? 只有 A 走索引定位、C 用不上(MySQL 5.6 起的索引下推能在存储引擎层用 C 过滤,但不算定位);B=?、C=?、B=? AND C=? 直接用不上这个索引。范围条件会截断后续列:A=? AND B>? AND C=? 里 C 无法再用于定位。另外 ORDER BY/GROUP BY 能否免排序,同样取决于是否满足最左前缀且排序方向一致。

索引失效的常见情形:对索引列做函数或运算(WHERE DATE(create_time)=?、WHERE id+1=?)、隐式类型转换(字符串列传数字、字符集或排序规则不一致的 JOIN)、以 % 开头的 LIKE、OR 两边有一侧没索引、!=/NOT IN 导致优化器判断全表更快、以及优化器基于统计信息估算选择性太差而主动放弃索引。判断实际用没用上要看 EXPLAIN 的 key、rows、filtered 和 Extra(出现 Using index 是覆盖索引,Using filesort 说明排序没走索引)。设计上的经验是:等值条件多、区分度高的列放前面,范围查询列放最后,并且用 (等值列, 范围列) 的顺序去覆盖高频 SQL。

RBAC 模型与一次请求的权限校验流程

RBAC 的核心是把「权限」从用户身上解耦出来,用角色做中间层:用户通过角色间接获得权限,从而支持批量授权、岗位复用和审计。标准五张表是 user、role、permission(或 menu/resource)、user_role、role_permission;用户与角色、角色与权限都是多对多,靠中间表关联(中间表对 (user_id, role_id) 建联合唯一键)。

一次请求的完整校验可以这样讲:① 认证——从 Cookie/JWT 里解析身份,校验签名与有效期,拿到 userId(网关或过滤器统一做);② 取权限——按 userId 查出全部角色,再按角色查出权限集合(生产上是按用户缓存权限码到 Redis,key 带用户维度 + 版本号);③ 匹配——把当前请求映射成权限标识(如 order:refund),判断是否在集合内,需要数据级权限时再叠加「本人/本部门/全部」的数据范围过滤;④ 拒绝与审计——不通过的按策略返回 403 或降级为只读,并记录审计日志。

有两个高频追问点。一是多角色冲突,常见做法是角色互斥(如「财务审批」和「发起付款」不能同人兼任):把互斥规则存成配置表,分配角色时校验、运行时取权限用「交集」而不是「并集」,并配合双人复核;二是权限缓存的失效,用「用户权限版本号 + 主动失效」而不是固定 TTL,避免改完角色后要等缓存过期才生效。往上一层是 RBAC 的扩展:把「谁、在什么条件下、对哪个对象」拆出来就是 ABAC/ReBAC,权限粒度要求到行级时,往往要引入数据范围表和策略引擎。

Redis 的常见数据结构与用法

String 用来做缓存值、计数器(INCR)、分布式锁(SET key value NX PX)、位图(签到、活跃统计)。Hash 适合存对象的多个字段,可以只读写单个 field,比整段 JSON 更省带宽。List 是双向链表(底层 quicklist),做简单队列或最新 N 条列表。Set 做去重、共同好友这类交并差运算。ZSet 是「唯一成员 + 分数」,用于排行榜、延迟队列(分数存到期时间戳)、按时间范围分页。再往上是 HyperLogLog(基数估算,UV 统计)、Bitmap、GEO(附近的人)、Stream(消息队列,支持消费组和 ACK)。

回答时最好结合项目讲「为什么用 Redis 而不是别的」:热点数据抗量、会话共享、分布式锁、限流计数、排行榜;同时点出踩坑——缓存与数据库的一致性和失效策略、大 key/热 key 的拆分、锁的续期与误删(用唯一 value + Lua 释放)、以及内存淘汰策略对缓存命中率的影响。

手撕:长度为 K 的无重复子串数量

滑动窗口的标准写法:用 left、right 维护窗口 [left, right),右指针每次右移、把 s[right] 计数加一;当该字符计数大于 1 时,说明窗口内出现重复,就不断右移 left 并把移出字符的计数减一,直到重复消失。此时窗口内所有以 right 结尾的子串都是无重复的。题目问「长度为 K 的无重复子串数量」,就在窗口合法且长度恰好为 K 时计数(或统计「窗口长度 ≥ K 时以当前右端结尾的 K 长子串合法」,用哈希或数组统计不同的 K 长子串则再加一层去重)。

写完主动交代边界:空串、K 大于字符串长度、K <= 0、字符集是 ASCII 还是 Unicode(决定用 int[128] 还是 HashMap)。复杂度是 O(n) 时间、O(字符集) 空间。面试官追问「指针什么时候移动」时按这个口径答:右指针每轮固定移动一步,左指针只在「窗口内出现重复字符」或「窗口长度超过 K」时移动,且移动是 while 循环直到条件恢复;两个指针都只单调右移,所以总量是线性的。最后主动用两个样例手跑一遍(如 s = "abcabc", K = 3、s = "aaaa", K = 2),把边界情况覆盖到。