第3章 ACL存储方案:选对存储,系统才能跑得稳

ACL数据存哪儿?这个问题看似简单,但选错了方案,后面有你受的。我见过不少团队,一开始随便塞进关系型数据库,结果权限查询越来越慢;也有人一股脑全丢进Redis,结果重启后权限丢了,用户全炸了。

说白了,ACL存储没有银弹。你得根据业务场景、数据规模、性能要求来权衡。这一章,我把三种主流方案掰开揉碎讲清楚。

3.1 基于关系型数据库的ACL存储

关系型数据库(MySQL、PostgreSQL)是ACL存储最传统、最稳妥的方案。为什么?因为ACL天然就是「用户-资源-权限」的三元组关系,关系型数据库最擅长处理这个。

3.1.1 标准表结构设计

我个人习惯用三张核心表来承载ACL数据:

表名 字段 说明
user id, name, role_id 用户表,关联角色
resource id, type, path 资源表,比如文档、API、菜单
acl_entry id, user_id, resource_id, permission, effect ACL条目,permission是read/write/delete,effect是allow/deny

嗯,这里要注意:effect字段一定要保留。我在项目中遇到过,有人只存allow,不存deny,结果想拒绝某个用户访问特定资源时,只能靠「不存记录」来隐式拒绝。这其实是个坑——万一漏了一条记录,权限就失控了。

-- 创建ACL条目表的DDL示例
CREATE TABLE acl_entry (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    resource_id BIGINT NOT NULL,
    permission VARCHAR(20) NOT NULL,  -- 'read', 'write', 'delete'
    effect VARCHAR(10) NOT NULL,      -- 'allow', 'deny'
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_resource_perm (user_id, resource_id, permission)
);

3.1.2 查询性能与索引策略

关系型数据库的ACL查询,最怕全表扫描。你想想看,每次用户请求一个资源,都要查一遍所有ACL规则,那系统不崩才怪。

我建议至少建两个复合索引:

  • (user_id, resource_id):快速定位某个用户对某个资源的所有权限
  • (resource_id, permission):快速查找某个资源上具有特定权限的所有用户
小技巧:如果ACL条目超过百万级,可以考虑按resource_id做分区表。我曾经帮一个SaaS平台做过改造,分区后查询耗时从800ms降到了15ms。

3.2 基于NoSQL的ACL存储

关系型数据库虽然稳,但遇到高并发、大流量场景,它的ACL查询就成了瓶颈。这时候,NoSQL就派上用场了。

3.2.1 文档型数据库(MongoDB)

MongoDB存ACL,最大的好处是灵活。你可以把某个用户的所有权限聚合成一个文档,一次查询全拿到。

// MongoDB ACL文档示例
{
  "_id": "user_1001",
  "acls": [
    { "resource": "/api/v1/documents", "permissions": ["read", "write"] },
    { "resource": "/api/v1/admin", "permissions": ["read"] },
    { "resource": "/api/v1/reports", "permissions": [] }
  ]
}

这种设计,查询时只需要一次文档读取,不需要JOIN。但有个问题——如果资源粒度很细,比如每个文档ID都不同,那这个文档会变得巨大无比。

避坑指南:我曾经见过一个团队,把每个用户的ACL都存成一个MongoDB文档,结果用户权限列表超过10MB,每次查询都卡死。后来改成按资源类型聚合,才解决问题。

3.2.2 键值型数据库(Redis)

Redis存ACL,核心思路是用Set或Hash来存储权限关系。比如:

# 用Set存储用户拥有的资源权限
SADD user:1001:permissions "doc:123:read"
SADD user:1001:permissions "doc:123:write"
SADD user:1001:permissions "report:456:read"

# 检查权限
SISMEMBER user:1001:permissions "doc:123:read"
# 返回1表示有权限

Redis的Set操作是O(1)的,性能极好。但你要记住:Redis是内存数据库,数据持久化需要额外配置。我建议用RDB+AOF双保险,防止重启丢数据。

3.3 内存缓存策略

不管用关系型还是NoSQL,磁盘IO始终是瓶颈。真正的生产环境,ACL数据一定要进内存缓存。

3.3.1 缓存什么?

不是所有ACL数据都适合缓存。我总结了三类:

  • 热数据:频繁访问的权限规则,比如管理员权限、公共资源权限
  • 用户会话级数据:用户登录后,把他的ACL一次性加载到缓存
  • 静态资源权限:比如菜单、按钮级别的权限,几乎不变

3.3.2 缓存更新策略

缓存最怕什么?数据不一致。权限改了,缓存还是旧的,用户要么多出权限,要么少了权限,都很危险。

我推荐两种策略:

  1. 主动失效:修改ACL后,立即删除对应的缓存key。下次查询时重新加载。
  2. 版本号机制:给每个用户的ACL加一个版本号,缓存中存版本号。查询时对比版本号,不一致就刷新。
核心原则:ACL缓存宁可没有,也不能过期不更新。安全第一,性能第二。

3.3.3 本地缓存 vs 分布式缓存

方案 优点 缺点 适用场景
本地缓存(Caffeine/Guava) 无网络开销,速度极快 多实例间数据不一致 单机部署、小规模系统
分布式缓存(Redis Cluster) 数据一致,支持水平扩展 有网络延迟 微服务、集群部署

我个人习惯的做法是:本地缓存做一级缓存,Redis做二级缓存。先查本地,命中直接返回;没命中再查Redis;Redis也没有,才去数据库查。这样既快又稳。

3.4 三种方案对比与选型建议

说了这么多,到底怎么选?我画了一张图,帮你理清思路。

ACL存储方案选型决策图 ACL存储方案 关系型数据库 NoSQL 内存缓存 MySQL / PostgreSQL MongoDB / Redis 文档型 / 键值型 本地 / 分布式 选型建议: • 数据一致性要求高 → 关系型数据库 + 本地缓存 • 高并发、低延迟 → NoSQL + 分布式缓存 • 混合场景 → 关系型做持久化,Redis做缓存层 • 千万级ACL条目 → 考虑分库分表 + 缓存预热

最后说句实在话:没有完美的存储方案,只有适合你业务的方案。我见过用MySQL撑起日活千万的系统,也见过用Redis做ACL结果丢数据被骂上热搜的。关键是你得理解每种方案的优缺点,然后根据业务场景做权衡。

嗯,这一章的内容就到这儿。记住:ACL存储选型,稳字当头,性能次之,安全永远第一。