RBAC(基于角色的访问控制)是后台管理系统中最常用的权限设计模型,几乎每个后台系统都有权限管理的需求。但是很多人设计的RBAC权限系统,在数据量大了之后,性能会很差,特别是权限校验的时候,需要查很多次数据库,导致接口响应很慢。我之前接手的一个项目就是这样,RBAC权限校验要查七八次数据库,每次接口响应都要几百毫秒,用户体验很差。后来我对RBAC权限设计做了一系列性能优化,把权限校验的时间从几百毫秒降到了几毫秒,提升了几十倍。今天就来分享一下这次RBAC权限设计性能优化的实战过程。

一、先看问题:原来的RBAC设计有多慢

我接手这个项目的时候,后台系统已经上线了一段时间,用户量也不小,有几百个用户,几十个角色,几百个权限点。但是用户普遍反馈系统很慢,特别是后台管理页面,点一下要等好几秒才能出来。

我一开始以为是业务逻辑的问题,后来用Xdebug做了性能分析,才发现大部分时间都花在了权限校验上。原来的RBAC权限校验流程是这样的:

  1. 根据用户ID查询用户的角色ID列表,查一次用户角色关联表。
  2. 根据角色ID列表查询角色的权限ID列表,查一次角色权限关联表。
  3. 根据权限ID列表查询权限的详细信息,查一次权限表。
  4. 还要查询用户的直接权限(有些用户有单独的权限,不通过角色),再查一次用户权限关联表。
  5. 然后在代码里把这些权限合并、去重,得到用户的所有权限列表。
  6. 最后判断当前访问的节点是否在用户的权限列表里。

整个流程要查四五次数据库,而且每次查询都有IN查询,数据量大了之后很慢。更糟糕的是,这个权限校验是在每个请求的中间件里执行的,也就是说,每个接口请求都要做这么一次权限校验,都要查四五次数据库,这就导致每个接口都很慢,数据库压力也很大。

而且原来的设计还有一个问题,就是权限数据没有缓存,每次请求都要重新查数据库,就算用户的权限没有变化,也要重新查,这就更浪费了。

找到问题之后,我就开始想怎么优化,目标是把权限校验的时间降到10毫秒以内,最好是几毫秒,而且要尽量减少数据库查询。

二、优化第一步:数据库层面的优化

首先从数据库层面优化,这是最基础的,也是效果最明显的。

第一个优化是加索引。原来的用户角色表、角色权限表、用户权限表,关联字段都没有加索引,查询的时候都是全表扫描,当然慢。我给这些关联字段都加上了索引,比如用户角色表的userid和roleid,角色权限表的roleid和permissionid,用户权限表的userid和permissionid,都加上了索引。加了索引之后,查询速度直接从几百毫秒降到了几十毫秒,效果立竿见影。

第二个优化是减少查询次数。原来要查四五次,我把它合并成一次查询,用联表查询,把用户、角色、权限三张表关联起来,一次查询就能查出用户的所有权限。SQL大概是这样的:

SELECT DISTINCT p.* FROM user_role ur
JOIN role_permission rp ON ur.role_id = rp.role_id
JOIN permission p ON rp.permission_id = p.id
WHERE ur.user_id = ?
UNION
SELECT p.* FROM user_permission up
JOIN permission p ON up.permission_id = p.id
WHERE up.user_id = ?

这样一次查询就能查出用户的所有权限,包括通过角色的和直接给用户的,用UNION合并,还能自动去重。查询次数从四五次降到了一次,性能又提升了不少。

第三个优化是只查需要的字段。原来的查询是SELECT *,把权限表的所有字段都查出来了,但是实际上权限校验只需要权限的标识(比如controller/action)或者权限ID,其他字段根本用不到。我把查询改成只查需要的字段,比如只查permission_code字段,这样数据传输量小了,查询也更快了。

经过数据库层面的优化,权限校验的时间从几百毫秒降到了十几毫秒,已经有了很大的提升,但是我觉得还不够,还能更快。

三、优化第二步:引入缓存,减少数据库查询

数据库优化完之后,虽然已经很快了,但是每个请求还是要查一次数据库,而且用户的权限不是经常变化的,可能几天甚至几周才变一次,每次都查数据库太浪费了。所以第二步就是引入缓存,把用户的权限数据缓存起来,不用每次都查数据库。

我用的是Redis做缓存,缓存的策略是这样的:

  1. 用户第一次访问的时候,从数据库查询用户的所有权限,然后存到Redis里,设置过期时间,比如1小时。
  2. 后续用户访问的时候,直接从Redis里取权限数据,不用查数据库。
  3. 当用户的权限发生变化的时候(比如给用户分配角色、修改角色权限),主动删除这个用户的缓存,下次访问的时候就会重新从数据库加载最新的权限。

这样做之后,大部分请求都直接从Redis取数据,Redis的读取速度是微秒级的,比数据库快得多,权限校验的时间直接从十几毫秒降到了1毫秒以内,几乎不耗时。

但是引入缓存之后,也要注意几个问题:

第一个是缓存一致性的问题。当用户权限变化的时候,一定要记得删除缓存,不然用户会看到旧的权限,或者新权限不生效。我一开始就踩了这个坑,修改角色权限的时候,只更新了数据库,没有删缓存,结果用户的权限没有变化,查了半天才发现是缓存的问题。后来我封装了一个权限服务,所有修改权限的操作都通过这个服务来做,在服务里统一处理缓存删除,就不会漏了。

第二个是缓存穿透的问题。如果查询一个不存在的用户ID,缓存里没有,数据库里也没有,每次都会查数据库,被恶意攻击的话数据库会挂。我的解决方法是,不存在的用户也缓存一个空值,设置短一点的过期时间,比如5分钟,这样就不会每次都查数据库了。

第三个是缓存雪崩的问题。如果大量用户的缓存同时过期,这时候所有请求都打到数据库,数据库压力会很大。我的解决方法是,给缓存的过期时间加一个随机值,比如基础时间1小时,加0到30分钟的随机数,这样缓存就不会同时过期了,避免了雪崩。

还有一个问题是,缓存里存什么?我一开始是把用户的所有权限列表都存到Redis里,用Hash或者Set结构。后来发现,权限校验其实只需要判断某个权限标识是否在用户的权限集合里,所以我用了Redis的Set结构,把用户的所有权限标识存到一个Set里,校验的时候用SISMEMBER命令判断是否存在,时间复杂度是O(1),非常快。而且Set结构还能自动去重,很适合存权限集合。

四、优化第三步:代码层面的优化

缓存加完之后,性能已经非常好了,但是我还在代码层面做了一些优化,进一步提升性能和可维护性。

第一个优化是权限校验的逻辑优化。原来的权限校验是把用户的所有权限都取出来,然后在代码里遍历,判断当前访问的节点是否在权限列表里,这样时间复杂度是O(n),权限多了之后会慢。我改成了用Set结构,不管是从Redis取还是在内存里,判断是否存在都是O(1),不管权限有多少,速度都一样。

第二个优化是超级管理员的特殊处理。超级管理员拥有所有权限,不需要校验,原来的代码还是会查超级管理员的权限,然后判断,这其实是浪费。我加了一个判断,如果用户是超级管理员,直接放行,不用查缓存也不用查数据库,这样超级管理员的访问速度更快。

第三个优化是请求级别的缓存。同一个用户的同一个请求,可能会在多个地方做权限校验,比如中间件校验一次,控制器里又校验一次,原来的代码每次都要从Redis取一次。我加了一个请求级别的缓存,把用户的权限数据存在请求的上下文里,同一个请求里多次校验只需要取一次,减少了Redis的访问次数。虽然Redis很快,但是能省一次是一次。

第四个优化是权限预加载。有些接口需要判断多个权限,比如一个页面上有多个按钮,每个按钮都要判断是否有权限,原来的代码每个按钮都要判断一次,每次都要访问Redis。我改成了在页面渲染之前,一次性把用户的所有权限都取出来,存在内存里,页面渲染的时候直接从内存里判断,不用每次都访问Redis,大大提升了页面渲染速度。

第五个优化是权限标识的设计。原来的权限标识设计得不太合理,有的用ID,有的用名称,有的用controller/action,不统一,校验的时候还要转换。我统一了权限标识的设计,都用"模块:操作"的格式,比如"user:add"、"article:edit",这样既清晰又方便校验,不用转换,直接判断就行。

五、优化效果和总结

经过这一系列的优化,RBAC权限校验的性能有了质的飞跃:

  • 优化前:每个请求要查四五次数据库,权限校验耗时几百毫秒,数据库压力大。
  • 优化后:大部分请求直接从Redis取数据,权限校验耗时1毫秒以内,数据库压力大大降低。

性能提升了几十倍甚至上百倍,用户体验好了很多,后台页面打开速度明显变快了,数据库的负载也降下来了,原来高峰期数据库CPU经常跑到80%以上,现在基本在30%左右。

总结一下这次RBAC权限性能优化的经验:

  1. 先测量再优化:不要瞎猜瓶颈在哪里,用性能分析工具找到真正的瓶颈,然后针对性优化。我一开始以为是业务逻辑慢,后来才发现是权限校验慢。
  2. 数据库优化是基础:加索引、减少查询次数、只查需要的字段,这些基础的优化能带来很大的提升,而且成本很低。
  3. 缓存是性能优化的大杀器:对于不经常变化的数据,一定要加缓存,能大大减少数据库压力,提升响应速度。但是要注意缓存一致性、穿透、雪崩等问题。
  4. 代码层面的优化不能少:好的代码结构和算法,能进一步提升性能,也能提升可维护性。
  5. 不要过度优化:优化到一定程度就够了,不要为了追求极致的性能把代码搞得很复杂,得不偿失。性能优化要投入产出比,花最少的精力获得最大的提升。

RBAC权限设计看起来简单,但是要做好、做快,还是有很多学问的。希望我的这次实战经验能帮到大家,让大家的RBAC权限系统又快又稳。

最后用一句话结尾:"性能优化不是一蹴而就的,是一个不断测量、不断优化的过程。"找到瓶颈,解决问题,一步一步来,你的系统一定会越来越快。