Spring Boot 使用AOP实现多个数据库源的读写分离
1、首先引入Maven依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${aspectj.version}</version> </dependency> <!--使用AspectJ方式注解需要相应的包 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectj.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>1.3.0.RELEASE</version> </dependency>
2.自定义注解类(annotation)
@Target({ElementType.METHOD, ElementType.TYPE})
@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
这里ElementType.METHOD用于描述方法,ElementType.TYPE用于描述类、接口(包括注解类型) 或enum声明
@Retention(RetentionPolicy.RUNTIME)生命周期
RetentionPolicy.RUNTIME表示运行时有效
import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ReadOnlyConnection {//定义注解的名称为ReadOnlyConnection
}
3、使用aop定义环绕(@Around)切面(即在运行切面方法的前后执行)
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.ProceedingJoinPoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import com.oadmin.superapp.config.DbContextHolder; import com.oadmin.superapp.config.DbType; @Aspect @Component public class ReadOnlyConnectionInterceptor implements Ordered { private static final Logger logger = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class); //@Pointcut("within(com.oadmin.superapp.manager.impl..*) && @annotation(com.oadmin.superapp.filter.ReadOnlyConnection)") @Pointcut("execution(* com.oadmin.superapp.manager.impl..*(..)) ") //定义切面的范围 public void managerMethodPointcut(){} @Around("managerMethodPointcut() and @annotation(readOnlyConnection)") public Object proceed(ProceedingJoinPoint proceedingJoinPoint,ReadOnlyConnection readOnlyConnection) throws Throwable{ try { logger.info("set database connection to read only"); DbContextHolder.setDbType(DbType.READ); Object result = proceedingJoinPoint.proceed(); return result; } finally { DbContextHolder.clearDbType(); logger.info("restore database connection"); } } @Override public int getOrder() { return 0; } }
4.定义DbContextHolder类,get()、set方法,对DbType赋值
public class DbContextHolder { private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>(); public static void setDbType(DbType dbType) { if (dbType == null) { throw new NullPointerException(); } contextHolder.set(dbType); } public static DbType getDbType() { return (DbType) contextHolder.get(); } public static void clearDbType() { contextHolder.remove(); } }
5、定义Dbtype的类型(枚举类enum),即write和read两种类型
public enum DbType { WRITE, READ, }
6、连接数据源(DataSource)
import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class RoutingDataSource extends AbstractRoutingDataSource { private Object writeDataSource; // 写数据源 private List<Object> readDataSources; // 多个读数据源 private int readDataSourceSize; // 读数据源个数 private int readDataSourcePollPattern = 0; // 获取读数据源方式,0:随机,1:轮询 private AtomicLong counter = new AtomicLong(0); private static final Long MAX_POOL = Long.MAX_VALUE; private final Lock lock = new ReentrantLock(); @Override public void afterPropertiesSet() { if (this.writeDataSource == null) { throw new IllegalArgumentException("Property 'writeDataSource' is required"); } setDefaultTargetDataSource(writeDataSource); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DbType.WRITE.name(), writeDataSource); if (this.readDataSources == null) { readDataSourceSize = 0; } else { for (int i = 0; i < readDataSources.size(); i++) { targetDataSources.put(DbType.READ.name() + i, readDataSources.get(i)); } readDataSourceSize = readDataSources.size(); } setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { DbType dynamicDataSourceGlobal = DbContextHolder.getDbType(); if (dynamicDataSourceGlobal == null || dynamicDataSourceGlobal == DbType.WRITE || readDataSourceSize <= 0) { return DbType.WRITE.name(); } int index = 1; if (readDataSourcePollPattern == 1) { // 轮询方式 long currValue = counter.incrementAndGet(); if ((currValue + 1) >= MAX_POOL) { try { lock.lock(); if ((currValue + 1) >= MAX_POOL) { counter.set(0); } } finally { lock.unlock(); } } index = (int) (currValue % readDataSourceSize); } else { // 随机方式 index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize); } return dynamicDataSourceGlobal.name() + index; } public void setWriteDataSource(Object writeDataSource) { this.writeDataSource = writeDataSource; } public void setReadDataSources(List<Object> readDataSources) { this.readDataSources = readDataSources; } public void setReadDataSourcePollPattern(int readDataSourcePollPattern) { this.readDataSourcePollPattern = readDataSourcePollPattern; } }
7、数据源配置(这里是在mybatis的配置文件配置数据源)
<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" /> <!-- 配置获取连接等待超时的时间 --> <property name="maxWait" value="60000" /> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000" /> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="300000" /> <property name="validationQuery" value="SELECT 'x'" /> <property name="testWhileIdle" value="true" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 --> <property name="poolPreparedStatements" value="true" /> <property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> <property name="filters" value="config" /> </bean> <bean id="dataSourceRead1" parent="abstractDataSource"> <property name="driverClassName" value="org.postgresql.Driver" /> <!-- 基本属性 url、user、password --> <property name="url" value="${read1.jdbc.url}" /> <property name="username" value="${read1.jdbc.user}" /> <property name="password" value="${read1.jdbc.password}" /> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="${read1.jdbc.initPoolSize}" /> <property name="minIdle" value="${read1.jdbc.minPoolSize}" /> <property name="maxActive" value="${read1.jdbc.maxPoolSize}" /> </bean> <bean id="dataSourceWrite" parent="abstractDataSource"> <property name="driverClassName" value="org.postgresql.Driver" /> <!-- 基本属性 url、user、password --> <property name="url" value="${write.jdbc.url}" /> <property name="username" value="${write.jdbc.user}" /> <property name="password" value="${write.jdbc.password}" /> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="${write.jdbc.initPoolSize}" /> <property name="minIdle" value="${write.jdbc.minPoolSize}" /> <property name="maxActive" value="${write.jdbc.maxPoolSize}" /> </bean> <!--配置了一个读一个学的资源--> <bean id="dataSource" class="com.oadmin.superapp.config.RoutingDataSource"> <property name="writeDataSource" ref="dataSourceWrite" /> <property name="readDataSources"> <list> <ref bean="dataSourceRead1" /> </list> </property> <!--轮询方式 --> <property name="readDataSourcePollPattern" value="1" /> <property name="defaultTargetDataSource" ref="dataSourceWrite" /> </bean>
<!--配置事务管理--> <tx:annotation-driven transaction-manager="transactionManager" order="100" /> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
8、properties文件文件配置
# Slave DataSource #server.port=8011 server.contextPath=/oadmin read1.jdbc.url=jdbc:postgresql://172.16.1.200:54321/database read1.jdbc.user=postgres read1.jdbc.password=123456 read1.jdbc.initPoolSize=1 read1.jdbc.minPoolSize=1 read1.jdbc.maxPoolSize=10 # Master DataSource write.jdbc.url=jdbc:postgresql://172.16.1.200:54321/database write.jdbc.user=postgres write.jdbc.password=123456 write.jdbc.initPoolSize=1 write.jdbc.minPoolSize=1 write.jdbc.maxPoolSize=10