From 01a8e116a371c188a7cbbafbc833f66b34f0e805 Mon Sep 17 00:00:00 2001 From: pllx Date: Mon, 6 Jul 2026 22:43:32 +0800 Subject: [PATCH] =?UTF-8?q?=E9=92=88=E5=AF=B9=E6=97=A5=E5=BF=97=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E9=97=AE=E9=A2=98=EF=BC=8C=E5=9C=A8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=AD=98=E7=82=B9=E4=B8=AD=E5=A2=9E=E5=8A=A0Redis=E5=88=86?= =?UTF-8?q?=E5=B8=83=E5=BC=8F=E5=86=B7=E5=8D=B4=E9=94=81=EF=BC=8C=E6=9D=9C?= =?UTF-8?q?=E7=BB=9D=E8=BF=9B=E7=A8=8B=E9=87=8D=E5=A4=8D=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=BB=8F=E9=AA=8C=E6=9A=B4=E6=B6=A8=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/AutoSaveExp.php | 12 ++++++++- tests/Feature/AutoSaveExpCommandTest.php | 32 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index 2ec825d..32c8747 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -91,17 +91,27 @@ class AutoSaveExp extends Command foreach ($roomMap as $roomId => $usernames) { foreach ($usernames as $username) { - // 如果本轮已经处理过该用户,跳过发放经验(防止多房间/多开重复吃经验) + // 1. 如果本轮已经处理过该用户,跳过发放经验(防止多房间/多开重复吃经验) if (isset($processedUsernames[$username])) { continue; } + // 2. 利用 Redis 缓存锁,防止在短时间内(200秒内)重复进行自动存点奖励(防御多进程并发/重复调度) + $cooldownKey = "user:auto_save_cooldown:{$username}"; + if (Redis::exists($cooldownKey)) { + continue; + } + $user = $usersByUsername->get($username); if (! $user) { continue; } $this->processUser($user, $roomId, $expGainRaw, $jjbGainRaw, $superLevel); + + // 3. 设置冷却标记,200秒后过期(本任务5分钟运行一次,200秒冷却期既防重复,又不影响下一次存点) + Redis::setex($cooldownKey, 200, 1); + $processedUsernames[$username] = true; $totalProcessed++; } diff --git a/tests/Feature/AutoSaveExpCommandTest.php b/tests/Feature/AutoSaveExpCommandTest.php index 4bdeec5..b3792b4 100644 --- a/tests/Feature/AutoSaveExpCommandTest.php +++ b/tests/Feature/AutoSaveExpCommandTest.php @@ -155,4 +155,36 @@ class AutoSaveExpCommandTest extends TestCase $user->refresh(); $this->assertEquals(15, $user->exp_num); } + + /** + * 测试自动存点命令会通过 Redis 冷却锁,防止在短时间内并发重复调度执行。 + */ + public function test_auto_save_cooldown_prevents_overlapping_process(): void + { + Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '5']); + Cache::flush(); + + $room = Room::create(['room_name' => 'cd']); + $user = User::factory()->create([ + 'username' => 'cooldown-user', + 'exp_num' => 10, + ]); + + Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE)); + Redis::setex("room:{$room->id}:alive:{$user->username}", 90, 1); + + // 第一次执行自动存点 + $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); + + // 验证第一次加了经验 (10 + 5 = 15) + $user->refresh(); + $this->assertEquals(15, $user->exp_num); + + // 紧接着在冷却时间内(200秒内)模拟第二次并发执行 + $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); + + // 验证因为有 Redis 冷却锁,第二次执行被拦截,经验仍为 15,并未再次增加 + $user->refresh(); + $this->assertEquals(15, $user->exp_num); + } }