半年前接手一套用户行为分析服务时我拿到的原始需求并不复杂既要有订单、用户这类强事务数据也要承接前端埋点产生的高并发日志流。第一版方案贪省事全部丢进MySQL结果单表冲到三千万行之后慢查询和锁竞争此起彼伏第二版拆成两个独立服务读写倒是顺畅了但联调、部署、数据一致性全都变成负担。最后落在眼前的这套方案上一套Spring服务内MySQL承载业务主数据HBase承载行为日志流水通过数据源路由把MyBatis和HBase客户端组织在一起。这篇博文就把这套方案从原理到落地、再到排查踩坑的整个过程完整写出来给正在做同类多数据源集成的朋友一份能直接操作的参考。1. 为什么MySQL和HBase会在同一个服务里出现数据特性与典型场景1.1 MySQL和HBase的“性格差异”先说一个反直觉的认知多数据源不是设计出来的秀操作而是数据属性本身逼出来的选择。MySQL是行式存储的关系型数据库擅长事务、约束、表间关联行列锁和MVCC机制保证强一致HBase是列式存储的分布式NoSQL擅长海量写入、水平扩展以及按RowKey的高效点查和范围扫描。用户问我为什么不用Redis、不用MongoDB、不用TiDB答案很简单业务里同时存在“账户余额”和“用户点击流”两种数据前者要精确、少出错后者要吞吐、不求强实时两个最合适的引擎天然不同放到一个存储里总得牺牲一头。拿一个典型的电商浏览链路举例用户点进商品页产生的浏览行为日志是一天几亿条的规模写入峰值很高但几乎没有跨条更新需求也不怎么需要JOIN而用户收货地址、订单状态这些数据必须严格保证事务任何一条都不能丢。把订单数据塞进HBase那种按列族扫描的模型里写性能和扩展性也许够但涉及订单防重复、状态机流转时事务和约束会把人折腾疯。反过来把行为日志塞进MySQL单表很快爆炸分库分表之后查询习惯和运维复杂度又上来了。结论很直接两种存储各管一段再用一套服务统一入口。1.2 最常见的组合场景我接触过的项目里这套组合常见于三种场景。第一种是“主数据 流水”型用户、订单、商品放在MySQL用户操作流水、积分变动、登录日志放在HBase保存时各写各的查询时根据业务入口决定走哪条数据链路。第二种是“宽表 明细”型MySQL存聚合后的统计结果HBase存原始明细后台任务从HBase扫明细做重算再把结果回刷到MySQL。第三种是“冷热分离”型交易数据在MySQL正常读写超过90天的历史订单异步归档到HBase归档后MySQL只保留索引需要查看历史详情时再去HBase抓。这三种场景有一个共同点两个库之间不存在强事务依赖最多是“先写HBase再更新MySQL状态位”这种可以容忍短时间不一致的最终一致关系。这一点非常重要决定了整套方案在事务处理上能做到多简单。1.3 什么信号说明你需要这套架构如果你的系统满足以下任意两条基本就到了引入多数据源的时机单表数据量接近千万级且还在高速增长存在大量和企业主流程无关的存储型数据这些数据极少更新但必须保留多年写入峰值远超数据库连接池能支撑的合理上限而查询要求并不复杂。这时候与其无限堆MySQL分库分表不如早点把数据按属性分开。需要提醒的是多数据源会让项目复杂度上一个台阶如果业务数据量很小、或者一个库靠索引和归档就能扛住还是别折腾。代码量虽然看着不多但事务边界、监控、连接管理各种隐性成本都在后面等着。2. 多数据源方案的十字路口三种写法的取舍既然决定了MySQL和HBase并存就要面对一个绕不开的问题代码里怎么组织这两个数据源。我至少见过三种写法对应完全不同的复杂度和适用场景。2.1 两套SqlSessionFactory直接隔离最简单直白的做法是给MySQL配置一套SqlSessionFactory和Mapper给HBase也配置一套。Service层根据业务逻辑决定调用哪套DAO两套完全独立互不干扰。优点很明显代码好理解调试时不会出现“数据源到底切到了哪边”的困惑事务也能各管各的不会互相污染。缺点也很突出如果业务方法里需要同时操作两边开发者得手动编排两次调用而且如果系统里的数据源数量继续增加每个数据源一套Factory的重复代码会越堆越多。这种写法适合数据源边界非常清晰、且不会频繁切换的项目比如MySQL负责全部事务、HBase只由定时任务写入两边几乎没有交织。2.2 基于AbstractRoutingDataSource的动态路由Spring提供了一个抽象类AbstractRoutingDataSource它允许在每次获取数据库连接时根据determineCurrentLookupKey()的返回结果从一组目标数据源中选择实际使用的那个。这正好是“路由”的语义运行时决定这次操作走哪个库而不是编译期写死。我在之前的项目里就是用一个自定义的DynamicDataSource继承这个类配合ThreadLocal保存当前线程应使用的数据源标识再通过自定义注解和AOP切面在Service方法前后自动设置和清理标识。这样一来业务代码里不用显式判断走哪条数据源链路只需要在方法上打一个DS(mysql)或DS(hbase)注解即可。这个方案的优点是把“选数据源”这件事从业务代码中剥离多个数据源可以统一注册到路由表里后续加一个MySQL从库也能复用同一套路由机制。缺点是有一个隐藏很深的事务绑定问题后面第6章会展开讲。2.3 HBase到底要不要进MyBatis很多人看到“springmybatis集成HBase/MySQL多数据源”这个标题第一反应是把HBase也做成一个MyBatis的数据源让它像MySQL一样通过Mapper接口查询。这里必须先说清楚一个技术现实MyBatis本质上是为JDBC关系型数据库设计的ORM框架HBase并不提供关系型SQL接口也没有标准的JDBC驱动除非引入Apache Phoenix这种SQL层。直接把HBase塞进MyBatis的SqlSessionFactory里理论上需要写大量类型处理器和方言适配实际项目里这么干的基本都后悔了。所以我最终采用的方案是MyBatis只负责MySQL这条线HBase走独立封装的DAO层基于HBase原生Java客户端。数据源路由机制里MySQL作为默认数据源HBase不参与MyBatis的SqlSession管理而是通过Spring的依赖注入获得连接后自行完成读写。这样既实现了“一套服务内同时集成两个存储”又不会逼着MyBatis去干它不擅长的事。2.4 我最终的选择结合项目现状我选了组合策略MySQL用MyBatis 动态数据源路由HBase用原生DAO Spring容器管理连接。如果以后真需要在MyBatis里跑HBase的SQL查询我可以再在DAO外包装一层但那是后话不会在主力项目中直接铺开。如果你所在团队对HBase的SQL化有强需求可以评估Phoenix它能将HBase以JDBC形式暴露给MyBatis但要做好心理准备Phoenix的SQL方言和MySQL差异很大事务能力有限运维还要额外维护Phoenix服务端。它是一种可选路线不是默认路线。3. 核心代码动态数据源、注解与AOP切换这个方案大白话讲就是三个零件一个能按“钥匙”选数据源的DynamicDataSource一个用来放钥匙的ThreadLocal一个自动放钥匙和收钥匙的AOP切面。3.1 DynamicDataSource路由的中枢AbstractRoutingDataSource的工作机制很简单先在targetDataSources里注册若干个数据源每个数据源对应一个key每次Spring要拿数据库连接时框架调用determineCurrentLookupKey()用返回值去map里找具体的数据源对象。如果没有找到且设置了defaultTargetDataSource就回退到默认数据源。public class DynamicDataSource extends AbstractRoutingDataSource { public static final String MYSQL mysql; public static final String HBASE hbase; private static final ThreadLocalString CONTEXT new ThreadLocal(); public static void setKey(String key) { CONTEXT.set(key); } public static void clear() { CONTEXT.remove(); } Override protected Object determineCurrentLookupKey() { return CONTEXT.get(); } }注意ThreadLocal的清理问题。如果一次请求处理过程中设置了key结束时没有remove()线程池中的线程被复用后下一次请求可能会拿到上一个残留的数据源标识导致路由错误。这也是为什么切面既要设置又要清理的原因。3.2 为什么用ThreadLocal而不是方法参数有些朋友会问直接把数据源标识作为参数传下去不行吗答案是可以但会污染方法签名。业务方法里可能有很多层调用只需要顶层的Service方法知道本次操作应该走哪个库下层DAO统一从上下文取即可。ThreadLocal天然适合这种“隐式传递”场景它在同一个线程的任意深度都能读到同一个值。代价是它和线程绑定如果代码里出现异步线程、线程池跨线程传递业务数据就必须显式把标识传递过去。常见处理是用一个包装Runnable的工具类在提交任务前设置子线程的ThreadLocal或者在Spring的TaskDecorator里做上下文传递。3.3 自定义注解与AOP切面定义注解本身不复杂Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DS { String value() default mysql; }再写一个切面在方法执行前根据注解的值设置路由key方法结束清理Aspect Component public class DataSourceAspect { Before(annotation(dataSource)) public void switchDataSource(JoinPoint point, DS dataSource) { DynamicDataSource.setKey(dataSource.value()); } After(annotation(dataSource)) public void restoreDataSource(Pointcut point, DS dataSource) { DynamicDataSource.clear(); } }这里有两个容易被忽略的细节。第一如果一个方法上有Transactional又有DS事务管理器会在方法进入时优先获取连接并绑定到事务上下文切面设置路由key的时机如果晚于事务开启路由就失效所以DS要打在事务边界的最外层方法上且最好是先切数据源、再开事务。第二Spring AOP默认用动态代理同类内部方法的自调用不会经过切面比如Service的A方法标注了DS(hbase)A方法内部直接调用同类B方法B上的注解不会生效解决方法要么拆开两个Service要么注入代理对象。3.4 一个完整调用链路假设用户浏览了一个商品详情页代码可以这样组织Service public class ViewService { Autowired private UserMapper userMapper; Autowired private BehaviorLogDao behaviorLogDao; DS(mysql) public UserInfo getUserInfo(String userId) { return userMapper.selectByUserId(userId); } DS(hbase) public void saveBehaviorLog(String rowKey, String behavior) { behaviorLogDao.save(rowKey, behavior); } }实际执行时getUserInfo方法进入切面路由key设为mysqlMyBatis的SqlSession在获取连接时通过DynamicDataSource拿到MySQL连接执行SQLsaveBehaviorLog方法则没有经过MyBatis它直接调用HBase DAO数据源路由对它没有实际约束作用。这就是第2章讲的“组合策略”路由主要管MySQLHBase用独立DAO。4. MyBatis配置细节让Mapper归位到MySQL动态数据源类写好了接下来要让Spring容器里的数据源、SqlSessionFactory、事务管理器全部围绕这个动态源转起来。4.1 工程结构规划为了让Mapper归属清晰我的工程里把包按职责划分com.example.demo ├── config │ ├── DynamicDataSource.java │ ├── DataSourceConfig.java │ ├── MyBatisConfig.java │ └── DataSourceAspect.java ├── mapper │ ├── UserMapper.java │ └── UserMapper.xml ├── hbase │ ├── HBaseConfig.java │ └── BehaviorLogDao.java ├── annotation │ └── DS.java └── service ├── UserService.java └── BehaviorLogService.javamapper包下只放MySQL的Mapperhbase包下放HBase访问代码。这样即使过三个月再回来看也一眼知道哪个包管哪个存储。4.2 数据源Bean的配置MySQL这边我推荐HikariCP轻量而且性能稳定。配置一个标准的DataSourceBean public DataSource mysqlDataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/biz?useUnicodetruecharacterEncodingutf8); config.setUsername(app_user); config.setPassword(***); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setPoolName(mysqlPool); return new HikariDataSource(config); }maximumPoolSize不要盲目调大MySQL默认连接数有限每个连接有内存占用池子过大反而拖慢线程调度。一般业务系统压测时从20起步观察连接等待时间再调。然后注册动态数据源Bean public DynamicDataSource dynamicDataSource(Qualifier(mysqlDataSource) DataSource mysqlDataSource) { DynamicDataSource dynamicDataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(DynamicDataSource.MYSQL, mysqlDataSource); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(mysqlDataSource); return dynamicDataSource; }HBase既然不走MyBatis就没有必要注册进targetDataSources省得路由表里有数据源类型混乱的问题。如果后续接了Phoenix要进路由表再单独处理。4.3 SqlSessionFactory与Mapper扫描MyBatis的SqlSessionFactory必须绑定到动态数据源这样Mapper执行SQL时才走动态路由Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dynamicDataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/*.xml)); factoryBean.setTypeAliasesPackage(com.example.demo.entity); return factoryBean.getObject(); } Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }Mapper扫描用MapperScanConfiguration MapperScan(basePackages com.example.demo.mapper, sqlSessionTemplateRef sqlSessionTemplate) public class MyBatisConfig { }这里有个容易踩的坑如果项目里同时用了多个MapperScan指定到不同包并且每个都指向同一个SqlSessionTemplate启动时可能报“Property sqlSessionFactory or sqlSessionTemplate are required”的错误。检查点是每个MapperScan的sqlSessionTemplateRef是否写对了并且确保它指向的Bean存在于容器中。4.4 事务管理器的正确绑定事务管理器要绑定动态数据源还是真实MySQL数据源我的做法是绑定动态数据源Bean public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); }这样在Transactional方法内即便切了DS(mysql)也能正常工作因为事务管理器拿到的连接来自动态数据源而动态数据源此时路由到了MySQL。但如果Transactional方法内没有设置数据源key默认也会走MySQL这也是我特意把MySQL设为默认源的一个原因事务注解不指定数据源时行为保持和单一MySQL一致。需要特别说明的是千万不要在一个Transactional方法里去切HBase并期待事务回滚保护它HBase本身就不参与Spring声明式事务跨库时要么接受最终一致要么用业务补偿没有银弹事务方案。4.5 如果你用的是Spring Boot传统Spring的XML配置我就不重复贴了Spring Boot下的差异主要是用application.yml控制连接池参数以及用ConfigurationProperties绑定HikariDataSource。核心的DynamicDataSource、切面、Mapper扫描逻辑完全一致唯一需要注意是Boot的自动装配会优先创建默认数据源需要显式排除DataSourceAutoConfiguration否则容器里会出现两个数据源路由表混乱。SpringBootApplication(exclude {DataSourceAutoConfiguration.class}) public class Application { }这句话很多文章会一带而过但它真的是Boot多数据源配置的关键漏掉这一行启动不报错但运行时连接串到默认数据源上的情况非常常见。5. HBase访问层实现一个不依赖MyBatis的DAO层数据源路由和MyBatis都归位后接着要解决HBase怎么读写。前面反复强调过HBase不经过MyBatis这里说出具体实现。5.1 为什么单独写DAOHBase客户端的编程模型是Table、Put、Get、Scan那一套和MyBatis的Mapper模型完全是两套语言。把HBase操作封装成一个DAO可以让上层Service用类似dao.save()的语义调用也方便以后替换成Phoenix或HBase新客户端。DAO里只暴露业务方法不暴露HBase底层对象这样上层代码不会散落Put、Bytes.toBytes这种细节。5.2 HBase客户端配置与Connection管理HBase的Connection是线程安全的也是重量级对象一个进程共享一个就够了。官方的ConnectionFactory.createConnection(conf)返回的就是这种可共享连接。配置类如下Configuration public class HBaseConfig { Value(${hbase.zookeeper.quorum}) private String quorum; Value(${hbase.zookeeper.property.clientPort:2181}) private String clientPort; Bean(destroyMethod close) public Connection hbaseConnection() throws IOException { org.apache.hadoop.conf.Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, quorum); conf.set(hbase.zookeeper.property.clientPort, clientPort); return ConnectionFactory.createConnection(conf); } }注意destroyMethod close这个属性Spring容器关闭时会自动调用close()释放连接避免资源泄漏。如果你用的是HBase 2.x及以上版本客户端的连接方式有变化建议按官方文档调整配置项但基本原理一致维护一个长连接不要在每次请求里创建连接那会带来不可接受的RPC开销。5.3 封装常用读写操作一个最简单的写入方法Repository public class BehaviorLogDao { private static final TableName TABLE TableName.valueOf(behavior_log); private final Connection connection; public BehaviorLogDao(Connection connection) { this.connection connection; } public void save(String rowKey, String family, String qualifier, String value) { try (Table table connection.getTable(TABLE)) { Put put new Put(Bytes.toBytes(rowKey)); put.addColumn(Bytes.toBytes(family), Bytes.toBytes(qualifier), Bytes.toBytes(value)); table.put(put); } catch (IOException e) { throw new RuntimeException(写入HBase失败, e); } } public ListString scanByPrefix(String prefix) { Scan scan new Scan(); scan.setFilter(new PrefixFilter(Bytes.toBytes(prefix))); ListString rows new ArrayList(); try (Table table connection.getTable(TABLE); ResultScanner scanner table.getScanner(scan)) { for (Result result : scanner) { rows.add(Bytes.toString(result.getRow())); } return rows; } catch (IOException e) { throw new RuntimeException(扫描HBase失败, e); } } }两个细节值得注意第一Table实例不是线程安全的所以用try-with-resources在方法内获取和释放而不是把它当单例字段保存第二HBase的第一次访问会做ZooKeeper发现和Region定位如果提前把数据都缓存到客户端第一次慢是正常的后续同一Region的请求会复用缓存。5.4 RowKey与表设计的一点提醒既然标题里带了HBase顺便提一句HBase设计的关键词——RowKey就是一切。查询模式决定了RowKey怎么拼。行为日志这类场景我习惯用userId 时间戳倒置作为RowKey既保证同一用户的记录落在邻近Region又能用前缀扫描快速拉出某个用户一段时间的行为。如果想把所有用户的最新行为做全局扫描那就按时间桶 userId来组织具体取舍完全依赖业务列表。列族数量也注意克制超过两个列族的表在读写性能和Region管理上都会产生不必要开销大部分场景一个列族够用。6. 我在实际项目中踩过的坑切换失效、连接泄漏、Mapper扫描冲突这一节写的是真金白银换来的教训。动态数据源方案网上文章一抓一大把但真正上线后摔跟头的地方恰恰是那些看着“没问题”的细节。6.1 坑位一DS在Transactional方法内“失灵”现象是Service方法上同时标了DS(hbase)和Transactional结果HBase写入抛了异常想去查MySQL关联数据时MySQL连接还是老的或者路由切不过去甚至报了“IllegalStateException: Already value [...] for current thread”之类的错。根因在于Spring的事务同步机制。事务管理器在事务开始时从动态数据源拿了一个连接然后把这个连接绑定到当前线程的事务同步器上。之后业务方法里再次通过MyBatis拿连接MyBatis-Spring会因为当前线程存在活跃事务直接复用事务同步器里已经绑定的连接根本不重新走determineCurrentLookupKey()。于是路由key再变也没用连接已经钉死。我的处理办法是事务型操作全部限定在MySQL侧HBase的读写方法不做Transactional。如果A方法需要先写HBase、再更新MySQL就把两步拆成两个非事务方法串行调用HBase写完再更新MySQL状态位配合补偿任务兜底。这也是为什么第2章强调最终一致性优先不要指望声明式事务跨两种异构存储。6.2 坑位二HBase的Table用完不关句柄一路飙升早期代码里有人图省事把Connection.getTable()得到的Table对象缓存成成员变量复用了。启动几个小时后HBase服务端连接数猛增RegionServer日志里出现大量“too many connections”应用侧GC压力也上来了。原因就是Table实例操持有底层连接资源不复用、也不关闭长期持有会让连接池和RPC通道失去回收能力。修复方式是回到第5章那种try (Table table connection.getTable(TABLE))的写法用完即关。Connection保持单例Table按方法获取这是HBase客户端使用的基本盘。6.3 坑位三Spring Boot下多个DataSource导致自动装配串线在一套老代码里改造时我引入了DynamicDataSource但没有排除DataSourceAutoConfiguration启动没有报错所有Mapper表面正常但读出的数据一直是本地开发库的。排查半天才发现Boot自动创建了一个默认的DataSourceBeanMyBatis自动配置直接绑到了它身上动态数据源成了摆设。加了SpringBootApplication(exclude {DataSourceAutoConfiguration.class})之后容器里不再有隐形的默认数据源所有连接都必须从我自己定义的DynamicDataSource走。这个坑在传统Spring工程里不存在但Spring Boot项目很容易碰到建议一上来就排除自动配置。6.4 坑位四HBase连接初始化拖垮容器启动HBase集群某次调整后ZooKeeper的地址配错了应用启动时hbaseConnection()这个Bean初始化直接抛ZooKeeperConnectionException导致整个Spring容器起不来所有依赖此服务的调用方一并雪崩。后来我把HBase连接改成了“启动不强校验”的懒初始化策略Bean照常注册但不在启动阶段触达ZooKeeper增加一个独立的健康检查接口由监控系统定时调用。这样即使HBase短暂故障应用主流程MySQL侧仍然可用只是行为日志写入会暂时失败由重试队列消化。这个改动对可用性提升非常明显。6.5 问题快速排查链路遇到数据源切换异常时我一般按下面顺序排查效率最高确认方法上的DS注解是否真的被AOP拦截到。最简单的方法是临时在切面里打一行日志打印point.getSignature()和dataSource.value()看是否有输出。确认当前线程是否有事务。查看调用链上是否有一层Transactional包裹如果事务已经开启基本可以断定连接复用问题不要再纠结路由key。检查mapper包扫描范围。确认MapperScan的包路径没有把HBase相关的类也纳入扫描否则MyBatis会尝试为它们创建代理产生奇怪的启动错误。直接查动态数据源路由结果。在DynamicDataSource.determineCurrentLookupKey()里临时返回当前key同时打印targetDataSources的map大小看看注册是否完整。HBase侧问题看客户端日志和RegionServer日志。连接数暴涨先看应用里是否有Table被长期持有再确认Connection是否为单例。这套链路我整理成一张检查表每次排查都按顺序走基本能在十分钟内定位是路由问题、事务问题还是资源管理问题。7. 上线之后的监控、灰度与演进思路多数据源系统上线后最怕的不是写错SQL而是链路中的隐性故障被偶然暴露。这里分享三个方向的实践。7.1 监控要盯住连接池和切换频率MySQL侧的连接池指标优先看active连接数、wait时长、idle空闲连接数一旦active长期顶到池子上限说明出现了连接泄漏或者SQL执行过长。HBase侧重点看客户端RPC的retries次数、单次put耗时分布HBase在Region split或RegionServer GC期间写请求偶发超时是正常的但如果retries持续增长就要结合RegionServer日志分析了。动态数据源本身的切换频率也值得监控。我在切面里通过MDC把数据源key写入日志这样每条日志都能看到当前线程走的是哪个库配合链路追踪系统可以快速确认是否有诡异切换。7.2 新存储链路先灰度再全量多数据源改造最忌讳一把梭。建议把DS注解的值做成可配置的比如先让某个用户ID段走HBase其他仍走MySQL跑通后再逐步放大流量。这个灰度过程不只能验证HBase读写是否正常还能验证补偿任务、监控告警有没有覆盖到新链路。我见过系统上线当天HBase的RowKey设计不合理热点集中在某一Region结果流量被压到一台机器上如果不灰度这种问题只会更惨烈。7.3 演进读写分离、分库分表、SQL化动态数据源路由这套机制本身就是通用的。如果后续MySQL单库压力上来可以再注册一个只读从库到targetDataSources里借助相同机制实现读写分离如果数据量继续膨胀可以引入分库分表中间件仍然保持Service层注解路由不变。HBase侧如果团队对SQL偏好很强可以评估Phoenix方案让HBase也能以数据源身份接入MyBatis体系但运维复杂度会明显上升。这套架构演变到后期数据源数量可能从两个变成四五个。此时建议把路由key从枚举值扩展成一组配置项用配置中心统一管理避免每个环境都要改代码。最后再分享一个我在多轮改造中养成的习惯每次上线前把HBase的Region分布、MySQL连接池水位、路由切面的日志采样三份数据截个图对比一下。这套系统真正考验人的地方不在于“能连接两个数据库”而在于对数据存储边界的清晰认知以及面对海量数据时仍然能保持每一层都按预期运转。如果你也正在做一个类似的组合型存储系统先把第6章那几个坑避开后面的路会顺很多。
阅读完成 · 觉得有帮助?