server:本demo開發(fā)工具采用springSTS
前提讀寫分離庫已經(jīng)搭建好
1.首先新建一個(gè)springboot項(xiàng)目贺待。
2.項(xiàng)目新建成功之后早敬,個(gè)人習(xí)慣在springboot入口寫一個(gè)配置文件類與Application平級。如圖
下面逐一說明一下注解的含義雳刺。
@EnableWebMvc 說明啟用了spring mvc
@Configuration 讓spring boot 項(xiàng)目啟動(dòng)時(shí)識(shí)別當(dāng)前配置類(讓spring容器知道這個(gè)類是一個(gè)xml的配置類)
@ComponentScan 掃描注解
@MapperScan(basePackages = "com.wz.mail.mapper") 掃描dao
3.說明一下spring boot中的配置文件 application.properties 個(gè)人比較喜歡使用 application.yum(好處是比較有層級感)配置文件中的內(nèi)容如下
## context-path代表項(xiàng)目名稱 端口 以及超時(shí)時(shí)間
server:
context-path: /mail-producer
port: 8001
session:
timeout: 900
## Spring配置:
spring:
http:
encoding:
charset: UTF-8
## 序列化將時(shí)間默認(rèn)序列化為該格式的時(shí)間营密;not_null如果有null默認(rèn)過濾
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: NON_NULL
##此處采用druid數(shù)據(jù)源 主從配置基本一樣 master slave 數(shù)據(jù)庫ip要區(qū)分
druid:
type: com.alibaba.druid.pool.DruidDataSource
master:
url: jdbc:mysql://localhost/mail?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
initialSize: 5
minIdle: 1
#maxIdle: 10
maxActive: 100
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,log4j
useGlobalDataSourceStat: true
slave:
url: jdbc:mysql://localhost:3306/mail?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
initialSize: 5
minIdle: 1
#maxIdle: 10
maxActive: 100
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,log4j
useGlobalDataSourceStat: true
##指定mybatis的配置文件
mybatis:
mapper-locations: classpath:com/wz/mail/mapping/*.xml
4.現(xiàn)在我們配置了兩個(gè)數(shù)據(jù)源 南缓,再啟動(dòng)項(xiàng)目的時(shí)候得把這兩個(gè)數(shù)據(jù)源都加載進(jìn)來
(1)需要把這兩個(gè)數(shù)據(jù)源先注入進(jìn)來
package com.wz.mail.config;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
@Configuration//上邊有介紹
@EnableTransactionManagement //開啟事物spring提供的注解
public class DataSourceConfiguration {
private static Logger LOGGER = LoggerFactory.getLogger(DataSourceConfiguration.class);
//默認(rèn)去找application.yum中的druid.type相當(dāng)于將配置文件中的該值賦值給dataSourceType
@Value("${druid.type}")
private Class<? extends DataSource> dataSourceType;
@Bean(name = "masterDataSource")
@Primary//優(yōu)先選擇主數(shù)據(jù)源(原因可寫可讀)
@ConfigurationProperties(prefix = "druid.master") //意思是從application.yum中找druid.master開頭所有的信息都要放到要?jiǎng)?chuàng)建的masterDataSource并且交給spring管理
public DataSource masterDataSource() throws SQLException{
DataSource masterDataSource = DataSourceBuilder.create().type(dataSourceType).build();
LOGGER.info("========MASTER: {}=========", masterDataSource);
return masterDataSource;
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "druid.slave")
public DataSource slaveDataSource(){
DataSource slaveDataSource = DataSourceBuilder.create().type(dataSourceType).build();
LOGGER.info("========SLAVE: {}=========", slaveDataSource);
return slaveDataSource;
}
//druid監(jiān)控界面需要用的到servlet
@Bean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean reg = new ServletRegistrationBean();
reg.setServlet(new StatViewServlet());
reg.addUrlMappings("/druid/*");
reg.addInitParameter("allow", "localhost");
reg.addInitParameter("deny","/deny");
LOGGER.info(" druid console manager init : {} ", reg);
return reg;
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico, /druid/*");
LOGGER.info(" druid filter register : {} ", filterRegistrationBean);
return filterRegistrationBean;
}
}
現(xiàn)在該啟動(dòng)項(xiàng)目了只要出現(xiàn)兩個(gè)數(shù)據(jù)源中的log說明數(shù)據(jù)源啟動(dòng)成功日志如下:
2017-07-27 22:35:06.844 INFO 14424 --- [ main] c.w.mail.config.DataSourceConfiguration : ========MASTER: {
CreateTime:"2017-07-27 22:35:06",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}=========
2017-07-27 22:35:07.178 INFO 14424 --- [ main] c.w.mail.config.DataSourceConfiguration : ========SLAVE: {
CreateTime:"2017-07-27 22:35:07",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}=========
瀏覽器輸入http://localhost:8001/mail-producer/druid
成功訪問的druid監(jiān)控臺(tái)
接下來該mybatis來整合數(shù)據(jù)源,經(jīng)典的SqlSessionFactory 厌秒,將這兩個(gè)數(shù)據(jù)源交給SqlSessionFactory 來管理读拆。然后怎么區(qū)分哪個(gè)是主數(shù)據(jù)源還是從數(shù)據(jù)源呢?
首先實(shí)現(xiàn)讀寫分離就意味著有兩個(gè)數(shù)據(jù)源鸵闪,當(dāng)寫操作時(shí)對主庫使用檐晕,當(dāng)讀操作時(shí)對從庫使用。也就是說我們再啟動(dòng)數(shù)據(jù)庫連接池時(shí)要啟動(dòng)兩個(gè)蚌讼。
但我們在真正使用的時(shí)候辟灰,可以在方法上加自定義注解的形式來區(qū)分讀還是寫。
思路:
首先配置兩個(gè)數(shù)據(jù)源后(已經(jīng)配置如上)要區(qū)分兩個(gè)數(shù)據(jù)源篡石。分別是主數(shù)據(jù)源和從數(shù)據(jù)源芥喇。
可以通過mybatis配置文件把兩個(gè)數(shù)據(jù)源注入到應(yīng)用中。但是我們要想實(shí)現(xiàn)讀寫分離凰萨,也就
是什么情況下用寫继控,什么情況下用讀,這里需要自己定義一個(gè)標(biāo)識(shí)來區(qū)分胖眷。要實(shí)現(xiàn)一個(gè)即時(shí)
切換主從數(shù)據(jù)源的標(biāo)識(shí)并且能保證線程安全的基礎(chǔ)下操作數(shù)據(jù)源(原因是并發(fā)會(huì)影響數(shù)據(jù)源
的獲取分不清主從武通,造成在從庫進(jìn)行寫操作,影響mysql(mariadb)數(shù)據(jù)庫的機(jī)制珊搀,導(dǎo)致
服務(wù)器異常厅须。這里使用threadocal來解決這個(gè)問題)
然后需要自定義注解,在方法上有注解則為只讀食棕,沒有則為寫操作
package com.bhz.mail.config.database;
public class DataBaseContextHolder {
//區(qū)分主從數(shù)據(jù)源
public enum DataBaseType {
MASTER, SLAVE
}
//線程局部變量
private static final ThreadLocal<DataBaseType> contextHolder = new ThreadLocal<DataBaseType>();
//往線程里邊set數(shù)據(jù)類型
public static void setDataBaseType(DataBaseType dataBaseType) {
if(dataBaseType == null) throw new NullPointerException();
contextHolder.set(dataBaseType);
}
//從容器中獲取數(shù)據(jù)類型
public static DataBaseType getDataBaseType(){
return contextHolder.get() == null ? DataBaseType.MASTER : contextHolder.get();
}
//清空容器中的數(shù)據(jù)類型
public static void clearDataBaseType(){
contextHolder.remove();
}
}
將這兩種數(shù)據(jù)源交給SqlSessionFactory 來管理朗和。接下來寫一個(gè)mybatis的配置類相當(dāng)于傳統(tǒng)的mybatis.xml
先配置數(shù)據(jù)源,在注入到SqlSessionFactory (強(qiáng)依賴關(guān)系有先有后)
怎樣確保mybatis配置類中先加載數(shù)據(jù)源在注入SqlSessionFactory 呢簿晓?代碼如下:
package com.wz.mail.config;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.apache.bcel.util.ClassLoaderRepository;
import org.aspectj.apache.bcel.util.ClassLoaderRepository.SoftHashMap;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
*
* @author wz
*
*/
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})//這個(gè)文件在DataSourceConfiguration加載完成之后再加載MybatisConfiguration
public class MybatisConfiguration extends MybatisAutoConfiguration {
@Resource(name="masterDataSource")
private DataSource masterDataSource;
@Resource(name="slaveDataSource")
private DataSource slaveDataSource;
@Bean(name="sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
//放入datasource 需要mybatis的AbstractRoutingDataSource 實(shí)現(xiàn)主從切換
return super.sqlSessionFactory(roundRobinDataSourceProxy());
}
public AbstractRoutingDataSource roundRobinDataSourceProxy(){
ReadWriteSplitRoutingDataSource proxy = new ReadWriteSplitRoutingDataSource();
//proxy.
SoftHashMap targetDataSource = new ClassLoaderRepository.SoftHashMap();
targetDataSource.put(DataBaseContextHolder.DataBaseType.MASTER, masterDataSource);
targetDataSource.put(DataBaseContextHolder.DataBaseType.SLAVE, slaveDataSource);
//默認(rèn)數(shù)據(jù)源
proxy.setDefaultTargetDataSource(masterDataSource);
//裝入兩個(gè)主從數(shù)據(jù)源
proxy.setTargetDataSources(targetDataSource);
return proxy;
}
}
package com.wz.mail.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
//mybatis動(dòng)態(tài)代理類
class ReadWriteSplitRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataBaseContextHolder.getDataBaseType();
}
}
自定義只讀注解眶拉,含義就是將默認(rèn)的主數(shù)據(jù)源修改為只讀數(shù)據(jù)源
package com.bhz.mail.config.database;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})//該注解應(yīng)用在方法上
@Retention(RetentionPolicy.RUNTIME)//在運(yùn)行時(shí)運(yùn)行
public @interface ReadOnlyConnection {
}
package com.wz.mail.config;
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 ReadOnlyConnectionInterceptor implements Ordered {
public static final Logger LOGGER = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class);
@Around("@annotation(readOnlyConnection)")//在注解上加入切入點(diǎn)語法,實(shí)現(xiàn)方法
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ReadOnlyConnection readOnlyConnection) throws Throwable {
try{
LOGGER.info("---------------set database connection read only---------------");
DataBaseContextHolder.setDataBaseType(DataBaseContextHolder.DataBaseType.SLAVE);
Object result = proceedingJoinPoint.proceed();//讓這個(gè)方法執(zhí)行完畢
return result;
} finally {
DataBaseContextHolder.clearDataBaseType();
LOGGER.info("---------------clear database connection---------------");
}
}
@Override
public int getOrder() {
return 0;
}
}
代碼已經(jīng)OK憔儿,將注解寫到只讀方法上忆植。@ReadOnlyConnection
開始測試begin 日志打印如下
2017-07-30 21:35:13.499 INFO 8604 --- [nio-8001-exec-1] c.w.m.c.ReadOnlyConnectionInterceptor : ---------------set database connection 2 read only---------------
2017-07-30 21:35:13.735 INFO 8604 --- [nio-8001-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2017-07-30 21:35:13.761 INFO 8604 --- [nio-8001-exec-1] c.w.m.c.ReadOnlyConnectionInterceptor : ---------------clear database connection---------------
測試總共多少條:2
由日志可以看出使用的是只讀數(shù)據(jù)源并且使用之后清空容器里的數(shù)據(jù)源。