验证Lettuce在单连接上进行[类似]多路复用

本文通过实证分析展示了如何使用Lettuce在单TCP连接上进行多路复用,提高Redis操作效率,并探讨了blocking操作如何独立于多路复用机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


redis客户端利用pipeline机制来实现全双工通讯 类似多路复用,在存在并发请求时能减少大量通讯延迟,但pipeline不支持blocking相关的操作(如BLPOP)

配置 RedisTemplate

RedisTemplate自动根据操作类型,选择是在单连接上进行多路复用(setex/get),还是申请新的连接/等待空闲连接(blpop)

@Configuration
public class RedisTemplateConfig {
   @Bean
   public LettuceConnectionFactory lettuceConnectionFactory(@Value("${redis.cluster}") String address,
           @Value("${redis.password}") String password) {
       // 配置连接池管理
       var poolConfig = new GenericObjectPoolConfig<StatefulRedisClusterConnection<String, String>>();
       poolConfig.setMaxTotal(20);
       // 池内空闲连接总数大于这个设置,会立即关闭归还的连接
       poolConfig.setMaxIdle(20);
       poolConfig.setMinIdle(2);
       poolConfig.setTestWhileIdle(true);
       // commons-pool-evictor线程定期执行Eviction任务:
       // 1、直接关闭长期空闲连接
       // 2、验证短期空闲连接的可用性
       // 3、空闲连接总数小于minIdle,会自动新建连接
       poolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(30000));
       // 连接空闲时长超过这个值后,会在下次执行Eviction任务时被直接关闭
       poolConfig.setMinEvictableIdleDuration(Duration.ofMillis(60000));
       poolConfig.setNumTestsPerEvictionRun(-1);
       // 配置客户端行为
       var clientConfig = LettucePoolingClientConfiguration.builder()
               .clientOptions(ClusterClientOptions.builder()
                       .autoReconnect(true)
                       .pingBeforeActivateConnection(true)
                       .socketOptions(SocketOptions.builder()
                               .connectTimeout(Duration.ofSeconds(3))
                               // tcp_user_timeout控制多久收不到ack则认为连接已中断
                               .tcpUserTimeout(TcpUserTimeoutOptions.builder()
                                       .tcpUserTimeout(Duration.ofSeconds(6))
                                       .build())
                               .build())
                       .timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(3)).build())
                       .topologyRefreshOptions(ClusterTopologyRefreshOptions.builder()
                               .enableAdaptiveRefreshTrigger(RefreshTrigger.MOVED_REDIRECT,
                                       RefreshTrigger.PERSISTENT_RECONNECTS)
                               .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10))
                               .build())
                       .build())
               .poolConfig(poolConfig)
               .build();
       // 配置集群连接信息
       var redisConfig = new RedisClusterConfiguration();
       redisConfig.setMaxRedirects(5);
       redisConfig.setPassword(password);
       String[] serverArray = address.split(",|,|;|;");// 获取服务器数组
       Set<RedisNode> nodes = new HashSet<>();
       for (String ipPort : serverArray) {
           nodes.add(RedisNode.fromString(ipPort));
       }
       redisConfig.setClusterNodes(nodes);
       return new LettuceConnectionFactory(redisConfig, clientConfig);
   }

   @Bean
   public StringRedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
       return new StringRedisTemplate(lettuceConnectionFactory);
   }
}

验证blocking操作在独立连接上进行

实现blocking LPOP操作的方法在opsForList()里;
并发100压测期间查看客户端本地的tcp连接,可以看到和每个redis节点都建立了大量连接;
证明RedisTemplate没有选择让不同并发线程共用同一个StatefulConnection

    @Test
    public void blpop() throws Exception {
        long start = System.currentTimeMillis();
        AtomicLong err = new AtomicLong();
        int maxThreads = 100;
        Semaphore semaphore = new Semaphore(maxThreads);
        for (int i = 0; i < maxThreads; i++) {
            final int threadnum = i + 1;
            semaphore.acquire(1);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 这里是blocking LPOP
                        template.opsForList().leftPop("test" + threadnum, 10, TimeUnit.SECONDS);
                    } catch (Exception ex) {
                        log.error("leftPop", ex);
                        err.addAndGet(1L);
                    } finally {
                        semaphore.release(1);
                    }
                }
            }).start();
        }
        semaphore.acquire(maxThreads);
        long end = System.currentTimeMillis();
        log.info("耗时{}ms, 错误{}", end - start, err.get());
    }

在这个场景下,redis template为每个阻塞式命令分配专用的tcp连接;
Lettuce的StatefulConnection是线程安全的,如果多线程共用一个StatefulConnection来执行blocking LPOP操作,那么会串行执行,严重影响执行效率(可通过操作Lettuce原生接口进行验证)

验证单tcp连接的多路复用

发起100个线程,每个线程连续进行100w次setex + get操作;
执行期间查看客户端本地的tcp连接,可以看到只建立了一个和redis节点的tcp连接;
Lettuce负责接收来自多个线程的请求,并通过一个tcp连接上的一个session,串行地发送pipeline格式指令给服务端;
服务端收到pipeline格式指令后,会依次地串行地返回执行结果,然后由Lettuce负责分发给各个线程;
利用pipeline机制实现的全双工通讯,效果类似多路复用,但不是真的多路复用,因为是严格按请求顺序来接收每个请求的响应数据包;
如果pipeline上存在很多已发送但未收到结果的指令,则会引发redis服务端指令缓冲区过大,所以需要限制客户端程序的最大并发线程数量;
和使用半双工通讯的Jedis进行对比,需要配置Jedis连接池maxTotal和maxIdle连接数都到100,才能有相同通讯效率

@Slf4j
@EnableAutoConfiguration(exclude = { RedisAutoConfiguration.class })
@SpringBootTest(classes = { RedisTemplateConfig.class })
public class RedisTemplateTest {
    @Autowired
    StringRedisTemplate template;

    @Test
    public void getSet() throws Exception {
        long start = System.currentTimeMillis();
        int maxThreads = 100;
        long maxMessagess = 1000000;
        AtomicLong err = new AtomicLong();
        AtomicLong num = new AtomicLong();
        Semaphore semaphore = new Semaphore(maxThreads);
        for (int i = 0; i < maxThreads; i++) {
            final int threadnum = i + 1;
            semaphore.acquire(1);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int j = 0;
                    try {
                        for (; j < maxMessagess; j++) {
                            String key = "thread" + threadnum + "test" + j;
                            String value = "test" + j;
                            template.opsForValue().set(key, value, 1, TimeUnit.SECONDS);
                            assertEquals(value, template.opsForValue().get(key));
                        }
                    } finally {
                        num.addAndGet(j);
                        semaphore.release(1);
                    }
                }
            }).start();
        }
        semaphore.acquire(maxThreads);
        long end = System.currentTimeMillis();
        double rate = 1000d * num.get() / (end - start);
        log.info("每秒发送并读取消息{}; 耗时{}ms, 累计发送{}", rate, end - start, num.get());
    }

RedisTemplate屏蔽了哪些并发命令可以共用连接的决策难点,所以不要自行使用Lettuce客户端获取连接或从连接池申请连接。

必须配置tcpUserTimeout,因为单个命令超时,不会导致lettuce舍弃pipeline连接;lettuce也没有在pipeline指令流里插入探活指令;操作系统默认舍弃异常中断的连接(另一端实际不再响应ack),需要等待15分钟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值