.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);
相关推荐
uzong1 小时前
技术故障复盘模版
后端
GetcharZp1 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程2 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研2 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国3 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy3 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack4 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9654 小时前
pip install 已经不再安全
后端
寻月隐君4 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github