SpringBoot數(shù)據(jù)庫讀寫分離

1. 引言

讀寫分離要做的事情就是對于一條SQL該選擇哪個數(shù)據(jù)庫去執(zhí)行吧寺,至于誰來做選擇數(shù)據(jù)庫這件事兒刁俭,無非兩個橄仍,要么中間件幫我們做,要么程序自己做牍戚。因此侮繁,一般來講,讀寫分離有兩種實現(xiàn)方式如孝。第一種是依靠中間件(比如:MyCat)宪哩,也就是說應(yīng)用程序連接到中間件,中間件幫我們做SQL分離第晰;第二種是應(yīng)用程序自己去做分離锁孟。這里我們選擇程序自己來做,主要是利用Spring提供的路由數(shù)據(jù)源茁瘦,以及AOP

然而品抽,應(yīng)用程序?qū)用嫒プ鲎x寫分離最大的弱點(不足之處)在于無法動態(tài)增加數(shù)據(jù)庫節(jié)點,因為數(shù)據(jù)源配置都是寫在配置中的甜熔,新增數(shù)據(jù)庫意味著新加一個數(shù)據(jù)源圆恤,必然改配置,并重啟應(yīng)用腔稀。當(dāng)然盆昙,好處就是相對簡單。

2. AbstractRoutingDataSource

基于特定的查找key路由到特定的數(shù)據(jù)源烧颖。它內(nèi)部維護了一組目標數(shù)據(jù)源弱左,并且做了路由key與目標數(shù)據(jù)源之間的映射窄陡,提供基于key查找數(shù)據(jù)源的方法炕淮。

3. 實踐

3.1. maven依賴
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
3.2. 數(shù)據(jù)源配置

application.yml

spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://localhost:3306/test1
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave1:
      jdbc-url: jdbc:mysql://localhost:3306/test2
      username: pig   # 只讀賬戶
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave2:
      jdbc-url: jdbc:mysql://localhost:3306/test3
      username: pig   # 只讀賬戶
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver

多數(shù)據(jù)源配置

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @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個數(shù)據(jù)源跳夭,1個master涂圆,2兩個slave,1個路由數(shù)據(jù)源币叹。前3個數(shù)據(jù)源都是為了生成第4個數(shù)據(jù)源润歉,而且后續(xù)我們只用這最后一個路由數(shù)據(jù)源。

MyBatis配置

@Configuration
public class MyBatisConfig {

    @Resource(name = "myRoutingDataSource")
    private DataSource myRoutingDataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager() {
        return new DataSourceTransactionManager(myRoutingDataSource);
    }
}

由于Spring容器中現(xiàn)在有4個數(shù)據(jù)源颈抚,所以我們需要為事務(wù)管理器和MyBatis手動指定一個明確的數(shù)據(jù)源踩衩。

3.3. 設(shè)置路由key / 查找數(shù)據(jù)源

目標數(shù)據(jù)源就是那前3個這個我們是知道的,但是使用的時候是如果查找數(shù)據(jù)源的呢?

首先驱富,我們定義一個枚舉來代表這三個數(shù)據(jù)源

package com.cjs.example.enums;

public enum DBTypeEnum {
    MASTER, SLAVE1, SLAVE2;
}

接下來锚赤,通過ThreadLocal將數(shù)據(jù)源設(shè)置到每個線程上下文中

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

public class MyRoutingDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.get();
    }
}

設(shè)置路由key
默認情況下,所有的查詢都走從庫褐鸥,插入/修改/刪除走主庫线脚。我們通過方法名來區(qū)分操作類型(CRUD)

@Aspect
@Component
public class DataSourceAop {


    @Pointcut("!@annotation(com.cjs.example.annotation.Master) " +
            "&& (execution(* com.cjs.example.service..*.select*(..)) " +
            "|| execution(* com.cjs.example.service..*.get*(..)))")
    public void readPointcut() {


    }


    @Pointcut("@annotation(com.cjs.example.annotation.Master) " +
            "|| execution(* com.cjs.example.service..*.insert*(..)) " +
            "|| execution(* com.cjs.example.service..*.add*(..)) " +
            "|| execution(* com.cjs.example.service..*.update*(..)) " +
            "|| execution(* com.cjs.example.service..*.edit*(..)) " +
            "|| execution(* com.cjs.example.service..*.delete*(..)) " +
            "|| execution(* com.cjs.example.service..*.remove*(..))")
    public void writePointcut() {


    }


    @Before("readPointcut()")
    public void read() {
        DBContextHolder.slave();
    }


    @Before("writePointcut()")
    public void write() {
        DBContextHolder.master();
    }

    /**
     * 另一種寫法:if...else...  判斷哪些需要讀從數(shù)據(jù)庫,其余的走主數(shù)據(jù)庫
     */
//    @Before("execution(* com.cjs.example.service.impl.*.*(..))")
//    public void before(JoinPoint jp) {
//        String methodName = jp.getSignature().getName();
//
//        if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
//            DBContextHolder.slave();
//        }else {
//            DBContextHolder.master();
//        }
//    }
}

有一般情況就有特殊情況叫榕,特殊情況是某些情況下我們需要強制讀主庫浑侥,針對這種情況,我們定義一個主鍵晰绎,用該注解標注的就讀主庫

package com.cjs.example.annotation;
public @interface Master {
}

例如寓落,假設(shè)我們有一張表member

@Service
public class MemberServiceImpl implements MemberService {


    @Autowired
    private MemberMapper memberMapper;


    @Transactional
    @Override
    public int insert(Member member) {
        return memberMapper.insert(member);
    }


    @Master
    @Override
    public int save(Member member) {
        return memberMapper.insert(member);
    }


    @Override
    public List<Member> selectAll() {
        return memberMapper.selectByExample(new MemberExample());
    }

    @Master
    @Override
    public String getToken(String appId) {
        //  有些讀操作必須讀主數(shù)據(jù)庫
        //  比如,獲取微信access_token寒匙,因為高峰時期主從同步可能延遲
        //  這種情況下就必須強制從主數(shù)據(jù)讀
        return null;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末零如,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锄弱,更是在濱河造成了極大的恐慌考蕾,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会宪,死亡現(xiàn)場離奇詭異肖卧,居然都是意外死亡,警方通過查閱死者的電腦和手機掸鹅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門塞帐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人巍沙,你說我怎么就攤上這事葵姥。” “怎么了句携?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵榔幸,是天一觀的道長。 經(jīng)常有香客問我矮嫉,道長削咆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任蠢笋,我火速辦了婚禮拨齐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昨寞。我一直安慰自己瞻惋,他們只是感情好厦滤,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歼狼,像睡著了一般馁害。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蹂匹,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天碘菜,我揣著相機與錄音,去河邊找鬼限寞。 笑死忍啸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的履植。 我是一名探鬼主播计雌,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼玫霎!你這毒婦竟也來了凿滤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤庶近,失蹤者是張志新(化名)和其女友劉穎翁脆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鼻种,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡反番,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了叉钥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罢缸。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖投队,靈堂內(nèi)的尸體忽然破棺而出枫疆,到底是詐尸還是另有隱情,我是刑警寧澤敷鸦,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布息楔,位于F島的核電站,受9級特大地震影響轧膘,放射性物質(zhì)發(fā)生泄漏钞螟。R本人自食惡果不足惜兔甘,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一谎碍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洞焙,春花似錦蟆淀、人聲如沸拯啦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽褒链。三九已至,卻和暖如春疑苔,著一層夾襖步出監(jiān)牢的瞬間甫匹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工惦费, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兵迅,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓薪贫,卻偏偏與公主長得像恍箭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瞧省,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容