说明
springboot中可以定义多个数据源,通过AbstractRoutingDataSource + AOP可以实现读写分离。
如下示例中:
- 有三个mysql节点,一个主节点两个从节点。
- 自定义了一个注解@DBReadOnly,有此方法的注解,表示为只读操作,使用从节点。无此注解的方法,不管是读还是写,均使用主节点。
示例
pom.xml
添加mysql/aop的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
DbConfiguration
定义了三个数据源、事务管理器、JdbcTemplate。
/**
* 配置类,创建DataSource/JdbcTemplate/TransactionManager的Bean实例
*/
@Configuration
public class DbConfiguration {
private Logger logger = LoggerFactory.getLogger(getClass());
@Value("${spring.datasource.read1.url:}")
private String read1Url;
@Value("${spring.datasource.read2.url:}")
private String read2Url;
@Bean
@Primary
@ConfigurationProperties("spring.datasource.write")
public DataSourceProperties writeDataSourceProperties() {
logger.info("create WRITE PROP");
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.write.hikari")
public HikariDataSource writeDataSource() {
logger.info("create WRITE DS");
return writeDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.read1")
public DataSourceProperties read1DataSourceProperties() {
if (read1Url != null && read1Url.length() != 0) {
logger.info("create READ1 PROP");
return new DataSourceProperties();
} else {
logger.info("READ1 use WRITE PROP");
return writeDataSourceProperties();
}
}
@Bean
@ConfigurationProperties("spring.datasource.read1.hikari")
public HikariDataSource read1DataSource() {
if (read1Url != null && read1Url.length() != 0) {
logger.info("create READ1 DS");
return read1DataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
} else {
logger.info("READ1 use WRITE DS");
return writeDataSource();
}
}
@Bean
@ConfigurationProperties("spring.datasource.read2")
public DataSourceProperties read2DataSourceProperties() {
if (read2Url != null && read2Url.length() != 0) {
logger.info("create READ2 PROP");
return new DataSourceProperties();
} else {
logger.info("READ2 use WRITE PROP");
return writeDataSourceProperties();
}
}
@Bean
@ConfigurationProperties("spring.datasource.read2.hikari")
public HikariDataSource read2DataSource() {
if (read2Url != null && read2Url.length() != 0) {
logger.info("create READ2 DS");
return read2DataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
} else {
logger.info("READ2 use WRITE DS");
return writeDataSource();
}
}
@Bean
public AbstractRoutingDataSource routingDataSource() {
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(3);
targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
targetDataSources.put(DbContextHolder.READ + "1", read1DataSource());
targetDataSources.put(DbContextHolder.READ + "2", read2DataSource());
proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
@Bean
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jt = new JdbcTemplate();
jt.setDataSource(routingDataSource());
return jt;
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
}
DbContextHolder
此类中的contextHolder变量,存储着各线程是使用写库,还是使用读库。
public class DbContextHolder {
private static Logger log = LoggerFactory.getLogger(DbContextHolder.class);
public static final String WRITE = "write";
public static final String READ = "read";
private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDbType(String dbType) {
if (dbType == null) {
log.error("dbType为空");
throw new NullPointerException();
}
log.info("设置dbType为:{}", dbType);
contextHolder.set(dbType);
}
public static String getDbType() {
return contextHolder.get() == null ? WRITE : contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
log.info("清除dbType");
}
}
DBReadOnly
注解类,以此注解标注的方法,使用读库。
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DBReadOnly {
}
DBReadOnlyAspect
切面类,此切面的以上述的DBReadOnly注解为切点。
此注解标注的方法,执行前设置DbContextHolder为使用读库,执行后清除DbContextHolder。
@Aspect
@Component
public class DBReadOnlyAspect {
@Around("@annotation(DBReadOnly)")
public Object read(ProceedingJoinPoint joinPoint) throws Throwable {
try {
DbContextHolder.setDbType(DbContextHolder.READ);
return joinPoint.proceed();
} finally {
DbContextHolder.clearDbType();
}
}
}
MyAbstractRoutingDataSource
获取数据源前,会调用此类中的determineCurrentLookupKey()方法,通过此方法返回值,判断使用哪个数据源。
在DbConfiguration中routingDataSource()方法中,可以看到,声明MyAbstractRoutingDataSource的Bean时,向其中设置了一个HashMap,其中key为一个字符串,value为一个数据源。determineCurrentLookupKey()方法的返回值,即为这个HashMap的key,如果此方法返回的key在HashMap中不存在,则会使用默认的数据源。
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DbContextHolder.getDbType();
if (typeKey == DbContextHolder.WRITE) {
logger.info("use WRITE DS");
return typeKey;
}
int random = getRandom();
logger.info("use READ" + random + " DS");
return DbContextHolder.READ + String.valueOf(random);
}
/**
* 获取随机数
*
* @return 1或2
*/
private int getRandom() {
int result = new Random().nextInt(2) + 1;
return result;
}
}
TestDao
数据库操作类,此类中有三个方法,其中有一个方法标注了@DBReadOnly,调用此方法时,会使用读库。
@Component
public class TestDao {
@Autowired
private JdbcTemplate jdbc;
@DBReadOnly
public List<Map<String, Object>> query() {
List<Map<String, Object>> result = null;
String sql = "select ip_addr from test.t_syb";
result = jdbc.queryForList(sql);
return result;
}
public void insert() {
String addSql = "insert into test.t_syb values (‘xxx‘)";
jdbc.update(addSql);
}
public void delete() {
String sql = "delete from test.t_syb where ip_addr=‘xxx‘";
jdbc.update(sql);
}
}
TestCtrl
此类是一个RestController,仅是为了测试TestDao方法调用的。
@RestController
public class TestCtrl {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private TestDao testDao;
@RequestMapping("/query")
public Object query(@RequestParam(required = false) String type) {
List<Map<String, Object>> list = null;
try {
list = testDao.query();
} catch (Exception e) {
logger.error("", e);
}
return list;
}
@RequestMapping("/insert")
public Object insert() {
try {
testDao.insert();
} catch (Exception e) {
logger.error("", e);
}
return "OK";
}
@RequestMapping("/delete")
public Object delete() {
try {
testDao.delete();
} catch (Exception e) {
logger.error("", e);
}
return "OK";
}
}
注意
判断是否使用读库,是通过AOP实现的,而为使AOP生效,一定要使用代理调用,即先获取TestDao的Bean实例,然后通过Bean实例去调用方法。
如果是在TestDao实例内部进行调用,AOP是不会生效的。
spring @Transactional注解也是一样的原理,需要注意。