Spring Boot 專案實戰(四)整合 Redis
上篇介紹了介面文件工具 Swagger 及專案監控工具 JavaMelody 的整合過程,使專案更加健壯。在 JAVA Web 專案某些場景中,我們需要用快取解決如熱點資料訪問的效能問題,業界常用的中介軟體如 Memcached 、 Redis 等。相比 Memcached ,Redis 支援更豐富的資料結構。本篇將主要介紹在 Spring Boot 中整合 Redis 的過程。
二、整合 Redis
在 Spring Boot 中使用 Redis 有兩種方式:
- 基於 RedisTemplate 類,該類是 Spring Data 提供的工具,可以直接注入使用。
- 基於 Jedis,Jedis 是 Redis 官方推薦的面向 JAVA 的客戶端。
本文將介紹第一種使用方式。
2.1 引入依賴包
其實 Spring Boot 提供的父工程中已經包含了所依賴的 Redis jar 包,我們只需在相應模組引入即可。第一篇我們已經提到過 demo-common 層是公用元件層,那麼 Redis 相關的宣告及配置應該在該層定義。於是乎在 demo-common 層的 pom 檔案中引入 Redis 的依賴包。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.2 RedisTemplate 的自動配置
其實我們現在就可以在專案中注入 RedisTemplate 並使用了,至於原因,首先看下「 RedisAutoConfiguration 」類的原始碼:
@Configuration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }
從原始碼可以看出,Spring Boot 會自動幫我們生成了一個 RedisTemplate 及一個 StringRedisTemplate ,但是這個 RedisTemplate 的泛型是 <Object, Object> ,如果我們直接使用就需要處理各種型別轉換。所以為了方便使用,我們需要自定義一個泛型為 <String, Object> 的 RedisTemplate 。
而 @ConditionalOnMissingBean 註解的作用是在當前 Spring 上下文中不存在某個物件時,才會自動例項化一個 Bean 。因此我們可以自定義 RedisTemplate 從而替代預設的。
2.2 自定義 Redis 配置類
Spring Data 提供了若干個 Serializer ,主要包括:
- JdkSerializationRedisSerializer — 使用 JAVA 自帶的序列化機制將物件序列化為一個字串
- OxmSerializer — 將物件序列化為 XML 字串
- Jackson2JsonRedisSerializer — 將物件序列化為 JSON 字串
其中 RedisTemplate 預設的序列化方式是 Jdk ,雖然是效率比較高但是序列化結果的字串是最長的。而 JSON 由於其資料格式的緊湊型,序列化結果的字串是最小的,即佔用的記憶體最小。所以我們選擇用 Jackson 替代預設的 Jdk 方式。
① 首先在專案父 pom 檔案中定義 Jackson 的版本號且宣告 Jackson 依賴。
<properties> ...省略其餘部分... <jackson.version>2.9.4</jackson.version> </properties>
<dependencyManagement> <dependencies> ...省略其餘部分... <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> </dependencyManagement>
② 其次在 demo-common 層的 pom 檔案中新增上述 Jackson 依賴。
<dependencies> ...省略其餘部分... <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> </dependencies>
③ 最後在 demo-common 層建立 com.example.demo.common 包,新增 Redis 目錄並在其中建立 RedisConfig 配置類。
package com.example.demo.common.redis; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 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.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author linjian * @date 2019/3/2 */ @Configuration public class RedisConfig { @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); objectMapper.registerModule(new JavaTimeModule()); objectMapper.findAndRegisterModules(); objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // 使用 Jackson2JsonRedisSerialize 替換預設序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>(); redisTemplate.setConnectionFactory(factory); // key 採用 String 的序列化方式 redisTemplate.setKeySerializer(stringRedisSerializer); // hash 的 key 也採用 String 的序列化方式 redisTemplate.setHashKeySerializer(stringRedisSerializer); // value 序列化方式採用 jackson redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // hash 的 value 序列化方式採用 jackson redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
2.3 自定義 Redis 工具類
直接使用 RedisTemplate 操作 Redis 需要很多額外的程式碼,最好封裝成一個工具類,使用時直接注入。
① 定義一個常用的快取時間常量類
package com.example.demo.common.redis; /** * @author linjian * @date 2019/3/2 */ public class CacheTime { /** * 快取時效 5秒鐘 */ public static int CACHE_EXP_FIVE_SECONDS = 5; /** * 快取時效 1分鐘 */ public static int CACHE_EXP_MINUTE = 60; /** * 快取時效 5分鐘 */ public static int CACHE_EXP_FIVE_MINUTES = 60 * 5; /** * 快取時效 10分鐘 */ public static int CACHE_EXP_TEN_MINUTES = 60 * 10; /** * 快取時效 15分鐘 */ public static int CACHE_EXP_QUARTER_MINUTES = 60 * 15; /** * 快取時效 60分鐘 */ public static int CACHE_EXP_HOUR = 60 * 60; /** * 快取時效 12小時 */ public static int CACHE_EXP_HALF_DAY = 12 * 60 * 60; /** * 快取時效 1天 */ public static int CACHE_EXP_DAY = 3600 * 24; /** * 快取時效 1周 */ public static int CACHE_EXP_WEEK = 3600 * 24 * 7; /** * 快取時效 1月 */ public static int CACHE_EXP_MONTH = 3600 * 24 * 30 * 7; /** * 快取時效 永久 */ public static int CACHE_EXP_FOREVER = 0; }
② 定義工具類
package com.example.demo.common.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @author linjian * @date 2019/3/2 */ @Component public class RedisClient { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 指定快取失效時間 * * @param key鍵 * @param time 時間(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根據key 獲取剩餘過期時間 * * @param key 鍵 不能為null * @return 時間(秒) 返回0代表為永久有效 */ public long ttl(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判斷key是否存在 * * @param key 鍵 * @return true 存在 false不存在 */ public boolean exists(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除快取 * * @param key 可以傳一個值 或多個 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } /** * 模糊匹配批量刪除 * * @param pattern 匹配的字首 */ public void deleteByPattern(String pattern) { Set<String> keys = redisTemplate.keys(pattern); if (!CollectionUtils.isEmpty(keys)) { redisTemplate.delete(keys); } } /** * 設定指定 key 的值 * * @param key鍵 * @param value 值 * @param time時間(秒) time要大於0 如果time小於等於0 將設定無限期 * @return true成功 false 失敗 */ public boolean set(String key, Object value, long time) { try { if (time == CacheTime.CACHE_EXP_FOREVER) { redisTemplate.opsForValue().set(key, value); } else { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 獲取指定 key 的值 * * @param key 鍵 * @return 值 */ @SuppressWarnings("unchecked") public <T> T get(String key) { return key == null ? null : (T) redisTemplate.opsForValue().get(key); } /** * 將 key 中儲存的數字值遞增 * * @param key鍵 * @param delta 要增加幾(大於0) * @return */ public long incr(String key, long delta) { if (delta <= 0) { throw new IllegalArgumentException("遞增因子必須大於0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 將 key 中儲存的數字值遞減 * * @param key鍵 * @param delta 要減少幾(小於0) * @return */ public long decr(String key, long delta) { if (delta <= 0) { throw new IllegalArgumentException("遞減因子必須大於0"); } return redisTemplate.opsForValue().increment(key, -delta); } /** * 將雜湊表 key 中的欄位 field 的值設為 value * * @param key鍵 * @param field 欄位 * @param value 值 * @param time時間(秒) 注意:如果已存在的hash表有時間,這裡將會替換原有的時間 * @return true 成功 false失敗 */ public boolean hset(String key, String field, Object value, long time) { try { redisTemplate.opsForHash().put(key, field, value); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 同時將多個 field-value (域-值)對設定到雜湊表 key 中 * * @param key鍵 * @param map對應多個鍵值 * @param time 時間(秒) * @return true成功 false失敗 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除一個或多個雜湊表字段 * * @param key鍵 * @param field 欄位 可以多個 */ public void hdel(String key, Object... field) { redisTemplate.opsForHash().delete(key, field); } /** * 獲取儲存在雜湊表中指定欄位的值 * * @param key鍵 * @param field 欄位 * @return 值 */ public <T> T hget(String key, String field) { return (T) redisTemplate.opsForHash().get(key, field); } /** * 獲取在雜湊表中指定 key 的所有欄位和值 * * @param key 鍵 * @return 對應的多個鍵值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * 檢視雜湊表 key 中,指定的欄位是否存在 * * @param key鍵 * @param field 欄位 * @return true 存在 false不存在 */ public boolean hexists(String key, String field) { return redisTemplate.opsForHash().hasKey(key, field); } /** * 獲取雜湊表中欄位的數量 * * @param key 鍵 * @return 欄位數量 */ public long hlen(String key) { try { return redisTemplate.opsForHash().size(key); } catch (Exception e) { e.printStackTrace(); return 0L; } } /** * 向集合新增一個或多個成員 * * @param key鍵 * @param time時間(秒) * @param values 成員 可以是多個 * @return 成功個數 */ public long sadd(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return count; } catch (Exception e) { e.printStackTrace(); return 0L; } } /** * 移除集合中一個或多個成員 * * @param key鍵 * @param values 成員 可以是多個 * @return 移除的個數 */ public long srem(String key, Object... values) { try { return redisTemplate.opsForSet().remove(key, values); } catch (Exception e) { e.printStackTrace(); return 0L; } } /** * 返回集合中的所有成員 * * @param key 鍵 * @return 成員列表 */ public <T> Set<T> smembers(String key) { try { return (Set<T>) redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 判斷 member 元素是否是集合 key 的成員 * * @param key鍵 * @param member 成員 * @return true 存在 false不存在 */ public boolean sismember(String key, Object member) { try { return redisTemplate.opsForSet().isMember(key, member); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 獲取集合的成員數 * * @param key 鍵 * @return 成員數 */ public long slen(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0L; } } /** * 在列表頭部新增一個值 * * @param key鍵 * @param value 值 * @param time時間(秒) * @return boolean */ public boolean lpush(String key, Object value, long time) { try { redisTemplate.opsForList().leftPush(key, value); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 在列表頭部新增多個值 * * @param key鍵 * @param values 值 * @param time時間(秒) * @return boolean */ public boolean lpush(String key, List<Object> values, long time) { try { redisTemplate.opsForList().leftPushAll(key, values); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 在列表尾部新增一個值 * * @param key鍵 * @param value 值 * @param time時間(秒) * @return boolean */ public boolean rpush(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 在列表尾部新增多個值 * * @param key鍵 * @param values 值 * @param time時間(秒) * @return boolean */ public boolean rpush(String key, List<Object> values, long time) { try { redisTemplate.opsForList().rightPushAll(key, values); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除列表元素 * * @param key鍵 * @param count 移除多少個 * @param value 值 * @return 移除的個數 */ public long lrem(String key, long count, Object value) { try { return redisTemplate.opsForList().remove(key, count, value); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通過索引設定列表元素的值 * * @param key鍵 * @param index 索引 * @param value 值 * @return boolean */ public boolean lset(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 獲取列表指定範圍內的元素 * * @param key鍵 * @param start 開始 * @param end結束 0 到 -1代表所有值 * @return 元素列表 */ @SuppressWarnings("unchecked") public <T> List<T> lrange(String key, long start, long end) { try { return (List<T>) redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 通過索引獲取列表中的元素 * * @param key鍵 * @param index 索引 index>=0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推 * @return */ public Object lindex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 獲取列表長度 * * @param key 鍵 * @return 列表長度 */ public long llen(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0L; } } /** * 向有序集合新增一個成員,或者更新已存在成員的分數 * * @param key鍵 * @param time時間(秒) * @param member 成員 * @param score分數 * @return */ public boolean zadd(String key, long time, Object member, double score) { try { boolean ret = redisTemplate.opsForZSet().add(key, member, score); if (time != CacheTime.CACHE_EXP_FOREVER) { expire(key, time); } return ret; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除有序集合中的一個或多個成員 * * @param key鍵 * @param values 值 可以是多個 * @return 移除的個數 */ public long zrem(String key, Object... values) { try { return redisTemplate.opsForZSet().remove(key, values); } catch (Exception e) { e.printStackTrace(); return 0L; } } /** * 通過索引區間返回有序集合成指定區間內的成員 分數從低到高 * * @param key鍵 * @param start 開始 * @param end結束 0 到 -1代表所有值 * @return 成員集合 */ public Set<Object> zrange(String key, long start, long end) { try { return redisTemplate.opsForZSet().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 通過索引區間返回有序集合成指定區間內的成員 分數從高到低 * * @param key鍵 * @param start 開始 * @param end結束 0 到 -1代表所有值 * @return 成員集合 */ public Set<Object> zrevrange(String key, long start, long end) { try { return redisTemplate.opsForZSet().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 返回有序集合中某個成員的分數值 * * @param key鍵 * @param member 成員 * @return 分數值 */ public double zscore(String key, Object member) { try { return redisTemplate.opsForZSet().score(key, member); } catch (Exception e) { e.printStackTrace(); return 0.0; } } /** * 判斷有序集合中某個成員是否存在 * * @param key鍵 * @param member 成員 * @return true 存在 false不存在 */ public boolean zexist(String key, Object member) { try { return null != redisTemplate.opsForZSet().score(key, member); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 獲取有序集合的成員數 * * @param key 鍵 * @return 成員數 */ public long zlen(String key) { try { return redisTemplate.opsForZSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0L; } } }
2.4 新增 Redis 常用配置項
在 application.properties 檔案中的新增 Redis 相關的配置項:
# 資料庫索引(預設為0) spring.redis.database = 1 # 伺服器地址 spring.redis.host = 127.0.0.1 # 伺服器連線埠 spring.redis.port = 6379 # 伺服器連線密碼(預設為空) spring.redis.password = # 連線池最大阻塞等待時間(使用負值表示沒有限制) spring.redis.pool.max-wait = -1 # 連線超時時間(毫秒) spring.redis.timeout = 3000 # 連線池最大連線數 spring.redis.jedis.pool.max-active = 8 # 連線池中的最大空閒連線 spring.redis.jedis.pool.max-idle = 8 # 連線池中的最小空閒連線 spring.redis.jedis.pool.min-idle = 1
2.5 Redis 快取測試
① 首先在 DemoService 中注入 RedisClient ,修改 test 方法將 user 物件以 user:1 為鍵存放到 Redis 中。
Redis 開發規範: https://yq.aliyun.com/articles/531067
package com.example.demo.biz.service.impl; import com.example.demo.biz.service.DemoService; import com.example.demo.common.redis.CacheTime; import com.example.demo.common.redis.RedisClient; import com.example.demo.dao.entity.UserDO; import com.example.demo.dao.mapper.business.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author linjian * @date 2019/1/15 */ @Service public class DemoServiceImpl implements DemoService { @Autowired private UserMapper userMapper; @Autowired private RedisClient redisClient; @Override public String test() { UserDO user = userMapper.selectById(1); redisClient.set("user:1", user, CacheTime.CACHE_EXP_FIVE_MINUTES); return user.toString(); } }
② 之後使用 Redis Desktop Manager 客戶端連線 Redis 伺服器,選擇資料庫「 1 」,檢視剛存放的快取。

三、結語
至此 Spring Boot 整合 Redis 的具體步驟介紹完畢,我們自定義了 Redis 的序列化方式,並通過一個簡單的例子測試了 Redis 的可用性,相關程式碼已同步至 GitHub 。