天天看點

redis lettuce 逾時_SpringBoot 整合 redis 踩坑日記

redis lettuce 逾時_SpringBoot 整合 redis 踩坑日記

SpringBoot 中除了了對常用的關系型資料庫提供了優秀的自動化測試以外,對于很多 NoSQL 資料庫一樣提供了自動化配置的支援,包括:Redis, MongoDB, Elasticsearch, Solr 和 Cassandra。

Java架構免費學習資料​docs.qq.com

redis lettuce 逾時_SpringBoot 整合 redis 踩坑日記

01 整合redis

Redis是一個速度非常快的非關系型資料庫(non-relational database),它可以存儲鍵(key)與5種不同類型的值(value)之間的映射(mapping),可以将存儲在記憶體的鍵值對資料持久化到硬碟。可以使用複制特性來擴充讀性能,還可以使用用戶端分片來擴充寫性能。

  • redis官網
  • redis中文社群

1.1 引入依賴

Spring Boot 提供了對 Redis 內建的元件包:spring-boot-starter-data-redis,spring-boot-starter-data-redis 依賴于spring-data-redis 和 lettuce 。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
           

1.2 參數配置

在 application.properties 中加入Redis服務端的相關配置 :

#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

1.3 測試通路

通過編寫測試用例,舉例說明如何通路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 中儲存對象,我們就要在儲存對象時對對象進行序列化。下面通過一個執行個體來完成對象的對寫操作。

建立 User 實體
@Data
public class User implements Serializable {

    private String userName;

    private Integer age;
}
           
配置針對對象的RedisTemplate執行個體
@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);
        Home("result:{}",redisTemplate.opsForValue().get("chen"));
    }
}
           

這樣我們就能對對象進行緩存了。但是在對 redis 更深入的了解中,一不小心就踩進坑裡去了,下面對 redis 踩的坑做下記錄。

02 踩坑記錄

2.1 踩坑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("[email protected]");
        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.2 踩坑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;
    }
           

2.3 踩坑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: [[email protected]; 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: [[email protected]; 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": "百度一下,你就知道",
    "name": "baidu"
}
           

"@class": "com.pa.market.common.util.UserInfoExt",每個對象都會有這個 id 存在(可以通過源碼看出為嘛有這個 @class),如果使用者一直處在登入狀态,是以 com.pa.market.common.util.UserInfoExt 這個路徑進行的序列化操作。但是移動了 UserInfoExt 的類路徑後,包全名變了。是以會抛出 no such class found 的異常。這樣在判斷使用者是否存在的地方就抛出了異常,故而所有的請求都失敗了,已經登入的使用者沒法進行任何操作。

ok 把踩的坑都記錄下來,終于呼出了最後一口氣,以後遇到這種坑都能從容的避開了,但是 redis 中的坑還有很多,可能以後還是會輕輕松松的跳進去。

Java架構免費學習資料​docs.qq.com

redis lettuce 逾時_SpringBoot 整合 redis 踩坑日記

繼續閱讀