前段时间写了篇如何使用Sharding-JDBC进行分库分表的例子,相信能够感受到Sharding-JDBC的强大了,而且使用配置都非常干净。官方支持的功能还包括读写分离、分布式主键、强制路由等。这里再介绍下如何在分库分表的基础上集成读写分离的功能。

读写分离的概念

就是为了缓解数据库压力,将写入和读取操作分离为不同数据源,写库称为主库,读库称为从库,一主库可配置多从库。

设置主从库后,第一个问题是如何进行主从的同步。官方不支持主从的同步,也不支持因为主从同步延迟导致的数据不一致问题。工程实践上进行主从同步有很多做法,一种常用的做法是每天定时同步或者实时同步。这个话题太大,暂不展开。

读写分离快速入门

读写可以单独使用,也可以配合分库分表进行使用,由于上个分库分表的例子是基于1.5.4.1版本进行说明的,这里为了紧跟官方的步伐,升级Sharding-JDBC到最新的2.0.0.M2

项目结构如下:

pom依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.mybatis.spring.boot</groupId>
  8. <artifactId>mybatis-spring-boot-starter</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>mysql</groupId>
  12. <artifactId>mysql-connector-java</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>com.alibaba</groupId>
  16. <artifactId>druid</artifactId>
  17. </dependency>
  18. <!-- Sharding-JDBC核心依赖 -->
  19. <dependency>
  20. <groupId>io.shardingjdbc</groupId>
  21. <artifactId>sharding-jdbc-core</artifactId>
  22. </dependency>
  23. <!-- Sharding-JDBC Spring Boot Starter -->
  24. <dependency>
  25. <groupId>io.shardingjdbc</groupId>
  26. <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
  27. </dependency>
  28. <dependency>
  29. <groupId>com.google.guava</groupId>
  30. <artifactId>guava</artifactId>
  31. </dependency>
  32. <dependency>
  33. <groupId>org.springframework.boot</groupId>
  34. <artifactId>spring-boot-configuration-processor</artifactId>
  35. </dependency>
  36. <dependency>
  37. <groupId>org.springframework.boot</groupId>
  38. <artifactId>spring-boot-starter-test</artifactId>
  39. <scope>test</scope>
  40. </dependency>
  41. </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配置文件配置主从库的数据源即可:


  1. spring.application.name=spring-boot-mybatis-sharding-jdbc-masterslave
  2. server.context-path=/springboot
  3. mybatis.config-location=classpath:mybatis-config.xml
  4. # 所有主从库
  5. 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
  6. # ds_master_0
  7. sharding.jdbc.datasource.ds_master_0.type=com.alibaba.druid.pool.DruidDataSource
  8. sharding.jdbc.datasource.ds_master_0.driverClassName=com.mysql.jdbc.Driver
  9. sharding.jdbc.datasource.ds_master_0.url=jdbc:mysql://127.0.0.1:3306/ds_master_0?useSSL=false
  10. sharding.jdbc.datasource.ds_master_0.username=travis
  11. sharding.jdbc.datasource.ds_master_0.password=
  12. # slave for ds_master_0
  13. sharding.jdbc.datasource.ds_master_0_slave_0.type=com.alibaba.druid.pool.DruidDataSource
  14. sharding.jdbc.datasource.ds_master_0_slave_0.driverClassName=com.mysql.jdbc.Driver
  15. sharding.jdbc.datasource.ds_master_0_slave_0.url=jdbc:mysql://127.0.0.1:3306/ds_master_0_slave_0?useSSL=false
  16. sharding.jdbc.datasource.ds_master_0_slave_0.username=travis
  17. sharding.jdbc.datasource.ds_master_0_slave_0.password=
  18. sharding.jdbc.datasource.ds_master_0_slave_1.type=com.alibaba.druid.pool.DruidDataSource
  19. sharding.jdbc.datasource.ds_master_0_slave_1.driverClassName=com.mysql.jdbc.Driver
  20. sharding.jdbc.datasource.ds_master_0_slave_1.url=jdbc:mysql://127.0.0.1:3306/ds_master_0_slave_1?useSSL=false
  21. sharding.jdbc.datasource.ds_master_0_slave_1.username=travis
  22. sharding.jdbc.datasource.ds_master_0_slave_1.password=
  23. # ds_master_1
  24. sharding.jdbc.datasource.ds_master_1.type=com.alibaba.druid.pool.DruidDataSource
  25. sharding.jdbc.datasource.ds_master_1.driverClassName=com.mysql.jdbc.Driver
  26. sharding.jdbc.datasource.ds_master_1.url=jdbc:mysql://127.0.0.1:3306/ds_master_1?useSSL=false
  27. sharding.jdbc.datasource.ds_master_1.username=travis
  28. sharding.jdbc.datasource.ds_master_1.password=
  29. # slave for ds_master_1
  30. sharding.jdbc.datasource.ds_master_1_slave_0.type=com.alibaba.druid.pool.DruidDataSource
  31. sharding.jdbc.datasource.ds_master_1_slave_0.driverClassName=com.mysql.jdbc.Driver
  32. sharding.jdbc.datasource.ds_master_1_slave_0.url=jdbc:mysql://127.0.0.1:3306/ds_master_1_slave_0?useSSL=false
  33. sharding.jdbc.datasource.ds_master_1_slave_0.username=travis
  34. sharding.jdbc.datasource.ds_master_1_slave_0.password=
  35. sharding.jdbc.datasource.ds_master_1_slave_1.type=com.alibaba.druid.pool.DruidDataSource
  36. sharding.jdbc.datasource.ds_master_1_slave_1.driverClassName=com.mysql.jdbc.Driver
  37. sharding.jdbc.datasource.ds_master_1_slave_1.url=jdbc:mysql://127.0.0.1:3306/ds_master_1_slave_1?useSSL=false
  38. sharding.jdbc.datasource.ds_master_1_slave_1.username=travis
  39. sharding.jdbc.datasource.ds_master_1_slave_1.password=
  40. # 分库规则
  41. sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=city_id
  42. sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds_${city_id % 2}
  43. # 分表规则
  44. sharding.jdbc.config.sharding.tables.t_user.actualDataNodes=ds_${0..1}.t_user_${0..1}
  45. sharding.jdbc.config.sharding.tables.t_user.tableStrategy.inline.shardingColumn=user_id
  46. sharding.jdbc.config.sharding.tables.t_user.tableStrategy.inline.algorithmExpression=t_user_${user_id % 2}
  47. # 使用user_id作为分布式主键
  48. sharding.jdbc.config.sharding.tables.t_user.keyGeneratorColumnName=user_id
  49. # 逻辑主从库名和实际主从库映射关系
  50. sharding.jdbc.config.sharding.master-slave-rules.ds_0.masterDataSourceName=ds_master_0
  51. sharding.jdbc.config.sharding.master-slave-rules.ds_0.slaveDataSourceNames=ds_master_0_slave_0, ds_master_0_slave_1
  52. sharding.jdbc.config.sharding.master-slave-rules.ds_1.masterDataSourceName=ds_master_1
  53. sharding.jdbc.config.sharding.master-slave-rules.ds_1.slaveDataSourceNames=ds_master_1_slave_0, ds_master_1_slave_1

Test

测试代码如下:


  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class UserMapperTest {
  4. /** Logger */
  5. private static Logger log = LoggerFactory.getLogger(UserMapperTest.class);
  6. @Resource
  7. private UserMapper userMapper;
  8. @Before
  9. public void setup() throws Exception {
  10. create();
  11. clear();
  12. }
  13. private void create() throws SQLException {
  14. userMapper.createIfNotExistsTable();
  15. }
  16. private void clear() {
  17. userMapper.truncateTable();
  18. }
  19. @Test
  20. public void insert() throws Exception {
  21. UserEntity user = new UserEntity();
  22. user.setCityId(1);
  23. user.setUserName("insertTest");
  24. user.setAge(10);
  25. user.setBirth(new Date());
  26. assertTrue(userMapper.insert(user) > 0);
  27. Long userId = user.getUserId();
  28. log.info("Generated Key--userId:" + userId);
  29. userMapper.delete(userId);
  30. }
  31. @Test
  32. public void find() throws Exception {
  33. UserEntity userEntity = userMapper.find(138734796783222784L);
  34. log.info("user:{}", userEntity);
  35. }
  36. }

先运行insert方法,插入一条数据后,获取插入的user_id138734796783222784L(每次运行会不一样),由于city_id=1,读写分离约定,会落在主库,又根据分库规则会落在ds_master_1,再根据分表规则,会落在t_user_0

再运行find方法,指定userId,你会发现查出来是空的,这是因为Sharding-JDBC不支持主从同步以及主从同步延迟造成的数据不一致。这里我们显然术语第一种,因为根本就没有进行主从同步,那么从从库读取肯定是空的。

我们可以反向推理下,假如开启了主从同步,现在数据落在主库ds_master_1,这个主库有两个从库:ds_master_1_slave_0ds_master_1_slave_1,所以我们可以往这两个主库的t_user_0表插入刚才的数据,语句如下:

  1. 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_0t_user_0表插入该条数据,可以理解为主库同步到从库的数据。重新运行find方法,发现返回的数据和主库的一致,表明Sharding-JDBC从ds_master_1的从库ds_master_1_slave_0t_user_0表查到了数据。

再删掉ds_master_1_slave_0t_user_0表的数据,往ds_master_1_slave_1t_user_0表插入刚才那条数据,重新运行发现返回的结果为空,表明从ds_master_1的从库ds_master_1_slave_1t_user_0表没有查到数据。

最后往ds_master_1_slave_0t_user_0表重新插入刚才的数据,再运行发现又返回了数据。

基于以上现象,可以推论选择从库查询的时候经过了某种算法得到访问的从库,然后在从库根据分表规则查询数据。

读写分离实现

这里包括几个问题:

  1. 读写分离的查询流程?
  2. 如何做结果归并?
  3. 如何路由到某个从库进行查询?
  4. 可以强制路由主库进行读操作吗?

读写分离的流程

  1. 获取主从库配置规则,数据源封装成MasterSlaveDataSource
  2. 根据路由计算,得到PreparedStatementUnit单元列表,合并每个PreparedStatementUnit的执行结果返回
  3. 执行每个PrepareStatementUnit的时候需要获取连接,这里根据轮询负载均衡算法RoundRobinMasterSlaveLoadBalanceAlgorithm得到从库数据源,拿到连接后就开始执行具体的SQL查询了,这里通过PreparedStatementExecutor.execute()得到执行结果
  4. 结果归并后返回

MasterSlaveDataSource:


  1. public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
  2. private static final ThreadLocal<Boolean> DML_FLAG = new ThreadLocal<Boolean>() {
  3. @Override
  4. protected Boolean initialValue() {
  5. return false;
  6. }
  7. };
  8. // 主从配置关系
  9. private MasterSlaveRule masterSlaveRule;
  10. public MasterSlaveDataSource(final MasterSlaveRule masterSlaveRule) throws SQLException {
  11. super(getAllDataSources(masterSlaveRule.getMasterDataSource(), masterSlaveRule.getSlaveDataSourceMap().values()));
  12. this.masterSlaveRule = masterSlaveRule;
  13. }
  14. private static Collection<DataSource> getAllDataSources(final DataSource masterDataSource, final Collection<DataSource> slaveDataSources) {
  15. Collection<DataSource> result = new LinkedList<>(slaveDataSources);
  16. result.add(masterDataSource);
  17. return result;
  18. }
  19. ...省略部分代码
  20. // 获取数据源
  21. public NamedDataSource getDataSource(final SQLType sqlType) {
  22. // 强制路由到主库查询
  23. if (isMasterRoute(sqlType)) {
  24. DML_FLAG.set(true);
  25. return new NamedDataSource(masterSlaveRule.getMasterDataSourceName(), masterSlaveRule.getMasterDataSource());
  26. }
  27. // 获取选中的从库数据源
  28. String selectedSourceName = masterSlaveRule.getStrategy().getDataSource(masterSlaveRule.getName(),
  29. masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceMap().keySet()));
  30. DataSource selectedSource = selectedSourceName.equals(masterSlaveRule.getMasterDataSourceName())
  31. ? masterSlaveRule.getMasterDataSource() : masterSlaveRule.getSlaveDataSourceMap().get(selectedSourceName);
  32. Preconditions.checkNotNull(selectedSource, "");
  33. return new NamedDataSource(selectedSourceName, selectedSource);
  34. }

MasterSlaveRule:

  1. public final class MasterSlaveRule {
  2. // 名称(这里是ds_0和ds_1)
  3. private final String name;
  4. // 主库数据源名称(这里是ds_master_0和ds_master_1)
  5. private final String masterDataSourceName;
  6. // 主库数据源
  7. private final DataSource masterDataSource;
  8. // 所属从库列表,key为从库数据源名称,value是真实的数据源
  9. private final Map<String, DataSource> slaveDataSourceMap;
  10. // 主从库负载均衡算法
  11. private final MasterSlaveLoadBalanceAlgorithm strategy;

RoundRobinMasterSlaveLoadBalanceAlgorithm:

  1. // 轮询负载均衡策略,按照每个从节点访问次数均衡
  2. public final class RoundRobinMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {
  3. private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();
  4. @Override
  5. public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
  6. AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);
  7. COUNT_MAP.putIfAbsent(name, count);
  8. count.compareAndSet(slaveDataSourceNames.size(), 0);
  9. return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size());
  10. }
  11. }

DefaultResultSetHandler:


  1. @Override
  2. public List<Object> handleResultSets(Statement stmt) throws SQLException {
  3. ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
  4. // 返回的结果集
  5. final List<Object> multipleResults = new ArrayList<Object>();
  6. int resultSetCount = 0;
  7. ResultSetWrapper rsw = getFirstResultSet(stmt);
  8. List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  9. int resultMapCount = resultMaps.size();
  10. validateResultMapsCount(rsw, resultMapCount);
  11. while (rsw != null && resultMapCount > resultSetCount) {
  12. ResultMap resultMap = resultMaps.get(resultSetCount);
  13. // 将ResultSetWrapper的结果集添加到multipleResults中
  14. handleResultSet(rsw, resultMap, multipleResults, null);
  15. rsw = getNextResultSet(stmt);
  16. cleanUpAfterHandlingResultSet();
  17. resultSetCount++;
  18. }
  19. String[] resultSets = mappedStatement.getResultSets();
  20. if (resultSets != null) {
  21. while (rsw != null && resultSetCount < resultSets.length) {
  22. ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
  23. if (parentMapping != null) {
  24. String nestedResultMapId = parentMapping.getNestedResultMapId();
  25. ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
  26. handleResultSet(rsw, resultMap, null, parentMapping);
  27. }
  28. rsw = getNextResultSet(stmt);
  29. cleanUpAfterHandlingResultSet();
  30. resultSetCount++;
  31. }
  32. }
  33. return collapseSingleResultList(multipleResults);
  34. }
  35. private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
  36. try {
  37. if (parentMapping != null) {
  38. handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
  39. } else {
  40. if (resultHandler == null) {
  41. DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
  42. // 按照resultMap解析到defaultResultHandler中
  43. handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
  44. // 最后的结果就是这里加进去的
  45. multipleResults.add(defaultResultHandler.getResultList());
  46. } else {
  47. handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
  48. }
  49. }
  50. } finally {
  51. // issue #228 (close resultsets)
  52. closeResultSet(rsw.getResultSet());
  53. }
  54. }

在springboot项目中使用mybatis 集成 Sharding-JDBC的更多相关文章

  1. SpringBoot项目中,Mybatis的使用

    项目中使用MyBatis的地方很少,可以说是基本不用,慕课网上面这个项目介绍给也就是一些比较简单的使用例子,我用JPA比较的多,MyBatis有两种使用方式 1.导入MyBatis的依赖 <de ...

  2. 后端分页神器,mybatis pagehelper 在SSM与springboot项目中的使用

    mybatis pagehelper想必大家都耳熟能详了,是java后端用于做分页查询时一款非常好用的分页插件,同时也被人们称为mybatis三剑客之一,下面 就给大家讲讲如何在SSM项目和sprin ...

  3. SpringBoot12 QueryDSL01之QueryDSL介绍、springBoot项目中集成QueryDSL

    1 QueryDSL介绍 1.1 背景 QueryDSL的诞生解决了HQL查询类型安全方面的缺陷:HQL查询的扩展需要用字符串拼接的方式进行,这往往会导致代码的阅读困难:通过字符串对域类型和属性的不安 ...

  4. 在前后端分离的SpringBoot项目中集成Shiro权限框架

    参考[1].在前后端分离的SpringBoot项目中集成Shiro权限框架 参考[2]. Springboot + Vue + shiro 实现前后端分离.权限控制   以及跨域的问题也有涉及

  5. 五分钟后,你将学会在SpringBoot项目中如何集成CAT调用链

    买买买结算系统 一年一度的双十一购物狂欢节就要到了,又到剁手党们开始表演的时刻了.当我们把种草很久的商品放入购物车以后,点击"结算"按钮时,就来到了买买买必不可少的结算页面了.让我 ...

  6. SpringBoot项目中遇到的BUG

    1.启动项目的时候报错 1.Error starting ApplicationContext. To display the auto-configuration report re-run you ...

  7. 自身使用的springboot项目中比较全的pom.xml

    在学习的时候常建新的项目,mark下商用的jar <dependency> <groupId>org.mybatis</groupId> <artifactI ...

  8. 在SpringBoot项目中添加logback的MDC

    在SpringBoot项目中添加logback的MDC     先看下MDC是什么 Mapped Diagnostic Context,用于打LOG时跟踪一个“会话“.一个”事务“.举例,有一个web ...

  9. springboot 项目中获取默认注入的序列化对象 ObjectMapper

    在 springboot 项目中使用 @SpringBootApplication 会自动标记 @EnableAutoConfiguration 在接口中经常需要使用时间类型,Date ,如果想要格式 ...

随机推荐

  1. 1038 一元三次方程求解 2001年NOIP全国联赛提高组

    题目描述 Description 有形如:ax3+bx2+cx+d=0  这样的一个一元三次方程.给出该方程中各项的系数(a,b,c,d  均为实数),并约定该方程存在三个不同实根(根的范围在-100 ...

  2. HDU3439 Sequence

    今天下午学习了二项式反演,做了一道错排的题,开始了苦逼的经历. 显然答案是C(︀n,k)︀*H(n − k).其中H(i)为长度为i的错排序列 然后经过课件上一番二项式反演的推导 我就写了个扩展卢卡斯 ...

  3. 使用Python中的HTMLParser、cookielib抓取和解析网页、从HTML文档中提取链接、图像、文本、Cookies(二)(转)

    对搜索引擎.文件索引.文档转换.数据检索.站点备份或迁移等应用程序来说,经常用到对网页(即HTML文件)的解析处理.事实上,通过 Python语言提供的各种模块,我们无需借助Web服务器或者Web浏览 ...

  4. Optimizing Oracle RAC

    Oracle Real Application Clusters (RAC) databases form an increasing proportion of Oracle database sy ...

  5. discuz修改太阳,月亮,星星等级图标

    想必大家都想修改一下默认的等级图标吧,刚才在论坛上看见很多大神的方法都是要修改文件的,不过为了安全起见需要事先备份好才改,这种方法是可行的,但可能有些新手站长不会修改,又或者改错了恢复不来,现在我教大 ...

  6. 基于Linux的智能家居的设计(3)

    2  硬件设计 本课题的硬件设计包含主控制器.传输数据设计.数据採集设计.控制驱动设计.显示设计.门禁设计. 2.1  主控制器 依据方案三选择S3C6410主控芯片,S3C6410是由Samsung ...

  7. bat如何批量删除指定部分文件夹名的文件夹

    @echo offfor /f "delims=" %%i in ('dir /s/b/ad 123*') do ( rd /s/q "%%~i")exit

  8. Modbus TCP和Modbus Rtu协议的区别 转

    http://blog.csdn.net/educast/article/details/9177679   Modbus rtu和Modbus tcp两个协议的本质都是MODBUS协议,都是靠MOD ...

  9. HDU 4081 Qin Shi Huang&#39;s National Road System(最小生成树/次小生成树)

    题目链接:传送门 题意: 有n坐城市,知道每坐城市的坐标和人口.如今要在全部城市之间修路,保证每一个城市都能相连,而且保证A/B 最大.全部路径的花费和最小,A是某条路i两端城市人口的和,B表示除路i ...

  10. 在ASP.NET MVC中使用Castle Windsor

    平常用Inject比较多,今天接触到了Castle Windsor.本篇就来体验其在ASP.NET MVC中的应用过程. Visual Studio 2012创建一个ASP.NET MVC 4网站. ...