• 多线程+异步 遇到的异常:关键业务未执行前就结束了主线程


    背景:

    1:api提前生成一批数据

    2:hangfire服务中采用 异步(Task)+多线程(Parallel)方式 一个发送第三方消息的服务(每一分钟执行一次)


    问题描述:

    一条消息 发送多次 4、5-N次


    代码:

    hangfire

    /// <summary>
        /// 群发活动服务
        /// </summary>
        public class EnterpriseGroupSendJob : IRecurringJob
        {
    
            private readonly ILogger<EnterpriseGroupSendJob> _logger;
            private readonly string ConnectionString = ConfigurationManager.GetValue("ConnectionString");
            private readonly IEnterpriseGroupSendService _groupService;
            private static object TaskLock = new object();
            public EnterpriseGroupSendJob(ILogger<EnterpriseGroupSendJob> 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}结束】");
    
                }
            }
    
        }
    View Code

    service

    GetGroupSendMainIdList

    /// <summary>
            /// //1:获取群发消息主表
            /// </summary>
            /// <returns></returns>
            public List<EnterpriseSendMain> 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<EnterpriseSendMain> result = new List<EnterpriseSendMain>();
                using (var conn = new MySqlConnection(ConnectionString))
                {
                    result = conn.Query<EnterpriseSendMain>(sql.ToString()).ToList();
                }
                return result;
            }
    View Code

    GroupSend

     /// <summary>
            /// 3推送内容
            /// </summary>
            /// <param name="groupSendMainList"></param>
            /// <param name="groupSendDetailList"></param>
            /// <param name="baseDate"></param>
            public async void GroupSend(List<EnterpriseSendMain> groupSendMainList, List<EnterpriseGroupSendDetail> 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<YZTextMessageRequest>(groupSendMain, groupSendDetailList.Where(s => s.MainId == groupSendMain.Id).ToList(), sqlTitle);
                                break;
                            case 2://图片
                                PushVideo<YZImageMessageRequest>(groupSendMain, groupSendDetailList.Where(s => s.MainId == groupSendMain.Id).ToList());
                                break;
                            case 3://视频
                                PushVideo<YZVideoRequest>(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);
                            }
                        }
                    }
                }
            }
    View Code

    PushVideo

            /// <summary>
            /// 3.1发送小程序
            /// </summary>
            /// <param name="carManager"></param>
            /// <param name="h5RequestList"></param>
            private async Task PushVideo(EnterpriseSendMain groupSendMain, List<EnterpriseGroupSendDetail> groupSendDetailList)
            {
                var messageList = GetEnterpriseMessage(groupSendDetailList.Select(s => s.UUID).ToList(), new List<EnterpriseMessage>());
                var sendMessage = GetEnterpriseSendMsg(groupSendDetailList.Select(s => s.UUID).ToList(), new List<EnterpriseSendMsg>(), groupSendMain.CompId);
                using (var conn = new MySqlConnection(ConnectionString))
                {
                    foreach (var groupSendDetail in groupSendDetailList)
                    {
                        var videoRequest = new List<EnterprisePushMessageBase<YZMiniProgramRequest>>();
                        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<YZMiniProgramRequest>()
                            {
                                Content = instance,
                                NotifyCustomId = mssage.Id.ToString(),
                            });
                            var result = await _yZOperateService.TYZPush(
                                                                         new YZBaseRequest<EnterprisePushMessageBase<YZMiniProgramRequest>>()
                                                                         {
                                                                             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);
                            }
                        }
                    }
                }
            }
    
            /// <summary>
            /// 拼接易赚发送小程序url参数
            /// </summary>
            /// <param name="groupSendMain"></param>
            /// <param name="groupSendDetail"></param>
            /// <returns></returns>
            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;
            }
    
            /// <summary>
            /// 3.1推送
            /// </summary>
            /// <param name="carManager"></param>
            /// <param name="h5RequestList"></param>
            private async Task PushVideo<T>(EnterpriseSendMain groupSendMain, List<EnterpriseGroupSendDetail> 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<EnterpriseMessage>());
                var sendMessage = GetEnterpriseSendMsg(groupSendDetailList.Select(s => s.UUID).ToList(), new List<EnterpriseSendMsg>(), groupSendMain.CompId);
                using (var conn = new MySqlConnection(ConnectionString))
                {
                    foreach (var groupSendDetail in groupSendDetailList)
                    {
                        var videoRequest = new List<EnterprisePushMessageBase<T>>();
                        var mssage = messageList.Where(s => s.CustomerId == groupSendDetail.UUID).FirstOrDefault();
                        if (mssage != null)
                        {
                            videoRequest.Add(new EnterprisePushMessageBase<T>()
                            {
                                Content = (T)instance,
                                NotifyCustomId = mssage.Id.ToString(),
                            });
                            _logger.LogInformation(sqlTitle + "----" + groupSendDetail.ExternalUserId);
                            var result = await _yZOperateService.TYZPush(new YZBaseRequest<EnterprisePushMessageBase<T>>()
                            {
                                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);
                            }
                        }
                    }
                }
    
            }
    View Code

    问题出发条件:

    正常情况下没问题,问题出在了 超过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 


    核心:

    异步、多线程服务中 关键控制节点 保持主线程中写完。

  • 相关阅读:
    在spring boot中三分钟上手apache顶级分布式链路追踪系统skywalking
    Spring Boot Admin 2.1.0
    Spring Boot Admin 详解(Spring Boot 2.0,基于 Eureka 的实现)
    Spring Cloud Sleuth + Zipkin 链路监控
    guava布隆过滤器
    红包算法
    java国际化之时区问题处理
    SpringCloud2.0 Hystrix Feign 基于Feign实现断路器
    SpringBoot 自定义线程池,多线程
    基于Redisson+SpringBoot的Redission分布式锁
  • 原文地址:https://www.cnblogs.com/zhaokunbokeyuan256/p/12768092.html
Copyright © 2020-2023  润新知