跳转到主要内容

什么是 Lottery 类

Illuminate\Support\Lottery 是可以用流畅 API 描述基于概率操作的工具类。它可以简洁地表达「每 100 个请求执行一次」「只对部分请求记录详细日志」这类模式。
实现位于 src/Illuminate/Support/Lottery.php。Laravel 也在框架内部使用它,例如 Session GC、缓存锁的清理。

基本用法

用整数比率指定概率

Lottery::odds($chances, $outOf) 指定「outOf次中中outOf 次中中 chances 次」的概率。
use Illuminate\Support\Lottery;

Lottery::odds(1, 100)           // 100 次中 1 次
    ->winner(fn () => $this->runMaintenance())
    ->loser(fn () => null)
    ->choose();

用小数指定概率

省略 $outOf 而传入 0.01.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(),但思路是一致的。
1

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];
}
2

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();
}
3

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() 的条件分支链式设计。
最后修改于 2026年7月13日