javascript
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实现多数据源及读写分离的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vue 动态数据滚动
- 下一篇: lua打开浏览器并加载网页_Lua访问网