SpringBoot+MybatisPlus实现读写分离,自动切换数据源,主从同步

读写分离有必要吗?

实现读写分离势必要与你所做的项目相关,如果项目读多写少,那就可以设置读写分离,让“读”可以更快,因为你可以把你的“读”数据库的innodb设置为MyISAM引擎,让MySQL处理速度更快。

实现读写分离的步骤

监听MybatisPlus接口,判断是写入还是读取

在这里我使用的是AOP的方式,动态监听MybatisPlus中Mapper的方法。

import com.supostacks.wrdbrouter.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyBatisPlusAop {

    @Pointcut("execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.select*(..))")
    public void readPointCut(){
    }
	
    @Pointcut("execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.insert*(..))" +
              "||execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.update*(..))" +
              "||execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.delete*(..))")
    public void writePointCut(){
    }

    @Before("readPointCut()")
    public void readBefore(){
        DBContextHolder.setDBKey("dataread");
    }

    @Before("writePointCut()")
    public void writeBefore(){
        DBContextHolder.setDBKey("datawrite");
    }
}

定义介绍:
DBContextHolder中使用了ThreadLocal存储数据库名
readPointCut定义读的切点,如果调用的是BaseMapper.select*(…)则判断是读数据,则调用读库。
writePointCut定义写的切点,如果调用的是BaseMapper.insert|update|delete*(…)则判断是写数据,则调用写库

自定义MyBatis的DataSourceAutoConfiguration

DataSourceAutoConfiguration是Mybatis官方使用的SpringBootStarter,因为我这边自定义了Mybatis连接的相关属性名用来切换数据源,所以我需要自构一个DataSourceAutoConfig,代码如下:


@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {

    private final String TAG_GLOBAL = "global";
    /**
     * 数据源配置组
     */
    private final Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();

    /**
     * 默认数据源配置
     */
    private Map<String, Object> defaultDataSourceConfig;

    public DataSource createDataSource(Map<String,Object> attributes){
        try {
            DataSourceProperties dataSourceProperties = new DataSourceProperties();
            dataSourceProperties.setUrl(attributes.get("url").toString());
            dataSourceProperties.setUsername(attributes.get("username").toString());
            dataSourceProperties.setPassword(attributes.get("password").toString());

            String driverClassName = attributes.get("driver-class-name") == null ? "com.zaxxer.hikari.HikariDataSource" : attributes.get("driver-class-name").toString();
            dataSourceProperties.setDriverClassName(driverClassName);

            String typeClassName = attributes.get("type-class-name") == null ? "com.zaxxer.hikari.HikariDataSource" : attributes.get("type-class-name").toString();
            return dataSourceProperties.initializeDataSourceBuilder().type((Class<DataSource>) Class.forName(typeClassName)).build();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }

    @Bean
    public DataSource createDataSource() {
        // 创建数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        for (String dbInfo : dataSourceMap.keySet()) {
            Map<String, Object> objMap = dataSourceMap.get(dbInfo);
            // 根据objMap创建DataSourceProperties,遍历objMap根据属性反射创建DataSourceProperties
            DataSource ds = createDataSource(objMap);
            targetDataSources.put(dbInfo, ds);
        }

        // 设置数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        // db0为默认数据源
        dynamicDataSource.setDefaultTargetDataSource(createDataSource(defaultDataSourceConfig));

        return dynamicDataSource;
    }


    @Override
    public void setEnvironment(Environment environment) {
        String prefix = "wr-db-router.spring.datasource.";


        String datasource = environment.getProperty(prefix + "db");
        Map<String, Object> globalInfo = getGlobalProps(environment, prefix + TAG_GLOBAL);


        assert datasource != null;
        for(String db : datasource.split(",")){
            final String dbKey = prefix + db; //数据库列表
            Map<String,Object> datasourceProps = PropertyUtil.handle(environment,dbKey, Map.class);
            injectGlobals(datasourceProps, globalInfo);

            dataSourceMap.put(db,datasourceProps);
        }

        String defaultData = environment.getProperty(prefix + "default");
        defaultDataSourceConfig = PropertyUtil.handle(environment,prefix + defaultData, Map.class);
        injectGlobals(defaultDataSourceConfig, globalInfo);
    }

    public Map getGlobalProps(Environment env, String key){
        try {
            return PropertyUtil.handle(env,key, Map.class);
        } catch (Exception e) {
            return Collections.EMPTY_MAP;
        }
    }

    private void injectGlobals(Map<String,Object> origin,Map<String,Object> global){
        global.forEach((k,v)->{
            if(!origin.containsKey(k)){
                origin.put(k,v);
            }else{
                injectGlobals((Map<String, Object>) origin.get(k), (Map<String, Object>) global.get(k));
            }
        });
    }

DynamicDataSource 这个类继承了AbstractRoutingDataSource,通过获取ThreadLocal中的数据库名,动态切换数据源。

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Value("wr-db-router.spring.datasource.default")
    private String defaultDatasource;
    @Override
    protected Object determineCurrentLookupKey() {
        if(null == DBContextHolder.getDBKey()){
            return defaultDatasource;
        }else{
            return DBContextHolder.getDBKey();
        }
    }
}

我们通过重写determineCurrentLookupKey方法并设置对应的数据库名称,我们就可以实现切换数据源的功能了。

AbstractRoutingDataSource 主要源码如下:

 public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

...

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        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;
        }
    }

自定义MyBatisPlus的SpringBoot自动配置

MybatisPlus是默认使用的Mybatis的自带的DataSourceAutoConfiguration,但是我们已经将这个自定义了,所以我们也要去自定义一个MyBatisPlusAutoConfig,如果不自定义的话,系统启动将报错。代码如下:

@Configuration(
        proxyBeanMethods = false
)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfig.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MyBatisPlusAutoConfig  implements InitializingBean {
xxx
}

这个代码是直接拷贝了MyBatisPlusAutoConfiguration,只是将@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})改为了
@AutoConfigureAfter({DataSourceAutoConfig.class, MybatisPlusLanguageDriverAutoConfiguration.class})

这样启动就不会报错了。

其他步骤

上面这些开发完,就差不多可以实现数据库的动态切换从而实现读写分离了,不过其中有一个方法PropertyUtil,这是自定义的一个可以读取properties某个前缀下的所有属性的一个工具类。代码如下:


import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class PropertyUtil {
    private static int springBootVersion = 2;
    public static <T> T handle(final Environment environment,final String prefix,final Class<T> clazz){
        switch (springBootVersion){
            case 1:
                return (T) v1(environment,prefix);
            case 2:
                return (T) v2(environment,prefix,clazz);
            default:
                throw new RuntimeException("Unsupported Spring Boot version");

        }
    }

    public static Object v1(final Environment environment,final String prefix){
        try {
            Class<?> resolverClass = Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver");
            Constructor<?> resolverConstructor = resolverClass.getDeclaredConstructor(PropertyResolver.class);
            Method getSubPropertiesMethod = resolverClass.getDeclaredMethod("getSubProperties", String.class);
            Object resolverObject = resolverConstructor.newInstance(environment);
            String prefixParam = prefix.endsWith(".") ? prefix : prefix + ".";
            return getSubPropertiesMethod.invoke(resolverObject, prefixParam);
        } catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException
                       | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }

    }

    private static Object v2(final Environment environment, final String prefix, final Class<?> targetClass) {
        try {
            Class<?> binderClass = Class.forName("org.springframework.boot.context.properties.bind.Binder");
            Method getMethod = binderClass.getDeclaredMethod("get", Environment.class);
            Method bindMethod = binderClass.getDeclaredMethod("bind", String.class, Class.class);
            Object binderObject = getMethod.invoke(null, environment);
            String prefixParam = prefix.endsWith(".") ? prefix.substring(0, prefix.length() - 1) : prefix;
            Object bindResultObject = bindMethod.invoke(binderObject, prefixParam, targetClass);
            Method resultGetMethod = bindResultObject.getClass().getDeclaredMethod("get");
            return resultGetMethod.invoke(bindResultObject);
        }
        catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException
                     | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }
}

我将路由切换的功能逻辑单独拉成了一个SpringBootStarter,目录如下:
在这里插入图片描述
顺便介绍一下如何将以个项目在SpringBootStarter中自动装配
1.在resources中创建文件夹META-INF
2.创建spring.factories文件
3.在该文件中设置你需要自动装配的类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.xxx.wrdbrouter.config.DataSourceAutoConfig,\
  com.xxx.wrdbrouter.config.MyBatisPlusAutoConfig

主从同步配置方法

由于我自己只有一个服务器,于是我以本地为从库,云服务器为主库,两台MySQL服务器配置了主从同步。

一、修改主库的配置文件,my.cnf

# log config
log-bin = mysql-bin     #开启mysql的binlog日志功能
sync_binlog = 1         #控制数据库的binlog刷到磁盘上去 , 0 不控制,性能最好,1每次事物提交都会刷到日志文件中,性能最差,最安全
binlog_format = mixed   #binlog日志格式,mysql默认采用statement,建议使用mixed
expire_logs_days = 7                           #binlog过期清理时间
max_binlog_size = 100m                    #binlog每个日志文件大小
binlog_cache_size = 4m                        #binlog缓存大小
max_binlog_cache_size= 512m              #最大binlog缓存大
binlog-ignore-db=mysql #不生成日志文件的数据库,多个忽略数据库可以用逗号拼接,或者 复制这句话,写多行

auto-increment-offset = 1     # 自增值的偏移量
auto-increment-increment = 1  # 自增值的自增量
slave-skip-errors = all #跳过从库错误

2、修改后需要重启MySQL服务
3、建复制用户

CREATE USER repl_user IDENTIFIED BY 'repl_root';
CREATE USER 'repl_user'@'192.168.0.136' IDENTIFIED BY 'repl_root';

给复制用户进行复制授权

grant replication slave on *.* to 'repl_user'@'%';
grant replication slave on *.* to 'repl_user'@'192.168.0.136';

FLUSH PRIVILEGES;

修改复制用户对应的plugin

alter user 'repl_user'@'%' identified with mysql_native_password by 'repl_root';
alter user 'repl_user'@'192.168.0.136' identified with mysql_native_password by 'repl_root';

二、配置从库
1、修改从库配置文件

[mysqld]
server-id = 2
log-bin=mysql-bin
relay-log = mysql-relay-bin
##不同步的库表
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=test.%
replicate-wild-ignore-table=information_schema.%
replicate-wild-ignore-table=www_dwurl_site.%
replicate-wild-ignore-table=dragonwealths_co.%

2、进入从库MySQL,创建连接主库

change master to
master_host='xxxxx', ##主库IP地址
master_user='repl_user', ##复制用户
master_password='repl_root',##复制用户密码
master_port=3306,##主库port
master_log_file='mysql-bin.000009', ##主库最新的logbin文件
master_log_pos=21005389,##主库最新的logbin的position
master_retry_count=60,##重连次数
master_heartbeat_period=10000;##心跳

怎么获取master_log_file和master_log_pos?
在主库中输入show master status;
在这里插入图片描述
然后在从库中输入启动从库命令:start slave顺便说一下stop slave停止从库

接下来输入命令show slave status\G
在这里插入图片描述
只要Slave_IO_Running:Yes和Slave_SQL_Running:Yes,这样就表示已经主从同步成功了。

然后测试一下:
主库数据:
在这里插入图片描述

从库数据:
在这里插入图片描述
没有问题,但是昨天进行配置的时候Slave_SQL_Running是No,然后今天重新停止从库再重新配置一下change master to ,就没问题了。

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分离可以利用 SpringBootMybatisPlus 和 Druid 进行配置。下面是一个简单的实现过程: 1. 添加 MybatisPlus 和 Druid 的 Maven 依赖。 ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> ``` 2. 在配置文件中添加 Druid 数据相关配置。 ```yaml spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/db_name?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initialSize: 5 maxActive: 10 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,wall,log4j connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 ``` 3. 配置 MybatisPlus 的多数据功能。 ```java @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.MASTER.getType(), masterDataSource); targetDataSources.put(DataSourceType.SLAVE.getType(), slaveDataSource); return new DynamicDataSource(masterDataSource, targetDataSources); } } ``` 4. 创建一个 DataSourceHolder 类,用于保存当前线程的数据类型。 ```java public class DataSourceHolder { private static final ThreadLocal<String> HOLDER = new ThreadLocal<>(); public static String getDataSource() { return HOLDER.get(); } public static void setDataSource(String dataSource) { HOLDER.set(dataSource); } public static void clearDataSource() { HOLDER.remove(); } } ``` 5. 创建一个枚举类型 DataSourceType,用于表示数据类型。 ```java public enum DataSourceType { MASTER("master"), SLAVE("slave"); private final String type; DataSourceType(String type) { this.type = type; } public String getType() { return type; } } ``` 6. 创建一个 DynamicDataSource 类,继承 AbstractRoutingDataSource,用于动态切换数据。 ```java public class DynamicDataSource extends AbstractRoutingDataSource { private final Map<Object, Object> targetDataSources; public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultDataSource); this.targetDataSources = targetDataSources; super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { return DataSourceHolder.getDataSource(); } @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { this.targetDataSources.putAll(targetDataSources); super.setTargetDataSources(this.targetDataSources); super.afterPropertiesSet(); } @Override public void addTargetDataSource(Object key, Object dataSource) { this.targetDataSources.put(key, dataSource); super.setTargetDataSources(this.targetDataSources); super.afterPropertiesSet(); } } ``` 7. 创建一个 AopDataSourceConfig 类,用于配置切面,实现动态切换数据。 ```java @Configuration public class AopDataSourceConfig { @Bean public DataSourceAspect dataSourceAspect() { return new DataSourceAspect(); } } ``` ```java @Aspect public class DataSourceAspect { @Pointcut("@annotation(com.example.demo.annotation.Master) " + "|| execution(* com.example.demo.service..*.select*(..)) " + "|| execution(* com.example.demo.service..*.get*(..)) " + "|| execution(* com.example.demo.service..*.query*(..)) " + "|| execution(* com.example.demo.service..*.find*(..)) " + "|| execution(* com.example.demo.service..*.count*(..))") public void read() { } @Pointcut("execution(* com.example.demo.service..*.insert*(..)) " + "|| execution(* com.example.demo.service..*.update*(..)) " + "|| execution(* com.example.demo.service..*.delete*(..))") public void write() { } @Around("read()") public Object read(ProceedingJoinPoint joinPoint) throws Throwable { try { DataSourceHolder.setDataSource(DataSourceType.SLAVE.getType()); return joinPoint.proceed(); } finally { DataSourceHolder.clearDataSource(); } } @Around("write()") public Object write(ProceedingJoinPoint joinPoint) throws Throwable { try { DataSourceHolder.setDataSource(DataSourceType.MASTER.getType()); return joinPoint.proceed(); } finally { DataSourceHolder.clearDataSource(); } } } ``` 8. 在 Service 层的方法上使用 @Master 注解,表示强制使用主库。 ```java @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override @Master public boolean save(User user) { return super.save(user); } } ``` 这样就实现分离功能。需要注意的是,在使用 MybatisPlus 进行 CRUD 操作时,需要使用对应的 Service 方法,例如 selectList、selectPage、insert、updateById、deleteById,而不是直接调用 Mapper 方法。否则,数据切换将不会生效。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值