jpa 自定义sql if_SpringBoot整合JPA实现多数据源及读写分离
SpringBoot整合JPA实现多数据源及读写分离
项目地址:https://github.com/baojingyu/spring-boot-jpa-dynamic-datasource
本项目使用 SpringBoot 和 SpringData JPA 实现多数据源,动态数据源的切换。
前段时间写 MySQL 主从复制的技术分享,我就在想,既然主从复制都搭建好了,不写个 Demo 玩玩读写分离,好像差点意思哼~ 于是就有了这么个 Demo Project。
一、什么是读写分离?
读写分离其实就是将数据库分为主从库,简单点说是主库处理事务性增、改、删操作,多个从库处理查询操作。主库的事务性操作导致的变更通过BinLog 日志同步到集群中的从库。
二、为什么要读写分离?
其实最主要是减轻数据库的压力。数据库的写操作比较耗时,如果没有进行读写分离,写操作将会影响到读操作的效率。
三、什么时候要读写分离?
如果程序读多写少,那么可以考虑读写分离,利用数据库主从同步,能减轻数据库压力,提高性能。
四、读写分离方案
数据库读远大于写,查询多的情况,那就得考虑主库负责写操作,多个从库负责读操作,另外结合 Redis 等缓存来配合分担数据库的读操作。
五、代码层面进行读写分离
代码环境是SpringBoot + SpringData JPA + Druib连接池。想要读写分离就需要配置多个数据源,在进行写操作时选择写的数据源,读操作时选择读的数据源。其中有两个关键点:
- 如何切换数据源
- 如何根据不同的方法选择正确的数据源
1)、如何切换数据源
通常用 SpringBoot 时都是使用它的默认配置,只需要在配置文件中定义好连接属性就行了,但是现在我们需要自己来配置了,Spring 是支持多数据源的,多个datasource放在一个HashMapTargetDataSource中,通过dertermineCurrentLookupKey获取key来决定要使用哪个数据源。因此我们的目标就很明确了,建立多个datasource放到TargetDataSource中,同时重写dertermineCurrentLookupKey方法来决定使用哪个key。
用户自定义设置数据库路由
SpringBoot 提供了 AbstractRoutingDataSource 根据用户定义的规则选择当前的数据库,这样我们可以在执行查询之前,设置读取从库,在执行完成后,恢复到主库。AbstractRoutingDataSource 就是DataSource 的抽象,基于 lookupKey 的方式在多个数据库中进行切换。重点关注setTargetDataSources,setDefaultTargetDataSource,determineCurrentLookupKey三个方法。那么AbstractRoutingDataSource就是Spring读写分离的关键了。
实现可动态路由的数据源,在每次数据库查询操作前执行。
2)、如何选择数据源
事务一般是注解在Service层的,因此在开始这个service方法调用时要确定数据源,有什么通用方法能够在开始执行一个方法前做操作呢?相信你已经想到了那就是切面 。怎么切有两种办法:
注解式,定义一个只读注解,被该数据标注的方法使用读库 方法名,根据方法名写切点,比如getXXX用读库,setXXX用写库
六、部分代码
DataSourcesConfig
/** * @author jingyu.bao * @version 1.0 * @className DataSourceConfig * @description * @date 7/5/2020 20:09 **/@EnableTransactionManagement@Configurationpublic class DataSourceConfig { @Value("${spring.datasource.druid.master.name}") private String masterName; @Value("${spring.datasource.druid.master.url}") private String masterUrl; @Value("${spring.datasource.druid.master.username}") private String masterUsername; @Value("${spring.datasource.druid.master.password}") private String masterPassword; @Value("${spring.datasource.druid.master.driver-class-name}") private String masterDriverClassName; @Value("${spring.datasource.druid.slave.name}") private String slaveName; @Value("${spring.datasource.druid.slave.url}") private String slaveUrl; @Value("${spring.datasource.druid.slave.username}") private String slaveUsername; @Value("${spring.datasource.druid.slave.password}") private String slavePassword; @Value("${spring.datasource.druid.slave.driver-class-name}") private String slaveDriverClassName; @Value("${spring.datasource.druid.initial-size}") private String initialSize; @Value("${spring.datasource.druid.min-idle}") private String minIdle; @Value("${spring.datasource.druid.max-active}") private String maxActive; @Value("${spring.datasource.druid.max-wait}") private String maxWait; @Value("${spring.datasource.druid.time-between-eviction-runs-millis}") private String timeBetweenEvictionRunsMillis; @Value("${spring.datasource.druid.min-evictable-idle-time-millis}") private String minEvictableIdleTimeMillis; @Value("${spring.datasource.druid.validation-query}") private String validationQuery; @Value("${spring.datasource.druid.test-while-idle}") private String testWhileIdle; @Value("${spring.datasource.druid.test-on-borrow}") private String testOnBorrow; @Value("${spring.datasource.druid.test-on-return}") private String testOnReturn; @Value("${spring.datasource.druid.filters}") private String filters; @Value("{spring.datasource.druid.filter.stat.log-slow-sql}") private String logSlowSql; @Value("{spring.datasource.druid.filter.stat.slow-sql-millis}") private String slowSqlMillis; @Value("${spring.datasource.druid.type}") private String type; @Value("${spring.datasource.druid.stat-view-servlet.login-username}") private String loginUserName; @Value("${spring.datasource.druid.stat-view-servlet.login-password}") private String password; @Bean(name = "masterDataSource") public DataSource masterDataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(masterUrl); datasource.setUsername(masterUsername); datasource.setPassword(masterPassword); datasource.setDriverClassName(masterDriverClassName); //configuration if (!StringUtils.isEmpty(initialSize)) { datasource.setInitialSize(Integer.parseInt(initialSize)); } if (!StringUtils.isEmpty(minIdle)) { datasource.setMinIdle(Integer.parseInt(minIdle)); } if (!StringUtils.isEmpty(maxActive)) { datasource.setMaxActive(Integer.parseInt(maxActive)); } if (!StringUtils.isEmpty(maxWait)) { datasource.setMaxWait(Integer.parseInt(maxWait)); } if (!StringUtils.isEmpty(timeBetweenEvictionRunsMillis)) { datasource.setTimeBetweenEvictionRunsMillis(Integer.parseInt(timeBetweenEvictionRunsMillis)); } if (!StringUtils.isEmpty(minEvictableIdleTimeMillis)) { datasource.setMinEvictableIdleTimeMillis(Integer.parseInt(minEvictableIdleTimeMillis)); } if (!StringUtils.isEmpty(validationQuery)) { datasource.setValidationQuery(validationQuery); } if (!StringUtils.isEmpty(testWhileIdle)) { datasource.setTestWhileIdle(Boolean.parseBoolean(testWhileIdle)); } if (!StringUtils.isEmpty(testOnBorrow)) { datasource.setTestOnBorrow(Boolean.parseBoolean(testOnBorrow)); } if (!StringUtils.isEmpty(testOnReturn)) { datasource.setTestOnReturn(Boolean.parseBoolean(testOnReturn)); } try { datasource.setFilters(filters); } catch (SQLException e) { e.printStackTrace(); } return datasource; } @Bean(name = "slaveDataSource") public DataSource slaveDataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(masterUrl); datasource.setUsername(masterUsername); datasource.setPassword(masterPassword); datasource.setDriverClassName(masterDriverClassName); //configuration if (!StringUtils.isEmpty(initialSize)) { datasource.setInitialSize(Integer.parseInt(initialSize)); } if (!StringUtils.isEmpty(minIdle)) { datasource.setMinIdle(Integer.parseInt(minIdle)); } if (!StringUtils.isEmpty(maxActive)) { datasource.setMaxActive(Integer.parseInt(maxActive)); } if (!StringUtils.isEmpty(maxWait)) { datasource.setMaxWait(Integer.parseInt(maxWait)); } if (!StringUtils.isEmpty(timeBetweenEvictionRunsMillis)) { datasource.setTimeBetweenEvictionRunsMillis(Integer.parseInt(timeBetweenEvictionRunsMillis)); } if (!StringUtils.isEmpty(minEvictableIdleTimeMillis)) { datasource.setMinEvictableIdleTimeMillis(Integer.parseInt(minEvictableIdleTimeMillis)); } if (!StringUtils.isEmpty(validationQuery)) { datasource.setValidationQuery(validationQuery); } if (!StringUtils.isEmpty(testWhileIdle)) { datasource.setTestWhileIdle(Boolean.parseBoolean(testWhileIdle)); } if (!StringUtils.isEmpty(testOnBorrow)) { datasource.setTestOnBorrow(Boolean.parseBoolean(testOnBorrow)); } if (!StringUtils.isEmpty(testOnReturn)) { datasource.setTestOnReturn(Boolean.parseBoolean(testOnReturn)); } try { datasource.setFilters(filters); } catch (SQLException e) { e.printStackTrace(); } return datasource; } @Primary @Bean public DynamicRoutingDataSource dynamicDataSource(@Qualifier(value = "masterDataSource") DataSource masterDataSource, @Qualifier(value = "slaveDataSource") DataSource slaveDataSource) { Map targetDataSources = new HashMap<>(2); targetDataSources.put(DynamicRoutingDataSourceContext.MASTER, masterDataSource); targetDataSources.put(DynamicRoutingDataSourceContext.SLAVE, slaveDataSource); DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); //设置数据源 dynamicRoutingDataSource.setTargetDataSources(targetDataSources); //设置默认选择的数据源 dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource); dynamicRoutingDataSource.afterPropertiesSet(); return dynamicRoutingDataSource; } @Bean public ServletRegistrationBean statViewServlet() { ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); //设置ip白名单 servletRegistrationBean.addInitParameter("allow", ""); //设置ip黑名单,优先级高于白名单 servletRegistrationBean.addInitParameter("deny", ""); //设置控制台管理用户 servletRegistrationBean.addInitParameter("loginUsername", loginUserName); servletRegistrationBean.addInitParameter("loginPassword", password); //是否可以重置数据 servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; } @Bean public FilterRegistrationBean statFilter() { //创建过滤器 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); //设置过滤器过滤路径 filterRegistrationBean.addUrlPatterns("/*"); //忽略过滤的形式 filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; }
DynamicRoutingDataSource
/** * @author jingyu.bao * @version 1.0 * @className DynamicRoutingDataSource * @description * @date 7/5/2020 21:22 **/public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { Object lookupKey = DynamicRoutingDataSourceContext.getRoutingDataSource(); System.err.println(Thread.currentThread().getName() + " determineCurrentLookupKey : " + lookupKey); return lookupKey; }}
DynamicRoutingDataSourceContext
/** * @author jingyu.bao * @version 1.0 * @className DynamicRoutingDataSourceContext * @description * @date 7/5/2020 20:16 **/public class DynamicRoutingDataSourceContext { public static final String MASTER = "master"; public static final String SLAVE = "slave"; private static final ThreadLocal threadLocalDataSource = new ThreadLocal<>(); public static void setRoutingDataSource(Object dataSource) { if (dataSource == null) { throw new NullPointerException(); } threadLocalDataSource.set(dataSource); // System.err.println(Thread.currentThread().getName()+" set RoutingDataSource : " + dataSource); } public static Object getRoutingDataSource() { Object dataSourceType = threadLocalDataSource.get(); if (dataSourceType == null) { threadLocalDataSource.set(DynamicRoutingDataSourceContext.MASTER); return getRoutingDataSource(); } // System.err.println(Thread.currentThread().getName()+" get RoutingDataSource : " + dataSourceType); return dataSourceType; } public static void removeRoutingDataSource() { threadLocalDataSource.remove(); // System.err.println(Thread.currentThread().getName()+" remove RoutingDataSource"); }}
RoutingAopAspect
/** * @author jingyu.bao * @version 1.0 * @className RoutingAopAspect * @description * @date 7/5/2020 20:21 **/@Order(0)@Aspect@Componentpublic class RoutingAopAspect { @Around("@annotation(targetDataSource)") public Object routingWithDataSource(ProceedingJoinPoint joinPoint, TargetDataSource targetDataSource) throws Throwable { try { DynamicRoutingDataSourceContext.setRoutingDataSource(targetDataSource.value()); return joinPoint.proceed(); } finally { DynamicRoutingDataSourceContext.removeRoutingDataSource(); } }}
TargetDataSource
/** * @author jingyu.bao * @version 1.0 * @className TargetDataSource * @description * @date 7/5/2020 20:40 **/@Target({ElementType.METHOD, ElementType.TYPE})@Retention(value = RetentionPolicy.RUNTIME)@Documentedpublic @interface TargetDataSource { String value();}
UserInfoServiceImpl
/** * @author jingyu.bao * @version 1.0 * @className UserServiceImpl * @description * @date 7/5/2020 21:39 **/@Servicepublic class UserInfoServiceImpl implements IUserInfoService { @Autowired private IUserInfoRepository userInfoRepository; @TargetDataSource(value = "slave") @Override public List findAll() { return userInfoRepository.findAll(); } @Transactional @Override public UserInfoEntity save(UserInfoEntity userInfoEntity) { return userInfoRepository.save(userInfoEntity); } @TargetDataSource(value = "slave") @Override public UserInfoEntity findById(Long id) { Optional userInfoEntity = userInfoRepository.findById(id); return userInfoEntity.isPresent() ? userInfoEntity.get() : null; } @Override public List findAllMaster() { return userInfoRepository.findAll(); } @Transactional @Override public void saveAll(ArrayList list) { userInfoRepository.saveAll(list); }}
application.properties
server.port=8080server.tomcat.max-threads=3000server.tomcat.max-connections=20000server.tomcat.uri-encoding=UTF-8server.tomcat.accept-count=800# 自定义线程池参数fxea.threadPool.coreThreadNum=5fxea.threadPool.maxThreadNum=25# 这个参数是在建表的时候,将默认的存储引擎切换为 InnoDB 用的spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialectspring.jpa.database=mysql# 配置在日志中打印出执行的 SQL 语句信息。spring.jpa.show-sql=true# 第一次建表create,后面用update,要不然每次重启都会新建表spring.jpa.hibernate.ddl-auto=create# Druidspring.datasource.druid.type=com.alibaba.druid.pool.DruidDataSource#初始化连接大小spring.datasource.druid.initial-size=10#最小连接池数量spring.datasource.druid.min-idle=10#最大连接池数量spring.datasource.druid.max-active=100#配置获取连接等待超时的时间spring.datasource.druid.max-wait=60000#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒spring.datasource.druid.time-between-eviction-runs-millis=60000#配置一个连接在池中最小生存的时间,单位是毫秒spring.datasource.druid.min-evictable-idle-time-millis=300000#测试连接spring.datasource.druid.validation-query=SELECT 'x'#申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性spring.datasource.druid.test-while-idle=true#获取连接时执行检测,建议关闭,影响性能spring.datasource.druid.test-on-borrow=false#归还连接时执行检测,建议关闭,影响性能spring.datasource.druid.test-on-return=false#druid 用户spring.datasource.druid.stat-view-servlet.login-username=admin#druid 密码spring.datasource.druid.stat-view-servlet.login-password=admin#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙spring.datasource.druid.filters=stat,wallspring.datasource.druid.filter.stat.log-slow-sql=truespring.datasource.druid.filter.stat.slow-sql-millis=1# Db Masterspring.datasource.druid.master.name=masterspring.datasource.druid.master.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.druid.master.url=jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=truespring.datasource.druid.master.username=rootspring.datasource.druid.master.password=123456# Db Slavesspring.datasource.druid.slave.name=slavespring.datasource.druid.slave.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.druid.slave.url=jdbc:mysql://localhost:3307/test?useSSL=false&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=truespring.datasource.druid.slave.username=rootspring.datasource.druid.slave.password=123456
点击左下角阅读原文可跳转至项目下载地址:https://github.com/baojingyu/spring-boot-jpa-dynamic-datasource
点个关注,和我一起共同进步吧!
jpa 自定义sql if_SpringBoot整合JPA实现多数据源及读写分离相关推荐
- jpa 自定义sql if_mybatis仿jpa 使用@createDate @updateDate
最近在使用mybatis的时候发现一个问题,就是好多的时候保存实体的时候,都要set create 和update,这样很麻烦,有没有可能类似jap 使用注解自动生成.jpa 的注解原理也拦截sql ...
- Spring JPA自定义SQL分页查询
JPA自定义SQL分页查询 [存在的问题] 在使用Spring JPA原生SQL进行分页查询时存在如下问题: 1.使用List转Page工具类导致查询速度较慢,原因在于先把所有数据查询出来返回成Lis ...
- Sharding-JDBC教程:Spring Boot整合Sharding-JDBC实现数据分表+读写分离
点击上方"方志朋",选择"置顶公众号" 技术文章第一时间送达! 读写分离 在上一篇文章介绍了如何使用Sharing-JDBC实现数据库的读写分离.读写分离的好处 ...
- springboot jpa自定义SQL查询
说明 在使用JPA实现数据持久化过程中经常会遇到这种情况:我有2张表是一对多的关系,需要通过一个外键ID去关联查询到另外一张表的字段.例如,1张商品表food_info其中存有商品分类ID categ ...
- Spring Data JPA使用必备(三):Spring Data JPA自定义SQL写法
Spring Data JPA的前两篇已经写了通过方法名格式自动生成SQL,也简单的提到了@Query注解.但是往往真正的业务逻辑里面,这些是完全不够用的,涉及到一些稍微复杂一点的查询就会有点问题,如 ...
- jpa 自定义sql if_跟飞哥学编程:SQL入门-:函数、存储过程和触发器
最后不要忘记:SQL是一种结构化(Structured)的语言(Language),所以它具有 编程语言的特性 声明变量和赋值 所谓变量,可以是看成一个存储数据的容器,所有它里面存储的值是可以变化的. ...
- jpa 自定义sql if_数据产品经理必备之SQL基础
SQL语言是用于访问和处理数据库的标准的计算机语言,本文将从基本语法和高级语法两方面来介绍SQL语言. 数据产品经理从字面上看包含"数据"和"产品经理"两方面, ...
- jpa 自定义sql if_常用SQL语句大全总结
SQL是(Structured Query Language)结构化查询语言的简称,下面赵一鸣随笔博客从基础知识.判断对象和应用技巧等方面,介绍了SQL的应用方法. 基础 创建数据库 创建之前判断该数 ...
- Spring Boot笔记-JPA自定义SQL语句参数为对象时如何操作
环境: SpringBoot v2.1.10.RELEASE 举个例子: @Modifying @Query(nativeQuery = true, value = "update XXX. ...
最新文章
- 7月份没啥写的。。。
- 无敌了,不需要操作系统就能运行 Python 了!
- JavaWeb --第四章Maven详解
- os如何处理键盘的所有按键,显示or不显示,显示是如何显示
- hadoop Connection refused: no further information原因排查(Centos7)
- html标题前圆圈样式,漂亮的css3圆形鼠标悬停效果实例
- 数据驱动工程:跟踪使用,合理决策
- new 一个结构体数组_C语言结构体到底占多少内存?简单!一文助你解决结构体内存问题...
- linux 清除内存和Cache方法
- Nginx配置多域名代理
- HTTP的概念以及请求消息的数据格式
- Qt_QSS 样式表属性大全
- 控制理论基础(1)--控制工程概述
- foremost 原理和使用
- 最新爱客影院自动采集源码v3.5.5
- mysql下载jdbc
- 求助文章#C语言 #普中51单片机开发板
- 为什么-关于因果关系的新科学 | 导言
- Eclipse rap 开发经验总结
- HTML——表格、表格嵌套、表格布局