fix(tongji): 修复小游戏平台同步与并发问题
This commit is contained in:
@@ -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('分享码生成失败');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
-- 控糖消消乐:真实用户周榜、幂等局记录与轻量分享访问关系
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_allocator` (
|
||||
`week_start` date NOT NULL COMMENT '周一日期',
|
||||
`sex` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '同行组性别:0未知 1男 2女',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`week_start`, `sex`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐每周同行分组并发锁';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_group` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`week_start` date NOT NULL COMMENT '周一日期',
|
||||
@@ -68,6 +76,7 @@ CREATE TABLE IF NOT EXISTS `zyt_tcm_game_share_visit` (
|
||||
`visitor_user_id` int unsigned NOT NULL COMMENT '受邀打开用户ID',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_inviter_visitor` (`inviter_user_id`, `visitor_user_id`),
|
||||
UNIQUE KEY `uk_invite_visitor` (`invite_code`, `visitor_user_id`),
|
||||
KEY `idx_inviter` (`inviter_user_id`),
|
||||
KEY `idx_visitor` (`visitor_user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐分享打开记录(不做强绑定)';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
-- 仅用于已经执行过旧版小游戏建表脚本的数据库。
|
||||
-- 新安装直接执行 add_tcm_endless_game_platform.sql 即可。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_allocator` (
|
||||
`week_start` date NOT NULL COMMENT '周一日期',
|
||||
`sex` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '同行组性别:0未知 1男 2女',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`week_start`, `sex`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐每周同行分组并发锁';
|
||||
|
||||
SET @has_old_visit_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'zyt_tcm_game_share_visit'
|
||||
AND index_name = 'uk_inviter_visitor'
|
||||
);
|
||||
SET @drop_old_visit_index_sql = IF(
|
||||
@has_old_visit_index > 0,
|
||||
'ALTER TABLE `zyt_tcm_game_share_visit` DROP INDEX `uk_inviter_visitor`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE drop_old_visit_index_stmt FROM @drop_old_visit_index_sql;
|
||||
EXECUTE drop_old_visit_index_stmt;
|
||||
DEALLOCATE PREPARE drop_old_visit_index_stmt;
|
||||
|
||||
SET @has_invite_visit_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'zyt_tcm_game_share_visit'
|
||||
AND index_name = 'uk_invite_visitor'
|
||||
);
|
||||
SET @add_invite_visit_index_sql = IF(
|
||||
@has_invite_visit_index = 0,
|
||||
'ALTER TABLE `zyt_tcm_game_share_visit` ADD UNIQUE INDEX `uk_invite_visitor` (`invite_code`, `visitor_user_id`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE add_invite_visit_index_stmt FROM @add_invite_visit_index_sql;
|
||||
EXECUTE add_invite_visit_index_stmt;
|
||||
DEALLOCATE PREPARE add_invite_visit_index_stmt;
|
||||
|
||||
SET @has_inviter_index = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'zyt_tcm_game_share_visit'
|
||||
AND index_name = 'idx_inviter'
|
||||
);
|
||||
SET @add_inviter_index_sql = IF(
|
||||
@has_inviter_index = 0,
|
||||
'ALTER TABLE `zyt_tcm_game_share_visit` ADD INDEX `idx_inviter` (`inviter_user_id`)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE add_inviter_index_stmt FROM @add_inviter_index_sql;
|
||||
EXECUTE add_inviter_index_stmt;
|
||||
DEALLOCATE PREPARE add_inviter_index_stmt;
|
||||
Reference in New Issue
Block a user