本篇分享数据库主从方案,案例采用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. go 学习笔记之有意思的变量和不安分的常量

    首先希望学习 Go 语言的爱好者至少拥有其他语言的编程经验,如果是完全零基础的小白用户,本教程可能并不适合阅读或尝试阅读看看,系列笔记的目标是站在其他语言的角度学习新的语言,理解 Go 语言,进而写出 ...

  2. html5 拖拽(drag)和f放置(drop)

    知识要点 HTML5 (drag&drop) API  (Event) 拖放数据(对象):DataTransfer 拖放内容:setData getData 拖放效果(动作):dropEffe ...

  3. 解决 Nginx 代理Apex慢的问题

    前不久用 Nginx 代理 Oracle 的 Apex 速度非常慢,我之前Nginx 配置如下: server{ listen 80; server_name localhost; client_ma ...

  4. Vue-Router中History模式

    目录 history路由 官方示例 Express中间件 客户端兜底404 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在 ...

  5. 02 http和https协议

    1. HTTP协议 简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写, 是用于从万维网(WWW:World Wide Web )服务器传输超文本到本 ...

  6. 三维动画形变算法(Gradient-Based Deformation)

    将三角网格上的顶点坐标(x,y,z)看作3个独立的标量场,那么网格上每个三角片都存在3个独立的梯度场.该梯度场是网格的微分属性,相当于网格的特征,在形变过程中随控制点集的移动而变化.那么当用户拖拽网格 ...

  7. vi 多行注释与取消

    多行注释 1.在命令行模式下,按 Shift + v 进入 VISUAL LINE 模式 2.选择要注释内容 3.按下 Ctrl + Shift + v 锁定块(XShell中) 或 按下 Ctrl ...

  8. python 18 re模块

    目录 re 模块 1. 正则表达式 2. 匹配模式 3. 常用方法 re 模块 1. 正则表达式 \w 匹配字母(包含中文)或数字或下划线 \W 匹配非字母(包含中文)或数字或下划线 \s 匹配任意的 ...

  9. Nginx和Apache各自的优缺点

    nginx 相对 apache 的优点: 轻量级,同样起web 服务,比apache 占用更少的内存及资源 抗并发,nginx 处理请求是异步非阻塞的,而apache 则是阻塞型的,在高并发下ngin ...

  10. Unity之SDK接入(OPPO)

    简介:首先介绍一下,为什么博主要选择OPPO的SDK接入呢,因为OPPO的SDK接入是目前博主发现最简单的SDK.所以,博主选择OPPO,带领大家接SDK从入门到精通 工作准备: 1.环境配置(SDK ...