一. Spring Boot整合Redis實現
1. Redis簡介
Redis是一個緩存,消息中間件及具有豐富特性的鍵值存儲系統。Spring Boot爲Redis的客戶端Jedis提供了自動配置實現,Spring Data Redis提供了在它之上的抽象,spring-boot-starter-redis'Starter'爲我們提供了必要的依賴。
2. 環境配置
- Springboot 2.2.5;
- Redis 3.2.x;
- Redis可視化工具Redis Desktop Manager
3. 創建web項目
我們按照之前的經驗,創建一個web程序,並將之改造成Spring Boot項目,具體過程略。
4. 添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用默認的Lettuce時,若配置spring.redis.lettuce.pool則必須配置該依賴-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<!--解決jdk1.8中新時間API的序列化時出現com.fasterxml.jackson.databind.exc.InvalidDefinitionException的問題-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
5. 添加yml配置文件
SpringBoot集成Redis主要是使用RedisTemplate類進行操作,但是在SpringBoot2.0以後,底層默認訪問的不再是Jedis而是lettuce。
5.1 jedis客戶端和lettuce客戶端的區別
- jedis採用的是直連redis server,在多線程之間公用一個jedis實例,是線程不安全的。想要避免線程不安全,可以使用連接池pool,這樣每個線程單獨使用一個jedis實例。但是線程過多時,帶來的是redis server的負載較大,有點類似BIO模式。
- lettuce採用netty連接redis server,實例在多個線程間共享,不存在線程不安全的情況,這樣可以減少線程數量。當然在特殊情況下,lettuce也可以使用多個實例,有點類似NIO模式。
5.2 yml配置文件
spring:
datasource:
username: root
password: syc
url: jdbc:mysql://localhost:3306/db4?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: update
redis:
host: 127.0.0.1
port: 6379
password:
timeout: 3600ms #超時時間
lettuce: #若是在配置中配置了lettuce的pool屬性,那麼必須在pom.xml文件中加入commons-pool2的依賴。
pool:
max-active: 8 #最大連接數
max-idle: 8 #最大空閒連接 默認8
max-wait: -1ms #默認-1 最大連接阻塞等待時間
min-idle: 0 #最小空閒連接
# jedis:
# pool:
# max-active: 8 #最大連接數
# max-idle: 8 #最大空閒連接 默認8
# max-wait: -1ms #默認-1 最大連接阻塞等待時間
# min-idle: 0 #最小空閒連接
6. 創建RedisConfig配置類
6.1 RedisTemplate自動裝配
在SpringBoot中,已經自動幫我們在容器中生成了一個RedisTemplate和一個StringRedisTemplate。
下面是SpringBoot中關於RedisTemplate自動裝配的源碼:
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class 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;
}
}
從源碼中可以看出,我們開發時會存在2個問題:
- RdisTemplate的泛型是,我們在進行緩存時寫代碼不是很方便,因爲一般我們的key是String類型,所以我們需要一個的泛型。
- RedisTemplate沒有設置數據存儲在Redis時,Key和Value的序列化方式(採用默認的JDK序列化方式)。
那麼如何解決上述兩個問題呢?
@ConditionalOnMissing註解:如果Spring容器中已經定義了id爲redisTemplate的Bean,
那麼自動裝配的RedisTemplate不會實例化。
因此我們可以寫一個配置類,配置Redisemplate。
若未自定義RedisTemplate,默認會對key進行jdk序列化。
6.2 RedisSerializer序列化器
當我們利用StringRedisSerializer,Jackson2JsonRedisSerializer和JdkSerializationRedisSerializer進行序列化時,對同一個數據進行序列化前後的結果如下表:
本案例中,我們對於Key採用stringRedisSerializer;而對於Value我們採用jackson2JsonRedisSerializer的序列化方式。
ObjectMapper是Jackson操作的核心,Jackson所有的json操作都是在ObjectMapper中實現的。
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
設置所有訪問權限以及所有的實際類型都可序列化和反序列化
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson的ObjectMapper.DefaultTyping.NON_FINAL屬性的作用。
6.3 Jackson序列化特性
在JDK1.8中的時間類,採用了一套了新的API,但是在反序列化中,會出現異常。
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of java.time.LocalDate (no Creators, like default construct, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)
在SpringBoot中的解決方案:
在MAVEN中加入jackson-datatype-jsr310依賴。
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
配置Configuration中的ObjectMapper。
@Bean
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
6.4 自定義RedisTemplate
package com.yyg.boot.config;
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.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
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.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @Author 一一哥Sun
* @Date Created in 2020/4/8
* @Description Description
*/
@Configuration
public class RedisConfig {
@Value("${spring.redis.timeout}")
private Duration timeToLive = Duration.ZERO;
/**
* 由於原生的redis自動裝配,在存儲key和value時,沒有設置序列化方式,故自己創建redisTemplate實例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置連接工廠
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修飾符範圍,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化輸入的類型,類必須是非final修飾的,final修飾的類,比如String,Integer等會跑出異常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 值採用json序列化
template.setValueSerializer(jacksonSeial);
//使用StringRedisSerializer來序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 設置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
/**
* 解決jdk1.8中新時間API的序列化時出現com.fasterxml.jackson.databind.exc.InvalidDefinitionException的問題
*/
@Bean
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解決查詢緩存轉換異常的問題
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解決亂碼的問題)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(timeToLive)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
7. 創建SpringContextUtil工具類
package com.yyg.boot.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
/**
* @Author 一一哥Sun
* @Date Created in 2020/4/8
* @Description Description
*/
@Service
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.context = applicationContext;
}
public static <T> T getBean(String name, Class<T> requiredType) {
return context.getBean(name, requiredType);
}
}
8. 創建RedisUtil工具類
RedisTemplate模板類可以對Redis進行添加,刪除,設置緩存過期時間等設置。
RedisTemplate中主要的API是:
opsForValue()集合使用說明
- 1). set(K key,V value) 新建緩存
redisTemplate.opsForValue().set("key","value"); - 2). get(Object key) 獲取緩存
redisTemplate.opsForValue().get("key");
package com.yyg.boot.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.concurrent.TimeUnit;
/**
* @Author 一一哥Sun
* @Date Created in 2020/4/8
* @Description Description
*/
@Slf4j
public class RedisUtil {
//@Autowired
//private static RedisTemplate<String, Object> redisTemplate;
private static final RedisTemplate<String, Object> redisTemplate = SpringContextUtil.getBean("redisTemplate", RedisTemplate.class);
/**********************************************************************************
* redis-公共操作
**********************************************************************************/
/**
* 指定緩存失效時間
*
* @param key 鍵
* @param time 時間(秒)
* @return
*/
public static boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("【redis:指定緩存失效時間-異常】", e);
return false;
}
}
/**
* 根據key 獲取過期時間
*
* @param key 鍵 不能爲null
* @return 時間(秒) 返回0代表爲永久有效;如果該key已經過期,將返回"-2";
*/
public static long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判斷key是否存在
*
* @param key 鍵
* @return true 存在 false不存在
*/
public static boolean exists(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error("【redis:判斷{}是否存在-異常】", key, e);
return false;
}
}
/**********************************************************************************
* redis-String類型的操作
**********************************************************************************/
/**
* 普通緩存放入
*
* @param key 鍵
* @param value 值
* @return true成功 false失敗
*/
public static boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("【redis:普通緩存放入-異常】", e);
return false;
}
}
/**
* 普通緩存放入並設置時間
*
* @param key 鍵
* @param value 值
* @param time 時間(秒) time要大於0 如果time小於等於0 將設置無限期
* @return true成功 false 失敗
*/
public static boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error("【redis:普通緩存放入並設置時間-異常】", e);
return false;
}
}
/**
* 遞增
*
* @param key 鍵
* @param delta 要增加幾(大於0)
* @return
*/
public static long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("遞增因子必須大於0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 遞減
*
* @param key 鍵
* @param delta 要減少幾(小於0)
* @return
*/
public static long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("遞減因子必須大於0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* 刪除緩存
*
* @param key 可以傳一個值 或多個
*/
@SuppressWarnings("unchecked")
public static 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 key redis的key
* @param clazz value的class類型
* @param <T>
* @return value的實際對象
*/
public static <T> T get(String key, Class<T> clazz) {
Object obj = key == null ? null : redisTemplate.opsForValue().get(key);
if (!obj.getClass().isAssignableFrom(clazz)) {
throw new ClassCastException("類轉化異常");
}
return (T) obj;
}
/**
* 獲取泛型
*
* @param key 鍵
* @return 值
*/
public static Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
}
9. 創建RedisService及其實現類
RedisService類
package com.yyg.boot.service;
/**
* @Author 一一哥Sun
* @Date Created in 2020/4/8
* @Description Description
*/
public interface RedisService {
void setObj(String key, Object obj, long timeout);
Object getObj(String key);
}
RedisServiceImpl類
package com.yyg.boot.service.impl;
import com.yyg.boot.service.RedisService;
import com.yyg.boot.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Author 一一哥Sun
* @Date Created in 2020/4/8
* @Description Description
*/
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisUtil redisUtil;
@Override
public void setObj(String key, Object obj, long timeout) {
redisUtil.set(key,obj,timeout);
}
@Override
public Object getObj(String key) {
return redisUtil.get(key);
}
}
10. 創建UserController測試接口
package com.yyg.boot.web;
import com.yyg.boot.domain.User;
import com.yyg.boot.repository.UserRepository;
import com.yyg.boot.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @Author 一一哥Sun
* @Date Created in 2020/3/31
* @Description Description
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisService redisService;
@GetMapping("/{id}")
public User findUserById(@PathVariable("id") Long id) {
User user = (User) redisService.getObj("user" + id);
if (user == null) {
user = userRepository.findById(id).get();
redisService.setObj("user" + id, user, 1000 * 60 * 2);
return user;
}
return user;
}
}
12. 創建入口類
package com.yyg.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Author 一一哥Sun
* @Date Created in 2020/4/8
* @Description Description
*/
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args){
SpringApplication.run(RedisApplication.class,args);
}
}
13. 完整目錄結構
14. 啓動程序,進行測試
在我的數據庫中,有這樣的數據。
Redis數據庫中默認沒有緩存數據。
在瀏覽器中輸入地址,進行查詢。
此時再次去Redis DesktopManager中查看,會發現已經有了一條緩存的數據。