基于Spring Boot与MyBatis框架构建动态读写分离模型
2021-03-17张旭刚张昕高若寒
张旭刚, 张昕, 高若寒
(国电南瑞科技股份有限公司 信息系统集成分公司, 江苏 南京 210000)
0 引言
读写分离集群,不仅提高了系统的健壮性和可靠性,以及系统的吞吐量和性能,保障了系统业务的连续性,而且也实现了资源的最大利用率。当前的实现方法主要通过静态方式配置,主要有中间件方式,如amoeba和mysql-proxy,分业务方式,对读写操作配置url。静态方式缺乏灵活性,无法根据系统负载、用户需求等情况,实现资源的快速动态收缩,难以满足在不停机的条件下进行数据源切换,无法保证业务的连续性。
利用Spring Boot和MyBatis框架提供的优势,通过面向切面编程AOP,实现一种对应用透明、数据源可以动态收缩与切换的模型。
1 Spring Boot架构
Spring Boot是由Pivotal团队提供,简化Spring开发的微服务框架。通过约定优于配置和起步依赖,简化复杂的依赖关系,大量减少XML配置文件,基本实现自动化位置,能够快速创建独立运行的Spring项目,并且集成了主流框架,如AOP和MyBatis。为实现动态读写分离模型,主要利用面向切面编程技术AOP、MyBatis映射、SpringBoot的类Abstract Routing Data Source和Thread Local实现不同线程间的数据隔离[1]。
1.1 Spring AOP
Spring AOP(Aspect-Oriented Programming,面向切面编程),是一种称为“横切”的技术,把与业务无关逻辑,但为业务模块共同调用的逻辑或功能封装起来,将其命名为“Aspect”,即方面,减少系统的重复代码,降低模块间的耦合度,便于后期的操作和维护。在论文中,主要使用AOP的前置通知,拦截MyBatis映射的SQL语句,动态选择数据源。
1.2 MyBatis映射
Mybatis是一个支持普通SQL查询、存储过程和高级映射的优秀持久层框架,在持久层映射关系的开发中,可以不用写实现类,能以代理方式自动生成实现代码,同时SQL语句写在映射XML文件中,实现了代码与SQL分离,降低耦合度。在映射XML文件中,通过id标识不同类型的SQL语句,对查询、插入、删除和更新语句进行区分,如查询语句的id前缀为query,删除语句的id前缀为delete,通过甄别判断为不同SQL语句选择对应的数据源,实现动态的读写分离。
1.3 Abstract Routing Data Source
Spring Boot提供了Abstract Routing Data Source根据用户定义的规则选择当前的数据源,可以在执行SQL操作前,设置使用的数据源,实现动态路由数据源的模型,它的方法determine Target Data Source()返回一个数据源,在该方法内部会调用抽象方法determine Current Lookup Key()决定使用哪个数据源,lookup key键通常是通过Thread Local绑定的上下文来实现。
1.4 Thread Local
Thread Local作用是提供线程内的局部变量,维护变量时Thread Local为每个使用该变量的线程提供独立的变量副本。
在面向切面编程AOP的前置通知中通过Thread Local设置线程的数据源类型,是读数据源还是写数据源。在返回数据源的时候,通过determine Current Lookup Key()调用Thread Local取得线程的数据源类型,从而为本次访问指定具体的数据源,是访问读库还是写库[2]。
2 动态读写分离设计与实现
2.1 总体架构
程序实现基于Spring Boot框架,通过Maven进行编译、测试和打包。Spring Boot基于Spring,减少了配置,简化了编码,使开发更高效便捷[3]。整体实现分五层,第一层客户端即应用程序,发起数据访问;第二层访问到DAO(数据访问对象),访问的sql语句配置在MyBatis的映射文件里,与程序的DAO接口形成映射关系,由MyBatis自动实现接口的文件,对数据库进行访问;第三层,AOP,即面向切面编程层,在DAO访问数据库之前,进行拦截,根据访问id进行动态选择数据源,如果是查询语句则访问读库,如果是修改语句,则指向到主数据库,实现数据的读写分离,主要功能有负载均衡、高可用性、SQL过滤、读写分离和数据库路由等;第四层,创建和封装两个数据源,每个数据源创建一个数据库资源池,分别指向写数据库和读数据库;第五层,主备数据库之间,通过binlog进行数据实时同步,并进行故障切换[4]。
通过上面五层,与Spring Boot和MyBatis架构构建程序一致,对原有程序透明,无任何侵入,原程序不需要任何改造,简单便捷地实现了动态的数据库读写分离[5]。
同时,这种结构可以进行横向扩展,当性能无法满足需求时,添加数据源,添加数据库,进行负载分担,对应用透明,如图1所示。
2.2 读写分离的实现
实现MySQL数据库的动态读写分离,读写分离的实现类图,如图2所示。
主要由四个类实现,Dynamic Data Source动态的根据数据源的值返回数据源;Data Source Context Holder封装了Thread Local,用于设置和获取本次访问的数据源的值;Dynamic Data Source Aspect实现AOP的前置通知,拦截和解析SQL的id,根据id判断是读操作还是写操作,通过Data Source Context Holder动态设置数据源的值,然后Dynamic Data Source获取到要访问的数据源;Multi Data Source Con-fig配置多个数据源,在应用启动后有多个数据源可以选择。
图1 总体结构图
图2 读写分离的实现类图
Dynamic Data Source,用于获取数据库访问的数据源,如果是查询操作,返回只读库数据源,如果是增删改则访问写库。继承Abstract Routing Data Source并重写其中的方法determine Current Lookup Key(),该方法调用封装了Thread Local的Database Context Holder,获取当前线程的Database Type。
Data Source Context Holder,用户设置数据库访问的数据源,具体设置通过切面拦截调用该类的方法set Data Source Type。该类拥有一个Thread Local的静态常量私有属性private static final Thread Local〈String〉 CONTEXT_HOLDER = new Thread Local〈String〉(),静态方法set Data Source Type(String data Source Key)和get Data Source Type()通过CONTEXT_HOLDER属性,用于标识数据源,给每个访问数据库的线程返回要访问的数据源。
Dynamic Data Source Aspect用于定义要拦截的SQL操作,通过前置通知解析MyBatis中配置的id,根据id判断SQL操作是读操作还是增删改,并利用Data Source Context Holder的静态方法设置当前线程的数据源类型。在进行数据源选择时,Dynamic Data Source返回设置的当前线程的数据源类型,当前线程准确地找到需要访问的数据源。它的主要实现方法如下。
@Pointcut("execution( * com.sboot.dao.*.*(..))")
public void daoAspect() {
}
@Before("daoAspect()")
public void switchDataSource(JoinPoint point) {
System.out.println("Begin to execute "+point.getSignature().getName());
Boolean isQueryMethod = isQueryMethod(point.getSignature().getName());
if (isQueryMethod) {
DataSourceContextHolder.setDataSourceType("slave");
System.out.println("Slave DataSource begin to execute "+point.getSignature().getName());
}
}
Multi Data Source Config,是一个基于注解的配置,主要封装了写和读两个数据源,实现多数据源,需要取消Spring Boot的自动数据源配置,主要实现方法如下。
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
Map〈Object, Object〉 targetDataSources = new HashMap〈Object, Object〉();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(masterDataSource());
return dataSource;
}
2.3 配置多数据源
在application.yml中添加两个数据源[6]:
pring:
datasource:
master://写数据源的配置
url:
jdbc:mysql://192.168.10.12:3306/masterdb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: studba
password: stuDba1
driverClassName: com.mysql.cj.jdbc.Driver
slave://读数据源的配置
url:
jdbc:mysql://192.168.10.13:3306/slavedb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: studba
password: stuDba1
driverClassName: com.mysql.cj.jdbc.Driver
然后在类DataSourceConfig中,利用注解的方式生成数据源:
@Primary
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
通过@ConfigurationProperties注解把在配置文件的配置自动的匹配配置数据源需要的值,生成数据源。备数据源的原理与上面一致。
2.4 数据访问流程
数据访问流程,如图3所示。
图3 数据访问流程
(1) 客户端访问数据库,正常流程走到DAO层,MyBatis进行映射接口,取得映射的sql语句,如findStudentById。
(2) 取得sql语句访问数据库。
(3) 通过@Before("daoAspect()")拦截访问,并检查是查询语句,设置数据源为读数据库。
判断出是find开头的sql语句,设置读数据源DataSourceContextHolder.setDataSourceType("slave")。
(4) MultiDynamicDataSource
在方法determineCurrentLookupKey()中返回数据源类型return DataSourceContextHolder.getDataSourceType()。
(5) MultiDynamicDataSource的方法
determineTargetDataSource()根据上面determineCurrentLookupKey()函数返回的key值选择一个指定的数据源。
(6) 返回要访问的数据源,本次访问返回的是读数据源。
(7) 根据返回的读数据源,访问读数据库。
2.5 应用验证
通过学生ID查询学生信息进行验证,查询操作到读库进行操作。查询学生信息的MyBatis SQL id是findStudent ById,在浏览器输入http://192.168.1.10:8080/stuInfo,进行查询,日志输出信息,如图4所示。
图4 测试验证
日志打印出执行sql语句findStudentById,动态选择读数据源Slave DataSource执行。
3 总结
本文基于Spring Boot和MyBatis框架,实现了动态的MySQL读写分离模型,方法简单、便捷,对应用透明,低耦合,无侵入性,安装和拆卸对现有程序无任何影响,没有额外的成本。后续可加入多数据源,通过zookeeper进行状态监控和管理,实现更智能和动态的数据库的横向扩展和收缩,满足云计算场景需求。