.Net Framework 4.6中单线程Web服务多线程调用问题咨询
问题分析
核心问题是多线程并发调用时,同一PIN的通知被重复发送,但PIN生成本身是唯一的,说明ServiceGenPin.GenPin里的通知逻辑在并发场景下没有做唯一性校验或并发控制,而单线程串行执行时不会触发这个问题。你之前试的信号量和队列没解决,大概率是没精准控制到通知环节,或者没加重复校验。
解决方案
1. 给通知逻辑加细粒度锁,避免并发重复执行
不要用全局大锁影响整体性能,针对客户端ID+PIN的唯一组合加锁,确保同一PIN的通知只会被执行一次。同时必须加持久化的重复校验(比如存数据库/缓存),防止锁失效后重复发送。
示例修改ServiceGenPin:
public class ServiceGenPin { // 线程安全的锁容器,用客户端ID+PIN作为唯一锁键 private static readonly ConcurrentDictionary<string, object> _pinLocks = new ConcurrentDictionary<string, object>(); public ResponseResult GenPin(ClientPin client, string strGuidTrace) { ResponseResult result = new ResponseResult(); // 生成唯一PIN(你的原有逻辑,假设生成的PIN是generatedPin) string generatedPin = GenerateUniquePin(); string lockKey = $"{client.ClientId}_{generatedPin}"; // 获取或创建对应锁对象 object lockObj = _pinLocks.GetOrAdd(lockKey, k => new object()); lock(lockObj) { try { // 先查存储,确认这个PIN的通知没发过 if (!HasNotificationSent(generatedPin, client.ClientId)) { // 执行发送通知逻辑 SendPinNotification(generatedPin, client); // 标记已发送,写入数据库或缓存 MarkNotificationSent(generatedPin, client.ClientId); } } finally { // 处理完移除锁对象,防止内存泄漏 _pinLocks.TryRemove(lockKey, out _); } } result.Pin = generatedPin; // 其他业务逻辑... return result; } // 以下是辅助方法,替换成你的实际实现 private string GenerateUniquePin() { /* 原有PIN生成逻辑 */ } private bool HasNotificationSent(string pin, string clientId) { /* 查询数据库/缓存判断是否已发送 */ } private void SendPinNotification(string pin, ClientPin client) { /* 原有通知发送逻辑 */ } private void MarkNotificationSent(string pin, string clientId) { /* 记录发送状态到存储 */ } }
2. 用生产者-消费者模型,串行处理通知
既然单线程下完全没问题,直接把通知发送逻辑剥离到单线程消费队列里,所有生成的PIN先入队,由单个线程串行处理,从根源避免并发重复。
示例实现:
public class ServiceGenPin { // 线程安全的阻塞队列,存通知请求 private static readonly BlockingCollection<NotificationTask> _notificationQueue = new BlockingCollection<NotificationTask>(); // 启动单线程消费者 private static readonly Task _consumerTask; static ServiceGenPin() { _consumerTask = Task.Factory.StartNew(() => { // 持续消费队列 foreach (var task in _notificationQueue.GetConsumingEnumerable()) { try { // 同样先做重复校验 if (!HasNotificationSent(task.Pin, task.ClientId)) { SendPinNotification(task.Pin, task.Client); MarkNotificationSent(task.Pin, task.ClientId); } } catch (Exception ex) { // 异常处理:日志记录、重试逻辑 Trace.WriteLine($"通知发送失败:{ex.Message}"); } } }, TaskCreationOptions.LongRunning); } public ResponseResult GenPin(ClientPin client, string strGuidTrace) { ResponseResult result = new ResponseResult(); string generatedPin = GenerateUniquePin(); result.Pin = generatedPin; // 把通知请求加入队列,交给单线程处理 _notificationQueue.Add(new NotificationTask { Pin = generatedPin, ClientId = client.ClientId, Client = client }); // 其他业务逻辑... return result; } // 定义通知请求模型 private class NotificationTask { public string Pin { get; set; } public string ClientId { get; set; } public ClientPin Client { get; set; } } // 辅助方法同之前 private string GenerateUniquePin() { /* ... */ } private bool HasNotificationSent(string pin, string clientId) { /* ... */ } private void SendPinNotification(string pin, ClientPin client) { /* ... */ } private void MarkNotificationSent(string pin, string clientId) { /* ... */ } }
3. 解耦代码,降低维护成本
当前GenPinWS直接实例化ServiceGenPin,改成依赖注入,方便后续扩展和测试:
// 先给ServiceGenPin加接口 public interface IServiceGenPin { ResponseResult GenPin(ClientPin client, string strGuidTrace); } public class ServiceGenPin : IServiceGenPin { // 实现逻辑同上面的方案... } // 修改GenPinWS,构造注入依赖 public class GenPinWS : IGenPin { private readonly IServiceGenPin _serviceGenPin; public GenPinWS(IServiceGenPin serviceGenPin) { _serviceGenPin = serviceGenPin; } public ResponseResult GenPin(ClientPin client) { ResponseResult msgResponseResult = null; string strGuidTrace = Guid.NewGuid().ToString().Replace("-", ""); try { Traceability.InsertTraceability(...); msgResponseResult = _serviceGenPin.GenPin(client, strGuidTrace); Traceability.InsertTraceability(...); return msgResponseResult; } catch (Exception ex) { // 异常处理逻辑 ... } return msgResponseResult; } }
关键注意事项
- 必须加持久化校验:不管用锁还是队列,都要在发送前查数据库/缓存确认是否已发送,否则锁失效或队列重复入队都会导致重复通知。
- 避免内存泄漏:细粒度锁用完要移除锁对象;队列在应用停止时要调用
_notificationQueue.CompleteAdding(),让消费者线程正常退出。 - 性能平衡:全局锁会拖垮并发,一定要用细粒度锁;队列可以设置容量,避免请求过多导致内存溢出。
内容的提问来源于stack exchange,提问作者legitimateOC
相关产品推荐
相关产品推荐

