一、Redis基本操作——String(原理篇)

来源:http://www.cnblogs.com/idiotgroup/archive/2016/05/01/5450157.html
-Advertisement-
Play Games

小喵的嘮叨話:最近京東圖書大減價,小喵手癢了就買了本《Redis設計與實現》[1]來看看。這裡權當小喵看書的筆記啦。這一系列的模式,主要是先介紹Redis的實現原理(可能很大一部分會直接照搬原作者的描述),加上小喵自己的想法,之後配合Redis官網上的各種相關的操作命令(原書上貌似沒有很多的介紹命令 ...


小喵的嘮叨話:最近京東圖書大減價,小喵手癢了就買了本《Redis設計與實現》[1]來看看。這裡權當小喵看書的筆記啦。這一系列的模式,主要是先介紹Redis的實現原理(可能很大一部分會直接照搬原作者的描述),加上小喵自己的想法,之後配合Redis官網上的各種相關的操作命令(原書上貌似沒有很多的介紹命令)。

 

小喵的個人博客地址: http://miaoerduo.com, 隨時歡迎各位的大駕。

 

本章介紹Redis中最常用到的字元串(String)。

Redis的字元串(String)的實現

小喵之前有看到過《Redis設計與實現》的一部分章節。這是第一章的內容,小喵也是因為看了這一章的內容,才決定要買本仔細研究的。

首先,我們知道Redis是由C語言編寫的,以高效和輕量著稱。而C語言中的字元串是怎麼實現的呢?字元數組。

比如一個簡單的字元串”hello world”,其實是一個如下的字元的數組:

[‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘\0’]

最後的一個’\0’是空字元,表示字元串的結尾。

 

Redis由於各種原因,並沒有直接使用了C語言的字元串結構,而是對其做了一些封裝,得到了自己的簡單動態字元串(simple dynamic string, SDS)的抽象類型。Redis中,預設以SDS作為自己的字元串表示。只有在一些字元串不可能出現變化的地方使用C字元串。

 

SDS的定義如下:

 
1 2 3 4 5 6 7 8 9 struct sdshdr {     // 用於記錄buf數組中使用的位元組的數目     // 和SDS存儲的字元串的長度相等     int len;     // 用於記錄buf數組中沒有使用的位元組的數目     int free;     // 位元組數組,用於儲存字元串     char buf[]; };

可以看出來,SDS的結構並不複雜。

buf是一塊可用的記憶體空間,通常大小會大於等於需要存儲的字元串的大小(大於?為什麼要大於呢?讀者可以思考一下)。

len表示字元串的長度,也表示buf中已經被使用的空間的大小。

free表示buf中沒有被使用的空間的大小。

要註意的是,buf的大小等於len+free+1,其中多餘的1個位元組是用來存儲’\0’的。

 

那麼這麼封裝到底有什麼好處呢?我們一點一點剖析。

1,常數複雜度獲取字元串長度

在C語言中的字元串只是簡單的字元的數組,當使用strlen獲取字元串長度的時候,C語言內部其實是直接順序遍曆數組的內容,找到對應的’\0’對應的字元,從而計算出字元串的長度。顯然這個演算法複雜度和字元串的長度成正比,即O(N)。而對於SDS來說,只需要訪問SDS的len屬性就能得到字元串的長度,複雜度為O(1)。這樣,獲取字元串長度的操作就不會成為Redis的瓶頸(當然len的作用不止這麼簡單,後面還會介紹別的)。

2,杜絕緩衝區溢出

我們知道C++裡面的字元串使用了STL的string類型,我們開發者不太需要關註記憶體的分配和釋放的過程。但是Redis是C語言編寫的,並沒有這麼方便的數據類型。對於字元串的拼接、複製等操作,C語言開發者必須確保目標字元串的空間足夠大,不然就會出現溢出的情況。

 
1 2 3 char a[10] = "hello"; strcat(a, " world"); strcpy(a, "hello world");

上面的三句代碼,就是C語言的字元串拼接和複製的使用,但是明顯出現了緩衝區溢出的問題。字元數組a的長度是10,而”hello world”字元串的長度為11,則需要12個位元組的空間來存儲(不要忘記了’\0’)。

然後,我們看看Redis的SDS是怎麼處理字元串修改的這種情況。

當使用SDS的API對字元串進行修改的時候,API內部第一步會檢測字元串的大小是否滿足。如果空間已經滿足要求,那麼就像C語言一樣操作即可。如果不滿足,則拓展buf的空間,使得滿足操作的需求,之後再進行操作。每次操作之後,len和free的值會做相應的修改。

這就是SDS的全部的高明之處了嗎?當然不!

當API發現SDS的buf的容量不夠的時候,並不是簡單申請正好適合的大小,而是額外申請了一倍的空間!我們以sds的API sdscat函數為例,該函數實現了sds的拼接的功能。

下麵的例子是”hello” 和” world”的拼接的過程。

修改之前的sds

圖1 sdscat執行之前的sds

修改之後的sds

圖2 sdscat執行之後的sds

這裡的buf的容量是23(free + len + 1)。為什麼要這麼做呢?耐心向下看吧。

3,減少修改字元串時帶來的記憶體重分配次數

我們之前說到,對於一個N長的字元串,C語言中底層是一個N+1長的字元數組(有一個位元組存放空字元)。C字元串的長度和底層數組之間的長度存在著這樣的關係,因此當進行字元串的操作而導致字元串長度發生變化的時候,需要對記憶體進行重新分配。

如果操作會增長字元串,那麼在執行之前,就需要進行記憶體分配擴充底層數組的大小。如果是縮短字元串的操作,則需要釋放額外的記憶體(這是書中的意思,但小喵覺得如果字元串縮小的話,其實並不用立刻釋放記憶體,如果字元串是malloc出來的話,需要釋放的直接free就可以,也不需要給定空間的大小,所以不會出現記憶體泄露。當然,也可能Redis裡面是用別的方式實現,這樣小喵就不懂了)。

對於一般的程式而言,如果修改字元串的操作並不是特別常出現,那麼每次修改都重新分配一下記憶體也是可以接受的。但是Redis作為一個資料庫,其讀寫速度,數據修改頻率都被要求達到很高的效率。因此這種低效的方式並不適合Redis。

為了避免C字元串的這些弊端,SDS通過未使用空間解除了字元串長度和底層數組長度之間的關係。也就是之前說的buf的長度為len和free之和(再加1)。數字裡面可以包含未使用的空間,大小用free表示。

Redis主要通過以下兩種策略來處理記憶體問題。

i) 空間預分配

這種方式用於處理字元串長度增加的問題。如果對字元串的修改使得字元串的長度增加,API首先會判斷buf的空間大小是否滿足,如果滿足則直接操作,如果不滿足,則進行如下操作:

如果對SDS進行修改之後的,SDS的長度(即len的值)小於1MB。程式將額外分配和len一樣大小的未使用空間。以上面的”hello” + ” world”的操作為例。在這個例子中”hello”的len是5(不考慮’\0′),修改之後的字元串”hello world”長度為11,那麼新的SDS的buf的容量就是11*2+1。其中len和free都是11,多餘的1位元組用來存儲’\0’。

如果對SDS修改之後的長度大於1MB,那麼程式會分配1MB的未使用空間。比如原數據是5MB,修改之後需要6MB的空間,進行修改的操作後,buf的實際空間應該是7MB,其中len為6MB,free為1MB。

那麼這些未使用空間能夠做什麼呢?為什麼根據SDS的修改會的大小會有兩種不同的分配原則呢?

小喵是這麼認為的,如果數據被更改,則說明這個數據很可能會被再次更改,如果能夠提前分配多餘的空間,那麼下一次變化的時候很可能就不需要再次分配空間了。如果數據比較小(<1MB)的時候,可以分配等大的未使用空間。但是如果數據已經很大的時候(>1MB),再分配同等大小的記憶體會顯得十分浪費,畢竟不能確保這個字元串一定會被再次修改,所以只額外分配1MB的空間。

通過這種策略,SDS可以做到N次修改,最多進行N次記憶體分配。而C字元串在N次修改則一定要進行N次記憶體分配。一個是至多N次,一個是一定N次。用小喵的腦袋想,也覺得SDS這個策略簡單、粗暴、高效。

ii) 惰性空間釋放

當執行字元串長度縮短的操作的時候,SDS並不直接重新分配多出來的位元組,而是修改len和free的值(len相應減小,free相應增大,buf的空間大小不變化)。通過惰性空間釋放,可以很好的避免縮短字元串需要的記憶體重分配的情況。而且多餘的空間也可以為將來可能有的字元串增長的操作做優化。

當然,SDS也提供直接釋放未使用空間的API,在需要的時候,也能真正的釋放掉多餘的空間。

4,二進位安全

C字元串中的字元必須符合某種編碼(比如ASCII),並且字元串除了末尾之外不能出現空字元,否則會被程式認為是字元串的結尾。這就使得C字元串只能存儲文本數據,而不能保存圖像,音頻等二進位數據。(這裡,小喵的觀點是不同的,小喵本人是做圖像的,opencv等的庫,都是使用unsigned char*來存儲圖像的數據。我們完全可以把字元數組看成一堆記憶體,存放任何數據都可以)

使用SDS就不需要依賴控制符,而是用len來指定存儲數據的大小。同時所有的SDS操作的API都是二進位安全的(binary-safe),所有的SDS API都會以處理二進位的方式來處理SDS的buf的數據。程式不會對buf的數據做任何限制、過濾或假設,數據寫入的時候是什麼,讀取的時候依然不變。

這也是我們將SDS的buf屬性程式位元組數組的原因,Redis不是使用這個數組來保存字元,而是儲存一系列二進位數據。

5,相容部分C字元串函數

由於SDS的buf的定義和C字元串完全相同,因此很多的C字元串的操作都是適用於SDS->buf的。比如當buf裡面存的是文本字元串的時候,printf函數,也完全可以試用。這樣,Redis就不需要為所有的字元串的處理編寫自己的函數,大多數通過調用C語言的函數就可以。

總結

 

C字元串SDS
獲取字元串長度的複雜度為O(N) 獲取字元串長度的複雜度為O(1)
API是不安全的,可能會造成緩衝區溢出 API是安全的,不會造成緩衝區溢出
修改字元串長度N次必然需要執行N次記憶體重分配 修改字元串長度N次最多需要執行N次記憶體重分配
只能保存文本數據 可以保存文本或者二進位數據
可以使用所有庫中的函數 可以使用一部分庫的函數

以上則是Redis的string結構的原理部分。下一章我們會介紹一些string操作的redis命令。

轉載請註明出處。

參考:

[1]http://redisbook.com/


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

-Advertisement-
Play Games
更多相關文章
  • 1. 常量和變數 常量 和 變數 把一個名字(比如 'number' 或者 'welcomeMessage')和一個指定類型的值(比如數字'10'或者字元串 ' "Hello" ' )關聯起來。常量的值一旦設定就不能改變,而變數的值可以隨意更改。 1> 聲明變數和常量 常量 和 變數 必須在使用前聲 ...
  • Cell屬於UITableView中的組件,有多種定義方式,有系統自帶的方法,有自定義的方法。 可以使用系統的方法setSeparatorColor(設置分割線顏色) 設置setSeparatorStyle(設置分割線類型) 也可以自己自定義一個Cell 在Cell的下麵添加一個極細的UIView, ...
  • 獲取WebView對象 調用WebView對象的getSettings()方法,獲取WebSettings對象 調用WebSettings對象的setJavaScriptEnabled()方法,設置js可用,參數:布爾值 在判斷是否支持js的時候,不要用alert(),預設不起作用,可以先用docu ...
  • 在佈局文件中添加<EditText/>和<Button/>控制項, 在佈局文件中添加<WebView/>控制項 在Activity中獲取WebView對象 調用WebView對象的loadUrl()方法,參數:String路徑 添加訪問網路的許可權android.permission.INTERNET 調 ...
  • 在Activity從創建到銷毀的過程中需要在不同的階段調用7個生命周期的方法這7個生命周期方法定義如下: 上面7個生命周期方法分別在4個階段按一定的順序進行調用 1,開始Activity:在這個階段依次執行3個生命周期方法,分別是onCreate,onStart,onResume 2,Activit ...
  • 我們先用AndroidStudio新建一個項目,選擇空白模板,然後像其中拖入兩個Button,將他們的id分別命名為btDate(顯示日期),btTime(顯示時間),他的模板XML代碼很簡單 如圖所示 一個標準的Android應用程式視窗類需要繼承android.app.Activity類,至少實 ...
  • 監聽EditText的文本變化需要給EditText控制項加一個addTextChangeListener監聽器 editText.addTextChangeListener(textWatcher); 這裡的textWatcher是一個TextWatcher對象, TextWatcher是一個介面, ...
  • 項目:蒙文詞語檢索 日期:2016-05-01 提示:The constructor User.Student(String, String, String) is not visible 出處:Dbdao.insert(new Student("Achilles", "Male", "14")); ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...