SpringBoot集成Lettuce连接Redis:从基础配置到生产级优化实战

📅 2026/8/23 5:08:55
SpringBoot集成Lettuce连接Redis:从基础配置到生产级优化实战
1. 项目概述与核心价值最近在几个新项目里我又一次用到了SpringBoot集成Redis这套经典组合。不过这次我没有选择大家更熟悉的Jedis而是全面转向了Lettuce。原因很简单在微服务架构和高并发场景下Lettuce带来的性能提升和资源管理优势是实实在在能感受到的。很多朋友可能还停留在“SpringBoot Redis Jedis”的认知里或者虽然知道Lettuce但对其核心特性和最佳实践了解不深。今天我就以一个实际项目为背景从头到尾拆解一遍SpringBoot集成Lettuce连接Redis的完整流程、核心配置、高级用法以及那些容易踩坑的细节。无论你是刚开始接触还是想优化现有的集成方案这篇文章都能给你提供一份可以直接“抄作业”的实操指南。简单来说Lettuce是一个高性能、线程安全的Redis客户端它基于Netty构建支持响应式编程并且提供了连接池和异步操作等高级特性。在SpringBoot 2.x版本之后它已经取代Jedis成为Spring Data Redis默认的底层客户端。这意味着即使你在pom.xml里没有显式声明只要引入了spring-boot-starter-data-redis用的就是Lettuce。但“默认”不等于“最优”不经过合理配置你可能无法发挥其全部潜力甚至遇到连接泄漏、性能瓶颈等问题。接下来我们就从环境搭建开始一步步深入。2. 环境准备与项目初始化2.1 依赖引入与版本选择首先创建一个标准的SpringBoot项目。在pom.xml中核心依赖其实只需要一个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这个starter会自动引入spring-data-redis和lettuce-core。这里有一个关键点务必关注Lettuce的版本。SpringBoot的父POM会管理一个默认版本但有时为了修复特定Bug或使用新特性我们需要手动升级。你可以通过mvn dependency:tree命令查看实际引入的版本。例如在SpringBoot 2.7.x中默认的Lettuce版本可能是6.1.x。如果遇到连接超时或集群拓扑刷新问题可以考虑升级到6.2.x或更高版本。!-- 可选显式指定Lettuce版本 -- properties lettuce.version6.2.4.RELEASE/lettuce.version /properties dependency groupIdio.lettuce/groupId artifactIdlettuce-core/artifactId version${lettuce.version}/version /dependency为什么选择Lettuce而不是Jedis这是最常被问到的问题。Jedis是直连模式每个线程操作需要从连接池获取物理连接多线程环境下存在竞争。而Lettuce的连接是基于Netty的事件驱动模型一个连接StatefulRedisConnection可以在多个线程间共享通过异步方式处理所有请求减少了物理连接数量在高并发下资源利用率和吞吐量显著更高。简单类比Jedis像是一个需要频繁借还的“公共自行车”而Lettuce则像是一个高效的“共享巴士线路”。2.2 基础配置文件解析接下来是application.yml或application.properties的配置。基础的单机Redis配置大家都很熟悉spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword # 如果没有密码可以省略或留空 database: 0 # 默认使用0号数据库 lettuce: pool: max-active: 8 # 连接池最大连接数使用负值表示没有限制 max-idle: 8 # 连接池中的最大空闲连接 min-idle: 0 # 连接池中的最小空闲连接 max-wait: -1ms # 连接池最大阻塞等待时间使用负值表示无限等待 shutdown-timeout: 100ms # 关闭超时时间这里重点看lettuce.pool配置。虽然Lettuce本身基于共享连接性能优异但SpringBoot默认还是为其包装了一个通用的连接池实际上是commons-pool2。在绝大多数生产环境中我建议保留并合理配置连接池。为什么因为连接池可以管理连接的创建和销毁避免频繁建立TCP连接的开销同时能防止因网络闪断导致连接失效后业务线程获取到不可用的连接。max-active不宜设置过大通常8-16对于大多数应用足够了设置过大会增加Redis服务器端的负载和内存消耗。max-wait设置为-1无限等待在生产环境有风险可能造成线程饥饿建议设置为一个合理的值如5000ms并做好获取连接失败的异常处理。3. 核心配置类与连接工厂定制仅仅依靠默认配置往往不能满足生产需求。我们需要通过Configuration类来深度定制RedisConnectionFactory和RedisTemplate。3.1 自定义Redis连接工厂配置创建一个配置类例如RedisConfigConfiguration public class RedisConfig { Bean public RedisConnectionFactory redisConnectionFactory(RedisProperties properties) { RedisStandaloneConfiguration config new RedisStandaloneConfiguration(); config.setHostName(properties.getHost()); config.setPort(properties.getPort()); config.setPassword(RedisPassword.of(properties.getPassword())); config.setDatabase(properties.getDatabase()); // 构建Lettuce连接工厂 LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(2)) // 命令超时时间 .shutdownTimeout(Duration.ofMillis(100)) // 关闭超时 .clientOptions(ClientOptions.builder() .autoReconnect(true) // 自动重连 .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) // 断开时拒绝新命令 .build()) .build(); return new LettuceConnectionFactory(config, clientConfig); } }在这个配置中有几个关键点commandTimeout这是最重要的参数之一。它定义了Redis客户端等待服务器响应的最长时间。设置过短在Redis负载高或网络波动时容易超时设置过长可能导致故障线程被长时间挂起。根据业务容忍度和Redis集群性能通常设置在1-5秒。autoReconnect必须设置为true。这样在网络临时中断恢复后Lettuce会自动尝试重建连接对业务无感知。disconnectedBehavior我推荐设置为REJECT_COMMANDS。这意味着当连接断开时新的命令会立即收到异常而不是进入一个等待队列。这符合“快速失败”的原则让上层业务能及时感知故障并采取降级策略避免请求堆积。3.2 定制RedisTemplate解决序列化痛点SpringBoot默认提供的RedisTemplate的键值序列化器是JdkSerializationRedisSerializer。这会导致两个严重问题一是序列化后的键值可读性极差在Redis客户端里看到的是乱码二是不同JVM环境或类加载器可能导致反序列化失败。因此定制RedisTemplate是必须的。Configuration public class RedisConfig { // ... 上面的 redisConnectionFactory Bean ... Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); // 使用StringRedisSerializer来序列化和反序列化redis的key值 StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value值 GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }序列化器选择解析Key序列化器 (StringRedisSerializer)将String转换为byte[]存储到Redis里是人类可读的字符串。这是最通用的选择因为Redis的Key通常是字符串标识符。Value序列化器 (GenericJackson2JsonRedisSerializer)使用Jackson库将对象序列化为JSON字符串存储。优点是可读性好不同语言如Python、Node.js也能读取并且会在JSON中存入对象的类名信息class反序列化时能还原成正确的Java类型。缺点是相比专门的二进制序列化如Kryo体积稍大速度稍慢。但对于绝大多数业务场景JSON的通用性和可调试性优势更大。注意如果你确定Value只存储简单的String类型也可以使用StringRedisSerializer。但为了灵活性我通常统一使用JSON序列化器。4. 基础与高级操作案例实战配置好了我们来实战操作。Spring Data Redis提供了两种主要的操作方式RedisTemplate和StringRedisTemplate后者是前者的特化键值都使用String序列化器。我们使用自定义的RedisTemplate。4.1 注入与基础数据类型操作首先在Service中注入定制的RedisTemplateService public class RedisService { Autowired private RedisTemplateString, Object redisTemplate; // 获取实际操作对象针对不同数据结构 private ValueOperationsString, Object valueOps() { return redisTemplate.opsForValue(); } private HashOperationsString, String, Object hashOps() { return redisTemplate.opsForHash(); } private ListOperationsString, Object listOps() { return redisTemplate.opsForList(); } // ... 类似地获取 SetOperations, ZSetOperations }字符串String操作示例public void stringOperations() { // 设置值10秒后过期 valueOps().set(user:1001:name, 张三, Duration.ofSeconds(10)); // 仅当key不存在时设置实现分布式锁的基础 Boolean success valueOps().setIfAbsent(lock:order, locked, Duration.ofSeconds(30)); // 获取值 String name (String) valueOps().get(user:1001:name); // 原子递增 Long newCount valueOps().increment(article:1001:view); }哈希Hash操作示例适合存储对象public void hashOperations() { MapString, Object userMap new HashMap(); userMap.put(name, 李四); userMap.put(age, 28); userMap.put(city, 北京); // 存储整个Map hashOps().putAll(user:1002, userMap); // 获取单个字段 String city (String) hashOps().get(user:1002, city); // 增量修改某个字段 hashOps().increment(user:1002, age, 1L); }4.2 发布订阅Pub/Sub模式Lettuce天然支持异步和响应式编程这让实现发布订阅模式非常简洁。首先定义一个消息监听器Component public class RedisMessageListener implements MessageListener { Override public void onMessage(Message message, byte[] pattern) { String channel new String(message.getChannel()); String body new String(message.getBody()); System.out.println(收到频道 [ channel ] 的消息: body); // 这里进行具体的业务处理 } }然后在配置类中注册这个监听器到容器Configuration public class RedisConfig { Bean public RedisMessageListenerContainer messageListenerContainer(RedisConnectionFactory connectionFactory, RedisMessageListener listener) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); // 订阅频道 “news” Topic topic new ChannelTopic(news); container.addMessageListener(listener, topic); // 也可以订阅模式匹配的频道如 “log.*” // Topic patternTopic new PatternTopic(log.*); // container.addMessageListener(listener, patternTopic); return container; } }发布消息就很简单了Service public class NewsService { Autowired private RedisTemplateString, Object redisTemplate; public void publishNews(String news) { redisTemplate.convertAndSend(news, news); } }实操心得Pub/Sub模式是解耦系统的好工具比如用于系统间通知、配置刷新、缓存失效广播等。但要注意它是非持久化的。如果订阅者不在线消息就丢失了。对于要求消息必达的场景应该考虑更成熟的消息队列如RabbitMQ或Kafka。4.3 管道Pipelining与事务Transaction管道Pipelining用于批量执行多个命令减少网络往返RTT时间显著提升批量操作的性能。Lettuce通过RedisTemplate的executePipelined方法支持。public ListObject pipelineExample() { // 执行管道操作 ListObject results redisTemplate.executePipelined(new SessionCallbackObject() { Override public Object execute(RedisOperations operations) throws DataAccessException { ValueOperations ops operations.opsForValue(); for (int i 0; i 100; i) { ops.set(pipeline:key: i, value i); } // 注意管道内命令的返回值在管道执行完毕后统一返回 return null; } }); // results 包含了所有 set 命令的返回结果通常是 OK return results; }事务Transaction通过MULTI和EXEC命令保证一系列操作的原子性。在Spring中可以通过SessionCallback配合multi()和exec()来使用。public void transactionExample() { ListObject txResults redisTemplate.execute(new SessionCallbackListObject() { Override public ListObject execute(RedisOperations operations) throws DataAccessException { operations.multi(); // 开启事务 operations.opsForValue().set(tx:key1, value1); operations.opsForValue().increment(tx:counter); operations.opsForSet().add(tx:set, member); return operations.exec(); // 执行事务返回结果列表 } }); // txResults 包含三个命令的执行结果 }重要区别Redis的事务不是关系型数据库的ACID事务。它不支持回滚Rollback。如果在exec()之前有命令语法错误整个事务会被丢弃如果在exec()时运行时出错比如对字符串进行incr只有出错的命令会失败其他命令依然会执行。所以Redis事务更准确的叫法是“命令打包执行”。5. 生产级进阶配置与优化当项目从开发环境走向生产环境尤其是面对集群、哨兵模式或者需要更精细的资源控制时基础配置就不够用了。5.1 连接Redis集群与哨兵模式Redis集群模式配置spring: redis: cluster: nodes: 192.168.1.101:7001,192.168.1.102:7002,192.168.1.103:7003 max-redirects: 3 # 最大重定向次数 password: cluster-password lettuce: pool: max-active: 16 max-idle: 8 min-idle: 4在代码配置类中需要使用RedisClusterConfigurationBean public RedisConnectionFactory lettuceConnectionFactory(RedisProperties properties) { RedisClusterConfiguration clusterConfig new RedisClusterConfiguration(); // 解析配置文件中的节点列表 String[] nodes StringUtils.commaDelimitedListToStringArray(properties.getCluster().getNodes()); for (String node : nodes) { String[] hostPort StringUtils.split(node, :); clusterConfig.addClusterNode(new RedisNode(hostPort[0].trim(), Integer.parseInt(hostPort[1].trim()))); } clusterConfig.setMaxRedirects(properties.getCluster().getMaxRedirects()); clusterConfig.setPassword(RedisPassword.of(properties.getPassword())); LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从副本读取提升读性能 .commandTimeout(Duration.ofSeconds(3)) .build(); return new LettuceConnectionFactory(clusterConfig, clientConfig); }关键配置ReadFrom.REPLICA_PREFERRED这指示Lettuce优先从副本节点读取数据从而分摊主节点的读压力提升整体吞吐量。其他策略还有MASTER只从主节点读、REPLICA只从副本读、MASTER_PREFERRED优先主节点等。Redis哨兵模式配置spring: redis: sentinel: master: mymaster # 主节点名称 nodes: 192.168.1.201:26379,192.168.1.202:26379,192.168.1.203:26379 password: sentinel-password对应的Java配置使用RedisSentinelConfiguration。5.2 连接池深度调优与监控虽然Lettuce连接开销小但生产环境仍建议使用连接池。除了max-active等基础参数还有一些隐藏参数需要关注。我们可以通过自定义GenericObjectPoolConfig来精细控制Bean public LettuceConnectionFactory redisConnectionFactory(RedisProperties properties) { // ... 省略 RedisStandaloneConfiguration 或集群/哨兵配置 ... GenericObjectPoolConfigObject poolConfig new GenericObjectPoolConfig(); poolConfig.setMaxTotal(properties.getLettuce().getPool().getMaxActive()); poolConfig.setMaxIdle(properties.getLettuce().getPool().getMaxIdle()); poolConfig.setMinIdle(properties.getLettuce().getPool().getMinIdle()); poolConfig.setMaxWait(properties.getLettuce().getPool().getMaxWait()); // 重要开启空闲连接逐出和测试 poolConfig.setTestWhileIdle(true); // 空闲时是否测试连接有效性 poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30)); // 逐出扫描间隔 poolConfig.setMinEvictableIdleTime(Duration.ofSeconds(60)); // 连接最小空闲时间低于此值不被逐出 LettucePoolingClientConfiguration clientConfig LettucePoolingClientConfiguration.builder() .poolConfig(poolConfig) .commandTimeout(Duration.ofSeconds(2)) .shutdownTimeout(Duration.ofMillis(200)) .build(); return new LettuceConnectionFactory(standaloneConfig, clientConfig); }监控连接池状态可以将连接池注册到Spring Boot Actuator或通过JMX进行监控关注numActive活跃连接数、numIdle空闲连接数、numWaiters等待连接的线程数等指标。如果numWaiters持续大于0说明max-active可能设置偏小。5.3 超时与重试策略配置网络是不稳定的完善的超时和重试策略是系统韧性的保障。在LettuceClientConfiguration中可以进行全局配置LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(2)) // 单命令超时 .shutdownTimeout(Duration.ofMillis(500)) // 关闭客户端超时 .clientOptions(ClientOptions.builder() .socketOptions(SocketOptions.builder() .connectTimeout(Duration.ofSeconds(1)) // 连接建立超时 .keepAlive(true) // 开启TCP keepalive .build()) .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(5))) // 启用命令超时 .autoReconnect(true) // 自动重连 .cancelCommandsOnReconnectFailure(true) // 重连失败时取消挂起命令 .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) .build()) .clientResources(ClientResources.builder() .ioThreadPoolSize(4) // I/O线程数通常设置为CPU核心数 .computationThreadPoolSize(4) // 计算线程数 .build()) .build();connectTimeout建立TCP连接的超时时间不宜过长。TimeoutOptions.enabled启用Lettuce内置的命令超时控制比单纯依赖commandTimeout更精细。ClientResources用于配置Lettuce底层的Netty线程池。在Web容器如Tomcat中如果I/O线程数设置过多可能会与容器线程竞争。通常保持默认或设置为CPU核心数即可。6. 常见问题排查与性能优化实战在实际使用中一定会遇到各种问题。这里我总结几个最典型的案例和排查思路。6.1 连接超时与连接泄漏排查问题现象应用运行一段时间后出现RedisCommandTimeoutException或者监控发现Redis连接数持续增长不释放。排查步骤检查连接池配置确认max-active是否设置过小。通过redisTemplate.getConnectionFactory().getConnection().info(“stats”)可以查看Redis服务端的连接数。如果客户端连接数远大于max-active说明存在连接泄漏。检查命令超时时间commandTimeout是否设置过短在Redis执行slowlog get命令查看是否有慢查询阻塞了后续命令。检查连接泄漏这是Lettuce集成中最常见的问题之一。确保每次从连接池获取RedisConnection后必须在finally块中关闭。但更常见的是RedisTemplate在序列化/反序列化异常时没有正确关闭连接。一个有效的排查方法是在测试环境将连接池的removeAbandonedTimeout如果使用commons-pool2设置一个较短时间如30秒并开启logAbandoned观察日志中是否有连接泄露的警告。使用连接池监控通过JMX或Actuator的/actuator/metrics/redis.pool.*端点监控连接池的活跃、空闲、等待数量变化趋势。优化建议为所有Redis操作添加统一的异常处理确保连接被安全释放。考虑使用Transactional注解时注意其边界避免一个事务内包含过多的Redis操作或耗时操作导致连接占用时间过长。6.2 序列化导致的存储异常问题现象存入Redis的对象取出来时类型转换错误ClassCastException或者使用redis-cli看到的value是乱码。原因与解决序列化器不一致这是最根本的原因。确保写入和读取使用的是同一个RedisTemplate实例且其序列化配置完全相同。在微服务架构中如果多个服务共用Redis需要约定统一的序列化协议如都使用JSON。类路径变化使用GenericJackson2JsonRedisSerializer时JSON中存储了类全限定名。如果存储后类的包名修改了反序列化就会失败。对于这种场景可以考虑自定义ObjectMapper配置JsonTypeInfo使用更稳定的类型标识或者直接使用StringRedisSerializer存储手动序列化的JSON字符串。乱码问题如果redis-cli中看到\xac\xed\x00\x05t\x00\x05value这类乱码说明使用了默认的JDK序列化器。按照本文第3.2节的方法将其替换为StringRedisSerializer或GenericJackson2JsonRedisSerializer即可。6.3 集群环境下的拓扑刷新与路由问题现象在Redis集群模式下进行扩容增加分片或缩容后客户端出现MOVED或ASK错误或者部分请求失败。原理与配置Redis集群数据分布在不同的槽slot上。当集群拓扑变化主从切换、节点增减时客户端需要更新本地的槽位-节点映射关系。Lettuce会自动处理MOVED重定向并可以定期刷新集群拓扑。LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(3)) .clientOptions(ClientOptions.builder() .autoReconnect(true) .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) .topologyRefreshOptions( // 拓扑刷新配置 TopologyRefreshOptions.builder() .enablePeriodicRefresh(Duration.ofMinutes(10)) // 定期刷新 .enableAllAdaptiveRefreshTriggers() // 自适应刷新收到MOVED/ASK错误时 .build()) .build()) .build();enablePeriodicRefresh即使没有错误也定期如10分钟从集群拉取最新拓扑适合稳定的集群。enableAllAdaptiveRefreshTriggers在收到MOVED或ASK重定向错误时立即触发拓扑刷新。这在集群变更期间非常有用。实操心得在生产环境建议同时开启定期刷新和自适应刷新。拓扑刷新的过程会阻塞所有命令直到完成因此刷新间隔不宜过短。在集群执行CLUSTER FAILOVER或RESHARD操作前最好能通过运维手段通知应用或确保客户端配置了合理的重试和超时机制。6.4 性能瓶颈分析与优化建议如果发现Redis操作变慢可以按以下顺序排查Redis服务器本身通过redis-cli执行INFO commandstats查看命令耗时统计。执行SLOWLOG GET 10查看最近慢查询。检查服务器CPU、内存、网络带宽。客户端网络使用ping或redis-benchmark测试客户端到Redis服务器的网络延迟。Lettuce客户端配置连接池不足增加max-active但需同步观察Redis服务端连接数。线程池竞争检查ClientResources的ioThreadPoolSize是否过小。对于并发量极高的应用可以适当调大但不要超过CPU核心数太多。序列化开销对于存储大对象超过10KBGenericJackson2JsonRedisSerializer的序列化/反序列化可能成为瓶颈。可以考虑使用更高效的序列化工具如Kryo或Protostuff但需要自己实现RedisSerializer接口并牺牲可读性。业务代码层面避免大Key单个String类型的Value不应超过10KBHash、List等集合元素的平均大小也应控制。大Key会阻塞Redis影响稳定性。使用管道对于批量写入或读取务必使用executePipelined。避免频繁连接确保操作复用同一个RedisTemplate和底层的连接池。7. 测试策略与集成验证为了保证集成的可靠性必须编写完善的测试。7.1 单元测试使用嵌入式Redis对于开发环境和CI/CD流水线可以使用嵌入式Redis服务器避免依赖外部环境。dependency groupIdit.ozimov/groupId artifactIdembedded-redis/artifactId version0.7.3/version scopetest/scope /dependencySpringBootTest ActiveProfiles(test) public class RedisServiceTest { Autowired private RedisService redisService; Test void testSetAndGet() { redisService.set(testKey, testValue); String value redisService.get(testKey); assertEquals(testValue, value); } }在src/test/resources/application-test.yml中配置嵌入式Redis的端口。7.2 集成测试针对真实环境单元测试通过后还需要在类生产环境Staging进行集成测试重点验证集群模式下的读写数据是否正确地分布在不同的分片故障转移手动触发主节点故障观察客户端能否自动切换到新主节点业务是否受影响应有短暂超时。网络分区模拟网络抖动验证autoReconnect和重试机制是否生效。性能压测使用JMeter或自定义脚本模拟高并发场景观察响应时间、错误率和连接池指标。7.3 健康检查与就绪探针在K8s等容器化环境中需要为应用添加健康检查。Spring Boot Actuator提供了Redis健康指示器。management: endpoint: health: show-details: always health: redis: enabled: true访问/actuator/health可以查看Redis连接状态。在K8s的readinessProbe中配置此端点可以确保只有当Redis连接正常时流量才会被导入Pod。集成Lettuce连接Redis是一个从“能用”到“好用”再到“稳定高效”的持续优化过程。从最开始的基础配置、序列化器选择到生产环境下的连接池调优、超时重试策略、集群拓扑刷新每一个环节都需要根据实际业务场景和运维环境进行仔细考量。我个人的经验是前期多花时间在配置和测试上制定好规范如Key命名规范、序列化协议远比后期出了问题再救火要划算得多。最后再分享一个小技巧对于复杂的Redis操作可以考虑将其封装在独立的Service中并在内部做好异常处理、日志记录和指标上报如使用Micrometer这样业务代码会更清晰也更容易进行统一的监控和治理。