最简Feature Toggle表只需key(唯一索引)、enabled(TINYINT(1))、updated_at三列;读取须用Cache::remember()缓存60秒并主动forget刷新,禁用模型全局缓存;用户级策略应分离至无副作用的应用层策略类,而非数据库存储。
Feature Toggle 表结构怎么建才支持运行时开关
laravel 本身不内置 feature toggle 表,得自己建。最简可用的表只要三列:key(唯一标识,如 "new_checkout_flow")、enabled(布尔或 tinyint(1))、updated_at。别加多余字段,比如 environment 或 user_id——除非你明确需要按环境/用户灰度,否则会立刻让查询变慢、缓存失效变复杂。
-
key建唯一索引,避免重复插入 -
enabled用TINYINT(1)而非BOOLEAN(MySQL 的 BOOLEAN 实际是 TINYINT,但显式声明更稳妥) - 不要设
created_at,开关状态只看最后更新时间,updated_at足够
示例迁移:
php artisan make:migration create_feature_flags_table
在迁移中写:
Schema::create('feature_flags', function (Blueprint $table) {
$table->string('key')->primary();
$table->tinyInteger('enabled')->default(0);
$table->timestamps();
});Laravel 中如何安全读取开关状态而不拖慢请求
直接每次查数据库?不行。哪怕加了索引,高并发下也会成为瓶颈,且违背 Feature Toggle “快速决策” 的本意。必须加一层缓存,但不能简单用 Cache::get() + 永不过期——那样改了数据库也刷不出来。
- 用
Cache::remember()包裹数据库查询,TTL 设为 30–60 秒(足够短以保证可控,足够长以扛住峰值) - 键名固定格式,比如
"feature<em>flag</em>{$key}",避免键冲突 - 在写入开关时,主动清除对应缓存:
Cache::forget("feature<em>flag</em>{$key}") - 别用 Redis 的 keys * 扫描清理,线上禁止
示例读取封装(放在 app/Services/FeatureToggle.php):
public function isEnabled(string $key): bool
{
return Cache::remember("feature_flag_{$key}", 60, function () use ($key) {
return FeatureFlag::whereKey($key)->value('enabled') === 1;
});
}数据库改完开关后,为什么代码里还是旧值
这是最常踩的坑:缓存没清,或清错了键。尤其当用了 Laravel 的模型缓存包(如 genealabs/laravel-model-caching),它可能自动缓存了整张表,导致单条更新不触发刷新。
- 检查是否启用了全局模型缓存,如果是,关掉对
FeatureFlag模型的缓存,或手动调用FeatureFlag::flushCache() - 确认清除缓存的代码真被执行了——比如你在 Tinker 里改数据库,但没走应用层的 update 接口,那缓存当然不会清
- 开发环境开
LOG_QUERIES和CACHE_LOG,看实际有没有命中缓存、有没有执行forget()
临时排查命令:
php artisan tinker<br>>> Cache::get('feature_flag_new_checkout_flow')怎么让开关支持“仅对某类用户生效”而不炸掉性能
硬编码用户 ID 列 or JSON 字段?别。一来查询无法走索引,二来缓存粒度爆炸(每个用户一个 key?)。正确做法是分层:先用全局开关兜底,再叠加策略逻辑。
数据库只存
key和enabled,表示“默认状态”用户级规则放到应用层:比如用
App\FeatureStrategies\PercentageStrategy类判断当前用户是否命中 10% 流量切换策略时,只改配置(如 config/features.php),不动数据库;数据库只管“开/关”这个二元事实
如果真要存策略参数到 DB,单独建
feature_flag_strategies表,关联feature_flags.key,但查询时用with()预加载,别 N+1策略类必须无副作用、无外部依赖(比如不能查用户积分表),否则会拖慢所有请求
典型误用:
// ❌ 不要在 isEnabled() 里查 users 表<br>if ($flag->enabled && User::where('id', auth()->id())->where('score', '>', 100)->exists()) { ... }开关不是权限系统,别让它承担不该背的逻辑。


















