1、數據結構
string
Redis字符串是可修改字符串,在內存中以字節數組形式存在。java
下面是string在源碼中的定義,SDS(Simple Dynamic String)python
struct SDS<T> { T capacity; // 數組容量 T len; // 數組長度 byte flags; // 特殊標識位,不理睬它 byte[] content; // 數組內容 }
Redis規定字符串的長度不超過512M。golang
Redis字符串的兩種存儲方式:redis
- 長度特別短,使用emb形式存儲
- 長度超過44,使用raw形式存儲
擴容策略shell
字符串長度小於1M以前,擴容採用加倍策略,保留100%的冗餘空間。(從源碼中能夠看到是現有的數組長度len+設定的容量大小capacity) 字符串長度超過1M後,每次擴容只多分配1M大小的冗餘空間。數組
sds sdscatlen(sds s, const void *t, size_t len) { size_t curlen = sdslen(s); // 原字符串長度 // 按需調整空間,若是 capacity 不夠容納追加的內容,就會從新分配字節數組並複製原字符串的內容到新數組中 s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; // 內存不足 memcpy(s+curlen, t, len); // 追加目標字符串的內容到字節數組中 sdssetlen(s, curlen+len); // 設置追加後的長度值 s[curlen+len] = '\0'; // 讓字符串以\0 結尾,便於調試打印,還能夠直接使用 glibc 的字符串函數進行操做 return s; }
list
Redis的列表經常使用來作異步隊列使用。服務器
右邊進,左邊出是隊列。網絡
rpush books python java golang lpop books
右邊進,右邊出是棧。數據結構
rpush books python java golang rpop books
hash
Redis 的字典至關於 Java 語言裏面的 HashMap,它是無序字典。併發
hset books java "think in java" hgetall books
Redis相比於Java的HashMap不一樣的是,採起了漸進式rehash,爲的是不堵塞服務。
什麼是漸進式rehash?
建立新的hashtable,同時保留舊的hashtable,而且經過定時任務將舊的hashtable中的key-value轉移到新的hashtable,查詢時,會同時查詢新舊hashtable,等到徹底轉移完成,再將舊的刪除掉。
這裏思考一個問題?Java中的rehash,要怎麼處理?不是漸進式的,那如何保證在rehash時進行查詢,獲取到正確的值?
set
Redis 的集合至關於 Java 語言裏面的 HashSet,它內部的鍵值對是無序的惟一的。它的內部實現至關於一個特殊的字典,字典中全部的 value 都是一個值NULL。
set 結構能夠用來存儲活動中獎的用戶 ID,由於有去重功能,能夠保證同一個用戶不會中獎兩次
zset
底層實現?
zset相似於 Java 的 SortedSet 和 HashMap 的結合體,一方面它是一個 set,保證了內部 value 的惟一性,另外一方面它能夠給每一個 value 賦予一個 score,表明這個 value 的排序權重。它的內部實現用的是一種叫作「跳躍列表」的數據結構。
一個操做須要進行讀變量,寫變量兩個步驟,多個相同的操做同時進行就會出現併發問題。由於讀取和寫入兩個變量不是原子操做。
2、分佈式鎖
分佈式鎖本質上要實現的目標就是在 Redis 裏面佔一個「茅坑」,當別的進程也要來佔時,發現已經有人蹲在那裏了,就只好放棄或者稍後再試。
佔坑通常是使用 setnx(set if not exists) 指令,只容許被一個客戶端佔坑。先來先佔, 用完了,再調用 del 指令釋放茅坑。
加鎖
setnx lock:a true
釋放鎖
del lock:a
爲了不加鎖後,中間操做出現異常,最後沒有釋放鎖的問題,須要給鎖設置一個超時時間。
setnx lock:a true expire lock:a 5
出現另外一個問題,上面的操做也有可能失敗,例如設置過時時間失敗。
爲了解決上述的問題,Redis2.8增長了set指令的擴展參數,
set lock:a true ex 5 nx
要確保分佈式鎖可用,需至少要知足下面四個條件:
- 互斥性:只能一個客戶端持有鎖
- 不會發生死鎖
- 具備容錯性
- 解鈴還須繫鈴人
超時問題
若是執行代碼的時間太長,超出了鎖的超時限制,就會由其餘線程得到鎖。會致使代碼不能嚴格被執行。爲了不這個狀況,進行加鎖的執行代碼儘可能不要選擇執行時間過長的。
可重入性
可重入性指的是線程在持有鎖的狀況下,再次請求加鎖。
問題,爲什麼要設計可重入這一個特性?
搞清楚幾個概念:可重入鎖、公平鎖、非公平鎖。
可重入鎖: 線程1執行 synchronized A()方法,A()方法調用synchronized B()方法,當線程1獲取到A方法對應的對象鎖以後,再去調用B方法,就不須要從新申請鎖。
公平鎖: 多個線程等待鎖,當鎖釋放後,等待該鎖時間最久的(或者說最早申請鎖的),應該得到鎖。
非公平鎖: 多個線程等待鎖,不按照等待鎖的時間或申請鎖的前後順序,來得到鎖。
3、持久化
- 爲何要持久化
- 如何持久化
爲何要持久化?
由於Redis數據存在內存,若服務器宕機或重啓,數據會所有丟失,須要有一種機制保證數據不會由於故障丟失。
Redis是單線程的,而持久化就是說Redis須要將線程用到保存數據到磁盤,而且還要服務客戶端的請求,持久化的IO會嚴重影響性能。
那麼Redis是如何解決的?
這裏Redis使用了操做系統的 寫時複製(Copy On Write)。也就是從原先處理客戶端請求的進程中,fork出一個子進程,來進行持久化。
如何持久化?
- 快照
- AOF日誌
Copy On Write
(1)fork()函數
父進程執行fork()後,會產生一個子進程。當fork()被調用的時候,會返回兩個值。
爲何返回兩個值? 由於是兩個線程,返回給父線程,子線程的ID;返回給子線程,0。
(2)exec()函數
exec的做用是,替換當前進程的內存空間的映像,從而執行不一樣的任務。
也就是說,當子進程執行exec後,就再也不是父進程的副本了,由於有了獨立的內存空間。
4、管道
管道的本質是,將客戶端與服務端的交互,由寫—— 讀 —— 寫 —— 讀,變爲 寫—— 寫—— 讀——讀,是由客戶端提供的技術。
兩個連續的寫操做和兩個連續的讀操做總共只會花費一次網絡來回,就比如連續的 write 操做合併了,連續的 read 操做也合併了同樣。
5、位圖
使用bit來存儲數據,例如一週的簽到,可使用 7個位來表示,簽到了的用1標記,未簽到的用0標記。
設置與獲取
設置
127.0.0.1:6379> setbit week 1 1 (integer) 0 127.0.0.1:6379> setbit week 2 0 (integer) 0 127.0.0.1:6379> setbit week 3 0 (integer) 0 127.0.0.1:6379> setbit week 4 0 (integer) 0
獲取
127.0.0.1:6379> getbit week 4 (integer) 0
統計
bitcount name start end,其中的start和end的單位是byte,也就是8個bit。
例如bitcount s 0 0
統計的是第一個字符,也就是第一個8位的存在的1的個數
127.0.0.1:6379> bitcount week 0 0 (integer) 1
6、通訊協議與簡單Jedis-Client的實現
RESP協議概述
Redis客戶端和Redis服務端使用RESP協議進行通訊。
這個協議的特色:
- Simple to implement.實現簡單
- Fast to parse.快速解析
- Human readable.可讀性強
當我寫一個socket程序,去攔截Jedis客戶端發送的請求,程序以下:
public class JedisSocket { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(6379); Socket socket = serverSocket.accept(); byte[] bytes = new byte[2048]; socket.getInputStream().read(bytes); System.out.println(new String(bytes)); } catch (IOException e) { e.printStackTrace(); } } }
獲得以下結果:
*3 $3 SET $4 name $3 fon
$後面表示的是字符串長度。set的長度是3,name是4,fon是3,*號後面的數字,表明共有3組數據。這就是RESP的內容。
Jedis的使用
引入依賴
<!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.0.1</version> </dependency>
一個簡單的Jedis程序
public class JedisTest { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); jedis.set("name","coffejoy"); String str = jedis.get("name"); System.out.println(str); //關閉鏈接 jedis.close(); } }
使用Jedis鏈接池
interface CallWithJedis { void call(Jedis jedis); } class RedisPool { private JedisPool pool; public RedisPool() { this.pool = new JedisPool("bei1", 6379); } public void execute(CallWithJedis caller) { Jedis jedis = pool.getResource(); try { caller.call(jedis); } catch (JedisConnectionException e) { caller.call(jedis); // 重試一次 } finally { jedis.close(); } } } class Holder<T> { private T value; public Holder() { } public Holder(T value) { this.value = value; } public void value(T value) { this.value = value; } public T value() { return value; } } public class JedisPoolTest { public static void main(String[] args) { RedisPool redis = new RedisPool(); Holder<Long> countHolder = new Holder<>(); redis.execute(jedis -> { String name = jedis.get("name"); System.out.println(name); }); System.out.println(countHolder.value()); } }
實現一個Jedis客戶端程序
public class FonJedis { //客戶端set方法 private static String set(Socket socket, String key, String value) throws IOException { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("*3").append("\r\n"); stringBuffer.append("$3").append("\r\n"); stringBuffer.append("set").append("\r\n"); stringBuffer.append("$").append(key.getBytes().length).append("\r\n"); stringBuffer.append(key).append("\r\n"); stringBuffer.append("$").append(value.getBytes().length).append("\r\n"); stringBuffer.append(value).append("\r\n"); socket.getOutputStream().write(stringBuffer.toString().getBytes()); byte[] response = new byte[2048]; socket.getInputStream().read(response); return new String(response); } //客戶端get方法 public static String get(Socket socket, String key) throws IOException { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("*2").append("\r\n"); stringBuffer.append("$3").append("\r\n"); stringBuffer.append("get").append("\r\n"); stringBuffer.append("$").append(key.getBytes().length).append("\r\n"); stringBuffer.append(key).append("\r\n"); socket.getOutputStream().write(stringBuffer.toString().getBytes()); byte[] response = new byte[2048]; socket.getInputStream().read(response); return new String(response); } public static void main(String[] args) { try { Socket socket = new Socket("bei1", 6379); FonJedis.set(socket, "name", "coffejoy"); String value = FonJedis.get(socket, "name"); System.out.println(value); } catch (IOException e) { e.printStackTrace(); } } }
7、主從同步
CAP原理
分佈式系統的節點每每都是分佈在不一樣的機器上進行網絡隔離開的,這意味着必然會有網絡斷開的風險,這個網絡斷開的場景的專業詞彙叫着「網絡分區」。
在網絡分區發生時,兩個分佈式節點之間沒法進行通訊,咱們對一個節點進行的修改操做將沒法同步到另一個節點,因此數據的「一致性」將沒法知足,由於兩個分佈式節點的數據再也不保持一致。除非咱們犧牲「可用性」,也就是暫停分佈式節點服務,在網絡分區發生時,再也不提供修改數據的功能,直到網絡情況徹底恢復正常再繼續對外提供服務。
CAP 原理就是——網絡分區發生時,一致性和可用性兩難全。
最終一致
Redis主從節點是經過異步的方式來同步數據的,因此是不知足一致性的。即便在主從兩個節點產生分區,依然知足可用性,由於查詢和修改都是在主節點進行。Redis保證最終一致性,就是從節點的數據會努力與主節點的數據保持一致。
增量同步與快照同步
增量同步
主節點將修改性指令放到buffer中,而後異步傳輸給從節點,從節點一遍執行指令,另外一邊須要告訴主節點執行到哪裏。
快照同步
是一種比較耗資源的操做。主節點將當前數據存儲到磁盤文件中,而後發送給從節點,從節點讀取快照文件中的數據,讀取完成後,執行增量同步。