在Java中使用redisTemplate操作快取

detectiveHLH發表於2019-01-18

背景

在最近的專案中,有一個需求是對一個很大的資料庫進行查詢,資料量大概在幾千萬條。但同時對查詢速度的要求也比較高。

這個資料庫之前在沒有使用Presto的情況下,使用的是Hive,使用Hive進行一個簡單的查詢,速度可能在幾分鐘。當然幾分鐘也並不完全是跑SQL的時間,這裡麵包含發請求,查詢資料並且返回資料的時間的總和。但是即使這樣,這樣的速度明顯不能滿足互動式的查詢需求。

我們的下一個解決方案就是Presto,在使用了Presto之後,查詢速度降到了秒級。但是對於一個前端查詢介面的互動式查詢來說,十幾秒仍然是一個不能接受的時間。

雖然Presto相比Hive已經快了很多(FaceBook官方宣稱的是10倍),但是對分頁的支援不是很友好。我在使用的時候是自己在後端實現的分頁。

在這種情況下應用快取實屬無奈之舉。講道理,優化應從底層開始,自底而上。上層優化的方式和效率感覺都很有侷限。

為什麼要使用快取

前端查詢中,單次查詢的匹配資料量有可能會達到上百甚至上千條,在前端中肯定是需要分頁展示的。就算每次查詢10條資料,整個查詢也要耗時6-8s的時間。想象一下,每翻一頁等10s的場景。

所以,此時使用redis快取。減少請求資料庫的次數。將匹配的資料一併存入資料庫。這樣只有在第一次查詢時耗費長一點,一旦查詢完成,使用者點選下一頁就是毫秒級別的操作了。

使用redisTemplate

Spring封裝了一個比較強大的模板,也就是redisTemplate,方便在開發的時候操作Redis快取。在Redis中可以儲存String、List、Set、Hash、Zset。下面將針對List和Hash分別介紹。

List

Redis中的List為簡單的字串列表,常見的有下面幾種操作。

hasKey

判斷一個鍵是否存在,只需要呼叫hasKey就可以了。假設這個Key是test,具體用法如下。

if (redisTemplate.hasKey("test")) { 
System.out.println("存在");

} else {
System.out.println("不存在");

}複製程式碼

range

該函式用於從redis快取中獲取指定區間的資料。具體用法如下。

if (redisTemplate.hasKey("test")) { 
// 該鍵的值為 [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range("test", 0, 0));
// [4] System.out.println(redisTemplate.opsForList().range("test", 0, 1));
// [4, 3] System.out.println(redisTemplate.opsForList().range("test", 0, 2));
// [4, 3, 2] System.out.println(redisTemplate.opsForList().range("test", 0, 3));
// [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range("test", 0, 4));
// [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range("test", 0, 5));
// [4, 3, 2, 1] System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [4, 3, 2, 1] 如果結束位是-1, 則表示取所有的值
}複製程式碼

delete

刪除某個鍵。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [1, 2, 3, 4]redisTemplate.delete("test");
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// []複製程式碼

size

獲取該鍵的集合長度。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
System.out.println(redisTemplate.opsForList().size("test"));
// 4複製程式碼

leftPush

我們把存放這個值的地方想象成如圖所示的容器。

container

在Java中使用redisTemplate操作快取

並且取資料總是從左邊取,但是存資料可以從左也可以從右。左就是leftPush,右就是rightPush。leftPush如下圖所示。

left-push

在Java中使用redisTemplate操作快取

用法如下。

for (int i = 0;
i <
4;
i++) {
Integer value = i + 1;
redisTemplate.opsForList().leftPush("test", value.toString());
System.out.println(redisTemplate.opsForList().range("test", 0, -1));

}複製程式碼

控制檯輸出的結果如下。

[1][2, 1][3, 2, 1][4, 3, 2, 1]複製程式碼

leftPushAll

基本和leftPush一樣,只不過是一次性的將List入棧。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().leftPushAll("test", test);
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [4, 3, 2, 1]複製程式碼

當然你也可以這樣

redisTemplate.opsForList().leftPushAll("test", "1", "2", "3", "4");
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [4, 3, 2, 1]複製程式碼

leftPushIfPresent

leftPush是同樣的操作,唯一的不同是,當且僅當key存在時,才會更新key的值。如果key不存在則不會對資料進行任何操作。

redisTemplate.delete("test");
redisTemplate.opsForList().leftPushIfPresent("test", "1");
redisTemplate.opsForList().leftPushIfPresent("test", "2");
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// []複製程式碼

leftPop

該函式用於移除上面我們抽象的容器中的最左邊的一個元素。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
redisTemplate.opsForList().leftPop("test");
// [2, 3, 4]redisTemplate.opsForList().leftPop("test");
// [3, 4]redisTemplate.opsForList().leftPop("test");
// [4]redisTemplate.opsForList().leftPop("test");
// []redisTemplate.opsForList().leftPop("test");
// []複製程式碼

值得注意的是,當返回為空後,在redis中這個key也不復存在了。如果此時再呼叫leftPushIfPresent,是無法再新增資料的。有程式碼有真相。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
redisTemplate.opsForList().leftPop("test");
// [2, 3, 4]redisTemplate.opsForList().leftPop("test");
// [3, 4]redisTemplate.opsForList().leftPop("test");
// [4]redisTemplate.opsForList().leftPop("test");
// []redisTemplate.opsForList().leftPop("test");
// []redisTemplate.opsForList().leftPushIfPresent("test", "1");
// []redisTemplate.opsForList().leftPushIfPresent("test", "1");
// []複製程式碼

rightPush

rightPush如下圖所示。

right-push

在Java中使用redisTemplate操作快取

用法如下。

for (int i = 0;
i <
4;
i++) {
Integer value = i + 1;
redisTemplate.opsForList().leftPush("test", value.toString());
System.out.println(redisTemplate.opsForList().range("test", 0, -1));

}複製程式碼

控制檯輸出的結果如下。

[1][1, 2][1, 2, 3][1, 2, 3, 4]複製程式碼

rightPushAll

同rightPush,一次性將List存入。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [1, 2, 3, 4]複製程式碼

當然你也可以這樣。

redisTemplate.opsForList().rightPushAll("test", "1", "2", "3", "4");
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [1, 2, 3, 4]複製程式碼

rightPushIfPresent

rightPush是同樣的操作,唯一的不同是,當且僅當key存在時,才會更新key的值。如果key不存在則不會對資料進行任何操作。

redisTemplate.delete("test");
redisTemplate.opsForList().rightPushIfPresent("test", "1");
redisTemplate.opsForList().rightPushIfPresent("test", "2");
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// []複製程式碼

rightPop

該函式用於移除上面我們抽象的容器中的最右邊的一個元素。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
redisTemplate.opsForList().rightPop("test");
// [1, 2, 3]redisTemplate.opsForList().rightPop("test");
// [1, 2]redisTemplate.opsForList().rightPop("test");
// [1]redisTemplate.opsForList().rightPop("test");
// []redisTemplate.opsForList().rightPop("test");
// []複製程式碼

leftPop一樣,返回空之後,再呼叫rightPushIfPresent,是無法再新增資料的。

index

獲取list中指定位置的元素。

if (redisTemplate.hasKey("test")) { 
// 該鍵的值為 [1, 2, 3, 4] System.out.println(redisTemplate.opsForList().index("test", -1));
// 4 System.out.println(redisTemplate.opsForList().index("test", 0));
// 1 System.out.println(redisTemplate.opsForList().index("test", 1));
// 2 System.out.println(redisTemplate.opsForList().index("test", 2));
// 3 System.out.println(redisTemplate.opsForList().index("test", 3));
// 4 System.out.println(redisTemplate.opsForList().index("test", 4));
// null System.out.println(redisTemplate.opsForList().index("test", 5));
// null
}複製程式碼

值得注意的有兩點。一個是如果下標是-1的話,則會返回List最後一個元素,另一個如果陣列下標越界,則會返回null

trim

用於擷取指定區間的元素,可能你會理解成與range是一樣的作用。看了下面的程式碼之後應該就會立刻理解。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
redisTemplate.opsForList().rightPushAll("test", test);
// [1, 2, 3, 4]redisTemplate.opsForList().trim("test", 0, 2);
// [1, 2, 3]複製程式碼

其實作用完全不一樣。range是獲取指定區間內的資料,而trim是留下指定區間的資料,刪除不在區間的所有資料。trimvoid,不會返回任何資料。

remove

用於移除鍵中指定的元素。接受3個引數,分別是快取的鍵名,計數事件,要移除的值。計數事件可以傳入的有三個值,分別是-101

-1代表從儲存容器的最右邊開始,刪除一個與要移除的值匹配的資料;0代表刪除所有與傳入值匹配的資料;1代表從儲存容器的最左邊開始,刪除一個與要移除的值匹配的資料。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
test.add("4");
test.add("3");
test.add("2");
test.add("1");
redisTemplate.opsForList().rightPushAll("test", test);
// [1, 2, 3, 4, 4, 3, 2, 1]// 當計數事件是-1、傳入值是1時redisTemplate.opsForList().remove("test", -1, "1");
// [1, 2, 3, 4, 4, 3, 2]// 當計數事件是1,傳入值是1時redisTemplate.opsForList().remove("test", 1, "1");
// [2, 3, 4, 4, 3, 2]// 當計數事件是0,傳入值是4時redisTemplate.opsForList().remove("test", 0, "4");
// [2, 3, 3, 2]複製程式碼

rightPopAndLeftPush

該函式用於操作兩個鍵之間的資料,接受兩個引數,分別是源key、目標key。該函式會將源key進行rightPop,再將返回的值,作為輸入引數,在目標key上進行leftPush。具體程式碼如下。

List<
String>
test = new ArrayList<
>
();
test.add("1");
test.add("2");
test.add("3");
test.add("4");
List<
String>
test2 = new ArrayList<
>
();
test2.add("1");
test2.add("2");
test2.add("3");
redisTemplate.opsForList().rightPushAll("test", test);
// [1, 2, 3, 4]redisTemplate.opsForList().rightPushAll("test2", test2);
// [1, 2, 3]redisTemplate.opsForList().rightPopAndLeftPush("test", "test2");
System.out.println(redisTemplate.opsForList().range("test", 0, -1));
// [1, 2, 3]System.out.println(redisTemplate.opsForList().range("test2", 0, -1));
// [4, 1, 2, 3]複製程式碼

Hash

儲存型別為hash其實很好理解。在上述的List中,一個redis的Key可以理解為一個List,而在Hash中,一個redis的Key可以理解為一個HashMap。

put

用於寫入資料。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
// [1, 2, 3, 4]redisTemplate.opsForHash().put("test", "isAdmin", true);
// true複製程式碼

putAll

用於一次性向一個Hash鍵中新增多個key。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<
String>
list2 = new ArrayList<
>
();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
Map<
String, String>
valueMap = new HashMap<
>
();
valueMap.put("map1", list.toString());
valueMap.put("map2", list2.toString());
redisTemplate.opsForHash().putAll("test", valueMap);
// {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]
}
複製程式碼

putIfAbsent

用於向一個Hash鍵中寫入資料。當key在Hash鍵中已經存在時,則不會寫入任何資料,只有在Hash鍵中不存在這個key時,才會寫入資料。

同時,如果連這個Hash鍵都不存在,redisTemplate會新建一個Hash鍵,再寫入key。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().putIfAbsent("test", "map", list.toString());
System.out.println(redisTemplate.opsForHash().entries("test"));
// {map=[1, 2, 3, 4]
}
複製程式碼

get

用於獲取資料。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);
System.out.println(redisTemplate.opsForHash().get("test", "map"));
// [1, 2, 3, 4]System.out.println(redisTemplate.opsForHash().get("test", "isAdmin"));
// trueBoolean bool = (Boolean) redisTemplate.opsForHash().get("test", "isAdmin");
System.out.println(bool);
// trueString str = redisTemplate.opsForHash().get("test", "map").toString();
List<
String>
array = JSONArray.parseArray(str, String.class);
System.out.println(array.size());
// 4複製程式碼

值得注意的是,使用get函式獲取的資料都是Object型別。

所以需要使用型別與上述例子中的布林型別的話,則需要強制轉換一次。List型別則可以使用fastjson這種工具來進行轉換。轉換的例子已列舉在上述程式碼中。

delete

用於刪除一個Hash鍵中的key。可以理解為刪除一個map中的某個key。

 List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<
String>
list2 = new ArrayList<
>
();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
Map<
String, String>
valueMap = new HashMap<
>
();
valueMap.put("map1", list.toString());
valueMap.put("map2", list2.toString());
redisTemplate.opsForHash().putAll("test", valueMap);
// {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]
}
redisTemplate.opsForHash().delete("test", "map1");
// {map2=[5, 6, 7, 8]
}
複製程式碼

values

用於獲取一個Hash型別的鍵的所有值。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);
System.out.println(redisTemplate.opsForHash().values("test"));
// [[1, 2, 3, 4], true]複製程式碼

entries

用於以Map的格式獲取一個Hash鍵的所有值。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);
Map<
String, String>
map = redisTemplate.opsForHash().entries("test");
System.out.println(map.get("map"));
// [1, 2, 3, 4]System.out.println(map.get("map") instanceof String);
// trueSystem.out.println(redisTemplate.opsForHash().entries("test"));
// {a=[1, 2, 3, 4], isAdmin=true
}
複製程式碼

hasKey

用於獲取一個Hash鍵中是否含有某個鍵。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);
System.out.println(redisTemplate.opsForHash().hasKey("test", "map"));
// trueSystem.out.println(redisTemplate.opsForHash().hasKey("test", "b"));
// falseSystem.out.println(redisTemplate.opsForHash().hasKey("test", "isAdmin"));
// true複製程式碼

keys

用於獲取一個Hash鍵中所有的鍵。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);
System.out.println(redisTemplate.opsForHash().keys("test"));
// [a, isAdmin]複製程式碼

size

用於獲取一個Hash鍵中包含的鍵的數量。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
redisTemplate.opsForHash().put("test", "map", list.toString());
redisTemplate.opsForHash().put("test", "isAdmin", true);
System.out.println(redisTemplate.opsForHash().size("test"));
// 2複製程式碼

increment

用於讓一個Hash鍵中的某個key,根據傳入的值進行累加。傳入的數值只能是double或者long,不接受浮點型

redisTemplate.opsForHash().increment("test", "a", 3);
redisTemplate.opsForHash().increment("test", "a", -3);
redisTemplate.opsForHash().increment("test", "a", 1);
redisTemplate.opsForHash().increment("test", "a", 0);
System.out.println(redisTemplate.opsForHash().entries("test"));
// {a=1
}
複製程式碼

multiGet

用於批量的獲取一個Hash鍵中多個key的值。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<
String>
list2 = new ArrayList<
>
();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
redisTemplate.opsForHash().put("test", "map1", list.toString());
// [1, 2, 3, 4]redisTemplate.opsForHash().put("test", "map2", list2.toString());
// [5, 6, 7, 8]List<
String>
keys = new ArrayList<
>
();
keys.add("map1");
keys.add("map2");
System.out.println(redisTemplate.opsForHash().multiGet("test", keys));
// [[1, 2, 3, 4], [5, 6, 7, 8]]System.out.println(redisTemplate.opsForHash().multiGet("test", keys) instanceof List);
// true複製程式碼

scan

獲取所以匹配條件的Hash鍵中key的值。我查過一些資料,大部分寫的是無法模糊匹配,我自己嘗試了一下,其實是可以的。如下,使用scan模糊匹配hash鍵的key中,帶SCAN的key。

List<
String>
list = new ArrayList<
>
();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
List<
String>
list2 = new ArrayList<
>
();
list2.add("5");
list2.add("6");
list2.add("7");
list2.add("8");
List<
String>
list3 = new ArrayList<
>
();
list3.add("9");
list3.add("10");
list3.add("11");
list3.add("12");
Map<
String, String>
valueMap = new HashMap<
>
();
valueMap.put("map1", list.toString());
valueMap.put("SCAN_map2", list2.toString());
valueMap.put("map3", list3.toString());
redisTemplate.opsForHash().putAll("test", valueMap);
// {SCAN_map2=[5, 6, 7, 8], map3=[9, 10, 11, 12], map1=[1, 2, 3, 4]
}
Cursor<
Map.Entry<
String, String>
>
cursor = redisTemplate.opsForHash().scan("test", ScanOptions.scanOptions().match("*SCAN*").build());
if (cursor.hasNext()) {
while (cursor.hasNext()) {
Map.Entry<
String, String>
entry = cursor.next();
System.out.println(entry.getValue());
// [5, 6, 7, 8]
}
}複製程式碼

引入redisTemplate

如果大家看懂了怎麼用,就可以將redisTemplate引入專案中了。

引入pom依賴

<
dependency>
<
groupId>
org.springframework.boot<
/groupId>
<
artifactId>
spring-boot-starter-data-redis<
/artifactId>
<
version>
2.0.5.RELEASE<
/version>
<
/dependency>
複製程式碼

新建配置檔案

然後需要新建一個RedisConfig配置檔案。

package com.detectivehlh;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
/** * RedisConfig * * @author Lunhao Hu * @date 2019-01-17 15:12 **/@Configurationpublic class RedisConfig {
@Bean public RedisTemplate<
String, String>
redisTemplate(RedisConnectionFactory factory)
{
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//redis序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisTemplate template = new StringRedisTemplate(factory);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;

}
}複製程式碼

注入

將redisTemplate注入到需要使用的地方。

@Autowiredprivate RedisTemplate redisTemplate;
複製程式碼

寫在後面

Github

來源:https://juejin.im/post/5c4165d66fb9a049e93d05a3#comment

相關文章