Spring's Road to God Kapitel 52: Spring realisiert die Lese-/Schreibtrennung der Datenbank

Spring's Road to God Kapitel 52: Spring realisiert die Lese-/Schreibtrennung der Datenbank

1. Hintergrund

Die meisten Systeme lesen mehr und schreiben weniger. Um den Druck auf die Datenbank zu verringern, können mehrere Slave-Bibliotheken für die Hauptbibliothek erstellt werden. Die Slave-Bibliothek synchronisiert automatisch Daten aus der Hauptbibliothek. Das Programm sendet den Schreibvorgang an die Hauptbibliothek , und der Lesevorgang Senden Sie es zur Ausführung an die Slave-Bibliothek.

Das heutige Hauptziel: Bis zum Frühjahr eine Lese-Schreib-Trennung erreichen .

Die Lese-Schreib-Trennung muss die folgenden zwei Funktionen implementieren:

1. Die Lesemethode wird vom Aufrufer gesteuert, ob aus der Bibliothek oder der Hauptbibliothek gelesen wird

2. Es gibt eine Transaktionsmethode und alle internen Lese- und Schreibvorgänge gehen an die Hauptbibliothek

2. Denken Sie über 3 Fragen nach

1. Die Lesemethode wird vom Aufrufer gesteuert, ob aus der Slave-Bibliothek oder der Hauptbibliothek gelesen wird. Wie kann man das realisieren?

Sie können allen Lesemethoden einen Parameter hinzufügen, um zu steuern, ob aus der Bibliothek oder der Masterbibliothek gelesen wird.

2. Wie leitet man die Datenquelle weiter?

Das Paket spring-jdbc stellt eine abstrakte Klasse bereit: AbstractRoutingDataSource, die die Schnittstelle javax.sql.DataSource implementiert. Wir verwenden diese Klasse als Datenquellenklasse. Der Punkt ist, dass diese Klasse für das Datenquellen-Routing verwendet und intern konfiguriert werden kann . Es gibt mehrere reale Datenquellen und es liegt am Entwickler, zu entscheiden, welche Datenquelle er letztendlich verwendet.

In AbstractRoutingDataSource gibt es eine Karte zum Speichern mehrerer Zieldatenquellen

private Map<Object, DataSource> resolvedDataSources;

Beispielsweise kann die Master-Slave-Bibliothek so gespeichert werden

resolvedDataSources.put("master",主库数据源);
resolvedDataSources.put("salave",从库数据源);

Es gibt auch eine abstrakte Methode in AbstractRoutingDataSource determineCurrentLookupKey. Verwenden Sie den Rückgabewert dieser Methode als Schlüssel, um die entsprechende Datenquelle in den oben aufgelöstenDataSources als Datenquelle für die Datenbank des aktuellen Vorgangs zu finden

protected abstract Object determineCurrentLookupKey();

3. Wo wird die Trennung von Lesen und Schreiben kontrolliert?

Die Lese-Schreib-Trennung ist eine allgemeine Funktion, die durch spring aop realisiert werden kann. Fügen Sie einen Interceptor hinzu, bevor Sie die Zielmethode abfangen, bevor die Zielmethode ausgeführt wird, ermitteln Sie, welche Bibliothek Sie aktuell benötigen, und speichern Sie dieses Flag in ThreadLocal. Verwenden Sie dieses Flag als Rückgabewert der Methode AbstractRoutingDataSource.determineCurrentLookupKey(). Nachdem die Zielmethode im Interceptor ausgeführt wurde, löschen Sie dieses Flag aus ThreadLocal.

3. Code-Implementierung

3.1. Projektstrukturdiagramm

[Externer Link-Bildtransfer fehlgeschlagen, die Quellseite verfügt möglicherweise über einen Anti-Diebstahl-Link-Mechanismus, es wird empfohlen, das Bild zu speichern und direkt hochzuladen (img-xzUJnT7h-1684759744195)(%E6%96%B0%E5%BB%BA %E6%96%87%E6%9C%AC%E6%96%87%E6%A1%A3/1369022-20211107221309265-2099055565.png)]

3.2、DsType

Gibt den Typ der Datenquelle an und verfügt über zwei Werte, anhand derer unterschieden wird, ob es sich um die Master-Bibliothek oder die Slave-Bibliothek handelt.

package com.javacode2018.readwritesplit.base;

public enum DsType {
    
    
    MASTER, SLAVE;
}

3.3、DsTypeHolder

Im Inneren befindet sich ein ThreadLocal, mit dem aufgezeichnet wird, ob derzeit die Hauptbibliothek oder die Slave-Bibliothek verwendet wird. Dieses Flag wird in dsTypeThreadLocal platziert

package com.javacode2018.readwritesplit.base;

public class DsTypeHolder {
    
    
    private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>();

    public static void master() {
    
    
        dsTypeThreadLocal.set(DsType.MASTER);
    }

    public static void slave() {
    
    
        dsTypeThreadLocal.set(DsType.SLAVE);
    }

    public static DsType getDsType() {
    
    
        return dsTypeThreadLocal.get();
    }

    public static void clearDsType() {
    
    
        dsTypeThreadLocal.remove();
    }
}

3.4. IService-Schnittstelle

Diese Schnittstelle fungiert als Flag. Wenn eine Klasse die Lese-/Schreibtrennung aktivieren muss, muss sie diese Schnittstelle implementieren. Die Klasse, die diese Schnittstelle implementiert, wird vom Lese-/Schreibtrennungs-Interceptor abgefangen.

package com.javacode2018.readwritesplit.base;

//需要实现读写分离的service需要实现该接口
public interface IService {
    
    
}

3.5、ReadWriteDataSource

Lesen und schreiben Sie separate Datenquellen, erben Sie ReadWriteDataSource, achten Sie auf die interne Methode „determineCurrentLookupKey“ und erhalten Sie vom obigen ThreadLocal das Flag, ob zur Hauptbibliothek oder zur Slave-Bibliothek gewechselt werden soll.

package com.javacode2018.readwritesplit.base;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

public class ReadWriteDataSource extends AbstractRoutingDataSource {
    
    
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
    
    
        return DsTypeHolder.getDsType();
    }
}

3.6、ReadWriteInterceptor

Der Lese-/Schreib-Trennungs-Interceptor muss vor dem Transaktions-Interceptor ausgeführt werden. Durch den @1-Code legen wir die Reihenfolge dieses Interceptors auf Integer.MAX_VALUE - 2 fest, und später setzen wir die Reihenfolge des Transaktions-Interceptors auf Integer.MAX_VALUE - 1. Die Ausführungsreihenfolge des Transaktionsinterceptors ist von klein nach klein, sodass ReadWriteInterceptor vor dem Transaktionsinterceptor org.springframework.transaction.interceptor.TransactionInterceptor ausgeführt wird.

Da es in den Geschäftsmethoden gegenseitige Aufrufe gibt, beispielsweise wird service2.m2 in service1.m1 und service2.m3 in service2.m2 aufgerufen, müssen wir vor der m1-Methode nur die spezifische Datenquelle abrufen, die verwendet werden soll wird ausgeführt. Daher zeichnet der folgende Code auf, ob beim ersten Betreten des Interceptors zur Hauptbibliothek oder zur Slave-Bibliothek gewechselt werden soll.

Die folgende Methode ruft den letzten Parameter der aktuellen Zielmethode ab. Der letzte Parameter kann vom Typ DsType sein. Entwickler können diesen Parameter verwenden, um zu steuern, ob die Hauptbibliothek oder die Slave-Bibliothek verwendet werden soll.

package com.javacode2018.readwritesplit.base;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Objects;

@Aspect
@Order(Integer.MAX_VALUE - 2) //@1
@Component
public class ReadWriteInterceptor {
    
    

    @Pointcut("target(IService)")
    public void pointcut() {
    
    
    }

    //获取当前目标方法的最后一个参数
    private Object getLastArgs(final ProceedingJoinPoint pjp) {
    
    
        Object[] args = pjp.getArgs();
        if (Objects.nonNull(args) && args.length > 0) {
    
    
            return args[args.length - 1];
        } else {
    
    
            return null;
        }
    }

    @Around("pointcut()")
    public Object around(final ProceedingJoinPoint pjp) throws Throwable {
    
    
        //判断是否是第一次进来,用于处理事务嵌套
        boolean isFirst = false;
        try {
    
    
            if (DsTypeHolder.getDsType() == null) {
    
    
                isFirst = true;
            }
            if (isFirst) {
    
    
                Object lastArgs = getLastArgs(pjp);
                if (DsType.SLAVE.equals(lastArgs)) {
    
    
                    DsTypeHolder.slave();
                } else {
    
    
                    DsTypeHolder.master();
                }
            }
            return pjp.proceed();
        } finally {
    
    
            //退出的时候,清理
            if (isFirst) {
    
    
                DsTypeHolder.clearDsType();
            }
        }
    }
}

3.7、ReadWriteConfiguration

Federkonfigurationsklasse, Funktion

1. @3: Wird verwendet, um einige Klassen im Paket com.javacode2018.readwritesplit.base im Spring-Container zu registrieren, z. B. den obigen Interceptor ReadWriteInterceptor

2. @1: Aktivieren Sie die Funktion von Spring Aop

3. @2: Aktivieren Sie die Funktion von Spring, um Transaktionen automatisch zu verwalten. Die Reihenfolge von @EnableTransactionManagement wird verwendet, um die Reihenfolge des Transaktionsinterceptors org.springframework.transaction.interceptor.TransactionInterceptor anzugeben. Hier setzen wir die Reihenfolge auf Integer.MAX_VALUE - 1 und der obige ReadWriteInterceptor Die Reihenfolge ist Integer.MAX_VALUE - 2, sodass ReadWriteInterceptor vor dem Transaktionsinterceptor ausgeführt wird.

package com.javacode2018.readwritesplit.base;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableAspectJAutoProxy //@1
@EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) //@2
@ComponentScan(basePackageClasses = IService.class) //@3
public class ReadWriteConfiguration {
    
    
}

3.8、@EnableReadWrite

Diese Annotation verwendet zwei, um die Funktion der Lese-Schreib-Trennung zu aktivieren. @1 importiert ReadWriteConfiguration über @Import in den Spring-Container, wodurch die Funktion der Lese-Schreib-Trennung automatisch aktiviert wird. Wenn Sie in Ihrem Unternehmen die Lese-/Schreibtrennung verwenden müssen, müssen Sie nur die Annotation @EnableReadWrite zur Spring-Konfigurationsklasse hinzufügen.

package com.javacode2018.readwritesplit.base;

import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ReadWriteConfiguration.class) //@1
public @interface EnableReadWrite {
    
    
}

4. Fall

Der Schlüsselcode für die Lese- und Schreibtrennung ist fertig. Verwenden wir einen Fall, um den Effekt zu überprüfen.

4.1, SQL-Skript ausführen

Nachfolgend werden zwei Datenbanken vorbereitet: javacode2018_master (Hauptbibliothek), javacode2018_slave (Slave-Bibliothek)

In beiden Datenbanken wird eine t_user-Tabelle erstellt und jeweils ein Datenelement eingefügt. Später werden diese Daten verwendet, um zu überprüfen, ob es sich um die Master-Datenbank oder die Slave-Datenbank handelt.

DROP DATABASE IF EXISTS javacode2018_master;
CREATE DATABASE IF NOT EXISTS javacode2018_master;

USE javacode2018_master;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY       AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT ''
  COMMENT '姓名'
);

INSERT INTO t_user (name) VALUE ('master库');

DROP DATABASE IF EXISTS javacode2018_slave;
CREATE DATABASE IF NOT EXISTS javacode2018_slave;

USE javacode2018_slave;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY       AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT ''
  COMMENT '姓名'
);
INSERT INTO t_user (name) VALUE ('slave库');

4.2, Federkonfigurationsklasse

@1: Lese-Schreib-Trennung aktivieren

masterDs()-Methode: Definieren Sie die Hauptdatenquelle der Datenbank

SlaveDs()-Methode: Definieren Sie die Slave-Datenquelle

dataSource(): Definieren Sie die Routing-Datenquelle für die Lese-/Schreibtrennung

Es gibt zwei weitere Methoden zum Definieren der JdbcTemplate und des Transaktionsmanagers. In der Methode wird @Qualifier("dataSource") verwendet, um den Namen der injizierten Bean auf dataSource zu beschränken: d. h. die zurückgegebene Lese-/Schreib-Trennungs-Routing-Datenquelle durch die oben genannte dataSource() wird injiziert.

package com.javacode2018.readwritesplit.demo1;

import com.javacode2018.readwritesplit.base.DsType;
import com.javacode2018.readwritesplit.base.EnableReadWrite;
import com.javacode2018.readwritesplit.base.ReadWriteDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@EnableReadWrite //@1
@Configuration
@ComponentScan
public class MainConfig {
    
    
    //主库数据源
    @Bean
    public DataSource masterDs() {
    
    
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    //从库数据源
    @Bean
    public DataSource slaveDs() {
    
    
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    //读写分离路由数据源
    @Bean
    public ReadWriteDataSource dataSource() {
    
    
        ReadWriteDataSource dataSource = new ReadWriteDataSource();
        //设置主库为默认的库,当路由的时候没有在datasource那个map中找到对应的数据源的时候,会使用这个默认的数据源
        dataSource.setDefaultTargetDataSource(this.masterDs());
        //设置多个目标库
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DsType.MASTER, this.masterDs());
        targetDataSources.put(DsType.SLAVE, this.slaveDs());
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }

    //JdbcTemplate,dataSource为上面定义的注入读写分离的数据源
    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
    
    
        return new JdbcTemplate(dataSource);
    }

    //定义事务管理器,dataSource为上面定义的注入读写分离的数据源
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
    
    
        return new DataSourceTransactionManager(dataSource);
    }
}

4.3、Benutzerservice

Diese Klasse entspricht dem Dienst, den wir normalerweise schreiben. Ich verwende JdbcTemplate direkt, um die Datenbank für die Methode zu betreiben, und die Datenbank für den eigentlichen Projektbetrieb wird in Dao platziert.

getUserNameById-Methode: Namen nach ID abfragen.

Einfügemethode: Daten einfügen. Alle internen Vorgänge werden an die Hauptdatenbank gesendet. Um zu überprüfen, ob die Abfrage auch an die Hauptdatenbank gesendet wird. Nach dem Einfügen der Daten rufen wir this.userService.getUserNameById(id, DsType auf. SLAVE)-Methode zum Ausführen der Abfrageoperation, der zweite Parameter verwendet absichtlich SLAVE. Wenn die Abfrage Ergebnisse hat, bedeutet dies, dass die Master-Bibliothek verwendet wird, andernfalls wird die Slave-Bibliothek verwendet. Warum müssen Sie hier getUserNameById über this.userService aufrufen? ?

this.userService ist letztendlich ein Proxy-Objekt. Der Zugriff auf seine internen Methoden über das Proxy-Objekt wird vom Interceptor abgefangen, der Lesen und Schreiben trennt.

package com.javacode2018.readwritesplit.demo1;

import com.javacode2018.readwritesplit.base.DsType;
import com.javacode2018.readwritesplit.base.IService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Component
public class UserService implements IService {
    
    

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private UserService userService;

    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public String getUserNameById(long id, DsType dsType) {
    
    
        String sql = "select name from t_user where id=?";
        List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
        return (list != null && list.size() > 0) ? list.get(0) : null;
    }

    //这个insert方法会走主库,内部的所有操作都会走主库
    @Transactional
    public void insert(long id, String name) {
    
    
        System.out.println(String.format("插入数据{id:%s, name:%s}", id, name));
        this.jdbcTemplate.update("insert into t_user (id,name) values (?,?)", id, name);
        String userName = this.userService.getUserNameById(id, DsType.SLAVE);
        System.out.println("查询结果:" + userName);
    }

}

4.4. Testfälle

package com.javacode2018.readwritesplit.demo1;

import com.javacode2018.readwritesplit.base.DsType;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo1Test {
    
    

    UserService userService;

    @Before
    public void before() {
    
    
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MainConfig.class);
        context.refresh();
        this.userService = context.getBean(UserService.class);
    }

    @Test
    public void test1() {
    
    
        System.out.println(this.userService.getUserNameById(1, DsType.MASTER));
        System.out.println(this.userService.getUserNameById(1, DsType.SLAVE));
    }

    @Test
    public void test2() {
    
    
        long id = System.currentTimeMillis();
        System.out.println(id);
        this.userService.insert(id, "张三");
    }
}

Die Methode test1 führt zwei Abfragen aus, fragt jeweils die Hauptbibliothek und die Slave-Bibliothek ab und gibt Folgendes aus:

master库
slave库

Ist es nicht cool, dass die Entwickler selbst steuern, ob sie die Hauptbibliothek oder die Slave-Bibliothek verwenden?

Die Ausführungsergebnisse von test2 lauten wie folgt: Es ist ersichtlich, dass die gerade eingefügten Daten abgefragt wurden, was darauf hinweist, dass alle Vorgänge beim Einfügen über die Hauptdatenbank erfolgen.

1604905117467
插入数据{id:1604905117467, name:张三}
查询结果:张三

5. Fallquellcode

git地址:
https://gitee.com/javacode2018/spring-series

本文案例对应源码:
    spring-series\lesson-004-readwritesplit

Jeder markiert es, alle Codeserien werden darin enthalten sein.

Quelle: https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648938118&idx=2&sn=baef96540a8936e49db0bfe62f909f24&scene=21#wechat_redirect

ster-Bibliothek,
Slave-Bibliothek


是不是很爽,由开发者自己控制具体走主库还是从库。

test2 执行结果如下,可以看出查询到了刚刚插入的数据,说明 insert 中所有操作都走的是主库。

1604905117467
Daten einfügen {id: 1604905117467, Name: Zhang San}
Abfrageergebnis: Zhang San


## 5、案例源码

Git-Adresse:
https://gitee.com/javacode2018/spring-series

Der Quellcode, der dem Fall in diesem Artikel entspricht:
spring-series\lesson-004-readwritesplit


大家 star 一下,所有系列代码都会在这个里面。

来源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648938118&idx=2&sn=baef96540a8936e49db0bfe62f909f24&scene=21#wechat_redirect

 

Acho que você gosta

Origin blog.csdn.net/china_coding/article/details/130814999
Recomendado
Clasificación