爲何須要讀寫分離html
當項目愈來愈大和併發越來大的狀況下,單個數據庫服務器的壓力確定也是愈來愈大,最終演變成數據庫成爲性能的瓶頸,並且當數據愈來愈多時,查詢也更加耗費時間,固然數據庫數據過大時,能夠採用數據庫分庫分表,同時數據庫壓力過大時,也能夠採用Redis等緩存技術來下降壓力,可是任何一種技術都不是萬金油,不少時候都是經過多種技術搭配使用,而本文主要就是介紹經過讀寫分離來加快數據庫讀取速度java
實現方式mysql
讀寫分離實現的方式有多種,可是多種都須要配置數據庫的主從複製git
方式一github
數據庫中間件實現,如Mycat等數據庫中間件,對於項目自己來講,只有一個數據源,就是連接到Mycat,再由mycat根據規則去選擇從哪一個庫獲取數據spring
方式二sql
代碼中配置多數據源,經過代碼控制使用哪一個數據源,本文也是主要介紹這種方式數據庫
優勢apache
1.下降數據庫讀取壓力,尤爲是有些須要大量計算的實時報表類應用緩存
2.加強數據安全性,讀寫分離有個好處就是數據近乎實時備份,一旦某臺服務器硬盤發生了損壞,從庫的數據能夠無限接近主庫
3.能夠實現高可用,固然只是配置了讀寫分離並不能實現搞可用,最多就是在Master(主庫)宕機了還能進行查詢操做,具體高可用還須要其餘操做
缺點
1.增大成本,一臺數據庫服務器和多臺數據庫的成本確定是不同的
2.增大代碼複雜度,不過這點還比較輕微吧,可是也的確會必定程度上加劇
3.增大寫入成本,雖然下降了讀取成本,可是寫入成本倒是一點也沒有下降,畢竟還有從庫一直在向主庫請求數據
實踐:
項目結構:
application.yml
spring: datasource: master: # 寫帳戶 jdbc-url: jdbc:mysql://localhost:3306/otadb?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false username: root password: 123456 slave1: # 只讀帳戶 jdbc-url: jdbc:mysql://localhost:3306/otadb1?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false username: root password: 123456 slave2: # 只讀帳戶 jdbc-url: jdbc:mysql://localhost:3306/otadb2?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false username: root password: 123456
多數據源配置
import com.microservice.readwriteseparat.bean.MyRoutingDataSource; import com.microservice.readwriteseparat.enums.DBTypeEnum; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { /** * 寫庫 * @return */ @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } /** * 讀庫 * @return */ @Bean @ConfigurationProperties("spring.datasource.slave1") public DataSource slave1DataSource() { return DataSourceBuilder.create().build(); } /** * 讀庫 * @return */ @Bean @ConfigurationProperties("spring.datasource.slave2") public DataSource slave2DataSource() { return DataSourceBuilder.create().build(); } @Bean public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DBTypeEnum.MASTER, masterDataSource); targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource); targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource); MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource(); myRoutingDataSource.setDefaultTargetDataSource(masterDataSource); myRoutingDataSource.setTargetDataSources(targetDataSources); return myRoutingDataSource; } }
這裏,咱們配置了4個數據源,1個master,2兩個slave,1個路由數據源。前3個數據源都是爲了生成第4個數據源,並且後續咱們只用這最後一個路由數據源myRoutingDataSource。
MyBatis配置
import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.annotation.Resource; import javax.sql.DataSource; @EnableTransactionManagement @Configuration public class MyBatisConfig { @Resource(name = "myRoutingDataSource") private DataSource myRoutingDataSource; /** * 掃描mybatis下的xml文件 * @return * @throws Exception */ @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(myRoutingDataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/*.xml")); return sqlSessionFactoryBean.getObject(); } @Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(myRoutingDataSource);//因爲Spring容器中如今有4個數據源,因此咱們須要爲事務管理器和MyBatis手動指定一個明確的數據源。 } }
定義一個枚舉來表明這三個數據源
public enum DBTypeEnum { MASTER,SLAVE1,SLAVE2 }
經過ThreadLocal將數據源設置到每一個線程上下文中
import com.microservice.readwriteseparat.enums.DBTypeEnum; import java.util.concurrent.atomic.AtomicInteger; public class DBContextHolder { private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>(); private static final AtomicInteger counter = new AtomicInteger(-1); public static void set(DBTypeEnum dbType) { contextHolder.set(dbType); } public static DBTypeEnum get() { return contextHolder.get(); } public static void master() { set(DBTypeEnum.MASTER); System.out.println("切換到master"); } public static void slave() { // 輪詢 int index = counter.getAndIncrement() % 2; if (counter.get() > 9999) { counter.set(-1); } if (index == 0) { set(DBTypeEnum.SLAVE1); System.out.println("切換到slave1"); }else { set(DBTypeEnum.SLAVE2); System.out.println("切換到slave2"); } } }
獲取路由key
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.lang.Nullable; public class MyRoutingDataSource extends AbstractRoutingDataSource { @Nullable @Override protected Object determineCurrentLookupKey() { return DBContextHolder.get(); } }
默認狀況下,全部的查詢都走從庫,插入/修改/刪除走主庫。咱們經過方法名來區分操做類型(CRUD)
import com.microservice.readwriteseparat.bean.DBContextHolder; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class DataSourceAop { /** * 只讀: * 不是Master註解的對象或方法 && select開頭的方法 || get開頭的方法 */ @Pointcut("!@annotation(com.microservice.readwriteseparat.annotation.Master) " + "&& (execution(* com.microservice.readwriteseparat.service..*.select*(..)) " + "|| execution(* com.microservice.readwriteseparat.service..*.get*(..)))") public void readPointcut() { } /** * 寫: * Master註解的對象或方法 || insert開頭的方法 || add開頭的方法 || update開頭的方法 * || edlt開頭的方法 || delete開頭的方法 || remove開頭的方法 */ @Pointcut("@annotation(com.microservice.readwriteseparat.annotation.Master) " + "|| execution(* com.microservice.readwriteseparat.service..*.insert*(..)) " + "|| execution(* com.microservice.readwriteseparat.service..*.add*(..)) " + "|| execution(* com.microservice.readwriteseparat.service..*.update*(..)) " + "|| execution(* com.microservice.readwriteseparat.service..*.edit*(..)) " + "|| execution(* com.microservice.readwriteseparat.service..*.delete*(..)) " + "|| execution(* com.microservice.readwriteseparat..*.remove*(..))") public void writePointcut() { } @Before("readPointcut()") public void read() { DBContextHolder.slave(); } @Before("writePointcut()") public void write() { DBContextHolder.master(); } }
有通常狀況就有特殊狀況,特殊狀況是某些狀況下咱們須要強制讀主庫,針對這種狀況,咱們定義一個主鍵,用該註解標註的就讀主庫
public @interface Master { }
mapper:
import com.microservice.readwriteseparat.po.TestPO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface TestPOMapper { int deleteByPrimaryKey(Long id); int insert(TestPO record); int insertSelective(TestPO record); TestPO selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(TestPO record); int updateByPrimaryKey(TestPO record); }
po:
public class TestPO { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name == null ? null : name.trim(); } }
service:
import com.microservice.readwriteseparat.annotation.Master; import com.microservice.readwriteseparat.mapper.TestPOMapper; import com.microservice.readwriteseparat.po.TestPO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class TestService{ @Autowired private TestPOMapper testPOMapper; public int insert(TestPO aaa) { return testPOMapper.insert(aaa); } @Master public int save(TestPO aaa) { return testPOMapper.insert(aaa); } public TestPO selectByPrimaryKey(Long id) { return testPOMapper.selectByPrimaryKey(id); } @Master public TestPO getById(Long id) { // 有些讀操做必須讀主數據庫 // 好比,獲取微信access_token,由於高峯時期主從同步可能延遲 // 這種狀況下就必須強制從主數據讀 return testPOMapper.selectByPrimaryKey(id); } }
測試:
import com.microservice.readwriteseparat.po.TestPO; import com.microservice.readwriteseparat.service.TestService; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class ReadwriteseparatApplicationTests { private static final Logger logger = LoggerFactory.getLogger(ReadwriteseparatApplicationTests.class); @Autowired private TestService aaaService; /** * 寫庫進行寫入 */ @Test public void testWrite() { TestPO aaa = new TestPO(); aaaService.insert(aaa); } /** * 讀庫(otadb1和otadb2隨機)進行讀取 */ @Test public void testRead() { TestPO aaa = aaaService.selectByPrimaryKey(1l); logger.info("aaa="+aaa.toString()); } /** * 寫庫進行寫入 */ @Test public void testSave() { TestPO aaa = new TestPO(); aaaService.save(aaa); } /** * 寫庫進行讀取 */ @Test public void testReadFromMaster() { aaaService.getById(10001l); } }
查看控制檯:
源碼地址:https://github.com/qjm201000/micro_service_readwriteseparat.git
參考資料:https://www.cnblogs.com/cjsblog/p/9712457.html