大家在Spring Boot项目经常会配置多种环境,例如本地环境,生产环境,开发环境,测试环境会区分配置,而且Redis也是大家非常常用的分布式缓存选型,那么问题来了,我本地环境没有可用的redis,导致引入了redis相关jar包的项目在启动的时候因为无法连接redis而报错怎么办?
大家说,这个简单,如下图启动配置排除掉RedisAutoConfiguration.class就行了
的确,这样在你只是简单引入jar包的前提下是不会报错了。但是,
你配置RedisTemplate的时候idea便亲切地提示你,无法注入ConnectionFactory。既然前面排除了,自然不会自动注入,那么自己手动注入吧:
package com.sy.sydataingest.config;
import com.sy.sydataingest.prop.RedisProperties;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
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.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import javax.annotation.Resource;
import java.io.*;
import java.util.Map;
/**
* @author linss
* @program sy-data-ingest
* @create 2020-09-15 4:18 下午
*/
@Configuration
public class RedisConfig {
@Resource
private RedisProperties redisProperties;
@Bean
LettuceConnectionFactory lettuceConnectionFactory() {
// 单机redis
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(redisProperties.getHost() == null ? "localhost" : redisProperties.getHost());
redisConfig.setPort(redisProperties.getPort() == null ? 6379 : redisProperties.getPort());
// 连接池配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
Map<String, Map<String, Integer>> lettuce = redisProperties.getLettuce();
Map<String, Integer> poolMap;
if (lettuce != null && lettuce.size() > 0 && (poolMap = lettuce.get("pool")) != null && poolMap.size() > 0) {
Integer maxIdle = poolMap.get("maxIdle");
Integer minIdle = poolMap.get("minIdle");
Integer maxWait = poolMap.get("max-wait");
Integer maxActive = poolMap.get("max-active");
poolConfig.setMaxIdle(maxIdle == null ? 8 : maxIdle);
poolConfig.setMinIdle(minIdle == null ? 1 : minIdle);
poolConfig.setMaxTotal(maxActive == null ? 8 : maxActive);
poolConfig.setMaxWaitMillis(maxWait == null ? 5000L : maxWait);
}
LettucePoolingClientConfiguration lettucePoolingClientConfiguration = LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.build();
/*if (password != null && !"".equals(password)) {
redisConfig.setPassword(password);
}*/
// 哨兵redis
// RedisSentinelConfiguration redisClusterConfiguration = new RedisSentinelConfiguration();
// 集群redis
/*RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
Set<RedisNode> nodeses = new HashSet<>();
String[] hostses = nodes.split("-");
for (String h : hostses) {
h = h.replaceAll("\\s", "").replaceAll("\n", "");
if (!"".equals(h)) {
String host = h.split(":")[0];
int port = Integer.valueOf(h.split(":")[1]);
nodeses.add(new RedisNode(host, port));
}
}
redisClusterConfiguration.setClusterNodes(nodeses);
// 跨集群执行命令时要遵循的最大重定向数量
redisClusterConfiguration.setMaxRedirects(3);
redisClusterConfiguration.setPassword(password);*/
return new LettuceConnectionFactory(redisConfig, lettucePoolingClientConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(lettuceConnectionFactory);
//序列化类
MyRedisSerializer myRedisSerializer = new MyRedisSerializer();
//key序列化方式
template.setKeySerializer(myRedisSerializer);
//value序列化
template.setValueSerializer(myRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(myRedisSerializer);
return template;
}
static class MyRedisSerializer implements RedisSerializer<Object> {
@Override
public byte[] serialize(Object o) throws SerializationException {
return serializeObj(o);
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
return deserializeObj(bytes);
}
/**
* 序列化
*
* @param object
* @return
*/
private static byte[] serializeObj(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
throw new RuntimeException("序列化失败!", e);
}
}
/**
* 反序列化
*
* @param bytes
* @return
*/
private static Object deserializeObj(byte[] bytes) {
if (bytes == null) {
return null;
}
ByteArrayInputStream bais = null;
try {
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
throw new RuntimeException("反序列化失败!", e);
}
}
}
}
RedisProperties,这里不能用@Value来替代,因为我不希望每个环境都去配置redis。RedisProperties代码如下:
package com.sy.sydataingest.prop;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author linss
* @program tool
* @create 2020-06-18 8:04 下午
*/
@Component
@ConfigurationProperties(prefix = "spring.redis")
@EnableConfigurationProperties(RedisProperties.class)
@Getter
@Setter
public class RedisProperties {
private String nodes;
private String password;
private String host;
private Integer port;
private Integer timeout;
private Integer database;
private Map<String, Map<String, Integer>> lettuce;
}
相关配置 application-redis.yml如下:
spring:
redis:
host: redis-server
port: 6379
timeout: 10000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
database: 0
lettuce:
pool:
max-active: 16
max-wait: 3000
max-idle: 8
min-idle: 0
这样在你希望使用redis的配置文件中引入该配置,比如我在生产环境配置:application-prod.yml中添加:
# 配置缓存方式Bean: 默认为 caffeineHandlerImpl
cacheimpl: redisHandlerImpl
spring:
profiles:
include: redis
如上图,这里我额外配置了cacheimpl,这个是因为我虽然某些环境没有配置redis,或者不想使用redis,又或者没有必要使用redis,但也希望缓存相关的功能能够不至于被阉割掉,这里我多方位考虑是引入caffeine作为本地缓存的技术选型。
上文没有写的,这里补充下,pom中添加caffeine以及redis相关依赖包如下:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--lettuce pool連接池 如果不配置连接池可以不加-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置CaffeineConfig.java如下:
package com.sy.sydataingest.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Getter;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* @author linss
* @program toolkit-server
* @create 2019-06-19 12:05
*/
@Configuration
@EnableCaching
public class CaffeineConfig {
/**
* 定义cache名称、超时时长(秒)、最大容量
* 每个cache缺省:10秒超时、最多缓存50000条数据,需要修改可以在构造方法的参数中指定。
*/
@Getter
public enum OdsCacheNamespace {
/**
* 有效期 秒
*/
ODS_RECORD_CACHE(864000),
ODS_TOTAL_COUNT_CACHE(432000),
;
private OdsCacheNamespace(int ttl) {
this.ttl = ttl;
}
private int ttl;
}
/**
* 创建基于Caffeine的Cache Manager
*
* @return
*/
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
ArrayList<CaffeineCache> caches = new ArrayList<>();
for (OdsCacheNamespace c : OdsCacheNamespace.values()) {
caches.add(new CaffeineCache(c.name(), Caffeine.newBuilder().recordStats()
.expireAfterWrite(c.getTtl(), TimeUnit.SECONDS)
//.expireAfterAccess(c.getExpireAfterAccessSeconds(), TimeUnit.SECONDS)
.maximumSize(10000)
.build())
);
}
cacheManager.setCaches(caches);
return cacheManager;
}
}
说明下,这里OdsCacheNamespace的枚举的作用大意就是给每个缓存一个形式上类似空间的概念,好让每个key看起来有那么点归属感,属于缓存管理范畴。其实这个枚举不应该写在这里面哈,因为即便是要写到redis的数据也要来Caffeine的配置里来找找归属感明显是不太像那么一回事儿的,文章里我就不挪了哈。
到了这一步其实也算是那么一回事了,redis和caffeine都整合进来了,就差实际应用了。网上一查多了去的RedisUtil.java,一大堆方法密密麻麻,其实你用的也就那么几个。有需要的话自己网上搜下就有,业务场景比较复杂应该还是比较耐用的。我的场景比较简单,我定义了一个简单的缓存服务接口(CacheService.java)如下:
package com.sy.sydataingest.service;
/**
* @author linss
* @program sy-data-ingest
* @create 2020-09-10 3:49 下午
*/
public interface CacheService {
/**
* 推到缓存
*
* @param cacheName
* @param key
* @param value
*/
boolean putInCache(String cacheName, String key, Object value);
/**
* 查找
*
* @param cacheName
* @param key
* @return
*/
Object findInCache(String cacheName, String key);
/**
* 删除
*
* @param cacheName
* @param key
*/
boolean delInCache(String cacheName, String key);
}
然后分别用redis和caffeine两种方式实现之:
package com.sy.sydataingest.service.impl;
import com.sy.sydataingest.config.CaffeineConfig;
import com.sy.sydataingest.service.CacheService;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @author linss
* @program tool
* @create 2019-11-12 18:16
*/
@Service
@Slf4j
public class RedisHandlerImpl implements CacheService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
private static final String REDIS_CONNECT_SYMBOL = ":";
@Override
public boolean putInCache(String cacheName, String key, Object value) {
if (Strings.isBlank(cacheName) || Strings.isBlank(key)) {
return false;
}
for (CaffeineConfig.OdsCacheNamespace c : CaffeineConfig.OdsCacheNamespace.values()) {
if (cacheName.equals(c.name())) {
try {
redisTemplate.opsForValue().set(new StringBuilder(cacheName).append(REDIS_CONNECT_SYMBOL).append(key).toString(), value, c.getTtl(), TimeUnit.SECONDS);
return true;
} catch (Exception e) {
log.error("ERROR OCCURRED IN RedisHandlerImpl while putInCache-- cacheName:{} key:{} value:{} error:{}", cacheName, key, value, e.getMessage());
return false;
}
}
}
log.warn("缓存namespace:{} 配置可能不存在", cacheName);
return false;
}
@Override
public Object findInCache(String cacheName, String key) {
try {
return redisTemplate.opsForValue().get(new StringBuilder(cacheName).append(REDIS_CONNECT_SYMBOL).append(key).toString());
} catch (Exception e) {
log.error("ERROR OCCURRED IN RedisHandlerImpl while findInCache-- cacheName:{} key:{} error:{}", cacheName, key, e.getMessage());
return null;
}
}
@Override
public boolean delInCache(String cacheName, String key) {
try {
redisTemplate.delete(new StringBuilder(cacheName).append(REDIS_CONNECT_SYMBOL).append(key).toString());
return true;
} catch (Exception e) {
log.error("ERROR OCCURRED IN RedisHandlerImpl while delInCache-- cacheName:{} key:{} error:{}", cacheName, key, e.getMessage());
return false;
}
}
}
package com.sy.sydataingest.service.impl;
import com.sy.sydataingest.service.CacheService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author linss
* @program tool
* @create 2019-11-12 18:16
*/
@Slf4j
@Service
public class CaffeineHandlerImpl implements CacheService {
@Resource(name = "caffeineCacheManager")
private CacheManager cacheManager;
@Override
public boolean putInCache(String cacheName, String key, Object value) {
try {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
log.warn("缓存namespace:{} 配置可能不存在", cacheName);
}
cache.put(key, value);
return true;
} catch (Exception e) {
log.error("ERROR OCCURRED IN CaffeineHandlerImpl while putInCache-- cacheName:{} key:{} value:{} error:{}", cacheName, key, value, e.getMessage());
return false;
}
}
@Override
public Object findInCache(String cacheName, String key) {
try {
Cache.ValueWrapper valueWrapper = cacheManager.getCache(cacheName).get(key);
return valueWrapper != null ? valueWrapper.get() : null;
} catch (Exception e) {
log.error("ERROR OCCURRED IN CaffeineHandlerImpl while findInCache-- cacheName:{} key:{} error:{}", cacheName, key, e.getMessage());
return null;
}
}
@Override
public boolean delInCache(String cacheName, String key) {
try {
cacheManager.getCache(cacheName).evict(key);
return true;
}catch (Exception e) {
log.error("ERROR OCCURRED IN CaffeineHandlerImpl while delInCache-- cacheName:{} key:{} error:{}", cacheName, key, e.getMessage());
return false;
}
}
}
然后在具体使用的时候,使用@Resource根据配置将cacheimpl注入进来即可,并且配置默认值,这样只要对你需要用到redis的环境进行redis相关配置即可:
这里报红,我也很懊恼,应该算是idea的一个缺陷吧,实际上是不影响使用的,不知道道兄们有没有什么非掩耳盗铃式的解决方法。又或者有其他的替代方案也可以多多指点。
这样,是不是应该万事大吉了呢?在没有redis的本地环境启动试试看:
很无奈,但还有办法,那就是@Profile这个注解了
分别给RedisConfig.java以及RedisHandlerImpl.java两个类加上@Profile注解指定系列环境注入,这样再试着在local环境启动便不会报错啦。
最后提醒一句,redis缓存Bean对象要记得实现下序列化接口,serializeObj(o)会报错。
下面是我自己项目中缓存的应用片段:
package com.sy.sydataingest.service;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.sy.sydataingest.kafka.KafkaProducer;
import com.sy.sydataingest.rds.CacheInfo;
import com.sy.sydataingest.rds.RdsSink;
import com.sy.sydataingest.rds.RdsTableInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static com.sy.sydataingest.config.CaffeineConfig.OdsCacheNamespace.ODS_RECORD_CACHE;
import static com.sy.sydataingest.service.impl.IngestServiceImpl.EVENT_TIME;
/**
* @author linss
* @program square-storage-parent
* @create 2020-06-19 4:03 下午
*/
@Slf4j
@Service
public class AsyncService {
@Resource(name = "${cacheimpl:caffeineHandlerImpl}")
private CacheService cacheService;
@Resource(name = "rdsServiceImpl")
private RdsService rdsService;
@Async("taskExecutor")
public void asyncInsertRds(RdsTableInfo rdsTableInfo, Map<String, Object> dataMap) {
List<String> fields = Arrays.asList(rdsTableInfo.getFields().split(","));
RdsSink rdsSink = new RdsSink();
rdsSink.setSchema(rdsTableInfo.getSchema());
rdsSink.setTable(rdsTableInfo.getTable());
rdsSink.setFields(fields);
String partition = (String) dataMap.get("thedate");
if (partition == null) {
partition = DateFormatUtils.format((Long) dataMap.get(EVENT_TIME), "yyyy-MM-dd");
}
rdsSink.setPartition(partition);
CacheInfo recordsInCache = (CacheInfo) cacheService.findInCache(ODS_RECORD_CACHE.name(), rdsTableInfo.getTable());
boolean cacheOk = true;
boolean dataWritten = false;
if (recordsInCache == null) {
dataWritten = true;
//写入缓存
recordsInCache = new CacheInfo();
List<Map<String, Object>> records = Lists.newArrayList();
records.add(dataMap);
recordsInCache.setPartition(partition);
recordsInCache.setRecords(records);
cacheOk = cacheService.putInCache(ODS_RECORD_CACHE.name(), rdsTableInfo.getTable(), recordsInCache);
}
List<Map<String, Object>> records = recordsInCache.getRecords();
try {
//如果缓存无效或者新数据分区与缓存数据不同或者记录数超过阈值 直接入RDS
if (!cacheOk || !partition.equals(recordsInCache.getPartition()) || records.size() >= rdsTableInfo.getBatch()) {
rdsSink.setPartition(recordsInCache.getPartition());
log.info("start insert rds: {}.{} size:{}", rdsTableInfo.getSchema(), rdsTableInfo.getTable(), records.size());
rdsService.doInsertRds(Lists.newArrayList(records), rdsSink);
records.clear();
}
} catch (Exception e) {
log.error("insert table:{} error with msg:{}", rdsTableInfo.getTable(), e.getMessage());
}
if (!dataWritten) {
records.add(dataMap);
recordsInCache.setPartition(partition);
}
//如果本地缓存的话可以不用如下操作 但redis有需要
recordsInCache.setRecords(records);
cacheService.putInCache(ODS_RECORD_CACHE.name(), rdsTableInfo.getTable(), recordsInCache);
}
}
走过路过,请多多指正了。