.NET应用层的数据库连接池的设计

背景

项目需要做一个动态 API 模块,其中一个点是提供可以直接调用 sql 语句的接口,设计了一个数据库连接池的表,表中主要字段有数据库连接字符串、数据库类型、连接池大小、最大并发量等。

虽然数据库本身有连接池的概念,在连接字符串中配置,但是这是数据库层面的。现在需要在应用层面也加一个连接池,防止过多的连接打到数据库层。

设计思路

就是一个对象池的概念,提供取连接、放回连接的方法,只不过这个对象是数据库连接。

用内存中的支持并发的数据结构实现,一个连接字符串对应一个连接池。

外层: ConcurrentDictionary<string, DefaultConnectionPool>,代表每个连接字符串及对应的连接池

连接池的主要内容:

csharp 复制代码
private readonly Func<string, IDbConnection> _createConnectionFunc;
private readonly string _connectionString;
private readonly int _maxPoolSize;
private ConcurrentBag<IDbConnection> _connections;
private int _currentCount = 0;
private SemaphoreSlim _poolAccessSemaphore;  // Semaphore to control concurrency

使用 SemaphoreSlim,来控制并发量

使用一个计数值,来维护当前活跃的连接个数,注意修改这个计数值时考虑线程安全,采用Interlocked.Increment(ref _currentCount)、Interlocked.Decrement(ref _currentCount)这样的方法

连接池需要是单例的,注意领域服务默认瞬态

知识点

连接池大小、最大并发量的区别

连接池大小,是指的连接池中维护的活跃连接数的最大数量。(活跃的连接不一定都被使用,有可能有的在池中)

最大并发量:指的是同时被使用的连接的最大数量。

.net 中的SemaphoreSlim 介绍

SemaphoreSlim 是 .NET 提供的一个轻量级的同步原语,用于控制对一组资源或资源池的访问。它是 Semaphore 类的简化版本,提供了异步支持并且只能用于同一个进程的线程之间的同步。

基本概念

  • 信号量(Semaphore) :是一种用于控制访问数量的同步原语。它维护了一个计数器,表示允许同时访问某一资源或资源池的线程数。
  • 初始计数(Initial Count) :创建信号量时指定的初始允许的并发数。
  • 最大计数(Max Count) :信号量能够达到的最大并发访问数。

主要方法

  • Wait/WaitAsync:请求进入信号量。如果当前计数为0,则线程或任务将阻塞,直到信号量被释放(即其他线程调用 Release)。WaitAsync 提供了异步版本,可以在异步编程模式下使用,避免阻塞线程。
  • Release:释放信号量。每次调用都会将信号量的当前计数增加,如果有等待的线程或任务,它们将被解除阻塞。

下面是一个使用 SemaphoreSlim 来控制对数据库连接池资源访问的示例。假设我们有一个限制为10个并发连接的数据库连接池:

csharp 复制代码
public class ConnectionPool
{
    private SemaphoreSlim _poolSemaphore;
    private Stack<IDbConnection> _connections;

    public ConnectionPool(int poolSize)
    {
        _poolSemaphore = new SemaphoreSlim(poolSize, poolSize);
        _connections = new Stack<IDbConnection>(poolSize);
        // 假设这里初始化连接并放入 _connections
    }

    public async Task<IDbConnection> GetConnectionAsync()
    {
        await _poolSemaphore.WaitAsync();
        lock (_connections)
        {
            return _connections.Pop();
        }
    }

    public void ReturnConnection(IDbConnection connection)
    {
        lock (_connections)
        {
            _connections.Push(connection);
        }
        _poolSemaphore.Release();
    }
}

使用注意事项

  • 正确的释放:使用 SemaphoreSlim 时,非常重要的一点是确保每个 Wait/WaitAsync 调用最终都能得到对应的 Release 调用。否则,可能会导致死锁或资源永久不可用的情况。
  • 异常安全:在可能抛出异常的代码块使用 SemaphoreSlim 时,确保在 finally 块中调用 Release,以防止因异常导致的资源泄露。
  • 与 CancellationToken 配合使用:WaitAsync 可以接受一个 CancellationToken 参数,这使得在等待信号量时可以响应取消请求,这对于提高应用程序的响应性非常有帮助。

总的来说,SemaphoreSlim 是一个非常实用的同步工具,适用于需要精细控制资源访问计数的场景。

代码

ConnectionManager

csharp 复制代码
public class ConnectionManager : DomainService, ISingletonDependency
{
    //注意,连接池需要单例,领域服务默认瞬态
    private readonly ConcurrentDictionary<string, DefaultConnectionPool> _connectionPools;
    private readonly IConnectionPoolRepository _connectionRepository;
    private readonly IScriptExecutingEngineProvider _engineProvider;

    public ConnectionManager(IConnectionPoolRepository connectionRepository, IScriptExecutingEngineProvider engineProvider)
    {
        _connectionPools = new ConcurrentDictionary<string, DefaultConnectionPool>();
        _connectionRepository = connectionRepository;
        _engineProvider = engineProvider;
    }

    public async Task<IDbConnection> GetConnectionAsync(Guid connectionId)
    {
        var connectionPool = await _connectionRepository.FirstOrDefaultAsync(x => x.Id == connectionId);
        var scriptEngine = _engineProvider.Get(connectionPool.DbType);
        var pool = _connectionPools.GetOrAdd(connectionPool.ConnectionString, cs => new DefaultConnectionPool(cs, maxPoolSize: connectionPool.PoolSize, maxConcurrency: connectionPool.MaxConcurrency, scriptEngine.GetConnection));
        return await pool.GetConnectionAsync();
    }

    public void ReturnConnection(IDbConnection connection)
    {
        if (_connectionPools.TryGetValue(connection.ConnectionString, out var pool))
        {
            pool.ReturnConnection(connection);
        }
        else
        {
            throw new InvalidOperationException("No pool found for the given connection string.");
        }
    }
}

DefaultConnectionPool

说明,这个之所以叫DefaultConnectionPool,是因为ConnectionPool 这个名称用来定义连接池的实体类了

csharp 复制代码
public class DefaultConnectionPool
{
    private readonly Func<string, IDbConnection> _createConnectionFunc;
    private readonly string _connectionString;
    private readonly int _maxPoolSize;
    private ConcurrentBag<IDbConnection> _connections;
    private int _currentCount = 0;
    private SemaphoreSlim _poolAccessSemaphore;  // Semaphore to control concurrency


    public DefaultConnectionPool(string connectionString, int maxPoolSize, int maxConcurrency, Func<string, IDbConnection> createConnectionFunc)
    {
        _createConnectionFunc = createConnectionFunc;
        _connectionString = connectionString;
        _maxPoolSize = maxPoolSize;
        _connections = new ConcurrentBag<IDbConnection>();
        _poolAccessSemaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);  // Initialize the semaphore
    }

    public async Task<IDbConnection> GetConnectionAsync()
    {
        await _poolAccessSemaphore.WaitAsync();  // Wait to enter the semaphore

        if (_connections.TryTake(out var connection))
        {
            return connection;
        }

        if (_currentCount < _maxPoolSize)
        {
            Interlocked.Increment(ref _currentCount);
            return _createConnectionFunc(_connectionString);
        }

        throw new InvalidOperationException("Maximum pool size reached.");
    }

    public void ReturnConnection(IDbConnection connection)
    {
        if (_connections.Count < _maxPoolSize)
        {
            _connections.Add(connection);
        }
        else
        {
            // 如果池满了,可以选择关闭并丢弃这个连接
            connection.Close();
            Interlocked.Decrement(ref _currentCount);
        }
        _poolAccessSemaphore.Release();
    }
}

外层调用示例

csharp 复制代码
//拿到连接
var connection = await _connectionManager.GetConnectionAsync(apiExecutionContext.Pool.Id);
//...用这个数据库连接做一些操作
//归还连接
_connectionManager.ReturnConnection(connection);
相关推荐
蜗牛互联网1 小时前
WSL Containers GA:本地AI容器的生命周期、网络与治理验收
java·网络·人工智能·后端
Zhou1411362 小时前
SpringBoot_02_自动配置原理
java·spring boot·后端
子兮曰3 小时前
Node.js 50个优势场景盘点:一个人单干,为啥我多数时候只用它
前端·后端
IT_陈寒3 小时前
我花3小时debug,就因为JavaScript这个隐式转换坑
前端·人工智能·后端
程序猿阿越4 小时前
containerd如何拉取镜像
后端·kubernetes·源码阅读
一条小小yu4 小时前
为什么使用springboot
java·spring boot·后端
香瓜子rd4 小时前
MySQL InnoDB 并发控制核心原理:事务、隔离级别、MVCC 与锁机制
数据库·后端
RISCV_Explorer4 小时前
RISC-V RVV向量编程模型机制解析——vsetvl配置、掩码与尾元素策略及长度无关编程
后端·risc-v
对象存储与RustFS4 小时前
升级 RustFS 二进制不停机:一条一条换,留一条退路
后端·rust·开源
hasty4 小时前
限制写了却没生效:OpenTelemetry Go 的 Unicode 截断边界
开发语言·后端·golang