.Net Core访问PostgreSQL报连接数过多并发访问问题咨询
问题背景
现有两个基于 .Net Core 开发的控制台服务,共同访问同一个 PostgreSQL 数据库中的目标表:
- 服务1以后台线程形式运行,每分钟向目标表插入约500行数据
- 服务2持续读取同一张表的数据,其内置的MQTT发布端在收到新数据请求时会触发读表操作,读操作频率极高,至少为每分钟4~5次
服务运行过程中抛出固定错误:
FATAL: sorry, too many clients already
最初排查方向为读写高频并发时数据库连接未正确释放,曾考虑实现写入时阻塞读的逻辑,补充信息如下:
- 已知系统存在连接池机制但不清楚具体配置位置
- 核心诉求是解决PostgreSQL数据库并发访问冲突问题
- 目前使用DbContext时已经外层包裹
using语句,且在finally块中手动执行Dispose释放
现有读操作代码
using (PlatinumDBContext platinumDBContext = new PlatinumDBContext()) { try { var data = platinumDBContext.TrendPoints.Where(x => ids.Contains(x.TrendPointID) && x.TimeStamp >= DateTime.Now.AddHours(-timeinHours)); result = data.Select(x => new Last24hours { Label = x.TrendPointID.ToString(), Value = (double)x.TrendPointValue, time = x.TimeStamp.ToString("MM/dd/yyyy HH:mm:ss") }).ToList(); } catch (Exception oE) { } finally { platinumDBContext.Dispose(); } }
现有写操作代码
using (PlatinumDBContext platinumDBContext = new PlatinumDBContext()) { try { foreach (var point in trendPoints) { if (point != null) { TrendPoint item = new TrendPoint(); item.CreatedDate = DateTime.Now; item.ObjectState = ObjectState.Added; item.TrendPointID = point.TrendID; item.TrendPointValue = double.IsNaN(point.Value) ? decimal.MinValue : (decimal)point.Value; item.TimeStamp = new DateTime(point.TimeStamp); platinumDBContext.Add(item); } } platinumDBContext.SaveChanges(); } catch (Exception ex) { } finally { platinumDBContext.Dispose(); } }
解决方案
too many clients already错误和读写冲突没有直接关系,本质是数据库总连接数超过了PostgreSQL配置的最大连接上限,不需要实现写阻塞读的逻辑。PostgreSQL本身的MVCC机制天然支持读写不互斥,加阻塞逻辑反而会大幅降低性能。
第一优先级:修复代码里的连接泄漏问题
现有代码存在两个直接导致连接无法及时归还连接池的问题:
- 重复Dispose无实际作用,空catch块吞异常会引发状态异常:
using语句本身会在代码块退出时自动执行Dispose,在finally里再次写Dispose没有任何意义。更严重的是所有异常被直接吞掉,如果查询/写入过程中出现连接状态异常,没有任何日志可排查,甚至可能出现连接断裂后无法被连接池正常回收的情况。 - 每次实例化DbContext却未配置连接池参数:EF Core for PostgreSQL 默认开启连接池,但如果没有在连接字符串里显式配置
Max Pool Size,默认单个连接池最大连接数是100,两个服务高并发下如果连接归还不及时,很容易打满连接数。
先把代码改成正确写法,去掉重复的Dispose,补充异常记录逻辑:
修正后的读操作示例
// using会自动执行Dispose,不需要额外在finally中手动调用 using var platinumDBContext = new PlatinumDBContext(); try { var data = platinumDBContext.TrendPoints.Where(x => ids.Contains(x.TrendPointID) && x.TimeStamp >= DateTime.Now.AddHours(-timeinHours)); result = data.Select(x => new Last24hours { Label = x.TrendPointID.ToString(), Value = (double)x.TrendPointValue, time = x.TimeStamp.ToString("MM/dd/yyyy HH:mm:ss") }).ToList(); } catch (Exception oE) { // 必须记录异常,禁止空catch吞错 Console.WriteLine($"读数据出错: {oE}"); throw; }
修正后的写操作优化
现有写操作逐条Add的效率偏低,可以优化批量操作逻辑,缩短连接持有时间:
using var platinumDBContext = new PlatinumDBContext(); try { var insertList = new List<TrendPoint>(); foreach (var point in trendPoints) { if (point == null) continue; insertList.Add(new TrendPoint { CreatedDate = DateTime.Now, ObjectState = ObjectState.Added, TrendPointID = point.TrendID, TrendPointValue = double.IsNaN(point.Value) ? decimal.MinValue : (decimal)point.Value, TimeStamp = new DateTime(point.TimeStamp) }); } // 批量添加范围,比逐条Add效率更高 platinumDBContext.AddRange(insertList); platinumDBContext.SaveChanges(); } catch (Exception ex) { Console.WriteLine($"写数据出错: {ex}"); throw; }
第二优先级:正确配置PostgreSQL连接池
连接池配置直接写在数据库连接字符串中即可,配置示例:
Host=数据库地址;Port=5432;Database=库名;Username=账号;Password=密码;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50;Connection Idle Lifetime=300;
核心参数说明:
Maximum Pool Size:单个进程的连接池最大持有连接数,两个服务配置的最大连接数之和不要超过PostgreSQL的max_connections配置值(PostgreSQL默认max_connections为100,注意给系统运维操作预留连接配额)Connection Idle Lifetime:空闲连接超过300秒会自动回收,避免闲置连接长期占用数据库连接配额
第三优先级:检查PostgreSQL服务端配置
- 登录PostgreSQL执行以下命令查看当前最大连接数配置:
如果业务并发确实高,可以适当调大该值,注意PostgreSQL每个连接会占用约10MB左右内存,根据服务器内存配置调整,不要盲目调大。SHOW max_connections; - 执行以下命令查看当前所有连接的来源,排查是否有其他闲置连接占用配额:
清理长期处于idle状态的无用连接。SELECT client_addr, application_name, state, count(*) FROM pg_stat_activity GROUP BY client_addr, application_name, state;
额外优化建议
- 不要自行实现读写互斥锁,PostgreSQL的MVCC机制天生支持读写不阻塞,加锁只会大幅降低并发性能
- 读操作如果是高频固定查询,可以适当加二级缓存,减少直接访问数据库的频率
- 每分钟500条的写入量级非常小,用AddRange批量提交完全可以支撑,不需要做分库分表类的重优化
内容的提问来源于stack exchange,提问作者Geervani
相关产品推荐
相关产品推荐

