square1 / laravel-idempotency
为基于Laravel的API添加幂等性,防止重复请求处理。
Requires
- php: ^8.2
- illuminate/support: ^9.0|^10.0|^11.0
Requires (Dev)
- laravel/framework: ^11.0
- laravel/pint: ^1.14
- orchestra/testbench: ^9.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2024-09-21 10:46:11 UTC
README
为您的Laravel API添加幂等性
此包使您轻松将幂等性密钥支持添加到Laravel API中。当接收到带有先前使用的幂等性密钥的请求时,服务器将返回为原始请求生成的相同响应,并避免在服务器端进行任何重复处理。这在请求可能因网络问题或用户重试而重复的情况(如支付处理或表单提交)中特别有用。
目录
什么是幂等性?
幂等性是指能够多次执行相同的请求,并且只应用一次更改的能力。通过为每个传入请求添加一个唯一键,服务器可以检测到重复的请求。如果之前没有看到该请求,服务器可以安全地处理它。如果键之前已经出现过,服务器可以返回先前的响应,而无需重新处理请求。这在API客户端在不可靠的网络条件下操作时特别有用,其中请求在重新建立连接后会自动重试。
考虑向API发送支付请求的示例。
在这种情况下,客户端的连接性很差,因此并不总是收到“确认#1”响应。当连接丢失然后恢复时,客户端将重新发送请求。这里的风险是,原始请求已经正确处理,导致重试的事务处理了重复的支付请求,最终导致“确认#2”。
在上面的流程中,我们的用户将被收取两次费用。现在,让我们看看当相同的API和客户端实现幂等性时,相同的支付流程是如何工作的。
在这个流程中,客户端为每个请求生成一个幂等性键。第一次支付请求被发送,并且再次,客户端没有收到“确认#1”响应。然而,当重新尝试时,客户端正在发送相同的幂等性键。当发生这种情况时,服务器会识别出它已经处理了该请求。请求不会在后台重新处理(根本不会与银行通信来处理交易!)并再次返回原始响应(“确认#1”)。
包功能
- 幂等性键验证:确保每个API请求都是唯一的,并且只处理一次,防止重复操作。
- 用户特定缓存:幂等性键基于Laravel的默认认证对每个用户都是唯一的。
- 可定制的缓存持续时间:设置默认缓存持续时间并根据需要定制。
- 可配置的幂等性头:自定义幂等性头的名称。
- 灵活的用户ID检索:根据您应用程序的需求更改检索用户ID的方法。
安装
本文档涵盖在服务器端安装幂等性包。客户端生成的幂等性密钥的格式不由包强制规定。当前的最佳实践是使用V4 UUID或类似长度的随机键,具有足够的熵以避免冲突。
### 要求包
通过Composer
$ composer require square1/laravel-idempotency
包将自动注册。
发布配置(可选)
php artisan vendor:publish --provider="Square1\LaravelIdempotency\IdempotencyServiceProvider"
这将创建一个位于您的config
目录中的config/idempotency.php
文件。
配置
发布配置文件后,您可以根据需求对其进行修改。可用的主要配置选项包括:
cache_duration
:响应缓存时间,单位为秒。此值控制重用幂等键被视为新请求的周期。默认为1天。idempotency_header
:客户端用于传递幂等键的请求头名称。默认为Idempotency-Key
。ignore_empty_key
:默认情况下,如果我们期望传递幂等键但没有传递,将抛出MissingIdempotencyKeyException
异常。如果您想支持缺少键的请求,可以将此值设置为true
以防止抛出该异常。这在更新API客户端以推出键的期间可能很有用。默认为false
。enforced_verbs
:应用于幂等性检查的HTTP动词数组。通常GET和HEAD请求不改变状态,因此不需要幂等性检查,但如果您希望检查不同的动词,则可以编辑此数组。默认为['POST', 'PUT', 'PATCH', 'DELETE']
。on_duplicate_behaviour
:默认情况下,当幂等键被重复使用时,包将重新播放之前看到的响应。您的应用程序可能希望以不同的方式处理这种重复 - 例如抛出错误。将此值设置为exception
将在此情况下抛出DuplicateRequestException
异常。默认为replay
。max_lock_wait_time
:为了避免竞态条件,当请求到达时创建缓存锁。如果另一个请求在缓存填充之前到达但锁已被获取,则第二个请求将内部每秒轮询一次以查看缓存是否已填充,并在这些秒数后放弃。user_id_resolver
:幂等键对每个用户是唯一的,这意味着如果两个不同的用户以某种方式使用相同的键,则不会发生键冲突。这需要包在构建缓存时使用当前认证用户来构建缓存键。默认情况下,包将使用Laravel的auth()->user()->id
值。如果您想使用不同的值来唯一标识您的用户,可以将类-方法对添加到此值以实现该自定义值。
// Define custom resolver of per-user identifier. 'user_id_resolver' => [ExampleUserIdResolver::class, 'resolveUserId'],
// App\Services\ExampleUserIdResolver namespace App\Services; class ExampleUserIdResolver { public function resolveUserId() { // Implement custom logic to return the user ID return session()->special_user_token; } }
用法
通过中间件提供包的核心功能。要使用它,您只需将中间件添加到您的路由或控制器中。
注意 由于此包需要了解当前用户,请确保在执行任何用户身份验证操作之后添加中间件。
### 全局用法
Laravel 11+
要将中间件应用于所有路由,您可以将它追加到应用程序的app/bootstrap.php
文件中的全局中间件堆栈。
use Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware; ... ->withMiddleware(function (Middleware $middleware) { $middleware->append(IdempotencyMiddleware::class); })
这里的append
函数将此中间件添加到应用程序的全局中间件末尾。有关Laravel 11中处理中间件顺序的更多信息,请参阅文档。
Laravel <= 10
要将中间件应用于所有路由,请将其添加到app/Http/Kernel.php
中的$middlewareGroups
数组。
protected $middlewareGroups = [ 'api' => [ // other middleware... \Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware::class, ], ];
这将在此应用程序的所有路由上运行中间件。但是,包配置中的enforced_verbs
值将控制中间件是否对给定路由有任何影响(默认情况下,中间件不会干扰GET或HEAD请求)。
特定路由
Laravel 11+
您可以将中间件追加到所有API路由,利用Laravel的默认中间件组。
// app/boostrap.php use Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware; ... ->withMiddleware(function (Middleware $middleware) { $middleware->api(append: [ IdempotencyMiddleware::class ]); })
或者,您可以在路由文件中应用中间件到特定路由
Route::get('/profile', function () { // ... })->middleware(IdempotencyMiddleware::class);
Laravel <= 10
或者,您可以将中间件应用于特定路由
// App\Http\Kernel protected $middlewareAliases = [ ... 'idempotency' => \Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware::class, ... // routes/api.php Route::middleware('idempotency')->group(function () { Route::post('/your-api-endpoint', 'YourController@method'); // other routes });
识别幂等响应
当请求成功执行时,它将被返回给客户端,并且响应被缓存。在看到重复的幂等性键后,将返回此缓存值。在这种情况下,将返回一个额外的头信息 Idempotency-Relayed
。这个头信息包含了客户端发送的相同的幂等性键,并且是向客户端发出的信号,表示这个响应已被重复。这个头信息只出现在重复响应中,绝不会出现在原始响应中。
异常处理
此包可能会抛出多个异常,所有异常都位于 Square1\LaravelIdempotency\Exceptions
命名空间下
MismatchedPathException
:如果在具有相同幂等性键的重复请求中请求路径不同,则会抛出此异常。这通常是客户端存在错误的标志,例如,键在每次请求时没有更改,例如,客户端重复使用相同的键来请求POST /users
然后POST /accounts
。DuplicateRequestException
:默认情况下,当再次看到之前的幂等性键时,该包将重新播放响应。将配置值on_duplicate_behaviour
更改为exception
将导致抛出异常(对于应用程序来说,当重新发送的请求更可能是客户端的错误时非常有用)。LockExceededException
:为了避免具有相同键的两个请求之间的竞争条件,每个未看到已存在的缓存响应的请求首先尝试获取缓存锁。只有一个请求可以得到这个锁,因此失败的请求将定期轮询缓存以获取响应。如果等待时间超过config('idempotency.max_lock_wait_time')
中的值,将抛出LockExceededException
异常。MissingIdempotencyKeyException
:当由幂等性中间件处理的请求中没有键时,将抛出此异常。此检查是在enforced_verbs
检查之后执行的,因此,例如,如果GET请求不应由中间件考虑,则没有键的GET请求不会触发此异常。除非将配置值ignore_empty_key
更改为true
,否则将抛出此异常。
测试
$ composer test
许可协议
MIT许可协议(MIT)。请参阅 许可文件 了解更多信息。