目录
- 一、背景
- 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
五、总结
- 多想多做多尝试,不懂就问
- 别钻牛角尖,这个方法不行,就赶紧换下个方法,掌握方法论,在短时间内找到最有效的解决方法
- 技术基于业务场景,多思考应用场景,拓展思维