對不起,我錯了,這代碼不好寫

来源:https://www.cnblogs.com/zhuochongdashi/archive/2022/06/01/16335594.html
-Advertisement-
Play Games

hello,大家好呀,我是小樓。 前幾天不是寫了這篇文章《發現一個開源項目優化點,點進來就是你的了》嘛。 文章介紹了Sentinl的自適應緩存時間戳演算法,從原理到實現都手把手解讀了,而且還發現Sentinel-Go還未實現這個自適應演算法,於是我就覺得,這簡單啊,把Java代碼翻譯成Go不就可以混個P ...


hello,大家好呀,我是小樓。

前幾天不是寫了這篇文章《發現一個開源項目優化點,點進來就是你的了》嘛。

文章介紹了Sentinl的自適應緩存時間戳演算法,從原理到實現都手把手解讀了,而且還發現Sentinel-Go還未實現這個自適應演算法,於是我就覺得,這簡單啊,把Java代碼翻譯成Go不就可以混個PR?

甚至在文章初稿中把這個描述為:「有手就可以」,感覺不太妥當,後來被我刪掉了。

過了幾天,我想去看看有沒有人看了我的文章真的去提了個PR,發現仍然是沒有,心想,可能是大家太忙(懶)了吧。

於是準備自己來實現一遍,周末我拿出電腦試著寫一下這段代碼,結果被當頭一棒敲醒,原來這代碼不好寫啊。

image

如何實現

先簡單介紹一下我當時是如何實現的。

首先,定義了系統的四種狀態:

const (
	UNINITIALIZED = iota
	IDLE
	PREPARE
	RUNNING
)

這裡為了讓代碼更加貼近Go的習慣,用了iota

用了4種狀態,第一個狀態UNINITIALIZED是Java版里沒有的,因為Java在系統初始化時預設就啟動了定時緩存時間戳線程。

但Go版本不是這樣的,它有個開關,當開關開啟時,會調用StartTimeTicker來啟動緩存時間戳的協程,所以當沒有初始化時是需要直接返回系統時間戳,所以這裡多了一個UNINITIALIZED狀態。

然後我們需要能夠統計QPS的方法,這塊直接抄Java的實現,由於不是重點,但又怕你不理解,所以直接貼一點代碼,不想看可以往下劃。

定義我們需要的BucketWrap:

type statistic struct {
	reads  uint64
	writes uint64
}

func (s *statistic) NewEmptyBucket() interface{} {
	return statistic{
		reads:  0,
		writes: 0,
	}
}

func (s *statistic) ResetBucketTo(bucket *base.BucketWrap, startTime uint64) *base.BucketWrap {
	atomic.StoreUint64(&bucket.BucketStart, startTime)
	bucket.Value.Store(statistic{
		reads:  0,
		writes: 0,
	})
	return bucket
}

獲取當前的Bucket:

func currentCounter(now uint64) (*statistic, error) {
	if statistics == nil {
		return nil, fmt.Errorf("statistics is nil")
	}

	bk, err := statistics.CurrentBucketOfTime(now, bucketGenerator)
	if err != nil {
		return nil, err
	}
	if bk == nil {
		return nil, fmt.Errorf("current bucket is nil")
	}

	v := bk.Value.Load()
	if v == nil {
		return nil, fmt.Errorf("current bucket value is nil")
	}
	counter, ok := v.(*statistic)
	if !ok {
		return nil, fmt.Errorf("bucket fail to do type assert, expect: *statistic, in fact: %s", reflect.TypeOf(v).Name())
	}

	return counter, nil
}

獲取當前的QPS:

func currentQps(now uint64) (uint64, uint64) {
	if statistics == nil {
		return 0, 0
	}

	list := statistics.ValuesConditional(now, func(ws uint64) bool {
		return ws <= now && now < ws+uint64(bucketLengthInMs)
	})

	var reads, writes, cnt uint64
	for _, w := range list {
		if w == nil {
			continue
		}

		v := w.Value.Load()
		if v == nil {
			continue
		}

		s, ok := v.(*statistic)
		if !ok {
			continue
		}

		cnt++
		reads += s.reads
		writes += s.writes
	}

	if cnt < 1 {
		return 0, 0
	}

	return reads / cnt, writes / cnt
}

當我們有了這些準備後,來寫核心的check邏輯:

func check() {
	now := CurrentTimeMillsWithTicker(true)
	if now-lastCheck < checkInterval {
		return
	}

	lastCheck = now
	qps, tps := currentQps(now)
	if state == IDLE && qps > hitsUpperBoundary {
		logging.Warn("[time_ticker check] switches to PREPARE for better performance", "reads", qps, "writes", tps)
		state = PREPARE
	} else if state == RUNNING && qps < hitsLowerBoundary {
		logging.Warn("[time_ticker check] switches to IDLE due to not enough load", "reads", qps, "writes", tps)
		state = IDLE
	}
}

最後是調用check的地方:

func StartTimeTicker() {
	var err error
	statistics, err = base.NewLeapArray(sampleCount, intervalInMs, bucketGenerator)
	if err != nil {
		logging.Warn("[time_ticker StartTimeTicker] new leap array failed", "error", err.Error())
	}

	atomic.StoreUint64(&nowInMs, uint64(time.Now().UnixNano())/unixTimeUnitOffset)
	state = IDLE
	go func() {
		for {
			check()
			if state == RUNNING {
				now := uint64(time.Now().UnixNano()) / unixTimeUnitOffset
				atomic.StoreUint64(&nowInMs, now)
				counter, err := currentCounter(now)
				if err != nil && counter != nil {
					atomic.AddUint64(&counter.writes, 1)
				}
				time.Sleep(time.Millisecond)
				continue
			}
			if state == IDLE {
				time.Sleep(300 * time.Millisecond)
				continue
			}
			if state == PREPARE {
				now := uint64(time.Now().UnixNano()) / unixTimeUnitOffset
				atomic.StoreUint64(&nowInMs, now)
				state = RUNNING
				continue
			}
		}
	}()
}

自此,我們就實(抄)現(完)了自適應的緩存時間戳演算法。

測試一下

先編譯一下,咚,報錯了:import cycle not allowed!

image

啥意思呢?迴圈依賴了!

我們的時間戳獲取方法在包util中,然後我們使用的統計QPS相關的實現在base包中,util包依賴了base包,這個很好理解,反之,base包也依賴了util包,base包主要也使用了CurrentTimeMillis方法來獲取當前時間戳,我這裡截個圖,但不止這些,有好幾個地方都使用到了:

image

但我寫代碼時是特地繞開了迴圈依賴,也就是util中調用base包中的方法是不會反向依賴回來形成環的,為此還單獨寫了個方法:

image

使用新方法,就不會形成依賴環。但實際上編譯還是通過不了,這是因為Go在編譯時就直接禁止了迴圈依賴。

那我就好奇了啊,Java是怎麼實現的?

這是com.alibaba.csp.sentinel.util

image

這是com.alibaba.csp.sentinel.slots.statistic.base

image

Java也出現了迴圈依賴,但它沒事!

這瞬間勾起了我的興趣,如果我讓它運行時形成依賴環,會怎麼樣呢?

簡單做個測試,搞兩個包,互相調用,比如pk1pk2code方法都調用對方:

package org.newboo.pk1;

import org.newboo.pk2.Test2;

public class Test1 {
    public static int code() {
        return Test2.code();
    }

    public static void main(String[] args) {
        System.out.println(code());
    }
}

編譯可以通過,但運行報錯棧溢出了:

Exception in thread "main" java.lang.StackOverflowError
	at org.newboo.pk1.Test1.code(Test1.java:7)
	at org.newboo.pk2.Test2.code(Test2.java:7)
	...

這麼看來是Go編譯器做了校驗,強制不允許迴圈依賴。

說到這裡,其實Java里也有迴圈依賴校驗,比如:Maven不允許迴圈依賴,比如我在sentinel-core模塊中依賴sentinel-benchmark,編譯時就直接報錯。

image

再比如SpringBoot2.6.x預設禁用迴圈依賴,如果想用,還得手動打開才行。

Java中強制禁止的只有maven,語言層面、框架層面基本都沒有趕盡殺絕,但Go卻在語言層面強制不讓使用。

這讓我想起了之前在寫Go代碼時,Go的鎖不允許重入,經常寫出死鎖代碼。這擱Java上一點問題都沒有,當時我就沒想通,為啥Go不支持鎖的重入。

現在看來可能的原因:一是Go的設計者有代碼潔癖,想強制約束大家都有良好的代碼風格;二是由於Go有迴圈依賴的強制檢測,導致鎖重入的概率變小。

但這終究是理想狀態,往往在實施起來的時候令人痛苦。

反觀Java,一開始沒有強制禁用迴圈依賴,導致後面基本不可避免地寫出迴圈依賴的代碼,SpringBoot認為這是不好的,但又不能強制,只能預設禁止,但如果你真的需要,也還是可以打開的。

但話又說回來,迴圈依賴真的「醜陋」嗎?我看不一定,仁者見仁,智者見智。

如何解決

問題是這麼個問題,可能大家都有不同的觀點,或是吐槽Go,或是批判Java,這都不是重點,重點是我們還得在Go的規則下解決問題。

如何解決Go的迴圈依賴問題呢?稍微查了一下資料,大概有這麼幾種方法:

方法一

將兩個包合成一個,這是最簡單的方法,但這裡肯定不行,合成一個這個PR鐵定過不了。

方法二

抽取公共底層方法,雙方都依賴這個底層方法。比如這裡,我們把底層方法抽出來作為common,util和base同時依賴它,這樣util和base就不互相依賴了。

---- util
---- ---- common
---- base
---- ---- common

這個方法也是最常見,最正規的方法。

但在這裡,似乎也不好操作。因為獲取時間戳這個方法已經非常底層了,沒辦法抽出一個和統計QPS共用的方法,反正我是沒能想出來,如果有讀者朋友可以做到,歡迎私聊我,真心求教。

花了很多時間,還是沒能搞定。當時的感覺是,這下翻車了,這題可沒那麼簡單啊!

方法三

這個方法比較難想到,我也是在前兩個方法怎麼都搞不定的情況下咨詢了組裡的Go大佬才知道。

仔細看獲取時間戳的代碼:

// Returns the current Unix timestamp in milliseconds.
func CurrentTimeMillis() uint64 {
	return CurrentClock().CurrentTimeMillis()
}

這裡的CurrentClock()是什麼?其實是返回了一個Clock介面的實現

type Clock interface {
	Now() time.Time
	Sleep(d time.Duration)
	CurrentTimeMillis() uint64
	CurrentTimeNano() uint64
}

作者這麼寫的目的是為了在測試的時候,可以靈活地替換真實實現

image

實際使用時RealClock,也就是調用了我們正在調優的時間戳獲取;MockClock則是測試時使用的。

這個實現是什麼時候註入的呢?

func init() {
	realClock := NewRealClock()
	currentClock = new(atomic.Value)
	SetClock(realClock)

	realTickerCreator := NewRealTickerCreator()
	currentTickerCreator = new(atomic.Value)
	SetTickerCreator(realTickerCreator)
}

在util初始化時,就寫死註入了realClock。

這麼一細說,是不是對迴圈依賴的解決有點眉目了?

我們的realClock實際上依賴了base,但這個realClock可以放在util包外,util包內只留一個介面。

image

註入真實的realClock的地方也不能放在util的初始化中,也得放在util包外(比如Sentinel初始化的地方),這樣一來,util就不再直接依賴base了。

image

這樣一改造,編譯就能通過了,當然這代碼只是個示意,還需要精雕細琢。

最後

我們發現就算給你現成的代碼,抄起來也是比較難的,有點類似「腦子會了,但手不會」的尷尬境地。

同時每個編程語言都有自己的風格,也就是我們通常說的,Go代碼要寫得更「Go」一點,所以語言不止是一個工具這麼簡單,它的背後也存在著自己的思考方式。

本文其實是從一個案例分享瞭如何解決Go的迴圈依賴問題,以及一些和Java對比的思考,更偏向代碼工程。

如果你覺得還不過癮,也可以看看這篇文章,也是關於代碼工程的:

看完,記得點個關註在看哦,這樣我才有動力持續輸出優質技術文章 ~ 我們下期再見吧。


  • 搜索關註微信公眾號"捉蟲大師",後端技術分享,架構設計、性能優化、源碼閱讀、問題排查、踩坑實踐。

image


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 隨著短視頻時代的到來,音視頻剪輯應用不斷增加,市場競爭愈發激烈,如何為用戶提供差異化剪輯功能和優質的音頻處理體驗,已成為行業新的挑戰。 “音頻音樂剪輯“是武漢網冪科技開發的一款手機音頻剪輯應用,支持音樂剪輯、音頻提取、伴奏人聲提取、格式轉換、手機鈴聲製作、拼接、變速、混音、錄音、降噪等功能。為了給用 ...
  • 該項目是學習Nest.js框架所得,前端基於Vue.js + Vuex + VueRouter + ElementUI + SCSS,後端基於Node.js + TypeScript + Nest.js + MySQL + TypeORM。 ...
  • 思路:通過註冊表註冊自定義URL協議執行bat腳本,將文件路徑作為參數傳入 環境:win10 前置問題與條件 問題1:可以從瀏覽器直接打開可執行文件嗎? 答:不能。其實可以通過 ActiveXObject 實現軟體直接打開,但是它是不安全的,並且現在被大多數現代瀏覽器禁止,只能在 ie 使用。而通過 ...
  • 簡單介紹一下,我的 web 前端開發技術選擇。我更偏向於使用 jQuery 及其插件、CSS3、HTML5。不喜歡 mvvm 之類的技術。 ...
  • 近幾年國內外聲名鵲起的Rust編程語言,聲名遠播,影響力巨大,到底是什麼讓它如此強大?本文適合作為一篇初級入門的文章。本文的優勢是通過一個常見的例子作為線索,引出Rust的一些重要理念或者說特性,通過這些特性深刻體會Rust的魅力。 ...
  • synchronized 是java中常見的保證多線程訪問共用資源時的安全的一個關鍵字。很多人在講到synchronized 時都說synchronized 是一把重量級的鎖,那麼synchronized 真的很重麽? synchronized 在jdk 1.6以前(不包括1.6)的確是一把很重的鎖 ...
  • Spring Ioc源碼分析系列--@Autowired註解的實現原理 前言 前面系列文章分析了一把Spring Ioc的源碼,是不是雲里霧裡,感覺並沒有跟實際開發搭上半毛錢關係?看了一遍下來,對我的提升在哪?意義何在?如果沒點收穫,那浪費時間來看這個作甚,玩玩游戲不香? 這段玩笑話可不是真的玩笑, ...
  • 利用 Java語言使用 SSM 框架編寫完成書城項目後本地部署步驟操作指引 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...