<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手教你Spring實現(xiàn)DB讀寫分離 | Spring系列52篇

          共 19409字,需瀏覽 39分鐘

           ·

          2021-05-11 01:33

          1、背景

          大多數(shù)系統(tǒng)都是讀多寫少,為了降低數(shù)據(jù)庫的壓力,可以對主庫創(chuàng)建多個從庫,從庫自動從主庫同步數(shù)據(jù),程序中將寫的操作發(fā)送到主庫,將讀的操作發(fā)送到從庫去執(zhí)行。

          今天的主要目標:通過 spring 實現(xiàn)讀寫分離。

          讀寫分離需實現(xiàn)下面 2 個功能:

          1、讀的方法,由調(diào)用者來控制具體是讀從庫還是主庫

          2、有事務(wù)的方法,內(nèi)部的所有讀寫操作都走主庫

          2、思考 3 個問題

          1、讀的方法,由調(diào)用者來控制具體是讀從庫還是主庫,如何實現(xiàn)?

          可以給所有讀的方法添加一個參數(shù),來控制讀從庫還是主庫。

          2、數(shù)據(jù)源如何路由?

          spring-jdbc 包中提供了一個抽象類:AbstractRoutingDataSource,實現(xiàn)了 javax.sql.DataSource 接口,我們用這個類來作為數(shù)據(jù)源類,重點是這個類可以用來做數(shù)據(jù)源的路由,可以在其內(nèi)部配置多個真實的數(shù)據(jù)源,最終用哪個數(shù)據(jù)源,由開發(fā)者來決定。

          AbstractRoutingDataSource 中有個 map,用來存儲多個目標數(shù)據(jù)源

          private Map<Object, DataSource> resolvedDataSources;

          比如主從庫可以這么存儲

          resolvedDataSources.put("master",主庫數(shù)據(jù)源);
          resolvedDataSources.put("salave",從庫數(shù)據(jù)源);

          AbstractRoutingDataSource 中還有抽象方法determineCurrentLookupKey,將這個方法的返回值作為 key 到上面的 resolvedDataSources 中查找對應(yīng)的數(shù)據(jù)源,作為當前操作 db 的數(shù)據(jù)源

          protected abstract Object determineCurrentLookupKey();

          3、讀寫分離在哪控制?

          讀寫分離屬于一個通用的功能,可以通過 spring 的 aop 來實現(xiàn),添加一個攔截器,攔截目標方法的之前,在目標方法執(zhí)行之前,獲取一下當前需要走哪個庫,將這個標志存儲在 ThreadLocal 中,將這個標志作為 AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,攔截器中在目標方法執(zhí)行完畢之后,將這個標志從 ThreadLocal 中清除。

          3、代碼實現(xiàn)

          3.1、工程結(jié)構(gòu)圖

          3.2、DsType

          表示數(shù)據(jù)源類型,有 2 個值,用來區(qū)分是主庫還是從庫。

          package com.javacode2018.readwritesplit.base;

          public enum DsType {
              MASTER, SLAVE;
          }

          3.3、DsTypeHolder

          內(nèi)部有個 ThreadLocal,用來記錄當前走主庫還是從庫,將這個標志放在 dsTypeThreadLocal 中

          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 接口

          這個接口起到標志的作用,當某個類需要啟用讀寫分離的時候,需要實現(xiàn)這個接口,實現(xiàn)這個接口的類都會被讀寫分離攔截器攔截。

          package com.javacode2018.readwritesplit.base;

          //需要實現(xiàn)讀寫分離的service需要實現(xiàn)該接口
          public interface IService {
          }

          3.5、ReadWriteDataSource

          讀寫分離數(shù)據(jù)源,繼承 ReadWriteDataSource,注意其內(nèi)部的 determineCurrentLookupKey 方法,從上面的 ThreadLocal 中獲取當前需要走主庫還是從庫的標志。

          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

          讀寫分離攔截器,需放在事務(wù)攔截器前面執(zhí)行,通過@1 代碼我們將此攔截器的順序設(shè)置為 Integer.MAX_VALUE - 2,稍后我們將事務(wù)攔截器的順序設(shè)置為 Integer.MAX_VALUE - 1,事務(wù)攔截器的執(zhí)行順序是從小到達的,所以,ReadWriteInterceptor 會在事務(wù)攔截器 org.springframework.transaction.interceptor.TransactionInterceptor 之前執(zhí)行。

          由于業(yè)務(wù)方法中存在相互調(diào)用的情況,比如 service1.m1 中調(diào)用 service2.m2,而 service2.m2 中調(diào)用了 service2.m3,我們只需要在 m1 方法執(zhí)行之前,獲取具體要用哪個數(shù)據(jù)源就可以了,所以下面代碼中會在第一次進入這個攔截器的時候,記錄一下走主庫還是從庫。

          下面方法中會獲取當前目標方法的最后一個參數(shù),最后一個參數(shù)可以是 DsType 類型的,開發(fā)者可以通過這個參數(shù)來控制具體走主庫還是從庫。

          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() {
              }

              //獲取當前目標方法的最后一個參數(shù)
              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 {
                  //判斷是否是第一次進來,用于處理事務(wù)嵌套
                  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

          spring 配置類,作用

          1、@3:用來將 com.javacode2018.readwritesplit.base 包中的一些類注冊到 spring 容器中,比如上面的攔截器 ReadWriteInterceptor

          2、@1:開啟 spring aop 的功能

          3、@2:開啟 spring 自動管理事務(wù)的功能,@EnableTransactionManagement 的 order 用來指定事務(wù)攔截器 org.springframework.transaction.interceptor.TransactionInterceptor 順序,在這里我們將 order 設(shè)置為 Integer.MAX_VALUE - 1,而上面 ReadWriteInterceptor 的 order 是 Integer.MAX_VALUE - 2,所以 ReadWriteInterceptor 會在事務(wù)攔截器之前執(zhí)行。

          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

          這個注解用倆開啟讀寫分離的功能,@1 通過@Import 將 ReadWriteConfiguration 導(dǎo)入到 spring 容器了,這樣就會自動啟用讀寫分離的功能。業(yè)務(wù)中需要使用讀寫分離,只需要在 spring 配置類中加上@EnableReadWrite 注解就可以了。

          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、案例

          讀寫分離的關(guān)鍵代碼寫完了,下面我們來上案例驗證一下效果。

          4.1、執(zhí)行 sql 腳本

          下面準備 2 個數(shù)據(jù)庫:javacode2018_master(主庫)、javacode2018_slave(從庫)

          2 個庫中都創(chuàng)建一個 t_user 表,分別插入了一條數(shù)據(jù),稍后用這個數(shù)據(jù)來驗證走的是主庫還是從庫。

          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、spring 配置類

          @1:啟用讀寫分離

          masterDs()方法:定義主庫數(shù)據(jù)源

          slaveDs()方法:定義從庫數(shù)據(jù)源

          dataSource():定義讀寫分離路由數(shù)據(jù)源

          后面還有 2 個方法用來定義 JdbcTemplate 和事務(wù)管理器,方法中都通過@Qualifier("dataSource")限定了注入的 bean 名稱為 dataSource:即注入了上面 dataSource()返回的讀寫分離路由數(shù)據(jù)源。

          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 {
              //主庫數(shù)據(jù)源
              @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;
              }

              //從庫數(shù)據(jù)源
              @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;
              }

              //讀寫分離路由數(shù)據(jù)源
              @Bean
              public ReadWriteDataSource dataSource() {
                  ReadWriteDataSource dataSource = new ReadWriteDataSource();
                  //設(shè)置主庫為默認的庫,當路由的時候沒有在datasource那個map中找到對應(yīng)的數(shù)據(jù)源的時候,會使用這個默認的數(shù)據(jù)源
                  dataSource.setDefaultTargetDataSource(this.masterDs());
                  //設(shè)置多個目標庫
                  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為上面定義的注入讀寫分離的數(shù)據(jù)源
              @Bean
              public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
                  return new JdbcTemplate(dataSource);
              }

              //定義事務(wù)管理器,dataSource為上面定義的注入讀寫分離的數(shù)據(jù)源
              @Bean
              public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
                  return new DataSourceTransactionManager(dataSource);
              }
          }

          4.3、UserService

          這個類就相當于我們平時寫的 service,我是為了方法,直接在里面使用了 JdbcTemplate 來操作數(shù)據(jù)庫,真實的項目操作 db 會放在 dao 里面。

          getUserNameById 方法:通過 id 查詢 name。

          insert 方法:插入數(shù)據(jù),這個內(nèi)部的所有操作都會走主庫,為了驗證是不是查詢也會走主庫,插入數(shù)據(jù)之后,我們會調(diào)用 this.userService.getUserNameById(id, DsType.SLAVE)方法去執(zhí)行查詢操作,第二個參數(shù)故意使用 SLAVE,如果查詢有結(jié)果,說明走的是主庫,否則走的是從庫,這里為什么需要通過 this.userService 來調(diào)用 getUserNameById?

          this.userService 最終是個代理對象,通過代理對象訪問其內(nèi)部的方法,才會被讀寫分離的攔截器攔截。

          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.classid);
                  return (list != null && list.size() > 0) ? list.get(0) : null;
              }

              //這個insert方法會走主庫,內(nèi)部的所有操作都會走主庫
              @Transactional
              public void insert(long id, String name) {
                  System.out.println(String.format("插入數(shù)據(jù){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("查詢結(jié)果:" + userName);
              }

          }

          4.4、測試用例

          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, "張三");
              }
          }

          test1 方法執(zhí)行 2 次查詢,分別查詢主庫和從庫,輸出:

          master庫
          slave庫

          是不是很爽,由開發(fā)者自己控制具體走主庫還是從庫。

          test2 執(zhí)行結(jié)果如下,可以看出查詢到了剛剛插入的數(shù)據(jù),說明 insert 中所有操作都走的是主庫。

          1604905117467
          插入數(shù)據(jù){id:1604905117467, name:張三}
          查詢結(jié)果:張三

          5、案例源碼

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

          本文案例對應(yīng)源碼:
              spring-series\lesson-004-readwritesplit

          大家 star 一下,所有系列代碼都會在這個里面。

          推薦閱讀:

          數(shù)據(jù)庫系統(tǒng)設(shè)計概述

          Kafka原理篇:圖解kakfa架構(gòu)原理

          架構(gòu)設(shè)計方法論

          從面試角度一文學(xué)完 Kafka

          數(shù)據(jù)庫跟緩存的雙寫一致性

          全網(wǎng)最詳盡的負載均衡原理圖解


          關(guān)互聯(lián)網(wǎng)全棧架構(gòu),。

              


          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩电影一级片 | 性生活片日逼片 | 欧美操比在线视频 | 国内精品视频播放 | 午夜成人中文 |