Skip to content
Go
分布式与高性能

分布式与高性能

单实例的 Casbin 很简单;多实例部署时,“各实例内存中的策略如何保持一致"成为核心问题。本章讲并发安全、多实例同步(Watcher)、判定缓存与性能优化。

问题全景

              ┌──────────────┐
   写策略 ───→ │  实例 A      │──┐
              │  内存策略 ✅  │  │ AutoSave
              └──────────────┘  ▼
              ┌──────────────┐  DB(casbin_rule)✅
              │  实例 B      │
              │  内存策略 ❌旧 │ ← 谁来通知它重载?→ Watcher
              └──────────────┘
问题 解决方案
单实例内并发读写策略 SyncedEnforcer
多实例间策略同步(最终一致) Watcher(Redis pub/sub 等)
多实例强一致 Dispatcher(Raft,极少需要)
判定太频繁 / 策略太多 CachedEnforcer、BatchEnforce、过滤加载

SyncedEnforcer:单实例并发安全

e, err := casbin.NewSyncedEnforcer("model.conf", adapter)
  • 内部用 sync.RWMutex:Enforce 拿读锁(并发不互斥),Add/Remove/LoadPolicy 拿写锁
  • API 与普通 Enforcer 完全一致,无迁移成本
// 附赠:定时自动重载(简易同步方案,无 Redis 时可用)
e.StartAutoLoadPolicy(30 * time.Second) // 每 30s LoadPolicy 一次
defer e.StopAutoLoadPolicy()

💡 定时重载实现简单,但有最长 30s 的不一致窗口,且实例多时对 DB 有周期性压力。正规方案是 Watcher。

Watcher:多实例最终一致同步

🔬 深入原理

Watcher 是发布/订阅模式:任一实例修改策略 → 通过消息通道广播 → 其他实例收到后 LoadPolicy

实例 A: AddPolicy ──→ DB 落库(AutoSave)
                 └──→ Watcher.Update() ──→ Redis channel "/casbin"
                                              │
实例 B/C: 订阅该 channel ──收到──→ callback ──→ e.LoadPolicy() → 重载最新策略

一致性为最终一致(毫秒级延迟)。权限场景下这几乎总是可接受的——放行一个刚被撤权的请求几百毫秒,风险远小于引入强一致的复杂度。

redis-watcher 实战

go get github.com/casbin/redis-watcher/v2
import (
	"log"

	"github.com/casbin/casbin/v3"
	rediswatcher "github.com/casbin/redis-watcher/v2"
	"github.com/redis/go-redis/v9"
)

func initEnforcer() (*casbin.SyncedEnforcer, error) {
	w, err := rediswatcher.NewWatcher("localhost:6379", rediswatcher.WatcherOptions{
		Options: redis.Options{
			Network:  "tcp",
			Password: "",
		},
		Channel:    "/casbin",
		IgnoreSelf: true, // 忽略自己发出的通知(自己已经是最新的)
	})
	if err != nil {
		return nil, err
	}

	e, err := casbin.NewSyncedEnforcer("model.conf", adapter)
	if err != nil {
		return nil, err
	}

	// 绑定 watcher;之后所有策略修改 API 自动广播
	if err := e.SetWatcher(w); err != nil {
		return nil, err
	}
	// 收到通知时的动作:默认回调 = 重载策略
	if err := w.SetUpdateCallback(rediswatcher.DefaultUpdateCallback(e)); err != nil {
		return nil, err
	}
	return e, nil
}

Redis Cluster 环境用 rediswatcher.NewWatcherWithCluster("host1:6379,host2:6379,...", ...);已有 go-redis 客户端实例也可复用(NewWatcherWithExistingClient 系列,见其 README)。

🚨 陷阱IgnoreSelf: false 时实例会收到自己发的通知并再做一次 LoadPolicy——多余的全量重载。生产设为 true

🚨 陷阱:Redis pub/sub 是 fire-and-forget——订阅者断线期间的消息直接丢失。Watcher 断连重连后应主动 LoadPolicy() 一次补偿;再配合 StartAutoLoadPolicy(5 * time.Minute) 做低频兜底,双保险。

其他 Watcher 实现

Watcher 通道 备注
redis-watcher Redis pub/sub 最常用,依赖最普及
etcd-watcher etcd watch K8s 体系内自然
kafka / NATS / RocketMQ watcher 消息队列 已有 MQ 基础设施时

WatcherEx:增量通知(进阶)

标准 Watcher 通知不带内容,回调只能全量 LoadPolicy。实现了 WatcherEx 接口的 watcher 会携带"哪条策略被增/删"的信息,接收方可增量更新内存,免去全量重载。策略十万级、变更频繁时才值得关注。

Dispatcher:强一致集群(了解即可)

DistributedEnforcer + Dispatcher(如 casbin/hraft-dispatcher)用 Raft 把策略变更做成共识日志,所有实例强一致。代价是引入 Raft 集群的全部运维复杂度。除非合规要求"撤权必须瞬时全局生效”,否则 Watcher 足够

CachedEnforcer:判定结果缓存

e, _ := casbin.NewSyncedCachedEnforcer("model.conf", adapter)
e.EnableCache(true)
e.SetExpireTime(60 * time.Second) // 缓存过期时间

ok, _ := e.Enforce("alice", "/api/articles/1", "GET") // 第一次:真实判定
ok, _ = e.Enforce("alice", "/api/articles/1", "GET")  // 第二次:直接命中缓存

e.InvalidateCache() // 策略变更后清空缓存(写 API 会自动清)
  • 缓存 key 为请求参数拼接,value 为判定结果
  • 适合:策略多(判定慢)+ 请求模式集中(同样的判定反复出现)

🚨 陷阱:CachedEnforcer 的缓存 key 只支持字符串参数,ABAC 传结构体的场景无法缓存(也不该缓存——属性会变)。另外多实例下 Watcher 触发 LoadPolicy 后要确认缓存同步失效(SyncedCachedEnforcer 的 LoadPolicy 会清缓存)。

性能优化清单

1. 策略规模是第一因素。Enforce ≈ O(策略数 × matcher 复杂度):

策略量 单次 Enforce 量级(普通 matcher)
1 千条 ~0.1 ms
1 万条 ~1 ms
10 万条 ~10 ms,必须优化

优化手段按优先级:过滤加载(每实例只装自己租户的策略,见 05 章)→ CachedEnforcer → 模型简化。

2. matcher 里最贵的放最后&& 短路求值:把 == 等便宜条件放前面,regexMatch/eval 放最后,不匹配的策略提前出局。

# 好:先比 act(大量策略在此淘汰),再做昂贵的路径匹配
m = r.act == p.act && g(r.sub, p.sub) && keyMatch2(r.obj, p.obj)

3. 批量判定用 BatchEnforce。渲染菜单/按钮权限时一次问 N 个:

results, _ := e.BatchEnforce([][]any{
	{"alice", "/api/articles", "POST"},
	{"alice", "/api/users", "GET"},
	{"alice", "/api/admin", "GET"},
})
// [true, true, false] —— 一次锁获取,N 次判定

4. 前端权限用 Implicit API 一次取全。逐个 Enforce 生成权限清单不如 GetImplicitPermissionsForUser(user) 一次拿出所有权限交给前端自行匹配。

5. 避免不必要的 LoadPolicy。全量重载会重建角色图,十万条策略需数百毫秒(期间写锁阻塞所有 Enforce)。能增量就不全量(WatcherEx),能过滤就不全装。

多实例部署完整模板

// 生产推荐组合:SyncedEnforcer + gorm-adapter(AutoSave) + redis-watcher + 低频兜底重载
func MustInitAuthz(db *gorm.DB, redisAddr string) *casbin.SyncedEnforcer {
	a, err := gormadapter.NewAdapterByDB(db)
	if err != nil {
		log.Fatal(err)
	}
	m, err := model.NewModelFromString(modelText)
	if err != nil {
		log.Fatal(err)
	}
	e, err := casbin.NewSyncedEnforcer(m, a)
	if err != nil {
		log.Fatal(err)
	}

	w, err := rediswatcher.NewWatcher(redisAddr, rediswatcher.WatcherOptions{
		Channel:    "/casbin",
		IgnoreSelf: true,
	})
	if err != nil {
		log.Fatal(err)
	}
	if err := e.SetWatcher(w); err != nil {
		log.Fatal(err)
	}
	if err := w.SetUpdateCallback(rediswatcher.DefaultUpdateCallback(e)); err != nil {
		log.Fatal(err)
	}

	e.StartAutoLoadPolicy(5 * time.Minute) // 兜底:防 pub/sub 丢消息
	return e
}

常见陷阱

🚨 多实例只配了 adapter 没配 Watcher:实例 A 改的策略,实例 B 永远看不到(直到重启)。用户反馈"权限改了不生效,过一会又生效了"基本就是这个问题。

🚨 Watcher 回调里没有 LoadPolicySetUpdateCallback(func(s string) { log.Println(s) }) 只打日志不重载,同步形同虚设。用 DefaultUpdateCallback(e)

🚨 依赖 pub/sub 不丢消息:Redis pub/sub 无持久化,断连即丢。必须有兜底(定时重载或重连后强制 LoadPolicy)。

🚨 高频写策略 + 全量重载风暴:批量导入 1 万条策略逐条 AddPolicy 会广播 1 万次通知,所有实例做 1 万次全量 LoadPolicy。批量操作用 AddPolicies(一次通知),或导入期间临时摘除 Watcher、完毕后手动广播一次。

🚨 把 Casbin 当强一致系统:Watcher 是最终一致。对"撤权即刻生效"有硬要求的操作(如冻结账户),在业务层加一道实时黑名单检查,不要指望毫秒级同步。