fix(tongji): 修复小游戏平台同步与并发问题

This commit is contained in:
大哥大哥的大哥哥
2026-07-17 21:21:43 +08:00
parent 7192c4db8c
commit fd7e6071a8
8 changed files with 262 additions and 50 deletions
+100 -42
View File
@@ -195,18 +195,16 @@ class GamePlatformLogic
}
Db::startTrans();
$exists = Db::name('tcm_game_share_visit')
->where('inviter_user_id', $inviterUserId)
->where('visitor_user_id', $userId)
->lock(true)
->find();
if (!$exists) {
Db::name('tcm_game_share_visit')->insert([
'invite_code' => $inviteCode,
'inviter_user_id' => $inviterUserId,
'visitor_user_id' => $userId,
'create_time' => time(),
]);
$inserted = Db::name('tcm_game_share_visit')->duplicate([
'invite_code',
])->insert([
'invite_code' => $inviteCode,
'inviter_user_id' => $inviterUserId,
'visitor_user_id' => $userId,
'create_time' => time(),
]);
$accepted = $inserted === 1;
if ($accepted) {
Db::name('tcm_game_share_invite')->where('id', (int) $invite['id'])->inc('open_count')->update([
'update_time' => time(),
]);
@@ -215,7 +213,7 @@ class GamePlatformLogic
$inviter = Db::name('user')->where('id', $inviterUserId)->field('nickname')->find();
return [
'accepted' => !$exists,
'accepted' => $accepted,
'message' => '已加入控糖消消乐',
'inviter' => self::displayName((string) ($inviter['nickname'] ?? ''), $inviterUserId),
];
@@ -228,15 +226,18 @@ class GamePlatformLogic
private static function ensureWeeklyScore(int $userId, string $weekStart): array
{
$profile = self::userProfile($userId);
$now = time();
$existing = Db::name('tcm_game_weekly_score')
->where('week_start', $weekStart)
->where('user_id', $userId)
->lock(true)
->find();
$profile = self::userProfile($userId);
$now = time();
if ($existing) {
$existing = Db::name('tcm_game_weekly_score')
->where('id', (int) $existing['id'])
->lock(true)
->find();
$profileChanged = (string) $existing['nickname'] !== $profile['nickname']
|| (string) $existing['avatar'] !== $profile['avatar'];
if ($profileChanged) {
@@ -251,6 +252,35 @@ class GamePlatformLogic
return $existing;
}
// 每周、每个性别使用一行分配锁串行化首次入组。直接 upsert 锁行,
// 避免空分组上的间隙锁导致首批并发请求互相等待或偶发死锁。
Db::name('tcm_game_weekly_allocator')->duplicate([
'update_time',
])->insert([
'week_start' => $weekStart,
'sex' => $profile['sex'],
'create_time'=> $now,
'update_time'=> $now,
]);
$allocator = Db::name('tcm_game_weekly_allocator')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
->lock(true)
->find();
if (!$allocator) {
throw new \RuntimeException('同行分配锁创建失败');
}
// 等待分配锁期间,同一用户的另一个请求可能已经完成分配。
$existing = Db::name('tcm_game_weekly_score')
->where('week_start', $weekStart)
->where('user_id', $userId)
->lock(true)
->find();
if ($existing) {
return $existing;
}
$group = Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
@@ -265,7 +295,11 @@ class GamePlatformLogic
->where('sex', $profile['sex'])
->max('group_no');
$groupNo = $maxGroupNo + 1;
$groupId = Db::name('tcm_game_weekly_group')->insertGetId([
// 首批用户并发进入时可能同时算出相同 group_no。利用唯一键 upsert
// 让请求汇合到同一组,再锁定该组继续分配,避免偶发 1062/死锁。
Db::name('tcm_game_weekly_group')->duplicate([
'update_time',
])->insert([
'week_start' => $weekStart,
'sex' => $profile['sex'],
'group_no' => $groupNo,
@@ -273,10 +307,18 @@ class GamePlatformLogic
'create_time' => $now,
'update_time' => $now,
]);
$group = ['id' => $groupId, 'member_count' => 0];
$group = Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
->where('group_no', $groupNo)
->lock(true)
->find();
if (!$group) {
throw new \RuntimeException('同行分组创建失败');
}
}
$scoreId = Db::name('tcm_game_weekly_score')->insertGetId([
$scoreRow = [
'group_id' => (int) $group['id'],
'week_start' => $weekStart,
'user_id' => $userId,
@@ -289,25 +331,34 @@ class GamePlatformLogic
'sex' => $profile['sex'],
'create_time' => $now,
'update_time' => $now,
]);
Db::name('tcm_game_weekly_group')->where('id', (int) $group['id'])->update([
'member_count' => min(self::GROUP_SIZE, (int) $group['member_count'] + 1),
'update_time' => $now,
]);
return [
'id' => $scoreId,
'group_id' => (int) $group['id'],
'week_start' => $weekStart,
'user_id' => $userId,
'learned_count' => 0,
'best_score' => 0,
'games_played' => 0,
'share_count' => 0,
'nickname' => $profile['nickname'],
'avatar' => $profile['avatar'],
'sex' => $profile['sex'],
];
$inserted = Db::name('tcm_game_weekly_score')->duplicate([
'nickname',
'avatar',
'sex',
'update_time',
])->insert($scoreRow);
$score = Db::name('tcm_game_weekly_score')
->where('week_start', $weekStart)
->where('user_id', $userId)
->lock(true)
->find();
if (!$score) {
throw new \RuntimeException('同行成绩创建失败');
}
// 仅新插入时刷新人数;使用实际成绩行数纠正历史并发造成的计数漂移。
if ($inserted === 1) {
$memberCount = (int) Db::name('tcm_game_weekly_score')
->where('group_id', (int) $group['id'])
->count();
Db::name('tcm_game_weekly_group')->where('id', (int) $group['id'])->update([
'member_count' => min(self::GROUP_SIZE, $memberCount),
'update_time' => $now,
]);
}
return $score;
}
private static function buildLeaderboard(
@@ -435,11 +486,12 @@ class GamePlatformLogic
for ($attempt = 0; $attempt < 5; $attempt++) {
$code = strtoupper(bin2hex(random_bytes(6)));
if (Db::name('tcm_game_share_invite')->where('invite_code', $code)->find()) {
continue;
}
$now = time();
Db::name('tcm_game_share_invite')->insert([
// 同一用户并发打开榜单时,以 week_start + user_id 唯一键汇合;
// 极小概率随机码撞车时,查询不到本人的记录就继续生成新码。
Db::name('tcm_game_share_invite')->duplicate([
'update_time',
])->insert([
'invite_code' => $code,
'user_id' => $userId,
'week_start' => $weekStart,
@@ -447,7 +499,13 @@ class GamePlatformLogic
'create_time' => $now,
'update_time' => $now,
]);
return $code;
$invite = Db::name('tcm_game_share_invite')
->where('week_start', $weekStart)
->where('user_id', $userId)
->find();
if ($invite) {
return (string) $invite['invite_code'];
}
}
throw new \RuntimeException('分享码生成失败');
}