在springboot项目中使用mybatis 集成 Sharding-JDBC
前段时间写了篇如何使用Sharding-JDBC进行分库分表的例子,相信能够感受到Sharding-JDBC的强大了,而且使用配置都非常干净。官方支持的功能还包括读写分离、分布式主键、强制路由等。这里再介绍下如何在分库分表的基础上集成读写分离的功能。
读写分离的概念
就是为了缓解数据库压力,将写入和读取操作分离为不同数据源,写库称为主库,读库称为从库,一主库可配置多从库。
设置主从库后,第一个问题是如何进行主从的同步。官方不支持主从的同步,也不支持因为主从同步延迟导致的数据不一致问题。工程实践上进行主从同步有很多做法,一种常用的做法是每天定时同步或者实时同步。这个话题太大,暂不展开。
读写分离快速入门
读写可以单独使用,也可以配合分库分表进行使用,由于上个分库分表的例子是基于1.5.4.1
版本进行说明的,这里为了紧跟官方的步伐,升级Sharding-JDBC到最新的2.0.0.M2
项目结构如下:
pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!-- Sharding-JDBC核心依赖 -->
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
</dependency>
<!-- Sharding-JDBC Spring Boot Starter -->
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
主从数据库配置
在配置前,我们希望分库分表规则和之前保持一致:
基于
t_user
表,根据city_id
进行分库,如果city_id mod 2
为奇数则落在ds_master_1
库,偶数则落在ds_master_0
库;根据user_id
进行分表,如果user_id mod 2
为奇数则落在t_user_1
表,偶数则落在t_user_0
表
读写分离规则:
读都落在从库,写落在主库
因为使用Sharding-JDBC Spring Boot Starter
,所以只需要在properties配置文件配置主从库的数据源即可:
spring.application.name=spring-boot-mybatis-sharding-jdbc-masterslave
server.context-path=/springboot
mybatis.config-location=classpath:mybatis-config.xml
# 所有主从库
sharding.jdbc.datasource.names=ds_master_0,ds_master_1,ds_master_0_slave_0,ds_master_0_slave_1,ds_master_1_slave_0,ds_master_1_slave_1
# ds_master_0
sharding.jdbc.datasource.ds_master_0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds_master_0.driverClassName=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master_0.url=jdbc:mysql://127.0.0.1:3306/ds_master_0?useSSL=false
sharding.jdbc.datasource.ds_master_0.username=travis
sharding.jdbc.datasource.ds_master_0.password=
# slave for ds_master_0
sharding.jdbc.datasource.ds_master_0_slave_0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds_master_0_slave_0.driverClassName=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master_0_slave_0.url=jdbc:mysql://127.0.0.1:3306/ds_master_0_slave_0?useSSL=false
sharding.jdbc.datasource.ds_master_0_slave_0.username=travis
sharding.jdbc.datasource.ds_master_0_slave_0.password=
sharding.jdbc.datasource.ds_master_0_slave_1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds_master_0_slave_1.driverClassName=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master_0_slave_1.url=jdbc:mysql://127.0.0.1:3306/ds_master_0_slave_1?useSSL=false
sharding.jdbc.datasource.ds_master_0_slave_1.username=travis
sharding.jdbc.datasource.ds_master_0_slave_1.password=
# ds_master_1
sharding.jdbc.datasource.ds_master_1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds_master_1.driverClassName=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master_1.url=jdbc:mysql://127.0.0.1:3306/ds_master_1?useSSL=false
sharding.jdbc.datasource.ds_master_1.username=travis
sharding.jdbc.datasource.ds_master_1.password=
# slave for ds_master_1
sharding.jdbc.datasource.ds_master_1_slave_0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds_master_1_slave_0.driverClassName=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master_1_slave_0.url=jdbc:mysql://127.0.0.1:3306/ds_master_1_slave_0?useSSL=false
sharding.jdbc.datasource.ds_master_1_slave_0.username=travis
sharding.jdbc.datasource.ds_master_1_slave_0.password=
sharding.jdbc.datasource.ds_master_1_slave_1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds_master_1_slave_1.driverClassName=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master_1_slave_1.url=jdbc:mysql://127.0.0.1:3306/ds_master_1_slave_1?useSSL=false
sharding.jdbc.datasource.ds_master_1_slave_1.username=travis
sharding.jdbc.datasource.ds_master_1_slave_1.password=
# 分库规则
sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=city_id
sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds_${city_id % 2}
# 分表规则
sharding.jdbc.config.sharding.tables.t_user.actualDataNodes=ds_${0..1}.t_user_${0..1}
sharding.jdbc.config.sharding.tables.t_user.tableStrategy.inline.shardingColumn=user_id
sharding.jdbc.config.sharding.tables.t_user.tableStrategy.inline.algorithmExpression=t_user_${user_id % 2}
# 使用user_id作为分布式主键
sharding.jdbc.config.sharding.tables.t_user.keyGeneratorColumnName=user_id
# 逻辑主从库名和实际主从库映射关系
sharding.jdbc.config.sharding.master-slave-rules.ds_0.masterDataSourceName=ds_master_0
sharding.jdbc.config.sharding.master-slave-rules.ds_0.slaveDataSourceNames=ds_master_0_slave_0, ds_master_0_slave_1
sharding.jdbc.config.sharding.master-slave-rules.ds_1.masterDataSourceName=ds_master_1
sharding.jdbc.config.sharding.master-slave-rules.ds_1.slaveDataSourceNames=ds_master_1_slave_0, ds_master_1_slave_1
Test
测试代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
/** Logger */
private static Logger log = LoggerFactory.getLogger(UserMapperTest.class);
@Resource
private UserMapper userMapper;
@Before
public void setup() throws Exception {
create();
clear();
}
private void create() throws SQLException {
userMapper.createIfNotExistsTable();
}
private void clear() {
userMapper.truncateTable();
}
@Test
public void insert() throws Exception {
UserEntity user = new UserEntity();
user.setCityId(1);
user.setUserName("insertTest");
user.setAge(10);
user.setBirth(new Date());
assertTrue(userMapper.insert(user) > 0);
Long userId = user.getUserId();
log.info("Generated Key--userId:" + userId);
userMapper.delete(userId);
}
@Test
public void find() throws Exception {
UserEntity userEntity = userMapper.find(138734796783222784L);
log.info("user:{}", userEntity);
}
}
先运行insert
方法,插入一条数据后,获取插入的user_id
为138734796783222784L
(每次运行会不一样),由于city_id=1
,读写分离约定,会落在主库,又根据分库规则会落在ds_master_1
,再根据分表规则,会落在t_user_0
再运行find
方法,指定userId,你会发现查出来是空的,这是因为Sharding-JDBC不支持主从同步以及主从同步延迟造成的数据不一致。这里我们显然术语第一种,因为根本就没有进行主从同步,那么从从库读取肯定是空的。
我们可以反向推理下,假如开启了主从同步,现在数据落在主库ds_master_1
,这个主库有两个从库:ds_master_1_slave_0
和ds_master_1_slave_1
,所以我们可以往这两个主库的t_user_0
表插入刚才的数据,语句如下:
INSERT INTO t_user_0(user_id,city_id,user_name,age,birth) values(138734796783222784,1,'insertTest',10,'2017-11-18 00:00:00');
先往ds_master_1_slave_0
的t_user_0
表插入该条数据,可以理解为主库同步到从库的数据。重新运行find
方法,发现返回的数据和主库的一致,表明Sharding-JDBC从ds_master_1
的从库ds_master_1_slave_0
的t_user_0
表查到了数据。
再删掉ds_master_1_slave_0
的t_user_0
表的数据,往ds_master_1_slave_1
的t_user_0
表插入刚才那条数据,重新运行发现返回的结果为空,表明从ds_master_1
的从库ds_master_1_slave_1
的t_user_0
表没有查到数据。
最后往ds_master_1_slave_0
的t_user_0
表重新插入刚才的数据,再运行发现又返回了数据。
基于以上现象,可以推论选择从库查询的时候经过了某种算法得到访问的从库,然后在从库根据分表规则查询数据。
读写分离实现
这里包括几个问题:
- 读写分离的查询流程?
- 如何做结果归并?
- 如何路由到某个从库进行查询?
- 可以强制路由主库进行读操作吗?
读写分离的流程
- 获取主从库配置规则,数据源封装成
MasterSlaveDataSource
- 根据路由计算,得到PreparedStatementUnit单元列表,合并每个PreparedStatementUnit的执行结果返回
- 执行每个PrepareStatementUnit的时候需要获取连接,这里根据轮询负载均衡算法
RoundRobinMasterSlaveLoadBalanceAlgorithm
得到从库数据源,拿到连接后就开始执行具体的SQL查询了,这里通过PreparedStatementExecutor.execute()
得到执行结果 - 结果归并后返回
MasterSlaveDataSource:
public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
private static final ThreadLocal<Boolean> DML_FLAG = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return false;
}
};
// 主从配置关系
private MasterSlaveRule masterSlaveRule;
public MasterSlaveDataSource(final MasterSlaveRule masterSlaveRule) throws SQLException {
super(getAllDataSources(masterSlaveRule.getMasterDataSource(), masterSlaveRule.getSlaveDataSourceMap().values()));
this.masterSlaveRule = masterSlaveRule;
}
private static Collection<DataSource> getAllDataSources(final DataSource masterDataSource, final Collection<DataSource> slaveDataSources) {
Collection<DataSource> result = new LinkedList<>(slaveDataSources);
result.add(masterDataSource);
return result;
}
...省略部分代码
// 获取数据源
public NamedDataSource getDataSource(final SQLType sqlType) {
// 强制路由到主库查询
if (isMasterRoute(sqlType)) {
DML_FLAG.set(true);
return new NamedDataSource(masterSlaveRule.getMasterDataSourceName(), masterSlaveRule.getMasterDataSource());
}
// 获取选中的从库数据源
String selectedSourceName = masterSlaveRule.getStrategy().getDataSource(masterSlaveRule.getName(),
masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceMap().keySet()));
DataSource selectedSource = selectedSourceName.equals(masterSlaveRule.getMasterDataSourceName())
? masterSlaveRule.getMasterDataSource() : masterSlaveRule.getSlaveDataSourceMap().get(selectedSourceName);
Preconditions.checkNotNull(selectedSource, "");
return new NamedDataSource(selectedSourceName, selectedSource);
}
MasterSlaveRule:
public final class MasterSlaveRule {
// 名称(这里是ds_0和ds_1)
private final String name;
// 主库数据源名称(这里是ds_master_0和ds_master_1)
private final String masterDataSourceName;
// 主库数据源
private final DataSource masterDataSource;
// 所属从库列表,key为从库数据源名称,value是真实的数据源
private final Map<String, DataSource> slaveDataSourceMap;
// 主从库负载均衡算法
private final MasterSlaveLoadBalanceAlgorithm strategy;
RoundRobinMasterSlaveLoadBalanceAlgorithm:
// 轮询负载均衡策略,按照每个从节点访问次数均衡
public final class RoundRobinMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {
private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();
@Override
public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);
COUNT_MAP.putIfAbsent(name, count);
count.compareAndSet(slaveDataSourceNames.size(), 0);
return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size());
}
}
DefaultResultSetHandler:
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
// 返回的结果集
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 将ResultSetWrapper的结果集添加到multipleResults中
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
// 按照resultMap解析到defaultResultHandler中
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
// 最后的结果就是这里加进去的
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
在springboot项目中使用mybatis 集成 Sharding-JDBC的更多相关文章
- SpringBoot项目中,Mybatis的使用
项目中使用MyBatis的地方很少,可以说是基本不用,慕课网上面这个项目介绍给也就是一些比较简单的使用例子,我用JPA比较的多,MyBatis有两种使用方式 1.导入MyBatis的依赖 <de ...
- 后端分页神器,mybatis pagehelper 在SSM与springboot项目中的使用
mybatis pagehelper想必大家都耳熟能详了,是java后端用于做分页查询时一款非常好用的分页插件,同时也被人们称为mybatis三剑客之一,下面 就给大家讲讲如何在SSM项目和sprin ...
- SpringBoot12 QueryDSL01之QueryDSL介绍、springBoot项目中集成QueryDSL
1 QueryDSL介绍 1.1 背景 QueryDSL的诞生解决了HQL查询类型安全方面的缺陷:HQL查询的扩展需要用字符串拼接的方式进行,这往往会导致代码的阅读困难:通过字符串对域类型和属性的不安 ...
- 在前后端分离的SpringBoot项目中集成Shiro权限框架
参考[1].在前后端分离的SpringBoot项目中集成Shiro权限框架 参考[2]. Springboot + Vue + shiro 实现前后端分离.权限控制 以及跨域的问题也有涉及
- 五分钟后,你将学会在SpringBoot项目中如何集成CAT调用链
买买买结算系统 一年一度的双十一购物狂欢节就要到了,又到剁手党们开始表演的时刻了.当我们把种草很久的商品放入购物车以后,点击"结算"按钮时,就来到了买买买必不可少的结算页面了.让我 ...
- SpringBoot项目中遇到的BUG
1.启动项目的时候报错 1.Error starting ApplicationContext. To display the auto-configuration report re-run you ...
- 自身使用的springboot项目中比较全的pom.xml
在学习的时候常建新的项目,mark下商用的jar <dependency> <groupId>org.mybatis</groupId> <artifactI ...
- 在SpringBoot项目中添加logback的MDC
在SpringBoot项目中添加logback的MDC 先看下MDC是什么 Mapped Diagnostic Context,用于打LOG时跟踪一个“会话“.一个”事务“.举例,有一个web ...
- springboot 项目中获取默认注入的序列化对象 ObjectMapper
在 springboot 项目中使用 @SpringBootApplication 会自动标记 @EnableAutoConfiguration 在接口中经常需要使用时间类型,Date ,如果想要格式 ...
随机推荐
- Qt Quick自定义样式一套
弄了几个月的Qt,基本上以写上位机程序和工厂用的一些工具为主.老大的要求是快速.稳定.不出问题,不过他嫌.net要安装.还有升级(刚开始的时候由于这个出了些小问题),MFC开发东西又实在费劲,就让我找 ...
- 吴恩达-coursera-机器学习-week8
十三.聚类(Clustering) 13.1 无监督学习:简介 13.2 K-均值算法 13.3 优化目标 13.4 随机初始化 13.5 选择聚类数 十四.降维(Dimensionality Red ...
- ZOJ 2975 Kinds of Fuwas
K - Kinds of Fuwas Time Limit:2000MS Memory Limit:65536KB 64bit IO Format:%lld & %llu De ...
- 本地hosts文件IP地址解析
localhost是一个域名,127.0.0.1为IP地址.Windows系统中,约定127.0.0.1为本地IP地址.localhost是其对应的域名.配置是在hosts文件中设置的,Windows ...
- OpenNI2 + NiTE2开发教程
发现了一个非常不错的关于自然交互OpeNI2+NiTE2的资源,非常感谢Heresy,这里分享链接: OpenNI 2.x 教学文章(转载自:Heresy博客,地址:https://kheresy.w ...
- ant design Modal关闭时清除数据的解决方案
背景:modal组件关闭时不清除数据,原来输入的数据还存在 解决方案: 1.modal的api:destroyOnClose 2.手动控制modal的销毁 this.state = { destroy ...
- ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建
在上一篇"ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式.错误信息提示.validate方法的背后"中,了解了jQuery如何验证,如何显示错误信息,本篇要 ...
- libxml/HTMLparser.h file
在导入asihttprequest包时出问题导入了libxml2.dylib,但是却提示libxml/HTMLparser.h file not found,那是因为你的开发环境默认的路径无法找到这个 ...
- C#编程(十八)----------C#中的结构
C#中的结构 假设有一个类: class Dimensions { public double Length; public double Width; } 定义了一个类,它只存储某一项的长度和宽度. ...
- windows下PHP不能开启pgsql扩展的解决方法
Tip: 环境 windows8.1 64位 + xampp1.8.1 + postgresql 9.3.6-2 第一步: php.ini中开启pgsql扩展 extension=php_pgsql ...