利用 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.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
数据源初始化配置类
"sharding.jdbc") (prefix =
public class MasterSlaveConfig {
private Map
dataSources = new HashMap<>(); private MasterSlaveRuleConfiguration masterSlaveRule;
}
(DruidDataSource.class)
(MasterSlaveConfig.class)
({
"sharding.jdbc.data-sources.mvip.url",
"sharding.jdbc.master-slave-rule.master-data-source-name"
})
static class ShardingDruid extends DruidConfig {
private MasterSlaveConfig masterSlaveConfig;
"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;
}
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()
2. 一个空格引发的“惨案“
3. 大型网站架构演化发展历程
扫码二维码关注我
·end·
—如果本文有帮助,请分享到朋友圈吧—
我们一起愉快的玩耍!