Casbin匹配器缓存:一次权限校验不再路过编译器
【免费下载链接】casbinApache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC.项目地址: https://gitcode.com/GitHub_Trending/ca/casbin
你有没有发现,服务在高并发下 Enforce() 变慢,火焰图里全是表达式编译的帧?Casbin 的匹配器缓存就是为这个问题准备的:matcher 字符串只编译一次,之后每次权限校验都直接从缓存拿编译好的表达式树用,单次校验开销直接降一个量级。
先搞清楚一个前提
编译器才是瓶颈。
在 Casbin 里,matcher(模型文件里那句"如果…就…"的判词,比如r.sub == p.sub && keyMatch(r.obj, p.obj))本质上是一行文本,govaluate(把字符串变成可执行表达式树的表达式引擎)必须为它做一遍词法分析和语法分析。没有缓存,每次权限校验都完整付一遍编译成本;有了缓存,只有第一次付,后面每次都是一次查表,区别就这么点。
[配图建议:Enforcer、matcherMap 缓存与 govaluate 编译链的模块关系图]
它到底是怎么跑的:一次Casbin匹配器缓存命中的全程
跟着一笔 Enforce 调用走完整链路。
入口 enforcer.go 里的Enforce()最终都汇到enforce()。它先确定要用的 matcher 字符串:你传了自定义 matcher 就先做清理(去注释、转义特殊字符),没传就直接取模型文件里那句。然后**hasEval := util.HasEval(expString)**这一行决定了表达式命运——它里面包不含eval()(把某条 policy 规则字符串当表达式来求值的内建函数)?
为什么看这段:这里是缓存决策点,决定了哪些表达式绕过缓存、哪些走缓存。
hasEval := util.HasEval(expString) if hasEval { functions["eval"] = generateEvalFunction(functions, ¶meters) } var expression *govaluate.EvaluableExpression expression, err = e.getAndStoreMatcherExpression(hasEval, expString, functions)源码:enforcer.go
白话讲:matcher 含 eval() 就先注入动态 eval 函数,然后把参数交给缓存函数。
第二步查缓存在getAndStoreMatcherExpression里:
为什么看这段:它就是匹配器缓存的全部核心,命中与未命中两个分支就挤在这十几行里。
func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]govaluate.ExpressionFunction) (*govaluate.EvaluableExpression, error) { var expression *govaluate.EvaluableExpression var err error cachedExpression, isPresent := e.matcherMap.Load(expString) if !hasEval && isPresent { expression = cachedExpression.(*govaluate.EvaluableExpression) } else { expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions) if err != nil { return nil, err } e.matcherMap.Store(expString, expression) } return expression, nil }源码:enforcer.go
说白了,用 matcher 字符串本身当 key 去问matcherMap(一个自带并发安全的键值对):有,又不是 eval 表达式,直接拿;没有,重新编译再存进去,让下一次变成命中。
第三步是求值。缓存命中与否,下游完全无感:同一条expression.Eval(parameters),把当次的 subject、object、action 代进去算。有 policy 就逐条规则走一遍,拿编译好的表达式树当尺子量每条;没 policy 就只算一次。所以缓存只改变编译成本,不改判断逻辑。
链路还有最后一环:失效。**BuildRoleLinks()**和**BuildIncrementalRoleLinks()**一进门就调invalidateMatcherMap(),就一行e.matcherMap = sync.Map{}——整个缓存换成新的空缓存。这样角色继承关系一变,旧编译结果(里面带着旧的角色快照)就不会被误用。
什么时候你会真正感受到它的价值
效果最明显的就三种情况。
- 你的 API 网关每秒跑 5000 次 Enforce() 时:首发版本 CPU 曲线抬头,pprof(Go 的性能剖析工具)一打开,
NewEvaluableExpressionWithFunctions排在火焰图顶上;matcher 进缓存后这个函数只在首次跑一次,后面全是查表,火焰图立刻平下去。 - 你的策略表有几百条规则、用的是 RBAC-with-domains 这类 policy 模型时:体感是大策略表的 P99 延迟肉眼可见地降;它怎么兜底:编译只发生一次,剩下的全是随规则数线性增长的纯求值开销。
- 服务冷启动正好赶上 QPS 爬坡时:体感是权限模块的"预热期"从秒级缩到一次请求;它怎么兜底:第一个请求背下编译成本,之后全部走快路径。
用之前先看这里:匹配器缓存失效的边界
三个坑提前知道。
- ⚠️ 含 eval() 的表达式永远不会被缓存:
hasEval为真就强制重编译,因为 eval 函数闭包持有当前请求的 parameters,不能跨请求共享。你用 ABAC 却指望这份缓存提速,那不会发生。 - ⚠️ 失效绑定在角色链重建的 API 上:走官方
AddRoleForUser、LoadPolicy这些路径更新策略,缓存才会跟着清;绕过它们自己维护 RoleManager,缓存里的旧表达式可能一直拿着旧角色快照生效。 - ⚠️ 缓存只增不减:key 是 matcher 字符串,挂在 Enforcer 实例上,没有淘汰机制。运行时动态拼出一串新 matcher 再调
EnforceWithMatcher,它既永远不命中,还会让缓存条目越积越多。
你可以现在就去做的
三件事,现在就能开始。
- 用 pprof 确认
NewEvaluableExpressionWithFunctions是否还出现在你的热点路径里——在的话,多半是你的 matcher 字符串没保持恒定。 - 把 matcher 固定成字符串常量或模型配置里的句子,别用运行时字符串拼接动态 matcher,缓存 key 就是字符串本身。
- 从 enforcer.go 里
invalidateMatcherMap()的那一行读起,顺藤摸到 model/ 下模型加载的源码,看看策略更新后缓存为什么必须清掉 🚀
【免费下载链接】casbinApache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC.项目地址: https://gitcode.com/GitHub_Trending/ca/casbin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考