一.我们最开始先实现读写分离(其实和多数据源差不多,只是多数据源的定义更加广泛,读写分离只是其中的一个应用而已)
这里就不怎么探讨mysql的主从的一个原理了,我直接贴出一个博客,可以去看看,大致了解一下mysql主从。
我学东西喜欢先跑一次,如果成功了,我就再深入研究了,其实大体的逻辑还是很简单,在service层做一个dataSource的选择,(网上有很多在dao层做,这是不合道理的,因为mysql默认级别是RR,如果在一个有写的事务当中读是有快照,必须保证读出来的东西是一样的,因此直接选择在service进行处理,并且service中可能存在多个表的操作,因此事务在service层才是对的)。
我最开始参考的博客是:https://blog.csdn.net/wsbgmofo/article/details/79260896(这个博客写出来的demo有一个缺点,不能够有事务)
大家入门的话,可以采用这一个,至少还是可以运行起来的。那么接下来就准备一边攻克原理,一边进行修改,争取试试能不能往分表的用途上用。
那么首先整理一下大致的思路哈。
这是大致的思路图,那么接下来就是实现了。
看起来那么简单。其实突然发现如果不深入了解spring的话,那么看起来基本是很吃力的。那么又得讲一下spring的事务机制了。假设你在service层注入了事务的话,那么你先得确认该service使用的dataSource是哪个dataSource。那么这个时候,你就应该需要自己去告诉spring,这个方法应该选择哪个dataSource,那么为了不侵入业务代码,那么就采用aop的方式来做。
那么首先我们还是需要贴出博客中的application.properties
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 主数据源,默认的
spring.master.driver-class-name=com.mysql.jdbc.Driver
spring.master.url=jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&verifyServerCertificate=false&useSSL=false&requireSSL=false
spring.master.username=root
spring.master.password=root
spring.master.initialSize=5
spring.master.minIdle=5
spring.master.maxActive=50
spring.master.maxWait=60000
spring.master.timeBetweenEvictionRunsMillis=60000
spring.master.minEvictableIdleTimeMillis=300000
spring.master.poolPreparedStatements=true
spring.master.maxPoolPreparedStatementPerConnectionSize=20
# 从数据源
spring.slave.1.driver-class-name=com.mysql.jdbc.Driver
spring.slave.1.url=jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&verifyServerCertificate=false&useSSL=false&requireSSL=false
spring.slave.1.username=root
spring.slave.1.password=root
spring.slave.1.initialSize=5
spring.slave.1.minIdle=5
spring.slave.1.maxActive=50
spring.slave.1.maxWait=60000
spring.slave.1.timeBetweenEvictionRunsMillis=60000
spring.slave.1.minEvictableIdleTimeMillis=300000
spring.slave.1.poolPreparedStatements=true
spring.slave.1.maxPoolPreparedStatementPerConnectionSize=20
(1)
那么就得先有这两个读写分离的数据源—dataSource。
@Configuration
public class DataSourceConfig {
private static final Logger logger=LoggerFactory.getLogger(DataSourceConfig.class);
//是为了和具体的 连接池的实现 解耦
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Autowired
private Environment environment;
@Value("${spring.datasource.slave.size}")
private String slaveSize;
/**
* 写的数据源
*/
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.master")
//当一个接口有多个实现类时,需要primary来作为一个默认
@Primary
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 这里的list是多个从库的情况下为了实现简单负载均衡
* @return
* @throws SQLException
*/
@Bean("readDataSources")
public List<DataSource> readDataSources(ApplicationContext ac) throws SQLException{
List<DataSource> dataSources=new ArrayList<>();
DataSource dataSource=null;
String prefix=new String("spring.slave.");
Integer size = Integer.valueOf(slaveSize);
for(int i=1;i<=size;i++) {
try {
String temp=prefix+i;
String driverClassName = environment.getProperty(temp+".driver-class-name");
String url = environment.getProperty(temp+".url");
String password = environment.getProperty(temp+".password");
String username = environment.getProperty(temp+".username");
dataSource=DataSourceBuilder.create().type(dataSourceType)
.url(url).password(password).username(username).driverClassName(driverClassName).build();
dataSources.add(dataSource);
}catch (Exception e) {
logger.error("initialization dataSource" + i+" failed");
throw e;
}
}
if(dataSources.size() != size)
logger.info("real size not equal,you want "+size +" dataSources,but you create "+dataSources.size()+" dataSources");
return dataSources;
}
}
(2)
关键的地方在于AbstractRoutingDataSource这个类上。首先看一下源码,在获取dataSource的时候。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//因此,只需要实现lookupKey这个方法就可以了
Object lookupKey = determineCurrentLookupKey();
//resolvedDataSources 是一个map
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 + "]");
}
return dataSource;
}
那么接下来就看一下我的继承超类
/**
* 关键路由dataSource的关键类
* 这里默认实现了主 - 从的选择,而让其子类实现一个路由的选择即可
*/
public abstract class BaseAbstractRoutingDataSource extends AbstractRoutingDataSource{
/**
* 作为final的原因,是让一个子类来继承,但是不能够重写该方法
* 只需要实现该路由方法就可以了
*/
@Override
protected final Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if(null != typeKey && typeKey.equals("master")) {
return null;
}
//路由的选择
Object lookupKey = null;
try {
lookupKey = getLookupKey();
} catch (Exception e) {
logger.error("choose dataSource have happened exception:",e);
//默认使用主库
return null;
}
return lookupKey;
}
/**
* 具体的路由方法
* @return 返回的map中的key
*/
protected abstract Object getLookupKey() throws Exception;
}
那么如果你需要实现自己的路由方式的话,那么你可以创建一个BaseAbstractRoutingDataSource 的实现即可,重写getLookupKey()方法即可。那么默认的实现的话,是采用最简单的轮询机制。
/**
* 最简单的 路由:轮询
*/
public class DefaultAbstractRoutingDataSource extends BaseAbstractRoutingDataSource{
private int dataSourceNumber;
private AtomicInteger times=new AtomicInteger(0);
public DefaultAbstractRoutingDataSource(int dataSourceNumber) {
this.dataSourceNumber=dataSourceNumber;
}
@Override
protected Object getLookupKey() throws Exception{
int time = times.incrementAndGet();
int result = time % dataSourceNumber;
return result;
}
}
那么接下来看一下MybatisConfig
@Configuration
@Import({ DataSourceConfig.class})
public class ORMConfig {
/**
* 注入 SqlSessionFactory
*/
@Bean
@ConditionalOnMissingBean(name= {"sqlSessionFactory"})
public SqlSessionFactory sqlSessionFactory(ApplicationContext ac) throws Exception {
SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
factoryBean.setDataSource((DataSource) ac.getBean("myAbstractRoutingDataSource"));
return factoryBean.getObject();
}
/**
* 生成我们自己的AbstractRoutingDataSource
*/
@Bean("myAbstractRoutingDataSource")
@ConditionalOnBean(name={"targetDataSourcesMap","masterDataSource","defaultAbstractRoutingDataSource"})
public AbstractRoutingDataSource myAbstractRoutingDataSource(ApplicationContext ac) {
BaseAbstractRoutingDataSource myAbstractRoutingDataSource=(BaseAbstractRoutingDataSource) ac.getBean("defaultAbstractRoutingDataSource");
myAbstractRoutingDataSource.setDefaultTargetDataSource(ac.getBean("masterDataSource"));
return myAbstractRoutingDataSource;
}
/**
* 如果是你自己要实现路由,那么你生成一个defaultAbstractRoutingDataSource即可
* @return
*/
@Bean("defaultAbstractRoutingDataSource")
@ConditionalOnMissingBean(name= {"defaultAbstractRoutingDataSource"})
public AbstractRoutingDataSource defaultAbstractRoutingDataSource(ApplicationContext ac) {
Map<Object, Object> targetDataSources=(Map<Object, Object>) ac.getBean("targetDataSourcesMap");
BaseAbstractRoutingDataSource myAbstractRoutingDataSource=new DefaultAbstractRoutingDataSource(targetDataSources.size());
myAbstractRoutingDataSource.setTargetDataSources(targetDataSources);
return myAbstractRoutingDataSource;
}
/**
* 如果是你自己要实现路由,那么你生成一个map,注入给spring,命名为targetDataSourcesMap 即可
* @return
*/
@Bean("targetDataSourcesMap")
@ConditionalOnMissingBean(name= {"targetDataSourcesMap"})
public Map<Object, Object> targetDataSourcesMap(ApplicationContext ac){
List<DataSource> dataSources = (List<DataSource>) ac.getBean("readDataSources");
Map<Object, Object> targetDataSources=new HashMap<>();
for(int i=0;i<dataSources.size();i++)
targetDataSources.put(i, dataSources.get(i));
return targetDataSources;
}
/**
* 事务
*/
@Bean
public PlatformTransactionManager platformTransactionManager(ApplicationContext ac) {
return new DataSourceTransactionManager((DataSource) ac.getBean("myAbstractRoutingDataSource"));
}
}
(3)重点关注一下BaseAbstractRoutingDataSource 中的DataSourceContextHolder.getJdbcType();
public class DataSourceContextHolder {
/**
* 用来存放 当前service线程使用的数据源类型
*/
private static ThreadLocal<String> local=new ThreadLocal<>();
public static String getJdbcType() {
String type = local.get();
if(null == type) {
slave();
}
return type;
}
/**
* 从
*/
public static void slave() {
local.set(DataSourceType.SLAVE.getValue());
}
/**
* 主
*/
public static void master() {
local.set(DataSourceType.MASTER.getValue());
}
/**
* 还原
*/
public static void restore() {
local.set(null);
}
}
/**
* 主从 枚举
*/
public enum DataSourceType {
MASTER("主","master"),SLAVE("从","slave");
private String desc;
private String value;
private DataSourceType(String desc, String value) {
this.desc = desc;
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
private DataSourceType(String desc) {
this.desc = desc;
}
}
最重要的地方来了,就是aop
@Aspect
@Component
public class ChooseDataSourceAspect {
private static Logger log = LoggerFactory.getLogger(ChooseDataSourceAspect.class);
/**
* 主的 切入点
*/
//annotation里面是 注解的全路径
@Pointcut("@annotation(com.anno.dataSource.MasterAnnotation)")
public void masterPointCut() {}
/**
* 因为我想要的效果是,那么就会默认选择从
*/
@Before("masterPointCut()")
public void setMasterDataSource(JoinPoint point) {
DataSourceContextHolder.master();
log.info("dataSource切换到:write");
}
@After("masterPointCut()")
public void restoreDataSource(JoinPoint point) {
DataSourceContextHolder.restore();
log.info("dataSource已还原");
}
}
/**
* 主 的数据源的枚举
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterAnnotation {
String description() default "master";
}
(4)那么到了service层的使用
@Service
public class UserServiceImpl implements IUserService{
@Autowired
private UserMapper userMapper;
//没写注解就会是默认的负载 读数据源
@Override
public List<Map<String, Object>> readUser() {
return userMapper.readUser();
}
//写了注解就是写数据源
@Override
@MasterAnnotation
public void writerUser(User u) {
userMapper.writeUser(u);
}
}
--------------这是简单的数据库多数据源的应用— 读写分离,动态管理数据源我也已经做出来了,后续继续更新---------------------