大家在Spring Boot项目经常会配置多种环境,例如本地环境,生产环境,开发环境,测试环境会区分配置,而且Redis也是大家非常常用的分布式缓存选型,那么问题来了,我本地环境没有可用的redis,导致引入了redis相关jar包的项目在启动的时候因为无法连接redis而报错怎么办?

大家说,这个简单,如下图启动配置排除掉RedisAutoConfiguration.class就行了

spring boot properties配置redis_缓存

的确,这样在你只是简单引入jar包的前提下是不会报错了。但是,

spring boot properties配置redis_spring boot_02

你配置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相关配置即可:

spring boot properties配置redis_redis_03

这里报红,我也很懊恼,应该算是idea的一个缺陷吧,实际上是不影响使用的,不知道道兄们有没有什么非掩耳盗铃式的解决方法。又或者有其他的替代方案也可以多多指点。

这样,是不是应该万事大吉了呢?在没有redis的本地环境启动试试看:

spring boot properties配置redis_spring boot_04

很无奈,但还有办法,那就是@Profile这个注解了

spring boot properties配置redis_java_05

spring boot properties配置redis_caffeine_06

分别给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);
    }

}

走过路过,请多多指正了。