目录

  • 一、背景
  • 1、问题
  • 2、解决
  • 二、建立SSH通道
  • 1、pom引入依赖
  • 2、创建sshconfig
  • 3、SSHConnection 程序
  • 三、Spring boot整合Redis
  • 1、引入依赖
  • 2、配置信息
  • 3、RedisConfig的编写(切库处理配置)
  • 4、Redis操作的工具类
  • 四、两个大坑
  • 1、 长时间未操作,连接重置
  • 2、长时间未操作,无法获取resource
  • 五、总结



一、背景

使用Spring Boot自带的redis框架,访问S3的Elasticache(Redis),并从Redis的多个DB中同时取数据。

1、问题

  • S3的Redis缓存服务,官方文档中指出Elasticache不能从外部访问(复杂、不成功)
  • 但是可以通过同一个VPC下的AWS EC2来进行访问
  • 本地开发调试的时候怎么去连redis呢?


2、解决

  • 可以建立ssh通道,通过EC2作为跳板机进行端口转发,来访问AWS的Redis缓存服务

二、建立SSH通道

1、pom引入依赖

<!-- ssh -->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>

2、创建sshconfig

  • ssh.yml
sshconfig:
  #监听的本地端口
  local-port: 10010
  
  #远程的redis地址
  remote-host: xxxxxxxxxxxx.cache.amazonaws.com.cn
  
  #远程redis端口号
  remote-port: 6379
  
  ssh:
  	#EC2实例的地址
    host: xxxxxxxxxxxxxx.compute.amazonaws.com.cn
    port: 22
    user: ubuntu
    password:
    #EC2的秘钥对
    pem_file_path: /root/.aws/xxxxxxx-devops.pem

3、SSHConnection 程序

  • 这样当从程序启动的时候,可以将createSSH() 写入静态代码快,直接加载开启通道
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import lombok.extern.slf4j.Slf4j;

import java.io.InputStream;
import java.util.Properties;

/**
 * Through EC2 as a jumpServer, create SSH tunnel to access redis service.
 */

@Slf4j
public class SSHConnection {
    private static Integer localPort;
    private static String remoteHost;
    private static int remotePort;
    private static String user;
    private static String password;
    private static String path;
    private static String host;
    private static int port;
    private static Session session = null;

    static {
        try {
            // Get ss configuration file path.
            InputStream is = SSHConnection.class.getClassLoader().getResourceAsStream("ssh.yml");
            Properties prop = new Properties();
            prop.load(is);

            // Get each value.
            localPort = Integer.valueOf(prop.getProperty("local-port"));
            remoteHost = prop.getProperty("remote-host");
            remotePort = Integer.valueOf(prop.getProperty("remote-port"));

            user = prop.getProperty("user");
            password = prop.getProperty("password");
            path = prop.getProperty("pem_file_path");
            host = prop.getProperty("host");
            port = Integer.valueOf(prop.getProperty("port"));
        } catch (Exception e) {
            log.error("File not found exception: " + e);
        }
    }

    /**
     * Create ssh connection and set port forwarding.
     */
    public static void createSSH() {
        JSch jsch = new JSch();
        try {
            if (path != null) {
                jsch.addIdentity(path);
            }
            session = jsch.getSession(user, host, port);
            if (path == null) {
                session.setPassword(password);
            }

            session.setConfig("StrictHostKeyChecking", "no");
            session.connect();
            int assinged_port = session.setPortForwardingL(localPort, remoteHost, remotePort);
            log.info("The ssh connection is OK.");
        } catch (Exception e) {
            if (null != session) {
                //close ssh connection.
                session.disconnect();
            }
            log.error("Create ssh connection exception: " + e);
        }
    }

    /**
     * Close ssh connection.
     */
    public static void closeSSH() {
        log.info("The ssh connection is closed ! ");
        session.disconnect();
    }
}

三、Spring boot整合Redis

1、引入依赖

刚开始pool使用的spring boot默认的lettuce连接池,但是程序有时候出现错误,支持不太友好,又换回了Jedis连接池。

<!-- redis 去除默认的lettuce连接池-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- commons-pool2  连接池工具包 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>


        <!-- redis.clients/jedis客户端-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.3</version>
        </dependency>

2、配置信息

  • application.yml
spring:
  redis:
    host: localhost
    
    #要和SSH监听的本地端口一致
    port: 10010 
    #超时时间设置 这里要ms 因为后面用到的是Duration
    timeout: 0ms

    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: 3000 #最大连接等待超时时间
        max-idle: 20 #最大连接空闲数
        # 0:Could not get a resource from the pool
        min-idle: 10 #最小空闲数



#最大活动对象数     
redis.pool.maxTotal=1000    
#最大能够保持idel状态的对象数      
redis.pool.maxIdle=100  
#最小能够保持idel状态的对象数   
redis.pool.minIdle=50    
#当池内没有返回对象时,最大等待时间    
redis.pool.maxWaitMillis=10000    
#当调用borrow Object方法时,是否进行有效性检查    
redis.pool.testOnBorrow=true    
#当调用return Object方法时,是否进行有效性检查    
redis.pool.testOnReturn=true  
#“空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1.  
redis.pool.timeBetweenEvictionRunsMillis=30000  
#向调用者输出“链接”对象时,是否检测它的空闲超时;  
redis.pool.testWhileIdle=true  
# 对于“空闲链接”检测线程而言,每次检测的链接资源的个数。默认为3.  
redis.pool.numTestsPerEvictionRun=50  
#redis服务器的IP    
redis.ip=xxxxxx  
#redis服务器的Port    
redis1.port=6379   


详解:
maxActive:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态就成exhausted了,在JedisPoolConfig
maxIdle:控制一个pool最多有多少个状态为idle的jedis实例;
whenExhaustedAction:表示当pool中的jedis实例都被allocated完时,pool要采取的操作;默认有三种WHEN_EXHAUSTED_FAIL(表示无jedis实例时,直接抛出NoSuchElementException)、WHEN_EXHAUSTED_BLOCK(则表示阻塞住,或者达到maxWait时抛出JedisConnectionException)、WHEN_EXHAUSTED_GROW(则表示新建一个jedis实例,也就说设置的maxActive无用);
maxWait:表示当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;
testOnBorrow:在borrow一个jedis实例时,是否提前进行alidate操作;如果为true,则得到的jedis实例均是可用的;
testOnReturn:在return给pool时,是否提前进行validate操作;
testWhileIdle:如果为true,表示有一个idle object evitor线程对idle object进行扫描,如果validate失败,此object会被从pool中drop掉;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
timeBetweenEvictionRunsMillis:表示idle object evitor两次扫描之间要sleep的毫秒数;
numTestsPerEvictionRun:表示idle object evitor每次扫描的最多的对象数;
minEvictableIdleTimeMillis:表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
softMinEvictableIdleTimeMillis:在minEvictableIdleTimeMillis基础上,加入了至少minIdle个对象已经在pool里面了。如果为-1,evicted不会根据idle time驱逐任何对象。如果minEvictableIdleTimeMillis>0,则此项设置无意义,且只有在timeBetweenEvictionRunsMillis大于0时才有意义;
lifo:borrowObject返回对象时,是采用DEFAULT_LIFO(last in first out,即类似cache的最频繁使用队列),如果为False,则表示FIFO队列;

其中JedisPoolConfig对一些参数的默认设置如下:
testWhileIdle=true
minEvictableIdleTimeMills=60000
timeBetweenEvictionRunsMillis=30000
numTestsPerEvictionRun=-1

3、RedisConfig的编写(切库处理配置)

之前使用lettuce设定切库,但是有多个请求时,取出的库中的数据是混乱的,是线程不安全的,加了锁还是不行,索性就写了多个redisTemplat,每一个redisTemplat都只负责一个库

  • 同时操作Redis的三个库
  • 多个connectionFactory设定不同的DB
  • 建立多个 RedisTemplate<String, Object> redisTemplat
import com.bmw.fs.hp.service.hva.utils.SSHConnection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;

@Configuration
@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class}) // 注意exclude
public class RedisConfig {
	//开启ssh通道
    static {
        SSHConnection.createSSH();
    }

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private Duration timeout;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;
    @Value("${spring.redis.jedis.pool.max-wait}")
    private long maxWait;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;

    /** 
     * DB11 
     * DB12 
     * DB13
     */

    @Bean(name = "redisConnectionFactory11")
    public RedisConnectionFactory redisConnectionFactory11() {
       //Redis环境配置(单机、哨兵、集群)
        RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
        standaloneConfiguration.setHostName(host);
        standaloneConfiguration.setPort(port);
        //设定这个factory访问的DB
        standaloneConfiguration.setDatabase(11);

        // Jedis客户端配置
        JedisClientConfiguration jedisClientConfiguration = getJedisClientConfiguration();
        return new JedisConnectionFactory(standaloneConfiguration, jedisClientConfiguration);
    }


    @Bean(name = "redisConnectionFactory12")
    public RedisConnectionFactory redisConnectionFactory12() {
        RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
        standaloneConfiguration.setHostName(host);
        standaloneConfiguration.setPort(port);
        standaloneConfiguration.setDatabase(12);

        JedisClientConfiguration jedisClientConfiguration = getJedisClientConfiguration();
        return new JedisConnectionFactory(standaloneConfiguration, jedisClientConfiguration);
    }


    @Bean(name = "redisConnectionFactory13")
    public RedisConnectionFactory redisConnectionFactory13() {
        RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
        standaloneConfiguration.setHostName(host);
        standaloneConfiguration.setPort(port);
        standaloneConfiguration.setDatabase(13);

        JedisClientConfiguration jedisClientConfiguration = getJedisClientConfiguration();
        return new JedisConnectionFactory(standaloneConfiguration, jedisClientConfiguration);
    }


    @Bean(name = "redisTemplate11")
    public RedisTemplate<String, Object> redisTemplate11() {
        RedisTemplate<String, Object> redisTemplateObject = new RedisTemplate<String, Object>();
        redisTemplateObject.setConnectionFactory(redisConnectionFactory11());
        //进行序列化
        setSerializer(redisTemplateObject);
        redisTemplateObject.afterPropertiesSet();
        return redisTemplateObject;
    }

    @Bean(name = "redisTemplate12")
    public RedisTemplate<String, Object> redisTemplat12() {
        RedisTemplate<String, Object> redisTemplateObject = new RedisTemplate<String, Object>();
        redisTemplateObject.setConnectionFactory(redisConnectionFactory12());
        setSerializer(redisTemplateObject);
        redisTemplateObject.afterPropertiesSet();
        return redisTemplateObject;
    }

    @Bean(name = "redisTemplate13")
    public RedisTemplate<String, Object> redisTemplate13() {
        RedisTemplate<String, Object> redisTemplateObject = new RedisTemplate<String, Object>();
        redisTemplateObject.setConnectionFactory(redisConnectionFactory13());
        setSerializer(redisTemplateObject);
        redisTemplateObject.afterPropertiesSet();
        return redisTemplateObject;
    }

	 // 必须配置这个默认的,否则程序报错:匹配到多个Bean -> connectionFactory
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplateObject = new RedisTemplate<String, Object>();
        redisTemplateObject.setConnectionFactory(redisConnectionFactory11());
        setSerializer(redisTemplateObject);
        redisTemplateObject.afterPropertiesSet();
        return redisTemplateObject;
    }


    /**
     * Set configuration file for reids connection pool.
     *
     * @return JedisPoolConfig
     */
    private JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        //当池子内没用可用连接时,最大的等待时间
        poolConfig.setMaxWaitMillis(maxWait);
        //在获取连接时,检查有效性
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnReturn(true);
        //在空时检查连接有效性
        poolConfig.setTestWhileIdle(true);
        return poolConfig;
    }


    /**
     * Building Jedis client.
     *
     * @return JedisClientConfiguration
     */
    private JedisClientConfiguration getJedisClientConfiguration() {
        //通过构造器来构造客户端配置
        JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder();

        //  加入超时配置,不加的话,Jedis长时间不操作,连接会关闭,导致异常:
        //org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; 
        //nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Connection reset
        if (timeout != null) {
            builder.readTimeout(timeout).connectTimeout(timeout);
        }
        //修改连接池配置
        builder.usePooling().poolConfig(jedisPoolConfig());
        return builder.build();
    }

    /**
     * Serializing values.
     *
     * @param template
     */
    private void setSerializer(RedisTemplate<String, Object> template) {
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setValueSerializer(stringSerializer);
    }
}

4、Redis操作的工具类

  • 设定不同库的RedisUtils
  • 引入不同库的 redisTemplate
  • 这里只写了DB9的工具类,其他的也是一样的就不再赘述
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Get values from redisDB_9
 */
@Component
public class RedisSerDB9 {

    @Resource(name = "redisTemplate9")
    private RedisTemplate redisTemplate;

    /**
     * Take out all the keys of the prefix.
     *
     * @param prefix
     * @return Set<String> : all keys
     */
    public Set<String> getAllKey(String prefix) {
        Set keys = redisTemplate.keys(prefix + "*");
        RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());

        return keys;
    }

    /**
     * Add value to set.
     *
     * @param key
     * @param value
     * @param expireTime
     * @return boolean
     */
    public boolean add(String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            SetOperations<String, Object> set = redisTemplate.opsForSet();
            set.add(key, value);
            //设定过期时间,单位是秒
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        	// 解除连接
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
        return result;
    }

    /**
     * Get values from set.
     *
     * @param key
     * @return Set<Object>: All values of the current key.
     */
    public Set<Object> setMembers(String key) {
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        Set<Object> members = set.members(key);

        RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        return members;
    }
}

四、两个大坑

1、 长时间未操作,连接重置

  • org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Connection reset

在RedisConfig刚开始写的connectionFactory中,没有配置timeout导致的

if (timeout != null) {
            builder.readTimeout(timeout).connectTimeout(timeout);
        }

2、长时间未操作,无法获取resource

  • Error: Could not get a resource from the pool

connectionFactory,配置timeout之后,长时间未操作,还是显示报错:Could not get a resource from the pool
我这里是因为pool中配置的参数有问题:

之前min-idle配置的是0,导致没有空闲的连接数。 改成非0就行了

jedis:
      pool:
        max-active: 20
        max-wait: 3000
        max-idle: 20
        #0:Could not get a resource from the pool
        min-idle: 10

五、总结

  • 多想多做多尝试,不懂就问
  • 别钻牛角尖,这个方法不行,就赶紧换下个方法,掌握方法论,在短时间内找到最有效的解决方法
  • 技术基于业务场景,多思考应用场景,拓展思维