SpringBoot+JPA多数据源,动态数据源以及分库分表的实现
前言:本来想使用sharding-jdbc来实现这个,但是又不想不太熟悉新的框架而存在太多的坑而导致出现无法预测的问题或者及时的解决问题。因此按照实际开发习惯,使用原来简单项目,不引入新的框架,对JPA进行封装来实现。
jpa官网链接:jpa官网链接
1、使用前须知:
- 采用的数据库连接池:Druid
- 分库(多数据源实现采用):注解 + AOP 的方式实现(数据源注解可以在方法上,也可以在类上,当然规则可以自己在aop代码中进行更改,注解的值可以使用动态的替换值)
- 如果多个数据源的表结构大不相同,则不推荐使用,但是如果字段相同,只有表名不同依旧可以使用。
- 分表实现采用拦截jpa底层生成sql,对表名进行替换
2、使用方法:
1、controller层中
CenterFactory.setCenterInfoByTypeAndValue(CenterUtil.ORDER_ID, custOrderId);
这一句是为了设置当前动态数据源归属,如ORDER分为四个库,根据客户订单编号规则获取到究竟是属于哪一个库的最终定位到类似ORDER2
@RestController
public class indexController {
@Autowired
private IUserSV userSV;
@RequestMapping("/test")
public List<User> test(Long custOrderId) {
//这一句是为了设置当前动态数据源归属,如ORDER分为四个库,根据客户订单编号规则获取到究竟是属于哪一个库的最终定位到类似ORDER2
CenterFactory.setCenterInfoByTypeAndValue(CenterUtil.ORDER_ID, custOrderId);
return userSV.findByCustOrderId(custOrderId);
}
}
2、在SV层中
在service方法中添加自定义注解,@DataSource(source = "ORDER{CENTER}") source的值为数据库yml配置的值,{CENTER}为分库替换变量。
@Service
public class UserSVImpl implements IUserSV {
@Autowired
private IUserDAO dao;
@DataSource(source = "ORDER{CENTER}")
@Override
public List<User> findByCustOrderId(Long custOrderId) {
return dao.findByCustOrderId(custOrderId);
}
}
3、在pojo对象中
{REGION}为分表,具体分表规则自定义,当然如果不闲麻烦的话,仍然@Query可以通过入参对表名进行传递。
@Entity(name = "user_{REGION}")
public class User {
@Column
public Long custOrderId;
@Column
public String remark;
public Long getCustOrderId() {
return custOrderId;
}
public void setCustOrderId(Long custOrderId) {
this.custOrderId = custOrderId;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
以下即是对框架的核心类进行解析
其实使用起来就是两个核心,一个是在通过对注解的拦截进行设置数据源,一个是对sql的拦截进行分表设置。
依赖
需要依赖的包除非就是springboot以及Jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
配置文件
server:
port: 12305
spring:
#默认的数据源
datasource:
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.1:1521/test1
rname: tempquery
password: tempquery
validationQuery: SELECT 1 FROM DUAL
main:
allow-bean-definition-overriding: true
jpa:
database: oracle
show-sql: true
properties:
#不加此配置,获取不到当前currentsession
hibernate:
current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
#拦截sql,进行分表
session_factory:
statement_inspector: com.order.config.source.aop.JpaInterceptor
open-in-view: false
#配置日志
logging:
#配置日志文件路径
path: log
level:
xatu.zsl: debug #不同目录下的日志可配置不同级别
org.springfromework.web: info
mydatasources:
#主动开启多数据源
multiDatasourceOpen: true
datasource[0]:
dbName: ORDER1
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test2
username: ORDER1
password: ORDER1
datasource[1]:
dbName: ORDER2
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test3
username: ORDER2
password: ORDER2
datasource[2]:
dbName: ORDER3
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test4
username: ORDER3
password: ORDER3
datasource[3]:
dbName: ORDER4
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test5
username: ORDER4
password: ORDER4
datasource[4]:
dbName: CFG1
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test6
username: CFG1
password: CFG1234
datasource[5]:
dbName: CFG2
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test7
username: CFG2
password: CFG1234
datasource[6]:
dbName: CFG3
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test8
username: CFG3
password: CFG1234
datasource[7]:
dbName: CFG4
driverClassName: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.0.1:1521/test9
username: CFG4
password: CFG1234
添加注解类
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface DataSource {
String source() default "ORD_CFG";
}
数据源配置映射 yml
配置类
public class DruidProperties {
private final static Logger log = LoggerFactory.getLogger(DruidProperties.class);
public DruidProperties() {
log.info("default 数据源加载");
}
/**
* 数据源名称
*/
private String dbName = "CFG";
private String url;
private String username;
private String password;
/**
* 默认为 oracle 配置
*/
private String driverClassName = "oracle.jdbc.driver.OracleDriver";
private Integer initialSize = 10;
private Integer minIdle = 3;
private Integer maxActive = 60;
private Integer maxWait = 60000;
private Boolean removeAbandoned = true;
private Integer removeAbandonedTimeout = 180;
private Integer timeBetweenEvictionRunsMillis = 60000;
private Integer minEvictableIdleTimeMillis = 300000;
//用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
private String validationQuery = "SELECT 1 FROM DUAL";
//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,
//如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
private Boolean testWhileIdle = true;
//申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
private Boolean testOnBorrow = false;
//归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
private Boolean testOnReturn = false;
//PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭
private Boolean poolPreparedStatements = true;
private Integer maxPoolPreparedStatementPerConnectionSize = 50;
//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
//监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
private String filters = "stat";
}
多数据源切面类AOP:
/**
* @ClassName: MultiDataSourceAop.java
* @Description: 分库,动态切换数据源
* @version: v1.0.0
* @author: yulang
* @date: 2019/12/5 15:41
* <p>
* Modification History:
* Date Author Version Description
* ------------------------------------------------------------
* 2019/12/5 yulang v1.0.0 第一次创建
*/
@Aspect
@Component
@ConditionalOnProperty(prefix = "mydatasources", name = "multiDatasourceOpen", havingValue = "true")
public class MultiDataSourceAop implements Ordered {
private Logger log = LoggerFactory.getLogger(this.getClass());
public MultiDataSourceAop() {
log.info("多数据源初始化 AOP ");
}
@Pointcut(value = "@annotation(org.yulang.config.annotation.DataSource)")
private void cut() {
}
@Around("cut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
//获取当点方法的注解
Object target = point.getTarget();
Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
DataSource datasource = currentMethod.getAnnotation(DataSource.class);
if (datasource != null) {
DynamicDataSource.setDataSourceDbName(getDataSource(datasource));
} else {
// 获取类上的注解
datasource = point.getTarget().getClass().getAnnotation(DataSource.class);
DynamicDataSource.setDataSourceDbName(getDataSource(datasource));
if (datasource ==null){
DynamicDataSource.setDataSourceDbName("CFG");
log.info("设置数据源为:默认 --> CFG");
}
}
try {
return point.proceed();
} finally {
log.info("清空数据源信息!");
DynamicDataSource.clearDataSourceDbName();
}
}
private String getDataSource(DataSource datasource){
String source = datasource.source();
if (source.contains("{CENTER}")){
CenterInfo centerInfo = CenterFactory.getCenterInfo();
source = source.replace("{CENTER}",centerInfo.getCenter());
}
log.info("设置数据源为:" + source);
return source;
}
/**
* aop的顺序要早于spring的事务
*/
@Override
public int getOrder() {
return 1;
}
}
多数据源配置类:
@Component
public class MultiSourceConfig {
private final static Logger log = LoggerFactory.getLogger(MultiSourceConfig.class);
@Autowired
private DruidProperties druidProperties;
@Autowired
private MultiDataSource multiDataSource;
/**
* 单数据源连接池配置
*/
@Bean
@ConditionalOnProperty(name = "mydatasources.multiDatasourceOpen", havingValue = "false")
public DruidDataSource singleDatasource() {
log.error("singleDatasource");
return druidProperties.config(new DruidDataSource());
}
/**
* 多数据源连接池配置
*/
@Bean
@ConditionalOnProperty(name = "mydatasources.multiDatasourceOpen", havingValue = "true")
public DynamicDataSource mutiDataSource() {
log.error("mutiDataSource");
//存储数据源别名与数据源的映射
HashMap<Object, Object> dbNameMap = new HashMap<>();
// 核心数据源
DruidDataSource mainDataSource = druidProperties.config();
// 这里添加 主要数据库,其它数据库挂了,默认使用主数据库
dbNameMap.put("ORD_CFG", mainDataSource);
// 其它数据源
// 当前多数据源是否存在
if (multiDataSource.getDatasource() != null) {
//过滤掉没有添加 dbName 的数据源,先加载全局配置,再次加载当前配置
List<DruidDataSource> multiDataSourceList = multiDataSource.getDatasource().stream()
.filter(dp -> !"".equals(Optional.ofNullable(dp.getDbName()).orElse("")))
.map(dp -> {
DruidDataSource druidDataSource = dp.config(druidProperties.config());
dbNameMap.put(dp.getDbName(), druidDataSource);
return druidDataSource;
})
.collect(Collectors.toList());
// 测试所有的数据源
/* try {
mainDataSource.init();
for (DruidDataSource druidDataSource : multiDataSourceList) {
druidDataSource.init();
}
} catch (SQLException sql) {
log.error("======================= 多数据源配置错误 ==========================");
sql.printStackTrace();
}*/
}
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(dbNameMap);
dynamicDataSource.setDefaultTargetDataSource(mainDataSource);
return dynamicDataSource;
}
}
设置中心类
其中initMap方法为对应分库的配置方法。
具体可以根据数据的哪一列进行分库分表 如下客户订单编号前三位为100,200将会路由到数据1中心,
当然如果知道当前数据处于哪个分区(REGION),则可以直接根据REGION设置分区
/**
* @ClassName: com.order.common.center
* @Description: 设置中心,简易版
* @version: v1.0.0
* @author: yulang
* @date: 2019/12/5 12:09
* <p>
* Modification History:
* Date Author Version Description
* ------------------------------------------------------------
* 2019/12/5 yulang v1.1.0 第一次创建
*/
public class CenterFactory {
//设中心
private static Map<String, String> mapCenter;
private static final ThreadLocal CENTER_INFO = new ThreadLocal();
static {
if (mapCenter == null) {
initMap();
}
}
public static CenterInfo getCenterInfo() {
if (CENTER_INFO.get() == null) {
throw new RuntimeException("没有设置中心!");
} else {
return (CenterInfo)CENTER_INFO.get();
}
}
public static void setCenterInfoByTypeAndValue(String type, String region) {
CENTER_INFO.set(new CenterInfo(region,mapCenter.get(region)));
}
public static void setCenterInfoByTypeAndValue(String type, Long value) {
String region = value.toString();
if (type.equals(CenterUtil.ORDER_ID)){
region = region.substring(0, 3);
}
setCenterInfoByTypeAndValue(CenterUtil.REGION_ID,region);
}
public static Map<String, String> initMap() {
mapCenter = new HashMap<>();
//todo 具体可以根据数据的哪一列进行分库分表 如下客户订单编号前三位为100,200将会路由到数据1中心,
// 当然如果知道分区,可以直接根据REGION设置分区
mapCenter.put("100", "1");
mapCenter.put("200", "1");
mapCenter.put("300", "2");
mapCenter.put("400", "2");
mapCenter.put("500", "3");
mapCenter.put("600", "3");
mapCenter.put("700", "4");
mapCenter.put("800", "4");
mapCenter.put("900", "4");
return mapCenter;
}
}
CenterInfo对象
public class CenterInfo implements Serializable {
private String center = null;//划分出多少个库
private String regionId = null;//一个库中划分出多少分表
private String jvmid = null;
private Date date = null;//查询指定年月日
}
当然会存在多数据源情况下的事物问题,我们可以抽取一层sv层专注事物控制。
最简单的sv层不做事物,只做表的增删改查不做业务逻辑以及事物提交。
附录:
github源码地址:https://github.com/yulangde/coll/tree/master/mds