我用 C# 为回合制游戏制作了一个 TCP 服务器。服务器使用 MySql 作为数据库,并使用存储过程进行所有数据库交互。所有数据库交互都包含在存储库层中,该层被注入到服务层中。然后将这些服务注入到整个服务器中。数据库交互当前包括以下插入/选择/更新
我需要服务器扩展到数千个并发客户端。我开始进行负载测试,但很快就遇到了 MySql 错误“连接过多”。在任何给定时间,服务器可能需要调用 X 个存储过程,但是在任何给定时间我可以打开的连接数是有限的。
我考虑过的:
我希望得到什么答案:
当可用连接数量有限时,如何扩展服务器以处理 X 数量的数据库调用的解决方案。最好有 C# 或伪代码的书面示例。
您正在寻找连接池 - 应用程序中的一个层,它将打开固定数量的连接并允许更高层使用这些连接。
高层将从池中请求连接,执行操作并将连接释放回池中。
正如 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++;
}
}
}