SpringBoot 整合 redis 踩坑日記

SpringBoot 中除了了對經常使用的關係型數據庫提供了優秀的自動化測試之外,對於不少 NoSQL 數據庫同樣提供了自動化配置的支持,包括:Redis, MongoDB, Elasticsearch, Solr 和 Cassandra。前端

整合redis

Redis是一個速度很是快的非關係型數據庫(non-relational database),它能夠存儲鍵(key)與5種不一樣類型的值(value)之間的映射(mapping),能夠將存儲在內存的鍵值對數據持久化到硬盤。可使用複製特性來擴展讀性能,還可使用客戶端分片來擴展寫性能。java

  • redis官網
  • redis中文社區

引入依賴

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 踩的坑作下記錄。

踩坑記錄

踩坑1:cacheable註解引起的亂碼問題

@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;
    }
}
複製代碼

踩坑2:redis 獲取緩存異常

報錯信息:

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;
    }
複製代碼

踩坑3:類轉移路徑

異常打印:

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 中的坑還有不少,可能之後仍是會輕輕鬆鬆的跳進去。

相關文章
相關標籤/搜索