SpringBoot 中除了了對經常使用的關係型數據庫提供了優秀的自動化測試之外,對於不少 NoSQL 數據庫同樣提供了自動化配置的支持,包括:Redis, MongoDB, Elasticsearch, Solr 和 Cassandra。前端
Redis是一個速度很是快的非關係型數據庫(non-relational database),它能夠存儲鍵(key)與5種不一樣類型的值(value)之間的映射(mapping),能夠將存儲在內存的鍵值對數據持久化到硬盤。可使用複製特性來擴展讀性能,還可使用客戶端分片來擴展寫性能。java
Spring Boot 提供了對 Redis 集成的組件包:spring-boot-starter-data-redis,spring-boot-starter-data-redis 依賴於spring-data-redis 和 lettuce 。redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
複製代碼
在 application.properties 中加入Redis服務端的相關配置 :spring
#redis配置
#Redis服務器地址
spring.redis.host=127.0.0.1
#Redis服務器鏈接端口
spring.redis.port=6379
#Redis數據庫索引(默認爲0)
spring.redis.database=0
#鏈接池最大鏈接數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=50
#鏈接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=3000ms
#鏈接池中的最大空閒鏈接
spring.redis.jedis.pool.max-idle=20
#鏈接池中的最小空閒鏈接
spring.redis.jedis.pool.min-idle=2
#鏈接超時時間(毫秒)
spring.redis.timeout=5000ms
複製代碼
其中 spring.redis.database 的配置一般使用0便可,Redis 在配置的時候能夠設置數據庫數量,默認爲16,能夠理解爲數據庫的 schema數據庫
經過編寫測試用例,舉例說明如何訪問Redis。緩存
@RunWith(SpringRunner.class)
@SpringBootTest
public class FirstSampleApplicationTests {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void test() throws Exception {
// 保存字符串
stringRedisTemplate.opsForValue().set("name", "chen");
Assert.assertEquals("chen", stringRedisTemplate.opsForValue().get("name"));
}
}
複製代碼
上面的案例經過自動配置的 StringRedisTemplate
對象進行 redis 的對寫操做,從對象命名就可注意到支持的是 string 類型,若是有用過 spring-data-redis 的開發者必定熟悉 RedisTemplate<K,V>
接口,StringRedisTemplate 就至關於 RedisTemplate<String, String>
的實現。服務器
除了 String 類型,實戰中常常會在 redis 中儲存對象,咱們就要在儲存對象時對對象進行序列化。下面經過一個實例來完成對象的對寫操做。app
建立 User 實體less
@Data
public class User implements Serializable {
private String userName;
private Integer age;
}
複製代碼
配置針對對象的RedisTemplate實例spring-boot
@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {
/**
* 採用RedisCacheManager做爲緩存管理器
* @param connectionFactory
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(connectionFactory);
return redisCacheManager;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
//解決鍵、值序列化問題
StringRedisTemplate template = new StringRedisTemplate(factory);
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);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
複製代碼
完成了配置工做後,編寫測試用例實驗效果
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class FirstSampleApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Test
public void test() throws Exception {
//保存對象
User user = new User();
user.setUserName("chen");
user.setAge(22);
redisTemplate.opsForValue().set(user.getUserName(), user);
log.info("result:{}",redisTemplate.opsForValue().get("chen"));
}
}
複製代碼
這樣咱們就能對對象進行緩存了。可是在對 redis 更深刻的瞭解中,一不當心就踩進坑裏去了,下面對 redis 踩的坑作下記錄。
@RestController
@RequestMapping("/chen/user")
@Slf4j
public class UserController {
@Autowired
IUserService userService;
@GetMapping("/hello")
@Cacheable(value = "redis_key",key = "#name",unless = "#result == null")
public User hello(@RequestParam("name")String name){
User user = new User();
user.setName(name);
user.setAge(22);
user.setEmail("chen_ti@outlook.com");
return user;
}
}
複製代碼
在使用 SpringBoot1.x 的時候,經過簡單的配置 RedisTemplete 就能夠了,升級到 SpringBoot2.0,spring-boot-starter-data-redis 也跟着升起來了,@Cacheable 就出現了亂碼的狀況,能夠經過將上面的配置文件 RedisConfiguration 作以下更改解決 :
@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {
@Bean(name="redisTemplate")
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
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);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 配置序列化
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(redisCacheConfiguration)
.build();
return cacheManager;
}
}
複製代碼
報錯信息:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.tuhu.twosample.chen.entity.User
複製代碼
Redis獲取緩存異常:java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX。
出現這種異常,咱們須要自定義 ObjectMapper
,設置一些參數,而不是直接使用 Jackson2JsonRedisSerializer 類中黙認的 ObjectMapper,看源代碼能夠知道,Jackson2JsonRedisSerializer 中的 ObjectMapper 是直接使用new ObjectMapper() 建立的,這樣 ObjectMapper 會將 redis 中的字符串反序列化爲 java.util.LinkedHashMap類型,致使後續 Spring 對其進行轉換成報錯。其實咱們只要它返回 Object 類型就能夠了。
使用如下方法,構建一個 Jackson2JsonRedisSerializer
對象,將其注入 RedisCacheManager 便可。
/**
* 經過自定義配置構建Redis的Json序列化器
* @return Jackson2JsonRedisSerializer對象
*/
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(){
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 此項必須配置,不然會報java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
複製代碼
異常打印:
19:32:47 INFO - Started Application in 10.932 seconds (JVM running for 12.296)
19:32:50 INFO - get data from redis, key = 10d044f9-0e94-420b-9631-b83f5ca2ed30
19:32:50 WARN - /market/renewal/homePage/index
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve type id 'com.pa.market.common.util.UserInfoExt' into a subtype of [simple type, class java.lang.Object]: no such class found
at [Source: [B@641a684c; line: 1, column: 11]; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'com.pa.market.common.util.UserInfoExt' into a subtype of [simple type, class java.lang.Object]: no such class found at [Source: [B@641a684c; line: 1, column: 11]
複製代碼
問題緣由:
項目中使用了攔截器,對每一個 http 請求進行攔截。經過前端傳遞過來的 token,去 redis 緩存中獲取用戶信息UserInfoExt,用戶信息是在用戶登陸的時候存入到 redis 緩存中的。根據獲取到的用戶信息來判斷是否存是登陸狀態。 因此除白名單外的 url,其餘請求都須要進行這個操做。經過日誌打印,很明顯出如今 UserInfoExt 對象存儲到 redis 中序列化和反序列化的操做步驟。
解決辦法:
@Bean
public RedisTemplate<K, V> redisTemplate() {
RedisTemplate<K, V> redisTemplate = new RedisTemplate<K, V>();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
複製代碼
查看 redis 的 Bean 定義發現,對 key 的序列化使用的是 StringRedisSerializer 系列化,value 值的序列化是 GenericJackson2JsonRedisSerializer
的序列化方法。
其中 GenericJackson2JsonRedisSerializer
序列化方法會在 redis 中記錄類的 @class 信息,以下所示:
{
"@class": "com.pa.market.common.util.UserInfoExt",
"url": "www.baidu.com",
"name": "baidu"
}
複製代碼
"@class": "com.pa.market.common.util.UserInfoExt",每一個對象都會有這個 id 存在(能夠經過源碼看出爲嘛有這個 @class),若是用戶一直處在登陸狀態,是以 com.pa.market.common.util.UserInfoExt 這個路徑進行的序列化操做。可是移動了 UserInfoExt 的類路徑後,包全名變了。因此會拋出 no such class found 的異常。這樣在判斷用戶是否存在的地方就拋出了異常,故而全部的請求都失敗了,已經登陸的用戶無法進行任何操做。
ok 把踩的坑都記錄下來,終於呼出了最後一口氣,之後遇到這種坑都能從容的避開了,可是 redis 中的坑還有不少,可能之後仍是會輕輕鬆鬆的跳進去。