微服務-springboot-讀寫分離(多數據源切換)


為什么需要讀寫分離

當項目越來越大和並發越來大的情況下,單個數據庫服務器的壓力肯定也是越來越大,最終演變成數據庫成為性能的瓶頸,而且當數據越來越多時,查詢也更加耗費時間,當然數據庫數據過大時,可以采用數據庫分庫分表,同時數據庫壓力過大時,也可以采用Redis等緩存技術來降低壓力,但是任何一種技術都不是萬金油,很多時候都是通過多種技術搭配使用,而本文主要就是介紹通過讀寫分離來加快數據庫讀取速度

實現方式

讀寫分離實現的方式有多種,但是多種都需要配置數據庫的主從復制

方式一

數據庫中間件實現,如Mycat等數據庫中間件,對於項目本身來說,只有一個數據源,就是鏈接到Mycat,再由mycat根據規則去選擇從哪個庫獲取數據

方式二

代碼中配置多數據源,通過代碼控制使用哪個數據源,本文也是主要介紹這種方式

優點

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM