什么是 Lottery 类
Illuminate\Support\Lottery 是可以用流畅 API 描述基于概率操作的工具类。它可以简洁地表达「每 100 个请求执行一次」「只对部分请求记录详细日志」这类模式。
实现位于 src/Illuminate/Support/Lottery.php。Laravel 也在框架内部使用它,例如 Session GC、缓存锁的清理。
基本用法
用整数比率指定概率
Lottery::odds($chances, $outOf) 指定「outOf次中中chances 次」的概率。
use Illuminate\Support\Lottery;
Lottery::odds(1, 100) // 100 次中 1 次
->winner(fn () => $this->runMaintenance())
->loser(fn () => null)
->choose();
用小数指定概率
省略 $outOf 而传入 0.0〜1.0 的小数时,会直接作为概率使用。
Lottery::odds(0.01) // 1% 概率
->winner(fn () => $this->sample())
->choose();
以小数指定时,值超过 1.0 会抛出 RuntimeException。
无回调时返回布尔值
若未设置 winner / loser,choose() 会返回:中奖时 true,未中时 false。
$shouldSample = Lottery::odds(1, 50)->choose(); // bool
多次执行
给 choose($times) 传入次数会返回结果数组。
$results = Lottery::odds(1, 2)
->winner(fn () => 'win')
->loser(fn () => 'lose')
->choose(10);
// 例如:['win', 'lose', 'win', 'win', 'lose', ...]
作为 Callable 传入
Lottery 实例实现了 __invoke,因此可以直接作为 callable 传入。
// 作为 DB::whenQueryingForLongerThan 的第二个参数
DB::whenQueryingForLongerThan(
Interval::seconds(5),
Lottery::odds(1, 5)->winner(function ($connection) {
// 检测到慢查询时以 1/5 概率发送告警
Alert::send("Slow query on {$connection->getName()}");
})
);
实战用例
1. 缓存清理(每 100 次执行一次)
对于删除过期记录这类不需要每次都执行的维护处理非常合适。
Lottery::odds(1, 100)
->winner(fn () => Cache::store('database')->flush())
->choose();
2. 遥测采样(只对部分请求记录详细日志)
若对所有请求记日志成本过高,可以用它做采样。
Lottery::odds(1, 20)
->winner(function () use ($request) {
Log::channel('telemetry')->info('Request sampled', [
'url' => $request->url(),
'duration' => microtime(true) - LARAVEL_START,
'memory' => memory_get_peak_usage(true),
]);
})
->choose();
3. A/B 测试式的行为切换
概率性地把用户分配到两条不同的代码路径。
$result = Lottery::odds(1, 2)
->winner(fn ($user) => $this->newCheckoutFlow($user))
->loser(fn ($user) => $this->legacyCheckoutFlow($user))
->choose();
4. 作为 Scheduler 的辅助随机执行定时任务
在多台服务器中避免重复执行的同时随机执行某个任务时可以使用。
// app/Console/Kernel.php
$schedule->call(function () {
Lottery::odds(1, 3)
->winner(fn () => Artisan::call('cache:prune-stale-tags'))
->choose();
})->everyMinute();
Laravel 框架内部的概率模式
Laravel 在框架内部广泛使用了概率式维护处理。其中一些实现是在 Lottery 类被加入之前编写的,因此直接使用 random_int(),但思路是一致的。
Session:垃圾回收
Illuminate\Session\Middleware\StartSession::configHitsLottery() 使用 config/session.php 中的 lottery 配置,通过 random_int 概率判定并执行 GC。// config/session.php
'lottery' => [2, 100], // 每 100 次请求执行 2 次
// StartSession 内部(直接使用 random_int)
protected function configHitsLottery(array $config): bool
{
return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
}
DatabaseLock:清理过期锁
Illuminate\Cache\DatabaseLock::acquire() 在每次获取锁时以相同比率清理过期锁。// config/cache.php(database 驱动)
'lock_lottery' => [2, 100], // 每 100 次执行 2 次清理
// DatabaseLock::acquire() 内部(直接使用 random_int)
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
$this->pruneExpiredLocks();
}
DB::whenQueryingForLongerThan — 传入 Lottery 类的示例
Lottery 实例可作为 callable 传入,可直接用于慢查询检测回调。DB::whenQueryingForLongerThan(
Interval::seconds(5),
Lottery::odds(1, 5)->winner(function ($connection) {
Log::warning("Slow query on {$connection->getName()}");
})
);
Session 与 DatabaseLock 直接使用 random_int(),而 Lottery 类的优势在于测试时可以通过 alwaysWin() / alwaysLose() / fix() 控制结果。在包开发中选用 Lottery 类可提高可测试性。
测试时的用法
对包含随机性的代码测试,可使用 Lottery 提供的测试用 API。
Lottery::alwaysWin() — 始终中奖
public function test_maintenance_runs_on_win(): void
{
$ranMaintenance = false;
Lottery::alwaysWin(function () use (&$ranMaintenance) {
Lottery::odds(1, 100)
->winner(function () use (&$ranMaintenance) {
$ranMaintenance = true;
})
->choose();
});
$this->assertTrue($ranMaintenance);
}
Lottery::alwaysLose() — 始终未中
public function test_maintenance_skipped_on_lose(): void
{
$ranMaintenance = false;
Lottery::alwaysLose(function () use (&$ranMaintenance) {
Lottery::odds(1, 100)
->winner(function () use (&$ranMaintenance) {
$ranMaintenance = true;
})
->choose();
});
$this->assertFalse($ranMaintenance);
}
Lottery::fix() — 用序列固定结果
可用 true/false 数组控制多次调用的结果。
public function test_alternating_results(): void
{
Lottery::fix([true, false, true, false]);
$results = Lottery::odds(1, 100)
->winner(fn () => 'winner')
->loser(fn () => 'loser')
->choose(4);
$this->assertSame(['winner', 'loser', 'winner', 'loser'], $results);
Lottery::determineResultNormally(); // 测试后务必恢复
}
alwaysWin() / alwaysLose() / fix() 会改变全局静态属性。请务必在测试的 tearDown() 中调用 Lottery::determineResultNormally()。
protected function tearDown(): void
{
Lottery::determineResultNormally();
parent::tearDown();
}
Lottery::setResultFactory() — 注入自定义 Factory
如需更细粒度的控制,可使用自定义 Factory。
Lottery::setResultFactory(function ($chances, $outOf) {
// 始终中奖的自定义逻辑
return true;
});
// 测试后重置
Lottery::determineResultNormally();
在包开发中的应用
在 Service Provider 中注册
在包 Service Provider 中集成维护处理时,可用 Lottery 分散负载。
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
class AcmeServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->app->booted(function () {
Lottery::odds(1, 100)
->winner(fn () => $this->pruneExpiredRecords())
->choose();
});
}
protected function pruneExpiredRecords(): void
{
$this->app['db']->table('acme_logs')
->where('expires_at', '<', now())
->delete();
}
}
从配置读取赔率
允许通过配置文件调整概率,方便用户微调。
$lottery = config('acme.prune_lottery', [1, 100]);
Lottery::odds(...$lottery)
->winner(fn () => $this->pruneExpiredRecords())
->choose();
// config/acme.php
return [
// [中奖数, 试次数] = 每 100 次请求清理 1 次
'prune_lottery' => [1, 100],
];
在 Middleware 中做采样
use Illuminate\Support\Lottery;
class SampleTelemetryMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
Lottery::odds(1, 50)
->winner(fn () => $this->recordTelemetry($request, $response))
->choose();
return $response;
}
}
API 参考
| 方法 | 说明 |
|---|
Lottery::odds($chances, $outOf) | 创建 Lottery 实例的静态工厂 |
->winner(callable $callback) | 设置中奖时的回调 |
->loser(callable $callback) | 设置未中时的回调 |
->choose($times = null) | 执行 Lottery。指定 $times 时返回数组 |
Lottery::alwaysWin($callback) | 测试用:始终中奖 |
Lottery::alwaysLose($callback) | 测试用:始终未中 |
Lottery::fix($sequence) | 测试用:用序列固定结果 |
Lottery::determineResultNormally() | 重置测试用固定 |
Lottery::setResultFactory(callable) | 注入自定义判定逻辑 |
相关页面
Macroable trait
学习向既有类添加新方法的扩展模式。
Conditionable trait
学习基于 when() / unless() 的条件分支链式设计。