MySQL主从数据库(主读从写)
创始人
2024-02-02 20:58:53

MySQL多数据源

  • MySQL主从数据库(主读从写)
    • 1. 多数据源的实现原理
      • 1.1 配置多数据源yml文件
      • 1.2 创建配置类
      • 1.3 动态切换数据源类继承AbstractRoutingDataSource 类
      • 1.4 测试类测试
      • 1.5 附枚举代码
      • 1.6 总结
    • 2. 多数据源切换方式(优化)
      • 2.1 MyBatis插件(读写分离)
        • 2.1.1 创建插件实现数据源的动态切换
        • 2.1.2 在配置类中添加数据源动态切换的插件bean对象
      • 2.2 AOP方式切换数据源(业务复杂数据量大)
        • 2.2.1 使用AOP首先需要程序的核心启动类上添加注解
        • 2.2.2 自定义注解
        • 2.2.3 切面类
        • 2.2.4 使用注解

MySQL主从数据库(主读从写)

带薪学习:本文主要介绍主从数据库,在SpringBoot项目中我们需要连接多个数据源,多个数据库可能存在不同的服务上边,比如张三对MySQLA数据库只有读取数据的权限,对MySQLB数据库只有写数据的权限。这样的话一个项目中读取数据就要配置A的数据源,写入数据要配置B的数据源,这样就构成了多数据源切换问题。

1. 多数据源的实现原理

对于大多数的Java应用,都使用了spring架,spring-jdbc模块提供了AbstractRoutingDataSource,其内部可以包含了多个DataSoure,,然后在运行时来动态切换的访问数据库。这种方式对访问数据库的架构图如下所示:

在这里插入图片描述

应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(dbs1、dbs2)选择一个,来访问对应的数据库。

在这里插入图片描述

  1. 配置多数据源和AbstractRoutingDataSource的自定义实现类:DynamicDataSource

1.1 配置多数据源yml文件

application.yaml


spring:datasource:#主数据源master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://----:7788/guoguo?characterEncoding=utf-8&serviceTimezone=UTCusername: rootpassword: roottype: com.alibaba.druid.pool.DruidDataSourceinitialSize: 10minIdle: 10maxActive: 30#从数据库slave:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://-----.mysql.rds.aliyuncs.com:3306/platform_crawler?characterEncoding=utf-8&serviceTimezone=UTCusername: -----password: -----type: com.alibaba.druid.pool.DruidDataSourceinitialSize: 10minIdle: 10maxActive: 30masterslave:# 读写分离配置load-balance-algorithm-type: round_robin# 最终的数据源名称name: dataSource# 主库数据源名称master-data-source-name: master# 从库数据源名称列表,多个逗号分隔slave-data-source-names: slaveprops:# 开启SQL显示,默认falsesql:show: true

1.2 创建配置类

DataSourceConfig.java

package com.guo.mysql.config;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;@Slf4j
@Configuration
public class DataSourceConfig {/*** 主数据源* @return DataSource*/@Bean@ConfigurationProperties(prefix = "spring.datasource.master")public DataSource masterDataSource() {//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasourcereturn DruidDataSourceBuilder.create().build();}/*** 从 数据源* @return DataSource*/@Bean  //@ConfigurationProperties(prefix = "spring.datasource.slave")public DataSource slaveDataSource() {//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasourcereturn DruidDataSourceBuilder.create().build();}
}

1.3 动态切换数据源类继承AbstractRoutingDataSource 类

DynamicDataSource.java

package com.guo.mysql;import com.guo.mysql.enums.DataSourceEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;@Component
@Primary   //将该Bean设置为主要注入Bean  implements DataSource, InitializingBean
public class DynamicDataSource extends AbstractRoutingDataSource {//当前使用的数据源标识public static ThreadLocal name = new ThreadLocal<>();//主写@AutowiredDataSource masterDataSource;//从读@AutowiredDataSource slaveDataSource;//返回当前数据源标识@Overridepublic Object determineCurrentLookupKey() {return name.get();}/*** spring容器启动的时候被调用*/@Overridepublic void afterPropertiesSet() {//为targetDataSources初始化所有数据源Map targetDataSources = new HashMap<>();targetDataSources.put(DataSourceEnum.主库.getName(), masterDataSource);targetDataSources.put(DataSourceEnum.从库.getName(), slaveDataSource);super.setTargetDataSources(targetDataSources);//为defaultTargetDataSource设置默认的数据源super.setDefaultTargetDataSource(slaveDataSource);   //从读super.afterPropertiesSet();}
}

1.4 测试类测试

SpringbootSynchronizeMysqlApplicationTests.java
测试类,根据指定数据库名称,切换数据源连接,进行操作不同的数据库

package com.guo.mysql;import com.guo.mysql.enums.DataSourceEnum;
import com.guo.mysql.service.UserOsskeyService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class SpringbootSynchronizeMysqlApplicationTests {@Testvoid contextLoads() {}@Autowiredprivate UserOsskeyService userOsskeyService;@Testpublic void test(){DynamicDataSource.name.set(DataSourceEnum.从库.getName());System.out.println(userOsskeyService.count());DynamicDataSource.name.set(DataSourceEnum.主库.getName());System.out.println(userOsskeyService.count());}
}

输出:

2022-11-15 11:03:11.165  INFO 11692 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
569548
2022-11-15 11:03:20.525  INFO 11692 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} inited
1

1.5 附枚举代码

DataSourceEnum.java
为了提高代码的可用性,在代码中切换数据源按照名称切换,这里把名称写在枚举中,后续如有修改直接在枚举类中修改就可以了,不需要去每个类中去修改,提高代码的可用性

package com.guo.mysql.enums;import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;public enum DataSourceEnum {主库("master", "w"),从库("slave", "r"),NULL("-1", "");private String name;private String exec;DataSourceEnum(String name, String exec) {this.name = name;this.exec = exec;}public String getName() {return name;}public String getExec() {return exec;}private static final Map codeMap = new HashMap<>((int) (values().length / .75f) + 1);static {for(DataSourceEnum item : values()) {codeMap.put(item.name, item);}}public static DataSourceEnum fromCode(String name) {DataSourceEnum item = codeMap.get(name);return item == null ? NULL : item;}public static Collection all() {EnumSet enumSet = EnumSet.allOf(DataSourceEnum.class);enumSet.remove(NULL);return enumSet;}
}

1.6 总结

以上代码虽然可以实现基本数据源的切换,还存在一些问题

  1. 数据源表示设置代码耦合性过高,对后期维护造成很大的麻烦

2. 多数据源切换方式(优化)

  1. MyBatis插件(读写分离)
  2. AOP方式切换数据源(业务复杂数据量大)

2.1 MyBatis插件(读写分离)

为了解决数据库的读性能瓶颈(读比写性能更高,写锁会影响读阻塞,从而影响读的性能)
很对数据拥有主从架构,也就是,一台数据库服务器,是对外提供增删改业务的生产服务器;另外一(多)台数据库服务器,主要进行操作。
可以通过中间件(ShardingSphere, mycat,mysql-proxy,TDDL …)

这里的架构上类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert,update,delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力
在这里插入图片描述

2.1.1 创建插件实现数据源的动态切换

DynamicDataSourcePlugin.java

package com.guo.mysql.plugin;import com.guo.mysql.DynamicDataSource;
import com.guo.mysql.enums.DataSourceEnum;
import org.apache.ibatis.executor.Executor;
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.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.util.Properties;
//Mybatis拦截器注解
//@Intercepts:标识该类是一个拦截器
//@Signature:拦截器相关属性设置
@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})
})
public class DynamicDataSourcePlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {//拿到当前方法(update, query)所有参数Object[] objects = invocation.getArgs();//MappedStatement 封装SQLMappedStatement ms = (MappedStatement) objects[0];//读方法if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){DynamicDataSource.name.set(DataSourceEnum.从库.getName());}else {//写操作DynamicDataSource.name.set(DataSourceEnum.主库.getName());}// 修改当前线程要选择的数据源的Keyreturn invocation.proceed();}@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {Interceptor.super.setProperties(properties);}
}

2.1.2 在配置类中添加数据源动态切换的插件bean对象

package com.guo.mysql.config;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.guo.mysql.plugin.DynamicDataSourcePlugin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.plugin.Interceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;@Slf4j
@Configuration
public class DataSourceConfig {/*** 主数据源* @return DataSource*/@Bean@ConfigurationProperties(prefix = "spring.datasource.master")public DataSource masterDataSource() {//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasourcereturn DruidDataSourceBuilder.create().build();}/*** 从 数据源* @return DataSource*/@Bean  //@ConfigurationProperties(prefix = "spring.datasource.slave")public DataSource slaveDataSource() {//底层自动拿到Spring.datasource中的配置,创建一个DruidDatasourcereturn DruidDataSourceBuilder.create().build();}//mybatisplus 注入切换数据源的插件bean对象@Beanpublic Interceptor dynamicDataSourcePlugin(){return new DynamicDataSourcePlugin();}
}

核心代码解读

//拿到当前方法(update, query)所有参数
Object[] objects = invocation.getArgs();
//MappedStatement 封装SQL
MappedStatement ms = (MappedStatement) objects[0];//读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){DynamicDataSource.name.set(DataSourceEnum.从库.getName());
}else {//写操作DynamicDataSource.name.set(DataSourceEnum.主库.getName());
}

通过学习spring底层源码了解到,当我们操作查询SELECT操作的时候调用从库(从库读取数据),操作添加更新删除操作的时候 INSERT, UPDATE, DELETE,调用我们的主库(主库用来增删改操作);
通过上述判断修改当前线程要选择的数据源的Key

2.2 AOP方式切换数据源(业务复杂数据量大)

数据分布在不同的数据库中,数据库拆了,应用没有拆,一个公司多个子项目,各用各分数据库,涉及数据共享…

不同业务的数据源:一般利用AOP,结合自定义注解动态切换数据源
AOP+自定义注解
在这里插入图片描述

2.2.1 使用AOP首先需要程序的核心启动类上添加注解

@EnableAspectJAutoProxy //启动AOP

package com.guo.mysql;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@SpringBootApplication
@EnableAspectJAutoProxy  //启动AOP
public class SpringbootSynchronizeMysqlApplication {public static void main(String[] args) {SpringApplication.run(SpringbootSynchronizeMysqlApplication.class, args);}
}

2.2.2 自定义注解

WR.java

package com.guo.mysql.annotation;import com.guo.mysql.enums.DataSourceEnum;import javax.xml.bind.Element;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定义注解* 允许在方法和类型上使用该注解*/
@Target({ElementType.METHOD,ElementType.TYPE})
//保留方式   SOURCE不会编译在class文件中   CLASS:会编译不会被JVM加载,通过反射获取不到   RUNTIME:可以通过反射调用
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {String value() default "slave";   //默认是从库(大多数场景都是在读取数据)
}

2.2.3 切面类

DynamicDataSourceAspect .java

package com.guo.mysql.aspect;import com.guo.mysql.DynamicDataSource;
import com.guo.mysql.annotation.WR;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;@Component
@Aspect
public class DynamicDataSourceAspect {//前置通知   within指定包下的所有类@Before("within(com.guo.mysql.service.impl.*) && @annotation(wr)")public void before(JoinPoint point, WR wr){String name = wr.value();DynamicDataSource.name.set(name);System.out.println(name);}
}

2.2.4 使用注解

上边切面类中自定义切面的时指定的包为接口实现类中,所以这里需要在指定的包下使用注解,实现数据源的动态切换

package com.guo.mysql.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guo.mysql.annotation.WR;
import com.guo.mysql.entity.UserOsskey;
import com.guo.mysql.mapper.UserOsskeyMapper;
import com.guo.mysql.service.UserOsskeyService;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;@Service
public class UserOsskeyServiceImpl extends ServiceImpl implements UserOsskeyService {@Resourceprivate UserOsskeyMapper userOsskeyMapper;// 从库查询@Override@WR("slave")public List queryList(QueryWrapper queryWrapper) {return userOsskeyMapper.selectList(queryWrapper);}//主库插入@Override@WR("master")public int saveData(UserOsskey userOsskey) {return userOsskeyMapper.insert(userOsskey);}
}

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...