Spring源碼之自動裝配

引言

我們使用Spring開發過程中經常會用到Autowired註解注入依賴的bean,這部分也是面試的熱點問題之一。今天咱們一起來深入研究下自動注入的背後實現原理。首先上一個例子,如下所示:

@RestController
public class TestController {
    @Autowired
    List<ICheckRuleService> checkRuleService;

    @RequestMapping("/test")
    public void test(){
        checkRuleService.forEach(x->x.valid());
    }
}

從填充Bean開始

Autowired是怎麼實現自動注入的呢,今天我們來通過源碼分析一下。當Spring創建 TestController Bean時,會調用AbstractBeanFactory#doGetBean(如果對Spring創建Bean流程不熟的讀者,可以給我留言,後面考慮是否寫個IOC系列),doGetBean裏面會調用doCreateBean()方法去創建Bean,創建Bean之後,會對Bean進行填充

try {
    this.populateBean(beanName, mbd, instanceWrapper);
    exposedObject = this.initializeBean(beanName, exposedObject, mbd);
}

populateBean 里有這樣一段代碼,看起來是處理Autowired的,分別是autowireByName 和 autowireByType

PropertyValues pvs = mbd.hasPropertyValues() ? mbd.getPropertyValues() : null;
if (mbd.getResolvedAutowireMode() == 1 || mbd.getResolvedAutowireMode() == 2) {
    MutablePropertyValues newPvs = new MutablePropertyValues((PropertyValues)pvs);
    if (mbd.getResolvedAutowireMode() == 1) {
       this.autowireByName(beanName, mbd, bw, newPvs);
    }

   if (mbd.getResolvedAutowireMode() == 2) {
       this.autowireByType(beanName, mbd, bw, newPvs);
   }

    pvs = newPvs;
}

我們來驗證一下,通過斷點調試我們發現並不會進入if里,所以自動注入並不是這裏實現的。那這裡有什麼用呢,先放一放,後面再說。

後置處理器屬性填充

那麼到底是哪裡注入進去的呢?我們繼續往下看,在這段代碼下方有個BeanPostProcessor的邏輯,通過斷點我們發現有個AutowiredAnnotationBeanPostProcessor 的後置處理器,當這個BeanPostProcessor執行完 postProcessPropertyValues方法后,testController的checkRuleService 屬性就有了值了,說明屬性值注入肯定和 AutowiredAnnotationBeanPostProcessor 有關,我們跟進去看一下

進入AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues 方法里,裏面主要有兩部分邏輯

  • 首先看到一段 findAutowiringMetadata 的邏輯,根據方法名稱知道是獲取當前bean的注入元信息

  • 調用 metadata.inject 注入屬性

public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
    InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);

    try {
        metadata.inject(bean, beanName, pvs);
        return pvs;
    } catch (BeanCreationException var7) {
        throw var7;
    } catch (Throwable var8) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var8);
    }
}

我們先來看第一部分:findAutowiringMetadata

我們進入findAutowiringMetadata,看下它的邏輯,先從 injectionMetadataCache 緩存里取,如果取不到值,則調用buildAutowiringMetadata 構建 InjectionMetadata ,構建成功後設置到緩存里。

    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
        String cacheKey = StringUtils.hasLength(beanName) ? beanName : clazz.getName();
        InjectionMetadata metadata = (InjectionMetadata)this.injectionMetadataCache.get(cacheKey);
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized(this.injectionMetadataCache) {
                metadata = (InjectionMetadata)this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }

                    metadata = this.buildAutowiringMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }

        return metadata;
    }

我們來看下 buildAutowiringMetadata,繼續跟進去,源碼如下:

裏面是通過當前Bean的Class反射獲取 Field 和 Method ,然後對 Field 和 Method 分別調 findAutowiredAnnotation 方法獲取自動注入的註解,然後根據註解類型是否required構建不同類型的InjectedElement。

  • AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement:

boolean required = this.determineRequiredStatus(ann);
currElements.add(new AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement(field, required));
  • AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement:

boolean required = this.determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement(method, required, pd));

補充:通過AutowiredAnnotationBeanPostProcessor 構造函數我們知道,自動注入處理的是被 @Autowired 和 @Value 這兩個註解標註的屬性(Field)或方法(Method):

    public AutowiredAnnotationBeanPostProcessor() {

        this.autowiredAnnotationTypes.add(Autowired.class);

        this.autowiredAnnotationTypes.add(Value.class);

    //......

到這裏,需要注入的元數據信息就已經構建完成了,接下來就要到注入部分了。來看下 postProcessPropertyValues 的第二部分。

再看第二部分:metadata.inject

前面獲取到了需要注入的元數據信息,接下來是元數據 inject 的實現,繼續跟進去,裏面是一個for循環,循環調用了element的inject方法

if (!((Collection)elementsToIterate).isEmpty()) {
    for(Iterator var6 = ((Collection)elementsToIterate).iterator(); var6.hasNext(); element.inject(target, beanName, pvs)) {
        element = (InjectionMetadata.InjectedElement)var6.next();
        if (logger.isDebugEnabled()) {
            logger.debug("Processing injected element of bean '" + beanName + "': " + element);
        }
    }
}

我們斷點調試進去,發現element的真實類型是AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement,而當前element 真實類型是 TestController.checkRuleService 的集合。

我們進入AutowiredFieldElement#inject方法,首先嘗試從緩存里拿當前Field的值,肯定拿不到,所以走的是else分支,else分支里從beanFactory里解析當前Field屬性值

value = AutowiredAnnotationBeanPostProcessor.this.beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);

繼續跟進去,發現其實調用的 doResolveDependency 方法

越來越接近真相了,不要着急,繼續跟進去

發現一個類型為Object的 multipleBeans ,結果返回的也是這個Object,我們大膽猜測這個Object就是我們需要注入的List屬性,繼續跟進去驗證一下:

我們看一下 Collection 分支的源碼

 else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
            elementType = descriptor.getResolvableType().asCollection().resolveGeneric(new int[0]);
            if (elementType == null) {
                return null;
            } else {
                Map<String, Object> matchingBeans = this.findAutowireCandidates(beanName, elementType, new DefaultListableBeanFactory.MultiElementDescriptor(descriptor)); if (matchingBeans.isEmpty()) {
                    return null;
                } else {
                    if (autowiredBeanNames != null) {
                        autowiredBeanNames.addAll(matchingBeans.keySet());
                    }
​
                    TypeConverter converter = typeConverter != null ? typeConverter : this.getTypeConverter();
                    Object result = converter.convertIfNecessary(matchingBeans.values(), type);
                    if (this.getDependencyComparator() != null && result instanceof List) {
                        ((List)result).sort(this.adaptDependencyComparator(matchingBeans));
                    }
​
                    return result;
                }
            }
        }

裏面是調用了 findAutowireCandidates 來獲取Bean,findAutowireCandidates 內部會獲取到依賴的BeanNames,然後根據beanName 循環調用beanFactory#getBean 獲取需要注入的bean

this.findAutowireCandidates(beanName,elementType,new DefaultListableBeanFactory.MultiElementDescriptor(descriptor))

beanFactory#getBean方法,最終會調用 AbstractBeanFactory#doGetBean,獲取到需要裝配進去的屬性bean。

    public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory) throws BeansException {
        return beanFactory.getBean(beanName);
    }

當所有的循環執行完畢,就獲取到了 multipleBeans ,驗證了前面的猜測。真是太不容易,趕緊設置緩存  

最終通過field.set 將獲取到的List屬性值value設置到當前bean里,代碼如下:

if (value != null) {
    ReflectionUtils.makeAccessible(field);
    field.set(bean, value);
}

執行field的set方法后,再來看checkRuleService屬性就有值了

如果是Method注入,對應的就是通過反射調用 method.invoke 將屬性設置到方法參數里,大致流程差不多。到此,Autowired 裝配流程也就結束了。

前面在講到 populateBean 的時候,有個根據 autowireMode 判斷是否執行屬性注入,當時獲取的autowireMode==0,那麼什麼時候autowireMode 會有值並且會根據autowireByName 和 autowireByType來裝配呢?

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw)

其實也很好理解,通過源碼我們知道,這裏的 mbd 是一個 RootBeanDefinition ,也就是說這裏的 mbd.getResolvedAutowireMode()獲取的值是通過Bean定義或者通過PostProcessor拿到BeanDefinition,然後設置了AutowireMode屬性才會有值。當我們查看這裏的autowireByType源碼(AbstractAutowireCapableBeanFactory#autowireByType)可以發現,其實autowireByType也是會調用resolveDependency,繼續跟進去,發現其實調用的 doResolveDependency 方法,而AutowiredAnnotationBeanPostProcessor 也是通過這個方法實現的自動注入,後面的流程就都一樣了。

最後總結一下

1、bean創建完成后,會調用 populateBean() 填充Bean,在populateBean()方法里會獲取所有的BeanPostProcessor,並循環執行 BeanPostProcessor#postProcessPropertyValues() 設置屬性

2、其中有個AutowiredAnnotationBeanPostProcessor,這個處理器里會根據當前Bean的Class,通過反射獲取 Field 和 Method ,分別獲取 Field 和 Method 上的自動注入的註解(@Autowired 和 @Value),構建注入元數據InjectionMetadata

3、調用注入元數據InjectionMetadata的 inject() 方法,裝配屬性(有兩種:AutowiredFieldElement 和AutowiredMethodElement),會調用this.beanFactory.resolveDependency(desc,beanName,autowiredBeanNames, typeConverter) 解析依賴的屬性值

4、resolveDependency 最終會調用到 resolveMultipleBeans ,而 resolveMultipleBeans 會根據當前注入屬性的類型分別按 Array、Collection、Map 走不同的分支,在分支里調用 findAutowireCandidates 獲取注入bean的實例,最終回調到 AbstractBeanFactory#doGetBean

5、獲取到所有需要注入的屬性 bean 實例后,通過反射設置到對應的屬性或方法里去,就完成了自動注入全流程了

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

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

疫情衝擊酪農 日本北海道知事推牛奶挑戰

摘錄自2020年5月10日中央通訊社報導

日本境內2019冠狀病毒疾病(COVID-19)疫情延燒,連帶衝擊到北海道酪農生乳銷量,讓北海道知事鈴木直道推出「牛奶挑戰」,呼籲民眾多喝牛奶,增加牛奶或優格購買量。日本放送協會(NHK)報導,鈴木直道在一段影片中單手拿著一杯牛奶,然後一口氣喝完,這個稱為「牛奶挑戰」的活動,希望能提升牛奶消費量。

日本境內受到疫情影響,學校臨時停課,所以沒有提供營養午餐,讓牛奶消費量下滑;民眾避免外出及出遊,也造成牛奶製品的冰淇淋、蛋糕等甜點銷量銳減。牛奶消費量減少直接衝擊酪農,而北海道的農業生產額中,酪農占了4成,在北海道農產占有相當重要地位。

一般來說,需求減少可以減產因應,但事實上不然,北海道江別市酪農川口谷仁說,每天要幫牛擠3次奶,如果不這麼做,牛就會生病,所以無法停止擠奶。加上北海道每年5月到6月的氣候,對牛來說是最舒服的時候,也是一年中最容易增加生乳產量的時期。而再這樣下去的話,最糟的情況就是造成廠商無法再收購,每天固定生產的生乳將不得不廢棄。

生活環境
國際新聞
日本
北海道
酪農
牛奶
武漢肺炎
疫情下的食衣住行
公共衛生
經濟動物
動物福利

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

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

給非目標魚類的「逃生指示燈」 研究:漁網裝LED燈 可使混獲減半

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

疫情讓自然資源復甦 泰國家公園擬每年關3個月

摘錄自2020年5月11日中央社報導

為了避免武漢肺炎疫情擴大,泰國從3月25日關閉所有的國家公園,例如知名的考艾國家公園(Khao Yai National Park)是開園58年以來第一次閉園,而且數條穿過國家公園的公路也因此封閉。

泰國公視(Thai PBS)報導,泰國自然資源和環境部(Ministry of Natural Resources and Environment)部長烏拉沃(Varawut Silpa-archa)日前表示,過去兩個月國家公園因為疫情閉園,反讓許多野生動物再生。

烏拉沃表示,受到這樣狀況的啟發,自然資源和環境部未來準備讓全泰國157個國家公園每年閉園三個月,他要求國家公園局(Department of National Parks, Wildlife and Plant Conservation)在一切恢復正常後,研擬適合的閉園時間表。

泰媒經理人日報(Manager Daily)報導,國家公園局局長譚亞(Thanya Netithamkul)表示,國家公園局已經和相關單位討論過,會要求各個國家公園準備相關計畫。

生態保育
生物多樣性
國際新聞
泰國
國家公園
疫情

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

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分佈式鎖沒那麼難,手把手教你實現 Redis 分佈鎖!|保姆級教程

書接上文

上篇文章「MySQL 可重複讀,差點就讓我背上了一個 P0 事故!」發布之後,收到很多小夥伴們的留言,從中又學習到很多,總結一下。

上篇文章可能舉得例子有點不恰當,導致有些小夥伴沒看懂為什麼餘額會變負。

這次我們舉得實際一點,還是上篇文章 account 表,假設 id=1,balance=1000,不過這次我們扣款 1000,兩個事務的時序圖如下:

這次使用兩個命令窗口真實執行一把:

注意事務 2,③處查詢到 id=1,balance=1000,但是實際上由於此時事務 1 已經提交,最新結果如②處所示 id=1,balance=900

本來 Java 代碼層會做一層餘額判斷:

if (balance - amount < 0) {
  throw new XXException("餘額不足,扣減失敗");
}

但是此時由於 ③ 處使用快照讀,讀到是箇舊值,未讀到最新值,導致這層校驗失效,從而代碼繼續往下運行,執行了數據更新。

更新語句又採用如下寫法:

UPDATE account set balance=balance-1000 WHERE id =1;

這條更新語句又必須是在這條記錄的最新值的基礎做更新,更新語句執行結束,這條記錄就變成了 id=1,balance=-1000

之前有朋友疑惑 t12 更新之後,再次進行快照讀,結果會是多少。

上圖執行結果 ④ 可以看到結果為 id=1,balance=-1000,可以看到已經查詢最新的結果記錄。

這行數據最新版本由於是事務 2 自己更新的,自身事務更新永遠對自己可見

另外這次問題上本質上因為 Java 層與數據庫層數據不一致導致,有的朋友留言提出,可以在更新餘額時加一層判斷:

UPDATE account set balance=balance-1000 WHERE id =1 and balance>0;

然後更新完成,Java 層判斷更新有效行數是否大於 0。這種做法確實能規避這個問題。

最後這位朋友留言總結的挺好,粘貼一下:

先贊后看,微信搜索「程序通事」,關注就完事了

手擼分佈式鎖

現在切回正文,這篇文章本來是準備寫下 Mysql 查詢左匹配的問題,但是還沒研究出來。那就先寫下最近在鼓搗一個東西,使用 Redis 實現可重入分佈鎖。

看到這裏,有的朋友可能會提出來使用 redisson 不香嗎,為什麼還要自己實現?

哎,redisson 真的很香,但是現有項目中沒辦法使用,只好自己手擼一個可重入的分佈式鎖了。

雖然用不了 redisson,但是我可以研究其源碼,最後實現的可重入分佈鎖參考了 redisson 實現方式。

分佈式鎖

分佈式鎖特性就要在於排他性,同一時間內多個調用方加鎖競爭,只能有一個調用方加鎖成功。

Redis 由於內部單線程的執行,內部按照請求先後順序執行,沒有併發衝突,所以只會有一個調用方才會成功獲取鎖。

而且 Redis 基於內存操作,加解鎖速度性能高,另外我們還可以使用集群部署增強 Redis 可用性。

加鎖

使用 Redis 實現一個簡單的分佈式鎖,非常簡單,可以直接使用 SETNX 命令。

SETNX 是『SET if Not eXists』,如果不存在,才會設置,使用方法如下:

不過直接使用 SETNX 有一個缺陷,我們沒辦法對其設置過期時間,如果加鎖客戶端宕機了,這就導致這把鎖獲取不了了。

有的同學可能會提出,執行 SETNX 之後,再執行 EXPIRE 命令,主動設置過期時間,偽碼如下:

var result = setnx lock "client"
if(result==1){
    // 有效期 30 s
    expire lock 30
}

不過這樣還是存在缺陷,加鎖代碼並不能原子執行,如果調用加鎖語句,還沒來得及設置過期時間,應用就宕機了,還是會存在鎖過期不了的問題。

不過這個問題在 Redis 2.6.12 版本 就可以被完美解決。這個版本增強了 SET 命令,可以通過帶上 NX,EX 命令原子執行加鎖操作,解決上述問題。參數含義如下:

  • EX second :設置鍵的過期時間,單位為秒
  • NX 當鍵不存在時,進行設置操作,等同與 SETNX 操作

使用 SET 命令實現分佈式鎖只需要一行代碼:

SET lock_name anystring NX EX lock_time

解鎖

解鎖相比加鎖過程,就顯得非常簡單,只要調用 DEL 命令刪除鎖即可:

DEL lock_name

不過這種方式卻存在一個缺陷,可能會發生錯解鎖問題。

假設應用 1 加鎖成功,鎖超時時間為 30s。由於應用 1 業務邏輯執行時間過長,30 s 之後,鎖過期自動釋放。

這時應用 2 接着加鎖,加鎖成功,執行業務邏輯。這個期間,應用 1 終於執行結束,使用 DEL 成功釋放鎖。

這樣就導致了應用 1 錯誤釋放應用 2 的鎖,另外鎖被釋放之後,其他應用可能再次加鎖成功,這就可能導致業務重複執行。

為了使鎖不被錯誤釋放,我們需要在加鎖時設置隨機字符串,比如 UUID。

SET lock_name uuid NX EX lock_time

釋放鎖時,需要提前獲取當前鎖存儲的值,然後與加鎖時的 uuid 做比較,偽代碼如下:

var value= get lock_name
if value == uuid
	// 釋放鎖成功
else
	// 釋放鎖失敗

上述代碼我們不能通過 Java 代碼運行,因為無法保證上述代碼原子化執行。

幸好 Redis 2.6.0 增加執行 Lua 腳本的功能,lua 代碼可以運行在 Redis 服務器的上下文中,並且整個操作將會被當成一個整體執行,中間不會被其他命令插入。

這就保證了腳本將會以原子性的方式執行,當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執行。在其他的別的客戶端看來,執行腳本的效果,要麼是不可見的,要麼就是已完成的。

EVAL 與 EVALSHA

EVAL

Redis 可以使用 EVAL 執行 LUA 腳本,而我們可以在 LUA 腳本中執行判斷求值邏輯。EVAL 執行方式如下:

EVAL script numkeys key [key ...] arg [arg ...]

numkeys 參數用於建明參數,即後面 key 數組的個數。

key [key ...] 代表需要在腳本中用到的所有 Redis key,在 Lua 腳本使用使用數組的方式訪問 key,類似如下 KEYS[1]KEYS[2]。注意 Lua 數組起始位置與 Java 不同,Lua 數組是從 1 開始。

命令最後,是一些附加參數,可以用來當做 Redis Key 值存儲的 Value 值,使用方式如 KEYS 變量一樣,類似如下:ARGV[1]ARGV[2]

用一個簡單例子運行一下 EVAL 命令:

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2],ARGV[3]}" 2 key1 key2 first second third

運行效果如下:

可以看到 KEYSARGVS內部數組可以不一致。

在 Lua 腳本可以使用下面兩個函數執行 Redis 命令:

  • redis.call()
  • redis.pcall()

兩個函數作用法與作用完全一致,只不過對於錯誤的處理方式不一致,感興趣的小夥伴可以具體點擊以下鏈接,查看錯誤處理一章。

http://doc.redisfans.com/script/eval.html

下面我們統一在 Lua 腳本中使用 redis.call(),執行以下命令:

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 foo 樓下小黑哥

運行效果如下:

EVALSHA

EVAL 命令每次執行時都需要發送 Lua 腳本,但是 Redis 並不會每次都會重新編譯腳本。

當 Redis 第一次收到 Lua 腳本時,首先將會對 Lua 腳本進行 sha1 獲取簽名值,然後內部將會對其緩存起來。後續執行時,直接通過 sha1 計算過後簽名值查找已經編譯過的腳本,加快執行速度。

雖然 Redis 內部已經優化執行的速度,但是每次都需要發送腳本,還是有網絡傳輸的成本,如果腳本很大,這其中花在網絡傳輸的時間就會相應的增加。

所以 Redis 又實現了 EVALSHA 命令,原理與 EVAL 一致。只不過 EVALSHA 只需要傳入腳本經過 sha1計算過後的簽名值即可,這樣大大的減少了傳輸的字節大小,減少了網絡耗時。

EVALSHA命令如下:

evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 foo 樓下小黑哥

運行效果如下:

SCRIPT FLUSH 命令用來清除所有 Lua 腳本緩存。

可以看到,如果之前未執行過 EVAL命令,直接執行 EVALSHA 將會報錯。

優化執行 EVAL

我們可以結合使用 EVALEVALSHA,優化程序。下面就不寫偽碼了,以 Jedis 為例,優化代碼如下:

//連接本地的 Redis 服務
Jedis jedis = new Jedis("localhost", 6379);
jedis.auth("1234qwer");

System.out.println("服務正在運行: " + jedis.ping());

String lua_script = "return redis.call('set',KEYS[1],ARGV[1])";
String lua_sha1 = DigestUtils.sha1DigestAsHex(lua_script);

try {
    Object evalsha = jedis.evalsha(lua_sha1, Lists.newArrayList("foo"), Lists.newArrayList("樓下小黑哥"));
} catch (Exception e) {
    Throwable current = e;
    while (current != null) {
        String exMessage = current.getMessage();
        // 包含 NOSCRIPT,代表該 lua 腳本從未被執行,需要先執行 eval 命令
        if (exMessage != null && exMessage.contains("NOSCRIPT")) {
            Object eval = jedis.eval(lua_script, Lists.newArrayList("foo"), Lists.newArrayList("樓下小黑哥"));
            break;
        }

    }
}
String foo = jedis.get("foo");
System.out.println(foo);

上面的代碼看起來還是很複雜吧,不過這是使用原生 jedis 的情況下。如果我們使用 Spring Boot 的話,那就沒這麼麻煩了。Spring 組件執行的 Eval 方法內部就包含上述代碼的邏輯。

不過需要注意的是,如果 Spring-Boot 使用 Jedis 作為連接客戶端,並且使用Redis Cluster 集群模式,需要使用 2.1.9 以上版本的spring-boot-starter-data-redis,不然執行過程中將會拋出:

org.springframework.dao.InvalidDataAccessApiUsageException: EvalSha is not supported in cluster environment.

詳細情況可以參考這個修復的 IssueAdd support for scripting commands with Jedis Cluster

優化分佈式鎖

講完 Redis 執行 LUA 腳本的相關命令,我們來看下如何優化上面的分佈式鎖,使其無法釋放其他應用加的鎖。

以下代碼基於 spring-boot 2.2.7.RELEASE 版本,Redis 底層連接使用 Jedis。

加鎖的 Redis 命令如下:

SET lock_name uuid NX EX lock_time

加鎖代碼如下:

/**
 * 非阻塞式加鎖,若鎖存在,直接返回
 *
 * @param lockName  鎖名稱
 * @param request   唯一標識,防止其他應用/線程解鎖,可以使用 UUID 生成
 * @param leaseTime 超時時間
 * @param unit      時間單位
 * @return
 */
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    // 注意該方法是在 spring-boot-starter-data-redis 2.1 版本新增加的,若是之前版本 可以執行下面的方法
    return stringRedisTemplate.opsForValue().setIfAbsent(lockName, request, leaseTime, unit);
}

由於setIfAbsent方法是在 spring-boot-starter-data-redis 2.1 版本新增加,之前版本無法設置超時時間。如果使用之前的版本的,需要如下方法:

/**
 * 適用於 spring-boot-starter-data-redis 2.1 之前的版本
 *
 * @param lockName
 * @param request
 * @param leaseTime
 * @param unit
 * @return
 */
public Boolean doOldTryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    Boolean result = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
        RedisSerializer valueSerializer = stringRedisTemplate.getValueSerializer();
        RedisSerializer keySerializer = stringRedisTemplate.getKeySerializer();

        Boolean innerResult = connection.set(keySerializer.serialize(lockName),
                valueSerializer.serialize(request),
                Expiration.from(leaseTime, unit),
                RedisStringCommands.SetOption.SET_IF_ABSENT
        );
        return innerResult;
    });
    return result;
}

解鎖需要使用 Lua 腳本:

-- 解鎖代碼
-- 首先判斷傳入的唯一標識是否與現有標識一致
-- 如果一致,釋放這個鎖,否則直接返回
if redis.call('get', KEYS[1]) == ARGV[1] then
   return redis.call('del', KEYS[1])
else
   return 0
end

這段腳本將會判斷傳入的唯一標識是否與 Redis 存儲的標示一致,如果一直,釋放該鎖,否則立刻返回。

釋放鎖的方法如下:

/**
 * 解鎖
 * 如果傳入應用標識與之前加鎖一致,解鎖成功
 * 否則直接返回
 * @param lockName 鎖
 * @param request 唯一標識
 * @return
 */
public Boolean unlock(String lockName, String request) {
    DefaultRedisScript<Boolean> unlockScript = new DefaultRedisScript<>();
    unlockScript.setLocation(new ClassPathResource("simple_unlock.lua"));
    unlockScript.setResultType(Boolean.class);
    return stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
}

由於公號外鏈無法直接跳轉,關注『程序通事』,回復分佈式鎖獲取源代碼。

Redis 分佈式鎖的缺陷

無法重入

由於上述加鎖命令使用了 SETNX ,一旦鍵存在就無法再設置成功,這就導致後續同一線程內繼續加鎖,將會加鎖失敗。

如果想將 Redis 分佈式鎖改造成可重入的分佈式鎖,有兩種方案:

  • 本地應用使用 ThreadLocal 進行重入次數計數,加鎖時加 1,解鎖時減 1,當計數變為 0 釋放鎖
  • 第二種,使用 Redis Hash 表存儲可重入次數,使用 Lua 腳本加鎖/解鎖

第一種方案可以參考這篇文章分佈式鎖的實現之 redis 篇。第二個解決方案,下一篇文章就會具體來聊聊,敬請期待。

鎖超時釋放

假設線程 A 加鎖成功,鎖超時時間為 30s。由於線程 A 內部業務邏輯執行時間過長,30s 之後鎖過期自動釋放。

此時線程 B 成功獲取到鎖,進入執行內部業務邏輯。此時線程 A 還在執行執行業務,而線程 B 又進入執行這段業務邏輯,這就導致業務邏輯重複被執行。

這個問題我覺得,一般由於鎖的超時時間設置不當引起,可以評估下業務邏輯執行時間,在這基礎上再延長一下超時時間。

如果超時時間設置合理,但是業務邏輯還有偶發的超時,個人覺得需要排查下業務執行過長的問題。

如果說一定要做到業務執行期間,鎖只能被一個線程佔有的,那就需要增加一個守護線程,定時為即將的過期的但未釋放的鎖增加有效時間。

加鎖成功后,同時創建一個守護線程。守護線程將會定時查看鎖是否即將到期,如果鎖即將過期,那就執行 EXPIRE 等命令重新設置過期時間。

說實話,如果要這麼做,真的挺複雜的,感興趣的話可以參考下 redisson watchdog 實現方式。

Redis 分佈式鎖集群問題

為了保證生產高可用,一般我們會採用主從部署方式。採用這種方式,我們可以將讀寫分離,主節點提供寫服務,從節點提供讀服務。

Redis 主從之間數據同步採用異步複製方式,主節點寫入成功后,立刻返回給客戶端,然後異步複製給從節點。

如果數據寫入主節點成功,但是還未複製給從節點。此時主節點掛了,從節點立刻被提升為主節點。

這種情況下,還未同步的數據就丟失了,其他線程又可以被加鎖了。

針對這種情況, Redis 官方提出一種 RedLock 的算法,需要有 N 個Redis 主從節點,解決該問題,詳情參考:

https://redis.io/topics/distlock。

這個算法自己實現還是很複雜的,幸好 redisson 已經實現的 RedLock,詳情參考:redisson redlock

總結

本來這篇文章是想寫 Redis 可重入分佈式鎖的,可是沒想到寫分佈式鎖的實現方案就已經寫了這麼多,再寫下去,文章可能就很長,所以拆分成兩篇來寫。

嘿嘿,這不下星期不用想些什麼了,真是個小機靈鬼~

好了,幫大家再次總結一下本文內容。

簡單的 Redis 分佈式鎖的實現方式還是很簡單的,我們可以直接用 SETNX/DEL 命令實現加解鎖。

不過這種實現方式不夠健壯,可能存在應用宕機,鎖就無法被釋放的問題。

所以我們接着引入以下命令以及 Lua 腳本增強 Redis 分佈式鎖。

SET lock_name anystring NX EX lock_time

最後 Redis 分佈鎖還是存在一些缺陷,在這裏提出一些解決方案,感興趣同學可以自己實現一下。

下篇文章再來將將 Redis 可重入分佈式鎖~

參考資料

  1. 分佈式鎖的實現之 redis 篇
  2. 基於 Redis 的分佈式鎖

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

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

再生能源興起及疫情助攻 印度碳排放40年來首下降

摘錄自2020年5月12日自由時報報導

印度的二氧化碳排放量出現40年來首次下降,根據印度國家電力公司的數據,二氧化碳排放量在3月份下降了15%,4月份預計可下降30%。其中以燃煤減量為最主要的影響。

截至今年3月的財政年度報告,煤炭的運輸量下降約2%;而火力發電在過去10年中每年增長75%,石油消費需求也同樣下降。根據環境網站「Carbon Brief」的分析,用電下降和再生能源的競爭削弱了國內對化石燃料的需求,加上武漢肺炎(新型冠狀病毒疾病,Covid-19)疫情爆發,印度國內實施全境封鎖,成功改變碳排放量增長的趨勢。

據報導,印度今年3月石油的消耗量下降18%,再生能源供應在過去1年中則有所增加。根據國際能源署(International Energy Agency,IEA)4月底發布的數據,今年第一季度全球燃煤使用量也下降了8%。但分析師則提醒,化石燃料使用率的下降可能不會持續,分析師指出,當全球疫情趨緩,各國重啟經濟,排放量將再次飆升。

公害污染
空氣污染
能源議題
再生能源
污染治理
能源轉型
國際新聞
印度
碳排放
武漢肺炎
化石燃料
疫情看氣候與能源

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

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

蘇門答臘高速公路通車前夕驚見虎蹤 生態廊道功效恐不足 專家建議另增柵欄

環境資訊中心綜合外電;范震華 翻譯;賴慧玲 審校;稿源:Mongabay

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

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

毀林、武肺、林火三重危機 專家警告:亞馬遜今年火災恐比去年惡化50%

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

真香,擼一個SpringBoot在線代碼修改器

前言

項目上線之後,如果是後端報錯,只能重新編譯打包部署然後重啟;如果僅僅是前端頁面、樣式、腳本修改,只需要替換到就可以了。

小公司的話可能比較自由,可以隨意替換,但是有些公司權限設置的比較嚴格,需要提交申請交給運維去處理。

如果僅僅是一個前端問題,又很緊急,這時候提申請走流程勢必會影響到用戶的正常使用。

今天,擼主給大家推薦一款前端代碼文件編輯器來解決以上問題。

案例

定義實體,用於前端文件樹展示:

@Data
public class SysFile {
    private Integer fileId;
    private String name;
    private Integer parentId;
    private String parentPath;
}

由於項目採用的是SpringBoot框架,打成了war包部署,後端採用以下方式獲取文件列表:

/**
 * 列表
 * @return
 */
@RequestMapping(value = "list", method = RequestMethod.POST)
public Result list() throws FileNotFoundException {
    String filePath = ResourceUtils.getURL("classpath:").getPath();
    List<SysFile> fileList = new ArrayList<>();
    getAllFilePaths(filePath,fileList,0,"");
    return Result.ok(fileList);
}

遞歸獲取某目錄下的所有子目錄以及子文件:

/**
 * 遞歸獲取某目錄下的所有子目錄以及子文件
 * @param filePath
 * @param filePathList
 * @return
 */
private static List<SysFile> getAllFilePaths(String filePath, List<SysFile> filePathList,
                                             Integer level,String parentPath) {
    File[] files = new File(filePath).listFiles();
    if (files == null) {
        return filePathList;
    }
    for (File file : files) {
        int num = filePathList.size()+1;
        SysFile sysFile = new SysFile();
        sysFile.setName(file.getName());
        sysFile.setFileId(num);
        sysFile.setParentId(level);
        if (file.isDirectory()) {
            if(level==0){
                if(file.getName().equals("templates")||
                        file.getName().equals("static")){
                    filePathList.add(sysFile);
                    parentPath = SystemConstant.SF_FILE_SEPARATOR+file.getName();
                    getAllFilePaths(file.getAbsolutePath(), filePathList,num,parentPath);
                    num++;
                }
            }else{
                filePathList.add(sysFile);
                String subParentPath = parentPath+SystemConstant.SF_FILE_SEPARATOR+file.getName();
                getAllFilePaths(file.getAbsolutePath(), filePathList,num,subParentPath);
                num++;
            }
        } else {
            if(level!=0){
                sysFile.setParentPath(parentPath+SystemConstant.SF_FILE_SEPARATOR+file.getName());
                filePathList.add(sysFile);
                num++;
            }
        }
    }
    return filePathList;
}

獲取文件內容:

/**
 * 獲取內容
 * @return
 */
@RequestMapping(value = "getContent", method = RequestMethod.POST)
public Result getContent(String filePath) throws FileNotFoundException {
    String path = ResourceUtils.getURL("classpath:").getPath();
    String content = FileUtil.readUtf8String(path+filePath);
    return Result.ok(content);
}

修改保存:

/**
 * 保存內容
 * @return
 */
@RequestMapping(value = "save", method = RequestMethod.POST)
public Result save(String filePath, String content) throws FileNotFoundException {
    String path = ResourceUtils.getURL("classpath:").getPath();
    /**
     * 生產環境自行解除
     */
    if(active.equals("prod")){
        return Result.error("演示環境禁止插插插!!!");
    }else{
        File file = new File(path+filePath);
        long lastModified = file.lastModified();
        FileUtil.writeUtf8String(content,path+filePath);
        file.setLastModified(lastModified);
        return Result.ok();
    }
}

當然了,如果代碼修改比較多,也可以對文件進行上傳覆蓋操作。

截圖

小結

如果身邊恰好沒有工具連接遠程服務,亦或是自己沒有服務器的權限,這款在線修改器,擼主覺得還是很方便的。但一定要控制好權限,防止普通人員亂修改,還有一定要做好安全日誌記錄。

源碼

https://gitee.com/52itstyle/SPTools

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

※回頭車貨運收費標準

抓準時機 澳洲有望在武肺過後追趕全球綠能進度

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?