一、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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...