Redis6.0已經低調的發布了穩定版,最大的變化就是支持I/O多線程,但舊版本就真的是單線程嗎,事情往往不是這么簡單,
這里的單線程指的是只有一個執行線程串行處理命令,再配合多路復用機制,實際上數據持久化、主從同步、連接釋放等都
有其他線程來處理。既然6.0都出來了,之前的文章我也說了不少Redis相關的了,借此契機,我們就來看看Redis的一些高級用法吧。
作者原創文章,謝絕一切形式轉載,違者必究!
准備:
Idea2019.03/JDK11.0.4/Jedis3.3.0/Redisson3.12.5
難度: 新手--戰士--老兵--大師
目標:
- Pipeline管道的使用
- 位圖bitmap使用
- Redis紅鎖原理
- Redis事務
- Lua腳本
1 Pipeline
Redis-cli有命令mset/mget,hmset/hmget,可以一次批處理多個賦/取值命令,那么Java API有Pipeline(管道)進行批處理,
可以將多個命令放入Pipeline,並一次獲得全部的執行結果,與client交互只有一個來回,示意圖如下:
我們來一段測試代碼:
/** compile group: 'redis.clients', name: 'jedis', version: '3.3.0' */ public class RedisTest { public static void main(String[] args) { Jedis redis = new Jedis("127.0.0.1", 6379); //使用第1個庫 redis.select(1); redis.flushDB(); long startTime = System.currentTimeMillis(); for (int i = 0; i < 50000; i++) { redis.set("key_"+i,"v_"+i); redis.get("key_"+i); } long endTime = System.currentTimeMillis(); System.out.println("formal time used >>> " + (endTime - startTime) + " ms"); // startTime = System.currentTimeMillis(); Pipeline pipeline = redis.pipelined(); for (int i = 0; i < 50000; i++) { // 返回Response<T>,但這里還未直接返回給client pipeline.set("key_2"+i,"v_2"+i); // 返回Response<T>,但這里還未直接返回給client pipeline.get("key_2" + i); /**這里使用打印輸出會出錯*/ //Response<String> response = pipeline.get("key_2" + i); //System.out.println(response.get()); } // Synchronize pipeline by reading all responses. This operation close the pipeline // 這里將同步pipeline所有返回結果,並放入List,返回void pipeline.sync(); // 如果需要返回結果集,可以使用以下方法,但官方建議應盡量避免使用,因為需要對pipeline所有返回結果做同步,很耗時, //List<Object> returnAll = pipeline.syncAndReturnAll(); //System.out.println("completed commands >>> "+returnAll.size()); //returnAll.forEach(System.out::println); endTime = System.currentTimeMillis(); System.out.println("pipeline time used >>> " + (endTime - startTime) + " ms"); } }
運行結果,可見Pipeline十分省時:
注意:高頻命令場景下,應避免使用管道,因為需要先將全部執行命令放入管道,會耗時。另外,需要使用返回值的情況也不建議使用,
同步所有管道返回結果也是個耗時的過程!管道無法提供原子性/事務保障。
2 分布式鎖
首先,一個好的分布式鎖,應該具有以下特征:
- 互斥——同時刻只能有一個持有者;
- 可重入——同一個持有者可多次進入;
- 阻塞——多個鎖請求者時,未獲得鎖的阻塞等待;
- 無死鎖——持有鎖的客戶端崩潰(crashed)或者網絡被分裂(gets partitioned),鎖仍然可以被獲取;
- 容錯——只要大部分節點正常,仍然可以獲取和釋放鎖;
單Redis實例下,通過SETNX命令來實現Redis分布式鎖,已不推薦使用,如果說使用SET命令配合NX參數或許會讓面試官更為滿意,
因為SET命令可帶有EX/PX/NX/XX
參數,更為強大,可以完成更復雜業務,但這些在此不表,我想說的是分布式多實例下的紅鎖算法:
在Redis的分布式環境中,我們假設有N(建議為5)個Redis master,這些節點完全互相獨立,不存在主從復制或者其他集群協調機制。
獲取一個紅鎖的步驟如下:
- 獲取當前系統時間,毫秒為單位
- 依次(同時並發地)嘗試從N個實例,使用相同的Key和隨機值(全局唯一)獲取鎖
- 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)即為獲取鎖消耗的時間。當且僅當從大多數(
N/2+1
)的Redis節點都獲取到鎖,並且消耗時間小於鎖失效時間時,紅鎖才算獲取成功 - 如果獲取到了紅鎖,key真正的有效時間等於有效時間減去獲取鎖消耗的時間(步驟3中的計算結果)
- 如果獲取紅鎖失敗(沒有在至少
N/2+1
個Redis實例取到鎖或者取鎖時間超過了鎖的有效時間),客戶端應該在所有的Redis實例上進行解鎖(即使某些Redis實例根本就沒有加鎖成功)
注:
A. 鎖指單Redis實例上使用如SET命令獲取的鎖,紅鎖是將多個單Redis實例鎖組合為一個鎖來管理,從而避免單點故障
B. 步驟2中,當向Redis設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,
則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,
客戶端應該盡快嘗試另外一個Redis實例。
C. 當客戶端無法取到紅鎖時,應該在一個隨機延遲后重試,防止多個客戶端在同時搶奪同一資源的鎖(這樣會導致腦裂,沒有人會取到鎖),
並且當客戶端從大多數Redis實例獲取鎖失敗時,應該盡快地釋放(部分)已經成功取到的鎖,這樣其他的客戶端就不必非得等到鎖過完“有效時間”才能取到,
Redission實現的紅鎖:
/** compile group: 'org.redisson', name: 'redisson', version: '3.12.5' */ public class RedLockTest { public static void main(String[] args) throws InterruptedException { Config config = new Config(); // 集群模式 config.useClusterServers() .addNodeAddress("127.0.0.1:6379") .addNodeAddress("127.0.0.1:6380") .addNodeAddress("127.0.0.1:6381"); RedissonClient redisson = Redisson.create(config); // RLock:Redis based implementation of {java.util.concurrent.locks.Lock} RLock lock1 = redisson.getLock("lock1"); RLock lock2 = redisson.getLock("lock2"); RLock lock3 = redisson.getLock("lock3"); // 嚴格來講,此處多個RLock應該從獨立的Redis實例上獲取,再組合為RedLock // 將多個獨立的RLock組合為一個RedLock RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); // lock實際上調用的是有默認值的tryLock, // 返回void redLock.lock(); // 異步請求鎖,返回RFuture redLock.lockAsync(); // 返回Boolean redLock.tryLock(10L,5L,TimeUnit.SECONDS); try{ // 業務邏輯處理 System.out.println("RedLock acquire success."); }finally { redLock.unlock(); } } }
以上代碼解析:RLock是實現了java.util.concurrent.locks.Lock接口而基於Redis 的鎖,即單Redis實例的鎖,RedissonRedLock即紅鎖,使用上就和JUC下的Lock類似了。
3 位圖
Redis中有一種特殊的存儲類型,二進制位對象——位圖(bitmap),存儲上是按照”字符串”處理,如果看官知曉布隆過濾器,就應該十分熟悉位圖了,其特點很明顯,
就是每位只有0/1兩種狀態值,如下圖示例,然后就是體積小,10M可存儲8千萬bit位,最大允許512M,可存儲40億bit位。
Redis-cli中只有6個命令:
- SETBIT:設置或清除指定偏移量上的位(bit)。當 key 不存在時,自動生成一個新的字符串值。字符串會進行伸展(grown)以確保它可以將 value 保存在指定的偏移量上。當字符串值進行伸展時,空白位置以 0 填充。
- GETBIT:對 key 所儲存的字符串值,獲取指定偏移量上的位(bit)。當 offset 比字符串值的長度大,或者 key 不存在時,返回 0。
- BITCOUNT:計算給定字符串中,被設置為 1 的比特位的數量。對一個不存在的 key 進行 BITCOUNT 操作,結果為 0 。
- BITPOS:返回位圖中第一個值為 bit 的二進制位的位置。
- BITTOP:對一個或多個保存二進制位的字符串 key 進行位元操作,並將結果保存到 destkey 上。
- BITFIELD:將一個 Redis 字符串看作是一個由二進制位組成的數組, 並對這個數組中儲存的長度不同的整數進行訪問。
RedisCli下使用,略!Jedis API模式下,核心部分代碼如下,其他輔助代碼同上:
... Jedis redis = new Jedis("127.0.0.1", 6379); redis.setbit("bit_key",1000L, "1"); redis.getbit("bit_key",1000L); redis.bitcount("bit_key",1L,1500L); // 對"bit_key1","bit_key2"做AND位運算,並保存到"dest_key"中 redis.bitop(BitOP.AND,"dest_key","bit_key1","bit_key2"); // redis.bitpos("bit_key",false); redis.bitpos("bit_key",true); ...
適用場景:比如記錄用戶是否登錄,登錄的次數統計等;當然,實現布隆過濾器肯定是可以的。
4 事務
Redis使用MULTI標記一個事務塊的開始。 事務塊內的多條命令會按照先后順序被放進一個隊列當中,最后由 EXEC 命令原子性(atomic)地執行。
Redis-cli下使用如下圖:
Jedis API使用核心部分代碼如下,輔助代碼,略:
... Jedis redis = new Jedis("127.0.0.1", 6379); // 監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那么事務將被打斷 redis.watch("k1","k2","k3"); Transaction transaction = redis.multi(); // 事務內多個命令 transaction.setbit("bit_key",100L, Boolean.parseBoolean("1")); transaction.mset("k1","v1","k2","v2","k3","v3"); List<Object> list = transaction.exec(); // 放棄事務 //String discard = transaction.discard(); /**如果在執行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被執行了的話,那么就不需要再執行 UNWATCH 了 * 因為 EXEC 命令會執行事務,因此 WATCH 命令的效果已經產生了;而 DISCARD 命令在取消事務的同時也會 * 取消所有對 key 的監視,因此這兩個命令執行之后,就沒有必要執行 UNWATCH 了*/ redis.unwatch(); ...
5 Lua腳本
Redis 使用單個 Lua 解釋器去運行所有腳本,並且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,
不會有其他腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很類似。
Redis-cli模式,命令行格式:
EVAL script numkeys key [key …] arg [arg …]
Lua 中通過全局變量 KEYS 數組,用 1 為基址的形式訪問鍵名參數;不是鍵名參數的附加參數 arg [arg ...] ,可以在 Lua 中通過全局變量 ARGV 數組訪問。
在 Lua 腳本中,可以使用兩個不同函數來執行 Redis 命令,它們分別是:redis.call()和redis.pcall(),這兩個函數的唯一區別在於它們使用不同的方式處理
執行命令所產生的異常。對於Lua腳本,還有SCRIPT 命令進行管理,在此不表。
Jedis API使用核心部分代碼如下,輔助代碼略:
... Jedis redis = new Jedis("127.0.0.1", 6379); List<String> keyList = Arrays.asList("key01","key01"); List<String> argsList = Arrays.asList("value01","value02"); // eval(scripts,keyList,argsList) redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", keyList, argsList ); // eval(scripts,keyCount,...params) redis.eval( "return redis.call('set',KEYS[1],'bar')", 1, "foo"); ...
6 新特性
最后還是說下新Redis6.0,最大變化是把網絡讀寫改為多線程,5.0版本使用IO多路復用,6.0改為多線程與Socket綁定,
但是注意IO 多線程要么同時在讀,要么同時在寫,不會同時讀和寫,且IO 線程只負責讀寫 socket、 解析命令,不負責命令執行,
從這個角度看6.0還是單線程的!
IO 多線程的目的一是更充分的利用多核CPU資源,低版本只能使用一個核,二是提高同步IO讀寫負載。在設置時,IO線程數一
定要小於機器核數。實際上,只有你的機器Redis實例已經占用相當大的CPU耗時的時候才建議采用,否則使用多線程沒有意義,
所以此新特性很多時候我們也用不上!
全文完!
近期其他文章:
只寫原創,敬請關注