前段时间晚上突然收到 1300+ 的线上监控告警,点开一看,全是虚拟号码解绑的异常,看异常信息全是获取数据库连接超时,定位了一下代码,是系统里一个解绑虚拟号码的定时任务(UnbindJob)里面抛出的异常,最开始以为是 SQL 有问题,把里面涉及到数据库的 SQL 复制出来拿到控制台去执行,也正常,由于代码是 18 年的时候的老代码了,这个系统也是今年才重新开始启用的,最后一行一行看代码,找可能出现问题的地方。最后发现这个定时任务进行批量解绑是先将所有需要解绑的隐私号数据查询出来,然后再直接用 Paralle.ForEach 来并行执行单个的解绑逻辑(Unbind),而这个 Unbind 方法里面每次都会去读写一次数据库。


由于是公司项目,不方便直接贴源代码,问题代码差不多就是下面这个样子:

public void ExecuteUnbindJob()
{
    var unbindList = GetSecretPhoneNosToUnbind();

    Parallel.ForEach(unbindList, secretPhoneNo =>
    {
        Unbind(secretPhoneNo);
    });
}

private void Unbind(string secretPhoneNo)
{
    // 获取数据库连接
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        // 执行解绑逻辑
    }
}

想了想,最近暑假也开始了,系统业务量也起来了,每天需要解绑的隐私号码自然也多起来了,之前业务量没这么高,因此直接这样并发操作数据库也没把数据库的连接打满,也就没出现过这个问题,现在数据多起来了(每天需要解绑的隐私号大概 1500 个左右)自然就很容易将数据库连接打满。解决办法的话要么就是修改一下线程池配置的最大线程数,或者是在调用 Paralle.ForEach 时显式设置一下并行度更推荐在使用的时候显式设置并行度,因为修改线程池的话会影响到所有的地方,设置小了影响效率,设置大了依旧会存在数据库连接被打满的可能。


修改的代码也很简单:

public void ExecuteUnbindJob()
{
    var unbindList = GetSecretPhoneNosToUnbind();
    // 设置并行度限制
    var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; 

    Parallel.ForEach(unbindList, options, secretPhoneNo =>
    {
        Unbind(secretPhoneNo);
    });
}

private void Unbind(string secretPhoneNo)
{
    // 获取数据库连接
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        // 执行解绑逻辑
    }
}