簡介
我們在項目中大量使用redis,卻很少停下腳步細細研究它到底是個什麼東西。有人說它是nosql的一種,有人說它是緩存。redis官網中說到Redis是一個開源的基於內存的數據結構存儲器,可用作數據庫、緩存、消息代理(Message Broker)。提供了字符串、hash表、list、set、有序集合等非常豐富的數據結構。Redis具有內置的版本複製、Lua腳本、LRU回收、事物和多層次的磁盤持久化,還通過Redis Sentinel 實現了高可用性、通過redis cluster實現自動分區。
總的來說,Redis是一個基於內存實現的key-value形式的存儲結構。接下來,小方想從管道技術、發佈訂閱、事物這幾個點入手學習。
Redis管道技術
redis中使用了管道技術(pipeline)來加速查詢。
Redis是一個使用客戶端/服務端模式的TCP服務,這也就是說一個請求要經歷如下的過程:
(1)客戶端發送查詢請求到服務端,通常以阻塞的方式等待從socket中讀取服務端的響應。
(2)服務端處理查詢的命令,發送響應給客戶端。
假如客戶端要執行如下的四條命令,
- Client: INCR X
- Server: 1
- Client: INCR X
- Server: 2
- Client: INCR X
- Server: 3
- Client: INCR X
- Server: 4
如果像http協議一樣每發一個請求都必須等待響應再發下一條,上面的例子至少需要8個tcp包。實際上對於redis來說沒有這個必要,使用管道技術,客戶端可以在未收到響應的時候繼續發送請求,可以將多個請求一起發送,服務端將最終的結果返回。
使用管道技術,上面的例子可以優化爲
- Client: INCR X
- Client: INCR X
- Client: INCR X
- Client: INCR X
- Server: 1
- Server: 2
- Server: 3
- Server: 4
使用java API測試管道技術,分別在使用管道和不使用管道的情況下進行10000次的自增操作。
public class RedisTest {
@Test
public void testRedis(){
System.out.println();
long start = System.currentTimeMillis();
usePipeline();
long end = System.currentTimeMillis();
System.out.println("usePipeline:"+(end - start));
start = System.currentTimeMillis();
withoutPipeline();
end = System.currentTimeMillis();
System.out.println("withoutPipeline:"+(end - start));
}
private static void withoutPipeline() {
try {
Jedis jedis = new Jedis("127.0.0.1", 6379);
for (int i = 0; i < 10000; i++) {
jedis.incr("test2");
}
jedis.disconnect();
} catch (Exception e) {
}
}
private static void usePipeline() {
try {
Jedis jedis = new Jedis("127.0.0.1", 6379);
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.incr("test2");
}
pipeline.sync();
jedis.disconnect();
} catch (Exception e) {
}
}
}
測試結果:
usePipeline:4
withoutPipeline:349
差別非常明顯。
作爲redis的使用者,服務端如何實現無須過多的關注(其實是因爲小方看不懂c),下面來看java中封裝的Jedis和Pipeline。
在使用Jedis的過程中,一般情況下我們調用的都是redis.clients.jedis.Jedis這個類中的方法,以set方法爲例
public String set(String key, String value) {
this.checkIsInMultiOrPipeline();
this.client.set(key, value);
return this.client.getStatusCodeReply();
}
在每次調用client的方法之前,都會進行事務和管道的檢查,因爲Jedis不能進行有事務的操作,帶事務的連接要用redis.clients.jedis.Transaction類,同樣Jedis也不能進行管道的操作,要用redis.clients.jedis.Pipeline類。也就是說Jedis中的操作都是無事務的不使用管道的???
protected void checkIsInMultiOrPipeline() {
if (this.client.isInMulti()) {
throw new JedisDataException("Cannot use Jedis when in Multi. Please use Transation or reset jedis state.");
} else if (this.pipeline != null && this.pipeline.hasPipelinedResponse()) {
throw new JedisDataException("Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state .");
}
}
那如果要使用管道呢,調用jedis.pipelined()獲取管道的實例,之後通過Pipeline操作redis。
public Pipeline pipelined() {
this.pipeline = new Pipeline();
this.pipeline.setClient(this.client);
return this.pipeline;
}
再來看Pipeline 的set方法
public Response<String> set(String key, String value) {
this.getClient(key).set(key, value);
return this.getResponse(BuilderFactory.STRING);
}
僅僅set key-value的過程中看不出什麼問題,因爲兩者調用的都是redis.clients.jedis.Client中的方法。那麼這個時候就想兩者的區別在於響應信息的讀取嗎?
Pipeline中也區分了兩種模式,如果是事務模式(即isInMulti爲true)使用ArrayList來暫存響應,不是則使用LinkedList來暫存響應。下面僅看非事務模式。
實際上,this.getResponse(BuilderFactory.STRING)這行代碼並不是獲取響應,而是將響應放入緩存,並返回響應的實例。
既然返回響應的方式不一樣了,那麼編碼方式也不一樣了。
Jedis jedis = new Jedis("127.0.0.1",6379);
Pipeline pipeline = jedis.pipelined();
Response<String> response = pipeline.get("test2");
//System.out.println(response.get());//在未調用sync()之前,直接get會報JedisDataException
pipeline.sync();
System.out.println(response.get());
一定要在sync之後再讀取響應,因爲sync才真正將緩存的響應放到了Response中,也就是說我們可以在sync之前進行大量的操作,最後在sync一次性獲取所有響應。
public void sync() {
if (this.getPipelinedResponseLength() > 0) {
List<Object> unformatted = this.client.getAll();
Iterator var2 = unformatted.iterator();
while(var2.hasNext()) {
Object o = var2.next();
this.generateResponse(o);
}
}
}
總結
畢竟緩存響應使用的是ArrayList或者LinkedList,存儲空間有限,不宜一次性發送過多的命令。個人認爲,管道技術適合於不需要關注中間結果的頻繁操作,在發送完命令之後,最後一次性讀取結果,僅操作單個key無需使用管道。
下次更新:
Redis發佈訂閱
Redis事物
面試中常見的問題
- 爲什麼單線程的redis會認爲比多線程的數據庫操作快?
- 高併發環境中如何解決資源競爭問題
- redis適用的場景,優缺點
- redis與memcached
- redis的事物
- redis的緩存失效策略和主鍵失效機制