java作品集:企業信息門戶webtap

作品背景

隨着企業應用的軟件越來越多,並且信息軟件基本以B/S為主了,很多時候各種軟件的地址,讓大家記的頭昏腦脹,並且一堆密碼要記,而且大部分系統之間無法互通,雖然市面上有各種集成方案,但無法做到簡單有效,都是大型軟件廠商的PPT解決方案加一堆開發工作和大量的成本支出,最重要的是大部分都是體驗極差、毫無美感的東西。

解決方案

基於上述背景,個人利用業餘時間在持續完善做一款小作品,或多或少的解決一點問題,雖然目前還沒成熟,但是考慮再三,先開源出來,希望有志同道合的人一起完善。

作品說明

1.首頁

首頁主要功能有

  • app显示
  • 文件夾分類
  • 應用搜索
  • 登錄
  • 登錄后快捷新增應用
  • 背景自動每天同步bing搜索引擎的的壁紙
  • 應用和新聞站點鏈接(未完成)

通過點擊應用上的鎖 icon即可查看應用的賬號和密碼,在沒有單點登錄的功能情況下這個功能非常有用

2.系統登錄

點擊首頁右上角的 sigin 到登錄頁面

3.應用列表

後台管理 主功能只有新建應用、應用列表、系統設置,極其簡約,好不好看只是個人風格,默認登錄進來及显示應用列表。

4、新增應用

添加應用除了常規功能還增加了敏感信息輸入,敏感信息只能登錄后才能查看;
查看密碼功能考慮到很多時候連接地址需要密碼才能訪問;
查看權限目前只實現了登錄可見以及自己可見(權限功能還需要繼續完善);

5、系統設置

系統設置里可以進行基本信息維護,個人登錄信息維護,用戶管理,app分類管理,及多組織管理,主要介紹以下2重點功能。

常規設置

基本設置里為當前組織的組織名稱,訪問短鏈接地址(多組織情況下),以及組織的logo

多組織管理

技術架構

技術棧

  • springboot
  • mysql5.7
  • gradle
  • thymeleaf
  • vue2.0

代碼結構

數據庫結構

源碼地址

https://github.com/robotbird/webtap

https://gitee.com/robotbird/webtap

使用方法

  • 1、mysql 新建webtap數據庫
  • 2、導入工程目錄下doc/db/webtap.sql
  • 3、設置好application-dev.properties 里的數據庫密碼,默認root/root
  • 4、打個war包放到tomcat下即可運行,這地方沒有用jar包的原因是考慮上傳目錄採用jar包不知道什麼樣的方式合適。
  • 5、登錄管理員默認賬號robotbird@qq.com,密碼123456(暫時只支持郵箱登錄)

在線體驗

體驗地址:http://webtap.cn/
由於服務器在國外,訪問時候還請耐心等候。

總結

作品當前還未實現的功能,企業內部信息搜索集成、單點登錄集成、權限管理,以及後續考慮的小程序功能,但是依然放出來,激勵自己繼續完善下去。
此作品完全個人原創,開源遵從GNU General Public License v3.0,版權所屬個人所有,如果有同學對這個作品比較感興趣可以微信聯繫robotbird798

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

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

※回頭車貨運收費標準

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

OAuth + Security – 5 – Token存儲升級(數據庫、Redis)

PS:此文章為系列文章,建議從第一篇開始閱讀。

在我們之前的文章中,我們當時獲取到Token令牌時,此時的令牌時存儲在內存中的,這樣顯然不利於我們程序的擴展,所以為了解決這個問題,官方給我們還提供了其它的方式來存儲令牌,存儲到數據庫或者Redis中,下面我們就來看一看怎麼實現。

不使用Jwt令牌的實現

  • 存儲到數據庫中(JdbcTokenStore)

使用數據庫存儲方式之前,我們需要先準備好對應的表。Spring Security OAuth倉庫可以找到相應的腳本:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql。該腳本為HSQL,所以需要根據具體使用的數據庫進行相應的修改,以MySQL為例,並且當前項目中,只需要使用到oauth_access_token和oauth_refresh_token數據表,所以將創建這兩個庫表的語句改為MySQL語句:

CREATE TABLE oauth_access_token (
	token_id VARCHAR ( 256 ),
	token BLOB,
	authentication_id VARCHAR ( 256 ),
	user_name VARCHAR ( 256 ),
	client_id VARCHAR ( 256 ),
	authentication BLOB,
	refresh_token VARCHAR ( 256 ) 
);

CREATE TABLE oauth_refresh_token ( 
token_id VARCHAR ( 256 ), 
token BLOB, authentication BLOB 
);

然後我們需要去配置對應的認證服務器,主要就是修改之前文章中TokenConfigure類中的tokenStore()方法:

// 同時需要注入數據源
@Autowired
private DataSource dataSource;
    
@Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

同時需要添加jdbc的依賴:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

認證服務器中的配置保持本系列第一章的配置不變,此處不再贅述。

其中若有不理解的地方,請參考該系列的第一篇文章

關於數據源的補充:

在我們項目中配置的數據源,可能不一定是使用的官方提供的格式,比如我們自定義的格式,或者使用第三方的數據源,那麼我們如何去配置呢?這裏以mybatis-plus的多數據源為例:

@Configuration
@EnableAuthorizationServer
public class FebsAuthorizationServerConfigure extends AuthorizationServerConfigurerAdapter {

    ......
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;

    @Bean
    public TokenStore tokenStore() {
        DataSource dimplesCloudBase = dynamicRoutingDataSource.getDataSource("dimples_cloud_base");
        return new JdbcTokenStore(febsCloudBase);
    }
    ......
}

然後啟動項目,重新獲取Token進行測試,能正確的獲取到Token:

我們查看數據中,看是否已經存入相關信息到數據庫中:

  • 存儲到redis(RedisTokenStore)

令牌存儲到redis中相比於存儲到數據庫中來說,存儲redis中,首先在性能上有一定的提升,其次,令牌都有有效時間,超過這個時間,令牌將不可再用,而redis的可以給對應的key設置過期時間,完美切合需求,所有令牌存儲到redis中也是一種值得使用的方法。

首先,我們需要在項目中添加redis依賴,同時配置redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

新建配置類RedisConfigure

@Configuration
public class RedisConfigure{

    @Bean
    @ConditionalOnClass(RedisOperations.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key採用 String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的 key也採用 String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式採用 jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的 value序列化方式採用 jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
    
}

配置redis的連接

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        min-idle: 8
        max-idle: 500
        max-active: 2000
        max-wait: 10000
    timeout: 5000

這時我們啟動項目測試,會發現項目將會報錯:

Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig

這是由於我們配置RedisConfigure時,使用到了一個ObjectMapper類,這個類需要我們引入Apache的一個工具包

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

最後,我們需要在認證服務器中配置token的存儲方式,還是同jdbc的配置,在tokenStore()方法中配置,打開我們的TokenConfigure類,然後做如下配置:

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Bean
public TokenStore tokenStore() {
    return new RedisTokenStore(redisConnectionFactory);
}

認證服務器中的配置還是跟之前的一樣,保持不變,啟動項目,獲取Token測試:

我們查看Redis中,看是否已經存入相關信息到數據庫中:

  • 其它

我們打開TokenStore接口的實現類,會發現,還有一種JwkTokenStore,這種實際上就是將我們UUID格式令牌變成可以帶特殊含義的jwt格式的令牌,我們已經在第三篇文章中介紹過了,可以參考前面的文章。

但是我們需要明白一點的是,這種令牌還是存儲在內存中的,後期我們如何將其存儲到redis中是我們研究的方向。

在上面的token存儲到數據庫和存儲到redis中,我們會發現一個問題,那就是我們多次獲取令牌,但是其值是固定不變的,為了解決這個問題,我們可以使用如下方式解決:

@Bean
public TokenStore tokenStore() {
    RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
    // 解決每次生成的 token都一樣的問題
    redisTokenStore.setAuthenticationKeyGenerator(oAuth2Authentication -> UUID.randomUUID().toString());
    return redisTokenStore;
}

使用JWT令牌的實現

在之前的所有使用方法中,我們要麼是將Token存儲在數據庫或者redis中的時候,令牌的格式是UUID的無意義字符串,或者是使用JWT的有意義字符串,但是確是將令牌存儲在內存中,那麼,我們現在想使用JWT令牌的同時,也想將令牌存儲到數據庫或者Redis中。我們該怎麼配置呢?下面我們以Redis存儲為例,進行探索:

首先,我們還是要使用到TokenConfigure中的JWT和Redis配置:

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    //對稱秘鑰,資源服務器使用該秘鑰來驗證
    converter.setSigningKey(SIGNING_KEY);
    return converter;
}

@Bean
public TokenStore tokenStore() {
    return new RedisTokenStore(redisConnectionFactory);
}

接下來,是配置認證服務器,在認證服務器中,跟之前的配置有一點區別,如下:

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Autowired
private TokenStore tokenStore;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints
            // 配置密碼模式管理器
            .authenticationManager(authenticationManager)
            // 配置授權碼模式管理器
            .authorizationCodeServices(authorizationCodeServices())
            // 令牌管理
//                .tokenServices(tokenServices());
            .accessTokenConverter(jwtAccessTokenConverter)
            .tokenStore(tokenStore);

}

啟動項目,進行測試,獲取token

然後使用該令牌請求資源

至此,我們差不多完成了Token令牌的存儲和獲取

源碼傳送門: https://gitee.com/dimples820/dimples-explore

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

「封城新時代」垃圾量爆增! 防疫優先環保擺一邊

摘錄自2020年05月08日TVBS新聞網全球報導

全球進入「封城新時代」,人類減少外出,大大改善了地球環境,不過塑膠污染卻愈來愈嚴重。美國各州,為了防止重複使用的購物袋傳播病毒,開放使用塑膠袋,許多環保措施因此停擺。而宅經濟正夯,網購的貨品包裝,也製造出大量垃圾。

加州塑膠袋全面啟用,當地原先規定購買塑膠袋必須支付10美分,大約台幣3元錢,如今禁令暫停60天,因為衛生安全問題,暫時大於環保問題。就連舊金山,身為全美最先禁用塑膠袋的城市之一,也已經宣布,禁止消費者攜帶自己的環保杯、環保袋等用品到店內。

無論是外出購物,還是網購,似乎怎麼做都會製造塑膠廢棄物,知名零垃圾專家就教大家運用「5R方針」,包括Refuse拒絕垃圾、Reduse減量、Reuse重複使用、Recycle回收,以及Rot把廚餘做成堆肥。零廢棄專家BeaJohnson:「這5R在世界各地都適用,不管你身處什麼情況,包括全球大流行疫情期間也是。」

公害污染
污染治理
國際新聞
美國
塑膠袋
塑膠袋禁用政策
一次性包裝
一次性塑膠袋
廢棄物

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

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

不只陸上生物會感染 科學家擔憂武漢肺炎恐可傳「鯨魚」

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

武漢肺炎(新型冠狀病毒疾病,COVID-19)衝擊全球,不僅導致近400萬人染疫,與人類親近的犬、貓,甚至貂和大型貓科動物都有確診案例;專家則警告,雖目前還未有正式的科學期刊證實,但有研究顯示武漢肺炎可感染的潛在生物範圍恐擴及「鯨魚」。

根據加國媒體《Nunatsiaq news 》報導,目前科學界認為其他動物也會感染武漢肺炎的原因在於,牠們呼吸道細胞表面的蛋白質類似武漢肺炎的受體ACE2,當武漢肺炎病毒進入體內時,會經由ACE2進入細胞並開始複製,進而侵入呼吸道,造成感染。日前加利福尼亞大學戴維斯分校的研究人員創建了一份ACE2受體的動物清單,雖目前仍須更多研究佐證,但專家恩威亞(Martin Nweeia)日前在威爾遜國際學者中心的討論會上強調,北極海域的海洋哺乳動物有感染武漢肺炎的可能性。

雖然人類多透過飛沫感染到武漢肺炎,但根據國際文獻指出,人的糞便、尿液也會殘留病毒,當這些含有病毒的廢水排入水中時,有可能導致某些動物感染,雖然病毒可能被海水中的某些物質破壞,但是北極海有多個鹹淡水入口,以及因全球暖化冰川徑流和夏季冰融化增加了淡水系統,可能影響海洋生物以及病毒的存活。

生活環境
生態保育
物種保育
生物多樣性
國際新聞
武漢肺炎
鯨魚
動物與大環境變遷
公共衛生

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

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

摘錄自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維修中心

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

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

異步函數async await在wpf都做了什麼?

首先我們來看一段控制台應用代碼:

 class Program
 {
     static async Task Main(string[] args)
     {
        System.Console.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
        var result = await ExampleTask(2);
        System.Console.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
        System.Console.WriteLine(result);
        Console.WriteLine("Async Completed");
     }

     private static async Task<string> ExampleTask(int Second)
     {
        await Task.Delay(TimeSpan.FromSeconds(Second));
        return $"It's Async Completed in {Second} seconds";
     }
 }

輸出結果

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:4,Is Thread Pool:True
It's Async Completed in 2 seconds
Async Completed

如果這段代碼在WPF運行,猜猜會輸出啥?

      private async void Async_Click(object sender, RoutedEventArgs e)
      {
          Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
          var result= await ExampleTask(2);
          Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
          Debug.WriteLine(result);
          Debug.WriteLine("Async Completed");   
      }

      private async Task<string> ExampleTask(int Second)
      {
          await Task.Delay(TimeSpan.FromSeconds(Second));
          return $"It's Async Completed in {Second} seconds";
      }

輸出結果:

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:1,Is Thread Pool:False
It's Async Completed in 2 seconds
Async Completed

這時候你肯定是想說,小朋友,你是否有很多問號????,我們接下看下去

一.SynchronizationContext(同步上下文)

首先我們知道async await 異步函數本質是狀態機,我們通過反編譯工具dnspy,看看反編譯的兩段代碼是否有不同之處:

控制台應用:

internal class Program
{
    [DebuggerStepThrough]
	private static Task Main(string[] args)
	{
		Program.<Main>d__0 <Main>d__ = new Program.<Main>d__0();
		<Main>d__.args = args;
		<Main>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
		<Main>d__.<>1__state = -1;
		<Main>d__.<>t__builder.Start<Program.<Main>d__0>(ref <Main>d__);
		return <Main>d__.<>t__builder.Task;
	}
    
	[DebuggerStepThrough]
	private static Task<string> ExampleTask(int Second)
	{
		Program.<ExampleTask>d__1 <ExampleTask>d__ = new Program.<ExampleTask>d__1();
		<ExampleTask>d__.Second = Second;
		<ExampleTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
		<ExampleTask>d__.<>1__state = -1;
		<ExampleTask>d__.<>t__builder.Start<Program.<ExampleTask>d__1>(ref <ExampleTask>d__);
		return <ExampleTask>d__.<>t__builder.Task;
	}

	[DebuggerStepThrough]
	private static void <Main>(string[] args)
	{
	        Program.Main(args).GetAwaiter().GetResult();
	}
}

WPF:

public class MainWindow : Window, IComponentConnector
{

	public MainWindow()
	{
	       this.InitializeComponent();
	}

	[DebuggerStepThrough]
	private void Async_Click(object sender, RoutedEventArgs e)
	{
		MainWindow.<Async_Click>d__1 <Async_Click>d__ = new MainWindow.<Async_Click>d__1();
		<Async_Click>d__.<>4__this = this;
		<Async_Click>d__.sender = sender;
		<Async_Click>d__.e = e;
		<Async_Click>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
		<Async_Click>d__.<>1__state = -1;
		<Async_Click>d__.<>t__builder.Start<MainWindow.<Async_Click>d__1>(ref <Async_Click>d__);
	}

	[DebuggerStepThrough]
	private Task<string> ExampleTask(int Second)
	{
	        MainWindow.<ExampleTask>d__3 <ExampleTask>d__ = new MainWindow.<ExampleTask>d__3();
		<ExampleTask>d__.<>4__this = this;
		<ExampleTask>d__.Second = Second;
		<ExampleTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
		<ExampleTask>d__.<>1__state = -1;
		<ExampleTask>d__.<>t__builder.Start<MainWindow.<ExampleTask>d__3>(ref <ExampleTask>d__);
		return <ExampleTask>d__.<>t__builder.Task;
	}

	[DebuggerNonUserCode]
	[GeneratedCode("PresentationBuildTasks", "4.8.1.0")]
	public void InitializeComponent()
	{
		bool contentLoaded = this._contentLoaded;
		if (!contentLoaded)
		{
		     this._contentLoaded = true;
		     Uri resourceLocater = new Uri("/WpfApp1;component/mainwindow.xaml", UriKind.Relative);
		     Application.LoadComponent(this, resourceLocater);
		}
	}
	private bool _contentLoaded;
}

我們可以看到完全是一致的,沒有任何區別,為什麼編譯器生成的代碼是一致的,卻會產生不一樣的結果,我們看看創建和啟動狀態機代碼部分的實現:

public static AsyncVoidMethodBuilder Create()
{
	SynchronizationContext synchronizationContext = SynchronizationContext.Current;
	if (synchronizationContext != null)
	{
		synchronizationContext.OperationStarted();
	}
	return new AsyncVoidMethodBuilder
	{
		_synchronizationContext = synchronizationContext
	};
}

[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start<[Nullable(0)] TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	AsyncMethodBuilderCore.Start<TStateMachine>(ref stateMachine);
}

[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	if (stateMachine == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
	}
	Thread currentThread = Thread.CurrentThread;
	Thread thread = currentThread;
	ExecutionContext executionContext = currentThread._executionContext;
	ExecutionContext executionContext2 = executionContext;
	SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
	try
	{
	     stateMachine.MoveNext();//狀態機執行代碼
	}
	finally
	{
	     SynchronizationContext synchronizationContext2 = synchronizationContext;
	     Thread thread2 = thread;
	     if (synchronizationContext2 != thread2._synchronizationContext)
	     {
		  thread2._synchronizationContext = synchronizationContext2;
	     }
	     ExecutionContext executionContext3 = executionContext2;
	     ExecutionContext executionContext4 = thread2._executionContext;
	     if (executionContext3 != executionContext4)
	     {
		 ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4);
	     }
	}
}

在這裏總結下:

  • 創建狀態機的Create函數通過SynchronizationContext.Current獲取到當前同步執行上下文
  • 啟動狀態機的Start函數之後通過MoveNext函數執行我們的異步方法
  • 這裏還有一個小提示,不管async函數裏面有沒有await,都會生成狀態機,只是MoveNext函數執行同步方法,因此沒await的情況下避免將函數標記為async,會損耗性能

同樣的這裏貌似沒能獲取到原因,但是有個很關鍵的地方,就是Create函數為啥要獲取當前同步執行上下文,之後我從MSDN找到關於SynchronizationContext
的介紹,有興趣的朋友可以去閱讀以下,以下是各個.NET框架使用的SynchronizationContext:

SynchronizationContext 默認
WindowsFormsSynchronizationContext WindowsForm
DispatcherSynchronizationContext WPF/Silverlight
AspNetSynchronizationContext ASP.NET

我們貌似已經一步步接近真相了,接下來我們來看看DispatcherSynchronizationContext

二.DispatcherSynchronizationContext

首先來看看DispatcherSynchronizationContext類的比較關鍵的幾個函數實現:

public DispatcherSynchronizationContext(Dispatcher dispatcher, DispatcherPriority priority)
{
     if (dispatcher == null)
     {
         throw new ArgumentNullException("dispatcher");
     }
     Dispatcher.ValidatePriority(priority, "priority");
     _dispatcher = dispatcher;
     _priority = priority;
     SetWaitNotificationRequired();
 }

//同步執行
public override void Send(SendOrPostCallback d, object state)
{
     if (BaseCompatibilityPreferences.GetInlineDispatcherSynchronizationContextSend() && _dispatcher.CheckAccess())
     {
         _dispatcher.Invoke(DispatcherPriority.Send, d, state);
     }
     else
     {
          _dispatcher.Invoke(_priority, d, state);
     }
}

//異步執行
public override void Post(SendOrPostCallback d, object state)
{
     _dispatcher.BeginInvoke(_priority, d, state);
}

我們貌似看到了熟悉的東西了,Send函數調用Dispatcher的Invoke函數,Post函數調用Dispatcher的BeginInvoke函數,那麼是否WPF執行異步函數之後會調用這裏的函數嗎?我用dnspy進行了調試:

我通過調試之後發現,當等待執行完整個狀態機的之後,也就是兩秒后跳轉到該Post函數,那麼,我們可以將之前的WPF那段代碼大概可以改寫成如此:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    //async生成狀態機的Create函數。獲取到UI主線程的同步執行上下文
    DispatcherSynchronizationContext synchronizationContext = (DispatcherSynchronizationContext)SynchronizationContext.Current;
    
    //UI主線程執行
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    
    //開始在狀態機的MoveNext執行該異步操作
    var result= await ExampleTask(2);
    
    //等待兩秒,異步執行完成,再在同步上下文異步執行
    synchronizationContext.Post((state) =>
    {
         //模仿_dispatcher.BeginInvoke
         Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
         Debug.WriteLine(result);
         Debug.WriteLine("Async Completed");  
     },"Post");           
 }

輸出結果:

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:1,Is Thread Pool:False
It's Async Completed in 2 seconds
Async Completed

也就是asyn負責生成狀態機和執行狀態機,await將代碼分為兩部分,一部分是異步執行狀態機部分,一部分是異步執行完之後,通過之前拿到的DispatcherSynchronizationContext,再去異步執行接下來的部分。我們可以通過dnspy調試DispatcherSynchronizationContext的 _dispatcher字段的Thread屬性,知道Thread為UI主線程,而同步界面UI控件的時候,也就是通過Dispatcher的BeginInvoke函數去執行同步的

三.Task.ConfigureAwait

Task有個ConfigureAwait方法,是可以設置是否對Task的awaiter的延續任務執行原始上下文,也就是為true時,是以一開始那個UI主線程的DispatcherSynchronizationContext執行Post方法,而為false,則以await那個Task裏面的DispatcherSynchronizationContext執行Post方法,我們來驗證下:

我們將代碼改為以下:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    var result= await ExampleTask(2).ConfigureAwait(false);
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    Debug.WriteLine(result);
    Debug.WriteLine($"Async Completed");
}

輸出:

Thread Id is Thread:1,Is Thread Pool:False
Thread Id is Thread:4,Is Thread Pool:True
It's Async Completed in 2 seconds
Async Completed

結果和控制台輸出的一模一樣,且通過dnspy斷點調試依舊進入到DispatcherSynchronizationContext的Post方法,因此我們也可以證明我們上面的猜想,而且默認ConfigureAwait的參數是為true的,我們還可以將異步結果賦值給UI界面的Text block:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    var result= await ExampleTask(2).ConfigureAwait(false);
    Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}");
    this.txt.Text = result;//修改部分
    Debug.WriteLine($"Async Completed");
}

拋出異常:

調用線程無法訪問此對象,因為另一個線程擁有該對象

補充
推薦林大佬的一篇文章,也講的也簡潔透徹C# dotnet 自己實現一個線程同步上下文

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

ASP.NET Core通過Nacos SDK讀取阿里雲ACM

背景

前段時間,cranelee 在Github上給老黃提了個issues, 問到了如何用Nacos的SDK訪問阿里雲ACM。

https://github.com/catcherwong/nacos-sdk-csharp/issues/13

剛看到這個issues的時候,老黃也是覺得一臉懵逼,好像這兩者沒有什麼必然聯繫,打開ACM的文檔一看,就知道為什麼了。

原來Java和Go的已經是可以用nacos的SDK來訪問的了。那就說明兩者是兼容的。

這段時間抽空看了一下,把這個功能基本實現了。

下面就簡單介紹一下。

簡單看看ACM

開通ACM之後,可以看到類似這樣的界面。其實和Nacos控制台的配置部分差不遠。

要使用這個的話,需要幾個東西,一個是ACM上面的命名空間,一個是AccessKey ID,一個是AccessKey Secret。

其中的AK/SK可以在命名空間詳情裏面獲取。

然後就是添加配置了。

三張圖,看個大概就好了,下面來具體看看在.NET Core中怎麼使用。

如何使用

安裝最新預覽版的SDK

<ItemGroup>
    <PackageReference Include="nacos-sdk-csharp-unofficial.Extensions.Configuration" Version="0.2.7-alpha7" />
</ItemGroup>

注:目前還沒有發布正式版,不過不影響正常使用了。

修改Program

public class Program
{
    public static void Main(string[] args)
    {
        // 處理編碼問題
        System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);

        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, builder) =>
            {
                // 這兩行代碼就是關鍵
                var c = builder.Build();
                builder.AddNacosConfiguration(c.GetSection("NacosConfig"));
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

調整appsettings.json

{
  "NacosConfig": {
    "Optional": false,
    "DataId": "msconfigapp",
    "Group": "",
    "Tenant": "<換成您的命名空間>",
    "ServerAddresses": [],
    "AccessKey": "<換成您的AK>",
    "SecretKey": "<換成您的SK>",
    "EndPoint": "acm.aliyun.com"
  }
}

注: 由於老黃開通個人開通的,沒有內網服務器,所以用的是公網的EndPoint,這個需要根據情況自行調整。

實體映射(非必須)

public class AppSettings
{
    public string Str { get; set; }

    public int Num { get; set; }

    public List<int> Arr { get; set; }

    public SubObj SubObj { get; set; }
}

public class SubObj
{
    public string a { get; set; }
}

為了方便和配置一一對應,可以建立實體,做一個映射。

加了這個的,需要在Startup上面配置一下。

public void ConfigureServices(IServiceCollection services)
{   
    // others ...
    
    services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
}

讀取配置

這裏用控制器做為示例

[ApiController]
[Route("api/[controller]")]
public class ConfigController : ControllerBase
{
    private readonly ILogger<ConfigController> _logger;
    private readonly IConfiguration _configuration;
    private readonly AppSettings _settings;
    private readonly AppSettings _sSettings;
    private readonly AppSettings _mSettings;

    public ConfigController(
        ILogger<ConfigController> logger,
        IConfiguration configuration,
        IOptions<AppSettings> options,
        IOptionsSnapshot<AppSettings> sOptions,
        IOptionsMonitor<AppSettings> _mOptions
        )
    {
        _logger = logger;
        _configuration = configuration;
        _settings = options.Value;
        _sSettings = sOptions.Value;
        _mSettings = _mOptions.CurrentValue;
    }

    [HttpGet]
    public string Get()
    {
        string id = Guid.NewGuid().ToString("N");

        _logger.LogInformation($"============== begin {id} =====================");

        var conn = _configuration.GetConnectionString("Default");
        _logger.LogInformation($"{id} conn = {conn}");

        var version = _configuration["version"];
        _logger.LogInformation($"{id} version = {version}");

        var str1 = Newtonsoft.Json.JsonConvert.SerializeObject(_settings);
        _logger.LogInformation($"{id} IOptions = {str1}");

        var str2 = Newtonsoft.Json.JsonConvert.SerializeObject(_sSettings);
        _logger.LogInformation($"{id} IOptionsSnapshot = {str2}");

        var str3 = Newtonsoft.Json.JsonConvert.SerializeObject(_mSettings);
        _logger.LogInformation($"{id} IOptionsMonitor = {str3}");

        _logger.LogInformation($"===============================================");
        _logger.LogInformation($"===============================================");
        _logger.LogInformation($"===============================================");

        return "ok";
    }
}

附上一張操作動圖

在ACM上修改之後,程序是可以馬上讀取到的。

下面是本文的示例代碼。

https://github.com/catcherwong-archive/2020/tree/master/06/NacosACMDemo

小結

Nacos和ACM的操作基本都是一致的,比較不一樣的地方是,從直連Nacos變成要先去地址服務拿到Nacos的地址后再操作。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準