如何扩展数据库连接有限的服务器

问题描述 投票:0回答:2

我用 C# 为回合制游戏制作了一个 TCP 服务器。服务器使用 MySql 作为数据库,并使用存储过程进行所有数据库交互。所有数据库交互都包含在存储库层中,该层被注入到服务层中。然后将这些服务注入到整个服务器中。数据库交互当前包括以下插入/选择/更新

  • 记录
  • 账户系统
  • 比赛历史
  • 玩家统计数据

我需要服务器扩展到数千个并发客户端。我开始进行负载测试,但很快就遇到了 MySql 错误“连接过多”。在任何给定时间,服务器可能需要调用 X 个存储过程,但是在任何给定时间我可以打开的连接数是有限的。

我考虑过的:

  • 到目前为止我想到的唯一解决方案是拥有某种事件系统队列,您可以在其中将数据库请求添加到队列中,队列根据最大连接数触发这些请求,并且当每个请求时触发一个事件请求已完成,包含返回数据。但是,我以前没有这样做过,所以我对实现没有明确的想法。

我希望得到什么答案:

当可用连接数量有限时,如何扩展服务器以处理 X 数量的数据库调用的解决方案。最好有 C# 或伪代码的书面示例。

c# mysql client-server scalability
2个回答
1
投票

您正在寻找连接池 - 应用程序中的一个层,它将打开固定数量的连接并允许更高层使用这些连接。

高层将从池中请求连接,执行操作并将连接释放回池中。


0
投票

正如 AndrewR 所建议的,我研究了连接池,最终编写了自己的连接池。不确定它是否特别有效,但我已经用 10,000 个用户(机器人)的负载对其进行了测试,并且它似乎有效。这是:

public interface IDbConnection : IDisposable
{
    MySqlConnection? Connector { get; }

    void Connect();
    void Disconnect();
    void Release(bool dispose);
}

public sealed class DbConnection : IDbConnection
{
public MySqlConnection? Connector { get; private set; }

private readonly Config config;
private readonly System.Timers.Timer timeout;

public DbConnection(
    Config config)
{
    this.config = config;
    timeout = new System.Timers.Timer();
    timeout.Elapsed += TimeoutElapsed;
    timeout.Interval = config.Database?.ConnectionTimeout ?? 0;
    timeout.AutoReset = false;
}

public void Connect()
{
    timeout?.Stop();

    if (Connector?.State is System.Data.ConnectionState.Open)
        return;
    
    if (Connector != null && Connector?.State != System.Data.ConnectionState.Open)
        Disconnect();

    Connector = new MySqlConnection(config.Database?.DbConnectionString);
    Connector.Open();
}

public void Release(bool dispose)
{
    if (dispose)
        Dispose();
    else
        timeout?.Start();
}

private void TimeoutElapsed(object? sender, ElapsedEventArgs e) => Disconnect();

public void Disconnect()
{
    Connector?.Close();
    Connector?.Dispose();
}

public void Dispose()
{
    timeout?.Stop();
    timeout?.Dispose();
    Connector?.Close();
    Connector?.Dispose();
    Connector = null;
}
}

public interface IDbConnectionPool : IDisposable
{
int OpenDbConnections { get; }

void Start(CancellationToken cancelToken);
IDbConnection? GetConnection();
void ReleaseConnection(IDbConnection connection);
}

public sealed class DbConnectionPool : IDbConnectionPool
{
public int OpenDbConnections => config.Database?.MaxConnections ?? 0 - freeConnections.Count;

private readonly Config config;
private BlockingCollection<IDbConnection> freeConnections = new();
private CancellationToken cancelToken;

public DbConnectionPool(Config config) => this.config = config;

public void Start(CancellationToken cancelToken)
{
    this.cancelToken = cancelToken;
    freeConnections = new BlockingCollection<IDbConnection>(new ConcurrentBag<IDbConnection>(), config.Database?.MaxConnections ?? 0);
    InitConnections();
}       

public IDbConnection? GetConnection()
{
    if (cancelToken.IsCancellationRequested)
        return null;
    
    if (freeConnections.TryTake(out IDbConnection? connection, config.Database?.ConnectionWaitTime ?? 0))
        connection.Connect();

    return connection;
}

public void ReleaseConnection(IDbConnection connection)
{
    connection.Release(cancelToken.IsCancellationRequested);

    if (!cancelToken.IsCancellationRequested)
        freeConnections.Add(connection);

}       

public void Dispose()
{
    while (freeConnections.TryTake(out IDbConnection? connection))
        connection?.Dispose();

    try { freeConnections?.Dispose(); }
    catch (Exception) { }
}

private void InitConnections()
{
    int count = 0;
    while (count < config.Database?.MaxConnections)
    {
        var con = Container.ServiceProvider?.GetService<IDbConnection>();
        if (con != null)
            freeConnections.Add(con);

        count++;
    }
}
}
© www.soinside.com 2019 - 2024. All rights reserved.