Redis的慢查詢
許多存儲系統(例如 MySQL)提供慢查詢日志幫助開發和運維人員定位系統存在的慢操作。所謂慢查詢日志就是系統在命令執行前后計算每條命令的執行時間,當超過預設閥值,就將這條命令的相關信息(例如:發生時間,耗時,命令的詳細信息)記錄下來,Redis也提供了類似的功能。
Redis客戶端執行一條命令分為如下4個部分:
- 發送命令
- 命令排隊
- 執行命令
- 返回結果
需要注意,慢查詢只統計步驟3的時間,所以沒有慢查詢并不代表客戶端沒有超時問題。因為有可能是命令的網絡問題或者是命令在Redis在排隊,所以不是說命令執行很慢就說是慢查詢,而有可能是網絡的問題或者是Redis服務非常繁忙(隊列等待長)。
慢查詢配置
1、動態設置
慢查詢的閾值默認值是10毫秒。
參數:slowlog-log-slower-than
就是時間預設閥值,它的單位是微秒(1秒=1000毫秒=1 000 000微秒),默認值是10 000
,假如執行了一條“很慢”的命令(例如keys *),如果它的執行時間超過了10 000微秒,也就是10毫秒
,那么它將被記錄在慢查詢日志中。
查看slowlog-log-slower-than
config get slowlog-log-slower-than
更新slowlog-log-slower-than
config set slowlog-log-slower-than 20000
使用config set完后,若想將配置持久化保存到Redis.conf,要執行config rewrite
config rewrite
查看慢查詢
#設置慢查詢時間閾值為10微秒config set slowlog-log-slower-than 10#查看所有的keykeys *#查看慢查詢個數slowlog len#展示慢查詢信息(查詢2個慢查詢)slowlog get 2
慢查詢日志重置
slowlog reset
Pipeline
前面慢查詢的時候提到過時間消耗,其中1(發送命令)
和4(返回結果)
花費的時間稱為Round Trip Time (RTT
,往返時間),也就是數據在網絡上傳輸的時間。
Redis提供了批量操作命令(例如mget、mset等),有效地節約RTT。
但大部分命令是不支持批量操作的,例如要執行n次 hgetall命令,并沒有mhgetall命令存在,需要消耗n次RTT。
#設置一個hash對象json
hset json a 1 b 2#獲取json的屬性a
hget json a
#湖區json的屬性b
hget json b#一次性獲取json對象
hgetall json
舉例:Redis的客戶端和服務端可能部署在不同的機器上。例如客戶端在本地(上海),Redis服務器在阿里云的北京,兩地直線距離約為1200公里,那么1次RTT時間=1200 x2/ ( 300000×2/3 ) =12毫秒,(光在真空中傳輸速度為每秒30萬公里,這里假設光纖為光速的2/3 )。而Redis命令真正執行的時間通常在微秒(1000微妙=1毫秒)級別,所以才會有Redis 性能瓶頸是網絡這樣的說法。
Pipeline
(流水線)機制能改善上面這類問題,它能將一組 Redis命令進行組裝,通過一次RTT傳輸給Redis,再將這組Redis命令的執行結果按順序返回給客戶端,沒有使用Pipeline執行了n條命令,整個過程需要n次RTT。
使用Pipeline 執行了n次命令,整個過程需要1次RTT。
public void plSet(List<String> keys,List<String> values) {Jedis jedis = null;try {jedis = jedisPool.getResource();Pipeline pipelined = jedis.pipelined();for(int i=0;i<keys.size();i++){pipelined.set(keys.get(i),values.get(i));}pipelined.sync();} catch (Exception e) {throw new RuntimeException("執行Pipeline設值失敗!",e);} finally {jedis.close();}}/**逐個set和利用pipeline的處理時間對比*/public void testPipeline() {long setStart = System.currentTimeMillis();for (int i = 0; i < TEST_COUNT; i++) { //單個的操作redisString.set("testStringM:key_" + i, String.valueOf(i));}long setEnd = System.currentTimeMillis();System.out.println("非pipeline操作"+TEST_COUNT+"次字符串數據類型set寫入,耗時:" + (setEnd - setStart) + "毫秒");List<String> keys = new ArrayList<>(TEST_COUNT);List<String> values= new ArrayList<>(TEST_COUNT);for (int i = 0; i < keys.size(); i++) {keys.add("testpipelineM:key_"+i);values.add(String.valueOf(i));}long pipelineStart = System.currentTimeMillis();redisPipeline.plSet(keys,values);long pipelineEnd = System.currentTimeMillis();System.out.println("pipeline操作"+TEST_COUNT+"次字符串數據類型set寫入,耗時:" + (pipelineEnd - pipelineStart) + "毫秒");}
非pipeline操作10000次字符串數據類型set寫入,耗時:4934毫秒
pipeline操作10000次字符串數據類型set寫入,耗時:7毫秒
事務
Redis提供了簡單的事務功能,將一組需要一起執行的命令放到multi
和exec
兩個命令之間。multi 命令代表事務開始,exec命令代表事務結束。另外discard命令是回滾。
multi
sadd sa 1
sadd sb 2
exec
當沒有執行exec
的時候,開啟另外一個redis-cli
,利用SISMEMBER
確認關鍵字是否存在
sismember sa 1
(integer) 0
當執行完exec
的時候,開啟另外一個redis-cli
,利用SISMEMBER
確認關鍵字是否存在
sismember sa 1
(integer) 1
Redis的事務原理
事務是Redis實現在服務器端的行為,用戶執行MULTI
命令時,服務器會將對應這個用戶的客戶端對象設置一個特殊的狀態,在這個狀態下后續用戶執行的查詢命令不會被真的執行,而是被服務器緩存起來,直到用戶執行EXEC
命令為止,服務器會將這個用戶對應的客戶端對象中緩存的命令按照提交的順序依次執行。
Redis的watch命令
有些應用場景需要在事務之前,確保事務中的key沒有被其他客戶端修改過,才執行事務,否則不執行(類似樂觀鎖)。Redis 提供了watch命令來解決這類問題。
可以看到“客戶端-1”在執行multi之前執行了watch命令,“客戶端-2”在“客戶端-1”執行exec之前修改了key值,造成客戶端-1事務沒有執行(exec結果為nil)。
Redis客戶端中的事務使用代碼:
public final static String RS_TRANS_NS = "rts:";@Autowiredprivate JedisPool jedisPool;public List<Object> transaction(String... watchKeys){Jedis jedis = null;try {jedis = jedisPool.getResource();if(watchKeys.length>0){/*使用watch功能*/String watchResult = jedis.watch(watchKeys);if(!"OK".equals(watchResult)) {throw new RuntimeException("執行watch失敗:"+watchResult);}}Transaction multi = jedis.multi();multi.set(RS_TRANS_NS+"test1","a1");multi.set(RS_TRANS_NS+"test2","a2");multi.set(RS_TRANS_NS+"test3","a3");List<Object> execResult = multi.exec();if(execResult==null){throw new RuntimeException("事務無法執行,監視的key被修改:"+watchKeys);}System.out.println(execResult);return execResult;} catch (Exception e) {throw new RuntimeException("執行Redis事務失敗!",e);} finally {if(watchKeys.length>0){jedis.unwatch();/*前面如果watch了,這里就要unwatch*/}jedis.close();}}
Pipeline和事務的區別
PipeLine看起來和事務很類似,感覺都是一批批處理,但兩者還是有很大的區別。簡單來說。
1、pipeline是客戶端的行為,對于服務器來說是透明的,可以認為服務器無法區分客戶端發送來的查詢命令是以普通命令的形式還是以pipeline的形式發送到服務器的;
2、而事務則是實現在服務器端的行為,用戶執行MULTI命令時,服務器會將對應這個用戶的客戶端對象設置為一個特殊的狀態,在這個狀態下后續用戶執行的查詢命令不會被真的執行,而是被服務器緩存起來,直到用戶執行EXEC命令為止,服務器會將這個用戶對應的客戶端對象中緩存的命令按照提交的順序依次執行。
3、應用pipeline可以提服務器的吞吐能力,并提高Redis處理查詢請求的能力。
但是這里存在一個問題,當通過pipeline提交的查詢命令數據較少,可以被內核緩沖區所容納時,Redis可以保證這些命令執行的原子性。然而一旦數據量過大,超過了內核緩沖區的接收大小,那么命令的執行將會被打斷,原子性也就無法得到保證。因此pipeline只是一種提升服務器吞吐能力的機制,如果想要命令以事務的方式原子性的被執行,還是需要事務機制,或者使用更高級的腳本功能以及模塊功能。
4、可以將事務和pipeline結合起來使用,減少事務的命令在網絡上的傳輸時間,將多次網絡IO縮減為一次網絡IO。
Redis提供了簡單的事務,之所以說它簡單,主要是因為它不支持事務中的回滾特性,同時無法實現命令之間的邏輯關系計算,當然也體現了Redis 的“keep it simple”的特性,下一節將介紹的Lua腳本同樣可以實現事務的相關功能,但是功能要強大很多。