深入:SpringBoot是如何操作Redis的

最简单的配置使用

引入

 <dependencies>
        <!--spring-boot-starter-data-redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- redis依赖commons-pool-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

配置yml

spring:
  redis:
    host: 192.168.0.101
    port: 6379
    database: 0

使用

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class RedisTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void test(){
        redisTemplate.opsForValue().set("test","hello redis");
    }
}

可以看出只需3步就能轻松操作redis,那么其背后是如何实现的呢?

redis的自动装配

自动配置初始化流程相关类

SpringBoot启动类需要声明@SpringBootApplication注解,@SpringBootApplication注解里面还有个@EnableAutoConfiguration注解,该注解表示激活自动配置

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

  String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

  Class<?>[] exclude() default {};

  String[] excludeName() default {};

}

EnableAutoConfigurationexcludeexcludeName属性,如果启动的时候不想加载某个自动配置类,可以用该属性,例如@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})

EnableAutoConfiguration还导入了一个AutoConfigurationImportSelector自动配置导入选择器类,该类最后会调用SpringFactoriesLoaderloadSpringFactories方法

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            try {
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();
                ...
            } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
            }
        }
    }

关键来了,该方法加载了一个META-INFO/spring.factories文件,定位文件位置

其中就有个RedisAutoConfiguration配置,进入RedisAutoConfiguration配置类,就可以看到ReidsTemplateStringRedisTemplate

总结:SpringBoot启动的时候会加载autoconfigure->META-INF下的spring.factories文件,RedisAutoConfiguration就是其中之一,RedisAutoConfiguration为我们注入了RedisTemplateStringRedisTemplate组件

如何连接到redis

这里分两步,管理和连接

管理:用哪个客户端?用不用连接池?是主从还是哨兵?

连接:拿到对应的客户端,进行连接操作

管理

RedisAutoConfiguration类中可以看出该类导入了LettuceConnectionConfigurationJedisConnectionConfiguration两个类,这就是连接Redis的两个客户端

Jedis是通过socket实现的,是传统的BIO模型。每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,这种模型在实际生产中很少使用

Lettuce是通过netty实现的,是NIO模型。可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源。

SpringBoot默认用的是Lettuce客户端,这里就通过Lettuce来看是如何连接Redis

Redis配置流程相关类

首先LettuceConnectionConfiguration会判断是sentinel还是cluster还是默认模式,然后进行初始化LettuceConnectionFactory

@Bean
 @ConditionalOnMissingBean(RedisConnectionFactory.class)
 public LettuceConnectionFactory redisConnectionFactory(ClientResources clientResources)
   throws UnknownHostException {
  LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(clientResources,
    this.properties.getLettuce().getPool());
  return createLettuceConnectionFactory(clientConfig);
 }

 private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
  if (getSentinelConfig() != null) {
   return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
  }
  if (getClusterConfiguration() != null) {
   return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
  }
  return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
 }

LettuceConnectionFactory然后会初始化LettuceConnectionProvider,这个类用来提供连接

private @Nullable LettuceConnectionProvider connectionProvider;
 private @Nullable LettuceConnectionProvider reactiveConnectionProvider;
/*
  * (non-Javadoc)
  * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
  */
 public void afterPropertiesSet() {

  this.client = createClient();

  this.connectionProvider = createConnectionProvider(client, LettuceConnection.CODEC);
  this.reactiveConnectionProvider = createConnectionProvider(client, LettuceReactiveRedisConnection.CODEC);

  if (isClusterAware()) {
   this.clusterCommandExecutor = new ClusterCommandExecutor(
     new LettuceClusterTopologyProvider((RedisClusterClient) client),
     new LettuceClusterConnection.LettuceClusterNodeResourceProvider(this.connectionProvider),
     EXCEPTION_TRANSLATION);
  }
 }

默认情况LettuceConnectionProvider的实现类是StandaloneConnectionProvider

protected LettuceConnectionProvider doCreateConnectionProvider(AbstractRedisClient client, RedisCodec<?, ?> codec) {

  ReadFrom readFrom = getClientConfiguration().getReadFrom().orElse(null);

  if (isStaticMasterReplicaAware()) {

   List<RedisURI> nodes = ((RedisStaticMasterReplicaConfiguration) configuration).getNodes().stream() //
     .map(it -> createRedisURIAndApplySettings(it.getHostName(), it.getPort())) //
     .peek(it -> it.setDatabase(getDatabase())) //
     .collect(Collectors.toList());

   return new StaticMasterReplicaConnectionProvider((RedisClient) client, codec, nodes, readFrom);
  }

  if (isClusterAware()) {
   return new ClusterConnectionProvider((RedisClusterClient) client, codec, readFrom);
  }

  return new StandaloneConnectionProvider((RedisClient) client, codec, readFrom);
 }

获取回来的LettuceConnectionProvider会放在LettucePoolingConnectionProvider

private LettuceConnectionProvider createConnectionProvider(AbstractRedisClient client, RedisCodec<?, ?> codec) {

  LettuceConnectionProvider connectionProvider = doCreateConnectionProvider(client, codec);

  if (this.clientConfiguration instanceof LettucePoolingClientConfiguration) {
   return new LettucePoolingConnectionProvider(connectionProvider,
     (LettucePoolingClientConfiguration) this.clientConfiguration);
  }

  return connectionProvider;
 }

连接

RedisTemplate执行时,会调用execute方法,然后获取RedisConnectionFactory,最后用RedisConnectionUtils来获取RedisConnection

@Nullable
 public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {

  Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
  Assert.notNull(action, "Callback object must not be null");

  RedisConnectionFactory factory = getRequiredConnectionFactory();
  RedisConnection conn = null;
  try {

   if (enableTransactionSupport) {
    // only bind resources in case of potential transaction synchronization
    conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
   } else {
    conn = RedisConnectionUtils.getConnection(factory);
   }

   ....
  } finally {
   RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
  }
 }

RedisConnectionUtilsgetConnection

@Override
 public <T extends StatefulConnection<?, ?>> T getConnection(Class<T> connectionType) {

  if (connectionType.equals(StatefulRedisSentinelConnection.class)) {
   return connectionType.cast(client.connectSentinel());
  }

  if (connectionType.equals(StatefulRedisPubSubConnection.class)) {
   return connectionType.cast(client.connectPubSub(codec));
  }

  if (StatefulConnection.class.isAssignableFrom(connectionType)) {

   return connectionType.cast(readFrom.map(it -> this.masterReplicaConnection(redisURISupplier.get(), it))
     .orElseGet(() -> client.connect(codec)));
  }

  throw new UnsupportedOperationException("Connection type " + connectionType + " not supported!");
 }

client.connect最后的调用

private void initializeChannelAsync0(ConnectionBuilder connectionBuilder, CompletableFuture<Channel> channelReadyFuture,
            SocketAddress redisAddress) {
 
        logger.debug("Connecting to Redis at {}", redisAddress);
 
        Bootstrap redisBootstrap = connectionBuilder.bootstrap();
 
        RedisChannelInitializer initializer = connectionBuilder.build();
        redisBootstrap.handler(initializer);
 
        clientResources.nettyCustomizer().afterBootstrapInitialized(redisBootstrap);
        CompletableFuture<Boolean> initFuture = initializer.channelInitialized();
        ChannelFuture connectFuture = redisBootstrap.connect(redisAddress);  
    }

可以看出最后是通过netty连接到redis

附加jedis连接redis的方式

 @Override
  public Socket createSocket() throws JedisConnectionException {
    Socket socket = null;
    try {
      socket = new Socket();
      ...
      HostAndPort hostAndPort = getSocketHostAndPort();
      socket.connect(new InetSocketAddress(hostAndPort.getHost(), hostAndPort.getPort()), getConnectionTimeout());
      socket.setSoTimeout(getSoTimeout()); 
      ...
      return socket;

    } catch (IOException ex) {

      IOUtils.closeQuietly(socket);

      throw new JedisConnectionException("Failed to create socket.", ex);
    }
  }

lettuceredis功能比较

功能lettuceredis
独立连接XX
主/副本连接X
Redis前哨主查询,前哨身份验证,副本读取主查询
Redis集群群集连接,群集节点连接,副本读取群集连接,群集节点连接
运输渠道TCP,OS原生TCP(epoll,kqueue),Unix域套接字TCP协议
连接池X(使用commons-pool2)X(使用commons-pool2)
其他连接功能非阻塞命令的单例连接共享JedisShardInfo支持
SSL支持XX
发布/订阅XX
流水线XX
交易次数XX
数据类型支持Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLogKey, String, List, Set, Sorted Set, Hash, Server, Scripting, Geo, HyperLogLog
响应式(非阻塞)APIX

SpringBoot关于redis的相关配置

# REDIS (RedisProperties)
spring.redis.cluster.max-redirects= # Maximum number of redirects to follow when executing commands across the cluster.
spring.redis.cluster.nodes= # Comma-separated list of "host:port" pairs to bootstrap from.
spring.redis.database=0 # Database index used by the connection factory.
spring.redis.url= # Connection URL. Overrides host, port, and password. User is ignored. Example: redis://user:password@example.com:6379
spring.redis.host=localhost # Redis server host.
spring.redis.jedis.pool.max-active=8 # Maximum number of connections that can be allocated by the pool at a given time. Use a negative value for no limit.
spring.redis.jedis.pool.max-idle=8 # Maximum number of "idle" connections in the pool. Use a negative value to indicate an unlimited number of idle connections.
spring.redis.jedis.pool.max-wait=-1ms # Maximum amount of time a connection allocation should block before throwing an exception when the pool is exhausted. Use a negative value to block indefinitely.
spring.redis.jedis.pool.min-idle=0 # Target for the minimum number of idle connections to maintain in the pool. This setting only has an effect if it is positive.
spring.redis.lettuce.pool.max-active=8 # Maximum number of connections that can be allocated by the pool at a given time. Use a negative value for no limit.
spring.redis.lettuce.pool.max-idle=8 # Maximum number of "idle" connections in the pool. Use a negative value to indicate an unlimited number of idle connections.
spring.redis.lettuce.pool.max-wait=-1ms # Maximum amount of time a connection allocation should block before throwing an exception when the pool is exhausted. Use a negative value to block indefinitely.
spring.redis.lettuce.pool.min-idle=0 # Target for the minimum number of idle connections to maintain in the pool. This setting only has an effect if it is positive.
spring.redis.lettuce.shutdown-timeout=100ms # Shutdown timeout.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.
spring.redis.sentinel.master= # Name of the Redis server.
spring.redis.sentinel.nodes= # Comma-separated list of "host:port" pairs.
spring.redis.ssl=false # Whether to enable SSL support.
spring.redis.timeout= # Connection timeout.
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尘世中-迷途小书童

欢迎IT从业者的头脑风暴

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值