1. 程式人生 > >springboot+ssm+mysql 讀寫分離+動態修改資料來源

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);
    }
    
}

--------------這是簡單的資料庫多資料來源的應用— 讀寫分離,動態管理資料來源我也已經做出來了,後續繼續更新---------------------