SpringBoot实现MySQL读写分离
在高并发下,需要对应用进行读写分离,配置多数据源,即写操作走主库,读操作则走从库,主从数据库负责各自的读和写,缓解了锁的争用,提高了读取性能。
实现读写分离有多种方式,如使用中间件MyCat、Sharding-JDBC等,这里我们使用Aop的方式在代码层面实现读写分离。
实现原理
实现读写分离,首先要对Mysql做主从复制,即搭建一个主数据库,以及一个或多个从数据库。
具体实现主从复制,可参照前一篇博客《基于docker配置MySQL主从复制》
使用Aop的方式,当调用业务层方法前,判断请求是否是只读操作,动态切换数据源,如果是只读操作,则切换从数据库的数据源,否则使用主数据库的数据源。
系统架构
这是我之前写的一个项目,项目就是使用了本文章介绍的读写分离方式,架构图大概如下:
代码实现
在application.yml配置MySQL
spring:datasource:type: com.alibaba.druid.pool.DruidDataSource#主机master:username: rootpassword: 123456url: jdbc:mysql://服务器ip:3306/letfit?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMTdriver-class-name: com.mysql.cj.jdbc.Driver#从机slave:username: rootpassword: 123456url: jdbc:mysql://服务器ip:3307/letfit?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMTdriver-class-name: com.mysql.cj.jdbc.Driver#连接池druid:initialSize: 5minIdle: 5maxActive: 20maxWait: 60000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truefilters: stat,wallmaxPoolPreparedStatementPerConnectionSize: 20useGlobalDataSourceStat: trueconnectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
创建 ReadOnly
注解
在业务层的方法上使用该注解,使用 ReadOnly
注解的方法只处理读操作,会切换到从机的数据源
package com.letfit.aop.annotation;/*** 只读注解*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}
创建枚举类
定义两个枚举类型 MASTER
、slave
分别代表数据库类型
package com.letfit.common;/*** 数据库类型*/
public enum DBTypeEnum {/*** 主数据库*/MASTER,/*** 从数据库*/SLAVE;
}
编写动态切换数据源的工具类
package com.letfit.util;/*** 动态切换数据源工具类*/
@Slf4j
public class DynamicDbUtil {/*** 用来存储代表数据源的对象*/private static final ThreadLocal<DBTypeEnum> CONTEXT_HAND = new ThreadLocal<>();/*** 切换当前线程要使用的数据源* @param dbTypeEnum*/public static void set(DBTypeEnum dbTypeEnum){CONTEXT_HAND.set(dbTypeEnum);log.info("切换数据源:{}", dbTypeEnum);}/*** 切换到主数据库*/public static void master(){set(DBTypeEnum.MASTER);}/*** 切换到从数据库*/public static void slave(){set(DBTypeEnum.SLAVE);}/*** 移除当前线程使用的数据源*/public static void remove(){CONTEXT_HAND.remove();}/*** 获取当前线程使用的枚举类* @return*/public static DBTypeEnum get(){return CONTEXT_HAND.get();}}
编写 AbstractRoutingDataSource
的实现类
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
AbstractRoutingDataSource 的部分源码如下
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {/** 用来存储多个数据源*/@Nullableprivate Map<Object, Object> targetDataSources;/** 默认数据源*/@Nullableprivate Object defaultTargetDataSource;@Nullableprivate Map<Object, DataSource> resolvedDataSources;@Nullableprivate DataSource resolvedDefaultDataSource;public AbstractRoutingDataSource() {}/** 设置多数据源,最终使用哪个数据源由determineTargetDataSource()返回决定*/public void setTargetDataSources(Map<Object, Object> targetDataSources) {this.targetDataSources = targetDataSources;}/** 设置默认数据源*/public void setDefaultTargetDataSource(Object defaultTargetDataSource) {this.defaultTargetDataSource = defaultTargetDataSource;}/** 决定使用的数据源,选择的策略需要我们自己去定义*/protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");//调用determineCurrentLookupKey()获取数据源的keyObject lookupKey = this.determineCurrentLookupKey();//根据key获取对应数据源DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");} else {return dataSource;}}/** 抽象方法,需要我们自己去实现*/@Nullableprotected abstract Object determineCurrentLookupKey();
}
编写 DynamicDataSource
继承 AbstractRoutingDataSource
package com.letfit.common;public class DynamicDataSource extends AbstractRoutingDataSource {/*** 返回当前线程正在使用代表数据库的枚举对象* @return*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDbUtil.get();}}
流程步骤:
1、重写数据源选择策略determineCurrentLookupKey()。
2、数据源配置类将数据源存放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
3、进行数据库连接时,调用AbstractRoutingDataSource的getConnection()的方法,此时会先调用determineTargetDataSource()方法返回DataSource再进行getConnection()。
编写多数据源配置类
package com.letfit.config;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.letfit.common.DBTypeEnum;
import com.letfit.common.DynamicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;@Configuration
public class DataSourceConfig {/*** 主数据库数据源,存入Spring容器* 注解@Primary表示主数据源* @return*/@ConfigurationProperties(prefix = "spring.datasource.master")@Primary@Beanpublic DataSource masterDataSource(){return DruidDataSourceBuilder.create().build();}/*** 从数据库数据源,存入Spring容器* @return*/@ConfigurationProperties(prefix = "spring.datasource.slave")@Beanpublic DataSource slaveDataSource(){return DruidDataSourceBuilder.create().build();}/*** 决定最终数据源* @param masterDataSource* @param slaveDataSource* @return*/@Beanpublic DataSource targetDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource){//存放主从数据源Map<Object,Object> targetDataSource = new HashMap<>(2);//主数据源targetDataSource.put(DBTypeEnum.MASTER, masterDataSource);//从数据源targetDataSource.put(DBTypeEnum.SLAVE, slaveDataSource);//实现动态切换DynamicDataSource dynamicDataSource = new DynamicDataSource();//绑定所有数据源dynamicDataSource.setTargetDataSources(targetDataSource);//设置默认数据源dynamicDataSource.setDefaultTargetDataSource(masterDataSource);return dynamicDataSource;}}
配置Mybatis
当我们只有一个数据源时,SpringBoot会默认配置Mybatis,现在我们有多个数据源,就需要手动配置Mybatis的SqlSessionFactory
package com.letfit.config;import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Objects;/*** 多数据源需要手动配置SqlSessionFactory*/
@Configuration
@EnableTransactionManagement
public class MybatisConfig {@Resource(name = "targetDataSource")private DataSource dataSource;/*** 配置SqlSessionFactory* @return* @throws Exception*/public SqlSessionFactory sqlSessionFactory() throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource);//配置映射文件路径factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));//配置别名factoryBean.setTypeAliasesPackage("com.letfit.pojo");//设置驼峰命名Objects.requireNonNull(factoryBean.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);return factoryBean.getObject();}/*** 配置事务管理* @return*/@Beanpublic PlatformTransactionManager transactionManager(){return new DataSourceTransactionManager(dataSource);}@Beanpublic SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory){return new SqlSessionTemplate(sqlSessionFactory);}}
配置Aop
package com.letfit.aop;@Component
@Aspect
@Slf4j
public class DataSourceAop {@Pointcut("@annotation(com.letfit.aop.annotation.ReadOnly)")public void readPointcut(){}/*** 配置前置通知,切换数据源为从数据库*/@Before("readPointcut()")public void readAdvise(){log.info("切换数据源为从数据库");DynamicDbUtil.slave();}}
业务层方法上使用 ReadOnly
注解
/*** 根据标题关键字模糊查询资源* @param title* @return*/
@ReadOnly
@Override
public ResultInfo<List<Source>> searchSource(String title) {if(!ValiDateUtil.isLegalString(title)){return ResultInfo.error(CodeEnum.PARAM_NOT_IDEAL, null);}List<Source> sourceList = sourceMapper.searchSource(title);return ResultInfo.success(CodeEnum.SUCCESS, sourceList);
}
- 至此,读写分离的工作就完成了!
SpringBoot实现MySQL读写分离相关推荐
- MySQL的主从配置+SpringBoot的MySQL读写分离配置
MySQL的主从复制 点击前往查看MySQL的安装 1.主库操作 vim /etc/my.cnf 添加如下配置 log-bin=mysql-bin #[必须]启用二进制日志 server-id=128 ...
- SpringBoot + MyBatis + MySQL 读写分离实战
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://t.cn/AiKuJEB9 1. 引言 读写分 ...
- Springboot Mybatis MySQL读写分离及事物配置
为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...
- 【MySQL】Gaea 数据库中间件实现 MySQL 读写分离
声明: 以下内容是学习macro作者的文章,作者原创文章链接:你还在代码里做读写分离么,试试这个中间件吧! 下面夹杂了我自己运行过程中的错误与心得,能运行成功 传统的MySql读写分离方案是通过在代码 ...
- Mysql 读写分离中间件(Gaea)
01 摘要 传统的MySql读写分离方案是通过在代码中根据SQL语句的类型动态切换数据源来实现的,那么有没有什么中间件可以自动实现读写分离呢?小米开源的数据库中间件Gaea就可以实现,接下来我们将详细 ...
- proxy实现 mysql 读写分离
实现 mysql 读写分离 图解: 环境: iptables 和 selinux 关闭 proxy:test2 172.25.1.2 Master: test3 172.25.1.3 Slave:te ...
- 项目性能优化(MySQL读写分离、MySQL主从同步、Django实现MySQL读写分离)
当项目中数据库表越来越多,数据量也逐渐增多时,需要做数据库的安全和性能的优化.对于数据库的优化,可以选择使用MySQL读写分离实现. 1.MySQL主从同步 1.主从同步机制 1.1.主从同步介绍和优 ...
- asp.net mysql 读写分离_MySQL读写分离
MySQL读写分离 1,为啥要读写分离? 系统到了高并发阶段,数据库一定要做的读写分离了,因为大部分的项目都是读多写少.所以针对这个情况,把写操作放一个主库,主库下挂多个从库处理读操作,这样就可以支撑 ...
- mysql读写分离,主从配置
2019独角兽企业重金招聘Python工程师标准>>> 一个完整的mysql读写分离环境包括以下几个部分: 应用程序client database proxy database集群 ...
- php mysql读写分离主从复制_mysql主从复制 读写分离原理及实现
主从复制,读写分离原理 在实际的生产环境中,对数据库的读和写都在同一个数据库服务器中,是不能满足实际需求的.无论是在安全性.高可用性还是高并发等各个方面都是完全不能满足实际需求的.因此,通过主从复制的 ...
最新文章
- Oracle视图添加约束,Oracle的约束视图
- Gartner:2018年十大科技趋势与其对IT和执行的影响
- python能做游戏吗-python能开发游戏吗
- APUE-文件和目录(二)函数access,mask,chmod和粘着位
- 老男孩为网友工作疑难问题解答一例
- 数值保留几位小数后四舍五入、向上取值、向下取值、
- int main(int argc,char* argv[])详解
- 通过hsv筛选颜色 python_OpenCV-Python 光流介绍(附代码)
- 安装java错误_安装JAVA JDK错误提示正在进行另一JAVA安装解决方法
- QTcpServer. QTcpSocket. QUdpSocket之间的区别
- 低代码开发平台+KM知识文档管理系统搭配的好处
- UI设计---化繁为简
- 数据库设计3个泛式和经验谈
- php c端,蛋白测序(N端,C端测序)
- 浅谈云计算和大数据技术
- python阴阳师脚本_阴阳师肝不动了,试试Python吧
- uint64 和字符串相互转换
- 学人工智能要看什么书?AI入门到进阶10本必看书
- PPT中如何修改插入的图片为透明色
- 什么是大数据采集?大数据采集的过程是什么?