SpringBoot整合实现MySQL读写分离原来如此简单


封面

一、为什么要用读写分离,有哪些使用场景?

  1. 高并发读取场景:当应用程序有大量读取操作,但相对较少的写入操作时,使用读写分离可以将读取操作分散到多个从库,提高系统的读取性能和并发能力。
  2. 数据报表和分析场景:在需要生成复杂报表或进行数据分析的场景中,读写分离可以将读取操作与写入操作分开,确保报表和分析任务不会影响主库的写入性能。
  3. 全球分布式架构:对于全球性的应用程序,使用读写分离可以将读取操作路由到离用户更近的从库,减少网络延迟,提高用户体验。
  4. 容灾和高可用性:通过将数据复制到多个从库,读写分离可以提供容灾和高可用性。当主库发生故障时,可以快速切换到可用的从库,确保业务的持续运行。
  5. 降低数据库负载:通过将读取操作分散到从库,读写分离可以降低主库的负载压力,提高主库的写入性能和吞吐量。
    需要注意的是,读写分离并非适用于所有场景。对于对数据一致性要求非常高的应用程序,可能需要额外的同步机制来保证主库和从库之间的数据一致性。此外,读写分离还需要考虑数据库同步延迟和数据更新策略等因素。

二、读写分离有什么好处?

  1. 提高性能和可扩展性:通过将读操作分配给只负责读取的从库,可以将读操作的负载均衡分散到多个数据库实例上,从而提高读取性能和系统的可扩展性。
  2. 降低主库压力:将读操作从主库转移到从库,可以减轻主库的负载压力,让主库专注于处理写操作。这可以提高主库的写入性能和吞吐量。
  3. 提高数据可用性和容灾能力:通过使用多个从库进行数据复制,可以提高系统的容灾能力。当主库发生故障时,可以快速切换到从库,确保业务的持续运行。
  4. 灵活的数据分发:读写分离允许根据应用程序需求将不同类型的读操作分发到特定的从库。例如,可以将只读查询路由到离用户更近的从库,以减少网络延迟并提供更好的用户体验。
  5. 降低数据库锁竞争:读写分离可以减少主库上的并发写操作,从而减少数据库锁竞争的可能性,提高整体系统的并发能力。

总体而言,MySQL读写分离可以通过优化读操作的负载分布、提高性能和可用性、降低主库压力和提供灵活的数据分发,从而提升应用程序的性能和可伸缩性,并改善用户体验。
总的来说,就是降压提性能

三、读写分离分析

技术选型

  • SpringBoot
  • MybatisPlus
  • MySQL

基本原理逻辑

  1. 配置主从数据库
  2. 创建数据源配置类
  3. 创建动态数据源
  4. 配置MyBatis-Plus
  5. 事务管理配置

我自己的理解就是,先在服务器上做主从复制,保证数据的一致性。再通过SpringBoot与MyBatisPlus配置DataSource(master与slave两种),创建一个动态数据源类,例如DynamicDataSource,继承自AbstractRoutingDataSource。创建一个SqlSessionFactory bean,并设置数据源为动态数据源。创建一个DataSourceTransactionManager bean,并设置数据源为动态数据源。通过AOP+自定义注解,使得读写可以在业务中进行配置。

四、具体代码实现

我的mysql是在云服务器上,通过docker部署的,所以下面的将用此方法,但是整体逻辑大同小异

First. MySQL主从复制

前提: 已经使用Docker搭建了两个数据库了,一主一从。版本相同,端口不同,都启动了二进制日志(Binary Log),同时主库和从库之间需要具备可靠的网络连接。

① 修改my.cnf

主库与从库my.cnf中的server-id 需要不同,保证MySQL实例的唯一性。该配置在[mysqld]模块中。如下图所示:
my.cnf配置

② 配置主库

首先需要查看当前数据库正在写入的二进制日志文件中的名称与位置。

show master status;

二进制日志文件
进行配置:

CHANGE MASTER TO
  MASTER_HOST='120.46.129.14',
  MASTER_PORT=3389,
  MASTER_USER='root',
  MASTER_PASSWORD='root_886YJ',
  MASTER_LOG_FILE='mysql_bin.000003',
  MASTER_LOG_POS=154;

配置
查看是否状态

SHOW SLAVE STATUS\G

在这里插入图片描述
出现了两个Yes即为成功

Second. 前提准备

  1. 创建数据表,作为测试
DROP TABLE IF EXISTS `user`; 

CREATE TABLE `user` ( 
  `user_id` bigint(20) NOT NULL COMMENT '用户id', 
  `user_name` varchar(255) DEFAULT '' COMMENT '用户名称', 
  `user_phone` varchar(50) DEFAULT '' COMMENT '用户手机', 
  `address` varchar(255) DEFAULT '' COMMENT '住址', 
  `weight` int(3) NOT NULL DEFAULT '1' COMMENT '权重,大者优先', 
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 
  PRIMARY KEY (`user_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
 
INSERT INTO `user` VALUES ('1196978513958141952', '测试1', '18826334748', '广州市海珠区', '1', '2019-11-20 10:28:51', '2019-11-22 14:28:26'); 
INSERT INTO `user` VALUES ('1196978513958141953', '测试2', '18826274230', '广州市天河区', '2', '2019-11-20 10:29:37', '2019-11-22 14:28:14'); 
INSERT INTO `user` VALUES ('1196978513958141954', '测试3', '18826273900', '广州市天河区', '1', '2019-11-20 10:30:19', '2019-11-22 14:28:30'); 
  1. 创建SpringBoot项目,创建对应实体类,配置pom文件,填写yml文件

User实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
 * @author LukeZhang
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @TableId(type = IdType.AUTO)
    private Long userId;
    private String userName;
    private String userPhone;
    private String address;
    private Integer weight;
    private Date createdAt;
    private Date updatedAt;
}

pom配置-依赖部分

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- Druid连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
    </dependencies>

yml配置

server: 
  port: 8002
spring: 
  jackson: 
   date-format: yyyy-MM-dd HH:mm:ss 
   time-zone: GMT+8 
  datasource: 
    type: com.alibaba.druid.pool.DruidDataSource
    master:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://<yourip>:<yourport>/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: <yourusername>
      password: <yourpassword>
    slave:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://<yourip>:<yourport>/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: <yourusername>
      password: <yourpassword>

mapper创建

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.rwdemo.domain.User;
/**
 * @author LukeZhang
 * @date 2023/6/14 16:35 
 */
public interface UserMapper extends BaseMapper<User> {
}

Third. 实现动态数据源

  1. 枚举类方便区分主从
import lombok.Getter;
/**
 * @author LukeZhang
 */
@Getter
public enum DynamicDataSourceEnum {
    MASTER("master"),
    SLAVE("slave");
    private String dataSourceName;
    DynamicDataSourceEnum(String dataSourceName) {
        this.dataSourceName = dataSourceName;
    }
}
  1. 创建对应实体类接收参数
    MasterDataBase
/**
 * @author LukeZhang
 */
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.master")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Master {
    private String url;
    private String driverClassName;
    private String username;
    private String password;
}

SlaveDataBase

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
 * @author LukeZhang
 */
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.slave")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Slave {
    private String url;
    private String driverClassName;
    private String username;
    private String password;
}

  1. 配置动态数据源
/**
 * @author LukeZhang
 * @date 2023/6/14 16:37 
 */
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.example.rwdemo.domain.DynamicDataSourceEnum;
import com.example.rwdemo.holder.DynamicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@MapperScan("com.example.rwdemo.mapper")
public class DataSourceConfig {
    @Autowired
    private Master master;
    @Autowired
    private Slave slave;
    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().url(master.getUrl()).driverClassName(master.getDriverClassName()).password(master.getPassword()).username(master.getUsername()).build();
    }
    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .url(slave.getUrl()).driverClassName(slave.getDriverClassName()).username(slave.getUsername()).password(slave.getPassword())
                .build();
    }
    /**
     * 主从动态配置
     */
    @Bean
    public DynamicDataSource dynamicDb(@Qualifier("masterDataSource") DataSource masterDataSource,
                                       @Autowired(required = false) @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
        if (slaveDataSource != null) {
            targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
        }
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDb") DataSource dynamicDataSource) throws Exception {
        MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dynamicDataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
        return sessionFactoryBean.getObject();
    }
    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDb") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

  1. 通过aop方便实现

注解获取信息

import com.example.rwdemo.domain.DynamicDataSourceEnum;
import java.lang.annotation.*;
/**
 * @author LukeZhang
 * @date 2023/6/14 16:40 
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSelector {
    DynamicDataSourceEnum value() default DynamicDataSourceEnum.MASTER;
    boolean clear() default true;
}

编写AOP配置

import com.example.rwdemo.annotation.DataSourceSelector;
import com.example.rwdemo.holder.DataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
 * @author LukeZhang 
 */
@Slf4j 
@Aspect 
@Order(value = 1) 
@Component 
public class DataSourceContextAop {
         @Around("@annotation(com.example.rwdemo.annotation.DataSourceSelector)")
            public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
                boolean clear = true; 
                try { 
                    Method method = this.getMethod(pjp);
                    DataSourceSelector dataSourceImport = method.getAnnotation(DataSourceSelector.class);
                    clear = dataSourceImport.clear(); 
                    DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());
                    log.info("========数据源切换至:{}", dataSourceImport.value().getDataSourceName()); 
                    return pjp.proceed(); 
                } finally { 
                    if (clear) { 
                        DataSourceContextHolder.clear(); 
                    } 
         
                } 
            } 
            private Method getMethod(JoinPoint pjp) {
                MethodSignature signature = (MethodSignature)pjp.getSignature();
                return signature.getMethod(); 
            } 
         
        } 

Fourth. 编写服务与测试

  1. 编写UserService
import com.example.rwdemo.annotation.DataSourceSelector;
import com.example.rwdemo.domain.DynamicDataSourceEnum;
import com.example.rwdemo.domain.User;
import com.example.rwdemo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * @author LukeZhang
 * @date 2023/6/14 16:45 
 */
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @DataSourceSelector(value = DynamicDataSourceEnum.MASTER)
    public int update(Long userId) {
        User user = new User();
        user.setUserId(userId);
        user.setUserName("老薛");
        return userMapper.updateById(user);
    }
    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public User find(Long userId) {
        return userMapper.selectById(userId);
    }
} 
  1. 编写测试

import com.example.rwdemo.domain.User;
import com.example.rwdemo.mapper.UserMapper;
import com.example.rwdemo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
/**
 * @author LukeZhang
 * @date 2023/6/14 16:45 
 */
@SpringBootTest
class UserServiceTest {
    @Autowired
    UserService userService;
    @Autowired
    UserMapper userMapper;
    @Test
    void find() { 
        User user = userService.find(1196978513958141952L);
        System.out.println("id:" + user.getUserId()); 
        System.out.println("name:" + user.getUserName()); 
        System.out.println("phone:" + user.getUserPhone()); 
    }
    @Test 
    void update() { 
        Long userId = 1196978513958141952L;
        int update = userService.update(userId);
        System.out.println("update执行"+update);
    }
    @Test
    public void test() {
        List<User> users = userMapper.selectList(null);
        users.forEach(
                user -> {
                    System.out.println(user.toString());
                }
        );
    }
} 

结果

参考

读写分离原来这么简单,一个小注解就够了
友情支持————》chatgpt

总结

第一篇博客!接下来会把自己学到的东西慢慢的记录下来,也算是自己的一个成长记录。同时也方便自己查阅以及和大家一起学习探讨。

  • 8
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值