忆梦|利用Sharding-JDBC解决数据库读写分离查询延时问题

前言一般熟知 Mysql 数据库的朋友知道 , 当表的数据量达到千万级时 , SQL 查询会逐渐变的缓慢起来 , 往往会成为一个系统的瓶颈所在 。 为了提升程序的性能 , 除了在表字段建立索引(如主键索引、唯一索引、普通索引等)、优化程序代码以及 SQL 语句等常规手段外 , 利用数据库主从读写分离(Master/Slave)架构 , 是一个不错的选择 。 但是在这种分离架构中普遍存在一个共性问题:数据读写一致性问题 。
数据读写一致性问题主从库同步逻辑
主库 Master 负责“写” , 会把数据库的 BinLog 日志记录通过 I/O 线程异步操作同步到从库(负责“读”) , 这样每当业务系统发送 select 语句时 , 会直接路由到从库去查询数据 , 而不是主库 。
忆梦|利用Sharding-JDBC解决数据库读写分离查询延时问题但是这种同步逻辑有一个比较严重的缺陷:数据延时问题 。
我们可以想象一下这样的场景:
当一段程序在更新完数据后 , 需要立即查询更新后的数据 , 那么真的能查询到更新后的数据吗?
答案是:不一定!
这是因为主从数据同步时是异步操作 , 主从同步期间会存在数据延时问题 , 平常主库写数据量比较少的情况下 , 偶尔会遇到查询不到数据的情况 。 但是随着时间的推移 , 当使用系统的用户增多时 , 会发现这种查询不到数据的情况会变的越来越糟糕 。
Sharding-jdbc想必大家并不陌生 , Sharding-JDBC 定位为轻量级 Java 框架 , 在 Java 的 JDBC 层提供的额外服务 。
忆梦|利用Sharding-JDBC解决数据库读写分离查询延时问题它使用客户端直连数据库 , 以 jar 包形式提供服务 , 无需额外部署和依赖 , 可理解为增强版的 JDBC 驱动 , 完全兼容 JDBC 和各种 ORM 框架 。

  • 适用于任何基于 JDBC 的 ORM 框架 , 如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC 。
  • 支持任何第三方的数据库连接池 , 如:DBCP, C3P0, BoneCP, Druid, HikariCP 等 。
  • 支持任意实现 JDBC 规范的数据库 , 目前支持 MySQL , Oracle , SQLServer , PostgreSQL 以及任何遵循 SQL92 标准的数据库 。
读写分离特性
  • 提供了一主多从的读写分离配置 , 可独立使用 , 也可配合分库分表使用 。
  • 同个调用线程 , 执行多条语句 , 其中一旦发现有非读操作 , 后续所有读操作均从主库读取 。
  • Spring命名空间 。
  • 基于Hint的强制主库路由 。
ShardingSphere-JDBC 官方提供 HintManager 分片键值管理器 ,通过调用hintManager.setMasterRouteOnly() 强制路由到主库查询 , 这样就解决了数据延时问题 , 无论什么时候都能够从主库 Master 查询到最新数据 , 而不用走从库查询 。
HintManager hintManager = HintManager.etInstance() ; hintManager.setMasterRouteOnly();实际案例核心依赖
io.shardingjdbcsharding-jdbc-core${sharding-jdbc.version}数据库配置
sharding: jdbc:data-sources:mvip:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://${ha.basedb.mvip.ip}:${ha.basedb.mvip.port}/unicom_portal?allowMultiQueries=trueprivate MasterSlaveRuleConfiguration masterSlaveRule;}@ConditionalOnClass(DruidDataSource.class)@EnableConfigurationProperties(MasterSlaveConfig.class)@ConditionalOnProperty({"sharding.jdbc.data-sources.mvip.url","sharding.jdbc.master-slave-rule.master-data-source-name"})static class ShardingDruid extends DruidConfig {@Autowiredprivate MasterSlaveConfig masterSlaveConfig;@Bean("masterSlaveDataSource")public DataSource dataSource() throws SQLException {masterSlaveConfig.getDataSources().forEach((k, v) -> configDruidParams(v));Map dataSourceMap = Maps.newHashMap();dataSourceMap.putAll(masterSlaveConfig.getDataSources());DataSource dataSource = MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveConfig.getMasterSlaveRule(), Maps.newHashMap());return dataSource;}@Beanpublic PlatformTransactionManager txManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}private void configDruidParams(DruidDataSource druidDataSource) {druidDataSource.setMaxActive(20);druidDataSource.setInitialSize(1);// 配置获取连接等待超时的时间druidDataSource.setMaxWait(10000);druidDataSource.setMinIdle(1);// 配置间隔多久才进行一次检测 , 检测需要关闭的空闲连接 , 单位是毫秒druidDataSource.setTimeBetweenEvictionRunsMillis(60000);// 配置一个连接在池中最小生存的时间 , 单位是毫秒 超过这个时间每次会回收默认3个连接druidDataSource.setMinEvictableIdleTimeMillis(30000);// 线上配置的mysql断开闲置连接时间为1小时,数据源配置回收时间为3分钟,以最后一次活跃时间开始算druidDataSource.setMaxEvictableIdleTimeMillis(180000);// 连接最大存活时间 , 默认是-1(不限制物理连接时间) , 从创建连接开始计算 , 如果超过该时间 , 则会被清理druidDataSource.setPhyTimeoutMillis(15000);druidDataSource.setValidationQuery("select 1");druidDataSource.setTestWhileIdle(true);druidDataSource.setTestOnBorrow(false);druidDataSource.setTestOnReturn(false);druidDataSource.setPoolPreparedStatements(true);druidDataSource.setMaxOpenPreparedStatements(20);druidDataSource.setUseGlobalDataSourceStat(true);druidDataSource.setKeepAlive(true);druidDataSource.setRemoveAbandoned(true);druidDataSource.setRemoveAbandonedTimeout(180);try {druidDataSource.setFilters("stat,slf4j");List filterList = new ArrayList