在高并发下,需要对应用进行读写分离,配置多数据源,即写操作走主库,读操作则走从库,主从数据库负责各自的读和写,缓解了锁的争用,提高了读取性能。

实现读写分离有多种方式,如使用中间件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 {}

创建枚举类

定义两个枚举类型 MASTERslave分别代表数据库类型

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读写分离相关推荐

  1. MySQL的主从配置+SpringBoot的MySQL读写分离配置

    MySQL的主从复制 点击前往查看MySQL的安装 1.主库操作 vim /etc/my.cnf 添加如下配置 log-bin=mysql-bin #[必须]启用二进制日志 server-id=128 ...

  2. SpringBoot + MyBatis + MySQL 读写分离实战

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://t.cn/AiKuJEB9 1. 引言 读写分 ...

  3. Springboot Mybatis MySQL读写分离及事物配置

    为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...

  4. 【MySQL】Gaea 数据库中间件实现 MySQL 读写分离

    声明: 以下内容是学习macro作者的文章,作者原创文章链接:你还在代码里做读写分离么,试试这个中间件吧! 下面夹杂了我自己运行过程中的错误与心得,能运行成功 传统的MySql读写分离方案是通过在代码 ...

  5. Mysql 读写分离中间件(Gaea)

    01 摘要 传统的MySql读写分离方案是通过在代码中根据SQL语句的类型动态切换数据源来实现的,那么有没有什么中间件可以自动实现读写分离呢?小米开源的数据库中间件Gaea就可以实现,接下来我们将详细 ...

  6. proxy实现 mysql 读写分离

    实现 mysql 读写分离 图解: 环境: iptables 和 selinux 关闭 proxy:test2 172.25.1.2 Master: test3 172.25.1.3 Slave:te ...

  7. 项目性能优化(MySQL读写分离、MySQL主从同步、Django实现MySQL读写分离)

    当项目中数据库表越来越多,数据量也逐渐增多时,需要做数据库的安全和性能的优化.对于数据库的优化,可以选择使用MySQL读写分离实现. 1.MySQL主从同步 1.主从同步机制 1.1.主从同步介绍和优 ...

  8. asp.net mysql 读写分离_MySQL读写分离

    MySQL读写分离 1,为啥要读写分离? 系统到了高并发阶段,数据库一定要做的读写分离了,因为大部分的项目都是读多写少.所以针对这个情况,把写操作放一个主库,主库下挂多个从库处理读操作,这样就可以支撑 ...

  9. mysql读写分离,主从配置

    2019独角兽企业重金招聘Python工程师标准>>> 一个完整的mysql读写分离环境包括以下几个部分: 应用程序client database proxy database集群 ...

  10. php mysql读写分离主从复制_mysql主从复制 读写分离原理及实现

    主从复制,读写分离原理 在实际的生产环境中,对数据库的读和写都在同一个数据库服务器中,是不能满足实际需求的.无论是在安全性.高可用性还是高并发等各个方面都是完全不能满足实际需求的.因此,通过主从复制的 ...

最新文章

  1. Oracle视图添加约束,Oracle的约束视图
  2. Gartner:2018年十大科技趋势与其对IT和执行的影响
  3. python能做游戏吗-python能开发游戏吗
  4. APUE-文件和目录(二)函数access,mask,chmod和粘着位
  5. 老男孩为网友工作疑难问题解答一例
  6. 数值保留几位小数后四舍五入、向上取值、向下取值、
  7. int main(int argc,char* argv[])详解
  8. 通过hsv筛选颜色 python_OpenCV-Python 光流介绍(附代码)
  9. 安装java错误_安装JAVA JDK错误提示正在进行另一JAVA安装解决方法
  10. QTcpServer. QTcpSocket. QUdpSocket之间的区别
  11. 低代码开发平台+KM知识文档管理系统搭配的好处
  12. UI设计---化繁为简
  13. 数据库设计3个泛式和经验谈
  14. php c端,蛋白测序(N端,C端测序)
  15. 浅谈云计算和大数据技术
  16. python阴阳师脚本_阴阳师肝不动了,试试Python吧
  17. uint64 和字符串相互转换
  18. 学人工智能要看什么书?AI入门到进阶10本必看书
  19. PPT中如何修改插入的图片为透明色
  20. 什么是大数据采集?大数据采集的过程是什么?

热门文章

  1. 如何将两个mp3文件合成一个?
  2. 人工智能在围棋程序中的应用
  3. 2022年10款好用免费数据恢复软件分享
  4. macOS:卸载JRE或JDK
  5. 第21期状元简讯:自贸区首个跨境电商平台将上线
  6. uchome持久XSS(2.0版本测试通过)
  7. 戴尔服务器预装系统如何降级,在戴尔计算机上降级系统BIOS | Dell 中国
  8. 简述et代理换ip软件网络功能。
  9. <自由之路>LeetCode每日一题(DFS + 记忆化搜索)
  10. CS 3:威胁情报解决方案峰会——数据是威胁情报的基础