6. SpringBoot整合Redis
Redis是NoSQL類型的數據庫,我們也常稱爲內存型數據庫類型.在SpringBoot中使用Redis非常簡單.
6.1 添加包依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
添加依賴後其實就直接可以使用redis了(SpringBoot提供了默認配置,也可以自己配置)
6.2 配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.timeout=1000
spring.redis.lettuce.pool.max-active=10
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-wait=1000
6.3 編寫Controller測試
@Controller
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/set")
@ResponseBody
public String redisStringSet(String key, String value) {
ValueOperations ops = redisTemplate.opsForValue();
ops.set(key, value);
return "OK";
}
}
當執行了上面的代碼後,使用Redis客戶端可查看到redis中多出了一個記錄.但是該記錄是編碼後的,不方面我們查看.這就需要我們進行相應的配置了.讓redis直接以字符串的方式顯示我們插入的內容.
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 自定義的string序列化器和fastjson序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// jackson 序列化器
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// fastjson 序列化器
// GenericFastJsonRedisSerializer jsonRedisSerializer = new GenericFastJsonRedisSerializer();
// kv 序列化
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jsonRedisSerializer);
// hash 序列化
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
RedisTemplate是我們操作數據庫的類,Spring也提供了StringRedisTemplate和HashKeyRedisTemplate分別用於處理內容序列化顯示的問題。添加完上面的配置代碼後,重新執行redis插入,就可以看到redis顯示了真實的字符串內容。
Redis提供7種基本數據類型,可通過RedisTemplate提供的方法執行。
/獲取地理位置操作接口
redisTemplate.opsForGeo();
/獲取散列操作接口
redisTemplate.opsForHash();
//獲取基數操作接口
redisTemplate.opsForHyperLogLog();
/獲取列表操作接口
redisTemplate.opsForlist();
/獲取集合操作接口
redisTemplate.opsForset();
//獲取字符串操作接口
redisTemplate.opsForvalue();
//獲取有序集合操作接口
redisTemplate.psForzset();
一個Redis連接中只執行一個redis命令。Redis也提供了方法讓我們可以在一個連接中執行多個命令。就是SessionCallback和RedisCallback接口。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-RllYlt4s-1578452712845)(C:\Users\mxlei\AppData\Roaming\Typora\typora-user-images\image-20200107151716183.png)]
Redis的各種數據類型的使用這裏不做介紹。
6.4 Redis高級用法
6.4.1 redis事務
@RequestMapping("/transaction")
@ResponseBody
public List redisTransaction() {
//key1設置爲字符串類型
redisTemplate.opsForValue().set("key1", "value1");
//Redis事務
List list = (List) redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
//設置要監控的KEY
operations.watch("key1");
//開啓事務,在exec命令執行之前,全部都知識加入隊列
operations.multi();
operations.opsForValue().set("key2", "value2");
// operations.opsForValue().increment("key1",1); //①
//取得的值爲null,因爲redis只是把命令加入隊列
Object value2 = operations.opsForValue().get("key2");
System.out.println("取得的值爲null,因爲redis只是把命令加入隊列 value2 = " + value2);
operations.opsForValue().set("key3", "value3");
Object value3 = operations.opsForValue().get("key3");
System.out.println("取得的值爲null,因爲redis只是把命令加入隊列 value3 = " + value3);
//執行exec命令,將判斷key1是否在監控後被修改過,如果是則不執行事務,否則就執行事務
return operations.exec(); //②
}
});
System.out.println(list);
return list;
}
爲了揭示 Redis事務的特性,我們對這段代碼做以下兩種測試先在 Redis客戶端清空key2和key3兩個鍵的數據,然後在②處設置斷點,在調試的環境下讓請求達到斷點,此時在 Redis上修改keyl的值,然後再跳過斷點,在請求完成後在 Redis上查詢key2和key3值,可以發現key2、key3返回的值都爲空(ni),因爲程序中先使得 Redis的 watch命令監控了keyl的值,而後的 multi讓之後的命令進入隊列,而在exec方法運行前我們修改了keyl,根據 Redis事務的規則,它在exec方法後會探測keyl是否被修改過,如果沒有則會執行事務,否則就取消事務,所以key2和key3沒有被保存到Redis服務器中迷續把key2和key3兩個值清空,把①處的註釋取消,讓代碼可以運行,因爲keyl是一個字符串,所以這裏的代碼是對字符串加一,這顯然是不能運算的。同樣地,我們運行這段代碼後,可以看到服務器拋出了異常,然後我們去 Redis服務器查詢key2和key3,可以看到它們已經有了值。注意,這就是 Redis事務和數據庫事務的不一樣,對於 Redis事務是先讓命令進入隊列,所以一開始它並沒有檢測這個加一命令是否能夠成功,只有在exec命令執行的時候,才能發現錯誤,對於出錯的命令 Redis只是報出錯誤,而錯誤後面的命令依舊被執行,所以key2和key3都存在數據,這就是 Redis事務的特點,也是使用Reds事務需要特別注意的地方。爲了克服這個問題,一般我們要在執行 Redis事務前,嚴格地檢查數據,以避免這樣的情況發生。
6.4.2 Redis流水線
在默認的情況下, Redis客戶端是一條條命令發送給 Redis服務器的,這樣顯然性能不高。在關係數據庫中我們可以使用批量,也就是隻有需要執行SQL時,才一次性地發送所有的SQL去執行,這樣性能就提高了許多。對於 Redis也是可以的,這便是流水線( pipline)技術,在很多情況下並不是 Redis性能不佳,而是網絡傳輸的速度造成瓶頸,使用流水線後就可以大幅度地在需要執行很多命令時提升 Redis的性能。
下面使用Redis流水線技術測試10萬次讀寫功能
@RequestMapping("/pipeline")
@ResponseBody
public Long pipeline(){
long start = System.currentTimeMillis();
List list = (List)redisTemplate.executePipelined(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
for(int i=0;i<=100000;i++){
redisOperations.opsForValue().set("pipeline_"+i,"value_"+i);
String value = (String) redisOperations.opsForValue().get("pipeline_"+i);
if(i == 100000){
System.out.println("命令只是進入隊列,所有值爲空 = "+value);
}
}
return null;
}
});
long end = System.currentTimeMillis();
long cost = end - start;
System.out.println("耗時:"+cost+" 毫秒");
return cost;
}
爲了測試性能,這裏記錄了開始執行時間和結束執行時間,並且打出了耗時。在我的測試中,這10萬次讀寫基本在300600ms,大約平均值在400500ms,也就是不到s就能執行10萬次讀和寫命令,這個速度還是十分快的。在使用非流水線的情況下,我的測試大約每秒只能執行2萬~3萬條命令,可見使用流水線後可以提升大約10倍的速度,它十分適合大數據量的執行。
6.4.3 Redis發佈訂閱
發佈訂閱是消息的一種常用模式。例如,在企業分配任務之後,可以通過郵件、短信或者微信通知到相關的責任人,這就是一種典型的發佈訂閱模式。首先是 Redis提供一個渠道,讓消息能夠發送到這個渠道上,而多個系統可以監聽這個渠道,如短信、微信和郵件系統都可以監聽這個渠道,當一條消息發送到渠道,渠道就會通知它的監聽者,這樣短信、微信和郵件系統就能夠得到這個渠道給它們的消息了,這些監聽者會根據自己的需要去處理這個消息,於是我們就可以得到各種各樣的通知了。
爲了接收 Redis渠道發送過來的消息,我們先定義一個消息監聽器( Messagelistener)
@Configuration
public class RedisMessageListenerConfig {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisConnectionFactory connectionFactory;
@Autowired
private RedisMessageListener redisMessageListener;
private ThreadPoolTaskExecutor taskExecutor;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setTaskExecutor(threadPoolTaskExecutor());
Topic topic = new ChannelTopic("topic1");
container.addMessageListener(redisMessageListener, topic);
return container;
}
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
if(taskExecutor != null){
return taskExecutor;
}
taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
return taskExecutor;
}
}
這裏 RedisTemplate和 RedisConnection Factory對象都是 Spring Boot自動創建的,所以這裏只是把它們注入進來,只需要使用@ Autowired註解即可。然後定義了一個任務池,並設置了任務池大小爲20,這樣它將可以運行線程,並進行阻塞,等待 Redis消息的傳入。接着再定義了一個 Redis消息監聽的容器 RedisMessageListenerContainer,並且往容器設置了 Redis連接工廠和指定運行消息的線程池,定義了接收“ topicI”渠道的消息,這樣系統就可以監聽 Redis關於“ topic1”渠道的消息了。啓用 Spring Boot項目後,在 Redis的客戶端輸入命令:
publish topic msg
在 Spring中,我們也可以使用 Redis Template來發送消息,例如:
redisTemplate.convertAndSend(channel, message);
其中, channel代表渠道, message代表消息,這樣就能夠得到 Redis發送過來的消息了。
6.4.4 使用Lua腳本
Redis中有很多的命令,但是嚴格來說 Redis提供的計算能力還是比較有限的。爲了增強 Redis的計算能力, Redis在2.6版本後提供了Lua腳本的支持,而且執行Lua腳本在 Redis中還具備原子性,所以在需要保證數據一致性的高併發環境中,我們也可以使用 Redis的Lua語言來保證數據的致性,且Lua腳本具備更加強大的運算功能,在高併發需要保證數據一致性時,Lua腳本方案比使用Redis自身提供的事務要更好一些。
在 Redis中有兩種運行Lua的方法,一種是直接發送Lua到 Redis服務器去執行,另一種是先把Lua發送給 Redis, Redis會對Lua腳本進行緩存,然後返回一個SHA1的32位編碼回來,之後只需要發送SHA1和相關參數給 Redis便可以執行了。這裏需要解釋的是爲什麼會存在通過32位編碼執行的方法。如果Lua腳本很長,那麼就需要通過網絡傳遞腳本給 Redis去執行了,而現實的情況是網絡的傳遞速度往往跟不上 Redis的執行速度,所以網絡就會成爲 Redis執行的瓶頸。如果只是傳遞32位編碼和參數,那麼需要傳遞的消息就少了許多,這樣就可以極大地減少網絡傳輸的內容,從而提供系統性能。
爲了支持 Redis的Lua腳本, Spring提供了 Redis Script接口,與此同時也有一個 DefaultRedis Script
實現類。讓我們先來看看 Redis script接口的源碼
public interface RedisScript<T> {
//獲取腳本的SHA1
String getSha1();
//獲取腳本的返回值類型
@Nullable
Class<T> getResultType();
//獲取腳本的字符串內容
String getScriptAsString();
}
這裏 Spring會將Lua腳本發送到 Redis服務器進行緩存,而此時 Redis服務器會返回一個32位的SHA編碼,這時候通過 geiSha方法就可以得到 Redis返回的這個編碼了; getResult Type方法是獲取Lua腳本返回的Java類型; getScriptAs String是返回腳本的字符串,以便我們觀看腳本。下面我們採用 Redis Script接口執行一個十分簡單的Lua腳本,這個腳本只是簡單地返回一個字符串
這裏的代碼,首先Lua只是定義了一個簡單的字符串,然後就返回了,而返回類型則定義爲字符串。這裏必須定義返回類型,否則對於 Spring不會把腳本執行的結果返回。接着獲取了由RedisTemplate自動創建的字符串序列化器,而後使用 Redis Template的 execute方法執行了腳本。在Redis Template中, execute方法執行腳本的方法有兩種,其定義如下:
6.5 使用Spring緩存註解操作Redis
略