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

互联网全栈架构

共 6220字,需浏览 13分钟

 ·

2020-08-29 06:04

点击上方蓝色“架构荟萃”关注我们,输入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.shardingjdbcgroupId>   <artifactId>sharding-jdbc-coreartifactId>   <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 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 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 f = new FutureTask(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·

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

我们一起愉快的玩耍!



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

浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报