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);
}
}
--------------這是簡單的資料庫多資料來源的應用— 讀寫分離,動態管理資料來源我也已經做出來了,後續繼續更新---------------------