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库);
上一篇: Java性能 -- CAS乐观锁
下一篇: 扯面和什么菜好吃?扯面的制作窍门有哪些?