滴滴出行后端一面:RBAC 权限设计与联合索引拷打
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 项目背景是什么?
- 这个项目你主要负责什么?
- 核心产出是什么?
- 遇到的最大挑战是什么?
- 这个挑战你是怎么解决的?
- RBAC 是什么?
- User、Role、Permission 之间是什么关系?
- 用户和角色、角色和权限是一对多还是多对多?
- 权限粒度怎么设计?
- 一次请求完整的权限校验流程是什么?
- RBAC 表结构怎么设计?
- 多角色、角色互斥怎么处理?
- 项目里有没有用 Redis?
- Redis 是做什么的?
- Redis 有哪些常见数据结构?
- MySQL 有哪些索引类型?
- 索引底层是什么数据结构?
- B+Tree 怎么理解?
- 什么是联合索引?
- 为什么联合索引的字段顺序重要?
- 最左前缀原则怎么理解?
(A,B,C,D)联合索引配合具体 WHERE 条件,哪些字段能用上索引?- 什么情况下会索引失效?
- 手撕:长度为 K 的无重复子串数量。
- 手撕追问:左右指针什么时候移动?移动的条件是什么?
《参考解析》
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),把边界情况覆盖到。