英文:
How to prevent race condition when updating session value?
问题
在我的控制器中,我需要检查会话是否已过期,如果是,就更新值:
public function some_method(Request $request)
{
// 获取先前的最后活动时间
$session_last_activity = session('session_last_activity');
// 将最后活动时间更新为当前时间
session(['session_last_activity' => now()]);
if ($session_expiry_date < now()->subMinutes(10)) {
$some_value = Str::random(30);
session(['some_value' => $some_value]);
} else {
$some_value = session('some_value');
}
// 继续更新或创建数据库上的某些记录
}
问题出现在会话过期时,而且同一页有多个快速请求。在这种立即的请求中,每个请求都会创建一个新的随机字符串并更新会话。
因此,最后一个请求将拥有最后的会话值,然而在那时,数据库已经创建了多条记录,因为对于这些请求中的每一个,会话曾在某一时刻过期。
例如,用户在会话过期时快速点击了 example.com/page1
两次。现在有两个请求,它们读取了已过期的会话,设置了一个新的键,并使用两个不同的键创建了两条记录,尽管本应只有一条记录。
我该如何在不使用缓存的情况下,或不在数据库端(即使用数据库锁)而是在代码端防止这种情况发生?
英文:
In my Controller, I need to check if a session has expired, and if so, update the value:
public function some_method(Request $request)
{
// get previous last activity
$session_last_activity = session('session_last_activity');
// update last activity to now
session(['session_last_activity'] => now());
if ($session_expiry_date < now()->subMinutes(10)) {
$some_value = Str::random(30);
session(['some_value' => $some_value]);
} else {
$some_value = session('some_value');
}
// proceed to update or create some record on the DB
}
The problem occurs when the session is expired, and there are quick multiple requests to the same page. Then, for those immediate requests, each request creates a new random string and updates the session.
So the last request will have the last session value, however, by that time, the database created multiple records because, for each of these requests, the session expired for a moment.
For example, a user clicks example.com/page1
twice quickly while the session was expired. Now there are 2 requests that read the session as expired, set a new key, and create 2 records with 2 different keys, even though there was supposed to be only one.
How can I prevent that without using cache, or not on the DB side (i.e. with DB locks), but rather on the code side?
答案1
得分: 1
One approach is to use a flag stored in the session itself to indicate whether a request is already in the process of updating the session.
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
public function some_method(Request $request)
{
$session_expiry_date = session('session_expiry_date');
if ($session_expiry_date < now()) {
$lockKey = 'session_update_lock';
// Check if the session update lock flag is set
if (!Session::has($lockKey)) {
// Set the lock flag to prevent other requests from updating the session
Session::put($lockKey, true);
$some_value = Str::random(30);
// Update the session with the new value
session(['some_value' => $some_value]);
// Proceed to update or create the record on the DB
// ...
// Clear the lock flag to allow other requests to update the session
Session::forget($lockKey);
} else {
// Another request is already updating the session, so retrieve the value
$some_value = session('some_value');
}
} else {
$some_value = session('some_value');
}
// Proceed with the rest of the code
}
英文:
Answer update based on your question update:
One approach is to use a flag stored in the session itself to indicate whether a request is already in the process of updating the session.
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
public function some_method(Request $request)
{
$session_expiry_date = session('session_expiry_date');
if ($session_expiry_date < now()) {
$lockKey = 'session_update_lock';
// Check if the session update lock flag is set
if (!Session::has($lockKey)) {
// Set the lock flag to prevent other requests from updating the session
Session::put($lockKey, true);
$some_value = Str::random(30);
// Update the session with the new value
session(['some_value' => $some_value]);
// Proceed to update or create the record on the DB
// ...
// Clear the lock flag to allow other requests to update the session
Session::forget($lockKey);
} else {
// Another request is already updating the session, so retrieve the value
$some_value = session('some_value');
}
} else {
$some_value = session('some_value');
}
// Proceed with the rest of the code
}
答案2
得分: 0
要锁定会话,您可以在路由上使用 block
。每当有多个同时请求进入时,将为此请求获取会话锁定。
因此,如果此请求是用于 example.com/page1
,则代码片段如下:
Route::get('/page1', function () {
// 您的代码
})->block(5, 5);
但是,为了使其生效,您必须使用支持原子锁的缓存驱动程序。从文档中引用:
要使用会话阻塞功能,您的应用程序必须使用支持原子锁的缓存驱动程序。目前,这些缓存驱动程序包括memcached,dynamodb,redis和数据库驱动程序。此外,您不能使用cookie会话驱动程序。
英文:
To lock the session, you can use block
on your route. The session locking will be acquired for this request whenever many simultaneous requests are incoming.
So, if this request is for say example.com/page1
, then the snippet would be:
Route::get('/page1', function () {
// your code
})->block(5, 5);
However, for this to be in effect, you must be using a cache driver that supports atomic locks. Quoting from the docs:
> To utilize session blocking, your application must be using a cache
> driver that supports atomic locks. Currently, those cache drivers
> include the memcached, dynamodb, redis, and database drivers. In
> addition, you may not use the cookie session driver.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论