对象资源池 ObjectPool 为Redis客户端、Rpc客户端等提供连接池能力。此外还实现了轻量级对象池 Pool<T>,无锁高性能。
Nuget包:NewLife.Core
源码:https://github.com/NewLifeX/X/blob/master/NewLife.Core/Collections/ObjectPool.cs
视频:https://www.bilibili.com/video/BV15W4y1H7F5
池接口IPool
有借有还,IPool定义了获取Get和归还Put方法。
/// <summary>对象池接口</summary>
/// <typeparam name="T"></typeparam>
public interface IPool<T> where T : class
{
/// <summary>对象池大小</summary>
Int32 Max { get; set; }
/// <summary>获取</summary>
/// <returns></returns>
T Get();
/// <summary>归还</summary>
/// <param name="value"></param>
Boolean Return(T value);
/// <summary>清空</summary>
Int32 Clear();
}
对象池ObjectPool
ObjectPool 是比较完整的对象池设计,具有所需各种功能,包括定时清理闲置资源。
Redis客户端内维持了多个到服务器的连接,用RedisClient表示,需要用一个对象池来管理,每次读写操作时取一个连接出来使用,用完以后还回去。
Redis连接池实现如下,每次借出时,如果池里RedisClient对象不够,则调用外部OnCreate创建连接对象。同时,借出对象前,做一次Reset清理操作,避免因上一次失败而导致残留返回数据影响这一次操作。
private class MyPool : ObjectPool<RedisClient>
{
public Redis Instance { get; set; }
protected override RedisClient OnCreate() => Instance.OnCreate();
protected override Boolean OnGet(RedisClient value)
{
// 借出时清空残留
value?.Reset();
return base.OnGet(value);
}
}
private MyPool _Pool;
/// <summary>连接池</summary>
public IPool<RedisClient> Pool
{
get
{
if (_Pool != null) return _Pool;
lock (this)
{
if (_Pool != null) return _Pool;
var pool = new MyPool
{
Name = Name + "Pool",
Instance = this,
Min = 2,
Max = 100000,
IdleTime = 20,
AllIdleTime = 120,
Log = Log,
};
return _Pool = pool;
}
}
}
这里我们看到,连接池设置了最小2个连接,最大100000个连接。空闲时间20秒,超过该空闲时间的连接将被销毁释放,直到留下最后2个。完全空闲时间120秒,把最后2个连接都干掉。
再来看看Redis连接池用法,对实际情况作了一些简化:
/// <summary>执行命令</summary>
/// <typeparam name="TResult">返回类型</typeparam>
/// <param name="key">命令key,用于选择集群节点</param>
/// <param name="func">回调函数</param>
/// <param name="write">是否写入操作</param>
/// <returns></returns>
public virtual TResult Execute<TResult>(String key, Func<RedisClient, TResult> func, Boolean write = false)
{
// 统计性能
var sw = Counter?.StartCount();
// 每次重试都需要重新从池里借出连接
var client = Pool.Get();
try
{
client.Reset();
return func(client);
}
finally
{
Pool.Return(client);
Counter?.StopCount(sw);
}
}
轻量级对象池Pool
Pool<T> 借助数组实现,无锁操作,具有超高性能,适用于高频调用场合,常用于规避GC。Pool 没有过期机制,只有最基本的借还。
内置了两个最常见的池,分别是用于字符串拼接的StringBuilder池和用于内存流处理的MemoryStream池,主要目的就是减少内存分配,减少GC。
StringBuilder池
/// <summary>字符串构建器池</summary>
public static IPool<StringBuilder> StringBuilder { get; set; } = new StringBuilderPool();
/// <summary>归还一个字符串构建器到对象池</summary>
/// <param name="sb"></param>
/// <param name="requireResult">是否需要返回结果</param>
/// <returns></returns>
public static String Return(this StringBuilder sb, Boolean requireResult = false)
{
if (sb == null) return null;
var str = requireResult ? sb.ToString() : null;
Pool.StringBuilder.Return(sb);
return str;
}
/// <summary>字符串构建器池</summary>
public class StringBuilderPool : Pool<StringBuilder>
{
/// <summary>初始容量。默认100个</summary>
public Int32 InitialCapacity { get; set; } = 100;
/// <summary>最大容量。超过该大小时不进入池内,默认4k</summary>
public Int32 MaximumCapacity { get; set; } = 4 * 1024;
/// <summary>创建</summary>
/// <returns></returns>
protected override StringBuilder OnCreate() => new StringBuilder(InitialCapacity);
/// <summary>归还</summary>
/// <param name="value"></param>
/// <returns></returns>
public override Boolean Return(StringBuilder value)
{
if (value.Capacity > MaximumCapacity) return false;
value.Clear();
return true;
}
}
在RedisClient中多次用到StringBuilder池,主要用于解析返回数据时,需要逐个字符拼接,不方便每次都分配新的StringBuilder实例,那样会带来很大GC压力。
private static String ReadLine(Stream ms)
{
var sb = Pool.StringBuilder.Get();
while (true)
{
var b = ms.ReadByte();
if (b < 0) break;
if (b == '\r')
{
var b2 = ms.ReadByte();
if (b2 < 0) break;
if (b2 == '\n') break;
sb.Append((Char)b);
sb.Append((Char)b2);
}
else
sb.Append((Char)b);
}
return sb.Return(true);
}
MemoryStream池
/// <summary>内存流池</summary>
public static IPool<MemoryStream> MemoryStream { get; set; } = new MemoryStreamPool();
/// <summary>归还一个内存流到对象池</summary>
/// <param name="ms"></param>
/// <param name="requireResult">是否需要返回结果</param>
/// <returns></returns>
public static Byte[] Return(this MemoryStream ms, Boolean requireResult = false)
{
if (ms == null) return null;
var buf = requireResult ? ms.ToArray() : null;
Pool.MemoryStream.Return(ms);
return buf;
}
/// <summary>内存流池</summary>
public class MemoryStreamPool : Pool<MemoryStream>
{
/// <summary>初始容量。默认1024个</summary>
public Int32 InitialCapacity { get; set; } = 1024;
/// <summary>最大容量。超过该大小时不进入池内,默认64k</summary>
public Int32 MaximumCapacity { get; set; } = 64 * 1024;
/// <summary>创建</summary>
/// <returns></returns>
protected override MemoryStream OnCreate() => new MemoryStream(InitialCapacity);
/// <summary>归还</summary>
/// <param name="value"></param>
/// <returns></returns>
public override Boolean Return(MemoryStream value)
{
if (value.Capacity > MaximumCapacity) return false;
value.Position = 0;
value.SetLength(0);
return true;
}
}
在RedisClient中有使用内存池,处理请求命令到内存流中,然后整体写入到网络流ns。如果不使用内存流池,就会导致每一次Redis操作都需要分配一个MemoryStream对象,用完后释放等待GC回收。在Redis高并发操作时,将会给GC系统带来很大压力。
var ms = Pool.MemoryStream.Get();
GetRequest(ms, cmd, args, oriArgs);
if (ms.Length > 0) ms.WriteTo(ns);
ms.Return();
总结
Redis 是对象池最大用户,除了ObjectPool,还有两个内置轻量级池都用到了。