spring boot 集成 redis lettuce


一、簡介

spring boot框架中已經集成了redis,在1.x.x的版本時默認使用的jedis客戶端,現在是2.x.x版本默認使用的lettuce客戶端,兩種客戶端的區別如下

# Jedis和Lettuce都是Redis Client

# Jedis 是直連模式,在多個線程間共享一個 Jedis 實例時是線程不安全的,
# 如果想要在多線程環境下使用 Jedis,需要使用連接池,
# 每個線程都去拿自己的 Jedis 實例,當連接數量增多時,物理連接成本就較高了。
# Lettuce的連接是基於Netty的,連接實例可以在多個線程間共享,
# 所以,一個多線程的應用可以使用同一個連接實例,而不用擔心並發線程的數量。
# 當然這個也是可伸縮的設計,一個連接實例不夠的情況也可以按需增加連接實例。

# 通過異步的方式可以讓我們更好的利用系統資源,而不用浪費線程等待網絡或磁盤I/O。
# Lettuce 是基於 netty 的,netty 是一個多線程、事件驅動的 I/O 框架,
# 所以 Lettuce 可以幫助我們充分利用異步的優勢。

由於我的項目是spring boot 2.0.4的,所以我是用lettuce來配置,在我的這個文章里面和其他文章不一樣的地方是,其他文章直接把cache操作類放在跟spring boot同一個模塊中

而實際開發時,這種緩存類都是獨立放在common模塊中的,所以Autowired就失效了,使用其他方式進行注入

以下是我的項目結構:

 

二、Common模塊代碼

1、先在pom中引入redis及其它jar包

<dependencies>        
        <!-- spring boot redis 緩存引入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <!-- lettuce pool 緩存連接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.5.0</version>
        </dependency>
        <!-- jackson json 優化緩存對象序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.6</version>
        </dependency>
    </dependencies>

2、編寫緩存配置類CacheConfig用於調優緩存默認配置,RedisTemplate<String, Object>的類型兼容性更高

大家可以看到在redisTemplate()這個方法中更換掉了Redis默認的序列化方式

spring-data-redis中序列化類有以下幾個:

  • GenericToStringSerializer:可以將任何對象泛化為字符創並序列化
  • Jackson2JsonRedisSerializer:序列化Object對象為json字符創(與JacksonJsonRedisSerializer相同)
  • JdkSerializationRedisSerializer:序列化java對象
  • StringRedisSerializer:簡單的字符串序列化

JdkSerializationRedisSerializer序列化

被序列化對象必須實現Serializable接口,被序列化除屬性內容還有其他內容,長度長且不易閱讀

存儲內容如下:

"\xac\xed\x00\x05sr\x00!com.oreilly.springdata.redis.User\xb1\x1c \n\xcd\xed%\xd8\x02\x00\x02I\x00\x03ageL\x00\buserNamet\x00\x12Ljava/lang/String;xp\x00\x00\x00\x14t\x00\x05user1"

JacksonJsonRedisSerializer序列化

被序列化對象不需要實現Serializable接口,被序列化的結果清晰,容易閱讀,而且存儲字節少,速度快

存儲內容如下:

"{\"userName\":\"user1\",\"age\":20}"

StringRedisSerializer序列化

一般如果key、value都是string字符串的話,就是用這個就可以了

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import java.lang.reflect.Method;

/**
 * 緩存配置-使用Lettuce客戶端,自動注入配置的方式
 */
@Configuration
@EnableCaching //啟用緩存
public class CacheConfig extends CachingConfigurerSupport {

    /**
     * 自定義緩存key的生成策略。默認的生成策略是看不懂的(亂碼內容) 通過Spring 的依賴注入特性進行自定義的配置注入並且此類是一個配置類可以更多程度的自定義配置
     *
     * @return
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    /**
     * 緩存配置管理器
     */
    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory factory) {
        //以鎖寫入的方式創建RedisCacheWriter對象
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
        /*
        設置CacheManager的Value序列化方式為JdkSerializationRedisSerializer,
        但其實RedisCacheConfiguration默認就是使用
        StringRedisSerializer序列化key,
        JdkSerializationRedisSerializer序列化value,
        所以以下注釋代碼就是默認實現,沒必要寫,直接注釋掉
         */
        // RedisSerializationContext.SerializationPair pair = RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader()));
        // RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
        //創建默認緩存配置對象
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(writer, config);
        return cacheManager;
    }

    /**
     * 獲取緩存操作助手對象
     *
     * @return
     */
    @Bean
    public RedisTemplate<String, String> redisTemplate(LettuceConnectionFactory factory) {
        //創建Redis緩存操作助手RedisTemplate對象
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(factory);
        //以下代碼為將RedisTemplate的Value序列化方式由JdkSerializationRedisSerializer更換為Jackson2JsonRedisSerializer
        //此種序列化方式結果清晰、容易閱讀、存儲字節少、速度快,所以推薦更換
        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;//StringRedisTemplate是RedisTempLate<String, String>的子類
    }
}

3、編寫緩存操作提供類CacheProvider,用於給開發提供緩存操作

import com.google.gson.Gson;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * 緩存提供類
 */
public class CacheProvider {

    //由於當前class不在spring boot框架內(不在web項目中)所以無法使用autowired,使用此種方法進行注入
    private static RedisTemplate<String, String> template = (RedisTemplate<String, String>) SpringBeanUtil.getBean("redisTemplate");

    public static <T> boolean set(String key, T value) {
        Gson gson = new Gson();
        return set(key, gson.toJson(value));
    }

    public static boolean set(String key, String value, long validTime) {
        boolean result = template.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = template.getStringSerializer();
                connection.set(serializer.serialize(key), serializer.serialize(value));
                connection.expire(serializer.serialize(key), validTime);
                return true;
            }
        });
        return result;
    }

    public static <T> T get(String key, Class<T> clazz) {
        Gson gson = new Gson();
        return gson.fromJson(get(key), clazz);
    }

    public static String get(String key) {
        String result = template.execute(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = template.getStringSerializer();
                byte[] value = connection.get(serializer.serialize(key));
                return serializer.deserialize(value);
            }
        });
        return result;
    }

    public static boolean del(String key) {
        return template.delete(key);
    }
}

4、此時你會發現我們並沒有用Autowired做自動注入,而是用SpringBeanUtil.getBean("redisTemplate")自己寫的類進行注入,

因為這個Common模塊並不在Spring boot框架內,自動注入無效,所以改用這個

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringBeanUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringBeanUtil.applicationContext == null) {
            SpringBeanUtil.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 通過Bean名字獲取Bean
     *
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName) {
        return getApplicationContext().getBean(beanName);
    }

    /**
     * 通過Bean類型獲取Bean
     *
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> beanClass) {
        return getApplicationContext().getBean(beanClass);
    }

    /**
     * 通過Bean名字和Bean類型獲取Bean
     *
     * @param beanName
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> T getBean(String beanName, Class<T> beanClass) {
        return getApplicationContext().getBean(beanName, beanClass);
    }
}

5、現在Common模塊就編寫完成了,大家可以發現CacheConfig類中使用的自動讀取配置文件的方式,以下再提供一種手動配置的方式

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.time.Duration;

/**
 * 緩存配置-使用Lettuce客戶端,手動注入配置的方式
 */
@Configuration
@EnableCaching //啟用緩存
@ConfigurationProperties(prefix = "spring.redis") //指明配置節點
public class CacheConfigLettuceManual extends CachingConfigurerSupport {

    // Redis服務器地址
    @Value("${spring.redis.host}")
    private String host;
    // Redis服務器連接端口
    @Value("${spring.redis.port}")
    private Integer port;
    // Redis數據庫索引(默認為0)
    @Value("${spring.redis.database}")
    private Integer database;
    // Redis服務器連接密碼(默認為空)
    @Value("${spring.redis.password}")
    private String password;
    // 連接超時時間(毫秒)
    @Value("${spring.redis.timeout}")
    private Integer timeout;

    // 連接池最大連接數(使用負值表示沒有限制)
    @Value("${spring.redis.lettuce.pool.max-active}")
    private Integer maxTotal;
    // 連接池最大阻塞等待時間(使用負值表示沒有限制)
    @Value("${spring.redis.lettuce.pool.max-wait}")
    private Integer maxWait;
    // 連接池中的最大空閑連接
    @Value("${spring.redis.lettuce.pool.max-idle}")
    private Integer maxIdle;
    // 連接池中的最小空閑連接
    @Value("${spring.redis.lettuce.pool.min-idle}")
    private Integer minIdle;
    // 關閉超時時間
    @Value("${spring.redis.lettuce.shutdown-timeout}")
    private Integer shutdown;

    /**
     * 自定義緩存key的生成策略。默認的生成策略是看不懂的(亂碼內容) 通過Spring 的依賴注入特性進行自定義的配置注入並且此類是一個配置類可以更多程度的自定義配置
     *
     * @return
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    /**
     * 緩存配置管理器
     */
    @Bean
    @Override
    public CacheManager cacheManager() {
        //以鎖寫入的方式創建RedisCacheWriter對象
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(getConnectionFactory());
        /*
        設置CacheManager的Value序列化方式為JdkSerializationRedisSerializer,
        但其實RedisCacheConfiguration默認就是使用
        StringRedisSerializer序列化key,
        JdkSerializationRedisSerializer序列化value,
        所以以下注釋代碼就是默認實現,沒必要寫,直接注釋掉
         */
        // RedisSerializationContext.SerializationPair pair = RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader()));
        // RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
        //創建默認緩存配置對象
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(writer, config);
        return cacheManager;
    }

    /**
     * 獲取緩存操作助手對象
     *
     * @return
     */
    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        //創建Redis緩存操作助手RedisTemplate對象
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(getConnectionFactory());
        //以下代碼為將RedisTemplate的Value序列化方式由JdkSerializationRedisSerializer更換為Jackson2JsonRedisSerializer
        //此種序列化方式結果清晰、容易閱讀、存儲字節少、速度快,所以推薦更換
        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.setKeySerializer(new StringRedisSerializer());//RedisTemplate對象需要指明Key序列化方式,如果聲明StringRedisTemplate對象則不需要
        //template.setEnableTransactionSupport(true);//是否啟用事務
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 獲取緩存連接
     *
     * @return
     */
    @Bean
    public RedisConnectionFactory getConnectionFactory() {
        //單機模式
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPort(port);
        configuration.setDatabase(database);
        configuration.setPassword(RedisPassword.of(password));
        //哨兵模式
        //RedisSentinelConfiguration configuration1 = new RedisSentinelConfiguration();
        //集群模式
        //RedisClusterConfiguration configuration2 = new RedisClusterConfiguration();
        LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration, getPoolConfig());
        //factory.setShareNativeConnection(false);//是否允許多個線程操作共用同一個緩存連接,默認true,false時每個操作都將開辟新的連接
        return factory;
    }

    /**
     * 獲取緩存連接池
     *
     * @return
     */
    @Bean
    public LettucePoolingClientConfiguration getPoolConfig() {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxWaitMillis(maxWait);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        LettucePoolingClientConfiguration pool = LettucePoolingClientConfiguration.builder()
                .poolConfig(config)
                .commandTimeout(Duration.ofMillis(timeout))
                .shutdownTimeout(Duration.ofMillis(shutdown))
                .build();
        return pool;
    }

}
View Code

 

三、Web模塊代碼

這里只是一個調用方,使用spring boot先添加pom.xml信息

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.3.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>javademo.tyh</groupId>
            <artifactId>javademo-tyh-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

添加配置文件及里面的配置

spring.application.name=javademo-tyh-job
server.port=15000

#redis
# Redis服務器地址
spring.redis.host=10.11.12.237
# Redis服務器連接端口
spring.redis.port=6379
# Redis數據庫索引(默認為0)
spring.redis.database=0
# Redis服務器連接密碼(默認為空)
spring.redis.password=
# 連接超時時間(毫秒)
spring.redis.timeout=10000

# 以下連接池已在SpringBoot2.0不推薦使用
#spring.redis.pool.max-active=8
#spring.redis.pool.max-wait=-1
#spring.redis.pool.max-idle=8
#spring.redis.pool.min-idle=0

# Jedis
#spring.redis.jredis.max-active=8
#spring.redis.jredis.max-wait=10000
#spring.redis.jredis.max-idle=8
#spring.redis.jredis.min-idle=0

# Lettuce
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.lettuce.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.lettuce.pool.max-wait=10000
# 連接池中的最大空閑連接
spring.redis.lettuce.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.lettuce.pool.min-idle=0
# 關閉超時時間
spring.redis.lettuce.shutdown-timeout=100

啟動main()方法

SpringBoot在寫啟動類的時候如果不使用@ComponentScan指明對象掃描范圍,默認只掃描當前啟動類所在的包里的對象,
因為啟動類不能直接放在main/java文件夾下,必須要建一個包把它放進去,這是就需要使用@ComponentScan指明要掃描的包。
如:
javademo-tyh-common模塊的包名:javademo.tyh.common
javademo-tyh-job模塊的包名:javademo.tyh.job
這樣默認就不會把common模塊中標記@Component的組件裝配到SpringBoot中,因為它默認只掃描javademo.tyh.job包下的組件,
所以這時就需要在main()啟動類中使用@ComponentScan注解來指明要掃描那些包,但只掃描該注解指定的包,當前mian()方法所在的包就不會被掃描了
所以要寫它的上級包“javademo.tyh”
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

/*
SpringBoot在寫啟動類的時候如果不使用@ComponentScan指明對象掃描范圍,默認只掃描當前啟動類所在的包里的對象,
因為啟動類不能直接放在main/java文件夾下,必須要建一個包把它放進去,這是就需要使用@ComponentScan指明要掃描的包。
如:
javademo-tyh-common模塊的包名:javademo.tyh.common
javademo-tyh-job模塊的包名:javademo.tyh.job
這樣默認就不會把common模塊中標記@Component的組件裝配到SpringBoot中,因為它默認只掃描javademo.tyh.job包下的組件,
所以這時就需要在main()啟動類中使用@ComponentScan注解來指明要掃描那些包,但只掃描該注解指定的包,當前mian()方法所在的包就不會被掃描了,
所以要寫它的上級包“javademo.tyh”
*/ @ComponentScan("javademo.tyh") @SpringBootApplication public class AppJob { public static void main( String[] args ) { SpringApplication.run(AppJob.class); } }

controller控制器

import javademo.tyh.common.CacheProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.Cookie;

@Controller
@RequestMapping("/test")
public class TestController {

    @ResponseBody
    @RequestMapping("index")
    public String index(){

        String str = "";

        str += CacheProvider.set("tyh", "aaaaaaaaaaaaaaaaaa");
        str += "|";
        str += CacheProvider.get("tyh");
        str += "|";
        str += CacheProvider.del("tyh");

        str += "|||";

        Cookie cookie = new Cookie("aaa", "bbb");
        str += CacheProvider.set("cookie", cookie);
        str += "|";
        str += CacheProvider.get("cookie", Cookie.class);
        str += "|";
        str += CacheProvider.del("cookie");

        return str.toString();
    }
}

 

好了,啟動程序,打開http://localhost:15000/cacheManage/test 可以看到如下結果,就證明已經集成完成了

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM