Spring------mysql讀寫分離

1. 爲何要進行讀寫分離

大量的JavaWeb應用作的是IO密集型任務, 數據庫的壓力較大, 須要分流java

大量的應用場景, 是讀多寫少, 數據庫讀取的壓力更大mysql

一個很天然的思路是使用一主多從的數據庫集羣: 一個是主庫,負責寫入數據;其它都是從庫,負責讀取數據. 主從庫數據同步.spring

 

mysql原生支持主從複製sql

mysql主(稱master)從(稱slave)複製的原理:
一、master將數據改變記錄到二進制日誌(bin log)中,  這些記錄叫binary log events
二、slave將master的binary log events拷貝到它的中繼日誌(relay log)
三、slave重作中繼日誌中的事件, 將改變反映它本身的數據(數據重演)數據庫

 

解決讀寫分離的方案大體有兩種:express

1)在應用層給讀/寫分別指定數據庫安全

好處是數據源切換方便, 不用引入其餘組件. 可是不能動態添加數據源.app

2)使用中間件解決ide

好處是源程序不須要作任何改動, 還能夠動態添加數據源. 但中間件會帶來必定的性能損失.性能

目前有mysql-proxy, mycat, altas等

 

2. MySQL主從配置

主庫配置

修改my.ini:
#開啓主從複製,主庫的配置
log-bin = mysql3306-bin
#指定主庫serverid
server-id=101
#指定同步的數據庫,若是不指定則同步所有數據庫
binlog-do-db=mydb

執行如下SQL:
SHOW MASTER STATUS;

記錄下Position值,須要在從庫中設置同步起始值。

 

#受權從庫用戶slave01使用123456密碼登陸本庫
grant replication slave on *.* to 'slave01'@'127.0.0.1' identified by '123456';
flush privileges;

 

從庫配置

修改my.ini:

#指定serverid
server-id=102

執行如下SQL:

CHANGE MASTER TO
 master_host='127.0.0.1',
 master_user='slave01',
 master_password='123456',
 master_port=3306,
 master_log_file='mysql3306-bin.000006',   #設置主庫時記下的Position
 master_log_pos=1120;

#啓動slave同步
START SLAVE;

#查看同步狀態    Slave_IO_Running和Slave_SQL_Running都爲Yes說明同步成功
SHOW SLAVE STATUS;

 

3. Spring動態數據源+AOP實現讀寫分離

這裏採用的是應用層的讀寫分離方案

使用AOP, 在執行Service方法前判斷,是使用寫庫仍是讀庫
能夠根據方法名做爲依據判斷,好比說以query、find、get等開頭的就走讀庫,其餘的走寫庫

切面類:

複製代碼
/**
 * 若是在spring配置了事務的策略,則標記了ReadOnly的方法用從庫Slave, 其它使用主庫Master。
 * 若是沒有配置事務策略, 則採用方法名匹配, 以query、find、get開頭的方法用Slave,其它用Master。
 */
public class DataSourceAspect {

    private List<String> slaveMethodPattern = new ArrayList<String>();  //保存有readonly屬性的帶通配符方法名
    private static final String[] defaultSlaveMethodStartWith = new String[]{"query", "find", "get" };
    private String[] slaveMethodStartWith;  //保存有slaveMethodStartWith屬性的方法名頭部
    
    //注入
    public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception {
        if (txAdvice == null) {
            // 沒有配置事務策略
            return;
        }
        //從txAdvice獲取策略配置信息
        TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource();
        if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) {
            return;
        }
        //使用反射技術獲取到NameMatchTransactionAttributeSource對象中的nameMap屬性值
        NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource;
        Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
        nameMapField.setAccessible(true); //設置該字段可訪問
        //獲取nameMap的值
        Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField.get(matchTransactionAttributeSource);
        //遍歷nameMap
        for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) {
            if (!entry.getValue().isReadOnly()) {   // 定義了ReadOnly的策略才加入到slaveMethodPattern
                continue;
            }
            slaveMethodPattern.add(entry.getKey());
        }
    }

    // 切面 before方法
    public void before(JoinPoint point) {
        // 獲取到當前執行的方法名
        String methodName = point.getSignature().getName();

        boolean isSlave = false;

        if (slaveMethodPattern.isEmpty()) {
            // 沒有配置read-only屬性,採用方法名匹配方式
            isSlave = isSlaveByMethodName(methodName);
        } else {
            // 配置read-only屬性, 採用通配符匹配
            for (String mappedName : slaveMethodPattern) {
                if (isSlaveByConfigWildcard(methodName, mappedName)) {
                    isSlave = true;
                    break;
                }
            }
        }
        if (isSlave) {
            // 標記爲讀庫
            DynamicDataSource.markMaster(true);
        } else {
            // 標記爲寫庫
            DynamicDataSource.markMaster(false);
        }
    }

    // 匹配以指定名稱開頭的方法名, 配置了slaveMethodStartWith屬性, 或使用默認
    private Boolean isSlaveByMethodName(String methodName) {
        return StringUtils.startsWithAny(methodName, getSlaveMethodStartWith());
    }

    // 匹配帶通配符"xxx*", "*xxx" 和 "*xxx*"的方法名, 源自配置了readonly屬性的方法名
    protected boolean isSlaveByConfigWildcard(String methodName, String mappedName) {
        return PatternMatchUtils.simpleMatch(mappedName, methodName);
    }

    // 注入
    public void setSlaveMethodStartWith(String[] slaveMethodStartWith) {
        this.slaveMethodStartWith = slaveMethodStartWith;
    }

    public String[] getSlaveMethodStartWith() {
        if(this.slaveMethodStartWith == null){
            // 沒有配置slaveMethodStartWith屬性,使用默認
            return defaultSlaveMethodStartWith;
        }
        return slaveMethodStartWith;
    }
}
複製代碼

Spring的RoutingDataSource

複製代碼
/**
 * 使用Spring的動態數據源,須要實現AbstractRoutingDataSource
 * 經過determineCurrentLookupKey方法拿到識別key來判斷選擇讀/寫數據源
 * token顯然是多例的, 因此引入ThreadLocal保存
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    // 讀庫總數
    private Integer slaveCount;  
    // 讀庫輪詢計數, 初始爲-1, 本類爲單例, AtomicInteger線程安全
    private AtomicInteger counter = new AtomicInteger(-1);
    // 存儲讀庫的識別key sl1ve01, slave02...  寫庫識別key爲master
    private List<Object> slaveDataSources = new ArrayList<Object>();
    
    //當前線程的寫庫/讀庫token
    private static final ThreadLocal<Boolean> tokenHolder = new ThreadLocal<>();
    
    public static void markMaster(boolean isMaster){
        tokenHolder.set(isMaster);
    }
    
    @Override
    protected Object determineCurrentLookupKey() {
        if (tokenHolder.get()) {
            return "master";   // 寫庫
        }
        // 輪詢讀庫, 獲得的下標爲:0、一、2...
        Integer index = counter.incrementAndGet() % slaveCount;
        if (counter.get() > 99999) { // 以避免超出Integer範圍
            counter.set(-1); 
        }
        return slaveDataSources.get(index);
    }

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        // 父類的resolvedDataSources屬性是private, 須要使用反射獲取
        Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources");
        field.setAccessible(true); // 設置可訪問
        try {
            Map<Object, DataSource> resolvedDataSources = (Map<Object, DataSource>) field.get(this);
            // 讀庫數等於dataSource總數減寫庫數
            this.slaveCount = resolvedDataSources.size() - 1;
            for (Map.Entry<Object, DataSource> entry : resolvedDataSources.entrySet()) {
                if ("master".equals(entry.getKey())) {
                    continue;
                }
                slaveDataSources.add(entry.getKey());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

spring配置文件

複製代碼
<!-- 定義事務策略 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--全部以query開頭的方法都是隻讀的 -->
            <tx:method name="query*" read-only="true" />  <!-- readonly屬性 -->
            <!--其餘方法使用默認事務策略 -->
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>
    
    <!-- 定義AOP切面處理器 -->
    <bean class="com.zx.DataSourceAspect" id="dataSourceAspect">
        <!-- 注入事務策略 -->
        <property name="txAdvice" ref="txAdvice"/>
        <!-- 指定slave方法的前綴(非必須) -->
        <property name="slaveMethodStartWith" value="query,find,get"/>
    </bean>

    <aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* com.zx.service.*.*(..))" />
        <!-- 將切面應用到自定義的切面處理器上,-9999保證該切面優先級最高執行 -->
        <aop:aspect ref="dataSourceAspect" order="-9999">
            <aop:before method="before" pointcut-ref="myPointcut" />
        </aop:aspect>
    </aop:config>
    
    <!-- 定義數據源,繼承了spring的動態數據源 -->
    <bean id="dataSource" class="com.zx.DynamicDataSource">
        <!-- 設置多個數據源 -->
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <!-- 這些設置的key和determineCurrentLookupKey方法拿到的key相比對, 根據匹配選擇數據源 -->
                <entry key="master" value-ref="masterDataSource"/>   <!-- value-ref指向數據源 -->
                <entry key="slave01" value-ref="slave01DataSource"/>
                <entry key="slave02" value-ref="slave02DataSource"/>
                <entry key="slave03" value-ref="slave03DataSource"/>
            </map>
        </property>
        <!-- 設置默認的數據源,這裏默認走寫庫 -->
        <property name="defaultTargetDataSource" ref="masterDataSource"/>
    </bean>
複製代碼
相關文章
相關標籤/搜索