本文目录导读:
权限判断的优化提速,核心思路是将“运行时计算”转化为“预计算”和“缓存查找”,就是避免在每次请求时都做复杂的数据库查询和逻辑计算。
下面从架构、缓存、算法、数据结构四个层面来介绍优化方法,并给出具体建议。
核心原则
- 数据与逻辑分离:权限规则(谁可以做什么)和权限检查逻辑分开。
- 尽可能提前:在用户登录或权限变更时,就把权限算好存起来。
- 用空间换时间:用更高效的数据结构(如位图、布隆过滤器)来快速判断。
- 减少IO:内存操作远快于磁盘/网络IO。
架构层面:分层与预计算
这是最根本的优化,将权限判断的“计算”环节提前。
权限预计算(最有效)
- 问题:传统做法是每次请求都去数据库查角色、查角色权限、再判断,这是N次SQL查询。
- 优化:在用户登录成功(或权限变更)时,将用户所有最终的有效权限(如:
[发布文章, 删除评论, 访问后台])计算并存储。 - 存储方式:
- Session / JWT:
- 优点:完全无状态,不用查DB,JWT(JSON Web Token,一种用于身份认证的令牌)本身就可以携带权限列表。
- 缺点:JWT体积可能变大,权限变更需要重新签发或配合黑名单。
- 内存缓存(Redis):
- 将用户ID作为Key,权限集合作为Value,设置过期时间。
- 优点:读写极快,TTL(Time To Live,生存时间)机制天然解决缓存一致性问题。
- 操作:
SISMEMBER user:123:permissions "article:delete"(O(1)复杂度)。
- Session / JWT:
# 伪代码示例:预计算后存储
def on_user_login(user_id):
# 1. 查库,获取用户所有角色
roles = get_roles(user_id)
# 2. 查库,获取所有角色的权限并集
permissions = set()
for role in roles:
permissions.update(get_permissions_of_role(role))
# 3. 预计算结果存入Redis,过期时间1小时
redis.sadd(f"user:{user_id}:perms", *permissions)
redis.expire(f"user:{user_id}:perms", 3600)
# 权限检查时
def check_permission(user_id, required_perm):
return redis.sismember(f"user:{user_id}:perms", required_perm)
权限变更的即时生效
预计算后,权限变更不能等缓存过期。
- 解决方案:发布一个事件/消息(如
[user_id] permission changed)。 - 处理:收到消息后,立即删除或刷新该用户的权限缓存,下一次请求时,会重新预计算。
缓存层面:多级缓存
除了Redis,在应用进程内再加一层缓存。
本地进程缓存(如 Caffeine, Guava Cache)
- 场景:判断一个用户能否访问
/admin页面,这可能有百万次请求。 - 做法:在应用启动时,或第一次访问时,将“用户-权限”映射加载到本地内存。
- 优点:比Redis还快(无网络IO,纳秒级)。
- 缺点:需要处理多节点间的缓存一致性(通常可以通过广播或短TTL解决)。
热点权限缓存
- 场景:一个页面需要判断10个不同的权限点。
- 做法:一次性批量获取该页面所需的所有权限点。
- 不优:循环调用10次Redis。
- 优化:
redis.smembers(f"user:{user_id}:perms")一次获取所有权限,然后在本地做交集判断。
数据结构和算法层面:极致性能
当权限量特别大(比如千万级用户,上万个权限点),或者对延迟要求极高,可以用更“硬核”的方法。
位图(Bitmap)
- 思想:为每个用户分配一个二进制位数组,位索引代表权限ID,值0/1代表是否有权限。
- 例子:权限ID为5,用户有权限,则第5位为1。
- 判断:
bitfield user:123:perm_bitmap get u1 #5(获取第5位的值)。 - 优点:极致的内存效率和速度,一个用户仅需几KB,判断是O(1)。
- 缺点:需要对权限ID做全局规划(不易扩展),权限点变更需要修改位图。
布隆过滤器(Bloom Filter)
- 场景:拒绝大部分无效请求(如恶意爬虫、未登录用户试图访问后台接口)。
- 做法:将“可以访问的权限点”放入布隆过滤器。
- 判断:检查请求的权限点是否在过滤器中。
- 效果:不在过滤器中 -> 100%拒绝(秒杀无效请求),在过滤器中 -> 可能误判,需要二次确认。
- 优点:不会误杀,但能极快过滤掉99%的无效请求,极大降低后端压力。
有序的权限树(Trie树 / 前缀树)
- 场景:资源是树形结构的(如
/project/123/document/456)。 - 问题:需要判断用户是否有对
/project/123/document/456的读权限,传统做法需要逐级查找。 - 优化:
- 用户权限存储时,存储成路径格式:
/project/123/read。 - 判断时,在Trie树中查找最长匹配前缀。
- 优点:查找速度快于逐级递归,且支持通配符(如
/project/123/*)。
- 用户权限存储时,存储成路径格式:
更快的CASL(能力安全授权语言)或ABAC(基于属性的访问控制)引擎
- CASL:一种前端友好的权限定义方式,将复杂的权限逻辑(如“只有作者可以编辑自己的文章”)编译成高效的JSON规则。
- ABAC:将权限判断抽象为对“属性”的匹配(如
subject.role == 'admin' OR (subject.id == resource.owner_id)),优秀的ABAC引擎(如Open Policy Agent, OPA)会将策略编译成IR(Intermediate Representation,中间表示),用并行BDD(二元决策图)进行判断,性能极高。
具体优化清单(按优先级排序)
| 优化手段 | 预期提速效果 | 实施难度 | 适用场景 |
|---|---|---|---|
| 预计算 + Redis缓存 | 10x - 100x | 低 | 所有系统首选 |
| 减少冗余查询 | 2x - 5x | 低 | 优化现有代码(如批量获取) |
| 本地进程缓存 | 100x - 1000x | 中 | 读多写少的高并发场景 |
| 位图(Bitmap) | 1000x+ | 高 | 权限点固定且极大规模 |
| 布隆过滤器 | 减少80%数据库压力 | 中 | 防刷、防未授权访问 |
| ABAC引擎(如OPA) | 10x(相比传统逐级判断) | 高 | 复杂策略、动态环境 |
最快的权限判断,不判断”或“几乎不判断”。
- 第一步:停止在请求中做循环查库。
- 第二步:采用 预计算 + Redis,这是性价比最高的方案,能解决90%的性能问题。
- 第三步:如果不够,加入 本地缓存。
- 第四步:如果还是不够(比如上亿请求),考虑 位图 或 布隆过滤器。
常见误区:过度优化,如果你的系统每秒只有几百个请求,用预计算 + Redis就足够了,不需要引入复杂的位图或ABAC引擎,复杂度应与业务规模相匹配。
标签: 规则索引