> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Lottery 类

> 讲解如何使用 Illuminate\Support\Lottery 优雅地实现概率性操作。同时介绍在包开发中的用法与测试时的结果固定方法。

## 什么是 Lottery 类

`Illuminate\Support\Lottery` 是可以用流畅 API 描述基于概率操作的工具类。它可以简洁地表达「每 100 个请求执行一次」「只对部分请求记录详细日志」这类模式。

<Info>
  实现位于 `src/Illuminate/Support/Lottery.php`。Laravel 也在框架内部使用它，例如 Session GC、缓存锁的清理。
</Info>

```mermaid theme={null}
flowchart LR
  A["Lottery::odds(1, 100)"] --> B{"随机判定"}
  B -->|"中奖 (1%)"| C["执行 winner 回调"]
  B -->|"未中 (99%)"| D["执行 loser 回调"]
```

## 基本用法

### 用整数比率指定概率

`Lottery::odds($chances, $outOf)` 指定「$outOf 次中中 $chances 次」的概率。

```php theme={null}
use Illuminate\Support\Lottery;

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

### 用小数指定概率

省略 `$outOf` 而传入 `0.0`〜`1.0` 的小数时，会直接作为概率使用。

```php theme={null}
Lottery::odds(0.01)             // 1% 概率
    ->winner(fn () => $this->sample())
    ->choose();
```

<Warning>
  以小数指定时，值超过 `1.0` 会抛出 `RuntimeException`。
</Warning>

### 无回调时返回布尔值

若未设置 winner / loser，`choose()` 会返回：中奖时 `true`，未中时 `false`。

```php theme={null}
$shouldSample = Lottery::odds(1, 50)->choose(); // bool
```

### 多次执行

给 `choose($times)` 传入次数会返回结果数组。

```php theme={null}
$results = Lottery::odds(1, 2)
    ->winner(fn () => 'win')
    ->loser(fn () => 'lose')
    ->choose(10);

// 例如：['win', 'lose', 'win', 'win', 'lose', ...]
```

### 作为 Callable 传入

`Lottery` 实例实现了 `__invoke`，因此可以直接作为 callable 传入。

```php theme={null}
// 作为 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 次执行一次）

对于删除过期记录这类不需要每次都执行的维护处理非常合适。

```php theme={null}
Lottery::odds(1, 100)
    ->winner(fn () => Cache::store('database')->flush())
    ->choose();
```

### 2. 遥测采样（只对部分请求记录详细日志）

若对所有请求记日志成本过高，可以用它做采样。

```php theme={null}
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 测试式的行为切换

概率性地把用户分配到两条不同的代码路径。

```php theme={null}
$result = Lottery::odds(1, 2)
    ->winner(fn ($user) => $this->newCheckoutFlow($user))
    ->loser(fn ($user) => $this->legacyCheckoutFlow($user))
    ->choose();
```

### 4. 作为 Scheduler 的辅助随机执行定时任务

在多台服务器中避免重复执行的同时随机执行某个任务时可以使用。

```php theme={null}
// 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()`，但思路是一致的。

<Steps>
  <Step title="Session：垃圾回收">
    `Illuminate\Session\Middleware\StartSession::configHitsLottery()` 使用 `config/session.php` 中的 `lottery` 配置，通过 `random_int` 概率判定并执行 GC。

    ```php theme={null}
    // 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];
    }
    ```
  </Step>

  <Step title="DatabaseLock：清理过期锁">
    `Illuminate\Cache\DatabaseLock::acquire()` 在每次获取锁时以相同比率清理过期锁。

    ```php theme={null}
    // 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();
    }
    ```
  </Step>

  <Step title="DB::whenQueryingForLongerThan — 传入 Lottery 类的示例">
    `Lottery` 实例可作为 callable 传入，可直接用于慢查询检测回调。

    ```php theme={null}
    DB::whenQueryingForLongerThan(
        Interval::seconds(5),
        Lottery::odds(1, 5)->winner(function ($connection) {
            Log::warning("Slow query on {$connection->getName()}");
        })
    );
    ```
  </Step>
</Steps>

<Tip>
  Session 与 DatabaseLock 直接使用 `random_int()`，而 `Lottery` 类的优势在于测试时可以通过 `alwaysWin()` / `alwaysLose()` / `fix()` 控制结果。在包开发中选用 `Lottery` 类可提高可测试性。
</Tip>

## 测试时的用法

对包含随机性的代码测试，可使用 `Lottery` 提供的测试用 API。

### `Lottery::alwaysWin()` — 始终中奖

```php theme={null}
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()` — 始终未中

```php theme={null}
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` 数组控制多次调用的结果。

```php theme={null}
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(); // 测试后务必恢复
}
```

<Warning>
  `alwaysWin()` / `alwaysLose()` / `fix()` 会改变全局静态属性。请务必在测试的 `tearDown()` 中调用 `Lottery::determineResultNormally()`。
</Warning>

```php theme={null}
protected function tearDown(): void
{
    Lottery::determineResultNormally();

    parent::tearDown();
}
```

### `Lottery::setResultFactory()` — 注入自定义 Factory

如需更细粒度的控制，可使用自定义 Factory。

```php theme={null}
Lottery::setResultFactory(function ($chances, $outOf) {
    // 始终中奖的自定义逻辑
    return true;
});

// 测试后重置
Lottery::determineResultNormally();
```

## 在包开发中的应用

### 在 Service Provider 中注册

在包 Service Provider 中集成维护处理时，可用 `Lottery` 分散负载。

```php theme={null}
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();
    }
}
```

### 从配置读取赔率

允许通过配置文件调整概率，方便用户微调。

```php theme={null}
$lottery = config('acme.prune_lottery', [1, 100]);

Lottery::odds(...$lottery)
    ->winner(fn () => $this->pruneExpiredRecords())
    ->choose();
```

```php theme={null}
// config/acme.php
return [
    // [中奖数, 试次数] = 每 100 次请求清理 1 次
    'prune_lottery' => [1, 100],
];
```

### 在 Middleware 中做采样

```php theme={null}
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)` | 注入自定义判定逻辑                    |

## 相关页面

<Columns cols={2}>
  <Card title="Macroable trait" icon="puzzle-piece" href="/zh/advanced/macroable">
    学习向既有类添加新方法的扩展模式。
  </Card>

  <Card title="Conditionable trait" icon="git-branch" href="/zh/advanced/conditionable">
    学习基于 `when()` / `unless()` 的条件分支链式设计。
  </Card>
</Columns>


## Related topics

- [Fluent 类](/zh/advanced/fluent.md)
- [字符串操作（Str 类）](/zh/strings.md)
- [用于 Laravel AI SDK 的 Amazon Bedrock 驱动](/zh/packages/laravel-amazon-bedrock.md)
- [邮件发送](/zh/mail.md)
- [Eloquent 自定义类型转换](/zh/advanced/eloquent-casts.md)
