spring boot多数据源的动态切换
添加配置文件
spring: jpa: properties: hibernate: session_factory: statement_inspector: com.gutousu.dynamic_switch_data_source.config.StatementInspectorImpl datasource: primary: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://0.0.0.0:1/gutousu_dynamic_switch_data_source_test_1?useSSL=true&verifyServerCertificate=false&useUnicode=true&characterEncoding=UTF8 username: password: secondary: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://0.0.0.0:2/gutousu_dynamic_switch_data_source_test_2?useSSL=true&verifyServerCertificate=false&useUnicode=true&characterEncoding=UTF8 username: password:
这个是拦截最终执行的sql语句用的,用于测试,观察事务
spring: jpa: properties: hibernate: session_factory: statement_inspector: com.gutousu.dynamic_switch_data_source.config.StatementInspectorImpl
这两个是数据源的配置
datasource: primary: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://0.0.0.0:1/gutousu_dynamic_switch_data_source_test_1?useSSL=true&verifyServerCertificate=false&useUnicode=true&characterEncoding=UTF8 username: password: secondary: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://0.0.0.0:2/gutousu_dynamic_switch_data_source_test_2?useSSL=true&verifyServerCertificate=false&useUnicode=true&characterEncoding=UTF8 username: password:
添加数据源配置类
@Setter @ConfigurationProperties(prefix = "spring.datasource") @Configuration public class DataSourceConfig { private static final ThreadLocal<String> datasourceHolder = new ThreadLocal<>(); private HikariDataSource primary; private HikariDataSource secondary; private static final AbstractRoutingDataSource abstractRoutingDataSource = new AbstractRoutingDataSource() { @Override protected Object determineCurrentLookupKey() { return datasourceHolder.get(); } }; public static void setDataSource(String sourceName) { datasourceHolder.set(sourceName); } public static void clearDataSource() { datasourceHolder.remove(); } @Bean public DataSource dataSource() { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceEnum.PRIMARY.getValue(), primary); targetDataSources.put(DataSourceEnum.SECONDARY.getValue(), secondary); abstractRoutingDataSource.setTargetDataSources(targetDataSources); abstractRoutingDataSource.setDefaultTargetDataSource(primary); return abstractRoutingDataSource; } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
private static final ThreadLocal<String> datasourceHolder = new ThreadLocal<>();
通过ConfigurationProperties注解读取配置文件中的数据库连接信息,将他们放到这里
private HikariDataSource primary; private HikariDataSource secondary;
获取连接信息的类,实现determineCurrentLookupKey方法获取数据源的key
private static final AbstractRoutingDataSource abstractRoutingDataSource = new AbstractRoutingDataSource() { @Override protected Object determineCurrentLookupKey() { return datasourceHolder.get(); } };
用两个静态方法,切换数据源,和清除key信息
public static void setDataSource(String sourceName) { datasourceHolder.set(sourceName); } public static void clearDataSource() { datasourceHolder.remove(); }
将数据源放到一个Map中
然后将Map放到abstractRoutingDataSource中
返回abstractRoutingDataSource到spring的bean容器中
@Bean public DataSource dataSource() { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceEnum.PRIMARY.getValue(), primary); targetDataSources.put(DataSourceEnum.SECONDARY.getValue(), secondary); abstractRoutingDataSource.setTargetDataSources(targetDataSources); abstractRoutingDataSource.setDefaultTargetDataSource(primary); return abstractRoutingDataSource; }
获取刚刚添加的bean,将其放到事务中,再返回到spring的bean容器中
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
添加一个枚举
@Getter @AllArgsConstructor @NoArgsConstructor public enum DataSourceEnum { PRIMARY("primary"), SECONDARY("secondary"); private String value; }
添加一个注解
@Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { DataSourceEnum value(); }
添加一个aop,拦截方法,切换数据源
@Aspect @Order(-1) @Component public class DataSourceAop { @Before("@annotation(dataSource)") public void before(JoinPoint joinPoint, DataSource dataSource) { DataSourceConfig.setDataSource(dataSource.value().getValue()); } @After("@annotation(dataSource)") public void after(JoinPoint joinPoint, DataSource dataSource) { DataSourceConfig.clearDataSource(); } }
这里是为了,在事务开启之前切换数据源
@Order(-1)
实现一下StatementInspector接口,这个接口会拦截所有将要执行的sql语句,用于测试事务
@Component public class StatementInspectorImpl implements StatementInspector { @Override public String inspect(String sql) { return sql; } }
--------------------------------------------------------------------------------------------------------------
最后添加一些测试
两个方法测试的接口
public interface ITestService { String t1(); String t2(); }
实现方法测试接口,在方法上加上 aop直接
@Service public class TestService implements ITestService { @Autowired private IUserRepository userRepository; @DataSource(value = DataSourceEnum.SECONDARY) //@Transactional public String t1() { user user = userRepository.save(new user("张三",21)); System.out.print(""); return ""; } @DataSource(value = DataSourceEnum.PRIMARY) @Transactional public String t2() { user user = userRepository.save(new user("张三",21)); System.out.print(""); return ""; } }
添加一个controller
@RestController @RequestMapping("/test") public class TestController { @Autowired private ITestService testService; @GetMapping("/t1") public String t1() { testService.t1(); return ""; } @GetMapping("/t2") public String t2() { testService.t2(); return ""; } }
测试一下没有事务的方法一
被aop拦截到了,切换到了注解中指定的数据源
进入方法,执行添加方法
这里直接执行了添加的sql语句
数据库中有数据了
测试一下有事务的方法二
被aop拦截到了,并切换到了另一个数据源
进入方法添加数据
这里执行完添加了,但是没有执行sql语句,说事务没有提交,只在事务中进行了添加
最后结束方法的时候执行了添加的sql语句
完结!撒花!