背景:
1:api提前生成一批数据
2:hangfire服务中采用 异步(Task)+多线程(Parallel)方式 一个发送第三方消息的服务(每一分钟执行一次)
问题描述:
一条消息 发送多次 4、5-N次
代码:
hangfire
////// 群发活动服务 /// public class EnterpriseGroupSendJob : IRecurringJob { private readonly ILogger _logger; private readonly string ConnectionString = ConfigurationManager.GetValue("ConnectionString"); private readonly IEnterpriseGroupSendService _groupService; private static object TaskLock = new object(); public EnterpriseGroupSendJob(ILogger logger, IEnterpriseGroupSendService groupService) { _logger = logger; _groupService = groupService; } public void Execute(PerformContext context) { //_logger.LogInformation($"【EnterpriseGroupSendJob】"); lock (TaskLock) { //1:获取群发消息主表 //1.1:如果处于待发送 状态 并且时间 小于等于当前时间的 //2:获取 根据商户 获取这部分的 群发活动 子表数据 //3:根据商户 开启线程 请求 wehub接口 (文本、图片、视频) //3.1:更新 群发活动主表表 状态 (加锁 TaskLock 不会执行两次), //4:在task里写回调更新 字表的 发送状态 //5:将群发消息推送到crm逻辑: //5.1 表 enterprise_message 与易赚关联表 enterprise_send_msg 消息体记录表 enterprise_groupsend_detail 业务表 三表通过guid关联 //5.2 生成数据时将三表数据统一生成,发送逻辑不变 更新 send_msg表的 update状态 //5.3 回调里需要根据uuid customerid找到 send_msg 表里的 requestmsg 同步给crm var sqlTitle = "企业微信-群发活动服务" + DateTime.Now; _logger.LogInformation($"【{sqlTitle}开始】"); var baseDate = DateTime.Now; var groupSendMainList = _groupService.GetGroupSendMainIdList(baseDate); if (groupSendMainList.Count() == 0) { //_logger.LogInformation($"【{sqlTitle} 未查询到群发活动】"); return; } var groupSendDetailList = _groupService.GetGroupSendDetailList(groupSendMainList); if (groupSendDetailList.Count() == 0) { //_logger.LogInformation($"【{sqlTitle} 未查询到群发活动子表数据】"); return; } groupSendDetailList = _groupService.GetEligibleGroup(groupSendDetailList, sqlTitle, baseDate); if (groupSendDetailList.Count() == 0) { //_logger.LogInformation($"【{sqlTitle} 没有符合企业微信有效期内条件的群发活动子表数据】"); return; } ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = Convert.ToInt32(ConfigurationManager.GetValue("GroupSendCondition:TaskCount")) }; Parallel.ForEach(groupSendDetailList.DistinctEx(s => s.CompId).Select(s => s.CompId).ToList(), parallelOptions,groupSendCompId => { _groupService.GroupSend(groupSendMainList.Where(s => s.CompId == groupSendCompId).ToList(), groupSendDetailList.Where(s => s.CompId == groupSendCompId).ToList(), sqlTitle); }); _logger.LogInformation($"【{sqlTitle}结束】"); } } }
service
GetGroupSendMainIdList
////// //1:获取群发消息主表 /// /// public List GetGroupSendMainIdList(DateTime baseDate) { StringBuilder sql = new StringBuilder().AppendFormat("select Id,ContentType,Title,Content,CompId,MaterialSource,MaterialTitle,HomePicUrl,MaterialCode,MaterialURL from enterprise_groupsend_main where state={0} and SendTime<='{1}' order by sendTime ", (int)SolicitGroupSendMainStateEnum.待推送, baseDate.ToString("yyyy-MM-dd HH:mm:ss")); List result = new List (); using (var conn = new MySqlConnection(ConnectionString)) { result = conn.Query (sql.ToString()).ToList(); } return result; }
GroupSend
////// 3推送内容 /// /// /// /// public async void GroupSend(List groupSendMainList, List groupSendDetailList, string sqlTitle, int reSendTime = 0) { //这个方法可能会出现的异常 : 本方法 async 且 await pushVideo //会导致pushvideo之前不更新 enterprise_groupsend_main表的数据 1分钟后 新的服务启动后 重新走一遍流程 //异步方法中 关键控制节点不异步处理 foreach (var groupSendMain in groupSendMainList) { if (groupSendMain.MaterialSource == 1) { PushVideo(groupSendMain, groupSendDetailList.Where(s => s.MainId == groupSendMain.Id).ToList()); } else { switch (groupSendMain.ContentType) { case 1://文本 PushVideo (groupSendMain, groupSendDetailList.Where(s => s.MainId == groupSendMain.Id).ToList(), sqlTitle); break; case 2://图片 PushVideo (groupSendMain, groupSendDetailList.Where(s => s.MainId == groupSendMain.Id).ToList()); break; case 3://视频 PushVideo (groupSendMain, groupSendDetailList.Where(s => s.MainId == groupSendMain.Id).ToList()); break; default: break; } } string sql = $"update enterprise_groupsend_main set state={(int)SolicitGroupSendMainStateEnum.已推送} where id={groupSendMain.Id} and state={(int)SolicitGroupSendMainStateEnum.待推送}"; //_logger.LogInformation($"【{sqlTitle} 修改群发主表状态sql】:{sql}"); using (var conn = new MySqlConnection(ConnectionString)) { conn.Execute(sql); } if (reSendTime > 0) { if (groupSendDetailList.Count(s => s.MainId == groupSendMain.Id) > 0) { string sqlUpdateDetailReSendTime = $"update enterprise_groupsend_detail set ResendTimes ={reSendTime} where sendState=9 and id in ({string.Join(",", groupSendDetailList.Select(s => s.Id))});"; //_logger.LogInformation($"【{sqlTitle} 修改群发子表ResendTimes sql】:{sqlUpdateDetailReSendTime}"); using (var conn = new MySqlConnection(ConnectionString)) { conn.Execute(sqlUpdateDetailReSendTime); } } } } }
PushVideo
////// 3.1发送小程序 /// /// /// private async Task PushVideo(EnterpriseSendMain groupSendMain, List groupSendDetailList) { var messageList = GetEnterpriseMessage(groupSendDetailList.Select(s => s.UUID).ToList(), new List ()); var sendMessage = GetEnterpriseSendMsg(groupSendDetailList.Select(s => s.UUID).ToList(), new List (), groupSendMain.CompId); using (var conn = new MySqlConnection(ConnectionString)) { foreach (var groupSendDetail in groupSendDetailList) { var videoRequest = new List >(); var mssage = messageList.Where(s => s.CustomerId == groupSendDetail.UUID).FirstOrDefault(); if (mssage != null) { var instance = new YZMiniProgramRequest() { AccountOrgianlId = WechatMiniAccountOrgianlId, Cover = groupSendMain.HomePicUrl, Icon = WechatMiniIcon, Title = groupSendMain.MaterialTitle, Url = CheckEnterpriseMiniProgramUrl(groupSendMain, groupSendDetail), }; videoRequest.Add(new EnterprisePushMessageBase () { Content = instance, NotifyCustomId = mssage.Id.ToString(), }); var result = await _yZOperateService.TYZPush( new YZBaseRequest >() { EmployeeId = groupSendDetail.EmployeeId.ToString(), CompId = groupSendDetail.CompId, EnterpriseId = groupSendDetail.EnterpriseId, To = groupSendDetail.ExternalUserId, ToType = 0, MsgList = videoRequest, }); var sendmsg = sendMessage.FirstOrDefault(s => s.Customid == groupSendDetail.UUID); if (sendmsg != null) { //更新表EnterpriseSendMsg 的requestmsg YZMiniProgramRequestExtend extendMiniRequest = new YZMiniProgramRequestExtend() { AccountOrgianlId = instance.AccountOrgianlId, Cover = instance.Cover, Icon = instance.Icon, Title = instance.Title, Url = instance.Url, OriginalContentUrl = groupSendMain.MaterialURL }; var sql = "update enterprise_send_msg set RequestMsg='" + extendMiniRequest.ToJson() + "'"; if (result.Status == 1) { sql += " , state=2 "; } sql += " where id=" + sendmsg.Id; await conn.ExecuteAsync(sql); //_logger.LogInformation("【群发更新enterprise_send_msg】" + sql); } } } } } /// /// 拼接易赚发送小程序url参数 /// /// /// /// public string CheckEnterpriseMiniProgramUrl(EnterpriseSendMain groupSendMain, EnterpriseGroupSendDetail groupSendDetail) { var url = ""; switch (groupSendMain.ContentType) { case 3: url = string.Format(EnterPriseConvideo, groupSendMain.MaterialCode, groupSendMain.CompId, groupSendDetail.ExternalUserId); break; case 4: url = string.Format(EnterPriseConarticle, groupSendMain.MaterialCode, groupSendMain.CompId, groupSendDetail.ExternalUserId); break; case 2: url = string.Format(EnterPriseConposter, groupSendMain.MaterialCode, groupSendMain.CompId, groupSendDetail.ExternalUserId); break; default: break; } return url; } /// /// 3.1推送 /// /// /// private async Task PushVideo (EnterpriseSendMain groupSendMain, List groupSendDetailList, string sqlTitle = "") { var instance = Activator.CreateInstance(typeof(T)); if (typeof(T) == typeof(YZTextMessageRequest)) { var instancemsg = instance.GetType().GetProperty("Text"); instancemsg.SetValue(instance, groupSendMain.Content); } else if (typeof(T) == typeof(YZImageMessageRequest)) { var instancemsg = instance.GetType().GetProperty("ImageHttpUrl"); instancemsg.SetValue(instance, groupSendMain.Content); } else if (typeof(T) == typeof(YZVideoRequest)) { var instancemsg = instance.GetType().GetProperty("VideoHttpUrl"); instancemsg.SetValue(instance, groupSendMain.Content); } var messageList = GetEnterpriseMessage(groupSendDetailList.Select(s => s.UUID).ToList(), new List ()); var sendMessage = GetEnterpriseSendMsg(groupSendDetailList.Select(s => s.UUID).ToList(), new List (), groupSendMain.CompId); using (var conn = new MySqlConnection(ConnectionString)) { foreach (var groupSendDetail in groupSendDetailList) { var videoRequest = new List >(); var mssage = messageList.Where(s => s.CustomerId == groupSendDetail.UUID).FirstOrDefault(); if (mssage != null) { videoRequest.Add(new EnterprisePushMessageBase () { Content = (T)instance, NotifyCustomId = mssage.Id.ToString(), }); _logger.LogInformation(sqlTitle + "----" + groupSendDetail.ExternalUserId); var result = await _yZOperateService.TYZPush(new YZBaseRequest >() { EmployeeId = groupSendDetail.EmployeeId.ToString(), CompId = groupSendDetail.CompId, EnterpriseId = groupSendDetail.EnterpriseId, To = groupSendDetail.ExternalUserId, ToType = 0, MsgList = videoRequest, }); //发送记录 var sendmsg = sendMessage.FirstOrDefault(s => s.Customid == groupSendDetail.UUID); if (sendmsg != null) { //更新表EnterpriseSendMsg 的requestmsg var sql = "update enterprise_send_msg set RequestMsg='" + instance.ToJson() + "'"; if (result.Status == 1) { sql += " , state=2 "; } sql += " where id=" + sendmsg.Id; await conn.ExecuteAsync(sql); //_logger.LogInformation("【群发更新enterprise_send_msg】" + sql); } } } } }
问题出发条件:
正常情况下没问题,问题出在了 超过500+的时候
问题所在:
1:GroupSend 采用了 async ,
2:hangfire.job.Execute 不支持 async 在 Parallel.foreach的时候 没有 await 或者 .wait
3:PushVideo 采用了await
4:导致 groupsend方法 中关键的一个 sql没有执行 时 就跑完了本次 服务 ,然后 下一次服务进来后 还会再跑一边逻辑。
解决方案:
1:groupsend 不采用 async
2:pushvideo 不 await
核心:
异步、多线程服务中 关键控制节点 保持主线程中写完。