springboot實現讀寫分離
server:本demo開發工具採用springSTS
前提讀寫分離庫已經搭建好
1.首先新建一個springboot專案。
2.專案新建成功之後,個人習慣在springboot入口寫一個配置檔案類與Application平級。如圖
下面逐一說明一下註解的含義。
@EnableWebMvc 說明啟用了spring mvc
@Configuration 讓spring boot 專案啟動時識別當前配置類(讓spring容器知道這個類是一個xml的配置類)
@ComponentScan 掃描註解
@MapperScan(basePackages = "com.wz.mail.mapper") 掃描dao
3.說明一下spring boot中的配置檔案 application.properties 個人比較喜歡使用 application.yum(好處是比較有層級感)配置檔案中的內容如下
## context-path代表專案名稱 埠 以及超時時間
server:
context-path: /mail-producer
port: 8001
session:
timeout: 900
## Spring配置:
spring:
http:
encoding:
charset: UTF-8
## 序列化將時間預設序列化為該格式的時間;not_null如果有null預設過濾
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: NON_NULL
##此處採用druid資料來源 主從配置基本一樣 master slave 資料庫ip要區分
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.現在我們配置了兩個資料來源 ,再啟動專案的時候得把這兩個資料來源都載入進來
(1)需要把這兩個資料來源先注入進來
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);
//預設去找application.yum中的druid.type相當於將配置檔案中的該值賦值給dataSourceType
@Value("${druid.type}")
private Class<? extends DataSource> dataSourceType;
@Bean(name = "masterDataSource")
@Primary//優先選擇主資料來源(原因可寫可讀)
@ConfigurationProperties(prefix = "druid.master") //意思是從application.yum中找druid.master開頭所有的資訊都要放到要建立的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監控介面需要用的到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;
}
}
現在該啟動專案了只要出現兩個資料來源中的log說明資料來源啟動成功日誌如下:
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監控臺
接下來該mybatis來整合資料來源,經典的SqlSessionFactory ,將這兩個資料來源交給SqlSessionFactory 來管理。然後怎麼區分哪個是主資料來源還是從資料來源呢?
首先實現讀寫分離就意味著有兩個資料來源,當寫操作時對主庫使用,當讀操作時對從庫使用。也就是說我們再啟動資料庫連線池時要啟動兩個。
但我們在真正使用的時候,可以在方法上加自定義註解的形式來區分讀還是寫。
思路:
首先配置兩個資料來源後(已經配置如上)要區分兩個資料來源。分別是主資料來源和從資料來源。
可以通過mybatis配置檔案把兩個資料來源注入到應用中。但是我們要想實現讀寫分離,也就
是什麼情況下用寫,什麼情況下用讀,這裡需要自己定義一個標識來區分。要實現一個即時
切換主從資料來源的標識並且能保證執行緒安全的基礎下運算元據源(原因是併發會影響資料來源
的獲取分不清主從,造成在從庫進行寫操作,影響mysql(mariadb)資料庫的機制,導致
伺服器異常。這裡使用threadocal來解決這個問題)
然後需要自定義註解,在方法上有註解則為只讀,沒有則為寫操作
package com.bhz.mail.config.database;
public class DataBaseContextHolder {
//區分主從資料來源
public enum DataBaseType {
MASTER, SLAVE
}
//執行緒區域性變數
private static final ThreadLocal<DataBaseType> contextHolder = new ThreadLocal<DataBaseType>();
//往執行緒裡邊set資料型別
public static void setDataBaseType(DataBaseType dataBaseType) {
if(dataBaseType == null) throw new NullPointerException();
contextHolder.set(dataBaseType);
}
//從容器中獲取資料型別
public static DataBaseType getDataBaseType(){
return contextHolder.get() == null ? DataBaseType.MASTER : contextHolder.get();
}
//清空容器中的資料型別
public static void clearDataBaseType(){
contextHolder.remove();
}
}
將這兩種資料來源交給SqlSessionFactory 來管理。接下來寫一個mybatis的配置類相當於傳統的mybatis.xml
先配置資料來源,在注入到SqlSessionFactory (強依賴關係有先有後)
怎樣確保mybatis配置類中先載入資料來源在注入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})//這個檔案在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 實現主從切換
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);
//預設資料來源
proxy.setDefaultTargetDataSource(masterDataSource);
//裝入兩個主從資料來源
proxy.setTargetDataSources(targetDataSource);
return proxy;
}
}
package com.wz.mail.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
//mybatis動態代理類
class ReadWriteSplitRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataBaseContextHolder.getDataBaseType();
}
}
自定義只讀註解,含義就是將預設的主資料來源修改為只讀資料來源
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})//該註解應用在方法上
@Retention(RetentionPolicy.RUNTIME)//在執行時執行
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)")//在註解上加入切入點語法,實現方法
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();//讓這個方法執行完畢
return result;
} finally {
DataBaseContextHolder.clearDataBaseType();
LOGGER.info("---------------clear database connection---------------");
}
}
@Override
public int getOrder() {
return 0;
}
}
程式碼已經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
由日誌可以看出使用的是隻讀資料來源並且使用之後清空容器裡的資料來源。
相關文章
- SpringBoot 專案優雅實現讀寫分離Spring Boot
- ShardingSphere + Mysql,實現分庫分表、讀寫分離,並整合 SpringBootMySqlSpring Boot
- 分庫分表(6)--- SpringBoot+ShardingSphere實現分表+ 讀寫分離Spring Boot
- MySQL-SpringBoot整合JPA實現資料讀寫分離MySqlSpring Boot
- Sharding-JDBC基本使用,整合Springboot實現分庫分表,讀寫分離JDBCSpring Boot
- springboot+mybatis+druid實現mysql主從讀寫分離(五)Spring BootMyBatisUIMySql
- ProxySQL實現MySQL讀寫分離MySql
- ShardingSphere(七) 讀寫分離配置,實現分庫讀寫操作
- 【Mongo】Mongo讀寫分離的實現Go
- Docker實現Mariadb分庫分表、讀寫分離Docker
- SpringBoot使用Sharding-JDBC讀寫分離Spring BootJDBC
- Kubernetes 中實現 MySQL 的讀寫分離MySql
- PostgreSQL+Pgpool實現HA讀寫分離SQL
- docker+atlas+mysql實現讀寫分離DockerMySql
- 讀寫分離很難嗎?springboot結合aop簡單就實現了Spring Boot
- 位元組面試:什麼是讀寫分離?讀寫分離的底層如何實現?面試
- 搭建MySQL主從實現Django讀寫分離MySqlDjango
- MHA+ProxySQL實現讀寫分離高可用SQL
- Spring Aop實現資料庫讀寫分離Spring資料庫
- ProxySQL實現Mysql讀寫分離 - 部署手冊MySql
- 搭建基於springmvc,ibatis的工程實現讀寫分離,配置分離SpringMVCBAT
- springboot多資料來源配合docker部署mysql主從實現讀寫分離Spring BootDockerMySql
- 基於Sharding-Jdbc 實現的讀寫分離實現JDBC
- Mycat中介軟體實現Percona Cluster讀寫分離
- redis客戶端實現高可用讀寫分離Redis客戶端
- Mycat實現mysql的負載均衡讀寫分離MySql負載
- StoneDB 讀寫分離實踐方案
- ShardingSphere-proxy +PostgreSQL實現讀寫分離(靜態策略)SQL
- Mycat中介軟體實現Mysql主從讀寫分離MySql
- 關於Dapper實現讀寫分離的個人思考APP
- 資料庫讀寫分離,主從同步實現方法資料庫主從同步
- mysql讀寫分離的最佳實踐MySql
- MySQL怎麼實現主從同步和Django實現MySQL讀寫分離MySql主從同步Django
- 搭建Redis哨兵叢集並使用RedisTemplate實現讀寫分離Redis
- KunlunBase 讀寫分離方案
- Redis的讀寫分離Redis
- Laravel讀寫分離原理Laravel
- discuz 配置讀寫分離(主寫從讀)
- MyCat分庫分表、讀寫分離