深入理解Redis(一)——高級鍵管理與數據結構


引語

這個章節主要講解了三部分內容:

  • 如何設計並管理Redis的鍵以及與其關聯的數據結構;
  • 了解並使用Redis客戶端對象映射器;
  • 介紹如何利用大O標記來評估Redis性能。

鍵與數據結構

我們先來看書中的一段原話:

運行32位還是64位版本的Redis將決定Redis鍵大小的實際限制。對於32位的版本來說,任何長於32的鍵名需要更多的字節空間,因此增加了Redis的內存使用。使用64位版本的Redis允許更長的鍵長度,但是對於短小的鍵來說,也會分配完整的64位空間,從而導致額外的空間浪費。

Redis本身對於鍵的命名本身並沒有做過多的限制,但在實際的應用場景中,我們的Redis服務器不大可能給單一業務來使用,如果沒有相關規范,開發A使用了名稱“RedisKey_1”,開發B也使用了名稱“RedisKey_1”,但他們並沒有去做溝通,那么業務就很容易出現問題;又或者去使用A、B、C、D、1、2、3、4這種無意義的Key,即不利於閱讀,也很容易沖突。書的原文中通過大量篇幅列舉了如何去給Key命名,總結下來有三點:

  1. 不推薦太長的鍵,這樣做不僅會大量消耗內存,也會提高查找的計算成本,也會讓其他開發者感到困惑;
  2. 不推薦太短的鍵,例如使用“u_1”這種形式的鍵,雖然很短,但實在是得不償失,不僅僅讓開發者感到困惑還提高了鍵沖突的可能;
  3. 官方推薦統一的命名,如:lib:book:,這個鍵表示存儲了圖書館下所有圖書的名稱、原文中推薦是用“:”和“.”來進行分割,個人覺得可以使用類似[項目簡稱].[模塊名].[內容名]的形式來進行存儲;

雖然官方給出了規范,但是在實際的使用過程中還是計只能依賴於團隊規范,良好的規范可以提高Redis的性能和可維護性,對於key的操作,大家可以去命令手冊中查看相關操作命令,這里列出幾個常用的:

  • exists key:通常我們可以使用這個命令來判斷某個鍵是否存在,時間復雜度O(1);
  • keys pattern:如果我們不記得某個key的全拼時,可以使用這個命令列出符合pattern的key,如 keys lib:*就可以列出lib:,時間復雜度為O(N),所以這個命令不到萬不得已不推薦使用,它會造成Redis長時間堵塞,甚至會導致Redis內存耗盡,推薦使用scan;
  • type key:有時候我們想知道查找的key存儲的數據是什么類型,以便使用合適的命令查詢其中的值,這時候type命令就可以排上用場,他可以返回key所存儲的數據類型,如string、list、set、zset、hash等,當然若是key不存在,則會返回none;
  • expire key seconds:為key設置過期時間,當key過期后,redis會自動刪除過期鍵。

大O標記

在Redis文檔中,每個Redis命令的時間復雜度都由大O示例給出

不論你是用命令手冊還是使用官方文檔去查看命令,會發現后面均給出了時間復雜度,我們可以據此大致評估出我們算法效率,這里我們順便回顧一下時間復雜度的概念,老司機請直接跳過看數據結構的內容即可。大O標記用於描述函數漸進行為的數學符號,在計算機領域中,被用來分析算法的時間或空間復雜度,例如:UpdateStr的時間復雜度為O(n),就表示隨着輸入的增長,處理時間會隨着n的變化而線性變化:

        public void UpdateStr(List<string> oldVals,string newVal)
        {
            for (int i = 0; i < oldVals.Count; i++)
            {
                oldVals[i] = newVal;
            }
        }

如果你處理1000條數據的時間為100ms,那么你處理10000條數據的時間理論上就為1000ms,但是你不能認為所有復雜度同為O(n)的數據庫插入操作也是1000ms,但這只是評估值,並不代表實際的處理結果。常見的復雜度有: O(1):表示該算法的時間復雜度為常量,不會隨輸入數據集的大小變化而變化,如:

        public void UpdateStr(List<string> oldVals, List<string> newVals)
        {
            oldVals = newVals;
        }

O(n):表示該算法的時間復雜度會隨着輸入數據的大小變化而變化,如我們前面舉的例子; O(n^2):表示算法會隨着數據數據的增長出現二次增長,如:

        public void UpdateStr(List<string> oldVals, string newVal)
        {
            for (int i = 0; i < oldVals.Count; i++)
            {
                for (int j = 0; j < oldVals.Count; i++)
                {
                    oldVals[i] = newVal;
                }               
            }
        }

O(logN):對數級的復雜度算法效率也算比較高的,常見如二分查找、歐幾里得算法、冪運算都算是O(logN),這里給出二分查找的例子:

        public int BinSearch(int[] sortedArr, int low, int high, int hasVal)
        {
            int mid = (low + high) / 2;
            if (low > high)
                return -1;

            if (sortedArr[mid] == hasVal)
                return mid;
            else if (sortedArr[mid] > hasVal)
                return BinSearch(sortedArr, low, mid - 1, hasVal);
            else
                return BinSearch(sortedArr, mid + 1, high, hasVal);
        }

一般來講,復雜度的C(常數)<logN(對數)<log2N(對數平方根)<N(線性級)<NlogN<N2(平方級)<N3(立方級)<2N(指數級),了解了大O的概念后,就可以對我們的操作進行評估了,也知道為什么keys這個命令在大數據量的情況下最好不要慎重使用了。

數據結構

其實我之前也詳細介紹過Redis的數據結構,這里我們重新回顧一下,命令的使用還是推薦大家使用命令手冊。最新的Redis在原來五種數據結構之上又增加了HyperLogLog結構,接下來我們逐一介紹:

  • String(字符串):這個是Redis中最基本的數據結構和其他鍵-值存儲如Memechached類似,常用的Get和Set操作的時間復雜度均為O(1),我們可以利用他來實現網站訪問量的統計、利用bitmap實現用戶上線次數統計、限速器、共享Session、分布式鎖等功能,參見String手冊
  • Hash(哈希):哈希應該是我們在使用Redis過程中最常使用的結構之一了,我們可以使用Hash來存儲用戶是否被禁言,在官方的memory-optimization一文也推薦使用Hash來作為常用存儲,因為它非常節省內存,參見Hash手冊
  • List(列表):是字符串的有序集合,它允許使用重復的字符串值,因為列表的特性,他經常被用來做安全隊列,用於不同程序之間進行信息交換。進程A通過LPUSH將消息放入隊列中,進程B通過RPOP取出消息,若是考慮安全性也可以使用RPOPLPUSH命令來防止數據丟失,在處理完成后,再使用LTRIM刪除即可,參見List手冊
  • Set(集合):Redis中的集合保證了字符串值的唯一性,但是並不保證這些值的順序,Redis也實現了集合中的並集(sunion)、交集(sinter)和差級(sdiff),參見Set手冊
  • SortedSet(有序集合):有序集合,兼具了Redis列表和集合的特性,有序集合中的值都是為唯一且有序的,我們可以利用他的特性來實現諸如游戲排名功能,參見SortedSet手冊
  • HyperLogLog(基數統計)):這是Redis2.8.9版本添加的一個概率數據結構,它的優點是在輸入元素的數量或體積非常大時,計算基數所需要的空間總是固定的並且很小,但是他只是一個估計基數,存在一定誤差,而且無法獲取具體的元素值,因此在對准確性要求不是很高的場景中很有用,如QQ同時在線人數,網站IP訪問數等HyperLogLog手冊
  • GEO(地理位置):用戶存儲指定空間的經緯度,這里不做展開,有興趣的參見GEOADD手冊

對象映射器

文章中用了Nodejs來舉例,奈何本人前端是個戰五渣,大致看了一下,大概類似於數據的ORM之類的東西,.NET的兩個主流客戶端沒發現類似的功能,這里就不做深入了,目前網上最流行的版本Redis驅動有兩個ServiceStack.Redis和StackExchange.Redis兩個版本,現在分別給出兩個版本操作的示例代碼:

ServiceStack.Redis

這里值得一提的是ServiceStack.Redis已經開始轉向商用,若想正常使用需要購買License或者使用低版本,謹慎使用:

    /// <summary>
    /// ServiceStack.Redis操作示例。
    /// </summary>
    public class RedisHelper
    {
        private static Dictionary<string, PooledRedisClientManager> ClientPool = new Dictionary<string, PooledRedisClientManager>();

        private static object AddLock = new object();

        /// <summary>
        /// 構建IRedisClient對象,可以直接通過IRedisClient實現主要的數據操作。
        /// </summary>
        /// <remarks>
        /// 普通地址:127.0.0.1:6379
        /// 帶密碼地址:password@127.0.0.1:6379
        /// </remarks>
        public static IRedisClient GetRedisClient(string address, string key = "")
        {
            if (string.IsNullOrEmpty(key))
                key = address;
            if (!ClientPool.ContainsKey(key))
            {
                lock (AddLock)
                {
                    ClientPool[key] = new PooledRedisClientManager(new string[] { address });
                }
            }
            return ClientPool[key].GetClient();
        }


        /// <summary>
        /// 這里特別針對管道進行演示,用於對批量操作進行優化。
        /// </summary>
        public static void BatchAdd()
        {
            Dictionary<string, string> maps = new Dictionary<string, string>();

            var client = GetRedisClient("", "");

            //創建管道
            var pipeline = client.CreatePipeline();
            foreach (var item in maps)
            {
                pipeline.QueueCommand(p => p.SetEntry(item.Key, item.Value));
            }
            //提交
            pipeline.Flush();
        }
    }

StackExchange.Redis

這個客戶端也被大家廣泛使用,開源免費,可放心使用。

    /// <summary>
    /// StackExchange.Redis操作示例。
    /// </summary>
    public class RedisHelper
    {
        private static object _lock = new object();
        private static string _rConnStr = "127.0.0.1:6379";
        private static Lazy<ConnectionMultiplexer> _rLazyConn;

        /// <summary>
        /// 構建IDatabase,效果等同於IRedisClient,可以通過IDatabase進行Redis常規操作。
        /// </summary>
        /// <remarks>
        /// 普通地址:127.0.0.1:6379
        /// 帶密碼地址:127.0.0.1:6379,password=123456
        /// </remarks>
        public static IDatabase GetDb(int rDb = 0)
        {
            if (_rLazyConn == null)
            {
                lock (_lock)
                {
                    if (_rLazyConn == null)
                    {
                        if (_rConnStr == null)
                            throw new ArgumentException("缺少ConnStr的初始化配置。");

                        _rLazyConn = new Lazy<ConnectionMultiplexer>(() => { return ConnectionMultiplexer.Connect(_rConnStr); });
                    }
                }
            }
            return _rLazyConn.Value.GetDatabase(rDb);
        }

        /// <summary>
        /// 批量操作的寫法,效果等同於Pipeline。
        /// </summary>
        public static void BatchAdd()
        {
            Dictionary<string, string> maps = new Dictionary<string, string>();
            var batch = GetDb().CreateBatch();
            var tasks = new List<Task>();
            foreach (var item in maps)
            {
                tasks.Add(batch.StringSetAsync(item.Key, item.Value));
            }
            batch.Execute();
            Task.WaitAll(tasks.ToArray());
        }
    }

實戰案例

光說不練假把式,書中舉例用的是圖書館的案例,業務結構復雜,這里我們舉一個網上比較常見的案例,用過QQ的同志想必都看過那個同時在線人數這個功能,這里我們來看如何使用Redis來實現此功能。

方案一:String

當用戶登錄時,我們使用命令“incr QQ:Online:Count”來增加在線用戶數,當用戶注銷時,我們使用命令“decr QQ:Online:Count”,需要統計時,就可以直接通過“get QQ:Online:Count”獲取到現在用戶數了,這樣就極為簡單的實現了現在用戶數的統計。

方案二:Set

雖然通過String數據結構能夠極為簡便的實現我們的目標,但是產品的心就像女人的心一樣難以捉摸,需求發生變化,用戶可以多登陸,在線統計需要排重,方案一已經無法滿足產品的胃口了,只能采用方案二了,前面介紹過Set可以保證存儲數據的唯一性,那么用Set來做用戶的在線記錄比較理想,使用命令“sadd QQ:Online:User 000001”來記錄在線用戶,當用戶注銷時,我們使用命令“srem QQ:Online:User 000001”來移除用戶,通過“scard QQ:Online:User”來獲取用戶在線數,似乎已經完美達到了要求。

方案三:SoretedSet

雖然Set可以完美解決需求,但是sadd和srem命令的時間復雜度為O(N),當數據量比較小時,速度還是很快的,但是當數據量變的越來越大時,性能消耗也就也來越大。那么有沒有繼續提升的空間呢,答案是肯定的,可以用有序集合來解決,使用命令“zadd QQ:Online:User 18072431 000001”來記錄在線用戶,最后一位記錄的是登錄時間,當用戶注銷時,我們使用命令“zrem QQ:Online:User 000001”來移除用戶,通過“zcard QQ:Online:User”來獲取用戶在線數,由於集合是有序的,所以zadd和zrem的時間復雜度均為O(LogN)。

方案四:HyperLogLog

方案二和方案四雖然能夠滿足需求,但是如果只是但存的統計在線用戶數的話,這兩個方案比較占用內存,如果對於在線用戶數的要求不是十分精確的話,使用HyperLogLog似乎也是個不錯的選擇,使用“pfadd QQ:Online:User 000001”使用“pfcount QQ:Online:User”來統計,不過沒有發現HyperLogLog的移除功能,所以一般情況下只能統計當日登錄的用戶數。

雖然一般我們不需要去記憶數據結構都有哪些操作,在使用的時候大可以去查命令手冊,但還是建議大家把命令手冊通讀一遍,對於你理解Redis會有很大的幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM