基於RabbitMQ.Client組件實現RabbitMQ可復用的 ConnectionPool(連接池)

来源:https://www.cnblogs.com/zuowj/archive/2018/04/21/8901191.html
-Advertisement-
Play Games

一、本文產生原由: 之前文章《總結消息隊列RabbitMQ的基本用法》已對RabbitMQ的安裝、用法都做了詳細說明,而本文主要是針對在高併發且單次從RabbitMQ中消費消息時,出現了連接數不足、連接響應較慢、RabbitMQ伺服器崩潰等各種性能問題的解方案,之所以會出現我列舉的這些問題,究基根源 ...


一、本文產生原由:  

 之前文章《總結消息隊列RabbitMQ的基本用法》已對RabbitMQ的安裝、用法都做了詳細說明,而本文主要是針對在高併發且單次從RabbitMQ中消費消息時,出現了連接數不足、連接響應較慢、RabbitMQ伺服器崩潰等各種性能問題的解方案,之所以會出現我列舉的這些問題,究基根源,其實是TCP連接創建與斷開太過頻繁所致,這與我們使用ADO.NET來訪問常規的關係型DB(如:SQL SERVER、MYSQL)有所不同,在訪問DB時,我們一般都建議大家使用using包裹,目的是每次創建完DB連接,使用完成後自動釋放連接,避免不必要的連接數及資源占用。可能有人會問,為何訪問DB,可以每次創建再斷開連接,都沒有問題,而同樣訪問MQ(本文所指的MQ均是RabbitMQ),每次創建再斷開連接,如果在高併發且創建與斷開頻率高的時候,會出現性能問題呢?其實如果瞭解了DB的連接創建與斷開以及MQ的連接創建與斷開原理就知道其中的區別了。這裡我簡要說明一下,DB連接與MQ連接 其實底層都是基於TCP連接,創建TCP連接肯定是有資源消耗的,是非常昂貴的,原則上儘可能少的去創建與斷開TCP連接,DB創建連接、MQ創建連接可以說是一樣的,但在斷開銷毀連接上就有很大的不同,DB創建連接再斷開時,預設情況下是把該連接回收到連接池中,下次如果再有DB連接創建請求,則先判斷DB連接池中是否有空閑的連接,若有則直接復用,若沒有才創建連接,這樣就達到了TCP連接的復用,而MQ創建連接都是新創建的TCP連接,斷開時則直接斷開TCP連接,簡單粗暴,看似資源清理更徹底,但若在高併發高頻率每次都重新創建與斷開MQ連接,則性能只會越來越差(上面說過TCP連接是非常昂貴的),我在公司項目中就出現了該問題,後面在技術總監的指導下,對MQ的連接創建與斷開作了優化,實現了類似DB連接池的概念。

連接池,故名思義,連接的池子,所有的連接作為一種資源集中存放在池中,需要使用時就可以到池中獲取空閑連接資源,用完後再放回池中,以此達到連接資源的有效重用,同時也控制了資源的過度消耗與浪費(資源多少取決於池子的容量)

二、源代碼奉獻(可直接複製應用到大家的項目中) 

下麵就先貼出實現MQHelper(含連接池)的源代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RabbitMQ.Util;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Web.Caching;
using System.Web;
using System.Configuration;
using System.IO;
using System.Collections.Concurrent;
using System.Threading;
using System.Runtime.CompilerServices;

namespace Zuowj.Core
{
    public class MQHelper
    {
        private const string CacheKey_MQConnectionSetting = "MQConnectionSetting";
        private const string CacheKey_MQMaxConnectionCount = "MQMaxConnectionCount";

        private readonly static ConcurrentQueue<IConnection> FreeConnectionQueue;//空閑連接對象隊列
        private readonly static ConcurrentDictionary<IConnection, bool> BusyConnectionDic;//使用中(忙)連接對象集合
        private readonly static ConcurrentDictionary<IConnection, int> MQConnectionPoolUsingDicNew;//連接池使用率
        private readonly static Semaphore MQConnectionPoolSemaphore;
        private readonly static object freeConnLock = new object(), addConnLock = new object();
        private static int connCount = 0;

        public const int DefaultMaxConnectionCount = 30;//預設最大保持可用連接數
        public const int DefaultMaxConnectionUsingCount = 10000;//預設最大連接可訪問次數


        private static int MaxConnectionCount
        {
            get
            {
                if (HttpRuntime.Cache[CacheKey_MQMaxConnectionCount] != null)
                {
                    return Convert.ToInt32(HttpRuntime.Cache[CacheKey_MQMaxConnectionCount]);
                }
                else
                {
                    int mqMaxConnectionCount = 0;
                    string mqMaxConnectionCountStr = ConfigurationManager.AppSettings[CacheKey_MQMaxConnectionCount];
                    if (!int.TryParse(mqMaxConnectionCountStr, out mqMaxConnectionCount) || mqMaxConnectionCount <= 0)
                    {
                        mqMaxConnectionCount = DefaultMaxConnectionCount;
                    }

                    string appConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App.config");
                    HttpRuntime.Cache.Insert(CacheKey_MQMaxConnectionCount, mqMaxConnectionCount, new CacheDependency(appConfigPath));

                    return mqMaxConnectionCount;
                }

            }
        }

        /// <summary>
        /// 建立連接
        /// </summary>
        /// <param name="hostName">伺服器地址</param>
        /// <param name="userName">登錄賬號</param>
        /// <param name="passWord">登錄密碼</param>
        /// <returns></returns>
        private static ConnectionFactory CrateFactory()
        {
            var mqConnectionSetting = GetMQConnectionSetting();
            var connectionfactory = new ConnectionFactory();
            connectionfactory.HostName = mqConnectionSetting[0];
            connectionfactory.UserName = mqConnectionSetting[1];
            connectionfactory.Password = mqConnectionSetting[2];
            if (mqConnectionSetting.Length > 3) //增加埠號
            {
                connectionfactory.Port = Convert.ToInt32(mqConnectionSetting[3]);
            }
            return connectionfactory;
        }

        private static string[] GetMQConnectionSetting()
        {
            string[] mqConnectionSetting = null;
            if (HttpRuntime.Cache[CacheKey_MQConnectionSetting] == null)
            {
                //MQConnectionSetting=Host IP|;userid;|;password
                string mqConnSettingStr = ConfigurationManager.AppSettings[CacheKey_MQConnectionSetting];
                if (!string.IsNullOrWhiteSpace(mqConnSettingStr))
                {
                    mqConnSettingStr = EncryptUtility.Decrypt(mqConnSettingStr);//解密MQ連接字元串,若項目中無此需求可移除,EncryptUtility是一個AES的加解密工具類,大家網上可自行查找
                    if (mqConnSettingStr.Contains(";|;"))
                    {
                        mqConnectionSetting = mqConnSettingStr.Split(new[] { ";|;" }, StringSplitOptions.RemoveEmptyEntries);
                    }
                }

                if (mqConnectionSetting == null || mqConnectionSetting.Length < 3)
                {
                    throw new Exception("MQConnectionSetting未配置或配置不正確");
                }

                string appConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App.config");
                HttpRuntime.Cache.Insert(CacheKey_MQConnectionSetting, mqConnectionSetting, new CacheDependency(appConfigPath));
            }
            else
            {
                mqConnectionSetting = HttpRuntime.Cache[CacheKey_MQConnectionSetting] as string[];
            }

            return mqConnectionSetting;
        }




        public static IConnection CreateMQConnection()
        {
            var factory = CrateFactory();
            factory.AutomaticRecoveryEnabled = true;//自動重連
            var connection = factory.CreateConnection();
            connection.AutoClose = false;
            return connection;
        }


        static MQHelper()
        {
            FreeConnectionQueue = new ConcurrentQueue<IConnection>();
            BusyConnectionDic = new ConcurrentDictionary<IConnection, bool>();
            MQConnectionPoolUsingDicNew = new ConcurrentDictionary<IConnection, int>();//連接池使用率
            MQConnectionPoolSemaphore = new Semaphore(MaxConnectionCount, MaxConnectionCount, "MQConnectionPoolSemaphore");//信號量,控制同時併發可用線程數

        }

        public static IConnection CreateMQConnectionInPoolNew()
        {

        SelectMQConnectionLine:

            MQConnectionPoolSemaphore.WaitOne();//當<MaxConnectionCount時,會直接進入,否則會等待直到空閑連接出現

            IConnection mqConnection = null;
            if (FreeConnectionQueue.Count + BusyConnectionDic.Count < MaxConnectionCount)//如果已有連接數小於最大可用連接數,則直接創建新連接
            {
                lock (addConnLock)
                {
                    if (FreeConnectionQueue.Count + BusyConnectionDic.Count < MaxConnectionCount)
                    {
                        mqConnection = CreateMQConnection();
                        BusyConnectionDic[mqConnection] = true;//加入到忙連接集合中
                        MQConnectionPoolUsingDicNew[mqConnection] = 1;
                        //  BaseUtil.Logger.DebugFormat("Create a MQConnection:{0},FreeConnectionCount:{1}, BusyConnectionCount:{2}", mqConnection.GetHashCode().ToString(), FreeConnectionQueue.Count, BusyConnectionDic.Count);
                        return mqConnection;
                    }
                }
            }


            if (!FreeConnectionQueue.TryDequeue(out mqConnection)) //如果沒有可用空閑連接,則重新進入等待排隊
            {
                // BaseUtil.Logger.DebugFormat("no FreeConnection,FreeConnectionCount:{0}, BusyConnectionCount:{1}", FreeConnectionQueue.Count, BusyConnectionDic.Count);
                goto SelectMQConnectionLine;
            }
            else if (MQConnectionPoolUsingDicNew[mqConnection] + 1 > DefaultMaxConnectionUsingCount || !mqConnection.IsOpen) //如果取到空閑連接,判斷是否使用次數是否超過最大限制,超過則釋放連接並重新創建
            {
                mqConnection.Close();
                mqConnection.Dispose();
                // BaseUtil.Logger.DebugFormat("close > DefaultMaxConnectionUsingCount mqConnection,FreeConnectionCount:{0}, BusyConnectionCount:{1}", FreeConnectionQueue.Count, BusyConnectionDic.Count);

                mqConnection = CreateMQConnection();
                MQConnectionPoolUsingDicNew[mqConnection] = 0;
                // BaseUtil.Logger.DebugFormat("create new mqConnection,FreeConnectionCount:{0}, BusyConnectionCount:{1}", FreeConnectionQueue.Count, BusyConnectionDic.Count);
            }

            BusyConnectionDic[mqConnection] = true;//加入到忙連接集合中
            MQConnectionPoolUsingDicNew[mqConnection] = MQConnectionPoolUsingDicNew[mqConnection] + 1;//使用次數加1

            // BaseUtil.Logger.DebugFormat("set BusyConnectionDic:{0},FreeConnectionCount:{1}, BusyConnectionCount:{2}", mqConnection.GetHashCode().ToString(), FreeConnectionQueue.Count, BusyConnectionDic.Count);

            return mqConnection;
        }

        private static void ResetMQConnectionToFree(IConnection connection)
        {
            lock (freeConnLock)
            {
                bool result = false;
                if (BusyConnectionDic.TryRemove(connection, out result)) //從忙隊列中取出
                {
                    //  BaseUtil.Logger.DebugFormat("set FreeConnectionQueue:{0},FreeConnectionCount:{1}, BusyConnectionCount:{2}", connection.GetHashCode().ToString(), FreeConnectionQueue.Count, BusyConnectionDic.Count);
                }
                else
                {
                    // BaseUtil.Logger.DebugFormat("failed TryRemove BusyConnectionDic:{0},FreeConnectionCount:{1}, BusyConnectionCount:{2}", connection.GetHashCode().ToString(), FreeConnectionQueue.Count, BusyConnectionDic.Count);
                }

                if (FreeConnectionQueue.Count + BusyConnectionDic.Count > MaxConnectionCount)//如果因為高併發出現極少概率的>MaxConnectionCount,則直接釋放該連接
                {
                    connection.Close();
                    connection.Dispose();
                }
                else
                {
                    FreeConnectionQueue.Enqueue(connection);//加入到空閑隊列,以便持續提供連接服務
                }

                MQConnectionPoolSemaphore.Release();//釋放一個空閑連接信號

                //Interlocked.Decrement(ref connCount);
                //BaseUtil.Logger.DebugFormat("Enqueue FreeConnectionQueue:{0},FreeConnectionCount:{1}, BusyConnectionCount:{2},thread count:{3}", connection.GetHashCode().ToString(), FreeConnectionQueue.Count, BusyConnectionDic.Count,connCount);
            }
        }


        /// <summary>
        /// 發送消息
        /// </summary>
        /// <param name="connection">消息隊列連接對象</param>
        /// <typeparam name="T">消息類型</typeparam>
        /// <param name="queueName">隊列名稱</param>
        /// <param name="durable">是否持久化</param>
        /// <param name="msg">消息</param>
        /// <returns></returns>
        public static string SendMsg(IConnection connection, string queueName, string msg, bool durable = true)
        {
            try
            {

                using (var channel = connection.CreateModel())//建立通訊通道
                {
                    // 參數從前面開始分別意思為:隊列名稱,是否持久化,獨占的隊列,不使用時是否自動刪除,其他參數
                    channel.QueueDeclare(queueName, durable, false, false, null);

                    var properties = channel.CreateBasicProperties();
                    properties.DeliveryMode = 2;//1表示不持久,2.表示持久化

                    if (!durable)
                        properties = null;

                    var body = Encoding.UTF8.GetBytes(msg);
                    channel.BasicPublish("", queueName, properties, body);
                }


                return string.Empty;
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
            finally
            {
                ResetMQConnectionToFree(connection);
            }
        }

        /// <summary>
        /// 消費消息
        /// </summary>
        /// <param name="connection">消息隊列連接對象</param>
        /// <param name="queueName">隊列名稱</param>
        /// <param name="durable">是否持久化</param>
        /// <param name="dealMessage">消息處理函數</param>
        /// <param name="saveLog">保存日誌方法,可選</param>
        public static void ConsumeMsg(IConnection connection, string queueName, bool durable, Func<string, ConsumeAction> dealMessage, Action<string, Exception> saveLog = null)
        {
            try
            {

                using (var channel = connection.CreateModel())
                {
                    channel.QueueDeclare(queueName, durable, false, false, null); //獲取隊列 
                    channel.BasicQos(0, 1, false); //分發機製為觸髮式

                    var consumer = new QueueingBasicConsumer(channel); //建立消費者
                    // 從左到右參數意思分別是:隊列名稱、是否讀取消息後直接刪除消息,消費者
                    channel.BasicConsume(queueName, false, consumer);

                    while (true)  //如果隊列中有消息
                    {
                        ConsumeAction consumeResult = ConsumeAction.RETRY;
                        var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue(); //獲取消息
                        string message = null;

                        try
                        {
                            var body = ea.Body;
                            message = Encoding.UTF8.GetString(body);
                            consumeResult = dealMessage(message);
                        }
                        catch (Exception ex)
                        {
                            if (saveLog != null)
                            {
                                saveLog(message, ex);
                            }
                        }
                        if (consumeResult == ConsumeAction.ACCEPT)
                        {
                            channel.BasicAck(ea.DeliveryTag, false);  //消息從隊列中刪除
                        }
                        else if (consumeResult == ConsumeAction.RETRY)
                        {
                            channel.BasicNack(ea.DeliveryTag, false, true); //消息重回隊列
                        }
                        else
                        {
                            channel.BasicNack(ea.DeliveryTag, false, false); //消息直接丟棄
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                if (saveLog != null)
                {
                    saveLog("QueueName:" + queueName, ex);
                }

                throw ex;
            }
            finally
            {
                ResetMQConnectionToFree(connection);
            }
        }


        /// <summary>
        /// 依次獲取單個消息
        /// </summary>
        /// <param name="connection">消息隊列連接對象</param>
        /// <param name="QueueName">隊列名稱</param>
        /// <param name="durable">持久化</param>
        /// <param name="dealMessage">處理消息委托</param>
        public static void ConsumeMsgSingle(IConnection connection, string QueueName, bool durable, Func<string, ConsumeAction> dealMessage)
        {
            try
            {

                using (var channel = connection.CreateModel())
                {
                    channel.QueueDeclare(QueueName, durable, false, false, null); //獲取隊列 
                    channel.BasicQos(0, 1, false); //分發機製為觸髮式

                    uint msgCount = channel.MessageCount(QueueName);

                    if (msgCount > 0)
                    {
                        var consumer = new QueueingBasicConsumer(channel); //建立消費者
                        // 從左到右參數意思分別是:隊列名稱、是否讀取消息後直接刪除消息,消費者
                        channel.BasicConsume(QueueName, false, consumer);

                        ConsumeAction consumeResult = ConsumeAction.RETRY;
                        var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue(); //獲取消息
                        try
                        {
                            var body = ea.Body;
                            var message = Encoding.UTF8.GetString(body);
                            consumeResult = dealMessage(message);
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        finally
                        {
                            if (consumeResult == ConsumeAction.ACCEPT)
                            {
                                channel.BasicAck(ea.DeliveryTag, false);  //消息從隊列中刪除
                            }
                            else if (consumeResult == ConsumeAction.RETRY)
                            {
                                channel.BasicNack(ea.DeliveryTag, false, true); //消息重回隊列
                            }
                            else
                            {
                                channel.BasicNack(ea.DeliveryTag, false, false); //消息直接丟棄
                            }
                        }
                    }
                    else
                    {
                        dealMessage(string.Empty);
                    }
                }

            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                ResetMQConnectionToFree(connection);
            }
        }


        /// <summary>
        /// 獲取隊列消息數
        /// </summary>
        /// <param name="connection"></param>
        /// <param name="QueueName"></param>
        /// <returns></returns>
        public static int GetMessageCount(IConnection connection, string QueueName)
        {
            int msgCount = 0;
            try
            {

                using (var channel = connection.CreateModel())
                {
                    channel.QueueDeclare(QueueName, true, false, false, null); //獲取隊列 
                    msgCount = (int)channel.MessageCount(QueueName);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                ResetMQConnectionToFree(connection);
            }

            return msgCount;
        }


    }

    public enum ConsumeAction
    {
        ACCEPT,  // 消費成功
        RETRY,   // 消費失敗,可以放回隊列重新消費
        REJECT,  // 消費失敗,直接丟棄
    }
}

現在對上述代碼的核心點作一個簡要的說明:

先說一下靜態構造函數:

FreeConnectionQueue 用於存放空閑連接對象隊列,為何使用Queue,因為當我從中取出1個空閑連接後,空閑連接數就應該少1個,這個Queue很好滿足這個需求,而且這個Queue是併發安全的Queue哦(ConcurrentQueue)

BusyConnectionDic 忙(使用中)連接對象集合,為何這裡使用字典對象呢,因為當我用完後,需要能夠快速的找出使用中的連接對象,並能快速移出,同時重新放入到空閑隊列FreeConnectionQueue ,達到連接復用

MQConnectionPoolUsingDicNew 連接使用次數記錄集合,這個只是輔助記錄連接使用次數,以便可以計算一個連接的已使用次數,當達到最大使用次數時,則應斷開重新創建

MQConnectionPoolSemaphore 這個是信號量,這是控制併發連接的重要手段,連接池的容量等同於這個信號量的最大可並行數,保證同時使用的連接數不超過連接池的容量,若超過則會等待;

具體步驟說明:

1.MaxConnectionCount:最大保持可用連接數(可以理解為連接池的容量),可以通過CONFIG配置,預設為30; 

2.DefaultMaxConnectionUsingCount:預設最大連接可訪問次數,我這裡沒有使用配置,而是直接使用常量固定為1000,大家若有需要可以改成從CONFIG配置,參考MaxConnectionCount的屬性設置(採取了依賴緩存)

3.CreateMQConnectionInPoolNew:從連接池中創建MQ連接對象,這個是核心方法,是實現連接池的地方,代碼中已註釋了重要的步驟邏輯,這裡說一下實現思路:

  3.1 通過MQConnectionPoolSemaphore.WaitOne() 利用信號量的並行等待方法,如果當前併發超過信號量的最大並行度(也就是作為連接池的最大容量),則需要等待空閑連接池,防止連接數超過池的容量,如果併發沒有超過池的容量,則可以進入獲取連接的邏輯;

  3.2FreeConnectionQueue.Count + BusyConnectionDic.Count < MaxConnectionCount,如果空閑連接隊列+忙連接集合的總數小於連接池的容量,則可以直接創建新的MQ連接,否則FreeConnectionQueue.TryDequeue(out mqConnection) 嘗試從空閑連接隊列中獲取一個可用的空閑連接使用,若空閑連接都沒有,則需要返回到方法首行,重新等待空閑連接;

  3.3MQConnectionPoolUsingDicNew[mqConnection] + 1 > DefaultMaxConnectionUsingCount || !mqConnection.IsOpen 如果取到空閑連接,則先判斷使用次數是否超過最大限制,超過則釋放連接或空閑連接已斷開連接也需要重新創建,否則該連接可用;

  3.4BusyConnectionDic[mqConnection] = true;加入到忙連接集合中,MQConnectionPoolUsingDicNew[mqConnection] = MQConnectionPoolUsingDicNew[mqConnection] + 1; 使用次數加1,確保每使用一次連接,連接次數能記錄

4.ResetMQConnectionToFree:重置釋放連接對象,這個是保證MQ連接用完後能夠回收到空閑連接隊列中(即:回到連接池中),而不是直接斷開連接,這個方法很簡單就不作作過多說明。

 好了,都說明瞭如何實現含連接池的MQHelper,現在再來舉幾個例子來說明如何用:

三、實際應用(簡單易上手)

獲取並消費一個消息:

        public string GetMessage(string queueName)
        {
            string message = null;
            try
            {
                var connection = MQHelper.CreateMQConnectionInPoolNew();

                MQHelper.ConsumeMsgSingle(connection, queueName, true, (msg) =>
                {
                    message = msg;
                    return ConsumeAction.ACCEPT;
                });
            }
            catch (Exception ex)
            {
                BaseUtil.Logger.Error(string.Format("MQHelper.ConsumeMsgSingle Error:{0}", ex.Message), ex);
                message = "ERROR:" + ex.Message;
            }

            //BaseUtil.Logger.InfoFormat("第{0}次請求,從消息隊列(隊列名稱:{1})中獲取消息值為:{2}", Interlocked.Increment(ref requestCount), queueName, message);


            return message;


        }

 發送一個消息:

        public string SendMessage(string queueName, string msg)
        {
            string result = null;
            try
            {
                var connection = MQHelper.CreateMQConnectionInPoolNew();

                result = MQHelper.SendMsg(connection, queueName, msg);
            }
            catch (Exception ex)
            {
                BaseUtil.Logger.Error(string.Format("MQHelper.SendMessage Error:{0}", ex.Message), ex);
                result = ex.Message;
            }

            return result;
        }

 獲取消息隊列消息數:

        public int GetMessageCount(string queueName)
        {
            int result = -1;
            try
            {
                var connection = MQHelper.CreateMQConnectionInPoolNew();

                result = MQHelper.GetMessageCount(connection, queueName);
            }
            catch (Exception ex)
            {
                BaseUtil.Logger.Error(string.Format("MQHelper.GetMessageCount Error:{0}", ex.Message), ex);
                result = -1;
            }

            return result;
        }

 這裡說一下:BaseUtil.Logger 是Log4Net的實例對象,另外上面沒有針對持續訂閱消費消息(ConsumeMsg)作說明,因為這個其實可以不用連接池也不會有問題,因為它是一個持久訂閱並持久消費的過程,不會出現頻繁創建連接對象的情況。

 最後要說的是,雖說代碼貼出來,大家一看就覺得很簡單,好像沒有什麼技術含量,但如果沒有完整的思路也還是需要花費一些時間和精力的,代碼中核心是如何簡單高效的解決併發及連接復用的的問題,該MQHelper有經過壓力測試並順利在我司項目中使用,完美解決了之前的問題,由於這個方案是我在公司通宵實現的,可能有一些方面的不足,大家可以相互交流或完善後入到自己的項目中。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 配置指令 管理會話存儲介質 確定如何存儲會話信息 session. save_handler = files(平面文件)mm(共用記憶體)sqlite(SQLite資料庫)user(自定義) 設置會話文件路徑 如果session.save_handler = files,則session.save_p ...
  • 在做這個SDRAM控制器之前,博主有一個疑問,對於學生來說,是否有必要學慣用純Verilog寫一個SDRAM控制器?因為目前X家和A家都有了DDR IP Core,對於要實現一個應用可以直接調用IP Core,只需要對其介面操作即可。對於開發者來說,與其費時費力用Verilog去寫一個性能差而且老的 ...
  • 轉載請註明出處:http://www.cnblogs.com/qm-article/p/8903893.html 一、介紹 在介紹該源碼之前,先來瞭解一下鏈表,接觸過數據結構的都知道,有種結構叫鏈表,當然鏈表也分多種,如常見的單鏈表、雙鏈表等,單鏈表結構如下圖所示(圖來自百度) 有一個頭結點指著下一 ...
  • 內容:流的分類,文件寫入(位元組輸出流),異常處理,獲取一個文件夾下的特定文件集合 位元組流的抽象基類:InputStream,OutputStream字元流的抽象基類:Reader,Writer由這四個類派生出來的子類名稱都是以父類名作為子類名的尾碼。如:InputStream的子類FileInput ...
  • 第一個 Python 程式 目標 第一個 程式 與 版本簡介 執行 程式的三種方式 解釋器 —— / 互動式 —— 集成開發環境 —— 01. 第一個 程式 1.1 Python 源程式的基本概念 1. Python 源程式就是 一個特殊格式的文本文件 ,可以 使用任意文本編輯軟體 做 的開發 2. ...
  • C++多態 Polymorphism 本文為C++官網對多態闡述:http://www.cplusplus.com/doc/tutorial/polymorphism/ 在瞭解多態前需具備如下知識:類、結構體、友元和繼承。 指向基類的指針 先來看一個基礎例子 1 // pointers to bas ...
  • 作者: "zyl910" 一、緣由 NLog是一個很好用的日誌類庫。利用它,可以很方便的將日誌輸出到 調試器、文件 等目標,還支持輸出到窗體界面中的RichTextBox等目標。 而且它還支持在運行時修改配置,例如可用於實現這樣的需求——在界面上做個下拉框,可動態調整RichTextBox的日誌級別 ...
  • 問題描述:由於最近項目需要使用Mac地址與註冊碼進行加密處理,但是又因為Web程式的局限性不能獲取客戶端電腦系統信息,當然IE瀏覽器有一個activex控制項他是可以通過Js在前端代碼中直接獲取的,局限性太小放棄。我的實現方法是通過windows服務嵌套一個HttpService服務實現。本人初級菜鳥 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...