分布式服务把相同或不同职责的服务拆到多台服务器上协同对外提供能力。权限控制一旦进入这种部署方式,问题就不只是“能否鉴权”,还包括“各节点上的策略是否一致”。对于使用 PHP-Casbin 的常驻内存服务来说,Enforcer 往往以单例形式存在,策略会长期保留在内存中,这让访问性能很可观,但也意味着某个节点的策略变更不会天然同步到其他节点。
这篇文章聚焦的就是这类场景:当 PHP 运行在 Swoole、WorkerMan、ReactPHP 一类常驻进程框架中时,怎样借助 Watcher 把策略变更广播出去,并让其他实例自动刷新。读完后,你可以判断自己的服务是否需要额外做策略同步,以及 Redis Watcher 在整条链路中承担了什么角色。
为什么常驻内存服务需要 Watcher
在传统 PHP-FPM 模式下,请求到来时通常会重新初始化 Enforcer 并重新加载策略,因此不需要额外考虑实例间的策略同步问题。
PHP-FPM 下每次都重新初始化
Enforcer并重新加载策略,不需要做额外的策略同步。
但常驻内存服务不一样。无论是单实例多进程,还是多实例分布式部署,进程中的策略一旦加载完成,后续请求大多直接读取内存数据。如果其中一个进程或节点修改了策略,其他进程并不会自动得知这次变化,结果就是不同服务节点可能基于不同版本的策略做鉴权。
这正是 Casbin 提供 Watcher 的原因。它负责把“策略已经发生变化”这件事通知给其他实例,让各自的 Enforcer 执行刷新动作,而不是继续使用过期策略。
Watcher 的同步原理是什么
整体机制并不复杂:当某个实例的 Enforcer 中策略发生变化时,会调用 Watcher,由它向消息队列(MQ)推送一条通知。其他正在监听该消息的 Enforcer 收到通知后,会自动刷新当前实例中的策略。

换句话说,Watcher 同步的不是整份策略内容本身,而是“需要重新加载策略”的信号。真正的策略数据仍然来自你配置的存储层,例如数据库适配器。
这套方式主要用于常驻进程框架中的 Casbin 集成场景,例如 Swoole、WorkerMan、ReactPHP 等。文章提到的两类情况都适用:
- 单实例、多进程:不同 worker 之间数据隔离。
- 多实例部署:不同服务器或不同容器上的服务节点各自维护内存策略。
只要策略会被缓存进内存,并且更新后需要尽快传播到别的执行单元,Watcher 就有实际价值。
演示环境与依赖安装
本文采用 PHP 8.4、Swoole 作为演示环境,并使用 Redis watcher for PHP-Casbin in Swoole 作为 Watcher 实现。
先通过 composer 安装依赖:
composer require casbin/casbin
composer require casbin/dbal-adapter
composer require casbin/swoole-redis-watcher
这里的职责可以简单理解为:
casbin/casbin:权限模型与决策器本体。casbin/dbal-adapter:把策略存到数据库,并负责读取。casbin/swoole-redis-watcher:借助 Redis 发布/订阅,向其他实例广播策略变更通知。
Swoole 中如何接入 PHP-Casbin Watcher
先初始化 Swoole 的 Server,启动一个常驻内存的 HTTP 服务:

use CasbinEnforcer;
use CasbinAdapterDBALAdapter;
use CasbinWatcherSwooleRedisWatcher;
use SwooleHttpServer;
Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);
$serv = new Server('127.0.0.1', 9501);
$serv->set(['worker_num' => 4]);
这里的重点不是 HTTP 服务本身,而是它会常驻运行,并且配置了 worker_num 为 4。这意味着每个 worker 都可能持有自己的一份内存策略,因此后续的同步机制才有意义。
接着在服务启动后,通过回调初始化 Casbin 决策器,并把 Watcher 绑定到 Enforcer 上:
$serv->on('WorkerStart', function($server, $worker_id) {
global $enforcer;
$adapter = Adapter::newAdapter([
'driver' => 'pdo_mysql',
'host' => '127.0.0.1',
'dbname' => 'test',
'user' => 'root',
'password' => '',
'port' => '3306',
]);
$enforcer = new Enforcer('./path/to/model.conf', $adapter);
// 设置 Watcher
$watcher = new Watcher([
'host' => '127.0.0.1',
'password' => '',
'port' => 6379,
'database' => 0,
]);
$enforcer->setWatcher($watcher);
});
$serv->start();
这段代码表达了两个关键动作。
先让策略有统一来源
Adapter::newAdapter() 配置的是数据库连接,说明策略最终仍以数据库为准。各个 worker 或实例中的 Enforcer 只是把策略加载到内存中使用。
再把变更通知能力挂到 Enforcer 上
创建 Watcher 后,通过 $enforcer->setWatcher($watcher) 完成绑定。这样一来,某个实例中的策略发生变更时,Watcher 就能把通知发出去;其他订阅者收到消息后,再触发自己的策略刷新。
对于常驻内存架构来说,真正需要关心的并不是“每次请求都重新拉策略”,而是“在策略有变化时,让所有节点及时知道并重载”。这也是 Watcher 的设计价值所在。
Redis Watcher 在 Swoole 里做了什么
Swoole Watcher 的实现依赖 Redis 的发布和订阅能力。它在 Swoole 环境中通过协程异步、非阻塞地订阅 Redis 消息,一旦监听到策略变更通知,就会触发回调,随后调用决策器的 loadPolicy(),把最新策略重新加载到当前实例中。
因此整条链路可以概括为:
- 某个实例修改策略。
Watcher向 Redis 发布一条变更消息。- 其他实例或 worker 收到订阅消息。
- 对应的
Enforcer执行loadPolicy()。 - 所有节点重新回到一致的策略状态。
这种方式的优点是简单直接,而且比较契合常驻进程模型:请求处理链路仍然走内存数据,只有在策略变化时才通过消息机制触发刷新,不会把每次鉴权都变成远程读取。
适用场景与实践判断
如果你的 PHP 应用仍然主要运行在 PHP-FPM 下,那么通常不需要专门引入 Watcher,因为每次请求都会重建上下文并重新加载策略。
但如果已经转向常驻内存运行方式,比如 Swoole、FrankenPHP、RoadRunner、ReactPHP、WorkerMan 等,情况就不同了。此时 PHP-Casbin 的内存缓存优势会更明显,同时策略同步也会成为必须补上的一环。
本文示例展示的就是一种典型做法:使用数据库适配器保存策略,再用 Redis Watcher 负责变更通知,让各个进程或实例在收到消息后调用 loadPolicy() 完成刷新。对于分布式、多 worker、常驻内存服务,这是一套比较自然且实用的同步方案。
PHP-Casbin 还有更多扩展,可用于不同框架和运行环境的集成,项目地址为:https://github.com/php-casbin。









