1. 程式人生 > >spring boot 整合 redis lettuce

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 可以看到如下結果,就證明已經整合完成了