.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);
相关推荐
攸攸太上10 分钟前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
罗曼蒂克在消亡26 分钟前
graphql--快速了解graphql特点
后端·graphql
潘多编程28 分钟前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
大神薯条老师1 小时前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
2401_857622662 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端
AskHarries2 小时前
如何优雅的处理NPE问题?
java·spring boot·后端
计算机学姐3 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
猿java4 小时前
Cookie和Session的区别
java·后端·面试
程序员陆通4 小时前
Spring Boot RESTful API开发教程
spring boot·后端·restful
无理 Java4 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc