本篇分享数据库主从方案,案例采用springboot+mysql+mybatis演示;要想在代码中做主从选择,通常需要明白什么时候切换数据源,怎么切换数据源,下面以代码示例来做阐述;

  • 搭建测试环境(1个master库2个slave库)
  • DataSource多数据源配置
  • 设置mybatis数据源
  • 拦截器+注解设置master和slave库选择
  • 选出当前请求要使用的slave从库
  • 测试用例

搭建测试环境(1个master库2个slave库)

由于测试资源优先在本地模拟创建3个数据库,分别是1个master库2个slave库,里面分别都有一个tblArticle表,内容也大致相同(为了演示主从效果,我把从库中表的title列值增加了slave字样):

再来创建一个db.properties,分别配置3个数据源,格式如下:

 spring.datasource0.jdbc-url=jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource0.username=root
spring.datasource0.password=
spring.datasource0.driver-class-name=com.mysql.jdbc.Driver spring.datasource1.jdbc-url=jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource1.username=root
spring.datasource1.password=
spring.datasource1.driver-class-name=com.mysql.jdbc.Driver spring.datasource2.jdbc-url=jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource2.username=root
spring.datasource2.password=
spring.datasource2.driver-class-name=com.mysql.jdbc.Driver

同时我们创建具有对应关系的DbType枚举,帮助我们使代码更已读:

 public class DbEmHelper {
public enum DbTypeEm {
db0(, "db0(默认master)", -),
db1(, "db1", ),
db2(, "db2", ); /**
* 用于筛选从库
*
* @param slaveNum 从库顺序编号 0开始
* @return
*/
public static Optional<DbTypeEm> getDbTypeBySlaveNum(int slaveNum) {
return Arrays.stream(DbTypeEm.values()).filter(b -> b.getSlaveNum() == slaveNum).findFirst();
} DbTypeEm(int code, String des, int slaveNum) {
this.code = code;
this.des = des;
this.slaveNum = slaveNum;
} private int code;
private String des;
private int slaveNum; //get,set省略
}
}

DataSource多数据源配置

使用上面3个库连接串信息,配置3个不同的DataSource实例,达到多个DataSource目的;由于在代码中库的实例需要动态选择,因此我们利用AbstractRoutingDataSource来聚合多个数据源;下面是生成多个DataSource代码:

 @Configuration
public class DbConfig { @Bean(name = "dbRouting")
public DataSource dbRouting() throws IOException {
//加载db配置文件
InputStream in = this.getClass().getClassLoader().getResourceAsStream("db.properties");
Properties pp = new Properties();
pp.load(in); //创建每个库的datasource
Map<Object, Object> targetDataSources = new HashMap<>(DbEmHelper.DbTypeEm.values().length);
Arrays.stream(DbEmHelper.DbTypeEm.values()).forEach(dbTypeEm -> {
targetDataSources.put(dbTypeEm, getDataSource(pp, dbTypeEm));
}); //设置多数据源
DbRouting dbRouting = new DbRouting();
dbRouting.setTargetDataSources(targetDataSources);
return dbRouting;
} /**
* 创建库的datasource
*
* @param pp
* @param dbTypeEm
* @return
*/
private DataSource getDataSource(Properties pp, DbEmHelper.DbTypeEm dbTypeEm) {
DataSourceBuilder<?> builder = DataSourceBuilder.create(); builder.driverClassName(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.driver-class-name", dbTypeEm.getCode())));
builder.url(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.jdbc-url", dbTypeEm.getCode())));
builder.username(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.username", dbTypeEm.getCode())));
builder.password(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.password", dbTypeEm.getCode()))); return builder.build();
}
}

能够看到一个DbRouting实例,其是继承了AbstractRoutingDataSource,她里面有个Map变量来存储多个数据源信息:

 public class DbRouting extends AbstractRoutingDataSource {

     @Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDb().orElse(DbEmHelper.DbTypeEm.db0);
}
}

DbRouting里面主要重写了determineCurrentLookupKey(),通过设置和存储DataSource集合的Map相同的key,以此达到选择不同DataSource的目的,这里使用ThreadLocal获取同一线程存储的key;主要看AbstractRoutingDataSource类中下面代码:

     protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if(dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if(dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}

设置mybatis数据源

本次演示为了便利,这里使用mybatis的注解方式来查询数据库,我们需要给mybatis设置数据源,我们可以从上面的声明DataSource的bean方法获取:

 @EnableTransactionManagement
@Configuration
public class MybaitisConfig {
@Resource(name = "dbRouting")
DataSource dataSource; @Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:*"));
return factoryBean.getObject();
}
}

我们使用的mybatis注解方式来查询数据库,所以不需要加载mapper的xml文件,下面注解方式查询sql:

 @Mapper
public interface ArticleMapper {
@Select("select * from tblArticle where id = #{id}")
Article selectById(int id);
}

拦截器+注解来选择master和slave库

通常操作数据的业务逻辑都放在service层,我们希望service中不同方法使用不同的库;比如:添加、修改、删除、部分查询方法等,使用master主库来操作,而大部分查询操作可以使用slave库来查询;这里通过拦截器+灵活的自定义注解来实现我们的需求:

 @Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DbType {
boolean isMaster() default true;
}

注解参数默认选择master库来操作业务(看具体需求吧)

 @Aspect
@Component
public class DbInterceptor { //全部service层请求都走这里,ThreadLocal才能有DbType值
private final String pointcut = "execution(* com.sm.service..*.*(..))"; @Pointcut(value = pointcut)
public void dbType() {
} @Before("dbType()")
void before(JoinPoint joinPoint) {
System.out.println("before..."); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
DbType dbType = method.getAnnotation(DbType.class);
//设置Db
DbContextHolder.setDb(dbType == null ? false : dbType.isMaster());
} @After("dbType()")
void after() {
System.out.println("after..."); DbContextHolder.remove();
}
}

拦截器拦截service层的所有方法,然后获取带有自定义注解DbType的方法的isMaster值,DbContextHolder.setDb()方法判断走master还是slave库,并赋值给ThreadLocal:

 public class DbContextHolder {
private static final ThreadLocal<Optional<DbEmHelper.DbTypeEm>> dbTypeEmThreadLocal = new ThreadLocal<>();
private static final AtomicInteger atoCounter = new AtomicInteger(); public static void setDb(DbEmHelper.DbTypeEm dbTypeEm) {
dbTypeEmThreadLocal.set(Optional.ofNullable(dbTypeEm));
} public static Optional<DbEmHelper.DbTypeEm> getDb() {
return dbTypeEmThreadLocal.get();
} public static void remove() {
dbTypeEmThreadLocal.remove();
} /**
* 设置主从库
*
* @param isMaster
*/
public static void setDb(boolean isMaster) {
if (isMaster) {
//主库
setDb(DbEmHelper.DbTypeEm.db0);
} else {
//从库
setSlave();
}
} private static void setSlave() {
//累加值达到最大时,重置
if (atoCounter.get() >= ) {
atoCounter.set();
} //排除master,选出当前线程请求要使用的db从库 - 从库算法
int slaveNum = atoCounter.getAndIncrement() % (DbEmHelper.DbTypeEm.values().length - );
Optional<DbEmHelper.DbTypeEm> dbTypeEm = DbEmHelper.DbTypeEm.getDbTypeBySlaveNum(slaveNum);
if (dbTypeEm.isPresent()) {
setDb(dbTypeEm.get());
} else {
throw new IllegalArgumentException("从库未匹配");
}
}
}

这一步骤很重要,通过拦截器来到达选择master和slave目的,当然也有其他方式的;

选出当前请求要使用的slave从库

上面能选择出master和slave走向了,但是往往slave至少有两个库存在;我们需要知道怎么来选择多个slave库,目前最常用的方式通过计数器取余的方式来选择:

     private static void setSlave() {
//累加值达到最大时,重置
if (atoCounter.get() >= ) {
atoCounter.set();
} //排除master,选出当前线程请求要使用的db从库 - 从库算法
int slaveNum = atoCounter.getAndIncrement() % (DbEmHelper.DbTypeEm.values().length - );
Optional<DbEmHelper.DbTypeEm> dbTypeEm = DbEmHelper.DbTypeEm.getDbTypeBySlaveNum(slaveNum);
if (dbTypeEm.isPresent()) {
setDb(dbTypeEm.get());
} else {
throw new IllegalArgumentException("从库未匹配");
}
}

这里根据余数来匹配对应DbType枚举,选出DataSource的Map需要的key,并且赋值到当前线程ThreadLocal中;

         /**
* 用于筛选从库 * @param slaveNum 从库顺序编号 0开始
* @return
*/
public static Optional<DbTypeEm> getDbTypeBySlaveNum(int slaveNum) {
return Arrays.stream(DbTypeEm.values()).filter(b -> b.getSlaveNum() == slaveNum).findFirst();
}

测试用例

完成上面操作后,我们搭建个测试例子,ArticleService中分别如下3个方法,不同点在于@DbType注解的标记:

 @Service
public class ArticleService { @Autowired
ArticleMapper articleMapper; @DbType
public Article selectById01(int id) {
Article article = articleMapper.selectById(id);
System.out.println(JsonUtil.formatMsg("selectById01:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle()));
return article;
} @DbType(isMaster = false)
public Article selectById02(int id) {
Article article = articleMapper.selectById(id);
System.out.println(JsonUtil.formatMsg("selectById02:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle()));
return article;
} public Article selectById(int id) {
Article article = articleMapper.selectById(id);
System.out.println(JsonUtil.formatMsg("selectById:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle()));
return article;
}
}

在同一个Controller层接口方法中去调用这3个service层方法,按照正常逻辑来讲,不出意外得到的结果是这样:

请求了两次接口,得到结果是:
selectById01方法:标记了@DbType,但默认走isMaster=true,实际走了db0(master)库
selectById02方法:标记了@DbType(isMaster = false),实际走了db1(slave1)库
selectById方法:没有标记了@DbType,实际走了db2(slave2)库,因为拦截器中没有找到DbType注解,让其走了slave方法;因为selectById02执行过一次slave方法,计数器+1了,因此余数也变了所以定位到了slave2库(如果是基数调用,selectById02和selectById方法来回切换走不同slave库);

springboot数据库主从方案的更多相关文章

  1. SpringBoot数据库读写分离之基于Docker构建主从数据库同步实例

    看了好久的SpringBoot结合MyBatista实现读写,但是一直没有勇气实现他,今天终于接触到了读写分离的东西,读写分离就是讲读操作执行在Slave数据库(从数据库),写操作在Master数据库 ...

  2. Mysql读写分离与主从数据库设置方案

    Mysql读写分离与主从数据库设置方案 亿仁网 18-10-0711:31 Mysql无非四个功能:增,删,改,读.而将增删改和读分离操作.这样有利于提高系统性能.下面是非常直观的操作: 1.配置: ...

  3. Mysql主从方案的实现

    Mysql主从方案介绍 mysql主从方案主要作用: 读写分离,使数据库能支撑更大的并发.在报表中尤其重要.由于部分报表sql语句非常的慢,导致锁表,影响前台服务.如果前台使用master,报表使用s ...

  4. MySQL-5.7数据库主从同步实战教程

    主从形式 MySQ主从复制原理(主库写入数据,从库读取数据) MySql常用命令: MySQL5.7设置密码 ') where user='root': MySQL5.6设置密码 ') WHERE U ...

  5. Spring AOP实现Mysql数据库主从切换(一主多从)

    设置数据库主从切换的原因:数据库中经常发生的是“读多写少”,这样读操作对数据库压力比较大,通过采用数据库集群方案, 一个数据库是主库,负责写:其他为从库,负责读,从而实现读写分离增大数据库的容错率.  ...

  6. 基于 EntityFramework 的数据库主从读写分离服务插件

    基于 EntityFramework 的数据库主从读写分离服务插件 1. 版本信息和源码 1.1 版本信息 v1.01 beta(2015-04-07),基于 EF 6.1 开发,支持 EF 6.1 ...

  7. (转)Mysql数据库主从心得整理

    Mysql数据库主从心得整理 原文:http://blog.sae.sina.com.cn/archives/4666 管理mysql主从有2年多了,管理过200多组mysql主从,几乎涉及到各个版本 ...

  8. redis 数据库主从不一致问题解决方案

     在聊数据库与缓存一致性问题之前,先聊聊数据库主库与从库的一致性问题. 问:常见的数据库集群架构如何? 答:一主多从,主从同步,读写分离. 如上图: (1)一个主库提供写服务 (2)多个从库提供读服务 ...

  9. Spring数据库主从分离

    1.spring+spring mvc +mybatis+druid 实现数据库主从分离 2.Spring+MyBatis主从读写分离 3.MyCat痛点 4.Spring+MyBatis实现数据库读 ...

随机推荐

  1. 【0802 | Day 7】Python进阶(一)

    目 录  数字类型的内置方法 一.整型内置方法(int) 二.浮点型内置方法(float) 字符串类型内置方法 一.字符串类型内置方法(str) 二.常用操作和内置方法 优先掌握: 1.索引取值 2. ...

  2. python之闭包+装饰器

    闭包 内部函数对外部函数作用域变量的引用. 函数内的属性都是有生命周期的,都是在函数执行期间 闭包内的闭包函数私有化了变量,类似于面向对象 图片解析 示例一 https://www.bilibili. ...

  3. 做梦也没有想到:Windows 上的 .NET Core 表现更糟糕

    昨天晚上 18:15 左右我们发布了跑在 Windows 上 .NET Core 博客系统,本想与 .NET Framework 版进行同“窗”的较量,结果刚发布上线就发现 CPU 占用异常高,发布不 ...

  4. 2019牛客暑期多校训练营(第十场)F-Popping Balloons

    >传送门< 题意:现在给你n个点 ,让你横着划三条线间距为r 然后竖着划三条线间距同样为r ,求经过最多的点数 思路:比赛看到这题的时候觉得能做,但是一看时间限制是5s,搞得我有不敢去碰了 ...

  5. Laravel框架内实现api文档:markdown转为html

    前后端分离的工作模式于今是非常流行了,前后端工作的对接,就离开不了API文档的辅助. 根据自己以往的工作经历,以及了解的一些资讯,API文档的建立,无非以下几种方式: 1. word文档模板 2. 第 ...

  6. Net微信网页开发之使用微信JS-SDK获取当前地理位置

    前言: 前段时间有一个关于通过获取用户当前经纬度坐标,计算出该用户距离某指定地点之间的距离.因为做这个项目需要能够获取到比较精确的经纬度坐标,刚开始使用的是百度地图结果发现百度地图地位不太准确(有时候 ...

  7. bi-Lstm +CRF 实现命名实体标注

    1. https://blog.csdn.net/buppt/article/details/82227030 (Bilstm+crf中的crf详解,包括是整体架构) 2. 邹博关于CRF的讲解视频 ...

  8. 2019年 iPad无法充电

    2019年 iPad无法充电  到售后网点检测没毛病,可能是apple产品做了低温保护逻辑机制低温无法充电,或者说是冬天温度跟iPad电池充电温度要求不符.各位有遇到情况的可以看看是不是这种问题,这问 ...

  9. CAP 2.6 版本发布通告

    前言 今天,我们很高兴宣布 CAP 发布 2.6 版本正式版.同时我们也很高兴的告诉你 CAP 在 GitHub 已经突破了3000 Star. 自从上次 CAP 2.5 版本发布 以来,已经过去了几 ...

  10. 蓝桥杯单片机CT107D 01 底层驱动基础

    代码下载 https://share.weiyun.com/5NHvLxG 这两个代码文件是其他底层驱动代码的基础: 包含了控制138573(间接控制数码管led和蜂鸣器等).delay延时函数.CT ...