<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot 實現(xiàn) MySQL 讀寫分離

          共 8163字,需瀏覽 17分鐘

           ·

          2021-01-19 13:45

          ??Java大聯(lián)盟

          ? 幫助萬千Java學習者持續(xù)成長

          關注



          作者|Yrion

          cnblogs.com/wyq178/p/13352707.html


          B 站搜索:楠哥教你學Java

          獲取更多優(yōu)質視頻教程


          前言

          首先思考一個問題:在高并發(fā)的場景中,關于數(shù)據(jù)庫都有哪些優(yōu)化的手段?常用的有以下的實現(xiàn)方法:讀寫分離、加緩存、主從架構集群、分庫分表等,在互聯(lián)網(wǎng)應用中,大部分都是讀多寫少的場景,設置兩個庫,主庫和讀庫。

          主庫的職能是負責寫,從庫主要是負責讀,可以建立讀庫集群,通過讀寫職能在數(shù)據(jù)源上的隔離達到減少讀寫沖突、?釋壓數(shù)據(jù)庫負載、保護數(shù)據(jù)庫的目的。在實際的使用中,凡是涉及到寫的部分直接切換到主庫,讀的部分直接切換到讀庫,這就是典型的讀寫分離技術。本篇博文將聚焦讀寫分離,探討如何實現(xiàn)它。

          目錄

          • 一: 主從數(shù)據(jù)源的配置
          • 二: 數(shù)據(jù)源路由的配置
          • 三:數(shù)據(jù)源上下文環(huán)境
          • 四:切換注解和 Aop 配置
          • 五:用法以及測試
          • 六:總結

          主從同步的局限性:這里分為主數(shù)據(jù)庫和從數(shù)據(jù)庫,主數(shù)據(jù)庫和從數(shù)據(jù)庫保持數(shù)據(jù)庫結構的一致,主庫負責寫,當寫入數(shù)據(jù)的時候,會自動同步數(shù)據(jù)到從數(shù)據(jù)庫;從數(shù)據(jù)庫負責讀,當讀請求來的時候,直接從讀庫讀取數(shù)據(jù),主數(shù)據(jù)庫會自動進行數(shù)據(jù)復制到從數(shù)據(jù)庫中。不過本篇博客不介紹這部分配置的知識,因為它更偏運維工作一點。

          這里涉及到一個問題:主從復制的延遲問題,當寫入到主數(shù)據(jù)庫的過程中,突然來了一個讀請求,而此時數(shù)據(jù)還沒有完全同步,就會出現(xiàn)讀請求的數(shù)據(jù)讀不到或者讀出的數(shù)據(jù)比原始值少的情況。具體的解決方法最簡單的就是將讀請求暫時指向主庫,但是同時也失去了主從分離的部分意義。也就是說在嚴格意義上的數(shù)據(jù)一致性場景中,讀寫分離并非是完全適合的,注意更新的時效性是讀寫分離使用的缺點。

          好了,這部分只是了解,接下來我們看下具體如何通過 java 代碼來實現(xiàn)讀寫分離:

          該項目需要引入如下依賴:springBoot、spring-aop、spring-jdbc、aspectjweaver 等。

          一: 主從數(shù)據(jù)源的配置

          我們需要配置主從數(shù)據(jù)庫,主從數(shù)據(jù)庫的配置一般都是寫在配置文件里面。通過@ConfigurationProperties 注解,可以將配置文件(一般命名為:application.Properties)里的屬性映射到具體的類屬性上,從而讀取到寫入的值注入到具體的代碼配置中,按照習慣大于約定的原則,主庫我們都是注為 master,從庫注為 slave。

          本項目采用了阿里的 druid 數(shù)據(jù)庫連接池,使用 build 建造者模式創(chuàng)建 DataSource 對象,DataSource 就是代碼層面抽象出來的數(shù)據(jù)源,接著需要配置 sessionFactory、sqlTemplate、事務管理器等。

          /** * 主從配置 * * @author wyq */@Configuration@MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate")public class DataSourceConfig {
          /** * 主庫 */ @Bean @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource master() { return DruidDataSourceBuilder.create().build(); }
          /** * 從庫 */ @Bean @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaver() { return DruidDataSourceBuilder.create().build(); }

          /** * 實例化數(shù)據(jù)源路由 */ @Bean public DataSourceRouter dynamicDB(@Qualifier("master") DataSource masterDataSource, @Autowired(required = false) @Qualifier("slaver") DataSource slaveDataSource) { DataSourceRouter dynamicDataSource = new DataSourceRouter(); Map targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(), masterDataSource); if (slaveDataSource != null) { targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource); } dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; }

          /** * 配置sessionFactory * @param dynamicDataSource * @return * @throws Exception */ @Bean public SqlSessionFactory sessionFactory(@Qualifier("dynamicDB") DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml")); bean.setDataSource(dynamicDataSource); return bean.getObject(); }

          /** * 創(chuàng)建sqlTemplate * @param sqlSessionFactory * @return */ @Bean public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }

          /** * 事務配置 * * @param dynamicDataSource * @return */ @Bean(name = "dataSourceTx") public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB") DataSource dynamicDataSource) { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dynamicDataSource); return dataSourceTransactionManager; }}

          二: 數(shù)據(jù)源路由的配置

          路由在主從分離是非常重要的,基本是讀寫切換的核心。Spring 提供了 AbstractRoutingDataSource 根據(jù)用戶定義的規(guī)則選擇當前的數(shù)據(jù)源,作用就是在執(zhí)行查詢之前,設置使用的數(shù)據(jù)源,實現(xiàn)動態(tài)路由的數(shù)據(jù)源,在每次數(shù)據(jù)庫查詢操作前執(zhí)行它的抽象方法 determineCurrentLookupKey() 決定使用哪個數(shù)據(jù)源。

          為了能有一個全局的數(shù)據(jù)源管理器,此時我們需要引入 DataSourceContextHolder 這個數(shù)據(jù)庫上下文管理器,可以理解為全局的變量,隨時可取(見下面詳細介紹),它的主要作用就是保存當前的數(shù)據(jù)源;

          public class DataSourceRouter extends AbstractRoutingDataSource {
          /** * 最終的determineCurrentLookupKey返回的是從DataSourceContextHolder中拿到的,因此在動態(tài)切換數(shù)據(jù)源的時候注解 * 應該給DataSourceContextHolder設值 * * @return */ @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get();
          }}

          三:數(shù)據(jù)源上下文環(huán)境

          數(shù)據(jù)源上下文保存器,便于程序中可以隨時取到當前的數(shù)據(jù)源,它主要利用 ThreadLocal 封裝,因為 ThreadLocal 是線程隔離的,天然具有線程安全的優(yōu)勢。這里暴露了 set 和 get、clear 方法,set 方法用于賦值當前的數(shù)據(jù)源名,get 方法用于獲取當前的數(shù)據(jù)源名稱,clear 方法用于清除 ThreadLocal 中的內容,因為 ThreadLocal 的 key 是 weakReference 是有內存泄漏風險的,通過 remove 方法防止內存泄漏;

          /** * 利用ThreadLocal封裝的保存數(shù)據(jù)源上線的上下文context */public class DataSourceContextHolder {
          private static final ThreadLocal context = new ThreadLocal<>();
          /** * 賦值 * * @param datasourceType */ public static void set(String datasourceType) { context.set(datasourceType); }
          /** * 獲取值 * @return */ public static String get() { return context.get(); }
          public static void clear() { context.remove(); }}

          四:切換注解和 Aop 配置

          首先我們來定義一個@DataSourceSwitcher 注解,擁有兩個屬性 ① 當前的數(shù)據(jù)源 ② 是否清除當前的數(shù)據(jù)源,并且只能放在方法上,(不可以放在類上,也沒必要放在類上,因為我們在進行數(shù)據(jù)源切換的時候肯定是方法操作),該注解的主要作用就是進行數(shù)據(jù)源的切換,在 dao 層進行操作數(shù)據(jù)庫的時候,可以在方法上注明表示的是當前使用哪個數(shù)據(jù)源;

          @DataSourceSwitcher 注解的定義:

          @Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic @interface DataSourceSwitcher {    /**     * 默認數(shù)據(jù)源     * @return     */    DataSourceEnum value() default DataSourceEnum.MASTER;    /**     * 清除     * @return     */    boolean clear() default true;
          }

          DataSourceAop 配置

          為了賦予@DataSourceSwitcher 注解能夠切換數(shù)據(jù)源的能力,我們需要使用 AOP,然后使用@Aroud 注解找到方法上有@DataSourceSwitcher.class 的方法,然后取注解上配置的數(shù)據(jù)源的值,設置到 DataSourceContextHolder 中,就實現(xiàn)了將當前方法上配置的數(shù)據(jù)源注入到全局作用域當中;

          @Slf4j@Aspect@Order(value = 1)@Componentpublic class DataSourceContextAop {
          @Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)") public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable { boolean clear = false; try { Method method = this.getMethod(pjp); DataSourceSwitcher dataSourceSwitcher = method.getAnnotation(DataSourceSwitcher.class); clear = dataSourceSwitcher.clear(); DataSourceContextHolder.set(dataSourceSwitcher.value().getDataSourceName()); log.info("數(shù)據(jù)源切換至:{}", dataSourceSwitcher.value().getDataSourceName()); return pjp.proceed(); } finally { if (clear) { DataSourceContextHolder.clear(); }
          } }
          private Method getMethod(JoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); return signature.getMethod(); }
          }

          五:用法以及測試

          在配置好了讀寫分離之后,就可以在代碼中使用了,一般而言我們使用在 service 層或者 dao 層,在需要查詢的方法上添加@DataSourceSwitcher(DataSourceEnum.SLAVE),它表示該方法下所有的操作都走的是讀庫;在需要 update 或者 insert 的時候使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下來將會走寫庫。

          其實還有一種更為自動的寫法,可以根據(jù)方法的前綴來配置 AOP 自動切換數(shù)據(jù)源,比如 update、insert、fresh 等前綴的方法名一律自動設置為寫庫,select、get、query 等前綴的方法名一律配置為讀庫,這是一種更為自動的配置寫法。缺點就是方法名需要按照 aop 配置的嚴格來定義,否則就會失效

          @Servicepublic class OrderService {
          @Resource private OrderMapper orderMapper;

          /** * 讀操作 * * @param orderId * @return */ @DataSourceSwitcher(DataSourceEnum.SLAVE) public List getOrder(String orderId) { return orderMapper.listOrders(orderId);
          }
          /** * 寫操作 * * @param orderId * @return */ @DataSourceSwitcher(DataSourceEnum.MASTER) public List insertOrder(Long orderId) { Order order = new Order(); order.setOrderId(orderId); return orderMapper.saveOrder(order); }}

          六:總結

          上面是基本流程簡圖,本篇博客介紹了如何實現(xiàn)數(shù)據(jù)庫讀寫分離,注意讀寫分離的核心點就是數(shù)據(jù)路由,需要繼承 AbstractRoutingDataSource,復寫它的 determineCurrentLookupKey 方法,同時需要注意全局的上下文管理器 DataSourceContextHolder,它是保存數(shù)據(jù)源上下文的主要類,也是路由方法中尋找的數(shù)據(jù)源取值,相當于數(shù)據(jù)源的中轉站.再結合 jdbc-Template 的底層去創(chuàng)建和管理數(shù)據(jù)源、事務等,我們的數(shù)據(jù)庫讀寫分離就完美實現(xiàn)了。


          推薦閱讀

          1、Spring Boot+Vue項目實戰(zhàn)

          2、B站:4小時上手MyBatis Plus

          3、一文搞懂前后端分離

          4、快速上手Spring Boot+Vue前后端分離


          楠哥簡介

          資深 Java 工程師,微信號?southwindss

          《Java零基礎實戰(zhàn)》一書作者

          騰訊課程官方 Java 面試官,今日頭條認證大V

          GitChat認證作者,B站認證UP主(楠哥教你學Java)

          致力于幫助萬千 Java 學習者持續(xù)成長。




          有收獲,就在看?
          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  豆花视频网 | 色色的视频在线观看 | 91丨国产丨熟女 熟女 | 免费黄网站在线观看 | 青娱乐在线视频网址 |