环境:springboot2.2.6RELEASE
实现目标:一写多读,读可以任意配置多个,默认都是从写库中进行操作,只有符合条件的方法(指定的目标方法或者标有指定注解的方法才会从读库中操作)。独立打成一个jar包放入本地仓库。
实现原理:通过aop。
复制
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
复制
pack: datasource: pointcut: execution(public * net.greatsoft.service.base.*.*(..)) || execution(public * net.greatsoft.service.xxx.*.*(..)) master: driverClassName: oracle.jdbc.driver.OracleDriver jdbcUrl: jdbc:oracle:thin:@10.100.102.113:1521/orcl username: test password: test minimumIdle: 10 maximumPoolSize: 200 autoCommit: true idleTimeout: 30000 poolName: MbookHikariCP maxLifetime: 1800000 connectionTimeout: 30000 connectionTestQuery: SELECT 1 FROM DUAL slaves: - driverClassName: oracle.jdbc.driver.OracleDriver jdbcUrl: jdbc:oracle:thin:@10.100.102.113:1521/orcl username: dc password: dc minimumIdle: 10 maximumPoolSize: 200 autoCommit: true idleTimeout: 30000 poolName: MbookHikariCP maxLifetime: 1800000 connectionTimeout: 30000 connectionTestQuery: SELECT 1 FROM DUAL - driverClassName: oracle.jdbc.driver.OracleDriver jdbcUrl: jdbc:oracle:thin:@10.100.102.113:1521/orcl username: empi password: empi minimumIdle: 10 maximumPoolSize: 200 autoCommit: true idleTimeout: 30000 poolName: MbookHikariCP maxLifetime: 1800000 connectionTimeout: 30000 connectionTestQuery: SELECT 1 FROM DUAL
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.
pointcut:定义切点,那些方法是需要拦截(从读库中操作)。
master:写库配置。
slaves:读库配置(List集合)。
复制
@Component @ConfigurationProperties(prefix = "pack.datasource") public class RWDataSourceProperties { private String pointcut ; private HikariConfig master ; private List<HikariConfig> slaves = new ArrayList<>(); }
1.
2.
3.
4.
5.
6.
7.
8.
9.
复制
public class RWConfig { private static Logger logger = LoggerFactory.getLogger(RWConfig.class) ; @Bean public HikariDataSource masterDataSource(RWDataSourceProperties rwDataSourceProperties) { return new HikariDataSource(rwDataSourceProperties.getMaster()) ; } @Bean public List<HikariDataSource> slaveDataSources(RWDataSourceProperties rwDataSourceProperties) { List<HikariDataSource> lists = new ArrayList<>() ; for(HikariConfig config : rwDataSourceProperties.getSlaves()) { lists.add(new HikariDataSource(config)) ; } return lists ; } @Bean @Primary @DependsOn({"masterDataSource", "slaveDataSources"}) public AbstractRoutingDataSource routingDataSource(@Qualifier("masterDataSource")DataSource masterDataSource, @Qualifier("slaveDataSources")List<HikariDataSource> slaveDataSources) { BaseRoutingDataSource ds = new BaseRoutingDataSource() ; Map<Object, Object> targetDataSources = new HashMap<>(2) ; targetDataSources.put("master", masterDataSource) ; for (int i = 0; i < slaveDataSources.size(); i++) { targetDataSources.put("slave-" + i, slaveDataSources.get(i)) ; } ds.setDefaultTargetDataSource(masterDataSource) ; ds.setTargetDataSources(targetDataSources) ; return ds ; } }
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.
复制
public class BaseRoutingDataSource extends AbstractRoutingDataSource { @Resource private DataSourceHolder holder; @Override protected Object determineCurrentLookupKey() { return holder.get() ; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
复制
public class DataSourceHolder { private ThreadLocal<Integer> context = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0 ; } }; @Resource private BaseSlaveLoad slaveLoad ; public String get() { Integer type = context.get() ; return type == null || type == 0 ? "master" : "slave-" + slaveLoad.load() ; } public void set(Integer type) { context.set(type) ; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
通过aop动态设置context的内容值,0为从写库中操作,其它的都在读库中操作。
BaseSlaveLoad类为到底从那个读库中选取的一个算法类,默认实现使用的是轮询算法。
复制
public interface BaseSlaveLoad { int load() ; }
1.
2.
3.
4.
5.
复制
public abstract class AbstractSlaveLoad implements BaseSlaveLoad { @Resource protected List<HikariDataSource> slaveDataSources ; }
1.
2.
3.
4.
5.
6.
这里定义一个抽象类注入了读库列表,所有的实现类从该类中继承即可。
复制
public class PollingLoad extends AbstractSlaveLoad { private int index = 0 ; private int size = 1 ; @PostConstruct public void init() { size = slaveDataSources.size() ; } @Override public int load() { int n = index ; synchronized (this) { index = (++index) % size ; } return n ; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
配置成Bean
复制
@Bean @ConditionalOnMissingBean public BaseSlaveLoad slaveLoad() { return new PollingLoad() ; } @Bean public DataSourceHolder dataSourceHolder() { return new DataSourceHolder() ; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
复制
public class DataSourceAspect implements MethodInterceptor { private DataSourceHolder holder ; public DataSourceAspect(DataSourceHolder holder) { this.holder = holder ; } @Override public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod() ; String methodName = method.getName() ; SlaveDB slaveDB = method.getAnnotation(SlaveDB.class) ; if (slaveDB == null) { slaveDB = method.getDeclaringClass().getAnnotation(SlaveDB.class) ; } if (methodName.startsWith("find") || methodName.startsWith("get") || methodName.startsWith("query") || methodName.startsWith("select") || methodName.startsWith("list") || slaveDB != null) { holder.set(1) ; } else { holder.set(0) ; } return invocation.proceed(); } }
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.
应该切点需要动态配置,所以这里采用spring aop的方式来配置
复制
@Bean public AspectJExpressionPointcutAdvisor logAdvisor(RWDataSourceProperties props, DataSourceHolder holder) { AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor() ; logger.info("执行表达式:{}", props.getPointcut()) ; advisor.setExpression(props.getPointcut()) ; advisor.setAdvice(new DataSourceAspect(holder)) ; return advisor ; }
1.
2.
3.
4.
5.
6.
7.
8.
复制
public class RWImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] {RWConfig.class.getName()} ; } }
1.
2.
3.
4.
5.
6.
7.
8.
这里的RWConfig为我们上面的配置类
复制
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import({RWImportSelector.class}) public @interface EnableRW { }
1.
2.
3.
4.
5.
6.
复制
@Documented @Retention(RUNTIME) @Target({ TYPE, METHOD }) public @interface SlaveDB { }
1.
2.
3.
4.
5.
有@SlaveDB的注解方法会类都会从读库中操作。
到此读写分离组件开发完成。
复制
mvn install -Dmaven.test.skip=true
1.
引入依赖
复制
<dependency> <groupId>com.pack</groupId> <artifactId>xg-component-rw</artifactId> <version>1.0.0</version> </dependency>
1.
2.
3.
4.
5.
启动类添加注解开启读写分离功能
复制
@SpringBootApplication @EnableRW public class BaseWebApplication { public static void main(String[] args) { SpringApplication.run(BaseWebApplication.class, args); } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
测试:
第一次查询:
第二次查询:
为了区别两个从库设置不同的数据
这里是写库
完毕!!!