如何在数据库连接数有限时扩展C#回合制游戏TCP服务器
解决MySQL "Too Many Connections" 问题的C#服务器扩展方案
核心思路
解决这个问题的关键是结合数据库连接池优化+异步请求限流队列,既利用连接池复用连接,又通过并发控制确保同时执行的数据库操作数不超过MySQL允许的连接上限。
第一步:优化MySQL连接池配置
首先确保你的连接字符串正确配置了连接池参数(以MySqlConnector为例),这是基础:
// 连接字符串示例,根据你的MySQL max_connections调整Max Pool Size string connectionString = "server=localhost;database=game_db;user=root;password=xxx;" + "Max Pool Size=100;" + // 设为MySQL max_connections的70%-80%,留余量给其他连接 "Min Pool Size=10;" + "Connection Timeout=15;" + "Pooling=true;"; // 必须开启连接池(默认开启)
MySQL默认max_connections是151,所以Max Pool Size建议设为100-120,避免占满所有数据库连接。
第二步:实现带并发限制的数据库请求队列
通过SemaphoreSlim控制同时执行的数据库操作数,确保不超过连接池上限。我们可以把这个限流逻辑封装到仓储层的基类中,避免重复代码。
1. 仓储基类(带限流)
public abstract class BaseRepository { private readonly SemaphoreSlim _dbSemaphore; protected readonly string _connectionString; // 通过DI注入SemaphoreSlim,限制并发数等于连接池Max Pool Size public BaseRepository(string connectionString, SemaphoreSlim dbSemaphore) { _connectionString = connectionString; _dbSemaphore = dbSemaphore; } protected async Task<T> ExecuteWithLimitAsync<T>(Func<MySqlConnection, Task<T>> dbOperation) { await _dbSemaphore.WaitAsync(); try { using var conn = new MySqlConnection(_connectionString); await conn.OpenAsync(); return await dbOperation(conn); } finally { _dbSemaphore.Release(); } } // 无返回值的重载 protected async Task ExecuteWithLimitAsync(Func<MySqlConnection, Task> dbOperation) { await _dbSemaphore.WaitAsync(); try { using var conn = new MySqlConnection(_connectionString); await conn.OpenAsync(); await dbOperation(conn); } finally { _dbSemaphore.Release(); } } }
2. 具体仓储实现(比如账号系统)
public class AccountRepository : BaseRepository, IAccountRepository { public AccountRepository(IConfiguration config, SemaphoreSlim dbSemaphore) : base(config.GetConnectionString("GameDb"), dbSemaphore) { } // 调用存储过程查询账号 public async Task<Account> GetAccountByIdAsync(int accountId) { return await ExecuteWithLimitAsync(async conn => { using var cmd = new MySqlCommand("sp_GetAccountById", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@AccountId", accountId); using var reader = await cmd.ExecuteReaderAsync(); if (await reader.ReadAsync()) { return new Account { Id = reader.GetInt32("Id"), Username = reader.GetString("Username"), // 其他字段... }; } return null; }); } // 插入账号的存储过程调用 public async Task<int> CreateAccountAsync(string username, string passwordHash) { return await ExecuteWithLimitAsync(async conn => { using var cmd = new MySqlCommand("sp_CreateAccount", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@Username", username); cmd.Parameters.AddWithValue("@PasswordHash", passwordHash); var outputParam = new MySqlParameter("@NewAccountId", MySqlDbType.Int32) { Direction = ParameterDirection.Output }; cmd.Parameters.Add(outputParam); await cmd.ExecuteNonQueryAsync(); return (int)outputParam.Value; }); } }
3. 依赖注入配置(比如ASP.NET Core或自定义DI容器)
// 在Startup.cs或DI初始化代码中 services.AddSingleton<SemaphoreSlim>(sp => new SemaphoreSlim(initialCount: 100, maxCount: 100)); // 对应连接池的Max Pool Size services.AddScoped<IAccountRepository, AccountRepository>(); services.AddScoped<IMatchHistoryRepository, MatchHistoryRepository>(); // 其他仓储和服务的注册...
额外优化建议
- 批量处理非实时请求:比如日志记录、对战历史统计,不需要实时写入的可以攒一批再调用存储过程批量插入,减少请求次数。
// 示例:批量日志插入 public async Task BulkInsertLogsAsync(List<GameLog> logs) { await ExecuteWithLimitAsync(async conn => { using var cmd = new MySqlCommand("sp_BulkInsertLogs", conn); cmd.CommandType = CommandType.StoredProcedure; // 传入表值参数(MySQL需要先定义用户自定义类型) var logsParam = cmd.Parameters.AddWithValue("@Logs", logs.ToDataTable()); logsParam.MySqlDbType = MySqlDbType.Structured; await cmd.ExecuteNonQueryAsync(); }); } - 避免长时间持有连接:确保所有数据库操作都用
using语句管理连接,操作完成后立即释放回连接池。 - 动态调整并发数:可以根据服务器负载和数据库连接状态,动态调整
SemaphoreSlim的并发上限,避免资源浪费。
内容的提问来源于stack exchange,提问作者user10266595
相关产品推荐
相关产品推荐

