前言

redis由于它是一种基于内存操作的高性能分布式数据库,很受大众喜爱,经常出现项目中,可以说是开发必备技能。

问题

在开发过程中使用不当会造成redis的各种异常:

  1. 第一种经常出现读超时:
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
  1. 第二种获取不到连接
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

springboot默认初始化的连接池和timeout时间

主要看RedisAutoConfiguration类和JedisConnectionFactory类。

如果我们引入starter,基于springboot自动注入原理,类RedisAutoConfiguration会被加载到spring容器中,RedisAutoConfiguration部分代码:

@Bean
		@ConditionalOnMissingBean(RedisConnectionFactory.class)
		public JedisConnectionFactory redisConnectionFactory()
				throws UnknownHostException {
			return applyProperties(createJedisConnectionFactory());
		}

		protected final JedisConnectionFactory applyProperties(
				JedisConnectionFactory factory) {
			configureConnection(factory);
			if (this.properties.isSsl()) {
				factory.setUseSsl(true);
			}
			factory.setDatabase(this.properties.getDatabase());
			if (this.properties.getTimeout() > 0) {
				factory.setTimeout(this.properties.getTimeout());
			}
			return factory;
		}

       省略部分代码

看到会自动注入JedisConnectionFactory这个bean到容器中

public class JedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory {

	private final static Log log = LogFactory.getLog(JedisConnectionFactory.class);
	private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new PassThroughExceptionTranslationStrategy(
			JedisConverters.exceptionConverter());

	private static final Method SET_TIMEOUT_METHOD;
	private static final Method GET_TIMEOUT_METHOD;

	static {

		// We need to configure Jedis socket timeout via reflection since the method-name was changed between releases.
		Method setTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "setTimeout", int.class);
		if (setTimeoutMethodCandidate == null) {
			// Jedis V 2.7.x changed the setTimeout method to setSoTimeout
			setTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "setSoTimeout", int.class);
		}
		SET_TIMEOUT_METHOD = setTimeoutMethodCandidate;

		Method getTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "getTimeout");
		if (getTimeoutMethodCandidate == null) {
			getTimeoutMethodCandidate = ReflectionUtils.findMethod(JedisShardInfo.class, "getSoTimeout");
		}

		GET_TIMEOUT_METHOD = getTimeoutMethodCandidate;
	}

	private JedisShardInfo shardInfo;
	private String hostName = "localhost";
	private int port = Protocol.DEFAULT_PORT;
	private int timeout = Protocol.DEFAULT_TIMEOUT;
	private String password;
	private boolean usePool = true;
	private boolean useSsl = false;
	private Pool<Jedis> pool;
	private JedisPoolConfig poolConfig = new JedisPoolConfig();
	private int dbIndex = 0;
	private String clientName;
	private boolean convertPipelineAndTxResults = true;
	private RedisSentinelConfiguration sentinelConfig;
	private RedisClusterConfiguration clusterConfig;
	private JedisCluster cluster;
	private ClusterCommandExecutor clusterCommandExecutor;

其中的Protocol.DEFAULT_TIMEOUT 默认是2000,单位是毫秒,即读超时时间是默认2秒钟。
再看JedisPoolConfig

public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    // defaults to make your life with connection pool easier :)
    setTestWhileIdle(true);
    setMinEvictableIdleTimeMillis(60000);
    setTimeBetweenEvictionRunsMillis(30000);
    setNumTestsPerEvictionRun(-1);
  }
}
public class GenericObjectPoolConfig extends BaseObjectPoolConfig {

    /**
     * The default value for the {@code maxTotal} configuration attribute.
     * @see GenericObjectPool#getMaxTotal()
     */
    public static final int DEFAULT_MAX_TOTAL = 8;

    /**
     * The default value for the {@code maxIdle} configuration attribute.
     * @see GenericObjectPool#getMaxIdle()
     */
    public static final int DEFAULT_MAX_IDLE = 8;

可以看到默认连接最大是8个,空闲最大也是8个。

异常场景重现

  1. 读取超时
        byte[] n = "abcdef".getBytes();
        for (int i = 0; i< 100; i++) {
            new Thread(() -> {
                jedisOperate.incr(n);
                try {
                    // 模拟耗时操作 
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
/**
 * @author huangd
 * @date 2021-10-08
 **/
/**
 * @author huangd
 * @date 2021-10-08
 **/
@Component
public class JedisOperate {

    private RedisTemplate redisTemplate;
    
    private Jedis jedis;


    JedisOperate(RedisTemplate redisTemplate, CommonProperties properties) {
        this.redisTemplate = redisTemplate;
    }

    @PostConstruct
    public void init() {
        JedisConnection connection = (JedisConnection) redisTemplate.getConnectionFactory().getConnection();
        this.jedis = connection.getNativeConnection();
    }


    public Long incr(byte[] key) {
        Long incr = jedis.incr(key);
        return incr;
    }

    public boolean exists(byte[] key) {
        return jedis.exists(key);
    }
}

以上会抛出异常:

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
	at redis.clients.jedis.Connection.connect(Connection.java:207)
	at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:93)
	at redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:1767)
	at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:106)
	at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:888)
	at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:432)
	at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361)
	at redis.clients.util.Pool.getResource(Pool.java:49)
  1. 获取不到连接

代码做一下修改

       byte[] n = "abcdef".getBytes();
        for (int i = 0; i< 100; i++) {
            new Thread(() -> {
             // 不用sleep时间
                jedisOperate.incr(n);
            }).start();
        }
/**
 * @author huangd
 * @date 2021-10-08
 **/
@Component
public class JedisOperate {

    private RedisTemplate redisTemplate;


    JedisOperate(RedisTemplate redisTemplate, CommonProperties properties) {
        this.redisTemplate = redisTemplate;
    }

    @PostConstruct
    public void init() {
//        JedisConnection connection = (JedisConnection) redisTemplate.getConnectionFactory().getConnection();
//        this.jedis = connection.getNativeConnection();
    }

    public Long incr(byte[] key) {
        Jedis jedis =  ((JedisConnection) redisTemplate.getConnectionFactory().getConnection()).getNativeConnection();
        Long incr = jedis.incr(key);
        return incr;
    }
}

两次区别在于第一种是共用一个jedis,第二种是每次都去获取。
抛出异常:

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
	at redis.clients.util.Pool.getResource(Pool.java:53)
	at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
	at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16)
	at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194)

报错原因是因为每次都去获取连接,但是用完又没有close掉,这个也是我们容易忽略的地方,要特别注意。如果使用RedisTemplate组件,spring最终会帮我们调用close方法的。

修改最大连接和超时时间

直接在项目的 application.properties文件或application.yml文件增加以下配置:
以application.yml为例

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    pool:
      max-active: 16
    timeout: 5000

最大连接=16,超时时间5秒。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐