高阶特性
Lettuce
有很多高阶使用特性,这里只列举个人认为常用的两点:
- 配置客户端资源。
- 使用连接池。
更多其他特性可以自行参看官方文档。
配置客户端资源
客户端资源的设置与Lettuce
的性能、并发和事件处理相关。线程池或者线程组相关配置占据客户端资源配置的大部分(EventLoopGroups
和EventExecutorGroup
),这些线程池或者线程组是连接程序的基础组件。一般情况下,客户端资源应该在多个Redis
客户端之间共享,并且在不再使用的时候需要自行关闭。笔者认为,客户端资源是面向Netty
的。注意:除非特别熟悉或者花长时间去测试调整下面提到的参数,否则在没有经验的前提下凭直觉修改默认值,有可能会踩坑。
客户端资源接口是ClientResources
,实现类是DefaultClientResources
。
构建DefaultClientResources
实例:
// 默认 ClientResources resources = DefaultClientResources.create(); // 建造器 ClientResources resources = DefaultClientResources.builder() .ioThreadPoolSize(4) .computationThreadPoolSize(4) .build() 复制代码
使用:
ClientResources resources = DefaultClientResources.create(); // 非集群 RedisClient client = RedisClient.create(resources, uri); // 集群 RedisClusterClient clusterClient = RedisClusterClient.create(resources, uris); // ...... client.shutdown(); clusterClient.shutdown(); // 关闭资源 resources.shutdown(); 复制代码
客户端资源基本配置:
属性 | 描述 | 默认值 |
ioThreadPoolSize |
I/O 线程数 |
Runtime.getRuntime().availableProcessors() |
computationThreadPoolSize |
任务线程数 | Runtime.getRuntime().availableProcessors() |
客户端资源高级配置:
属性 | 描述 | 默认值 |
eventLoopGroupProvider |
EventLoopGroup 提供商 |
- |
eventExecutorGroupProvider |
EventExecutorGroup 提供商 |
- |
eventBus |
事件总线 | DefaultEventBus |
commandLatencyCollectorOptions |
命令延时收集器配置 | DefaultCommandLatencyCollectorOptions |
commandLatencyCollector |
命令延时收集器 | DefaultCommandLatencyCollector |
commandLatencyPublisherOptions |
命令延时发布器配置 | DefaultEventPublisherOptions |
dnsResolver |
DNS 处理器 |
JDK或者Netty 提供 |
reconnectDelay |
重连延时配置 | Delay.exponential() |
nettyCustomizer |
Netty 自定义配置器 |
- |
tracing |
轨迹记录器 | - |
非集群客户端RedisClient
的属性配置:
Redis
非集群客户端RedisClient
本身提供了配置属性方法:
RedisClient client = RedisClient.create(uri); client.setOptions(ClientOptions.builder() .autoReconnect(false) .pingBeforeActivateConnection(true) .build()); 复制代码
非集群客户端的配置属性列表:
属性 | 描述 | 默认值 |
pingBeforeActivateConnection |
连接激活之前是否执行PING 命令 |
false |
autoReconnect |
是否自动重连 | true |
cancelCommandsOnReconnectFailure |
重连失败是否拒绝命令执行 | false |
suspendReconnectOnProtocolFailure |
底层协议失败是否挂起重连操作 | false |
requestQueueSize |
请求队列容量 | 2147483647(Integer#MAX_VALUE) |
disconnectedBehavior |
失去连接时候的行为 | DEFAULT |
sslOptions |
SSL配置 |
- |
socketOptions |
Socket 配置 |
10 seconds Connection-Timeout, no keep-alive, no TCP noDelay |
timeoutOptions |
超时配置 | - |
publishOnScheduler |
发布反应式信号数据的调度器 | 使用I/O 线程 |
集群客户端属性配置:
Redis
集群客户端RedisClusterClient
本身提供了配置属性方法:
RedisClusterClient client = RedisClusterClient.create(uri); ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(refreshPeriod(10, TimeUnit.MINUTES)) .enableAllAdaptiveRefreshTriggers() .build(); client.setOptions(ClusterClientOptions.builder() .topologyRefreshOptions(topologyRefreshOptions) .build()); 复制代码
集群客户端的配置属性列表:
属性 | 描述 | 默认值 |
enablePeriodicRefresh |
是否允许周期性更新集群拓扑视图 | false |
refreshPeriod |
更新集群拓扑视图周期 | 60秒 |
enableAdaptiveRefreshTrigger |
设置自适应更新集群拓扑视图触发器RefreshTrigger |
- |
adaptiveRefreshTriggersTimeout |
自适应更新集群拓扑视图触发器超时设置 | 30秒 |
refreshTriggersReconnectAttempts |
自适应更新集群拓扑视图触发重连次数 | 5 |
dynamicRefreshSources |
是否允许动态刷新拓扑资源 | true |
closeStaleConnections |
是否允许关闭陈旧的连接 | true |
maxRedirects |
集群重定向次数上限 | 5 |
validateClusterNodeMembership |
是否校验集群节点的成员关系 | true |
使用连接池
引入连接池依赖commons-pool2
:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.7.0</version> </dependency 复制代码
基本使用如下:
@Test public void testUseConnectionPool() throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport.createGenericObjectPool(redisClient::connect, poolConfig); try (StatefulRedisConnection<String, String> connection = pool.borrowObject()) { RedisCommands<String, String> command = connection.sync(); SetArgs setArgs = SetArgs.Builder.nx().ex(5); command.set("name", "throwable", setArgs); String n = command.get("name"); log.info("Get value:{}", n); } pool.close(); redisClient.shutdown(); } 复制代码
其中,同步连接的池化支持需要用ConnectionPoolSupport
,异步连接的池化支持需要用AsyncConnectionPoolSupport
(Lettuce
5.1之后才支持)。
几个常见的渐进式删除例子
渐进式删除Hash中的域-属性:
@Test public void testDelBigHashKey() throws Exception { // SCAN参数 ScanArgs scanArgs = ScanArgs.Builder.limit(2); // TEMP游标 ScanCursor cursor = ScanCursor.INITIAL; // 目标KEY String key = "BIG_HASH_KEY"; prepareHashTestData(key); log.info("开始渐进式删除Hash的元素..."); int counter = 0; do { MapScanCursor<String, String> result = COMMAND.hscan(key, cursor, scanArgs); // 重置TEMP游标 cursor = ScanCursor.of(result.getCursor()); cursor.setFinished(result.isFinished()); Collection<String> fields = result.getMap().values(); if (!fields.isEmpty()) { COMMAND.hdel(key, fields.toArray(new String[0])); } counter++; } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished())); log.info("渐进式删除Hash的元素完毕,迭代次数:{} ...", counter); } private void prepareHashTestData(String key) throws Exception { COMMAND.hset(key, "1", "1"); COMMAND.hset(key, "2", "2"); COMMAND.hset(key, "3", "3"); COMMAND.hset(key, "4", "4"); COMMAND.hset(key, "5", "5"); } 复制代码
渐进式删除集合中的元素:
@Test public void testDelBigSetKey() throws Exception { String key = "BIG_SET_KEY"; prepareSetTestData(key); // SCAN参数 ScanArgs scanArgs = ScanArgs.Builder.limit(2); // TEMP游标 ScanCursor cursor = ScanCursor.INITIAL; log.info("开始渐进式删除Set的元素..."); int counter = 0; do { ValueScanCursor<String> result = COMMAND.sscan(key, cursor, scanArgs); // 重置TEMP游标 cursor = ScanCursor.of(result.getCursor()); cursor.setFinished(result.isFinished()); List<String> values = result.getValues(); if (!values.isEmpty()) { COMMAND.srem(key, values.toArray(new String[0])); } counter++; } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished())); log.info("渐进式删除Set的元素完毕,迭代次数:{} ...", counter); } private void prepareSetTestData(String key) throws Exception { COMMAND.sadd(key, "1", "2", "3", "4", "5"); } 复制代码
渐进式删除有序集合中的元素:
@Test public void testDelBigZSetKey() throws Exception { // SCAN参数 ScanArgs scanArgs = ScanArgs.Builder.limit(2); // TEMP游标 ScanCursor cursor = ScanCursor.INITIAL; // 目标KEY String key = "BIG_ZSET_KEY"; prepareZSetTestData(key); log.info("开始渐进式删除ZSet的元素..."); int counter = 0; do { ScoredValueScanCursor<String> result = COMMAND.zscan(key, cursor, scanArgs); // 重置TEMP游标 cursor = ScanCursor.of(result.getCursor()); cursor.setFinished(result.isFinished()); List<ScoredValue<String>> scoredValues = result.getValues(); if (!scoredValues.isEmpty()) { COMMAND.zrem(key, scoredValues.stream().map(ScoredValue<String>::getValue).toArray(String[]::new)); } counter++; } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished())); log.info("渐进式删除ZSet的元素完毕,迭代次数:{} ...", counter); } private void prepareZSetTestData(String key) throws Exception { COMMAND.zadd(key, 0, "1"); COMMAND.zadd(key, 0, "2"); COMMAND.zadd(key, 0, "3"); COMMAND.zadd(key, 0, "4"); COMMAND.zadd(key, 0, "5"); } 复制代码
在SpringBoot中使用Lettuce
个人认为,spring-data-redis
中的API
封装并不是很优秀,用起来比较重,不够灵活,这里结合前面的例子和代码,在SpringBoot
脚手架项目中配置和整合Lettuce
。先引入依赖:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.8.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.1.8.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> </dependencies> 复制代码
一般情况下,每个应用应该使用单个Redis
客户端实例和单个连接实例,这里设计一个脚手架,适配单机、普通主从、哨兵和集群四种使用场景。对于客户端资源,采用默认的实现即可。对于Redis
的连接属性,比较主要的有Host
、Port
和Password
,其他可以暂时忽略。基于约定大于配置的原则,先定制一系列属性配置类(其实有些配置是可以完全共用,但是考虑到要清晰描述类之间的关系,这里拆分多个配置属性类和多个配置方法):
@Data @ConfigurationProperties(prefix = "lettuce") public class LettuceProperties { private LettuceSingleProperties single; private LettuceReplicaProperties replica; private LettuceSentinelProperties sentinel; private LettuceClusterProperties cluster; } @Data public class LettuceSingleProperties { private String host; private Integer port; private String password; } @EqualsAndHashCode(callSuper = true) @Data public class LettuceReplicaProperties extends LettuceSingleProperties { } @EqualsAndHashCode(callSuper = true) @Data public class LettuceSentinelProperties extends LettuceSingleProperties { private String masterId; } @EqualsAndHashCode(callSuper = true) @Data public class LettuceClusterProperties extends LettuceSingleProperties { } 复制代码
配置类如下,主要使用@ConditionalOnProperty
做隔离,一般情况下,很少有人会在一个应用使用一种以上的Redis
连接场景:
@RequiredArgsConstructor @Configuration @ConditionalOnClass(name = "io.lettuce.core.RedisURI") @EnableConfigurationProperties(value = LettuceProperties.class) public class LettuceAutoConfiguration { private final LettuceProperties lettuceProperties; @Bean(destroyMethod = "shutdown") public ClientResources clientResources() { return DefaultClientResources.create(); } @Bean @ConditionalOnProperty(name = "lettuce.single.host") public RedisURI singleRedisUri() { LettuceSingleProperties singleProperties = lettuceProperties.getSingle(); return RedisURI.builder() .withHost(singleProperties.getHost()) .withPort(singleProperties.getPort()) .withPassword(singleProperties.getPassword()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.single.host") public RedisClient singleRedisClient(ClientResources clientResources, @Qualifier("singleRedisUri") RedisURI redisUri) { return RedisClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.single.host") public StatefulRedisConnection<String, String> singleRedisConnection(@Qualifier("singleRedisClient") RedisClient singleRedisClient) { return singleRedisClient.connect(); } @Bean @ConditionalOnProperty(name = "lettuce.replica.host") public RedisURI replicaRedisUri() { LettuceReplicaProperties replicaProperties = lettuceProperties.getReplica(); return RedisURI.builder() .withHost(replicaProperties.getHost()) .withPort(replicaProperties.getPort()) .withPassword(replicaProperties.getPassword()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.replica.host") public RedisClient replicaRedisClient(ClientResources clientResources, @Qualifier("replicaRedisUri") RedisURI redisUri) { return RedisClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.replica.host") public StatefulRedisMasterSlaveConnection<String, String> replicaRedisConnection(@Qualifier("replicaRedisClient") RedisClient replicaRedisClient, @Qualifier("replicaRedisUri") RedisURI redisUri) { return MasterSlave.connect(replicaRedisClient, new Utf8StringCodec(), redisUri); } @Bean @ConditionalOnProperty(name = "lettuce.sentinel.host") public RedisURI sentinelRedisUri() { LettuceSentinelProperties sentinelProperties = lettuceProperties.getSentinel(); return RedisURI.builder() .withPassword(sentinelProperties.getPassword()) .withSentinel(sentinelProperties.getHost(), sentinelProperties.getPort()) .withSentinelMasterId(sentinelProperties.getMasterId()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.sentinel.host") public RedisClient sentinelRedisClient(ClientResources clientResources, @Qualifier("sentinelRedisUri") RedisURI redisUri) { return RedisClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.sentinel.host") public StatefulRedisMasterSlaveConnection<String, String> sentinelRedisConnection(@Qualifier("sentinelRedisClient") RedisClient sentinelRedisClient, @Qualifier("sentinelRedisUri") RedisURI redisUri) { return MasterSlave.connect(sentinelRedisClient, new Utf8StringCodec(), redisUri); } @Bean @ConditionalOnProperty(name = "lettuce.cluster.host") public RedisURI clusterRedisUri() { LettuceClusterProperties clusterProperties = lettuceProperties.getCluster(); return RedisURI.builder() .withHost(clusterProperties.getHost()) .withPort(clusterProperties.getPort()) .withPassword(clusterProperties.getPassword()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.cluster.host") public RedisClusterClient redisClusterClient(ClientResources clientResources, @Qualifier("clusterRedisUri") RedisURI redisUri) { return RedisClusterClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.cluster") public StatefulRedisClusterConnection<String, String> clusterConnection(RedisClusterClient clusterClient) { return clusterClient.connect(); } } 复制代码
最后为了让IDE
识别我们的配置,可以添加IDE
亲缘性,/META-INF
文件夹下新增一个文件spring-configuration-metadata.json
,内容如下:
{ "properties": [ { "name": "lettuce.single", "type": "club.throwable.spring.lettuce.LettuceSingleProperties", "description": "单机配置", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" }, { "name": "lettuce.replica", "type": "club.throwable.spring.lettuce.LettuceReplicaProperties", "description": "主从配置", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" }, { "name": "lettuce.sentinel", "type": "club.throwable.spring.lettuce.LettuceSentinelProperties", "description": "哨兵配置", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" }, { "name": "lettuce.single", "type": "club.throwable.spring.lettuce.LettuceClusterProperties", "description": "集群配置", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" } ] } 复制代码
如果想IDE
亲缘性做得更好,可以添加/META-INF/additional-spring-configuration-metadata.json
进行更多细节定义。简单使用如下:
@Slf4j @Component public class RedisCommandLineRunner implements CommandLineRunner { @Autowired @Qualifier("singleRedisConnection") private StatefulRedisConnection<String, String> connection; @Override public void run(String... args) throws Exception { RedisCommands<String, String> redisCommands = connection.sync(); redisCommands.setex("name", 5, "throwable"); log.info("Get value:{}", redisCommands.get("name")); } } // Get value:throwable 复制代码
小结
本文算是基于Lettuce
的官方文档,对它的使用进行全方位的分析,包括主要功能、配置都做了一些示例,限于篇幅部分特性和配置细节没有分析。Lettuce
已经被spring-data-redis
接纳作为官方的Redis
客户端驱动,所以值得信赖,它的一些API
设计确实比较合理,扩展性高的同时灵活性也高。个人建议,基于Lettuce
包自行添加配置到SpringBoot
应用用起来会得心应手,毕竟RedisTemplate
实在太笨重,而且还屏蔽了Lettuce
一些高级特性和灵活的API
。
参考资料: