多客户端线程并发调用SQL命令阻塞问题及方案咨询
你的方案思路没问题,绝非小题大做!
首先得给你吃个定心丸:用单线程串行处理SQL操作的思路非常合理,完全不是小题大做。多线程直接怼SQL会带来一堆麻烦——比如你遇到的连接占用问题,还有潜在的数据一致性风险(比如同时更新同一条记录导致的丢失更新)、数据库连接池过载等。串行化处理是低成本解决这类并发问题的经典方案。
不过你的现有实现确实有可以简化的空间,尤其是那个冗长的switch语句,还有手动维护队列的逻辑都能优化:
优化方向1:用线程安全队列替代手动维护的列表
你手动维护m_queuedEvents、m_executingEvents两个列表,还要写MoveQueuedEventsToExecuting来转移任务,这部分可以直接用.NET自带的BlockingCollection<T>——它本身就是线程安全的阻塞队列,自带从队列取元素的阻塞等待逻辑,不用自己写锁和转移代码。
优化方向2:用字典映射替代switch语句
把CommonSQL.Commands和对应的处理方法直接绑定到字典里,新增命令时只需要在字典里加一行映射,不用修改处理逻辑的代码,符合开闭原则。
优化后的完整示例代码
首先定义一个封装SQL任务的类,把命令、参数、回调打包在一起:
public class SqlTask { public CommonSQL.Commands Command { get; set; } public object[] Parameters { get; set; } public Action<object[]> Callback { get; set; } }
然后重构SQLThread类:
class SQLThread { // 线程安全的阻塞队列,自动处理多线程添加/取出任务 private static readonly BlockingCollection<SqlTask> _taskQueue = new BlockingCollection<SqlTask>(); // 命令与处理方法的映射表,替代switch private static readonly Dictionary<CommonSQL.Commands, Func<object[], object[]>> _commandHandlers = new() { { CommonSQL.Commands.SetEntityCoordinates, CommonSQL.SetEntityCoodinates }, { CommonSQL.Commands.GetMapData, CommonSQL.GetMapData }, { CommonSQL.Commands.GetEntityCoordinates, CommonSQL.GetEntityCoordinates }, { CommonSQL.Commands.GetEntityID, CommonSQL.GetEntityID }, { CommonSQL.Commands.GetPlayerIDAndPassword, CommonSQL.GetPlayerIDAndPassword }, { CommonSQL.Commands.CreateAccount, CommonSQL.CreateAccount }, { CommonSQL.Commands.CreateEntity, CommonSQL.CreateEntity } }; // 后台处理线程,静态构造函数自动启动 private static readonly Task _processingTask; static SQLThread() { // 用LongRunning标记,让线程池优先分配专用线程 _processingTask = Task.Factory.StartNew(ProcessTasks, TaskCreationOptions.LongRunning); } // 对外暴露的添加任务方法 public static void QueueSqlTask(CommonSQL.Commands command, object[] parameters, Action<object[]> callback) { _taskQueue.Add(new SqlTask { Command = command, Parameters = parameters, Callback = callback }); } private static void ProcessTasks() { try { // GetConsumingEnumerable会阻塞直到队列有任务,或者队列被标记为完成 foreach (var task in _taskQueue.GetConsumingEnumerable()) { if (_commandHandlers.TryGetValue(task.Command, out var handler)) { try { var result = handler(task.Parameters); task.Callback?.Invoke(result); } catch (Exception ex) { Console.WriteLine($"执行SQL命令 {task.Command} 出错: {ex.Message}"); // 可以在这里给回调传递错误信息,方便上层处理 task.Callback?.Invoke(new object[] { ex }); } } else { Console.WriteLine($"没有为命令 {task.Command} 注册处理方法"); } } } catch (Exception ex) { Console.WriteLine($"SQL处理线程异常终止: {ex.Message}"); } } // 程序退出时调用,停止处理线程 public static void StopProcessing() { _taskQueue.CompleteAdding(); _processingTask.Wait(); } }
额外提醒:尽早解决SQL注入问题
虽然你说先解决并发问题,但还是建议尽快把SQL语句改成参数化查询——现在的string.Format写法风险太高,后续改起来更麻烦。比如把你的示例改成:
public static void SetEntityCoodinates(int x, int y, int ID) { var query = @"UPDATE entities SET CellPosX = @x, CellPosY = @y WHERE entities.ID = @id;"; // 这里假设SQLReader.RunQuery支持接受参数化查询的参数(比如SqlParameter数组) var parameters = new[] { new SqlParameter("@x", x), new SqlParameter("@y", y), new SqlParameter("@id", ID) }; SQLReader.RunQuery(query, SQLReader.Stream.INPUT, parameters); }
内容的提问来源于stack exchange,提问作者Anthony
相关产品推荐
相关产品推荐

