项目中实现读写分离

news/2024/6/23 19:57:40

 1、AbstractRoutingDataSource    

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

大概意思是:
AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。

AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。

实现逻辑:

定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。

2、具体实现

2.1、新建springboot项目,引入pom文件

 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--Swagger-UI API文档生产工具--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.7.0</version></dependency><!--配置读写所需--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--mybatis plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.0.1</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency></dependencies>

2.2、使用mybatis plus 生成实体与xml等代码
2.3、在spring boot 启动类上添加扫描mapper注解 - @MapperScan(“com.cdzs.mapper”)
2.4、在配置文件 application.yml中添加多个(我这里是两个)数据源的配置信息

spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdruid:first:url: jdbc:mysql://172.16.7.35:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimeZone=Asia/shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 123456second:url: jdbc:mysql://172.16.7.139:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimeZone=Asia/shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 123456

3、集成动态数据源模块

3.1、新建注解 CurDataSource 指定要使用的数据源

import java.lang.annotation.*;/*** 多数据源注解* <p/>* 指定要使用的数据源*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurDataSource {String name() default "";}

3.2、新建常量存储于获取数据源

/*** 增加多数据源,在此配置*/
public interface DataSourceNames {String FIRST = "first";String SECOND = "second";}

3.3、新建类 DynamicDataSource

DynamicDataSource扩展Spring的AbstractRoutingDataSource抽象类,重写 determineCurrentLookupKey() 方法

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;
import java.util.Map;/*** 扩展 Spring 的 AbstractRoutingDataSource 抽象类,重写 determineCurrentLookupKey 方法* 动态数据源* determineCurrentLookupKey() 方法决定使用哪个数据源*/
public class DynamicDataSource extends AbstractRoutingDataSource {/*** ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();/*** 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好** @param defaultTargetDataSource 默认数据源* @param targetDataSources       目标数据源*/public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return getDataSource();}public static void setDataSource(String dataSource) {CONTEXT_HOLDER.set(dataSource);}public static String getDataSource() {return CONTEXT_HOLDER.get();}public static void clearDataSource() {CONTEXT_HOLDER.remove();}}

3.4、新建多数据源配置类

配置多数据源的信息,生成多个(我这里是两个,对应application.properties中定义的数据源)数据源

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
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 DynamicDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.druid.first")public DataSource firstDataSource(){return DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.druid.second")public DataSource secondDataSource(){return DruidDataSourceBuilder.create().build();}@Bean@Primarypublic DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {Map<Object, Object> targetDataSources = new HashMap<>(5);targetDataSources.put(DataSourceNames.FIRST, firstDataSource);targetDataSources.put(DataSourceNames.SECOND, secondDataSource);return new DynamicDataSource(firstDataSource, targetDataSources);}}

3.5、采用aop的方式,在需要修改数据源的地方使用注解方式去切换,然后切面修改ThreadLocal的内容

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;/*** 多数据源,切面处理类*/
@Slf4j
@Aspect
@Component
public class DataSourceAspect implements Ordered {@Pointcut("@annotation(com.cdzs.datasource.CurDataSource)")public void dataSourcePointCut() {}@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();CurDataSource ds = method.getAnnotation(CurDataSource.class);if (ds == null) {DynamicDataSource.setDataSource(DataSourceNames.FIRST);log.debug("set datasource is " + DataSourceNames.FIRST);} else {DynamicDataSource.setDataSource(ds.name());log.debug("set datasource is " + ds.name());}try {return point.proceed();} finally {DynamicDataSource.clearDataSource();log.debug("clean datasource");}}@Overridepublic int getOrder() {return 1;}
}

3.6、启动类上添加数据源配置

因为数据源是自己生成的,所以要去掉原先springboot启动时候自动装配的数据源配置。

import com.cdzs.datasource.DynamicDataSourceConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Import;@MapperScan("com.cdzs.mapper")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@Import({DynamicDataSourceConfig.class})
public class MinioProjectApplication {public static void main(String[] args) {SpringApplication.run(MinioProjectApplication.class,args);}
}

3.7、测试数据源切换

service中定义两个查询,分别查两个数据库:

import com.cdzs.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;/*** <p>* 系统用户 服务类* </p>** @author xiaohe* @since 2019-06-04*/
public interface SysUserService extends IService<SysUser> {SysUser findUserByFirstDb(long id);SysUser findUserBySecondDb(long id);SysUser selectFindAll();}

3.8、实现类:因为默认是使用第一个数据源,所以不用注解,使用数据源二需要添加注解 @CurDataSource(name = DataSourceNames.SECOND) 

import com.cdzs.datasource.CurDataSource;
import com.cdzs.datasource.DataSourceNames;
import com.cdzs.entity.SysUser;
import com.cdzs.mapper.SysUserMapper;
import com.cdzs.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;/*** <p>* 系统用户 服务实现类* </p>*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {@Autowiredprivate SysUserMapper sysUserMapper;@Overridepublic SysUser findUserByFirstDb(long id) {return this.baseMapper.selectById(id);}@CurDataSource(name = DataSourceNames.SECOND)@Overridepublic SysUser findUserBySecondDb(long id) {return this.baseMapper.selectById(id);}//配置主库还是从库读取数据@CurDataSource(name = DataSourceNames.SECOND)@Overridepublic SysUser selectFindAll() {return this.sysUserMapper.selectFindAll();}}

3.9、测试类

import com.cdzs.entity.SysUser;
import com.cdzs.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MinioProjectApplicationTests {@Autowiredprivate SysUserService userService;@Testpublic void contextLoads() {SysUser user = userService.getById(1);log.info(user.toString());}@Testpublic void test() {SysUser user = userService.findUserByFirstDb(1);log.info("第一个数据库 : [{}]", user.toString());SysUser user2 = userService.findUserBySecondDb(1);log.info("第二个数据库 : [{}]", user2.toString());}
}

4.0、建表与初始化sql:

-- 系统用户
CREATE TABLE `sys_user`
(`user_id`        bigint      NOT NULL AUTO_INCREMENT,`username`       varchar(50) NOT NULL COMMENT '用户名',`password`       varchar(100) COMMENT '密码',`salt`           varchar(20) COMMENT '盐',`email`          varchar(100) COMMENT '邮箱',`mobile`         varchar(100) COMMENT '手机号',`status`         tinyint COMMENT '状态  0:禁用   1:正常',`create_user_id` bigint(20) COMMENT '创建者ID',`create_time`    datetime COMMENT '创建时间',PRIMARY KEY (`user_id`),UNIQUE INDEX (`username`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8 COMMENT ='系统用户';-- 初始数据 
INSERT INTO `sys_user` (`user_id`, `username`, `password`, `salt`, `email`, `mobile`, `status`, `create_user_id`,`create_time`)
VALUES ('1', 'admin', '9ec9750e709431dad22365cabc5c625482e574c74adaebba7dd02f1129e4ce1d', 'YzcmCZNvbXocrsz9dm8e','root@renren.io', '13612345678', '1', '1', '2016-11-11 11:11:11');

把库2的username改为:admin2222

最后的测试结果打印如下:

 4、关于事物

AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。 spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。

出现多数据源动态切换失败的原因是因为在事务开启后,数据源就不能再进行随意切换了,也就是说,一个事务对应一个数据源。

传统的Spring管理事务是放在Service业务层操作的,所以更换数据源的操作要放在这个操作之前进行。也就是切换数据源操作放在Controller层,可是这样操作会造成Controller层代码混乱的结果。

故而想到的解决方案是将事务管理在数据持久 (Dao层) 开启,切换数据源的操作放在业务层进行操作,就可在事务开启之前顺利进行数据源切换,不会再出现切换失败了。


https://www.xjx100.cn/news/444340.html

相关文章

RecyclerView使用问题集合http://www.jianshu.com/p/333fe22cabc6

1.RecyclerViewCursorAdapter & Loader机制:http://www.jianshu.com/p/333fe22cabc6 2RecyclerView侧边字母索引排序 3.使用RecyclerView notifyItemChanged()出现图片闪烁问题解决 4.RecyclerView的实现item中下划线的长度最后一行长度全长&#xff0c;其余长度较短的实…

SpringBoot后端篇(一)创建SpringBoot项目

最近看了大佬的文章&#xff0c;觉得写的很不错&#xff0c;于是动手实践实践&#xff0c;但是也走了很多的坑&#xff0c;目前自己整理了以下&#xff0c;谨以此作自己成长过程中的垫脚石....... 数据库设计 -- ---------------------------------------------------- -- 用…

虚拟化概述

虚拟化概述 本文重点介绍了SR-IOV技术以及MLU270对SR-IOV的支持。 SR-IOV简介 SR-IOV&#xff08;Single Root I/O Virtualization&#xff09;技术是一种基于硬件的虚拟化解决方案&#xff0c;可提供高性能和可伸缩性的虚拟解决方案。SR-IOV制定了标准化机制来实现多个虚拟机共…

GAM调节电路及CABC设置

1. ICN9605 GAM 调节电路 ICN9605中R-String电路只有一条&#xff0c;R/G/B三种颜色可以分别设置其V1、V5、V15、V31...V255。原因是AMOLED每行的RGB不是同时打开&#xff0c;而是分别打开&#xff0c;所以可以在对应的颜色时间&#xff0c;设置R-String的配置。V1V1、V5、V15、…

Android 功耗(19)---LCD背光驱动节电技术-LABC/CABC

LCD背光驱动节电技术-LABC/CABC LCD背光驱动节电技术-LABC/CABC图像永远是最直观的表现方式&#xff0c;而LCD正是目前应用最多的表现媒介。随着技术的增强&#xff0c;人类对视觉的要求不断提高&#xff0c;对图像的分辨率、色彩的要求也越来越高。我们的手机基本用的都是LCD。…

lcd背光节能matlab代码,【技术分享】LCD背光驱动节电技术-LABC/CABC

LCD背光驱动节电技术-LABC/CABC 图像永远是最直观的表现方式,而LCD正是目前应用最多的表现媒介。随着技术的增强,人类对视觉的要求不断提高,对图像的分辨率、色彩的要求也越来越高。 我们的手机基本用的都是LCD。而自从安卓划时代的出现,LCD大屏手机不断地进入了我们的视野…

ILI9341的使用之【六】命令二

由于ILI9341命令体系比较庞大&#xff0c;因此为了查询方便&#xff0c;把命令部分的解释分为两篇&#xff0c;本文为第二篇。第一篇详细解释了编码在3Fh以下的指令。本文详细解释编码在3Fh以上的指令。 《ILI9341的使用之【一】TFT-LCD原理&#xff08;转载&#xff09;》 《…

MTK 平台 CABC 背光控制机制

转载自&#xff1a;LABC and CABC-MTK Platform 1、简介 显示屏在移动设备里一直的是耗电大户&#xff0c;显示屏的背光显然是电池杀手&#xff0c;所以拿背光”开刀”介绍两种实际应用较为可行省电方法。环境光侦测对应背光控制(LightAdaptive Brightness Control-LABC)&…