csharp/2881099/csredis/src/CSRedisCore/RedisClientPool.cs

RedisClientPool.cs
using CSRedis.Internal.ObjectPool;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using System.Linq;
using System.Net.Sockets;

namespace CSRedis
{
    public clast RedisClientPool : ObjectPool
    {

        public RedisClientPool(string connectionString, Action onConnected) : base(null)
        {
            _policy = new RedisClientPoolPolicy
            {
                _pool = this
            };
            _policy.Connected += (s, o) =>
            {
                RedisClient rc = s as RedisClient;
                try
                {
                    rc.ReceiveTimeout = _policy._syncTimeout;
                    rc.SendTimeout = _policy._syncTimeout;
                }
                catch { }
                if (!string.IsNullOrEmpty(_policy._pastword))
                {
                    try
                    {
                        rc.Auth(_policy._pastword);
                    }
                    catch (Exception authEx)
                    {
                        if (authEx.Message != "ERR Client sent AUTH, but no pastword is set")
                            throw authEx;
                    }
                }
                if (_policy._database > 0) rc.Select(_policy._database);
                onConnected(s as RedisClient);
            };
            this.Policy = _policy;
            _policy.ConnectionString = connectionString;
        }

        public void Return(Object obj, Exception exception, bool isRecreate = false)
        {
            if (exception != null)
            {
                try
                {
                    try
                    {
                        if (!obj.Value.IsConnected) obj.Value.Connect(_policy._connectTimeout);
                        obj.Value.Ping();

                        var fcolor = Console.ForegroundColor;
                        Console.WriteLine($"");
                        Console.ForegroundColor = ConsoleColor.DarkYellow;
                        Console.WriteLine($"csreids 错误【{Policy.Name}】:{exception.Message} {exception.StackTrace}");
                        Console.ForegroundColor = fcolor;
                        Console.WriteLine($"");
                    }
                    catch
                    {
                        obj.ResetValue();
                        if (!obj.Value.IsConnected) obj.Value.Connect(_policy._connectTimeout);
                        obj.Value.Ping();
                    }
                }
                catch (Exception ex)
                {
                    base.SetUnavailable(ex);
                }
            }
            base.Return(obj, isRecreate);
        }

        internal bool CheckAvailable() => base.LiveCheckAvailable();

        internal RedisClientPoolPolicy _policy;
        public string Key => _policy.Key;
        public string Prefix => _policy.Prefix;
        public Encoding Encoding { get; set; } = new UTF8Encoding(false);
    }

    public clast RedisClientPoolPolicy : IPolicy
    {

        internal RedisClientPool _pool;
        internal int _port = 6379, _database = 0, _tryit = 0, _connectTimeout = 5000, _syncTimeout = 10000;
        internal string _ip = "127.0.0.1", _pastword = "", _clientname = "";
        internal bool _ssl = false, _testCluster = true, _asyncPipeline = false;
        internal int _preheat = 5;
        internal string Key => $"{_ip}:{_port}/{_database}";
        internal string Prefix { get; set; }
        public event EventHandler Connected;

        public string Name { get => Key; set { throw new Exception("RedisClientPoolPolicy 不提供设置 Name 属性值。"); } }
        public int PoolSize { get; set; } = 50;
        public TimeSpan SyncGetTimeout { get; set; } = TimeSpan.FromSeconds(10);
        public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(20);
        public int AsyncGetCapacity { get; set; } = 100000;
        public bool IsThrowGetTimeoutException { get; set; } = true;
        public bool IsAutoDisposeWithSystem { get; set; } = true;
        public int CheckAvailableInterval { get; set; } = 5;

        internal string BuildConnectionString(string endpoint)
        {
            return $"{endpoint},pastword={_pastword},defaultDatabase={_database},poolsize={PoolSize}," +
                $"connectTimeout={_connectTimeout},syncTimeout={_syncTimeout},idletimeout={(int)IdleTimeout.TotalMilliseconds}," +
                $"preheat=false,ssl={(_ssl ? "true" : "false")},tryit={_tryit},name={_clientname},prefix={Prefix}," + 
                $"autodispose={(IsAutoDisposeWithSystem ? "true" : "false")},asyncpipeline={(_asyncPipeline ? "true" : "false")}";
        }

        internal void SetHost(string host)
        {
            if (string.IsNullOrEmpty(host?.Trim())) {
                _ip = "127.0.0.1";
                _port = 6379;
                return;
            }
            host = host.Trim();
            var ipv6 = Regex.Match(host, @"^\[([^\]]+)\]\s*(:\s*(\d+))?$");
            if (ipv6.Success) //ipv6+port 格式: [fe80::b164:55b3:4b4f:7ce6%15]:6379
            {
                _ip = ipv6.Groups[1].Value.Trim();
                _port = int.TryParse(ipv6.Groups[3].Value, out var tryint) && tryint > 0 ? tryint : 6379;
                return;
            }
            var spt = (host ?? "").Split(':');
            if (spt.Length == 1) //ipv4 or domain
            {
                _ip = string.IsNullOrEmpty(spt[0].Trim()) == false ? spt[0].Trim() : "127.0.0.1";
                _port = 6379;
                return;
            }
            if (spt.Length == 2) //ipv4:port or domain:port
            {
                if (int.TryParse(spt.Last().Trim(), out var testPort2))
                {
                    _ip = string.IsNullOrEmpty(spt[0].Trim()) == false ? spt[0].Trim() : "127.0.0.1";
                    _port = testPort2;
                    return;
                }
                _ip = host;
                _port = 6379;
                return;
            }
            if (IPAddress.TryParse(host, out var tryip) && tryip.AddressFamily == AddressFamily.InterNetworkV6) //test ipv6
            {
                _ip = host;
                _port = 6379;
                return;
            }
            if (int.TryParse(spt.Last().Trim(), out var testPort)) //test ipv6:port
            {
                var testHost = string.Join(":", spt.Where((a, b) => b < spt.Length - 1));
                if (IPAddress.TryParse(testHost, out tryip) && tryip.AddressFamily == AddressFamily.InterNetworkV6)
                {
                    _ip = testHost;
                    _port = 6379;
                    return;
                }
            }
            _ip = host;
            _port = 6379;
        }

        private string _connectionString;
        public string ConnectionString
        {
            get => _connectionString;
            set
            {
                _connectionString = value;
                if (string.IsNullOrEmpty(_connectionString)) return;

                //支持密码中带有逗号,将原有 split(',') 改成以下处理方式
                var vs = Regex.Split(_connectionString, @"\,([\w \t\r\n]+)=", RegexOptions.Multiline);
                this.SetHost(vs[0]);

                for (var a = 1; a < vs.Length; a += 2)
                {
                    var kv = new[] { vs[a].ToLower().Trim(), vs[a + 1] };
                    switch (kv[0])
                    {
                        case "pastword":
                            _pastword = kv.Length > 1 ? kv[1] : "";
                            break;
                        case "prefix":
                            Prefix = kv.Length > 1 ? kv[1] : "";
                            break;
                        case "defaultdatabase":
                            _database = int.TryParse(kv.Length > 1 ? kv[1].Trim() : "0", out _database) ? _database : 0;
                            break;
                        case "poolsize":
                            PoolSize = int.TryParse(kv.Length > 1 ? kv[1].Trim() : "0", out var poolsize) == false || poolsize  1 ? kv[1].ToLower().Trim() == "true" : false;
                            break;
                        case "preheat":
                            var kvtrim = kv.Length > 1 ? kv[1].ToLower().Trim() : null;
                            _preheat = kvtrim == "true" ? -1 : (int.TryParse(kvtrim, out _preheat) ? _preheat : 0);
                            break;
                        case "name":
                            _clientname = kv.Length > 1 ? kv[1] : "";
                            break;
                        case "tryit":
                            _tryit = int.TryParse(kv.Length > 1 ? kv[1].Trim() : "0", out _tryit) ? _tryit : 0;
                            break;
                        case "connecttimeout":
                            _connectTimeout = int.TryParse(kv.Length > 1 ? kv[1].Trim() : "5000", out var connectTimeout) == false || connectTimeout  1 ? kv[1].Trim() : "10000", out var syncTimeout) == false || syncTimeout  1 ? kv[1].Trim() : "0", out var idleTimeout) == false || idleTimeout  1 ? kv[1].ToLower().Trim() == "true" : true;
                            break;
                        case "autodispose":
                            IsAutoDisposeWithSystem = kv.Length > 1 ? kv[1].ToLower().Trim() == "true" : true;
                            break;
                        case "asyncpipeline":
                            _asyncPipeline = kv.Length > 1 ? kv[1].ToLower().Trim() == "true" : true;
                            break;
                    }
                }

                if (_preheat < 0) _preheat = PoolSize;
                if (_preheat > 0)
                    PrevReheatConnectionPool(_pool, _preheat);
            }
        }

        public bool OnCheckAvailable(Object obj)
        {
            obj.ResetValue();
            if (!obj.Value.IsConnected) obj.Value.Connect(_connectTimeout);
            return obj.Value.Ping() == "PONG";
        }

        public RedisClient OnCreate()
        {
            RedisClient client = null;
            if (IPAddress.TryParse(_ip, out var tryip))
            {
                client = new RedisClient(new IPEndPoint(tryip, _port), _ssl);
            }
            else
            {
                var ips = Dns.GetHostAddresses(_ip);
                if (ips.Length == 0) throw new Exception($"无法解析“{_ip}”");
                client = new RedisClient(_ip, _port, _ssl);
            }
            client.Connected += (s, o) =>
            {
                Connected(s, o);
                if (!string.IsNullOrEmpty(_clientname)) client.ClientSetName(_clientname);
            };
            return client;
        }

        public void OnDestroy(RedisClient obj)
        {
            if (obj != null)
            {
                //if (obj.IsConnected) try { obj.Quit(); } catch { } 此行会导致,服务器主动断开后,执行该命令超时停留10-20秒
                try { obj.Dispose(); } catch { }
            }
        }

        public void OnGet(Object obj)
        {
            if (_pool.Encoding != obj.Value.Encoding) obj.Value.Encoding = _pool.Encoding;
            if (_pool.IsAvailable)
            {
                if (DateTime.Now.Subtract(obj.LastReturnTime).TotalSeconds > 60 || obj.Value.IsConnected == false)
                {
                    try
                    {
                        if (!obj.Value.IsConnected) obj.Value.Connect(_connectTimeout);
                        obj.Value.Ping();
                    }
                    catch
                    {
                        obj.ResetValue();
                    }
                }
            }
        }
#if net40
#else
        async public Task OnGetAsync(Object obj)
        {
            if (_pool.Encoding != obj.Value.Encoding) obj.Value.Encoding = _pool.Encoding;
            if (_pool.IsAvailable)
            {
                if (DateTime.Now.Subtract(obj.LastReturnTime).TotalSeconds > 60 || obj.Value.IsConnected == false)
                {
                    try
                    {
                        if (!obj.Value.IsConnected) obj.Value.Connect(_connectTimeout);
                        await obj.Value.PingAsync();
                    }
                    catch
                    {
                        obj.ResetValue();
                    }
                }
            }
        }
#endif

        public void OnGetTimeout()
        {

        }

        public void OnReturn(Object obj)
        {

        }

        public void OnAvailable()
        {
        }
        public void OnUnavailable()
        {
        }

        public static void PrevReheatConnectionPool(ObjectPool pool, int minPoolSize)
        {
            if (minPoolSize  pool.Policy.PoolSize) minPoolSize = pool.Policy.PoolSize;
            var initTestOk = true;
            var initStartTime = DateTime.Now;
            var initConns = new ConcurrentBag();

            try
            {
                var conn = pool.Get();
                initConns.Add(conn);
                pool.Policy.OnCheckAvailable(conn);
            }
            catch (Exception ex)
            {
                initTestOk = false; //预热一次失败,后面将不进行
                pool.SetUnavailable(ex);
            }
            for (var a = 1; initTestOk && a < minPoolSize; a += 10)
            {
                if (initStartTime.Subtract(DateTime.Now).TotalSeconds > 3) break; //预热耗时超过3秒,退出
                var b = Math.Min(minPoolSize - a, 10); //每10个预热
                var initTasks = new Task[b];
                for (var c = 0; c < b; c++)
                {
                    initTasks[c] = TaskEx.Run(() =>
                    {
                        try
                        {
                            var conn = pool.Get();
                            initConns.Add(conn);
                            pool.Policy.OnCheckAvailable(conn);
                        }
                        catch
                        {
                            initTestOk = false;  //有失败,下一组退出预热
                        }
                    });
                }
                Task.WaitAll(initTasks);
            }
            while (initConns.TryTake(out var conn)) pool.Return(conn);
        }
    }
}