对象资源池 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 Put(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 = 1000,
                IdleTime = 20,
                AllIdleTime = 120,
                Log = Log,
            };

            return _Pool = pool;
        }
    }
}

这里我们看到,连接池设置了最小2个连接,最大1000个连接。空闲时间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.Put(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 Put(this StringBuilder sb, Boolean requireResult = false)
{
    if (sb == null) return null;

    var str = requireResult ? sb.ToString() : null;

    Pool.StringBuilder.Put(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 Put(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.Put(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[] Put(this MemoryStream ms, Boolean requireResult = false)
{
    if (ms == null) return null;

    var buf = requireResult ? ms.ToArray() : null;

    Pool.MemoryStream.Put(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 Put(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.Put();


总结

Redis 是对象池最大用户,除了ObjectPool,还有两个内置轻量级池都用到了。

分布式缓存NewLife.Redis