背景 数据库配置主从之后,如何在代码层面实现读写分离?
用户自定义设置数据库路由 Spring boot提供了AbstractRoutingDataSource
根据用户定义的规则选择当前的数据库,这样我们可以在执行查询之前,设置读取从库,在执行完成后,恢复到主库。
实现可动态路由的数据源,在每次数据库查询操作前执行
ReadWriteSplitRoutingDataSource.java 1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class ReadWriteSplitRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { return DbContextHolder.getDbType(); } }
线程私有路由配置,用于ReadWriteSplitRoutingDataSource
动态读取配置
DbContextHolder.java 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 public class DbContextHolder { public enum DbType { MASTER, SLAVE } private static final ThreadLocal<DbType> contextHolder = new ThreadLocal <>(); public static void setDbType (DbType dbType) { if (dbType == null ){ throw new NullPointerException (); } contextHolder.set(dbType); } public static DbType getDbType () { return contextHolder.get() == null ? DbType.MASTER : contextHolder.get(); } public static void clearDbType () { contextHolder.remove(); } }
AOP优化代码 利用AOP将设置数据库的操作从代码中抽离,这里的粒度控制在方法级别,所以利用注解的形式标注这个方法涉及的数据库事务只读,走从库。
只读注解,用于标注方法的数据库操作只走从库。
ReadOnlyConnection.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.wallstreetcn.hatano.config;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ReadOnlyConnection {}
ReadOnlyConnectionInterceptor.java 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 import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;@Aspect @Component public class ReadOnlyConnectionInterceptor implements Ordered { private static final Logger logger = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class); @Around("@annotation(readOnlyConnection)") public Object proceed (ProceedingJoinPoint proceedingJoinPoint, ReadOnlyConnection readOnlyConnection) throws Throwable { try { logger.info("set database connection to read only" ); DbContextHolder.setDbType(DbContextHolder.DbType.SLAVE); Object result = proceedingJoinPoint.proceed(); return result; } finally { DbContextHolder.clearDbType(); logger.info("restore database connection" ); } } @Override public int getOrder () { return 0 ; } }
UserService.java 1 2 3 4 @ReadOnlyConnection public List<User> getUsers (Integer page, Integer limit) { return repository.findAll(new PageRequest (page, limit)); }
配置Druid数据库连接池 build.gradle 1 compile ("com.alibaba:druid:1.0.18" )
groovy依赖注入 配置dataSource为可路由数据源
context.groovy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import com.alibaba.druid.pool.DruidDataSourceimport DbContextHolderimport ReadWriteSplitRoutingDataSource** SOME INITIALIZED CODE LOAD PROPERTIES ** def dataSourceMaster = new DruidDataSource()dataSourceMaster.url = properties.get('datasource.master.url' ) println("master set to " + dataSourceMaster.url) dataSourceMaster.username = properties.get('datasource.master.username' ) dataSourceMaster.password = properties.get('datasource.master.password' ) def dataSourceSlave = new DruidDataSource()dataSourceSlave.url = properties.get('datasource.slave.url' ) println("slave set to " + dataSourceSlave.url) dataSourceSlave.username = properties.get('datasource.slave.username' ) dataSourceSlave.password = properties.get('datasource.slave.password' ) beans { dataSource(ReadWriteSplitRoutingDataSource) { bean -> targetDataSources = [ (DbContextHolder.DbType.MASTER): dataSourceMaster, (DbContextHolder.DbType.SLAVE): dataSourceSlave ] } }
参考资料
Dynamic DataSource Routing with Spring @Transactional
Alibaba Druid