前言
之前没怎么用过Redis
,对Springboot
集成Redis
不是非常熟悉。在动手集成的时候又发现集成的有Jedis
和Lettuce
两种客户端。想改设置都不知道如何下手。索性就研究了一番。
在 springboot 1.5.x
版本的默认的客户端是 Jedis
实现的,springboot 2.x
版本中默认客户端是用 lettuce
实现的。
Jedis
和 Lettuce
是 Java
操作 Redis
的客户端。在 Spring Boot 1.x
版本默认使用的是 jedis
,而在 Spring Boot 2.x
版本默认使用的就是Lettuce
。关于 Jedis
跟 Lettuce
的区别如下:
Jedis
在实现上是直接连接的redis server
,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis
实例增加物理连接Lettuce
的连接是基于Netty的,连接实例(StatefulRedisConnection
)可以在多个线程间并发访问,应为StatefulRedisConnection
是线程安全的,所以一个连接实例(StatefulRedisConnection
)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
集成配置
通过研究,我们发现Lettuce
的性能会更好一些。所以下面在集成时会以Lettuce
客户端的API进行讲解。
导入依赖
除了Springboot
必要的依赖,还需要导入如下两个依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件
注意lettuce
客户端的配置时以spring.redis.lettuce
开头的。jedis
客户端的配置是以spring.redis.jedis
开头的。
# redis基本配置
spring.redis.database=9
spring.redis.host=192.168.0.102
spring.redis.port=33332
spring.redis.timeout=360000
# lettuce连接池配置(下面四项是默认项)
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
自定义RedisTemplate
默认情况下的模板只能支持 RedisTemplate<String,String>
,只能存入字符串,很多时候,我们需要自定义 RedisTemplate ,设置序列化器,这样我们可以很方便的操作实例对象。
@Configuration
public class RedisConfig {
//配置数据库,默认使用的是0号数据库,如果使用其他数据库可以指定
@Value("${spring.redis.database}")
private int database;
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
//设置数据库,默认为0
connectionFactory.setDatabase(database);
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
//下面几个对象不是必须的但是可以方便对不同数据类型进行操作
//hash类型的数据操作
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Serializable> redisTemplate) {
return redisTemplate.opsForHash();
}
//字符串类型数据操作
@Bean
public ValueOperations<String, Serializable> valueOperations(RedisTemplate<String, Serializable> redisTemplate) {
return redisTemplate.opsForValue();
}
//链表类型的数据操作
@Bean
public ListOperations<String, Serializable> listOperations(RedisTemplate<String, Serializable> redisTemplate) {
return redisTemplate.opsForList();
}
//无序集合类型的数据操作
@Bean
public SetOperations<String, Serializable> setOperations(RedisTemplate<String, Serializable> redisTemplate) {
return redisTemplate.opsForSet();
}
//有序集合类型的数据操作
@Bean
public ZSetOperations<String, Serializable> zSetOperations(RedisTemplate<String, Serializable> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
多数据库集成配置
切换数据库要解决的问题
在上一节集成配置当中简单介绍了指定Redis
数据库的配置。但是通常而言,如果仅仅使用一个redis
数据库,使用默认的0号数据库就可以。但是如果一个项目中用到了多个数据库就需要考虑数据库的切换问题。
需要考虑的问题主要有如下几个:
- DB切换时直接用一个
redisTemplate
是否可以? - DB1切换到DB2后,其他线程如果要继续使用DB1该如何选取?
- 工具类该如何封装?
要解决第一个问题可以使用如下代码。但是问题是切换数据库是在Factory
中进行的。在Factory
中切换数据库以后需要给redisTemplate
重新设置,然后reset Connection
。
如果同一项目使用多个数据库频繁进行切换Connection,性能可能会存在问题。因此,虽然下面这种方式可以,但是并不推荐。
//不推荐使用该方式
@Autowired
private StringRedisTemplate redisTemplate;
public void setDataBase(int num) {
LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
if (connectionFactory != null && num != connectionFactory.getDatabase()) {
//切换DB
connectionFactory.setDatabase(num);
//是否允许多个线程操作共用同一个缓存连接,默认 true,false 时每个操作都将开辟新的连接
connectionFactory.setShareNativeConnection(false);
this.redisTemplate.setConnectionFactory(connectionFactory);
connectionFactory.resetConnection();
}
}
既然多个数据库使用同一个redisTemplate
可能存在性能问题,那么最好的方式就是为不同的数据库创建不同的redisTemplate
。
多redisTemplate
难点
- 如何通过factory创建多个
restTemplate
? - 如何维护Bean的生命周期?yml如何规划
- redis工具类该如何定义?
Java代码配置
package it.aspirin.learnredis.config;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
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.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* @description: 根据yml创建redisTemplate并注入到Spring容器
* @date: Created in 2020/9/27 15:27
*/
@Configuration
public class Knife4jRedisRegister implements EnvironmentAware, ImportBeanDefinitionRegistrar {
private static final Logger logger = LoggerFactory.getLogger(Knife4jRedisRegister.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.host}")
private Integer port;
@Value("#{'${spring.redis.databases}'.split(',')}")
private List<Integer> databases;
@Value("${spring.redis.lettuce.pool.max-active}")
private Integer maxActive;
@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;
private static final Map<String, Object> registerBean = new ConcurrentHashMap<>();
private Environment environment;
private Binder binder;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
this.binder = Binder.get(this.environment);
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
boolean onPrimary = true;
System.out.println(databases+" ddddd "+port);
//根据多个库实例化出多个连接池和Template
for (Integer database : databases) {
//单机模式
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
configuration.setDatabase(database);
//池配置
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxIdle(maxIdle);
genericObjectPoolConfig.setMaxTotal(maxActive);
genericObjectPoolConfig.setMinIdle(minIdle);
if (maxWait != null) {
genericObjectPoolConfig.setMaxWaitMillis(maxWait);
}
Supplier<LettuceConnectionFactory> lettuceConnectionFactorySupplier = () -> {
LettuceConnectionFactory factory = (LettuceConnectionFactory) registerBean.get("LettuceConnectionFactory" + database);
if (factory != null) {
return factory;
}
LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
LettuceClientConfiguration clientConfiguration = builder.poolConfig(genericObjectPoolConfig).build();
factory = new LettuceConnectionFactory(configuration, clientConfiguration);
registerBean.put("LettuceConnectionFactory" + database, factory);
return factory;
};
LettuceConnectionFactory lettuceConnectionFactory = lettuceConnectionFactorySupplier.get();
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(LettuceConnectionFactory.class, lettuceConnectionFactorySupplier);
AbstractBeanDefinition factoryBean = builder.getRawBeanDefinition();
factoryBean.setPrimary(onPrimary);
beanDefinitionRegistry.registerBeanDefinition("lettuceConnectionFactory" + database, factoryBean);
// StringRedisTemplate
GenericBeanDefinition stringRedisTemplate = new GenericBeanDefinition();
stringRedisTemplate.setBeanClass(StringRedisTemplate.class);
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(0, lettuceConnectionFactory);
stringRedisTemplate.setConstructorArgumentValues(constructorArgumentValues);
stringRedisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
beanDefinitionRegistry.registerBeanDefinition("stringRedisTemplate" + database, stringRedisTemplate);
// 定义RedisTemplate对象
GenericBeanDefinition redisTemplate = new GenericBeanDefinition();
redisTemplate.setBeanClass(RedisTemplate.class);
redisTemplate.getPropertyValues().add("connectionFactory", lettuceConnectionFactory);
redisTemplate.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
RedisSerializer stringRedisSerializer = null;
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = null;
// 内置默认序列化(此处若不设置则采用默认的JDK设置,也可以在使用使自定义序列化方式)
jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式,value采用json序列化方式
redisTemplate.getPropertyValues().add("keySerializer",stringRedisSerializer);
redisTemplate.getPropertyValues().add("hashKeySerializer",stringRedisSerializer);
redisTemplate.getPropertyValues().add("valueSerializer",jackson2JsonRedisSerializer);
redisTemplate.getPropertyValues().add("hashValueSerializer",jackson2JsonRedisSerializer);
//注册Bean
beanDefinitionRegistry.registerBeanDefinition("redisTemplate" + database, redisTemplate);
logger.info("Registration redis ({}) !", database);
if (onPrimary) {
onPrimary = false;
}
}
}
}
package it.aspirin.learnredis.config;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Map;
/**
* @description: 给工具类提供manager,由先的configuration进行初始化和赋值
* @date: Created in 2020/9/27 15:27
*/
public class Knife4jRedisManager {
private Map<String, RedisTemplate> redisTemplateMap;
private Map<String, StringRedisTemplate> stringRedisTemplateMap;
public Knife4jRedisManager(Map<String, RedisTemplate> redisTemplateMap ,
Map<String, StringRedisTemplate> stringRedisTemplateMap) {
this.redisTemplateMap = redisTemplateMap;
this.stringRedisTemplateMap = stringRedisTemplateMap;
}
public RedisTemplate redisTemplate(int dbIndex) {
RedisTemplate redisTemplate = redisTemplateMap.get("redisTemplate" + dbIndex);
return redisTemplate;
}
public StringRedisTemplate stringRedisTemplate(int dbIndex) {
StringRedisTemplate stringRedisTemplate = stringRedisTemplateMap.get("stringRedisTemplate" + dbIndex);
stringRedisTemplate.setEnableTransactionSupport(true);
return stringRedisTemplate;
}
public Map<String, RedisTemplate> getRedisTemplateMap() {
return redisTemplateMap;
}
public Map<String, StringRedisTemplate> getStringRedisTemplateMap() {
return stringRedisTemplateMap;
}
}
package it.aspirin.learnredis.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @description: 核心配置,取出spring中的redisTemplate暂存到map中,并初始化redisManager
* @date: Created in 2020/9/27 15:27
*/
@AutoConfigureBefore({RedisAutoConfiguration.class})
@Import(Knife4jRedisRegister.class)
@EnableCaching
@Configuration
public class Knife4jRedisConfiguration implements EnvironmentAware, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(Knife4jRedisConfiguration.class);
@Value("#{'${spring.redis.databases}'.split(',')}")
private List<Integer> databases;
private static String key1 = "redisTemplate";
private static String key2 = "stringRedisTemplate";
Map<String, RedisTemplate> redisTemplateMap = new HashMap<>();
Map<String, StringRedisTemplate> stringRedisTemplateMap = new HashMap<>();
private Binder binder;
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
this.binder = Binder.get(this.environment);
}
@PostConstruct
public Map<String, RedisTemplate> initRedisTemplate() {
//根据多个库实例化出多个连接池和Template
if (databases == null || databases.size() == 0) {
logger.warn("no config property multi databases , default use db0 !!!");
databases.add(0);
}
//根据指定的数据库个数来加载对应的RedisTemplate
for (Integer database : databases) {
String key = key1 + database;
RedisTemplate redisTemplate = applicationContext.getBean(key, RedisTemplate.class);
if (redisTemplate != null) {
redisTemplateMap.put(key, redisTemplate);
}
key = key2 + database;
if (stringRedisTemplateMap != null) {
StringRedisTemplate stringRedisTemplate = applicationContext.getBean(key, StringRedisTemplate.class);
stringRedisTemplateMap.put(key, stringRedisTemplate);
}
}
if (redisTemplateMap.size() == 0 && stringRedisTemplateMap.size() == 0) {
throw new RuntimeException("load redisTemplate failure , please check knife4j.redis property config!!!");
}
return redisTemplateMap;
}
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Bean
public Knife4jRedisManager knife4jRedisManager() {
return new Knife4jRedisManager(redisTemplateMap, stringRedisTemplateMap);
}
}
package it.aspirin.learnredis.config;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @description: redis工具类基类,集成通用的方法,由`configuration`进行初始化
* @date: Created in 2020/9/27 11:20
*/
@Component
public class RedisBaseUtil {
@Resource
protected Knife4jRedisManager knife4jRedisManager;
protected int defaultDB = 0;
public void delete(String key) {
delete(0,key);
}
public void delete(int dbIndex ,String key) {
knife4jRedisManager.redisTemplate(dbIndex).delete(key);
}
public boolean set(String key, Object value) {
return set(0,key,value);
}
public boolean set(int dbIndex ,String key, Object value) {
try {
knife4jRedisManager.redisTemplate(dbIndex).opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public void delete(Collection<String> keys){
delete(defaultDB , keys);
}
public void delete(int dbIndex ,Collection<String> keys){
knife4jRedisManager.redisTemplate(dbIndex).delete(keys);
}
public Set<String> getKeys(String redisKey) {
return getKeys(0,redisKey);
}
public Set<String> getKeys(int dbIndex ,String redisKey) {
Set<Object> keys = knife4jRedisManager.redisTemplate(dbIndex).opsForHash().keys(redisKey);
Set<String> retKeys = new HashSet<>();
for (Object key : keys) {
retKeys.add(String.valueOf(key));
}
return retKeys;
}
/**
* 每个redis
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
return expire(defaultDB , key,time);
}
public boolean expire(int dbIndex ,String key, long time) {
try {
if (time > 0) {
knife4jRedisManager.redisTemplate(dbIndex).expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(int dbIndex , String key) {
return knife4jRedisManager.redisTemplate(dbIndex).getExpire(key, TimeUnit.SECONDS);
}
public long getExpire(String key) {
return getExpire(defaultDB , key);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
return hasKey(defaultDB , key);
}
public boolean hasKey(int dbIndex ,String key) {
try {
return knife4jRedisManager.redisTemplate(dbIndex).hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
测试
@RestController
@RequestMapping("/test")
public class RedisTestController {
@Autowired
private RedisBaseUtil redisBaseUtil;
/**
* 单值操作测试
* @param key
* @return
*/
@GetMapping("/val/{key}")
public String test(@PathVariable("key") String key){
redisBaseUtil.set(key , "默认库设置");
redisBaseUtil.set(1 , key , "指定1库设置值");
//查看key是否存在
boolean flag = redisBaseUtil.hasKey(1, key);
boolean flag1 = redisBaseUtil.hasKey(2, key);
System.out.println("指定1库获取值:" + flag+" "+flag1);
return "ok";
}
}