文章目录
1. AbstractRoutingDataSource类介绍
Spring Boot提供了AbstractRoutingDataSource 根据用户定义的规则选择要使用的数据源,这样我们可以在每次数据库操作前设置使用的数据源,实现可动态路由的数据源。它的抽象方法determineCurrentLookupKey() 决定使用哪个数据源。
getConnection()获取数据库连接,根据查找lookup key键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。通过这我们知道可以实现:数据源的动态切换,在程序运行时,把数据源动态织入到程序中,灵活得进行数据源切换,从而可以不依赖中间件,实现读写分离功能。
AbstractRoutingDataSource实现逻辑:
- 继承抽象类AbstractRoutingDataSource,并实现determineCurrentLookupKey()方法。自定义LookupKey的选择规则。
- 把配置的多个数据源放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中(使用setDefaultTargetDataSource和setTargetDataSources方法),然后通过afterPropertiesSet()方法将数据源分别进行复制到AbstractRoutingDataSource的resolvedDataSources属性和resolvedDefaultDataSource属性中。
- 调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。
determineTargetDataSource()方法通过调用determineCurrentLookupKey() 方法返回的lookupKey决定使用哪个数据源。
2. ThreadLocal类介绍
3. 环境准备
3.1 数据库准备
一个本地环境的MySQL数据库,数据库mydb,创建表t_user
CREATETABLE`t_user`(`c_id`varchar(20)NOTNULL,`c_username`varchar(20)DEFAULTNULL,`c_password`varchar(20)DEFAULTNULL,`c_gender`tinyint(2)DEFAULTNULL,PRIMARYKEY(`c_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`mydb`.`t_user`(`c_id`,`c_username`,`c_password`,`c_gender`)VALUES('1','思思','123',1);
一个云服务器的MySQL数据库,创建数据库book_db,创建表t_userinfo。
CREATE TABLE `t_user_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '登录密码',
`areaObj` varchar(255) DEFAULT NULL COMMENT '所在学院',
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`sex` tinyint(255) DEFAULT NULL COMMENT '性别',
`user_photo` varchar(255) DEFAULT NULL COMMENT '学生照片',
`birthday` varchar(20) DEFAULT NULL COMMENT '出生日期',
`telephone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`address` varchar(255) DEFAULT NULL COMMENT '家庭地址',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2DEFAULT CHARSET=utf8;
INSERT INTO `book_db`.`t_user_info`(`id`, `user_name`, `password`, `areaObj`, `name`, `sex`, `user_photo`, `birthday`, `telephone`, `address`) VALUES (1,'张三','123','哈尔滨','张三散',1,'123','02-16', '15756892458', '黑龙江省哈尔滨市');
创建数据库chatroom,创建表admin。
CREATE TABLE `admin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL COMMENT '登录账号',
`nickname` varchar(20) NOT NULL COMMENT '昵称',
`password` varchar(255) NOT NULL COMMENT '密码',
`user_profile` varchar(255) DEFAULT NULL COMMENT '管理员头像',PRIMARY KEY (`id`)USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
INSERT INTO `chatroom`.`admin`(`id`, `username`, `nickname`, `password`, `user_profile`) VALUES (1,'admin','系统管理员', '$2a$10$PyloUEVGuO0fUZdfeIaROOTluRmccl.Scifa8S7Os0Wt.s4bDkb', 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1784117537,3335593911&fm=26&gp=0.jpg');
3.2 项目创建
创建SpringBoot项目,整合MyBatis-Plus。pom.xml引入的依赖:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</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><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</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></dependencies>
配置读取resource文件夹下的mapper文件
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource><resource><directory>src/main/resources</directory></resource></resources><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
4. 具体实现
4.1 定义数据源枚举类
定义数据源枚举类DataSourceTypeEnum
publicenumDataSourceTypeEnum{/**
* chatroom
*/CHATROOM("chatroom"),/**
* book_db
*/BOOK_DB("book_db"),/**
* mydb
*/MY_DB("mydb");privatefinalString name;DataSourceTypeEnum(String name){this.name = name;}publicStringgetName(){return name;}}
4.2 创建动态多数据源类
定义一个动态多数据源类DynamicDataSource用于管理不同线程间多个数据源的选择和切换,扩展 Spring 提供的 AbstractRoutingDataSource 抽象类,重写 determineCurrentLookupKey 方法,其中的determineCurrentLookupKey() 方法用于决定使用哪个数据源。
publicclassDynamicDataSourceextendsAbstractRoutingDataSource{/**
* ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
*/privatestaticfinalThreadLocal<String> CONTEXT_HOLDER =newThreadLocal<>();/**
* 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
*
* @param defaultTargetDataSource 默认数据源
* @param targetDataSources 目标数据源
*/publicDynamicDataSource(DataSource defaultTargetDataSource,Map<Object,Object> targetDataSources){super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);super.afterPropertiesSet();}/**
* determineCurrentLookupKey决定使用哪个数据库
* @return
*/@OverrideprotectedObjectdetermineCurrentLookupKey(){returngetDataSource();}publicstaticvoidsetDataSource(String dataSource){
CONTEXT_HOLDER.set(dataSource);}publicstaticStringgetDataSource(){return CONTEXT_HOLDER.get();}publicstaticvoidclearDataSource(){
CONTEXT_HOLDER.remove();}}
4.3 创建动态多数据源配置类
DynamicDataSourceConfig类作为配置类,读取配置文件的三个数据源的配置,创建对应DataSource类型的Bean。
@ConfigurationpublicclassDynamicDataSourceConfig{@Bean(name="chatroom")@ConfigurationProperties("spring.datasource.druid.first")publicDataSourcedataSource1(){returnDruidDataSourceBuilder.create().build();}@Bean(name ="book_db")@ConfigurationProperties("spring.datasource.druid.second")publicDataSourcedataSource2(){returnDruidDataSourceBuilder.create().build();}@Bean(name="mydb")@ConfigurationProperties("spring.datasource.druid.third")publicDataSourcedataSource3(){returnDruidDataSourceBuilder.create().build();}@Bean(name="dynamicDataSource")@PrimarypublicDynamicDataSourcedataSource(){Map<Object,Object> targetDataSources =newHashMap<>(5);
targetDataSources.put(DataSourceTypeEnum.CHATROOM.getName(),dataSource1());
targetDataSources.put(DataSourceTypeEnum.BOOK_DB.getName(),dataSource2());
targetDataSources.put(DataSourceTypeEnum.MY_DB.getName(),dataSource3());returnnewDynamicDataSource(dataSource1(), targetDataSources);}}
4.4 自定义注解用于指定数据源
自定义注解@SpecifyDataSource用于在Service层方法上标记要使用哪个数据源。这里定义默认使用数据源 DataSourceType.CHATROOM。
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceSpecifyDataSource{/**
* @return
*/DataSourceTypeEnumvalue()defaultDataSourceTypeEnum.CHATROOM;}
4.5 AOP实现动态切换数据源
定义数据源界面类DataSourceAspect,用于实现有SpecifyDataSource注解标注的方法前切换注解指定的数据源。
@Aspect@Component@Order(value =1)publicclassDataSourceAspect{privateLogger logger =LoggerFactory.getLogger(this.getClass());@Pointcut("@annotation(top.javahai.datasource.annotation.SpecifyDataSource)")publicvoiddataSourcePointCut(){}@Around("dataSourcePointCut()")publicObjectaround(ProceedingJoinPoint point)throwsThrowable{MethodSignature signature =(MethodSignature) point.getSignature();Method method = signature.getMethod();SpecifyDataSource ds = method.getAnnotation(SpecifyDataSource.class);if(ds ==null){DynamicDataSource.setDataSource(DataSourceType.CHATROOM.getName());
logger.info("set datasource is "+DataSourceType.CHATROOM);}else{DynamicDataSource.setDataSource(ds.value().getName());
logger.info("set datasource is "+ ds.value().getName());}try{return point.proceed();}finally{DynamicDataSource.clearDataSource();
logger.info("clean datasource");}}}
5. 测试使用
5.1 配置数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
# 数据源1
spring.datasource.druid.first.url=jdbc:mysql://158.156.444.68:3306/chatroom?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.first.username=root
spring.datasource.druid.first.password=123456# 数据源2
spring.datasource.druid.second.url=jdbc:mysql://158.156.444.68:3306/book_db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.second.username=root
spring.datasource.druid.second.password=123456#数据源3
spring.datasource.druid.third.url=jdbc:mysql:///mydb?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.third.username=root
spring.datasource.druid.third.password=123456
mybatis-plus.mapper-locations=classpath:mapper/*.xml
#输出sql执行日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
5.2 创建实体类
创建实体类Admin
publicclassAdminimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@TableId(value ="id", type =IdType.AUTO)privateInteger id;/**
* 登录账号
*/privateString username;/**
* 昵称
*/privateString nickname;/**
* 密码
*/privateString password;/**
* 管理员头像
*/privateString userProfile;//省略getter/setter方法
创建实体类TUser
publicclassTUserimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@TableId(value ="c_id", type =IdType.AUTO)privateInteger cId;privateString cUsername;privateString cPassword;privateInteger cGender;}
创建实体类TUserinfo
@TableName(value ="t_user_info")publicclassTUserinfoimplementsSerializable{privatestaticfinallong serialVersionUID =1L;/**
* user_name
*/privateString userName;/**
* 登录密码
*/privateString password;/**
* 所在学院
*/@TableField("areaObj")privateString areaObj;/**
* 姓名
*/privateString name;/**
* 性别
*/privateInteger sex;/**
* 学生照片
*/privateString userPhoto;/**
* 出生日期
*/privateString birthday;/**
* 联系电话
*/privateString telephone;/**
* 家庭地址
*/privateString address;}
创建UserVO用于测试
publicclassUserVO{privateList<Admin> adminList;privateList<TUserinfo> tUserinfos;privateList<TUser> tUsers;}
5.3 服务层代码
@ServicepublicclassAdminServiceImplextendsServiceImpl<AdminMapper,Admin>implementsIAdminService{publicList<Admin>getAll(){returnthis.list(null);}}
@ServicepublicclassTUserinfoServiceImplextendsServiceImpl<TUserinfoMapper,TUserinfo>implementsITUserinfoService{@SpecifyDataSource(value =DataSourceTypeEnum.BOOK_DB)publicList<TUserinfo>selectAll(){returnthis.list(null);}}
@ServicepublicclassTUserServiceImplextendsServiceImpl<TUserMapper,TUser>implementsITUserService{@SpecifyDataSource(value =DataSourceTypeEnum.MY_DB)publicList<TUser>selectAll(){returnthis.list(null);}}
publicinterfaceAdminMapperextendsBaseMapper<Admin>{}publicinterfaceTUserinfoMapperextendsBaseMapper<TUserinfo>{}publicinterfaceTUserMapperextendsBaseMapper<TUser>{}
5.4 控制层代码
创建接口/test/list用于测试
@RestController@RequestMapping("/test")publicclassTestController{@AutowiredprivateAdminServiceImpl adminService;@AutowiredprivateTUserinfoServiceImpl userinfoService;@AutowiredprivateTUserServiceImpl userService;@GetMapping("/list")publicUserVOlist(){List<Admin> adminList= adminService.getAll();List<TUserinfo> tUserinfos = userinfoService.selectAll();List<TUser> tUsers = userService.selectAll();UserVO userVO =newUserVO();
userVO.setAdminList(adminList);
userVO.settUserinfos(tUserinfos);
userVO.settUsers(tUsers);return userVO;}}
浏览器请求/test/list
查看控制台输出,查看数据源的切换日志
完整Demo代码地址:https://github.com/JustCoding-Hai/learn-everyday/tree/master/learn-multi_data_source
版权归原作者 Code0cean 所有, 如有侵权,请联系我们删除。