上节继续学习,稍微复杂的业务系统,一般会将数据库按业务拆开,比如产品系统的数据库放在product db中,订单系统的数据库放在order db中...,然后,如果量大了,可能每个库还要考虑做读、写分离,以进一步提高系统性能,下面就来看看如何处理:

核心思路:配置多个数据源,然后利用RoutingDataSource结合AOP来动态切不同的库。

要解决的问题:

1、配置文件中,多数据源的配置节点如何设计?

 druid:
type: com.alibaba.druid.pool.DruidDataSource
study:
master: #study库的主库
url: jdbc:mysql://localhost:/study?useSSL=false&characterEncoding=UTF-&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: A1b2c3@def.com
initial-size:
min-idle:
max-active:
test-on-borrow: true
slave: #study库的从库
url: jdbc:mysql://localhost:/study_slave?useSSL=false&characterEncoding=UTF-&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: A1b2c3@def.com
initial-size:
min-idle:
max-active:
test-on-borrow: true
product:
master: #product库的主库
url: jdbc:mysql://localhost:/product?useSSL=false&characterEncoding=UTF-&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: A1b2c3@def.com
initial-size:
min-idle:
max-active:
test-on-borrow: true
slave: #product库的从库
url: jdbc:mysql://localhost:/product_slave?useSSL=false&characterEncoding=UTF-&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: A1b2c3@def.com
initial-size:
min-idle:
max-active:
test-on-borrow: true

上面的配置写法供参数,如果slave节点数要扩展,按这个格式,改造成slave1,slave2... 自行扩展。

2、配置类如何设计?

 package com.cnblogs.yjmyzz.db.config;

 /**
* Created by jimmy on 6/18/17.
*/ import com.cnblogs.yjmyzz.db.datasource.DbContextHolder;
import com.cnblogs.yjmyzz.db.datasource.MasterSlaveRoutingDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map; @Configuration
@EnableTransactionManagement
public class DataSourceConfiguration { @Value("${druid.type}")
private Class<? extends DataSource> dataSourceType; @Bean(name = "studyMasterDataSource")
@ConfigurationProperties(prefix = "druid.study.master")
public DataSource studyMasterDataSource() {
return DataSourceBuilder.create().type(dataSourceType).build();
} @Bean(name = "studySlaveDataSource")
@ConfigurationProperties(prefix = "druid.study.slave")
public DataSource studySlaveDataSource1() {
return DataSourceBuilder.create().type(dataSourceType).build();
} @Bean(name = "productMasterDataSource")
@ConfigurationProperties(prefix = "druid.product.master")
public DataSource productMasterDataSource() {
return DataSourceBuilder.create().type(dataSourceType).build();
} @Bean(name = "productSlaveDataSource")
@ConfigurationProperties(prefix = "druid.product.slave")
public DataSource productSlaveDataSource1() {
return DataSourceBuilder.create().type(dataSourceType).build();
} @Bean(name = "dataSource")
@Primary
public AbstractRoutingDataSource dataSource() {
MasterSlaveRoutingDataSource proxy = new MasterSlaveRoutingDataSource();
Map<Object, Object> targetDataResources = new HashMap<>();
targetDataResources.put(DbContextHolder.DbType.PRODUCT_MASTER, productMasterDataSource());
targetDataResources.put(DbContextHolder.DbType.PRODUCT_SLAVE, productSlaveDataSource1());
targetDataResources.put(DbContextHolder.DbType.STUDY_MASTER, studyMasterDataSource());
targetDataResources.put(DbContextHolder.DbType.STUDY_SLAVE, studySlaveDataSource1());
proxy.setDefaultTargetDataSource(productMasterDataSource());
proxy.setTargetDataSources(targetDataResources);
proxy.afterPropertiesSet();
return proxy;
} }

参考这个,一看就明,不说多(注:@Primary一定要在动态数据源上,否则事务回滚无效!)

3、根据什么来切换db?

有很多选择,

a、用约定的方法前缀,比如:get/query/list开头的约定为读从库,其它为主库,但是这样还要考虑不同业务库的切换(即:何时切换到product库,何时切换到order库,可以再用不同的Scanner来处理,略复杂)

b、用自定义注解来处理,比如 @ProductMaster注解,表示切换到product的master库,这样同时把业务库,以及主还是从,一次性解决了,推荐这种。

这里,我定义了4个注解,代表product,study二个库的主及从。

4、aop在哪里拦截,如何拦截?

service层和mapper层都可以拦截,推荐在服务层拦截,否则如果一个业务方法里,即有读又有写,还得考虑如果遇到事务,要考虑的东西更多。

当然,如果拦截特定的注解,就不用过多考虑在哪个层,只认注解就行(当然,注解还是建议打在服务层上)。

dubbo-starter的一个小坑:spring boot中,只有managed bean才能用aop拦截,而dubbo-starter中的@service注解不是spring中的注解(是阿里package下的自定义注解),生成的service provider实例,aop拦截不到,解决办法,再加一个注解让spring认识它,参考:

Aop拦截类的参考代码如下:

 package com.cnblogs.yjmyzz.db.aspect;

 import com.cnblogs.yjmyzz.db.annotation.ProductMaster;
import com.cnblogs.yjmyzz.db.annotation.ProductSlave;
import com.cnblogs.yjmyzz.db.annotation.StudyMaster;
import com.cnblogs.yjmyzz.db.annotation.StudySlave;
import com.cnblogs.yjmyzz.db.datasource.DbContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component; @Aspect
@Component
public class MasterSlaveAspect implements Ordered { public static final Logger logger = LoggerFactory.getLogger(MasterSlaveAspect.class); /**
* 切换到product主库
*
* @param proceedingJoinPoint
* @param productMaster
* @return
* @throws Throwable
*/
@Around("@annotation(productMaster)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ProductMaster productMaster) throws Throwable {
try {
logger.info("set database connection to product-master only");
DbContextHolder.setDbType(DbContextHolder.DbType.PRODUCT_MASTER);
Object result = proceedingJoinPoint.proceed();
return result;
} finally {
DbContextHolder.clearDbType();
logger.info("restore database connection");
}
} /**
* 切换到product从库
*
* @param proceedingJoinPoint
* @param productSlave
* @return
* @throws Throwable
*/
@Around("@annotation(productSlave)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ProductSlave productSlave) throws Throwable {
try {
logger.info("set database connection to product-slave only");
DbContextHolder.setDbType(DbContextHolder.DbType.PRODUCT_SLAVE);
Object result = proceedingJoinPoint.proceed();
return result;
} finally {
DbContextHolder.clearDbType();
logger.info("restore database connection");
}
} /**
* 切换到study主库
*
* @param proceedingJoinPoint
* @param studyMaster
* @return
* @throws Throwable
*/
@Around("@annotation(studyMaster)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, StudyMaster studyMaster) throws Throwable {
try {
logger.info("set database connection to study-master only");
DbContextHolder.setDbType(DbContextHolder.DbType.STUDY_MASTER);
Object result = proceedingJoinPoint.proceed();
return result;
} finally {
DbContextHolder.clearDbType();
logger.info("restore database connection");
}
} /**
* 切换到study从库
*
* @param proceedingJoinPoint
* @param studySlave
* @return
* @throws Throwable
*/
@Around("@annotation(studySlave)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, StudySlave studySlave) throws Throwable {
try {
logger.info("set database connection to study-slave only");
DbContextHolder.setDbType(DbContextHolder.DbType.STUDY_SLAVE);
Object result = proceedingJoinPoint.proceed();
return result;
} finally {
DbContextHolder.clearDbType();
logger.info("restore database connection");
}
} @Override
public int getOrder() {
return 0;
}
}

5、其它事项

启用类上,一定要排除spring-boot自带的datasource配置,即:

 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy
@ComponentScan("com.cnblogs.yjmyzz")
@MapperScan(basePackages = "com.cnblogs.yjmyzz.dao.mapper")
public class ServiceProvider {
public static void main(String[] args) {
SpringApplication.run(ServiceProvider.class, args);
}
}

第1行:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

6、日志中如何输出格式化且带参数值的sql?

一般的sql输出是这样的:

我们可以把它变成下面这样:

是不是更友好!

方法:加一个mybtais的拦截器即可

package com.cnblogs.yjmyzz.db.interceptor;

import com.cnblogs.yjmyzz.util.PrettySQLFormatter;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties; /**
* Created by 菩提树下的杨过(http://yjmyzz.cnblogs.com/) on 28/07/2017.
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
public class MybatisInterceptor implements Interceptor { private static Logger logger = LoggerFactory.getLogger(MybatisInterceptor.class); private Properties properties; private final static SimpleDateFormat sdt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = null;
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
String sqlId = mappedStatement.getId();
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
Configuration configuration = mappedStatement.getConfiguration();
Object returnValue;
long start = System.currentTimeMillis();
returnValue = invocation.proceed();
long end = System.currentTimeMillis();
long time = (end - start);
if (time > 1) {
String sql = getSql(configuration, boundSql, sqlId, time);
logger.debug("mapper method ==> " + sql.split("\\^")[0] + "\n," + PrettySQLFormatter.getPrettySql(sql.split("\\^")[1]) + "\n\n," + "sql execute time ==> " + time + " ms\n\n");
}
return returnValue;
} @Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
} @Override
public void setProperties(Properties properties) {
this.properties = properties;
} public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId, long time) {
String sql = showSql(configuration, boundSql);
StringBuilder str = new StringBuilder(100);
str.append(sqlId);
str.append("^");
str.append(sql);
str.append("^");
str.append(time);
str.append("ms");
return str.toString();
} private static String getParameterValue(Object obj) {
String value;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
} else if (obj instanceof Date) {
value = "'" + sdt.format(obj) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
} }
return value;
} public static String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", getParameterValue(parameterObject)); } else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
}
}
}
}
return sql;
}
}

这里面还用了hibernate的一个小工具,用于格式化sql

package com.cnblogs.yjmyzz.util;

import org.hibernate.engine.jdbc.internal.FormatStyle;

public class PrettySQLFormatter {

    public static void print(String sql) {
System.out.println(FormatStyle.BASIC.getFormatter().format(sql));
} public static void print(String remark, String sql) {
System.out.println(remark
+ FormatStyle.BASIC.getFormatter().format(sql));
} public static String getPrettySql(String sql) {
return FormatStyle.BASIC.getFormatter().format(sql);
} public static String getPrettySql(String remark, String sql) {
return remark + FormatStyle.BASIC.getFormatter().format(sql);
} public static void main(String[] args) {
System.out.println(getPrettySql("select * from MyUser as A join MyFriend as B on A.id = B.pid where B.name like ? "));
}
}

接下来,把这个拦截器配置在mybatis-config.xml里

 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings> <plugins>
<plugin interceptor="com.cnblogs.yjmyzz.db.interceptor.MybatisInterceptor">
</plugin>
</plugins> </configuration>

最后在application.yml里指定mybatis-config.xml所在的路径:

示例源码见:https://github.com/yjmyzz/spring-boot-dubbo-demo (dubbox2.8.5-multi-ds分支)

spring-boot 速成(9) druid+mybatis 多数据源及读写分离的处理的更多相关文章

  1. MyBatis多数据源配置(读写分离)

    原文:http://blog.csdn.net/isea533/article/details/46815385 MyBatis多数据源配置(读写分离) 首先说明,本文的配置使用的最直接的方式,实际用 ...

  2. spring boot(七):springboot+mybatis多数据源最简解决方案

    说起多数据源,一般都来解决那些问题呢,主从模式或者业务比较复杂需要连接不同的分库来支持业务.我们项目是后者的模式,网上找了很多,大都是根据jpa来做多数据源解决方案,要不就是老的spring多数据源解 ...

  3. 07.深入浅出 Spring Boot - 数据访问之Mybatis(附代码下载)

    MyBatis 在Spring Boot应用非常广,非常强大的一个半自动的ORM框架. 代码下载:https://github.com/Jackson0714/study-spring-boot.gi ...

  4. SpringBoot2 + Druid + Mybatis 多数据源动态配置

    在大数据高并发的应用场景下,为了更快的响应用户请求,读写分离是比较常见的应对方案.读写分离会使用多数据源的使用.下面记录如何搭建SpringBoot2 + Druid + Mybatis  多数据源配 ...

  5. spring boot 2.0.0 + mybatis 报:Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

    spring boot 2.0.0 + mybatis 报:Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required 无法启动 ...

  6. Spring Boot 框架下使用MyBatis访问数据库之基于XML配置的方式

    MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以使用简单的 XML ...

  7. Spring Boot 整合 JPA 使用多个数据源

    介绍 JPA(Java Persistence API)Java 持久化 API,是 Java 持久化的标准规范,Hibernate 是持久化规范的技术实现,而 Spring Data JPA 是在 ...

  8. spring boot打印sql语句-mybatis

    spring boot打印sql语句-mybatis 概述 当自己编写的程序出现了BUG等等,找了很久 调试运行了几遍到mapper层也进去调试进了源码,非常麻烦 我就想打印出sql语句,好进行解决B ...

  9. 6、Spring Boot 2.x 集成 MyBatis

    1.6 Spring Boot 2.x 集成 MyBatis 简介 详细介绍如何在Spring Boot中整合MyBatis,并通过注解方式实现映射. 完整源码: 1.6.1 创建 spring-bo ...

随机推荐

  1. swift相关文档

    swift官方文档 swift官方文档 https://itunes.apple.com/cn/book/swift-programming-language/id881256329?mt=11 sw ...

  2. UVALive 6467

    题目链接 : http://acm.sdibt.edu.cn/vjudge/contest/view.action?cid=2186#problem/C 题意:对于斐波那契数列,每个数都mod m , ...

  3. RabbitMQ集群下队列存放消息的问题

    RabbitMQ中队列有两种模式 1.默认 Default 2.镜像 Mirror [类似于mongoDB,从一直在通过主的操作日志来进行同步] *如果将队列定义为镜像模式,那么这个队列也将区分主从, ...

  4. df -h执行卡住不动问题解决【转】

    昨天生产环境报日志写不进去了,因此 登陆线上环境后,习惯用df -h命令查看空间使用情况,结果发现该命令执行半天也没有返回. 因此使用mount命令查看该机器上的目录: [conversant@swi ...

  5. 从TFS 删除工作项

    一.进入命令执行窗口的方式 1.若你的TFS服务器上安装了VS开发工作,可以按以下步骤,进入命令行方式,图如下: 2.若你TFS上没有安装VS开发工作,可以从你的开发机器上的VS安装目录下(一般情况下 ...

  6. nginx安装报错:configure: error: the HTTP rewrite module requires the PCRE library

    参考:http://blog.51cto.com/williamx/958398 需要安装pcre-devel与openssl-devel yum -y install pcre-devel open ...

  7. SCU 4444: Travel(最短路)

    Travel The country frog lives in has n towns which are conveniently numbered by 1,2,…,n . Among n(n− ...

  8. LOJ 10160 - 「一本通 5.2 练习 3」周年纪念晚会 / 没有上司的晚会

    题面 传送门 Ural 州立大学的校长正在筹备学校的 8080 周年纪念聚会.由于学校的职员有不同的职务级别,可以构成一棵以校长为根的人事关系树.每个资源都有一个唯一的整数编号,从 $1$ 到 $N$ ...

  9. 【BZOJ2059】Buying Feed 购买饲料

    题面 约翰开车来到镇上,他要带V吨饲料回家.如果他的车上有X吨饲料,每公里就要花费X^2元,开车D公里就需要D* X^2元.约翰可以从N家商店购买饲料,所有商店都在一个坐标轴上,第i家店的位置是Xi, ...

  10. SQL Server 1

    一.登陆服务器 连接服务器方式分为两类,一类是Windows身份验证,一类是SQL身份验证.后者需要用户名和密码,需要自己创建. 二.创建数据库 在左边对象资源管理器中,选中数据库,右键选择新建数据库 ...