在最近的項目中,有一個需求是對一個很大的數據庫進行查詢,數據量大概在幾千萬條。但同時對查詢速度的要求也比較高。前端
這個數據庫以前在沒有使用Presto的狀況下,使用的是Hive,使用Hive進行一個簡單的查詢,速度可能在幾分鐘。固然幾分鐘也並不徹底是跑SQL的時間,這裏麪包含發請求,查詢數據而且返回數據的時間的總和。可是即便這樣,這樣的速度明顯不能知足交互式的查詢需求。git
咱們的下一個解決方案就是Presto,在使用了Presto以後,查詢速度降到了秒級。可是對於一個前端查詢界面的交互式查詢來講,十幾秒仍然是一個不能接受的時間。github
雖然Presto相比Hive已經快了不少(FaceBook官方宣稱的是10倍),可是對分頁的支持不是很友好。我在使用的時候是本身在後端實現的分頁。redis
在這種狀況下應用緩存實屬無奈之舉。講道理,優化應從底層開始,自底而上。上層優化的方式和效率感受都頗有侷限。spring
<!--more-->數據庫
前端查詢中,單次查詢的匹配數據量有可能會達到上百甚至上千條,在前端中確定是須要分頁展現的。就算每次查詢10條數據,整個查詢也要耗時6-8s的時間。想象一下,每翻一頁等10s的場景。json
因此,此時使用redis緩存。減小請求數據庫的次數。將匹配的數據一併存入數據庫。這樣只有在第一次查詢時耗費長一點,一旦查詢完成,用戶點擊下一頁就是毫秒級別的操做了。後端
Spring封裝了一個比較強大的模板,也就是redisTemplate,方便在開發的時候操做Redis緩存。在Redis中能夠存儲String、List、Set、Hash、Zset。下面將針對List和Hash分別介紹。數組
Redis中的List爲簡單的字符串列表,常見的有下面幾種操做。緩存
判斷一個鍵是否存在,只須要調用hasKey
就能夠了。假設這個Key是test
,具體用法以下。
if (redisTemplate.hasKey("test")) { System.out.println("存在"); } else { System.out.println("不存在"); }
該函數用於從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, 則表示取全部的值 }
刪除某個鍵。
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)); // []
獲取該鍵的集合長度。
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
咱們把存放這個值的地方想象成如圖所示的容器。
container
而且取數據老是從左邊取,可是存數據能夠從左也能夠從右。左就是leftPush
,右就是rightPush
。leftPush以下圖所示。
left-push
用法以下。
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]
基本和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]
跟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)); // []
該函數用於移除上面咱們抽象的容器中的最左邊的一個元素。
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以下圖所示。
right-push
用法以下。
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]
同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]
跟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)); // []
該函數用於移除上面咱們抽象的容器中的最右邊的一個元素。
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,是沒法再添加數據的。
獲取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
。
用於截取指定區間的元素,可能你會理解成與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
是留下指定區間的數據,刪除不在區間的全部數據。trim
是void
,不會返回任何數據。
用於移除鍵中指定的元素。接受3個參數,分別是緩存的鍵名,計數事件,要移除的值。計數事件能夠傳入的有三個值,分別是-1
、0
、1
。
-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時 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]
該函數用於操做兩個鍵之間的數據,接受兩個參數,分別是源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其實很好理解。在上述的List
中,一個redis的Key能夠理解爲一個List,而在Hash
中,一個redis的Key能夠理解爲一個HashMap。
用於寫入數據。
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
用於一次性向一個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]}
用於向一個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]}
用於獲取數據。
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")); // true Boolean bool = (Boolean) redisTemplate.opsForHash().get("test", "isAdmin"); System.out.println(bool); // true String 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
這種工具來進行轉換。轉換的例子已列舉在上述代碼中。
用於刪除一個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]}
用於獲取一個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]
用於以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); // true System.out.println(redisTemplate.opsForHash().entries("test")); // {a=[1, 2, 3, 4], isAdmin=true}
用於獲取一個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")); // true System.out.println(redisTemplate.opsForHash().hasKey("test", "b")); // false System.out.println(redisTemplate.opsForHash().hasKey("test", "isAdmin")); // true
用於獲取一個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]
用於獲取一個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
用於讓一個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}
用於批量的獲取一個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
獲取因此匹配條件的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引入項目中了。
<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 **/ @Configuration public 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注入到須要使用的地方。
@Autowired private RedisTemplate redisTemplate;