You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

数据库并发读写问题:多用户同时加入游戏房间冲突如何解决

问题本质

这是典型的并发读写竞态条件:多个请求在几乎同一时间查询到User2为0的可用房间,都执行了赋值更新操作,因此所有请求都判定自己更新成功,导致多个用户被分配到同一个房间位置。改成同步方法没用是因为多线程服务器下,还是会有多个线程同时进入查询逻辑,未加锁的同步逻辑解决不了并发冲突问题。

可行解决方案

方案1:单条原子SQL更新(性能最优,推荐)

直接把查询和更新合并为一条原子SQL执行,利用数据库的原子操作特性,保证同一时间只有一个请求能更新成功,不需要额外加锁或事务,性能最高。

public async Task<string> JoinRoom(int leaguePosition, int userId)
{
    // 以下SQL以PostgreSQL为例,其他数据库调整对应语法即可
    var updateSql = @"
UPDATE ClassicGames 
SET User2 = @userId, User2State = @newState
WHERE Id = (
    SELECT cg.Id FROM ClassicGames cg
    INNER JOIN Leagues l ON cg.LeagueId = l.Id
    WHERE l.Position = @leaguePosition 
      AND cg.User2 = 0 
      AND cg.User2State = @observerState
    LIMIT 1
)
RETURNING Id, User1;
";
    var result = await context.Database.SqlQueryRaw<(int Id, int User1)>(
        updateSql,
        new SqlParameter("@userId", userId),
        new SqlParameter("@newState", (int)EClassicGameUserState.STAGNATION),
        new SqlParameter("@leaguePosition", leaguePosition),
        new SqlParameter("@observerState", (int)EClassicGameUserState.OBSERVER)
    ).FirstOrDefaultAsync();
    
    return result == default ? "" : $"{result.Id},{result.User1}";
}

方案2:乐观锁(适合并发量不高的场景)

给ClassicGames表添加行版本字段,EF Core更新时会自动校验版本,版本不匹配就抛出并发异常,说明房间已经被其他用户占用。

  1. 实体类添加版本字段
public class ClassicGame
{
    // 原有字段省略
    [Timestamp]
    public byte[] RowVersion { get; set; }
}
  1. 修改Service逻辑
public async Task<string> JoinRoom(int leaguePosition, int userId)
{
    try
    {
        var gameRoom = await 
            context.ClassicGames.Include(x => x.League)
            .FirstOrDefaultAsync(x => x.League.Position == leaguePosition && x.User2 == 0 && x.User2State == (int)EClassicGameUserState.OBSERVER);

        if (gameRoom is null) return "";
        
        gameRoom.User2 = userId;
        gameRoom.User2State = (int)EClassicGameUserState.STAGNATION;
        var affectedRows = await context.SaveChangesAsync();
        return affectedRows == 0 ? "" : $"{gameRoom.Id},{gameRoom.User1}";
    }
    catch (DbUpdateConcurrencyException)
    {
        // 并发冲突,说明房间已被占用
        return "";
    }
}

方案3:分布式锁(适合多实例部署场景)

如果你的服务是多实例部署,本地锁无效,可以用Redis分布式锁;如果是单实例部署,用静态SemaphoreSlim即可:

// 全局静态锁,同一时间只允许一个请求进入加入逻辑
private static readonly SemaphoreSlim _joinRoomLock = new SemaphoreSlim(1, 1);

public async Task<string> JoinRoom(int leaguePosition, int userId)
{
    await _joinRoomLock.WaitAsync();
    try
    {
        var gameRoom = await 
            context.ClassicGames.Include(x => x.League)
            .FirstOrDefaultAsync(x => x.League.Position == leaguePosition && x.User2 == 0 && x.User2State == (int)EClassicGameUserState.OBSERVER);

        if (gameRoom is null) return "";
        
        gameRoom.User2 = userId;
        gameRoom.User2State = (int)EClassicGameUserState.STAGNATION;
        await context.SaveChangesAsync();
        return $"{gameRoom.Id},{gameRoom.User1}";
    }
    finally
    {
        _joinRoomLock.Release();
    }
}

内容的提问来源于stack exchange,提问作者Ertryw

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 20:48:02