针对日志并发问题,在自动存点中增加Redis分布式冷却锁,杜绝进程重复触发导致经验暴涨,完善单元测试

This commit is contained in:
pllx
2026-07-06 22:43:32 +08:00
parent a20bcf13be
commit 01a8e116a3
2 changed files with 43 additions and 1 deletions
+11 -1
View File
@@ -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++;
}
+32
View File
@@ -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);
}
}