使用springboot给你带来多大便利,你就要接受方便使用带来的相应的复杂度。直线正常跑谁不会给油啊,弯道快,才是真的快。说一说,数据源,就是个DataSource,多数据源就是多个DataSource,读写分离,就是读用一个数据源,写入一个数据源。一般增删改使用主库,查使用从库。配置数据库主从复制。
在springboot中对数据源怎么操作嘞。说思路,springboot中的自动配置用的很舒服,我们要做的就是把springboot这个我们用的很舒服的自动配置给去掉。这个自动配置会往运行环境里添加一个默认的数据源。这个自动配置这块东西不少,各种花里胡哨的注解,我见都是第一次见,你让我知道它的作用,是有一点扯的。害,没有开发springbooot-stater的经验,不宜多说。
手动注入数据源
以前的XML写法
1 2 3 4 5 6 7
| <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>
|
JavaConfig写法
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class DataContext { @Bean public DataSource dataSource(){ HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("xxxx"); dataSource.setUsername("xxxx"); dataSource.setPassword("xxxx"); return dataSource; } }
|
springboot相当于是集JavaConfig于大成者,springboot的stater,大多数就是配置上面的内容,使用springboot的规范就能在启动的时候加载配置类。一般手动注入数据源,需要先把springboot自动加载的配置类给排除掉,改一下启动类的注解@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
改成这样就可以,为什么我要拿出来说呢,因为在下面实现动态数据源的时候也是需要排除这个的。
动态数据源
浅聊一下,动态数据源是对数据源的高级操作,所以就不要有这样的疑问,动态数据源能不能做到读写分离,能不能多数据源这样的问题。这种问题就和知乎上的提问:请问我拿了诺奖后可不可以保研?
引入依赖
1 2 3 4 5
| <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>${dynamic.verion}</version> </dependency>
|
yml配置
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
| spring: datasource: dynamic: primary: master strict: false p6spy: false datasource: master: url: jdbc:mysql://127.0.0.1:3306/xxxx username: root password: root second1: url: jdbc:mysql://127.0.0.1:3306/xxxx username: root password: root second2: url: jdbc:mysql://127.0.0.1:3306/xxxx username: root password: root
|
使用@DS
切换数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Service public class NameServer { @Resource JdbcTemplate jdbcTemplate; @DS(ConstSource.SECOND2) public String getName(){ List<Map<String, Object>> maps = jdbcTemplate.queryForList("select name from wlf"); return maps.get(0).get("name")+""; } }
public class ConstSource { public static final String MASTER = "master"; public static final String SECOND1 = "second1"; public static final String SECOND2 = "second2"; }
|
@DS
可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
注解 |
结果 |
没有@DS |
默认数据源 |
@DS(“dsName”) |
dsName可以为组名也可以为具体某个库的名称 |
编程式切换数据源
框架提供了编程式切换数据源。稍加封装改造一下。不建议手动修改数据源。
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| @Component public class DataContext {
@Resource protected DataSource dataSource;
@Resource protected DefaultDataSourceCreator dataSourceCreator;
public void createDataSource(String name, String username, String password, String url, String driver){ DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource;
DataSourceProperty dsp = new DataSourceProperty(); dsp.setPoolName(name); dsp.setUrl(url); dsp.setUsername(username); dsp.setPassword(password); dsp.setDriverClassName(driver);
DataSource dataSource = dataSourceCreator.createDataSource(dsp); dynamicRoutingDataSource.addDataSource(name, dataSource); }
public void changeDataSource(String name){ DynamicDataSourceContextHolder.push(name); }
public void cleanDataSource(String name) { DynamicDataSourceContextHolder.poll(); }
public void cleanDataSource() { DynamicDataSourceContextHolder.clear(); }
public void updateDataSource(String name, String username, String password, String url, String driver) { removeDataSource(name); createDataSource(name, username, password, url, driver); }
public void removeDataSource(String name) { DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource; dynamicRoutingDataSource.removeDataSource(name); }
public Map<String, DataSource> getDataSources() { DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource; return dynamicRoutingDataSource.getDataSources(); }
public DataSource getDataSource(String name) { DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dataSource; return dynamicRoutingDataSource.getDataSource(name); }
@PostConstruct public void loadDataSource() { List<SysDataSource> sysDataSources = sysDataSourceService.list(); sysDataSources.forEach(sysDataSource -> { createDataSource( sysDataSource.getName(), sysDataSource.getUsername(), sysDataSource.getPassword(), sysDataSource.getUrl(), sysDataSource.getDriver()); }); } }
|
PS: 这种编程运行时操作数据源已经有点十分方便了,核心就是这个类DefaultDataSourceCreator
,如果想深入了解可以看一看源码,可以以此种模式操作多租户的模式,根据不同用户连接不同的数据源。
使用MyBatis插件读写分离
添加配置
1 2 3 4 5 6 7
| @Configuration public class CoreConfig { @Bean public ConfigurationCustomizer mybatisConfigurationCustomizer() { return configuration -> configuration.addInterceptor(new MasterSlaveAutoRoutingPlugin()); } }
|
支持MybatisPlus。及MyBatis.
PS:主从数据源配置应为
1 2 3 4 5 6 7 8 9 10 11
| spring: datasource: dynamic: primary: master datasource: master: slave_1: slave_2:
|
slave_1
,slave_2
这样表示这两个数据源为一组,注解切换或者手动切换是数据源名应为slave
,而不是slave_1
,
切换数据源失败
使用了spring的事务,则切换数据源会有几率会失效,或者百分百失效
开发者原文:
原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional注解。
方法内部调用
查看以下示例 回答 外部调用 userservice.test1() 能在执行到 test2() 切换到second数据源吗?
1 2 3 4 5 6 7 8 9 10 11 12 13
| public UserService {
@DS("first") public void test1() { test2(); }
@DS("second") public void test2() { } }
|
答案:!!!不能不能不能!!!! 数据源核心原理是基于aop代理实现切换,内部方法调用不会使用aop。
解决方法:
把test2()方法提到另外一个service,单独调用。
使用了Shiro
建议不用,可以使用sa-token
来代替。
事务控制
上面说到了使用spring的事务,会导致切换数据源失败。框架开发者也了解这个情况,写了一个本地的事务工具类LocalTxUtil
来解决此问题
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
| public final class LocalTxUtil {
public static void startTransaction() { if (!StringUtils.isEmpty(TransactionContext.getXID())) { log.debug("dynamic-datasource exist local tx [{}]", TransactionContext.getXID()); } else { String xid = UUID.randomUUID().toString(); TransactionContext.bind(xid); log.debug("dynamic-datasource start local tx [{}]", xid); } }
public static void commit() { ConnectionFactory.notify(true); log.debug("dynamic-datasource commit local tx [{}]", TransactionContext.getXID()); TransactionContext.remove(); }
public static void rollback() { ConnectionFactory.notify(false); log.debug("dynamic-datasource rollback local tx [{}]", TransactionContext.getXID()); TransactionContext.remove(); } }
|
后话
在如今各种持久层框架中,我们越来越感知不到数据源的存在,好处是,配置简单了,坏处就是对数据源的处理时就有些麻烦。在古老的JDBC中,其实没有这个问题,万物queryForList()
,换个数据源继续queryForList()
。这个框架理论上支持任何持久层,因为人根本没把目光放在持久层上,专注于数据源,它只操作数据源的。
在springboot2.6.x的版本的不允许依赖循环,我属实不能理解。我不理解,
封面