分類
發燒車訊

Spring系列.事務管理原理簡析

Spring的事務管理功能能讓我們非常簡單地進行事務管理。只需要進行簡單的兩步配置即可:

step1:開啟事務管理功能

@Configuration
//@EnableTransactionManagement註解有以下幾個屬性
//proxyTargetClass屬相:指定事務的AOP是通過JDK動態代理實現,還是CGLIB動態代理實現。true的話是CGLIB,false的話是JDK動態代理
//                     需要注意的是這個屬性只有在AdviceMode設置成AdviceMode.PROXY的情況下才會生效,加入使用ASPECTJ這AOP框架的話,這個屬性就失效了。
//                     另外,這個屬性的設定可能會影響其他需要動態代理的類。比如說將這個屬性設置成true,@Async註解的方法也會使用CGLIB生成代理類。
//                     但是總的來說,這個屬性的設置不會造成什麼負面影響,畢竟JDK動態代理和CGLIB動態代理都能實現我們的需求

//mode屬性:Spring提供的AOP功能有兩種實現方式,一種是Spring自帶的AOP功能,主要靠JDK代理和CGLIB代理實現,另外一種是通過第三方框架ASPECTJ實現。這個選項
//        就是設定Spring用哪種方式提供AOP功能。AdviceMode.PROXY表示用Spring自帶的AOP功能,AdviceMode.ASPECTJ表示使用AdviceMode提供AOP功能。
//        需要注意的是Spring自帶的AOP功能不支持本地調用的代理功能,也就是說同一個類中的方法互相調用不會“觸發”代理方法。如果想讓自調用觸發代理,可以考慮使用ASPECTJ。

//order屬性:表示當一個連接點(方法)被切多次時(也就是說有多個Advice和連接點關聯),這些連接點的執行順序。
@EnableTransactionManagement
public class TxConfig {
}

step2:在需要事務管理的方法上添加@Transactional註解

@Override
@Transactional
public int saveSysUser(SysUser user) {
    int i = sysUserMapper.insert(user);
    return i;
}

整個使用流程就這麼簡單。這篇博客就來簡單分析下Spring是怎麼實現事務管理的。

對事務管理進行AOP的過程

Spring的很多功能都是通過AOP功能實現的,事務管理也是。我們之前的文章分析過Spring基礎AOP實現的原理。這邊再簡單提下Spring實現AOP的
原理:

Spring基礎的AOP功能的開關是@EnableAspectJAutoProxy,這個註解註冊了一個Bean——AnnotationAwareAspectJAutoProxyCreator,這個Bean才是實現AOP功能的關鍵。
這個Bean實現了InstantiationAwareBeanPostProcessor接口(這個接口是BeanPostProcessor的子接口)。熟悉Spring的讀者知道,實現BeanPostProcessor接口的Bean
會在其他Bean初始化之前初始,然後在其他Bean初始化的時候,BeanPostProcessor的實現會對這些Bean進行“加工處理”。

這邊AnnotationAwareAspectJAutoProxyCreator就承擔了加工處理類的角色。這個Bean在其他Bean初始化前後會判斷這個Bean中的方法是不是有對應的Advice,如果有的話就會
通過動態代理的方式生成動態代理類將通知織入進去。

我們發現開啟事務管理的方式和開啟AOP功能的方式很像,也是通過Enable註解開啟。所以很自然就猜想事務管理是不是也是通過BeanPostProcessor的方式實現的。帶着這個猜想去看下@EnableTransactionManagement註解。


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

	boolean proxyTargetClass() default false;
	AdviceMode mode() default AdviceMode.PROXY;
	int order() default Ordered.LOWEST_PRECEDENCE;
}

看到上面的代碼,我們很自然的會去看TransactionManagementConfigurationSelector的代碼。Spring有兩種方式提供AOP功能,一種是自帶的動態代理的功能,一種是
通過ASPECTJ的方式提供。這邊主要討論Spring自帶的AOP功能。

protected String[] selectImports(AdviceMode adviceMode) {
    switch (adviceMode) {
        //用代理的方式實現事務管理的AOP功能
        case PROXY:
            return new String[] {AutoProxyRegistrar.class.getName(),
                    ProxyTransactionManagementConfiguration.class.getName()};
        case ASPECTJ:
            return new String[] {determineTransactionAspectClass()};
        default:
            return null;
    }
}

上面的代碼中,我們主要關注PROXY這個case中的方法。這個case中註冊了兩個類:AutoProxyRegistrar和ProxyTransactionManagementConfiguration。

首先我們來看AutoProxyRegistrar這個類,層層點進入,我們發現這個類最終就是註冊了InfrastructureAdvisorAutoProxyCreator這個類。仔細看InfrastructureAdvisorAutoProxyCreator
這個類實現的接口的話,你會發現這個類也是BeanPostProcesser系列的類。看到這裏,我的直覺是事務管理的AOP過程和Spring基礎的AOP功能原理可能是一樣的。

再仔細看InfrastructureAdvisorAutoProxyCreator對BeanPostProcesser系列接口的實現,你會發現都是繼承的AbstractAutoProxyCreator。看到這個驗證了我之前的想法。

下面是Spring對事務管理進行AOP的過程,你會發現和基礎的AOP功能是一套代碼。

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}


protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    // Create proxy if we have advice.
    // 代碼1
    // 這邊是獲取Advice和Advisor的具體代碼
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        //生成代理類
        Object proxy = createProxy(
                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

再來看看ProxyTransactionManagementConfiguration做了些啥?點進源代碼你會發現這個類的功能很簡單,就是註冊了下面幾個事務管理相關的基礎Bean。

  • BeanFactoryTransactionAttributeSourceAdvisor;
  • TransactionAttributeSource;
  • TransactionInterceptor。

事務管理的生效過程

上面的章節中講了Spring是怎麼生成事務相關的AOP代理類的。這邊來講下Spring的事務管理是怎麼生效的——怎麼開啟事務,怎麼回滾事務,怎麼提交事務,Spring中的事務傳播
機制是怎麼生效的。

這塊的代碼主要是在TransactionAspectSupport的invokeWithinTransaction方法中(不要問我是怎麼找到這段代碼的…)。下面講下這個方法中的幾個關鍵點。


protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

    // If the transaction attribute is null, the method is non-transactional.
    TransactionAttributeSource tas = getTransactionAttributeSource();
    //獲取TransactionAttribute,這個類主要是@Transactional註解的配置信息
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    //確認事務管理器
    final TransactionManager tm = determineTransactionManager(txAttr);

    if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
        ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
            if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
                throw new TransactionUsageException(
                        "Unsupported annotated transaction on suspending function detected: " + method +
                        ". Use TransactionalOperator.transactional extensions instead.");
            }
            ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
            if (adapter == null) {
                throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
                        method.getReturnType());
            }
            return new ReactiveTransactionSupport(adapter);
        });
        return txSupport.invokeWithinTransaction(
                method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
    }

    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // Standard transaction demarcation with getTransaction and commit/rollback calls.
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // This is an around advice: Invoke the next interceptor in the chain.
            // This will normally result in a target object being invoked.
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            // target invocation exception
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            cleanupTransactionInfo(txInfo);
        }

        if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
            // Set rollback-only in case of Vavr failure matching our rollback rules...
            TransactionStatus status = txInfo.getTransactionStatus();
            if (status != null && txAttr != null) {
                retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
            }
        }

        commitTransactionAfterReturning(txInfo);
        return retVal;
    }else {
        final ThrowableHolder throwableHolder = new ThrowableHolder();

        // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
        try {
            Object result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
                TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
                try {
                    Object retVal = invocation.proceedWithInvocation();
                    if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                        // Set rollback-only in case of Vavr failure matching our rollback rules...
                        retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                    }
                    return retVal;
                }
                catch (Throwable ex) {
                    if (txAttr.rollbackOn(ex)) {
                        // A RuntimeException: will lead to a rollback.
                        if (ex instanceof RuntimeException) {
                            throw (RuntimeException) ex;
                        }
                        else {
                            throw new ThrowableHolderException(ex);
                        }
                    }
                    else {
                        // A normal return value: will lead to a commit.
                        throwableHolder.throwable = ex;
                        return null;
                    }
                }
                finally {
                    cleanupTransactionInfo(txInfo);
                }
            });

            // Check result state: It might indicate a Throwable to rethrow.
            if (throwableHolder.throwable != null) {
                throw throwableHolder.throwable;
            }
            return result;
        }
        catch (ThrowableHolderException ex) {
            throw ex.getCause();
        }
        catch (TransactionSystemException ex2) {
            if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                ex2.initApplicationException(throwableHolder.throwable);
            }
            throw ex2;
        }
        catch (Throwable ex2) {
            if (throwableHolder.throwable != null) {
                logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
            }
            throw ex2;
        }
    }
}

事務操作的主要代碼都在這個方法中,要詳細將這個方法能寫很多內容。這邊就不詳細展開了,大家感興趣的可以仔細研究下這個方法。

重要類總結

  • InfrastructureAdvisorAutoProxyCreator:事務管理AOP註冊
  • BeanFactoryTransactionAttributeSourceAdvisor:Spring事務管理基礎Bean
  • TransactionAttributeSource:Spring事務管理基礎Bean
  • TransactionInterceptor:Spring事務管理基礎Bean
  • TransactionAspectSupport的invokeWithinTransaction方法:事務處理的主要方法

相關註解

如果你仔細看過Spring的相關源代碼,會發現Spring的Enable系列的註解都是上面的“套路”,熟悉了@EnableTransactionManagement註解生效的原理,其他註解都是類似的生效規則。比如

  • @EnableAsync
  • @EnableScheduling

希望大家能做到觸類旁通。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

G20部長會議 矢言永續農業 消除饑餓

摘錄自2018年7月30日青年日報報導

G20農業部長會議28日於阿根廷布宜諾斯艾利斯落幕,會後各國農業部長發表聯合聲明,矢言「共同努力消除饑餓、促進永續農業,以實現未來糧食的不虞匱乏」。這將成為此屆G20的三大優先目標。

聲明且強調創新的重要性,並對「愈來愈常採取保護主義的非關稅貿易措施感到擔憂」;因為「農業發展、投資與貿易及引進改良技術,近數十年成為人類進步的驅動力,並改善全球數百萬人的生活條件」。G20農業部長因此鼓勵創新農業技術,提升農業生產力與永續性。

另重申土壤健康的重要性,認為其能強化農業在人類發展與資訊通訊技術(ICTs)中的角色,進而加強生產力、糧食安全與農業永續。同時呼籲減少食物損失與浪費,以增進糧食安全,減輕對氣候、水與土地資源造成壓力,並改善農夫、經銷商與家庭經濟的收入。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

數據為大貓洗污名 印度北方邦努力和虎豹當好鄰居

環境資訊中心綜合外電;范震華 編譯;賴慧玲 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

中國醞釀小範圍試點扶持微型電動車

據上證報從第四屆全球新能源大會上獲悉,中國大陸主管部門正在研究在全國部分城市開展小範圍示範試點工作,以加強管理,規範市場,對微型電動車的發展「謹慎扶持」。中國微型電動車市場潛力巨大,有車企人士表示,「去年大陸國內銷售達35萬輛左右,今年有望增至50萬輛。」

中汽協副秘書長葉盛基指出,國家正研究制定相關政策,推動微型電動車向準電動車發展,未來將提升微型電動車的安全標準,確保符合環保要求,並不斷提高微型電動車的電池性能。

此外,因採取租賃模式,微型電動車在浙江等省份發展迅速,如2,000元人民幣左右就能租到一輛微型電動車,使消費者的用車成本大幅下降,促進了微型電動車的快速成長。

據瞭解,已在美國納斯達克上市的浙江康迪車業(KNDI.US)是中國微型電動車的代表企業,受益於新能源汽車產業的廣闊空間,公司股價自去年底以來已創歷史新高。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

去年四季度Model S大賣6900輛 特斯拉股價飆

雖然美國電動車製造商特斯拉(Tesla)日前召回了2.9萬個充電器轉接頭,使Model S安全疑慮再起,但Tesla週二公布,2013年第4季(10~12月)Model S高級房車大賣6900輛,銷量與營收年增率可望雙雙超越20%。

據特斯拉提交美國國家公路交通安全管理局(NHTSA)的文件表示,2013年出廠的Model S充電器轉接頭藏有過熱可能性,「可能造成轉接頭融化,且在最遭情況下可能起火」。去年Model S一共傳出3起事故,全因車輛行駛時遭重物撞裂底盤電池防護蓋才起火,但3起事故皆未造成人員傷亡。

特斯拉周二發表的銷售數字再度驗證,消費者對Model S的信心絲毫不減。特斯拉近來不僅獲得多方產業專家肯定Model S安全性,也在美國增設快速充電的「超級充電站」,估計不久後用戶橫跨美國就不用擔心沒電。

Model S是特斯拉的主打車型,已有超過2.5萬輛Model S在上路行駛。特斯拉預期今年年底銷售分店應會增設2倍以上,今年特斯拉電動汽車銷量可能會大幅成長。

Tesla股價在周二收盤大漲16%至161.27美元,創6周以來最大漲幅,周三盤中續漲6%,每股171.37美元。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

北汽集團將成為美國新能源公司Atieva第一大股東

北京汽車集團有限公司(北汽集團)昨(17)日宣布,與美國新能源公司Atieva簽署股份認購協議,北汽集團將收購Atieva公司25.02%的股份。收購完成後,北汽將成為Atieva的第一大股東,雙方預計在第3年推出與奧迪A6L同等級的電動汽車。

由於美國電動車大廠特斯拉也正和北汽股份洽談合作事項,昨日北汽旗下的上市公司福田汽車,股價也跟著漲停。

去年,北汽集團新成立了新能源汽車公司,而此次收購的美國Atieva公司是一家新能源汽車核心系統提供商,曾主要參與過Tesla Roadster純電動跑車、雪佛蘭Volt插電式混合電動車、奧迪R8純電動跑車的開發。

北汽集團方面也表示,此次收購主要是為進一步提升北汽集團及下屬公司在新能源汽車尤其是高端純電動汽車領域的設計、研發和制造的能力和水平。

據悉,北汽由6家股東組成,除北汽集團以51%的股比成為控股股東外,首鋼股份有限公司以18.31%的股比成為第二大股東,其他股東包括北京市國資公司、現代創新控股公司及京能集團,而北京市國資委直屬的投融資平台-北京國有資本經營管理中心也持股5%。

另據外媒稍早報導,北汽可望在2014年第2季在香港IPO上市,籌資額度或達到20億美元。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

將電動車行程提升超10% 英公司開發MSYS動力系統

據英國媒體報導,英國動力系統設計公司宣佈,該公司正在開發一套名為MSYS的電動汽車動力系統,可以將電動汽車行駛里程增加10%至15%。MSYS系統預計將於2016年投產。

在英國即將舉辦的未來動力系統會議上,英國動力系統設計公司將發佈高效電動汽車動力系統報告。該動力系統提供了一種新的汽車換擋路徑,而且不會引起扭矩中斷。

該公司技術總監阿萊克斯•泰利•博達爾表示,該技術可提供55千瓦的持續電力供應,超過2000牛/米的扭矩,其電力動力系統效率可達91%。

阿萊克斯還透露,MSYS系統避免了其他傳動方法的弊端。雙離合器變速箱(DCT)在離合器開啟或關閉狀態時,都會持續消耗能量。而在換擋時,自動變速器(AMT)會受損於扭矩中斷。此外,基於行星齒輪變速器的自動裝置,則增加了複雜性和成本,並會拖慢速度。

MSYS系統將提供三種速度傳動裝置,使其換擋和快速換擋都能運用重疊換擋技術。隨著技術的完善,多重比例的選擇對電動汽車的變速會非常有益。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

Spring Boot 2.x基礎教程:Spring Data JPA的多數據源配置

上一篇我們介紹了在使用JdbcTemplate來做數據訪問時候的多數據源配置實現。接下來我們繼續學習如何在使用Spring Data JPA的時候,完成多數據源的配置和使用。

添加多數據源的配置

先在Spring Boot的配置文件application.properties中設置兩個你要鏈接的數據庫配置,比如這樣:

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=123456
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=123456
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

# 日誌打印執行的SQL
spring.jpa.show-sql=true
# Hibernate的DDL策略
spring.jpa.hibernate.ddl-auto=create-drop

這裏除了JPA自身相關的配置之外,與JdbcTemplate配置時候的數據源配置完全是一致的

說明與注意

  1. 多數據源配置的時候,與單數據源不同點在於spring.datasource之後多設置一個數據源名稱primarysecondary來區分不同的數據源配置,這個前綴將在後續初始化數據源的時候用到。
  2. 數據源連接配置2.x和1.x的配置項是有區別的:2.x使用spring.datasource.secondary.jdbc-url,而1.x版本使用spring.datasource.secondary.url。如果你在配置的時候發生了這個報錯java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.,那麼就是這個配置項的問題。

初始化數據源與JPA配置

完成多數據源的配置信息之後,就來創建個配置類來加載這些配置信息,初始化數據源,以及初始化每個數據源要用的JdbcTemplate。

由於JPA的配置要比JdbcTemplate的負責很多,所以我們將配置拆分一下來處理:

  1. 單獨建一個多數據源的配置類,比如下面這樣:
@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}

可以看到內容跟JdbcTemplate時候是一模一樣的。通過@ConfigurationProperties可以知道這兩個數據源分別加載了spring.datasource.primary.*spring.datasource.secondary.*的配置。@Primary註解指定了主數據源,就是當我們不特別指定哪個數據源的時候,就會使用這個Bean真正差異部分在下面的JPA配置上。

  1. 分別創建兩個數據源的JPA配置。

Primary數據源的JPA配置:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactoryPrimary",
        transactionManagerRef="transactionManagerPrimary",
        basePackages= { "com.didispace.chapter38.p" }) //設置Repository所在位置
public class PrimaryConfig {

    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    private Map<String, Object> getVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    @Primary
    @Bean(name = "entityManagerPrimary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
    }

    @Primary
    @Bean(name = "entityManagerFactoryPrimary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(primaryDataSource)
                .packages("com.didispace.chapter38.p") //設置實體類所在位置
                .persistenceUnit("primaryPersistenceUnit")
                .properties(getVendorProperties())
                .build();
    }

    @Primary
    @Bean(name = "transactionManagerPrimary")
    public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
    }

}

Secondary數據源的JPA配置:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactorySecondary",
        transactionManagerRef="transactionManagerSecondary",
        basePackages= { "com.didispace.chapter38.s" }) //設置Repository所在位置
public class SecondaryConfig {

    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    private Map<String, Object> getVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    @Bean(name = "entityManagerSecondary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactorySecondary(builder).getObject().createEntityManager();
    }

    @Bean(name = "entityManagerFactorySecondary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(secondaryDataSource)
                .packages("com.didispace.chapter38.s") //設置實體類所在位置
                .persistenceUnit("secondaryPersistenceUnit")
                .properties(getVendorProperties())
                .build();
    }

    @Bean(name = "transactionManagerSecondary")
    PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());
    }

}

說明與注意

  • 在使用JPA的時候,需要為不同的數據源創建不同的package來存放對應的Entity和Repository,以便於配置類的分區掃描
  • 類名上的註解@EnableJpaRepositories中指定Repository的所在位置
  • LocalContainerEntityManagerFactoryBean創建的時候,指定Entity所在的位置
  • 其他主要注意在互相注入時候,不同數據源不同配置的命名,基本就沒有什麼大問題了

測試一下

完成了上面之後,我們就可以寫個測試類來嘗試一下上面的多數據源配置是否正確了,比如下面這樣:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter38ApplicationTests {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MessageRepository messageRepository;

    @Test
    public void test() throws Exception {
        userRepository.save(new User("aaa", 10));
        userRepository.save(new User("bbb", 20));
        userRepository.save(new User("ccc", 30));
        userRepository.save(new User("ddd", 40));
        userRepository.save(new User("eee", 50));

        Assert.assertEquals(5, userRepository.findAll().size());

        messageRepository.save(new Message("o1", "aaaaaaaaaa"));
        messageRepository.save(new Message("o2", "bbbbbbbbbb"));
        messageRepository.save(new Message("o3", "cccccccccc"));

        Assert.assertEquals(3, messageRepository.findAll().size());
    }

}

說明與注意

  • 測試驗證的邏輯很簡單,就是通過不同的Repository往不同的數據源插入數據,然後查詢一下總數是否是對的
  • 這裏省略了Entity和Repository的細節,讀者可以在下方代碼示例中下載完整例子對照查看

代碼示例

本文的相關例子可以查看下面倉庫中的chapter3-8目錄:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!

相關閱讀

  • Spring Boot 1.x基礎教程:多數據源配置

本文首發:Spring Boot 2.x基礎教程:Spring Data JPA的多數據源配置,轉載請註明出處。
歡迎關注我的公眾號:程序猿DD,獲得獨家整理的學習資源和日常乾貨推送。
如果您對我的其他專題內容感興趣,直達我的個人博客:didispace.com。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

大文件上傳、斷點續傳、秒傳、beego、vue

大文件上傳

0、項目源碼地址

源碼地址 :https://github.com/zhuchangwu/large-file-upload

前端基於 vue-simple-uploader (感謝這個大佬)實現: https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md

vue-simple-uploader底層封裝了uploader.js : https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md

1、如何唯一標識一個文件?

文件的信息後端會存儲在mysql數據庫表中。

在上傳之前,前端通過 spark-md5.js 計算文件的md5值以此去唯一的標示一個文件。

spark-md5.js 地址:https://github.com/satazor/js-spark-md5

README.md中有spark-md5.js的使用demo,可以去看看。

2、斷點續傳是如何實現的?

斷點續傳可以實現這樣的功能,比如用戶上傳200M的文件,當用戶上傳完199M時,斷網了,有了斷點續傳的功能,我們允許RD再次上傳時,能從第199M的位置重新上傳。

實現原理:

實現斷點續傳的前提是,大文件切片上傳。然後前端得問後端哪些chunk曾經上傳過,讓前端跳過這些上傳過的chunk就好了。

前端的上傳器(uploader.js)在上傳時會先發送一個GET請求,這個請求不會攜帶任何chunk數據,作用就是向後端詢問哪些chunk曾經上傳過。 後端會將這些數據保存在mysql數據庫表中。比如按這種格式:1:2:3:5表示,曾經上傳過的分片有1,2,3,5。第四片沒有被上傳,前端會跳過1,2,3,5。 僅僅會將第四個chunk發送給後端。

3、秒傳是如何實現的?

秒傳實現的功能是:當RD重複上傳一份相同的文件時,除了第一次上傳會正常發送上傳請求后,其他的上傳都會跳過真正的上傳,直接显示秒成功。

實現方式:

後端存儲着當前文件的相關信息。為了實現秒傳,我們需要搞一個字段(isUploaded)表示當前md5對應的文件是否曾經上傳過。 後端在處理 前端的上傳器(uploader.js)發送的第一個GET請求時,會將這個字段發送給前端,比如 isUploaded = true。前端看到這個信息后,直接跳過上傳,显示上傳成功。

4、上傳暫停是如何實現的?

上傳的暫停:並不是去暫停一個已經發送出去的正在進行數據傳輸的http請求~

而是暫停發送起發送下一個http請求。

就我們的項目而言,因為我們的文件本來就是先切片,對於我們來說,暫停文件的上傳,本質上就是暫停發送下一個chunk。

5、前端上傳併發數是多少?

前端的uploader.js中默認會三條線程啟動併發上傳,前端會在同一時刻併發 發送3個chunk,後端就會相應的為每個請求開啟三個協程處理上傳的過來的chunk。

在我們的項目中,會將前端併發數調整成了1。原因如下:

因為考慮到了斷點續傳的實現,後端需要記錄下曾經上傳過哪些切片。(這個記錄在mysql的數據庫表中,以 ”1:2:3:4:5“ )這種格式記錄。

Mysql5.7默認的存儲引擎是innoDB,默認的隔離級別是RR。如果我們將前端的併發數調大,就會出現下面的異常情況:

1. goroutine1 獲取開啟事物,讀取當前上傳到記錄是 1:2 (未提交事物)
2. goroutine1 在現有的記錄上加上自己處理的分片3,並和現有的1:2拼接在一起成1:2:3 (未提交事物)
3. goroutine2 獲取開啟事物,(因為RR,所以它讀不到1:2:3)讀取當前上傳到記錄是 1:2 (未提交事物)
4. goroutine1 提交事物,將1:2:3寫回到mysql
5. goroutine2 在現有的記錄上加上自己處理的分片4,並和現有的1:2拼接在一起成1:2:4 (提交事物)

可以看到,如果前端併發上傳,後端就會出現分片丟失的問題。 故前端將併發數置為1。

6、單個chunk上傳失敗怎麼辦?

前端會重傳chunk?

由於網絡問題,或者時後端處理chunk時出現的其他未知的錯誤,會導致chunk上傳失敗。

uploaded.js 中有如下的配置項, 每次uploader.js 在上傳每一個切片實際上都是在發送一次post請求,後端根據這個post請求是會給前端一個狀態嗎。 uploader.js 就是根據這個狀態碼去判斷是失敗了還是成功了,如果失敗了就會重新發送這個上傳的請求。

那uploader.js是如何知道有哪些狀態嗎是它應該重傳chunk的標記呢? 看看下面uploader.js需要的options 就明白了,其中的permantErrors中配置的狀態碼標示:當遇到這個狀態碼時整個上傳直接失敗~

successStatuses中配置的狀態碼錶示chunk是上傳成功的~。 其他的狀態嗎uploader.js 就會任務chunk上傳的有問題,於是重新上傳~

        options: {
          target: 'http://localhost:8081/file/upload',
          maxChunkRetries: 3,
          permanentErrors:[502], // 永久性的上傳失敗~,會認為整個文件都上傳失敗了
          successStatuses:[200], // 當前chunk上傳成功后的狀態嗎
          ...
        }

7、超過重傳次數后,怎麼辦?

比如我們設置出錯后重傳的次數為3,那麼無論當前分片是第幾片,整個文件的上傳狀態被標記為false,這就意味着會終止所有的上傳。

肯定不會出現這種情況:chunk1重傳3次后失敗了,chunk2還能再去上傳,這樣的話數據肯定不一致了。

8、如何控制上傳多大的文件?

目前了解到nginx端的限制上單次上傳不能超過1M。

前端會對大文件進行切片突破nginx的限制。

        options: {
          target: 'http://localhost:8081/file/upload',
          chunkSize: 512000, // 單次上傳 512KB 
        }     

如果後續和nginx負責的同學達成一致,可以把這個值進行調整。前端可以後續將這個chunk的閾值加大。

9、如何保證上傳文件的百分百正確?

在上傳文件前,前端會計算出當前RD選擇的這個文件的 md5 值。

當後端檢測到所有的分片全部上傳完畢,這時會merge所有分片匯聚成單個文件。計算這個文件的md5 同 RD在前端提供的文件的md5值比對。 比對結果一致說明RD正確的完成了上傳。結果不一致,說明文件上傳失敗了~返回給前端任務失敗,提示RD重新上傳。

10、其他細節問題:

如何判斷文件上傳失敗了,給RD展示紅色?

如何控制上傳什麼類型的文件?

如何控制不能上傳空文件?

上面說過了,當 uploader.js 遇到了permanentErrors這種狀態碼時會認為文件上傳失敗了。

前端想在上傳失敗后,將進度條轉換成紅色,其實改一下CSS樣式就好了,問題就在於,根據什麼去修改?在哪裡去修改?

前端會將每一個file封裝成一個組件:如下圖中的files就是file的集合

整個的fileList會將會被渲染成下面這樣。

我們上傳的文件被vue-simple-uploader的作者封裝成一個file.vue組件,這個對象中會有個配置參數, 比如它會長下面這樣。

     options: {
        target: 'http://localhost:8081/file/upload',
        statusText: {
          success: '上傳成功',
          error: '上傳出錯,請重試',
          typeError: '暫不支持上傳您添加的文件格式',
          uploading: '上傳中',
          emptyError:'不能上傳空文件',
          paused: '請確認文件後點擊上傳',
          waiting: '等待中'
        }
      }
    },

我們將上面的配置添加給Uploader.js

      const uploader = new Uploader(this.options)

在file組件中有如下計算屬性的,分別是status和statusText

    computed: {
      // 計算出一個狀態信息
      status () {
        const isUploading = this.isUploading // 是否正在上傳
        const isComplete = this.isComplete // 是否已經上傳完成
        const isError = this.error // 是否出錯了
        const isTypeError = this.typeError // 是否出錯了
        const paused = this.paused // 是否暫停了
        const isEmpty = this.emptyError // 是否暫停了
        // 哪個屬性先不為空,就返回哪個屬性
        if (isComplete) {
          return 'success'
        } else if (isError) {
          return 'error'
        } else if (isUploading) {
          return 'uploading'
        } else if (isTypeError) {
          return 'typeError'
        } else if (isEmpty) {
          return 'emptyError'
        } else if (paused) {
          return 'paused'
        } else {
          return 'waiting'
        }
      },
      // 狀態文本提示信息
      statusText () {
        // 獲取到計算出的status屬性(相當於是個key,具體的值在下面的fileStatusText中獲取到)
        const status = this.status
        // 從file的uploader對象中獲取到 fileStatusText,也就是用自己定義的名字
        const fileStatusText = this.file.uploader.fileStatusText
        let txt = status
        if (typeof fileStatusText === 'function') {
          txt = fileStatusText(status, this.response)
        } else {
          txt = fileStatusText[status]
        }
        return txt || status
      },
    },

status綁定在html上

	<div class="uploader-file" :status="status">

對應的CSS樣式入下:

  .uploader-file[status="error"] .uploader-file-progress {
    background: #ffe0e0;
  }

綜上:有了上面代碼的編寫,我們可以直接像下面這樣控制就好了

  file.typeError = true // 表示文件的類型不符合我們的預期,不允許RD上傳
  file.error = true // 表示文件上傳失敗了
  file.emptyError = true // 表示文件為空,不允許上傳

11、後端數據庫表設計

CREATE TABLE `file_upload_detail` (                                                                               
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',                                                           
  `username` varchar(64) NOT NULL COMMENT '上傳文件的用戶賬號',                                                            
  `file_name` varchar(64) NOT NULL COMMENT '上傳文件名',                                                               
  `md5` varchar(255) NOT NULL COMMENT '上傳文件的MD5值',                                                                
  `is_uploaded` int(11) DEFAULT '0' COMMENT '是否完整上傳過 \n0:否\n1:是',                                                 
  `has_been_uploaded` varchar(1024) DEFAULT NULL COMMENT '曾經上傳過的分片號',                                             
  `url` varchar(255) DEFAULT NULL COMMENT 'bos中的url,或者是本機的url地址',                                                 
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '本條記錄創建時間',     
  `update_time` timestamp NULL DEFAULT NULL  COMMENT '本條記錄更新時間',                                                  
  `total_chunks` int(11) DEFAULT NULL COMMENT '文件的總分片數',                                                          
  PRIMARY KEY (`id`)                                                                                              
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8                                                             

12、關於什麼時候mergechunk

在本文中給出的demo中,merge是後端處理完成所有的chunk后,像前端返回 merge=1,這個表示來實現的。

前端拿着這個字段去發送/merge請求去合併所有的chunk。

值得注意的地方是:這個請求是在uploader.js認為所有的分片全部成功上傳后,在單個文件成功上傳的回調中執行的。我想了一下,感覺這麼搞其實不太友好,萬一merge的過程中失敗了,或者是某個chunk丟失了,chunk中的數據缺失,最終merge的產物的md5值其實並不等於原文件。當這種情況發生的時候,其實上傳是失敗的。但是後端既然告訴uploader.js 可以合併了,說明後端的upload函數認為任務是成功的。vue-simple-uploader上傳完最後一個chunk得到的狀態碼是200,它也會覺得任務是成功的,於是在前端段展示綠色的上傳成功給用戶看~(然而上傳是失敗的), 這麼看來,整個過程其實控制的不太好~

我現在的實現:直接幹掉merge請求,前端1條線程發送請求,將chunk依次發送到後端。後端檢測到所有的chunk都上傳過來後主動merge,merge完成后馬上校驗文件的md5值是否符合預期。這個處理過程在上傳最後一個chunk的請求中進行,因此可以實現的控制前端上傳成功還是失敗的樣式~
如果偏偏想追求極致的速度,可以考慮將後端更新isUpload字段的SQL換成 “select for update” 他可以鎖住你要更新的數據行
以及這一行上下的間隙,這樣就不會出現併發修改異常。前端也可以重新更換成多線程併發上傳的機制。理論上只要網絡帶寬允許你開啟五條線程,速度就快5倍。至於什麼時候merge,加個if判斷一下,當上傳過的分片數 == totalChunks 就可以merge了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧