springboot數據庫主從方案

本篇分享數據庫主從方案,案例採用springboot+mysql+mybatis演示;要想在代碼中做主從選擇,通常需要明白什麼時候切換數據源,怎麼切換數據源,下面以代碼示例來做闡述;

  • 搭建測試環境(1個master庫2個slave庫)
  • DataSource多數據源配置
  • 設置mybatis數據源
  • 攔截器+註解設置master和slave庫選擇
  • 選出當前請求要使用的slave從庫
  • 測試用例

搭建測試環境(1個master庫2個slave庫)

由於測試資源優先在本地模擬創建3個數據庫,分別是1個master庫2個slave庫,裏面分別都有一個tblArticle表,內容也大致相同(爲了演示主從效果,我把從庫中表的title列值增加了slave字樣):

再來創建一個db.properties,分別配置3個數據源,格式如下:

 1 spring.datasource0.jdbc-url=jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf-8&useSSL=false
 2 spring.datasource0.username=root
 3 spring.datasource0.password=123456
 4 spring.datasource0.driver-class-name=com.mysql.jdbc.Driver
 5 
 6 spring.datasource1.jdbc-url=jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
 7 spring.datasource1.username=root
 8 spring.datasource1.password=123456
 9 spring.datasource1.driver-class-name=com.mysql.jdbc.Driver
10 
11 spring.datasource2.jdbc-url=jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf-8&useSSL=false
12 spring.datasource2.username=root
13 spring.datasource2.password=123456
14 spring.datasource2.driver-class-name=com.mysql.jdbc.Driver

同時我們創建具有對應關係的DbType枚舉,幫助我們使代碼更已讀:

 1 public class DbEmHelper {
 2     public enum DbTypeEm {
 3         db0(0, "db0(默認master)", -1),
 4         db1(1, "db1", 0),
 5         db2(2, "db2", 1);
 6 
 7         /**
 8          * 用於篩選從庫
 9          *
10          * @param slaveNum 從庫順序編號 0開始
11          * @return
12          */
13         public static Optional<DbTypeEm> getDbTypeBySlaveNum(int slaveNum) {
14             return Arrays.stream(DbTypeEm.values()).filter(b -> b.getSlaveNum() == slaveNum).findFirst();
15         }
16 
17         DbTypeEm(int code, String des, int slaveNum) {
18             this.code = code;
19             this.des = des;
20             this.slaveNum = slaveNum;
21         }
22 
23         private int code;
24         private String des;
25         private int slaveNum;
26 
27         //get,set省略
28     }
29 }

DataSource多數據源配置

使用上面3個庫連接串信息,配置3個不同的DataSource實例,達到多個DataSource目的;由於在代碼中庫的實例需要動態選擇,因此我們利用AbstractRoutingDataSource來聚合多個數據源;下面是生成多個DataSource代碼:

 1 @Configuration
 2 public class DbConfig {
 3 
 4     @Bean(name = "dbRouting")
 5     public DataSource dbRouting() throws IOException {
 6         //加載db配置文件
 7         InputStream in = this.getClass().getClassLoader().getResourceAsStream("db.properties");
 8         Properties pp = new Properties();
 9         pp.load(in);
10 
11         //創建每個庫的datasource
12         Map<Object, Object> targetDataSources = new HashMap<>(DbEmHelper.DbTypeEm.values().length);
13         Arrays.stream(DbEmHelper.DbTypeEm.values()).forEach(dbTypeEm -> {
14             targetDataSources.put(dbTypeEm, getDataSource(pp, dbTypeEm));
15         });
16 
17         //設置多數據源
18         DbRouting dbRouting = new DbRouting();
19         dbRouting.setTargetDataSources(targetDataSources);
20         return dbRouting;
21     }
22 
23     /**
24      * 創建庫的datasource
25      *
26      * @param pp
27      * @param dbTypeEm
28      * @return
29      */
30     private DataSource getDataSource(Properties pp, DbEmHelper.DbTypeEm dbTypeEm) {
31         DataSourceBuilder<?> builder = DataSourceBuilder.create();
32 
33         builder.driverClassName(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.driver-class-name", dbTypeEm.getCode())));
34         builder.url(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.jdbc-url", dbTypeEm.getCode())));
35         builder.username(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.username", dbTypeEm.getCode())));
36         builder.password(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.password", dbTypeEm.getCode())));
37 
38         return builder.build();
39     }
40 }

能夠看到一個DbRouting實例,其是繼承了AbstractRoutingDataSource,她裏面有個Map變量來存儲多個數據源信息:

1 public class DbRouting extends AbstractRoutingDataSource {
2 
3     @Override
4     protected Object determineCurrentLookupKey() {
5         return DbContextHolder.getDb().orElse(DbEmHelper.DbTypeEm.db0);
6     }
7 }

DbRouting裏面主要重寫了determineCurrentLookupKey(),通過設置和存儲DataSource集合的Map相同的key,以此達到選擇不同DataSource的目的,這裏使用ThreadLocal獲取同一線程存儲的key;主要看AbstractRoutingDataSource類中下面代碼:

 1     protected DataSource determineTargetDataSource() {
 2         Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
 3         Object lookupKey = this.determineCurrentLookupKey();
 4         DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
 5         if(dataSource == null && (this.lenientFallback || lookupKey == null)) {
 6             dataSource = this.resolvedDefaultDataSource;
 7         }
 8         if(dataSource == null) {
 9             throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
10         } else {
11             return dataSource;
12         }
13     }

設置mybatis數據源

本次演示爲了便利,這裏使用mybatis的註解方式來查詢數據庫,我們需要給mybatis設置數據源,我們可以從上面的聲明DataSource的bean方法獲取:

 1 @EnableTransactionManagement
 2 @Configuration
 3 public class MybaitisConfig {
 4     @Resource(name = "dbRouting")
 5     DataSource dataSource;
 6 
 7     @Bean
 8     public SqlSessionFactory sqlSessionFactory() throws Exception {
 9         SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
10         factoryBean.setDataSource(dataSource);
11        // factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:*"));
12         return factoryBean.getObject();
13     }
14 }

我們使用的mybatis註解方式來查詢數據庫,所以不需要加載mapper的xml文件,下面註解方式查詢sql:

1 @Mapper
2 public interface ArticleMapper {
3     @Select("select * from tblArticle where id = #{id}")
4     Article selectById(int id);
5 }

攔截器+註解來選擇master和slave庫

通常操作數據的業務邏輯都放在service層,我們希望service中不同方法使用不同的庫;比如:添加、修改、刪除、部分查詢方法等,使用master主庫來操作,而大部分查詢操作可以使用slave庫來查詢;這裏通過攔截器+靈活的自定義註解來實現我們的需求:

1 @Documented
2 @Target({ElementType.METHOD})
3 @Retention(RetentionPolicy.RUNTIME)
4 public @interface DbType {
5     boolean isMaster() default true;
6 }

註解參數默認選擇master庫來操作業務(看具體需求吧)

 1 @Aspect
 2 @Component
 3 public class DbInterceptor {
 4 
 5     //全部service層請求都走這裏,ThreadLocal纔能有DbType值
 6     private final String pointcut = "execution(* com.sm.service..*.*(..))";
 7 
 8     @Pointcut(value = pointcut)
 9     public void dbType() {
10     }
11 
12     @Before("dbType()")
13     void before(JoinPoint joinPoint) {
14         System.out.println("before...");
15 
16         MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
17         Method method = methodSignature.getMethod();
18         DbType dbType = method.getAnnotation(DbType.class);
19         //設置Db
20         DbContextHolder.setDb(dbType == null ? false : dbType.isMaster());
21     }
22 
23     @After("dbType()")
24     void after() {
25         System.out.println("after...");
26 
27         DbContextHolder.remove();
28     }
29 }

攔截器攔截service層的所有方法,然後獲取帶有自定義註解DbType的方法的isMaster值,DbContextHolder.setDb()方法判斷走master還是slave庫,並賦值給ThreadLocal:

 1 public class DbContextHolder {
 2     private static final ThreadLocal<Optional<DbEmHelper.DbTypeEm>> dbTypeEmThreadLocal = new ThreadLocal<>();
 3     private static final AtomicInteger atoCounter = new AtomicInteger(0);
 4 
 5     public static void setDb(DbEmHelper.DbTypeEm dbTypeEm) {
 6         dbTypeEmThreadLocal.set(Optional.ofNullable(dbTypeEm));
 7     }
 8 
 9     public static Optional<DbEmHelper.DbTypeEm> getDb() {
10         return dbTypeEmThreadLocal.get();
11     }
12 
13     public static void remove() {
14         dbTypeEmThreadLocal.remove();
15     }
16 
17     /**
18      * 設置主從庫
19      *
20      * @param isMaster
21      */
22     public static void setDb(boolean isMaster) {
23         if (isMaster) {
24             //主庫
25             setDb(DbEmHelper.DbTypeEm.db0);
26         } else {
27             //從庫
28             setSlave();
29         }
30     }
31 
32     private static void setSlave() {
33         //累加值達到最大時,重置
34         if (atoCounter.get() >= 100000) {
35             atoCounter.set(0);
36         }
37 
38         //排除master,選出當前線程請求要使用的db從庫 - 從庫算法
39         int slaveNum = atoCounter.getAndIncrement() % (DbEmHelper.DbTypeEm.values().length - 1);
40         Optional<DbEmHelper.DbTypeEm> dbTypeEm = DbEmHelper.DbTypeEm.getDbTypeBySlaveNum(slaveNum);
41         if (dbTypeEm.isPresent()) {
42             setDb(dbTypeEm.get());
43         } else {
44             throw new IllegalArgumentException("從庫未匹配");
45         }
46     }
47 }

這一步驟很重要,通過攔截器來到達選擇master和slave目的,當然也有其他方式的;

選出當前請求要使用的slave從庫

上面能選擇出master和slave走向了,但是往往slave至少有兩個庫存在;我們需要知道怎麼來選擇多個slave庫,目前最常用的方式通過計數器取餘的方式來選擇:

 1     private static void setSlave() {
 2         //累加值達到最大時,重置
 3         if (atoCounter.get() >= 100000) {
 4             atoCounter.set(0);
 5         }
 6 
 7         //排除master,選出當前線程請求要使用的db從庫 - 從庫算法
 8         int slaveNum = atoCounter.getAndIncrement() % (DbEmHelper.DbTypeEm.values().length - 1);
 9         Optional<DbEmHelper.DbTypeEm> dbTypeEm = DbEmHelper.DbTypeEm.getDbTypeBySlaveNum(slaveNum);
10         if (dbTypeEm.isPresent()) {
11             setDb(dbTypeEm.get());
12         } else {
13             throw new IllegalArgumentException("從庫未匹配");
14         }
15     }

這裏根據餘數來匹配對應DbType枚舉,選出DataSource的Map需要的key,並且賦值到當前線程ThreadLocal中;

1         /**
2          * 用於篩選從庫4          * @param slaveNum 從庫順序編號 0開始
5          * @return
6          */
7         public static Optional<DbTypeEm> getDbTypeBySlaveNum(int slaveNum) {
8             return Arrays.stream(DbTypeEm.values()).filter(b -> b.getSlaveNum() == slaveNum).findFirst();
9         }

測試用例

完成上面操作後,我們搭建個測試例子,ArticleService中分別如下3個方法,不同點在於@DbType註解的標記:

 1 @Service
 2 public class ArticleService {
 3 
 4     @Autowired
 5     ArticleMapper articleMapper;
 6 
 7     @DbType
 8     public Article selectById01(int id) {
 9         Article article = articleMapper.selectById(id);
10         System.out.println(JsonUtil.formatMsg("selectById01:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle()));
11         return article;
12     }
13 
14     @DbType(isMaster = false)
15     public Article selectById02(int id) {
16         Article article = articleMapper.selectById(id);
17         System.out.println(JsonUtil.formatMsg("selectById02:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle()));
18         return article;
19     }
20 
21     public Article selectById(int id) {
22         Article article = articleMapper.selectById(id);
23         System.out.println(JsonUtil.formatMsg("selectById:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle()));
24         return article;
25     }
26 }

在同一個Controller層接口方法中去調用這3個service層方法,按照正常邏輯來講,不出意外得到的結果是這樣:

請求了兩次接口,得到結果是:
selectById01方法:標記了@DbType,但默認走isMaster=true,實際走了db0(master)庫
selectById02方法:標記了@DbType(isMaster = false),實際走了db1(slave1)庫
selectById方法:沒有標記了@DbType,實際走了db2(slave2)庫,因爲攔截器中沒有找到DbType註解,讓其走了slave方法;因爲selectById02執行過一次slave方法,計數器+1了,因此餘數也變了所以定位到了slave2庫(如果是基數調用,selectById02和selectById方法來回切換走不同slave庫);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章