目錄
1.4 SessionCallback和RedisCallback
在現今互聯網應用中,NoSql已經廣泛應用,在互聯網中起到加速系統的作用。有兩種NoSQL使用最爲廣泛,那就是Redis和MongoDB。
Redis是一種運行在內存的數據庫,支持7種數據類型的存儲。Redis的運行速度很快,大約是關係數據庫幾倍到幾十倍的速度。如果我們將常用的數據存儲在Redis中,用來代替關係數據庫的查詢訪問,網站性能將可以得到大幅提高。在現實中,查詢數據要遠遠大於更新數據,一般一個正常的網站查詢和更新的比例大約是1:9到3:7,在查詢比例較大的網站使用Redis可以數倍地提升網站的性能。
Redis自身數據類型比較少,命令功能也比較有限,運算能力一直不強,所以在Redis2.6版本之後,開始增加Lua語言的支持,這樣Redis的運算能力就大大提高了,而且在Redis中Lua語言的執行是原子性的,也就是在Redis執行Lua時,不會被其他命令所打斷,這就能夠保證在高併發場景下的一致性。
Spring是通過spring-data-redis項目對Redis開發進行支持的,在討論Spring Boot如何使用Redis之前,有必要簡單地介紹一下這個項目。
1.spring-data-redis項目簡介
1.1 spring-data-redis項目的設計
在Java中與Redis連接的驅動存在很多種,目前比較廣泛使用的是Jedis,其他的還有Lettuce、Jredis和Srp。Lettuce目前使用的比較少,Jredis和Srp則已經不再推薦使用,所以我們只討論Spring推薦使用的類庫Jedis的使用。
Spring中是通過RedisConnection接口操作Redis的,而RedisConnection則對原生的Jedis進行封裝。要獲取RedisConnection接口對象,是通過RedisConnectionFactory接口去生成的,所以第一步要配置的便是這個工廠,而配置工廠主要是配置Redis的連接池,對於連接池可以設定其最大連接數、超時時間等屬性。創建RedisConnectionFactory對象的實現的代碼如下:
package com.martin.config.chapter7;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;
/**
* @author: martin
* @date: 2019/11/16 19:35
* @description:
*/
@Configuration
public class RedisConfig {
private RedisConnectionFactory connectionFactory = null;
@Bean("redisConnectionFactory")
public RedisConnectionFactory initRedisConnectionFactory(){
if (this.connectionFactory != null){
return this.connectionFactory;
}
JedisPoolConfig poolConfig = new JedisPoolConfig();
//最大空閒數
poolConfig.setMaxIdle(30);
//最大連接數
poolConfig.setMaxTotal(50);
//最大等待毫秒數
poolConfig.setMaxWaitMillis(2000);
//創建Jedis連接工廠
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
//獲取單機Redis配置
RedisStandaloneConfiguration rsCfg = connectionFactory.getStandaloneConfiguration();
rsCfg.setHostName("192.168.0.1");
rsCfg.setPort(6379);
RedisPassword redisPassword = RedisPassword.of("12345678");
rsCfg.setPassword(redisPassword);
return connectionFactory;
}
}
這裏我們通過一個連接池的配置創建了RedisConnectionFactory,通過它就能夠創建RedisConnection接口對象。但是我們在使用一條連接時,要先從RedisConnectionFactory工廠獲取,然後在使用完成後還要自己關閉它。Spring爲了進一步簡化開發,提供了RedisTemplate。
1.2 RedisTemplate
RedisTemplate是一個強大的類,首先它會自動從RedisConnectionFactory工廠中獲取連接,然後執行對應的Redis命令,在最後還會關閉Redis連接。這些在RedisTemplate中都被封裝了,所以並不需要開發者關注Redis連接的閉合問題。 創建RedisTemplate的示例代碼如下:
@Bean("redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
然後測試它,測試代碼如下:
package com.martin.config.chapter7;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @author: martin
* @date: 2019/11/16 20:49
* @description:
*/
public class RedisTemplateTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
redisTemplate.opsForValue().set("key1", "value1");
redisTemplate.opsForHash().put("hash", "field", "hvalue");
}
}
這裏使用了Java配置文件RedisConfig來創建Spring IOC容器,然後從中獲取RedisTemplate對象,接着設置鍵值。代碼運行完成之後,我們在Redis中查詢存入的數據,查詢結果如下:
MyRedis:0>keys *key1
1) \xAC\xED\x00\x05t\x00\x04key1
我們發現,Redis存入的並不是key1這樣的字符串,這是怎麼回事呢?
Redis是一種基於字符串存儲的NoSql,而Java是基於對象的語言,對象是無法存儲到Redis中的,不過Java提供了序列化機制,只要類實現了java.io.Serializable接口,就代表類的對象能夠進行序列化,通過將類對象進行序列化就能夠得到二進制字符串,這樣Redis就可以將這些類對象以字符串進行存儲。Java也可以將那些二進制字符串通過反序列化轉爲Java對象,通過這個原理,Spring提供了序列化器接口RedisSerializer:
package org.springframework.data.redis.serializer;
import org.springframework.lang.Nullable;
public interface RedisSerializer<T> {
@Nullable
byte[] serialize(@Nullable T var1) throws SerializationException;
@Nullable
T deserialize(@Nullable byte[] var1) throws SerializationException;
}
該接口有兩個方法,這兩個方法一個是serialize,它能夠把那些可以序列化的對象轉換爲二進制字符串;另一個是deserialize,它能夠通過反序列化把二進制字符串轉換爲Java對象。StringRedisSerializer和JdkSerializationRedisSerializer是我們比較常用的兩個實現類,其中JdkSerializationRedisSerializer是RedisTemplate默認的序列化器,“key1”這個字符串就是被它序列化變爲一個比較奇怪的字符串。
RedisTemplate提供瞭如下表所示的幾個可以配置的屬性:
由於我們什麼都沒有配置,因此它會默認使用JdkSerializationRedisSerializer對對象進行序列化和反序列化。這就是我們看到複雜字符串的原因,爲了解決這個問題,我們可以修改代碼如下:
@Bean("redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//RedisTemplate會自動初始化StringRedisSerializer
RedisSerializer stringRedisSerializer = redisTemplate.getStringSerializer();
//設置字符串序列化器爲默認序列化器
redisTemplate.setDefaultSerializer(stringRedisSerializer);
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
這裏,我們將Redis的默認序列化器設置爲字符串序列化器,這樣把他們轉換出來就會採用字符串了。
MyRedis:0>get key1
value1
MyRedis:0>hget hash field
hvalue
1.3 Spring對Redis數據類型操作的封裝
Redis能夠支持7種類型的數據結構,這7種數據類型是字符串、散列、列表、集合、有序結合、計數和地理位置。爲此,Spring針對每一種數據結構的操作都提供了對應的操作接口。如下表所示:
它們都可以通過RedisTemplate得到,得到的方法非常簡單,代碼清單如下所示:
GeoOperations geoOperations = redisTemplate.opsForGeo();
HashOperations hashOperations = redisTemplate.opsForHash();
HyperLogLogOperations logLogOperations = redisTemplate.opsForHyperLogLog();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ValueOperations valueOperations = redisTemplate.opsForValue();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
這樣就可以通過各類的操作接口來操作不同的數據類型了。有時候我們需要對某一個鍵值對做連續的操作,例如有時需要連續操作一個散列數據類型或者列表多次,這時Spring爲我們提供了對應的BoundXXXOperations接口,接口如下表所示:
同樣地,RedisTemplate也對獲取它們提供了對應的方法,實例代碼如下:
BoundGeoOperations geoOperations = redisTemplate.boundGeoOps("geo");
BoundHashOperations hashOperations = redisTemplate.boundHashOps("hash");
BoundListOperations listOperations = redisTemplate.boundListOps("list");
BoundSetOperations setOperations = redisTemplate.boundSetOps("set");
BoundValueOperations valueOperations = redisTemplate.boundValueOps("string");
BoundZSetOperations zSetOperations = redisTemplate.boundZSetOps("zset");
獲取其中的操作接口後,我們就可以對某個鍵的數據進行多次操作,這樣我們就知道如何有效地通過Spring操作Redis的各種數據類型了。
1.4 SessionCallback和RedisCallback
如果我們希望在同一條Redis鏈接中,執行兩條或者多條命令,可以使用Spring爲我們提供的RedisCallback和SessionCallback兩個接口。這樣就可以避免一條一條的執行命令,對資源的浪費。
SessionCallback接口和RedisCallback接口的主要作用是讓RedisTemplate進行回調,通過它們可以在同一條連接下執行多個Redis命令。其中,SessionCallback提供了良好的封裝,因此在實際開發中應該優先使用;相對而言,RedisCallback接口比較底層,需要處理的內容也比較多,可讀性差,一般不考慮使用。使用SessionCallback的實例代碼如下:
public static void useRedisCallback(RedisTemplate redisTemplate) {
redisTemplate.execute((RedisCallback) (redisConnection) -> {
redisConnection.set("key1".getBytes(), "value1".getBytes());
redisConnection.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
return null;
});
}
執行日誌如下:
16:32:13.941 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
16:32:13.944 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'redisTemplate'
16:32:18.317 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Opening RedisConnection
16:32:18.742 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Closing Redis Connection
從日誌中我們看出,使用RedisCallback的方式能夠使得RedisTemplate使用同一條Redis連接進行回調,從而可以在同一條Redis連接下執行多個方法,避免RedisTemplate多次獲取不同的連接。
2.Spring Boot中配置和使用Redis
2.1 配置Redis
在Spring Boot中集成Redis更爲簡單,我們只需要在配置文件application.properties中加入如下代碼清單:
#配置連接池屬性
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-wait=2000
#配置Redis服務器屬性
spring.redis.port=6379
spring.redis.host=192.168.0.1
spring.redis.password=12345678
#連接超時時間,單位毫秒
spring.redis.timeout=1000
這裏我們配置了連接池和服務器的屬性,用以連接Redis服務器,這樣Spring Boot的自動裝配機制就會讀取這些配置來生成有關Redis的操作對象,這裏它會自動生成RedisConnectionFactory、RedisTemplate、StringRedisTemplate等常用的Redis對象。同時爲了修改RedisTemplate默認的序列化器,我們定義如下的代碼:
package com.martin.config.chapter7;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/16 19:35
* @description:
*/
@Component
public class RedisSerializationConfig implements InitializingBean {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void afterPropertiesSet() throws Exception {
RedisSerializer<String> redisSerializer = redisTemplate.getStringSerializer();
redisTemplate.setDefaultSerializer(redisSerializer);
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
}
}
這樣我們存儲到Redis的鍵值對就是String類型了。
2.2 操作Redis數據類型
這一節主要演示常用的Redis數據類型(字符串、散列、列表、集合和有序集合)的操作。
2.2.1 字符串和散列的操作
首先開始操作字符串和散列,這是Redis最爲常用的數據類型。實例代碼如下:
package com.martin.config.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
/**
* @author: martin
* @date: 2019/11/17 18:22
* @description:
*/
@Controller
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/stringAndHash")
@ResponseBody
public Map<String, Object> testStringAndHash() {
redisTemplate.opsForValue().set("key1", "value1");
redisTemplate.opsForValue().set("int_key", "1");
redisTemplate.opsForValue().increment("int_key", 2L);
//獲取底層Jedis連接
Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
//減1操作,這個命令RedisTemplate不支持,所以需要先獲取底層的連接再操作
jedis.decr("int_key");
Map<String, String> hash = new HashMap<>();
hash.put("field1", "value1");
hash.put("field2", "value2");
//存入一個散列的數據類型
redisTemplate.opsForHash().putAll("hash", hash);
//新增一個字段
redisTemplate.opsForHash().put("hash", "field3", "value3");
//綁定散列操作的key,這樣可以連續對同一個散列數據類型進行操作
BoundHashOperations hashOperations = redisTemplate.boundHashOps("hash");
//刪除其中的兩個字段
hashOperations.delete("field1", "field2");
//新增一個字段
hashOperations.put("field4", "value4");
Map<String, Object> result = new HashMap<>();
result.put("info", "success");
return result;
}
}
這裏需要注意的一點是我在做測試的時候,將redis連接池的配置單獨放置到文件application-redis.properties中了。爲了能夠加載到該文件,我需要在SpringBootConfigApplication中導入該文件:
@PropertySource({"classpath:application-redis.properties"})
代碼中的@Autowired注入了Spring Boot爲我們自動初始化的RedisTemplate和StringRedisTemplate對象。有一點需要注意的是,這裏進行的加一操作,因爲RedisTemplate並不能支持底層所有的Redis命令,所以這裏先獲取了原始的Redis連接的Jedis對象。
2.2.2 列表操作
列表在Redis中其實是一種鏈表結構,這就意味着查詢性能不高,而增刪節點的性能高,這是它的特性。操作實例代碼如下:
@RequestMapping("/list")
@ResponseBody
public Map<String, Object> testList() {
//鏈表從左向右的順序e,d,c,b
redisTemplate.opsForList().leftPushAll("list1", "b", "c", "d", "e");
//鏈表從左向右的順序爲f,g,h,i
redisTemplate.opsForList().rightPushAll("list2", "f", "g", "h", "i");
//綁定list2
BoundListOperations listOperations = redisTemplate.boundListOps("list2");
//從右邊彈出一個成員
System.out.println(listOperations.rightPop());
System.out.println(listOperations.index(1));
System.out.println(listOperations.size());
List<String> allElement = listOperations.range(0, listOperations.size() - 1);
System.out.println(allElement);
return (Map<String, Object>) new HashMap().put("success", true);
}
2.2.3 集合操作
集合是不允許成員重複的,它在數據結構上是一個散列表的結構,所以對於它而言是無序的。Redis還提供了交集、並集和差集的運算。實例代碼如下;
@RequestMapping("/set")
public void testSet() {
redisTemplate.opsForSet().add("set1", "a", "b", "c", "d", "e", "e");
redisTemplate.opsForSet().add("set2", "b", "c", "f", "g");
//綁定集和set1操作
BoundSetOperations setOperations = redisTemplate.boundSetOps("set1");
//添加兩個元素
setOperations.add("h", "i");
//刪除兩個元素
setOperations.remove("e", "h");
//返回所有的元素
Set<String> set1 = setOperations.members();
//元素的個數
setOperations.size();
//交集
setOperations.intersect("set2");
//差集
setOperations.diff("set2");
//求交集並保存到inter集和
setOperations.intersectAndStore("set2", "inter");
//求並集
setOperations.union("set2");
}
在一些網站中,經常會有排名,如最熱門的商品或者最大的購買買家,都是常見的場景。對於這類排名,刷新往往需要及時,也涉及到較大的統計,如果使用數據庫會很慢。爲了支持集合的排序,Redis提供了有序集合(zset),它的有序性通過在數據結構中增加一個屬性-score(分數)得以支持。Spring提供了TypedTuple接口以及默認的實現類DefaultTypedTuple來支持有序集合。實例代碼如下:
@RequestMapping("/zset")
public void testZSet() {
Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
//初始化有序集合
for (int i = 1; i < 9; i++) {
double score = i * 0.1;
ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
typedTupleSet.add(typedTuple);
}
//往有序集合插入元素
redisTemplate.opsForZSet().add("zset1",typedTupleSet);
//綁定zset有序集合操作
BoundZSetOperations<String,String> zSetOperations = redisTemplate.boundZSetOps("zset1");
//增加一個元素
zSetOperations.add("value10",0.26);
//獲取有序集合
System.out.println(zSetOperations.range(1,6));
System.out.println(zSetOperations.rangeByScore(0.2,0.6));
//獲取大於value3的元素
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
range.gt("value3");
//求分數
System.out.println(zSetOperations.score("value8"));
}
2.3 Redis的特殊用法
Redis除了操作數據類型以外,還能支持事務、流水線、發佈訂閱和Lua腳本等功能。
2.3.1 Redis事務
Redis中使用事務通常的命令組合是watch.......multi.......exec,也就是要在一個Redis連接中執行多個命令,其中watch命令監控Redis的一些鍵;multi命令是開始事務,開始事務以後,該客戶端的命令不會馬上執行,而是存放在一個隊列中;exe命令的意義在於執行事務,只是它在隊列命令執行前會判斷被watch監控的Redis的鍵的數據是否發生過變化,如果已經發生了變化,那麼Redis就會取消事務,否則就會執行事務,Redis執行事務時,要麼全部執行,要麼全部不執行,而且不會被其他客戶端打斷,這樣就保證了Redis事務下數據一致性。
2.3.2 Redis流水線
在默認情況下,Redis客戶端是一條條命令發送給Redis客戶端的,這樣顯然性能不高。在Redis中,使用流水線技術,可以一次性地發送多個命令去執行,大幅度地提升Redis的性能。
2.3.3 Redis發佈訂閱
發佈訂閱是消息的一種常用模式。首先是 Redis 提供一個渠道,讓消息能夠發送到這個渠道上 ,而多個系統可以監聽這個渠道, 如短信、微信和郵件系統都可以監聽這個渠道,當一條消息發送到渠道,渠道就會通知它的監聽者,這樣短信、微信和郵件系統就能夠得到這個渠道給它們的消息了,這些監聽者會根據自己的需要去處理這個消息,於是我們就可以得到各種各樣的通知了 。
Redis消息監聽器代碼如下:
package com.martin.config.redis;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2020/1/21
*/
@Component
public class RedisMsgListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
//消息體
String body = new String(message.getBody());
//渠道名稱
String topic = new String(pattern);
System.out.println(body);
System.out.println(topic);
}
}
2.3.4 使用Lua腳本
Redis中有很多的命令,但是嚴格來說 Redis提供的計算能力還是比較有限的 。爲了增強 Redis的計算能力,Redis在2.6版本後提供了 Lua 腳本的支持,而且執行 Lua 腳本在 Redis 中還具備原子性,所以在需要保證數據一致性的高併發環境中,我們也可以使用Redis的Lua語言來保證數據的一致性,且 Lua 腳本具備更加強大的運算功能,在高併發需要保證數據一致性時,Lua 腳本方案比使用Redis自身提供的事務要更好一些 。
在 Redis 中有兩種運行 Lua 的方法,一種是直接發送Lua到Redis服務器去執行,另一種是先把Lua發送給Redis,Redis會對Lua腳本進行緩存,然後返回一個SHA 1的32位編碼回來,之後只需要發送SHA 1和相關參數給Redis便可以執行了。這裏需要解釋的是爲什麼會存在通過32位編碼執行的方法。如果Lua腳本很長,那麼就需要通過網絡傳遞腳本給Redis去執行了,而現實的情況是網絡的傳遞速度往往跟不上 Redis的執行速度,所以網絡就會成爲Redis執行的瓶頸。如果只是傳遞32位編碼和參數,那麼需要傳遞的消息就少了許多,這樣就可以極大地減少網絡傳輸的內容,從而提高系統的性能。
爲了支持Redis的Lua腳本,Spring提供了RedisScript接口,與此同時也有一個DefaultRedisScript實現類。