第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):快速查找某个资源上具有特定权限的所有用户
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都不同,那这个文档会变得巨大无比。
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 缓存更新策略
缓存最怕什么?数据不一致。权限改了,缓存还是旧的,用户要么多出权限,要么少了权限,都很危险。
我推荐两种策略:
- 主动失效:修改ACL后,立即删除对应的缓存key。下次查询时重新加载。
- 版本号机制:给每个用户的ACL加一个版本号,缓存中存版本号。查询时对比版本号,不一致就刷新。
3.3.3 本地缓存 vs 分布式缓存
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地缓存(Caffeine/Guava) | 无网络开销,速度极快 | 多实例间数据不一致 | 单机部署、小规模系统 |
| 分布式缓存(Redis Cluster) | 数据一致,支持水平扩展 | 有网络延迟 | 微服务、集群部署 |
我个人习惯的做法是:本地缓存做一级缓存,Redis做二级缓存。先查本地,命中直接返回;没命中再查Redis;Redis也没有,才去数据库查。这样既快又稳。
3.4 三种方案对比与选型建议
说了这么多,到底怎么选?我画了一张图,帮你理清思路。
最后说句实在话:没有完美的存储方案,只有适合你业务的方案。我见过用MySQL撑起日活千万的系统,也见过用Redis做ACL结果丢数据被骂上热搜的。关键是你得理解每种方案的优缺点,然后根据业务场景做权衡。
嗯,这一章的内容就到这儿。记住:ACL存储选型,稳字当头,性能次之,安全永远第一。