SpringBoot 动态切换数据源
Code0cean 人气:01. 环境准备
1.1 数据库准备
一个本地环境的MySQL数据库,数据库mydb,创建表t_user
CREATE TABLE `t_user` ( `c_id` varchar(20) NOT NULL, `c_username` varchar(20) DEFAULT NULL, `c_password` varchar(20) DEFAULT NULL, `c_gender` tinyint(2) DEFAULT NULL, PRIMARY KEY (`c_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `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=2 DEFAULT 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=2 DEFAULT 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');
1.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>
2. ThreadLocal类介绍
3. 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决定使用哪个数据源。
4. 具体实现
4.1 定义数据源枚举类
定义数据源枚举类DataSourceTypeEnum
public enum DataSourceTypeEnum { /** * chatroom */ CHATROOM("chatroom"), /** * book_db */ BOOK_DB("book_db"), /** * mydb */ MY_DB("mydb"); private final String name; DataSourceTypeEnum(String name) { this.name = name; } public String getName() { return name; } }
4.2 创建动态多数据源类
定义一个动态多数据源类DynamicDataSource用于管理不同线程间多个数据源的选择和切换,扩展 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(); } /** * determineCurrentLookupKey决定使用哪个数据库 * @return */ @Override protected 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(); } }
4.3 创建动态多数据源配置类
DynamicDataSourceConfig类作为配置类,读取配置文件的三个数据源的配置,创建对应DataSource类型的Bean。
@Configuration public class DynamicDataSourceConfig { @Bean(name="chatroom") @ConfigurationProperties("spring.datasource.druid.first") public DataSource dataSource1(){ return DruidDataSourceBuilder.create().build(); } @Bean(name ="book_db") @ConfigurationProperties("spring.datasource.druid.second") public DataSource dataSource2(){ return DruidDataSourceBuilder.create().build(); } @Bean(name="mydb") @ConfigurationProperties("spring.datasource.druid.third") public DataSource dataSource3(){ return DruidDataSourceBuilder.create().build(); } @Bean(name="dynamicDataSource") @Primary public DynamicDataSource dataSource() { Map<Object, Object> targetDataSources = new HashMap<>(5); targetDataSources.put(DataSourceTypeEnum.CHATROOM.getName(), dataSource1()); targetDataSources.put(DataSourceTypeEnum.BOOK_DB.getName(), dataSource2()); targetDataSources.put(DataSourceTypeEnum.MY_DB.getName(), dataSource3()); return new DynamicDataSource(dataSource1(), targetDataSources); } }
4.4 自定义注解用于指定数据源
自定义注解@SpecifyDataSource用于在Service层方法上标记要使用哪个数据源。这里定义默认使用数据源 DataSourceType.CHATROOM。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SpecifyDataSource { /** * @return */ DataSourceTypeEnum value() default DataSourceTypeEnum.CHATROOM; }
4.5 AOP实现动态切换数据源
定义数据源界面类DataSourceAspect,用于实现有SpecifyDataSource注解标注的方法前切换注解指定的数据源。
@Aspect @Component @Order(value = 1) public class DataSourceAspect { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Pointcut("@annotation(top.javahai.datasource.annotation.SpecifyDataSource)") public void dataSourcePointCut() { } @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { 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
public class Admin implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 登录账号 */ private String username; /** * 昵称 */ private String nickname; /** * 密码 */ private String password; /** * 管理员头像 */ private String userProfile; //省略getter/setter方法
创建实体类TUser
public class TUser implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "c_id", type = IdType.AUTO) private Integer cId; private String cUsername; private String cPassword; private Integer cGender; }
创建实体类TUserinfo
@TableName(value = "t_user_info") public class TUserinfo implements Serializable { private static final long serialVersionUID = 1L; /** * user_name */ private String userName; /** * 登录密码 */ private String password; /** * 所在学院 */ @TableField("areaObj") private String areaObj; /** * 姓名 */ private String name; /** * 性别 */ private Integer sex; /** * 学生照片 */ private String userPhoto; /** * 出生日期 */ private String birthday; /** * 联系电话 */ private String telephone; /** * 家庭地址 */ private String address; }
创建UserVO用于测试
public class UserVO { private List<Admin> adminList; private List<TUserinfo> tUserinfos; private List<TUser> tUsers; }
5.3 服务层代码
@Service public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService { public List<Admin> getAll(){ return this.list(null); } }
@Service public class TUserinfoServiceImpl extends ServiceImpl<TUserinfoMapper, TUserinfo> implements ITUserinfoService { @SpecifyDataSource(value = DataSourceTypeEnum.BOOK_DB) public List<TUserinfo> selectAll(){ return this.list(null); } }
@Service public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements ITUserService { @SpecifyDataSource(value = DataSourceTypeEnum.MY_DB) public List<TUser> selectAll(){ return this.list(null); } }
public interface AdminMapper extends BaseMapper<Admin> { } public interface TUserinfoMapper extends BaseMapper<TUserinfo> { } public interface TUserMapper extends BaseMapper<TUser> { }
5.4 控制层代码
创建接口/test/list用于测试
@RestController @RequestMapping("/test") public class TestController { @Autowired private AdminServiceImpl adminService; @Autowired private TUserinfoServiceImpl userinfoService; @Autowired private TUserServiceImpl userService; @GetMapping("/list") public UserVO list(){ List<Admin> adminList= adminService.getAll(); List<TUserinfo> tUserinfos = userinfoService.selectAll(); List<TUser> tUsers = userService.selectAll(); UserVO userVO = new UserVO(); 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
加载全部内容