使用 SpringBoot+JPA多数据源,动态数据源以及分库分表的实现

 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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值