背景
项目需要做一个动态 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);