Spring Boot 和 MyBatis 实现多数据源、动态数据源切换
本项目使用 Spring Boot 和 MyBatis 实现多数据源,动态数据源的切换;有多种不同的实现方式,在学习的过程中发现没有文章将这些方式和常见的问题集中处理,所以将常用的方式和常见的问题都写在了在本项目的不同分支上:
- master: 使用了多数据源的 RESTful API 接口,使用 Druid 实现了 DAO 层数据源动态切换和只读数据源负载均衡
- dev: 最简单的切面和注解方式实现的动态数据源切换
- druid: 通过切面和注解方式实现的使用 Druid 连接池的动态数据源切换
- aspect_dao: 通过切面实现的 DAO 层的动态数据源切换
- roundrobin: 通过切面使用轮询方式实现的只读数据源负载均衡
以上分支都是基于 dev 分支修改或扩充而来,基本涵盖了常用的多数据源动态切换的方式,基本的原理都一样,都是通过切面根据不同的条件在执行数据库操作前切换数据源
在使用的过程中基本踩遍了所有动态数据源切换的坑,将常见的一些坑和解决方法写在了 Issues 里面
该项目使用了一个可写数据源和多个只读数据源,为了减少数据库压力,使用轮循的方式选择只读数据源;考虑到在一个 Service 中同时会有读和写的操作,所以本应用使用 AOP 切面通过 DAO 层的方法名切换只读数据源;但这种方式要求数据源主从一致,并且应当避免在同一个 Service 方法中写入后立即查询,如果必须在执行写入操作后立即读取,应当在 Service 方法上添加
@Transactional注解以保证使用主数据源
需要注意的是,使用 DAO 层切面后不应该在 Service 类层面上加
@Transactional注解,而应该添加在方法上,这也是 Spring 推荐的做法
动态切换数据源依赖
configuration包下的4个类来实现,分别是:
- DataSourceRoutingDataSource.java
- DataSourceConfigurer.java
- DynamicDataSourceContextHolder.java
- DynamicDataSourceAspect.java
添加依赖
1 | dependencies { |
创建数据库及表
- 分别创建数据库
product_master,product_slave_alpha,product_slave_beta,product_slave_gamma - 在以上数据库中分别创建表
product,并插入不同数据
1 | DROP DATABASE IF EXISTS product_master; |
配置数据源
- application.properties
1 | # Master datasource config |
配置数据源
DataSourceKey.java
1
2
3
4
5
6
7
8
9package cn.com.hellowood.dynamicdatasource.common;
public enum DataSourceKey {
master,
slaveAlpha,
slaveBeta,
slaveGamma
}DataSourceRoutingDataSource.java
该类继承自
AbstractRoutingDataSource类,在访问数据库时会调用该类的determineCurrentLookupKey()方法获取数据库实例的 key
1 | package cn.com.hellowood.dynamicdatasource.configuration; |
- DataSourceConfigurer.java
数据源配置类,在该类中生成多个数据源实例并将其注入到
ApplicationContext中
1 | package cn.com.hellowood.dynamicdatasource.configuration; |
- DynamicDataSourceContextHolder.java
该类为数据源上下文配置,用于切换数据源
1 | package cn.com.hellowood.dynamicdatasource.configuration; |
- DynamicDataSourceAspect.java
动态数据源切换的切面,切 DAO 层,通过 DAO 层方法名判断使用哪个数据源,实现数据源切换
关于切面的 Order 可以可以不设,因为@Transactional是最低的,取决于其他切面的设置,并且在org.springframework.core.annotation.AnnotationAwareOrderComparator会重新排序
1 | package cn.com.hellowood.dynamicdatasource.configuration; |
配置 Product REST API 接口
- ProductController.java
1 | package cn.com.hellowood.dynamicdatasource.controller; |
ProductService.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58package cn.com.hellowood.dynamicdatasource.service;
import cn.com.hellowood.dynamicdatasource.mapper.ProductDao;
import cn.com.hellowood.dynamicdatasource.modal.Product;
import cn.com.hellowood.dynamicdatasource.utils.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public class ProductService {
private ProductDao productDao;
public Product select(long productId) throws ServiceException {
Product product = productDao.select(productId);
if (product == null) {
throw new ServiceException("Product:" + productId + " not found");
}
return product;
}
public Product update(long productId, Product newProduct) throws ServiceException {
if (productDao.update(newProduct) <= 0) {
throw new ServiceException("Update product:" + productId + "failed");
}
return newProduct;
}
public boolean add(Product newProduct) throws ServiceException {
Integer num = productDao.insert(newProduct);
if (num <= 0) {
throw new ServiceException("Add product failed");
}
return true;
}
public boolean delete(long productId) throws ServiceException {
Integer num = productDao.delete(productId);
if (num <= 0) {
throw new ServiceException("Delete product:" + productId + "failed");
}
return true;
}
public List<Product> getAllProduct() {
return productDao.getAllProduct();
}
}ProductDao.java
1 | package cn.com.hellowood.dynamicdatasource.mapper; |
- ProductMapper.xml
启动项目,此时访问
/product/1会返回product_master数据库中product表中的所有数据,多次访问/product会分别返回product_slave_alpha、product_slave_beta、product_slave_gamma数据库中product表中的数据,同时也可以在看到切换数据源的 log,说明动态切换数据源是有效的
注意
在该应用中因为使用了 DAO 层的切面切换数据源,所以
@Transactional注解不能加在类上,只能用于方法;有@Trasactional注解的方法无法切换数据源