欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

springboot数据库主从方案

程序员文章站 2022-10-26 08:24:42
本篇分享数据库主从方案,案例采用springboot+mysql+mybatis演示;要想在代码中做主从选择,通常需要明白什么时候切换数据源,怎么切换数据源,下面以代码示例来做阐述; 搭建测试环境(1个master库2个slave库) DataSource多数据源配置 设置mybatis数据源 拦截 ......

本篇分享数据库主从方案,案例采用springboot+mysql+mybatis演示;要想在代码中做主从选择,通常需要明白什么时候切换数据源,怎么切换数据源,下面以代码示例来做阐述;

  • 搭建测试环境(1个master库2个slave库)
  • datasource多数据源配置
  • 设置mybatis数据源
  • 拦截器+注解设置master和slave库选择
  • 选出当前请求要使用的slave从库
  • 测试用例

搭建测试环境(1个master库2个slave库)

由于测试资源优先在本地模拟创建3个数据库,分别是1个master库2个slave库,里面分别都有一个tblarticle表,内容也大致相同(为了演示主从效果,我把从库中表的title列值增加了slave字样):

springboot数据库主从方案

再来创建一个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层方法,按照正常逻辑来讲,不出意外得到的结果是这样:

springboot数据库主从方案

请求了两次接口,得到结果是:
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库);