1、为什么要使用连接池以及常用客户端的区别

    众所周知,Redis是单线程的,那为什么还要使用连接池?首先Redis也是一种基于内存数据库,有着很高的性能,但是我们的系统使用Redis服务时需要先建立连接才能使用,使用之后又需要断开连接,而一个完整的请求处理过程中性能的消耗主要是在底层的网络通信;连接池则可以实现在客户端建立多个链接并且不释放,当需要使用连接的时候通过一定的算法获取已经建立的连接,使用完了以后则还给连接池,这就免去了数据库连接所占用的时间。
    在Java中Redis提供了常见的客户端如Lettuce和Jedis,这两种客户端主要的区别是Jedis是采用直接连接的模式来访问Redis服务,但是对于多线程中只有一个Jedis实例的情况是非线程安全的,我们看一下Jedis进行连接的源码来说明下为什么说Jedis是线程非安全的:

// outputStream/inputStream用于发送命令和获取返回值,但是这两个参数也是共享的,所以也会引发线程安全问题
private RedisOutputStream outputStream;
private RedisInputStream inputStream;
private Socket socket;
public void connect() {
    if (!isConnected()) {
      try {
      // 当有多个线程时,socket是共享的,所以socket 会出现线程安全问题
        socket = new Socket();
        // ->@wjw_add
        socket.setReuseAddress(true);
        socket.setKeepAlive(true); // Will monitor the TCP connection is
        // valid
        socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
        // ensure timely delivery of data
        socket.setSoLinger(true, 0); // Control calls close () method,
        // the underlying socket is closed
        // immediately
        // <-@wjw_add

        socket.connect(new InetSocketAddress(host, port), connectionTimeout);
        socket.setSoTimeout(soTimeout);

        if (ssl) {
          if (null == sslSocketFactory) {
            sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
          }
          socket = sslSocketFactory.createSocket(socket, host, port, true);
          if (null != sslParameters) {
            ((SSLSocket) socket).setSSLParameters(sslParameters);
          }
          if ((null != hostnameVerifier) &&
              (!hostnameVerifier.verify(host, ((SSLSocket) socket).getSession()))) {
            String message = String.format(
                "The connection to '%s' failed ssl/tls hostname verification.", host);
            throw new JedisConnectionException(message);
          }
        }

        outputStream = new RedisOutputStream(socket.getOutputStream());
        inputStream = new RedisInputStream(socket.getInputStream());
      } catch (IOException ex) {
        broken = true;
        throw new JedisConnectionException("Failed connecting to host " 
            + host + ":" + port, ex);
      }
    }
  }

可以通过建立多个Jedis实例来解决,但是如果链接数量过多时我们可以采用Jedis连接池(JedisPool)的方式来解决;如果使用了SpringBoot则在"JedisConnectionFactory"中有一个方法getConnection() 用来获取连接的,具体源码如下:

public RedisConnection getConnection() {
		if (isRedisClusterAware()) {
			return getClusterConnection();
		}
		Jedis jedis = fetchJedisConnector();
		JedisConnection connection = (getUsePool() ? new JedisConnection(jedis, pool, getDatabase(), getClientName())
				: new JedisConnection(jedis, null, getDatabase(), getClientName()));
		connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
		return postProcessConnection(connection);
	}

	protected Jedis fetchJedisConnector() {
		try {
		// 如果使用了连接池,则从连接池中获取一个Jedis的实例
			if (getUsePool() && pool != null) {
				return pool.getResource(); 
			}
		// 如果没有使用连接池,则重新生成一个Jedis实例
			Jedis jedis = createJedis();
			// force initialization (see Jedis issue #82)
			jedis.connect();

			potentiallySetClientName(jedis);
			return jedis;
		} catch (Exception ex) {
			throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
		}
	}

Lettuce是基于Netty来实现的,连接实例可以在多个线程间共享,Netty可以使多线程的应用使用同一个连接实例,而不用担心并发线程的数量。如果有必要的话也可以用来添加实例个数。


2、如何在Springboot中配置使用连接池

在本节中就上述介绍到的两个客户端进行连接池的配置,这两种客户端均有两种配置方式
方法一:使用系统配置文件进行连接池的配置

####################Jedis连接池配置########################
spring:
  cache:
    redis:
      cache-null-values: false  # 不缓存空值,如果是用于缓存时可以设置这一项
  redis:
    # Redis的服务器地址
    host: localhost
    # Redis的连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: 
    # 连接超时时间(毫秒)
    timeout: 1000
    # Redis数据库索引(默认为0);Redis数据库默认有16个独立的数据库,编号0-15
    database: 0
    jedis:
      pool:
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8

####################Jedis连接池配置########################
spring:
  cache:
    redis:
      cache-null-values: false  # 不缓存空值
  redis:
    # Redis的服务器地址
    host: localhost
    # Redis的连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: 
    # 连接超时时间(毫秒)
    timeout: 1000
    # Redis数据库索引(默认为0);Redis数据库默认有16个独立的数据库,编号0-15
    database: 0
    lettuce:
      # 在关闭客户端连接之前等待任务处理完成的最长时间,在这之后,无论任务是否执行完成,都会被执行器关闭,默认100ms
      shutdown-timeout: 1000
      pool:
        max-active: 8
        min-idle: 0
        max-wait: -1
        max-idle: 8

如果其他都使用默认的配置,到这里就可以了

方法二:在配置类中进行配置,现在配置文件中添加如下配置:

redis.host: localhost
redis.port: 6379
redis.database: 1
redis.timeout: 3000
redis.shutdown-timeout: 1000
redis.pool.max-idle: 8
redis.pool.max-active: 8
redis.pool.min-idle: 0
redis.pool.max-wait: -1

创建配置类文件:

@Configuration
public class RedisConfig {
	
	@Value("${redis.host}")
	private String redisHostName;

	@Value("${redis.port}")
	private int redisPort;
	
	@Value("${redis.database:0}")
	private int database;
	
	@Value("${redis.timeout}")
	private long timeOut;
	
	@Value("${redis.pool.max-idle}")
	private int maxIdle;
	
	@Value("${redis.pool.max-active}")
	private int maxActive;
	@Value("${redis.pool.min-idle}")
	private int minIdle;
	@Value("${redis.pool.max-wait}")
	private long maxWait;

    // **************LettuceConnectionFactory配置************************
	@Bean
	LettuceConnectionFactory lettuceConnectionFactory() {
		//redis基本配置
		RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHostName,redisPort);
		redisStandaloneConfiguration.setDatabase(database);

		// 连接池的配置
		GenericObjectPoolConfig<Object> genericObjectPoolConfig=new GenericObjectPoolConfig<Object>();
		genericObjectPoolConfig.setMaxIdle(maxIdle);
		genericObjectPoolConfig.setMaxWaitMillis(maxWait);
		genericObjectPoolConfig.setMinIdle(minIdle);
		genericObjectPoolConfig.setMaxTotal(maxActive);
		
		// redis客户端配置
		LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder poolBuilder=LettucePoolingClientConfiguration.builder();
		poolBuilder.poolConfig(genericObjectPoolConfig);
		poolBuilder.commandTimeout(Duration.ofMillis(timeOut)); // 设置超时时间
		poolBuilder.shutdownTimeout(Duration.ofMillis(timeOut)); // 设置客户端关闭之前的等待时间
		LettucePoolingClientConfiguration clientConfiguration=poolBuilder.build();
		
		// 创建连接
		LettuceConnectionFactory lettConnectionFactory=new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfiguration);
		return lettConnectionFactory;
	}

 // **************JedisConnectionFactory配置************************
	@Bean
	JedisConnectionFactory jedisConnectionFactory() {
		// redis基本配置
		RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHostName,redisPort);
		redisStandaloneConfiguration.setDatabase(database);
		
		// 连接池的配置
		GenericObjectPoolConfig<Object> genericObjectPoolConfig=new GenericObjectPoolConfig<Object>();
		genericObjectPoolConfig.setMaxIdle(maxIdle);
		genericObjectPoolConfig.setMaxWaitMillis(maxWait);
		genericObjectPoolConfig.setMinIdle(minIdle);
		genericObjectPoolConfig.setMaxTotal(maxActive);
		
		// redis客户端配置
		JedisClientConfiguration.JedisPoolingClientConfigurationBuilder poolBuilder=JedisClientConfiguration//
				.builder()//
				.connectTimeout(Duration.ofMillis(timeOut))//
				.usePooling()//
				.poolConfig(genericObjectPoolConfig);
		JedisClientConfiguration jedisClientConfiguration=poolBuilder.build();
		
		// 创建连接
		JedisConnectionFactory jedisConnectionFactory=new JedisConnectionFactory(redisStandaloneConfiguration,jedisClientConfiguration);
		return jedisConnectionFactory;
	}

	
// ....其他配置省略........
}

为了省略篇幅,将JedisConnectionFactory /LettuceConnectionFactory 的配置在一个类中展示了,实际开发中配置一个即可。