springboot+ssm+mysql 读写分离+动态修改数据源

一.我们最开始先实现读写分离(其实和多数据源差不多,只是多数据源的定义更加广泛,读写分离只是其中的一个应用而已)

这里就不怎么探讨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);
    }
    
}

--------------这是简单的数据库多数据源的应用— 读写分离,动态管理数据源我也已经做出来了,后续继续更新---------------------

猜你喜欢

转载自blog.csdn.net/qq_40384690/article/details/83418248
今日推荐