编写StudentDao接口,并进行测试
接口:
package cn.objectspace.springtestdemo.dao; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import cn.objectspace.springtestdemo.domain.Student; @Mapper public interface StudentDao { @Insert("INSERT INTO student(student_id,student_name)VALUES(#{studentId},#{studentName})") public Integer insertStudent(Student student); @Select("SELECT * FROM student WHERE student_id = #{studentId}") public Student queryStudentByStudentId(Student student); }
测试类:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {ApplicationStarter.class})// 指定启动类 public class DaoTest { @Autowired StudentDao studentDao; @Test public void test01() { Student student = new Student(); student.setStudentId("20191130"); student.setStudentName("Object6"); studentDao.insertStudent(student); studentDao.queryStudentByStudentId(student); } }
如果可以正确往数据库中插入数据,如下图,则MyBatis搭建成功。
正式搭建
通过上面的准备工作,我们已经可以实现对数据库的读写,但是并没有实现读写分离,现在才是开始实现数据库的读写分离。
修改application.yml
刚才我们的配置文件中只有单数据源,而读写分离肯定不会是单数据源,所以我们首先要在application.yml中配置多数据源。
server: port: 10001 spring: datasource: master: url: jdbc:mysql://192.168.43.201:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: Object password: Object971103. driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://192.168.43.202:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: Object password: Object971103. driver-class-name: com.mysql.cj.jdbc.Driver #MyBatis配置 mybatis: mapper-locations: classpath:mapper/*.xml configuration: cache-enabled: true #开启二级缓存 map-underscore-to-camel-case: true
DataSource的配置
首先要先创建两个ConfigurationProperties类,这一步不是非必须的,直接配置DataSource也是可以的,但是我还是比较习惯去写这个Properties。
MasterProperpties
package cn.objectspace.springtestdemo.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "spring.datasource.master") @Component public class MasterProperties { private String url; private String username; private String password; private String driverClassName; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } }
SlaveProperties
package cn.objectspace.springtestdemo.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "spring.datasource.slave") @Component public class SlaveProperties { private String url; private String username; private String password; private String driverClassName; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } }
DataSourceConfig
这个配置主要是对主从数据源进行配置。
@Configuration public class DataSourceConfig { private Logger logger = LoggerFactory.getLogger(DataSourceConfig.class); @Autowired private MasterProperties masterProperties; @Autowired private SlaveProperties slaveProperties; //默认是master数据源 @Bean(name = "masterDataSource") @Primary public DataSource masterProperties(){ logger.info("masterDataSource初始化"); HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(masterProperties.getUrl()); dataSource.setUsername(masterProperties.getUsername()); dataSource.setPassword(masterProperties.getPassword()); dataSource.setDriverClassName(masterProperties.getDriverClassName()); return dataSource; } @Bean(name = "slaveDataSource") public DataSource dataBase2DataSource(){ logger.info("slaveDataSource初始化"); HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(slaveProperties.getUrl()); dataSource.setUsername(slaveProperties.getUsername()); dataSource.setPassword(slaveProperties.getPassword()); dataSource.setDriverClassName(slaveProperties.getDriverClassName()); return dataSource; } }
动态数据源的切换
这里使用到的主要是Spring提供的AbstractRoutingDataSource,其提供了动态数据源的功能,可以帮助我们实现读写分离。其determineCurrentLookupKey()可以决定最终使用哪个数据源,这里我们自己创建了一个DynamicDataSourceHolder,来给他传一个数据源的类型(主、从)。
package cn.objectspace.springtestdemo.dao.split; import java.util.HashMap; import java.util.Map; import javax.annotation.Resource; import javax.sql.DataSource; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * * @Description: spring提供了AbstractRoutingDataSource,提供了动态选择数据源的功能,替换原有的单一数据源后,即可实现读写分离: * @Author: Object * @Date: 2019年11月30日 */ public class DynamicDataSource extends AbstractRoutingDataSource{ //注入主从数据源 @Resource(name="masterDataSource") private DataSource masterDataSource; @Resource(name="slaveDataSource") private DataSource slaveDataSource; @Override public void afterPropertiesSet() { setDefaultTargetDataSource(masterDataSource); Map<Object, Object> dataSourceMap = new HashMap<>(); //将两个数据源set入目标数据源 dataSourceMap.put("master", masterDataSource); dataSourceMap.put("slave", slaveDataSource); setTargetDataSources(dataSourceMap); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { //确定最终的目标数据源 return DynamicDataSourceHolder.getDbType(); } }
DynamicDataSourceHolder的实现
这个类由我们自己实现,主要是提供给Spring我们需要用到的数据源类型。
package cn.objectspace.springtestdemo.dao.split; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @Description: 获取DataSource * @Author: Object * @Date: 2019年11月30日 */ public class DynamicDataSourceHolder { private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceHolder.class); private static ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static final String DB_MASTER = "master"; public static final String DB_SLAVE="slave"; /** * @Description: 获取线程的DbType * @Param: args * @return: String * @Author: Object * @Date: 2019年11月30日 */ public static String getDbType() { String db = contextHolder.get(); if(db==null) { db = "master"; } return db; } /** * @Description: 设置线程的DbType * @Param: args * @return: void * @Author: Object * @Date: 2019年11月30日 */ public static void setDbType(String str) { logger.info("所使用的数据源为:"+str); contextHolder.set(str); } /** * @Description: 清理连接类型 * @Param: args * @return: void * @Author: Object * @Date: 2019年11月30日 */ public static void clearDbType() { contextHolder.remove(); } }
MyBatis拦截器的实现
最后就是我们实现读写分离的核心了,这个类可以对SQL进行判断,是读SQL还是写SQL,从而进行数据源的选择,最终调用DynamicDataSourceHolder的setDbType方法,将数据源类型传入。
package cn.objectspace.springtestdemo.dao.split; import java.util.Locale; import java.util.Properties; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.keygen.SelectKeyGenerator; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionSynchronizationManager; /** * @Description: MyBatis级别拦截器,根据SQL信息,选择不同的数据源 * @Author: Object * @Date: 2019年11月30日 */ @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class }) }) @Component public class DynamicDataSourceInterceptor implements Interceptor { private Logger logger = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class); // 验证是否为写SQL的正则表达式 private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; /** * 主要的拦截方法 */ @Override public Object intercept(Invocation invocation) throws Throwable { // 判断当前是否被事务管理 boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive(); String lookupKey = DynamicDataSourceHolder.DB_MASTER; if (!synchronizationActive) { //如果是非事务的,则再判断是读或者写。 // 获取SQL中的参数 Object[] objects = invocation.getArgs(); // object[0]会携带增删改查的信息,可以判断是读或者是写 MappedStatement ms = (MappedStatement) objects[0]; // 如果为读,且为自增id查询主键,则使用主库 // 这种判断主要用于插入时返回ID的操作,由于日志同步到从库有延时 // 所以如果插入时需要返回id,则不适用于到从库查询数据,有可能查询不到 if (ms.getSqlCommandType().equals(SqlCommandType.SELECT) && ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]); String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " "); // 正则验证 if (sql.matches(REGEX)) { // 如果是写语句 lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { lookupKey = DynamicDataSourceHolder.DB_SLAVE; } } } else { // 如果是通过事务管理的,一般都是写语句,直接通过主库 lookupKey = DynamicDataSourceHolder.DB_MASTER; } logger.info("在" + lookupKey + "中进行操作"); DynamicDataSourceHolder.setDbType(lookupKey); // 最后直接执行SQL return invocation.proceed(); } /** * 返回封装好的对象,或代理对象 */ @Override public Object plugin(Object target) { // 如果存在增删改查,则直接拦截下来,否则直接返回 if (target instanceof Executor) return Plugin.wrap(target, this); else return target; } /** * 类初始化的时候做一些相关的设置 */ @Override public void setProperties(Properties properties) { // TODO Auto-generated method stub } }
代码梳理
通过上文中的程序,我们已经可以实现读写分离了,但是这么看着还是挺乱的。所以在这里重新梳理一遍上文中的代码。
其实逻辑并不难:
- 通过
@Configuration
实现多数据源的配置。 - 通过MyBatis的拦截器,DynamicDataSourceInterceptor来判断某条SQL语句是读还是写,如果是读,则调用
DynamicDataSourceHolder.setDbType("slave")
,否则调用DynamicDataSourceHolder.setDbType("master")
。 - 通过AbstractRoutingDataSource的
determineCurrentLookupKey()
方法,返回DynamicDataSourceHolder.getDbType();
也就是我们在拦截器中设置的数据源。 - 对注入的数据源执行SQL。
测试
@RunWith(SpringRunner.class) @SpringBootTest(classes = {ApplicationStarter.class})// 指定启动类 public class DaoTest { @Autowired StudentDao studentDao; @Test public void test01() { Student student = new Student(); student.setStudentId("20191130"); student.setStudentName("Object6"); studentDao.insertStudent(student); studentDao.queryStudentByStudentId(student); } }
测试结果:
至此,代码层读写分离已完整地实现。
基于MyCat中间件实现读写分离、故障转移
简介
在上文中我们已经实现了使用手写代码的方式对数据库进行读写分离,但是不知道大家发现了没有,我只使用了一主一从。那么为什么我有一主二从的环境却只实现一主一从的读写分离呢?因为,在代码层实现一主多从的读写分离我也不会写。那么假设数据库集群不止于一主二从,而是一主三从,一主四从,多主多从呢?如果Master节点宕机了,又该怎么处理?
每次动态增加一个节点,我们就要重新修改我们的代码,这不但会给开发人员造成很大的负担,而且不符合开闭原则。
所以接下来的MyCat应该可以解决这样的问题。并且我会直接使用一主二从的环境演示。
MyCat介绍
这里直接套官方文档。
- 一个彻底开源的,面向企业应用开发的大数据库集群
- 支持事务、ACID、可以替代MySQL的加强版数据库
- 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群
- 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server
- 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品
- 一个新颖的数据库中间件产品
环境说明
MyCat 192.168.43.90
MySQL master 192.168.43.201
MySQL slave1 192.168.43.202
MySQL slave2 192.168.43.203
接上篇博客的MySQL数据库一主二从,不过MySQL版本需要从8.0改为5.7,否则会出现
密码问题无法连接。
另外,我们需要在每个数据库中都为MyCat创建一个账号并赋上权限:
CREATE USER 'user_name'@'host' IDENTIFIED BY 'password'; GRANT privileges ON databasename.tablename TO ‘username’@‘host’; --可以使用下面这句 赋予所有权限 GRANT ALL PRIVILEGES ON *.* TO ‘username’@‘host’; --最后刷新权限 FLUSH PRIVILEGES;
在开始之前,先保证主从库的搭建是成功的:
如何安装MyCat在这里我就不说了,百度上有很多帖子有,按照上面的教程一步一步来其实没有多大问题。我们着重说说和我们MyCat配置相关的两个配置文件——schema.xml和server.xml,当然还有一个rules.xml,但是这里暂时不介绍分库分表,所以这个暂且不提。