2023-10-12技术00
请注意,本文编写于 49 天前,最后修改于 49 天前,其中某些信息可能已经过时。

问题描述

在Golang服务端使用Casbin作为权限管理的场景下,会增加多个Casbin实例来保证请求高并发和稳定性,但是与此同时会出现多个实例接口返回的数据不一致的问题,这种问题会严重影响业务使用。

问题原因

在Casbin的设计中,为了保证高并发下的可观性能,Casbin内置一个内存缓存,每当执行读数据的动作时,会先获取内存缓存中的数据,而不是数据库的数据。

因此在多实例场景下,当一个实例的Casbin数据发生变更时,其它实例的Casbin缓存并没有及时同步刷新,这就导致了当请求被分发到对应的实例上时,获取到的依然还是旧的错误数据。

问题解决

Casbin官方提供了一种可行的解决方法——Watcher机制,可以阅读对应文档CasbinWathcer

文档中说明了casbin支持使用Redis、Kafka、zookeeper等中间件实现多实例之间的数据同步

我采用了成本较低的Redis作为Watcher,下面是我的代码参考:

  • Watcher回调函数:
// 这是一个包装方法,方便传入Enforcer实例来实现回调中对casbin的操作
func ConsumeCallback(e *casbin.Enforcer) func(msg string) {
	return func(msg string) {
		zap.L().Info("enforcer update msg:" + msg)
                // 在接收到任意的权限变更后,刷新Enforce实例,达到刷新缓存的目的
		e.LoadPolicy()
	}
}

需要注意的是我们这里给到的回调函数必须是func(msg string)的形式,因为Casbin Watcher规定了回调到的方法只会给到一个Msg信息,剩下的需要我们自己处理。

具体的MSG信息官方文档并没有给出,不过我在实际测试过程中有总结到一个大的结构体对象可以供有需要的进行参考

// 回调中给到的msg的结构大概就是下面struct中定义的样子
type MSG struct {
	Method      string
	ID          string
	Sec         string
	Ptype       string
	Params      interface{}
	NewRules    interface{}
	OldRules    interface{}
	NewRule     interface{}
	OldRule     interface{}
	FieldValues interface{}
}

func (m *MSG) MarshalBinary() ([]byte, error) {
	return json.Marshal(m)
}

// UnmarshalBinary decodes the struct into a User
func (m *MSG) UnmarshalBinary(data []byte) error {
	if err := json.Unmarshal(data, m); err != nil {
		return err
	}
	return nil
}
  • Casbin注册回调函数
func (c *CasbinOptions) Init(db *gorm.DB) (*casbin.Enforcer, error) {
	w, _ := rediswatcher.NewWatcher(viper.GetString("redis.addr"), rediswatcher.WatcherOptions{
		Options: redis.Options{
			Network:  "tcp",
			Password: "redis.password",
			DB:       "redis.iotDB",
			PoolSize: "redis.poolSize",
		},
		Channel: "/casbin",
		// 忽略容器自身发起的订阅消息推送,防止出现死循环
		IgnoreSelf: true,
	})

	adapter, err := gormadapter.NewAdapterByDB(db)
	if err != nil {
		zap.L().Error("casbin adapter init error:" + err.Error())
		return nil, err
	}
	e, err := casbin.NewEnforcer(c.Path, adapter)
	if err != nil {
		zap.L().Error("casbin enforcer init error:" + err.Error())
		return nil, err
	}
        // 设置RedisWatcher
	_ = e.SetWatcher(w)
        // 设置回调函数
	_ = w.SetUpdateCallback(ConsumeCallback(e))
	err = e.LoadPolicy()
	if err != nil {
		zap.L().Error("casbin policy init error:" + err.Error())
		return nil, err
	}
	_ = e.SavePolicy()
	return e, nil

}

完成后重新运行验证效果即可

本文作者:伞菌

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!