數據庫讀寫分離 - MyBatis

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

由于項目中數據量較大,訪問量也較高,故在數據庫的設計上,采用讀寫分離思想,達到性能要求!

?

簡單科普一下實現讀寫分離的思路

  1. 配置及加載數據庫信息,即加載數據源
  2. 繼承類AbstractRoutingDataSource,并重寫方法determineCurrentLookupKey(),在這個方法中決定使用哪個數據源,實現切換數據庫。這點非常重要。
  3. 使用AOP切面,截獲讀寫操作,修改數據庫讀寫標志。

?

廢話不多,上代碼

  1. 配置及加載數據庫信息
    (1)在yml文件中填寫數據庫配置信息,這里是mysql、1主2從:
    datasource:#從庫數量readSize: 2type: com.alibaba.druid.pool.DruidDataSource # 使用druid數據源#主庫write:url: jdbc:mysql://localhost:3306/lingguan_xiaowang?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: usernamepassword: passworddriver-class-name: com.mysql.jdbc.Driverfilters: statmaxActive: 20initialSize: 1maxWait: 60000minIdle: 1timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQueryTimeout: 900000validationQuery: SELECT SYSDATE() from dualtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxOpenPreparedStatements: 20# 從庫read1:url: jdbc:mysql://localhost:3306/slave1?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: usernamepassword: passworddriver-class-name: com.mysql.jdbc.Driverfilters: statmaxActive: 20initialSize: 1maxWait: 60000minIdle: 1timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQueryTimeout: 900000validationQuery: SELECT SYSDATE() from dualtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxOpenPreparedStatements: 20read2:url: jdbc:mysql://localhost:3306/slave2?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: usernamepassword: passworddriver-class-name: com.mysql.jdbc.Driverfilters: statmaxActive: 20initialSize: 1maxWait: 60000minIdle: 1timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQueryTimeout: 900000validationQuery: SELECT SYSDATE() from dualtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxOpenPreparedStatements: 20


    (2)通過讀取yml配置文件,構造動態數據源DataSource
    ?

    
    import com.gome.store.util.mybatis.DataSourceType;
    import com.gome.store.util.mybatis.MyAbstractRoutingDataSource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.env.Environment;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;/*** @Description 解析數據庫的配置項* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    @Configuration
    @EnableTransactionManagement
    public class DataSourceConfiguration {protected Logger logger = LoggerFactory.getLogger(this.getClass());@Value("${datasource.type}")private Class<? extends DataSource> dataSourceType;@Autowiredprivate Environment env;@Value("${datasource.readSize}")private String dataSourceSize;@Bean(name = "writeDataSource")@Primary@ConfigurationProperties(prefix = "datasource.write")public DataSource writeDataSource() {logger.info("-------------------- writeDataSource init ---------------------");return DataSourceBuilder.create().type(dataSourceType).build();}/*** 有多少個從庫就要配置多少個** @author wangjie* @date 2017/7/31* @params*/@Bean(name = "readDataSource1")@ConfigurationProperties(prefix = "datasource.read1")public DataSource readDataSourceOne() {logger.info("-------------------- readDataSourceOne init ---------------------");return DataSourceBuilder.create().type(dataSourceType).build();}@Bean(name = "readDataSource2")@ConfigurationProperties(prefix = "datasource.read2")public DataSource readDataSourceTwo() {logger.info("-------------------- readDataSourceTwo init ---------------------");return DataSourceBuilder.create().type(dataSourceType).build();}@Beanpublic MyAbstractRoutingDataSource dataSource(@Qualifier("writeDataSource") DataSource writeDataSource,@Qualifier("readDataSource1") DataSource readDataSourceOne,@Qualifier("readDataSource2") DataSource readDataSourceTwo) {int size = Integer.parseInt(dataSourceSize);Map<Object, Object> targetDataSources = new HashMap<>();// 寫targetDataSources.put(DataSourceType.write.getType(), writeDataSource);// 讀targetDataSources.put(DataSourceType.read.getType() + 0, readDataSourceOne);targetDataSources.put(DataSourceType.read.getType() + 1, readDataSourceTwo);MyAbstractRoutingDataSource dataSource = new MyAbstractRoutingDataSource(size);dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法dataSource.setDefaultTargetDataSource(writeDataSource);// 默認的datasource設置為myTestDbDataSourcereturn dataSource;}@Beanpublic SqlSessionFactory sqlSessionFactory(MyAbstractRoutingDataSource ds) throws Exception {SqlSessionFactoryBean fb = new SqlSessionFactoryBean();fb.setDataSource(ds);// 指定數據源(這個必須有,否則報錯)// 下邊兩句僅僅用于*.xml文件,如果整個持久層操作不需要使用到xml文件的話(只用注解就可以搞定),則不加fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));//return fb.getObject();}/*** 配置事務管理器*/@Beanpublic DataSourceTransactionManager transactionManager(MyAbstractRoutingDataSource dataSource) throws Exception {return new DataSourceTransactionManager(dataSource);}
    }
    

    注釋:
    ? ? ? ? ·?@Primary:指定在同一個接口有多個實現類可以注入的時候,默認選擇哪一個,而不是讓@Autowire注解報錯(一般用于多數據源的情況下)
    ????????·?@Qualifier:指定名稱的注入,當一個接口有多個實現類的時候使用(在本例中,有兩個DataSource類型的實例,需要指定名稱注入)
    ????????·?通過動態數據源構造SqlSessionFactory和事務管理器(如果不需要事務,后者可以去掉)
    ?

  2. 編寫相應的控制數據源類型類
    (1)數據源類型
    
    /*** @Description* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    public enum DataSourceType {read("read", "從庫"), write("write", "主庫");private String type;private String name;DataSourceType(String type, String name) {this.type = type;this.name = name;}public String getType() {return type;}public String getName() {return name;}
    }
    


    (2)數據源標志類
    
    /*** @Description 本地線程全局變量,用來存放當前操作是讀還是寫* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    public class DataSourceContextHolder {private static final ThreadLocal<String> local = new ThreadLocal<>();public static ThreadLocal<String> getLocal() {return local;}/** 讀可能是多個庫* @author wangjie* @date 2017/7/31* @params*/public static void read() {local.set(DataSourceType.read.getType());}/** 寫只有一個庫* @author wangjie* @date 2017/7/31* @params*/public static void write() {local.set(DataSourceType.write.getType());}public static String getJdbcType() {return local.get();}/** 清除數據源類型* @author wangjie* @date 2017/7/31* @params*/public static void clearDataSourceType() {local.remove();}
    }
    

    ?
  3. 繼承AbstractRoutingDataSource?
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import org.springframework.util.StringUtils;import java.util.concurrent.atomic.AtomicInteger;/*** @Description 多數據源切換* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {protected Logger logger = LoggerFactory.getLogger(this.getClass());private final int dataSourceNumber;private AtomicInteger count = new AtomicInteger(0);public MyAbstractRoutingDataSource(int dataSourceNumber) {this.dataSourceNumber = dataSourceNumber;}@Overrideprotected Object determineCurrentLookupKey() {Object resultObject = null;String typeKey = DataSourceContextHolder.getJdbcType();// 只對主庫開啟事務,如果typeKey為空表示獲取主庫的datasourceif (StringUtils.isEmpty(typeKey)) {resultObject = DataSourceType.write.getType();} else {// 讀簡單負載均衡int number = count.getAndAdd(1);int lookupKey = number % dataSourceNumber;resultObject = DataSourceType.read.getType() + lookupKey;}logger.info("determineCurrentLookupKey:" + resultObject);return resultObject;}
    }
    

    ?
  4. AOP的實現
    
    import com.gome.store.util.mybatis.DataSourceContextHolder;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.annotation.Transactional;/*** @Description aop 攔截 進行切換數據源* 如果service層 增加了@Transactional ,導致數據源MyAbstractRoutingDataSource的determineCurrentLookupKey()方法會在執行DataSourceAop攔截之前就進行全局事務綁定* 從而導致獲取 DataSourceContextHolder.getJdbcType(); 一直都是空值* @Author wangjie<https://my.oschina.net/xiaowangqiongyou>* @Date 2017/7/31*/
    @Aspect
    @Component
    public class DataSourceAop {private static Logger logger = LoggerFactory.getLogger(DataSourceAop.class);//    @Before("execution(* com.ggj.encrypt.modules.*.dao..*.find*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.get*(..)) or execution(* com.ggj.encrypt.modules.*.dao..*.select*(..))")@Before("execution(* com.gome.store.dao..*.query*(..)) or execution(* com.gome.store.dao..*.get*(..)) or execution(* com.gome.store.dao..*.select*(..))")public void setReadDataSourceType() {DataSourceContextHolder.read();logger.info("dataSource切換到:Read");}// 另一種方式
    //	@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    //    public void setWriteDataSourceType(ProceedingJoinPoint joinPoint) throws Throwable {
    //		Transactional datasource = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Transactional.class);
    //		if(datasource.readOnly()){
    //			DataSourceContextHolder.read();
    //            logger.info("dataSource切換到:Read");
    //		}else{
    //			DataSourceContextHolder.write();
    //            logger.info("dataSource切換到:write");
    //		}
    //		joinPoint.proceed();
    //	}
    }
    

    ?

  5. 測試

    @Override
    public User queryUsersByUsername(String userName) {return userDao.queryByName(userName);
    }@Transactional()
    @Override
    public void updateUserInfoById(User user) {userDao.updateBySelective(user);userDao.updateById(user.getId());
    }

    ?

寄語

????????這里只是簡單的實現了數據庫的讀寫分離,很多細節問題沒有具體實現,比如某個DB宕掉了時的處理,負載均衡只是簡單的采用輪訓方式,性能的統計,sql語句黑白名單等等問題。

?

轉載于:https://my.oschina.net/xiaowangqiongyou/blog/1499887

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/280928.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/280928.shtml
英文地址,請注明出處:http://en.pswp.cn/news/280928.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MySQL-03:數據表操作基本命令筆記

目錄 數據表 1、創建表 2、刪除表 3、清空表 4、修改表 5、基本數據類型 數據表 1、創建表 create table 表名(列名 類型 是否可以為空&#xff0c;列名 類型 是否可以為空 )ENGINEInnoDB DEFAULT CHARSETutf8 是否可空&#xff0c;null表示空&#xff0c;非字符串n…

java 怎么調試到第三方庫的內部,在有源碼的情況下

第一步&#xff0c; 把第三方庫加到workspace : https://stackoverflow.com/questions/370814/how-to-set-a-breakpoint-in-eclipse-in-a-third-party-library The most sure-fire way to do this (and end up with something thats actually useful) is to download the sou…

t-mobile頻段_T-Mobile再次被黑客入侵:超過200萬個帳號和地址可能泄漏

t-mobile頻段Attackers may have compromised three percent of T-Mobile’s 77 million customers on Monday, revealing personal information like addresses, phone numbers, and account numbers. 周一&#xff0c;攻擊者可能泄露了T-Mobile 7700萬客戶中的3&#xff05;&…

第二篇 第三章防火防煙分區檢查(一)

倉庫面積可以增加3倍 就是乘以4 要一定條件 : 第二篇 第三章防火防煙分區檢查&#xff08;一&#xff09; 21分鐘處 該題比較有代表性 停車庫 耐火等級允許最大面積 民用建筑防火分區 防煙分區的劃分    防火卷簾控制器的測試 防火閥 裝在通風,空調系統中 只有連在風機主管…

高斯數學

偉大的數學家高斯在9歲那年&#xff0c;用很短的時間完成了從1到100的累加。那原本是老師給學生們出的難題&#xff0c;希望他們能老老實實地待在教室里。高斯的方法很簡單&#xff0c;他發現這是50個101的求和&#xff1a;100&#xff0b;1、99&#xff0b;2、98&#xff0b;3…

Ant Design Blazor 發布 0.13.0,正式支持.NET 7!

時隔3個月&#xff0c;Ant Design Blazor 發布新功能版本 0.13.0&#xff0c;并正式支持.NET 7!大家快去訪問 antblazor.com 體驗吧&#xff01;&#x1f525; 新增 .NET 7 目標框架支持。#2810 ElderJames&#x1f525; 重構 Mentions 組件&#xff0c;修復定位和隱藏問題。#2…

gitlab 分支操作筆記\新建遠程分支\抓取遠程分支\復制遠程\刪除分支

密碼重新輸入與保存 git config --global http.emptyAuth truegit config --global credential.helper store 1.不復制遠程&#xff0c;直接新建遠程分支。&#xff08;非正規操作&#xff09; git init //初始化 git remote add origin http://****/*****/taskboard.git…

如何在Xbox One或PlayStation 4上為Skyrim特別版安裝Mods

The Elder Scrolls V: Skyrim Special Edition is now available on PlayStation 4 and Xbox One, and for the first time, “mods” are available to console gamers. Elder Scrolls V&#xff1a;Skyrim特別版現已在PlayStation 4和Xbox One上可用&#xff0c;并且首次向主…

微軟宣布:PowerBI 已經與 Office 整合,一切更簡單,變革又來了

很多人認為 Office 是 Office&#xff0c;PowerBI 是 PowerBI&#xff0c;怎么在 PPT 中顯示 PowerBI 呢&#xff1f;這種問題以后將再不會存在。微軟已經宣布&#xff0c;PowerBI 已經與 Office 深度整合&#xff0c;在未來的企業中&#xff0c;PowerBI 將與 Word&#xff0c;…

066:ORM查詢條件詳解-startswith和endswith:

ORM查詢條件詳解-startswith和endswith&#xff1a; startswith&#xff1a;判斷某個字段的值是否是以某個值開始的。大小寫敏感。示例代碼如下&#xff1a; articles1 Article.objects.filter(title__startswith"fuck") 以上代碼的意思是提取所有標題以 fuck 字符串…

前端工程師面試題匯總

HTML Doctype作用&#xff1f;嚴格模式與混雜模式如何區分&#xff1f;它們有何意義? HTML5 為什么只需要寫 <!DOCTYPE HTML>&#xff1f; 行內元素有哪些&#xff1f;塊級元素有哪些&#xff1f; 空(void)元素有那些&#xff1f; 頁面導入樣式時&#xff0c;使用lin…

MySQL-04:數據內容操作-增刪改查-基本命令筆記

1、增 insert into 表 (列名,列名...) values (值,值,值...) insert into 表 (列名,列名...) values (值,值,值...),(值,值,值...) insert into 表 (列名,列名...) select (列名,列名...) from 表 2、刪 delete from 表 delete from 表 where id&#xff1d;1 and name&…

火狐和chrome_Firefox,Chrome和Edge都將支持WebAuthn的硬件兩因素身份驗證

火狐和chromeLogging into Gmail or Facebook could soon mean plugging in a USB device, potentially making phishing a thing of the past. 登錄Gmail或Facebook可能很快就意味著要插入USB設備&#xff0c;這可能使網絡釣魚成為過去。 That’s thanks to WebAuthn, a new o…

Could not delete .........May be locked by another process.

問題 原因&#xff1a;默認的設置是文件修改后立即發布&#xff0c;這樣的設置是在你每個保存文件時都會觸發&#xff0c;如果tomcat已經在運行&#xff0c;這樣頻繁的操作也會造成文件鎖死 解決&#xff1a; Tomcat 右鍵clean 轉載于:https://www.cnblogs.com/feiZhou/p/93…

flask的基礎1

1.python 現階段三大主流web框架Django Tornado Flask的對比 1.Django 主要特點是大而全,集成了很多組件,例如: Models Admin Form 等等, 不管你用得到用不到,反正它全都有,屬于全能型框架 2.Tornado 主要特點是原生異步非阻塞,在IO密集型應用和多任務處理上占據絕對性的優勢,屬…

python實現批量壓縮文件夾

前段時間碰到一個需要把目錄下文件夾壓縮的項目&#xff0c;但是度娘里沒找到&#xff0c;只好自己寫腳本了。 #coding:utf-8 import os filePath raw_input("請輸入路徑&#xff1a;") if filePath "":os._exit() #需要退出ds list(os.walk(filePath…

單元測試01:nunit 安裝與代碼測試

1.nunit 下載與安裝 a.下載 下載地址&#xff1a; http://nunit.org/download/ b.添加到系統環境變量 解壓下載包后&#xff0c;添加兩個路徑到環境變量&#xff0c;如&#xff1a; D:\nunitD:\nunit\nunit-console 2.建立測試項目 a.建立class project b.project 里re…

如何將您的Google Authenticator憑證移至新的Android手機或平板電腦

Most of the app data on your Android is probably synced online will automatically sync to a new phone or tablet. However, your Google Authenticator credentials won’t — they aren’t synchronized for obvious security reasons. Android上的大多數應用程序數據可…

關于經緯度的兩個計算[Teaksxgluxv]

一、子午線周長(公里) 40008.548 赤道周長(公里) 40075.704 緯度40008.548 / 360(度) 111.135 公里/度40008.548 / (360*60)(分) 1.85 公里/分40008.548 / (360*60*60)(秒) 30.87 米/秒 經度首先算相應經度位置的緯度圈長度40075.704 * cos(經度)然后方法相同&#xff0c;除…

轉載通過 Docker 實現傳統應用程序的現代化

長期以來&#xff0c;IT 組織將其預算的 80% 用于簡單地維護現有應用程序&#xff0c;而只花費 20% 用于創新。 在過去的 10 年里&#xff0c;這一比例并沒有太大改觀。而同時又必須面對創新的壓力。無論是直接來自客戶的需求&#xff0c;要求提供新的功能&#xff0c;還是來自…