• Spring Boot项目优雅实现读写分离



    在这里插入图片描述

    🎉欢迎来到架构设计专栏~Spring Boot项目优雅实现读写分离



    Spring Boot作为一种快速开发框架,广泛应用于Java项目中。在一些大型应用中,数据库的读写分离是提升性能和扩展性的一种重要手段。本文将介绍如何在Spring Boot项目中优雅地实现读写分离,并通过适当的代码插入,详细展开实现步骤,同时进行拓展和分析。
    在这里插入图片描述

    1. 读写分离简介

    读写分离是指在数据库集群中,将数据库的读操作和写操作分别分配到不同的节点上。这样可以充分利用多台服务器的资源,提高系统的并发处理能力。一般来说,写操作相对读操作更为耗时,通过读写分离,可以有效减轻主库的负担。

    2. Spring Boot集成MyBatis

    首先,我们需要在Spring Boot项目中集成MyBatis,作为数据库访问的ORM框架。可以通过在pom.xml文件中添加依赖来引入MyBatis和MyBatis-Spring-Boot-Starter:

    
    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.2.0version>
    dependency>
    
    
    <dependency>
        <groupId>com.zaxxergroupId>
        <artifactId>HikariCPartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    然后,配置application.properties文件,指定数据库连接信息:

    # 主库
    spring.datasource.master.url=jdbc:mysql://master-host:3306/master_db
    spring.datasource.master.username=root
    spring.datasource.master.password=root
    # 从库
    spring.datasource.slave.url=jdbc:mysql://slave-host:3306/slave_db
    spring.datasource.slave.username=root
    spring.datasource.slave.password=root
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3. 配置读写分离数据源

    在实现读写分离前,我们需要定义一个数据源路由器,用于根据不同的操作选择不同的数据源。在Spring Boot中,可以通过AbstractRoutingDataSource实现这一功能。以下是一个简单的数据源路由器示例:

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    public class RoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDataSourceType();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上述代码中,determineCurrentLookupKey方法用于确定当前使用的数据源,DataSourceContextHolder是一个自定义的上下文持有类,用于存储当前线程使用的数据源类型。接下来,我们需要在配置类中配置这个数据源路由器:

    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    public class DataSourceConfig {
    
        @Bean(name = "masterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "slaveDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "routingDataSource")
        public RoutingDataSource routingDataSource(
                @Qualifier("masterDataSource") DataSource masterDataSource,
                @Qualifier("slaveDataSource") DataSource slaveDataSource) {
            RoutingDataSource routingDataSource = new RoutingDataSource();
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put(DataSourceType.MASTER, masterDataSource);
            targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
            routingDataSource.setTargetDataSources(targetDataSources);
            routingDataSource.setDefaultTargetDataSource(masterDataSource);
            return routingDataSource;
        }
    
        @Bean
        public PlatformTransactionManager transactionManager(DataSource routingDataSource) {
            return new DataSourceTransactionManager(routingDataSource);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在上述代码中,我们配置了两个数据源,一个用于主库,一个用于从库。然后,通过routingDataSource方法创建了一个RoutingDataSource实例,将主库和从库加入到数据源路由器中,并设置默认数据源为主库。

    4. 定义数据源上下文

    接下来,我们需要定义一个数据源上下文类,用于在当前线程中保存和获取当前使用的数据源类型。这个上下文类应该是线程安全的,因为它会在多个线程中被访问。

    public class DataSourceContextHolder {
    
        private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
    
        public static void setDataSourceType(DataSourceType dataSourceType) {
            contextHolder.set(dataSourceType);
        }
    
        public static DataSourceType getDataSourceType() {
            return contextHolder.get();
        }
    
        public static void clearDataSourceType() {
            contextHolder.remove();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5. 自定义注解和切面

    为了在Service层标注读操作和写操作,我们可以定义两个自定义注解@Master@Slave,并创建一个切面DataSourceAspect,通过AOP切入点拦截被这两个注解标记的方法,然后在方法执行前设置数据源类型。

    @Aspect
    @Component
    public class DataSourceAspect {
    
        @Before("@annotation(master)")
        public void setMasterDataSource(JoinPoint joinPoint, Master master) {
            DataSource
    
    ContextHolder.setDataSourceType(DataSourceType.MASTER);
        }
    
        @Before("@annotation(slave)")
        public void setSlaveDataSource(JoinPoint joinPoint, Slave slave) {
            DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
        }
    
        @After("@annotation(master) || @annotation(slave)")
        public void clearDataSourceType(JoinPoint joinPoint, Master master, Slave slave) {
            DataSourceContextHolder.clearDataSourceType();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在上述代码中,通过@Before注解定义了两个切入点,分别拦截被@Master@Slave注解标记的方法,在方法执行前设置对应的数据源类型。在@After注解中清除当前线程的数据源类型。

    6. 在Service层使用注解

    最后,在Service层需要进行读写分离的方法上使用定义好的注解,标记读操作和写操作。以下是一个示例:

    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Master
        public User getUserById(int userId) {
            return userMapper.selectById(userId);
        }
    
        @Slave
        public List<User> listAllUsers() {
            return userMapper.selectAll();
        }
    
        @Master
        public void saveUser(User user) {
            userMapper.insert(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在上述示例中,@Master@Slave注解分别用于标记读操作和写操作的方法。在实际应用中,根据具体需求和业务场景进行灵活使用。

    7. 拓展与分析

    7.1 多数据源的选择

    上述示例中使用了两个数据源,一个用于主库,一个用于从库。在实际应用中,如果有多个从库,可以在配置类中配置多个从库数据源,然后在数据源路由器中动态选择。

    7.2 事务的处理

    在涉及到事务的场景中,需要注意对事务的处理。在使用读写分离的情况下,一般将写操作放在事务中,而读操作不放在事务中。因为事务一般需要使用主库,而从库主要用于读取操作,不参与事务的提交与回滚。

    7.3 异常处理

    在使用读写分离的过程中,可能会遇到主从同步延迟导致的数据不一致问题。因此,在涉及到数据一致性要求较高的业务场景中,需要谨慎使用读写分离,考虑一些其他的解决方案,如数据的多版本控制等。

    7.4 动态数据源切换

    上述示例中使用AOP切面在方法执行前设置数据源类型。在某些场景中,可能需要在代码中动态切换数据源,这时可以通过编程式的方式设置数据源类型,而不是依赖AOP。

    7.5 Spring Boot版本适配

    请注意根据使用的Spring Boot版本来选择相应的依赖版本。在示例中,使用的MyBatis版本是2.2.0,如果使用的是较新的Spring Boot版本,建议查阅官方文档或相关依赖库的最新版本。

    在这里插入图片描述

    通过上述步骤,我们完成了Spring Boot项目中读写分离的优雅实现。通过合理的代码插入,详细展开了每个步骤的实现,并对一些拓展和分析进行了说明。希望这篇文章对正在进行数据库优化的开发者有所帮助。


    🧸结尾 ❤️ 感谢您的支持和鼓励! 😊🙏
    📜您可能感兴趣的内容:

    在这里插入图片描述

  • 相关阅读:
    6、Spring cloud注册中心之consul
    【Linux 基础】df -h 的输出信息解读
    ffmpeg云服务器推流
    PYTHON 实现 UNIX/LINUX 性能监视可视化
    LeetCode //C - 79. Word Search
    DP232在兼容FT232RL的注意事项
    js文件模块化引用问题(JavaScript modules)
    数据中台方案分析和发展方向
    前端埋点方式
    代码随想录算法训练营第三十二天 | 动态规划理论基础 509.斐波那契数 70.爬楼梯 746.使用最小花费爬楼梯
  • 原文地址:https://blog.csdn.net/qq_43546721/article/details/134395015