本篇分享數據庫主從方案,案例採用springboot+mysql+mybatis演示;要想在代碼中作主從選擇,一般須要明白何時切換數據源,怎麼切換數據源,下面以代碼示例來作闡述;mysql
因爲測試資源優先在本地模擬建立3個數據庫,分別是1個master庫2個slave庫,裏面分別都有一個tblArticle表,內容也大體相同(爲了演示主從效果,我把從庫中表的title列值增長了slave字樣):算法
再來建立一個db.properties,分別配置3個數據源,格式以下:spring
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枚舉,幫助咱們使代碼更已讀:sql
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 }
使用上面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變量來存儲多個數據源信息:springboot
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類中下面代碼:mybatis
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設置數據源,咱們能夠從上面的聲明DataSource的bean方法獲取:app
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:ide
1 @Mapper 2 public interface ArticleMapper { 3 @Select("select * from tblArticle where id = #{id}") 4 Article selectById(int id); 5 }
一般操做數據的業務邏輯都放在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目的,固然也有其餘方式的;
上面能選擇出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庫);