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

点击上方蓝色“架构荟萃”关注我们,输入1024,你懂的

一般熟知 Mysql 数据库的朋友知道,当表的数据量达到千万级时,SQL 查询会逐渐变的缓慢起来,往往会成为一个系统的瓶颈所在。为了提升程序的性能,除了在表字段建立索引(如主键索引、唯一索引、普通索引等)、优化程序代码以及 SQL 语句等常规手段外,利用数据库主从读写分离(Master/Slave)架构,是一个不错的选择。但是在这种分离架构中普遍存在一个共性问题:数据读写一致性问题。

数据读写一致性问题

主从库同步逻辑

主库 Master 负责“写”,会把数据库的 BinLog 日志记录通过 I/O 线程异步操作同步到从库(负责“读”),这样每当业务系统发送 select 语句时,会直接路由到从库去查询数据,而不是主库。

但是这种同步逻辑有一个比较严重的缺陷:数据延时问题

我们可以想象一下这样的场景:

当一段程序在更新完数据后,需要立即查询更新后的数据,那么真的能查询到更新后的数据吗?

答案是:不一定!

这是因为主从数据同步时是异步操作,主从同步期间会存在数据延时问题,平常主库写数据量比较少的情况下,偶尔会遇到查询不到数据的情况。但是随着时间的推移,当使用系统的用户增多时,会发现这种查询不到数据的情况会变的越来越糟糕。

Sharding-JDBC

想必大家并不陌生,Sharding-JDBC 定位为轻量级 Java 框架,在 Java 的 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.getInstance() ;
 hintManager.setMasterRouteOnly();

实际案例

核心依赖

<dependency>
   <groupId>io.shardingjdbc</groupId>
   <artifactId>sharding-jdbc-core</artifactId>
   <version>${sharding-jdbc.version}</version>
</dependency>

数据库配置

sharding:
 jdbc:
   data-sources:
     mvip:
       type: com.alibaba.druid.pool.DruidDataSource
       driver-class-name: com.mysql.jdbc.Driver
       url: jdbc:mysql://${ha.basedb.mvip.ip}:${ha.basedb.mvip.port}/unicom_portal?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
       username: ${ha.basedb.mvip.username}
       password: ${ha.basedb.mvip.password}
     svip:
       type: com.alibaba.druid.pool.DruidDataSource
       driver-class-name: com.mysql.jdbc.Driver
       url: jdbc:mysql://${ha.basedb.svip.ip}:${ha.basedb.svip.port}/unicom_portal?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
       username: ${ha.basedb.svip.username}
       password: ${ha.basedb.svip.password}
   master-slave-rule:
     name: ds_ms
     master-data-source-name: mvip
     slave-data-source-names: svip
     load-balance-algorithm-type: round_robin

数据源初始化配置类   

@Data
@ConfigurationProperties(prefix = "sharding.jdbc")
public class MasterSlaveConfig {
    private Map<String, DruidDataSource> dataSources = new HashMap<>();
   private 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 {
       @Autowired
       private MasterSlaveConfig masterSlaveConfig;
       @Bean("masterSlaveDataSource")
       public DataSource dataSource() throws SQLException {
           masterSlaveConfig.getDataSources().forEach((k, v) -> configDruidParams(v));
           Map<String, DataSource> dataSourceMap = Maps.newHashMap();
           dataSourceMap.putAll(masterSlaveConfig.getDataSources());
           DataSource dataSource = MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveConfig.getMasterSlaveRule(), Maps.newHashMap());
           return dataSource;
       }
       @Bean
       public 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<>();
               filterList.add(wallFilter());
               druidDataSource.setProxyFilters(filterList);
           } catch (SQLException e) {
               e.printStackTrace();
           }
       }
   }

强制路由到主库查询关键代码:

public ArticleEntity getWithMasterDB(Long id, String wid) {
  HintManager hintManager = HintManager.getInstance() ;
  hintManager.setMasterRouteOnly();
  ArticleEntity article = baseMapper.queryObject(id, wid);
}

通过强制路由到主库查询有个风险,对于更新并实时查询业务场景比较多,如果都切到主库查询,势必会对主库服务器性能造成影响,可能还会影响到主从数据同步,所以要根据实际业务场景评估采用这种方式带来的系统性能问题。

另外,如果业务层面可以做妥协的话,尽量减少这种更新并实时查询方式,一种思路是实时更新库,利用 Java Future 特性异步查询(例如更新后,睡眠1-2秒再查询),伪代码如下:

Callable c1 = new Callable(){  @Override
  public String call() throws Exception {
    ArticleEntity articleEntity = null
    try {
         Thread.sleep(2000);
         articleEntity = articleService.get(id)
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
    return articleEntity;
  }
};
FutureTask<ArticleEntity> f = new FutureTask<ArticleEntity>(c1);
new Thread(f).start();
ArticleEntity article = f.get()

1. 人人都能看懂的 6 种限流实现方案!

2. 一个空格引发的“惨案“

3. 大型网站架构演化发展历程

4Java语言“坑爹”排行榜TOP 10

5. 我是一个Java类(附带精彩吐槽)

6. 看完这篇Redis缓存三大问题,保你能和面试官互扯

7. 程序员必知的 89 个操作系统核心概念

8. 深入理解 MySQL:快速学会分析SQL执行效率

9. API 接口设计规范

10. Spring Boot 面试,一个问题就干趴下了!

扫码二维码关注我

·end·

—如果本文有帮助,请分享到朋友圈吧—

我们一起愉快的玩耍!

你点的每个赞,我都认真当成了喜欢


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值