真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

對(duì)不起,我錯(cuò)了,這代碼不好寫(xiě)

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

汕尾網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)建站從2013年開(kāi)始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站。

前幾天不是寫(xiě)了這篇文章《發(fā)現(xiàn)一個(gè)開(kāi)源項(xiàng)目?jī)?yōu)化點(diǎn),點(diǎn)進(jìn)來(lái)就是你的了》嘛。

文章介紹了Sentinl的自適應(yīng)緩存時(shí)間戳算法,從原理到實(shí)現(xiàn)都手把手解讀了,而且還發(fā)現(xiàn)Sentinel-Go還未實(shí)現(xiàn)這個(gè)自適應(yīng)算法,于是我就覺(jué)得,這簡(jiǎn)單啊,把Java代碼翻譯成Go不就可以混個(gè)PR?

甚至在文章初稿中把這個(gè)描述為:「有手就可以」,感覺(jué)不太妥當(dāng),后來(lái)被我刪掉了。

過(guò)了幾天,我想去看看有沒(méi)有人看了我的文章真的去提了個(gè)PR,發(fā)現(xiàn)仍然是沒(méi)有,心想,可能是大家太忙(懶)了吧。

于是準(zhǔn)備自己來(lái)實(shí)現(xiàn)一遍,周末我拿出電腦試著寫(xiě)一下這段代碼,結(jié)果被當(dāng)頭一棒敲醒,原來(lái)這代碼不好寫(xiě)啊。

如何實(shí)現(xiàn)

先簡(jiǎn)單介紹一下我當(dāng)時(shí)是如何實(shí)現(xiàn)的。

首先,定義了系統(tǒng)的四種狀態(tài):

const (
	UNINITIALIZED = iota
	IDLE
	PREPARE
	RUNNING
)

這里為了讓代碼更加貼近Go的習(xí)慣,用了iota。

用了4種狀態(tài),第一個(gè)狀態(tài)UNINITIALIZED是Java版里沒(méi)有的,因?yàn)镴ava在系統(tǒng)初始化時(shí)默認(rèn)就啟動(dòng)了定時(shí)緩存時(shí)間戳線程。

但Go版本不是這樣的,它有個(gè)開(kāi)關(guān),當(dāng)開(kāi)關(guān)開(kāi)啟時(shí),會(huì)調(diào)用StartTimeTicker來(lái)啟動(dòng)緩存時(shí)間戳的協(xié)程,所以當(dāng)沒(méi)有初始化時(shí)是需要直接返回系統(tǒng)時(shí)間戳,所以這里多了一個(gè)UNINITIALIZED狀態(tài)。

然后我們需要能夠統(tǒng)計(jì)QPS的方法,這塊直接抄Java的實(shí)現(xiàn),由于不是重點(diǎn),但又怕你不理解,所以直接貼一點(diǎn)代碼,不想看可以往下劃。

定義我們需要的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
}

獲取當(dāng)前的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
}

獲取當(dāng)前的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
}

當(dāng)我們有了這些準(zhǔn)備后,來(lái)寫(xiě)核心的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
	}
}

最后是調(diào)用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
			}
		}
	}()
}

自此,我們就實(shí)(抄)現(xiàn)(完)了自適應(yīng)的緩存時(shí)間戳算法。

測(cè)試一下

先編譯一下,咚,報(bào)錯(cuò)了:import cycle not allowed!

啥意思呢?循環(huán)依賴了!

我們的時(shí)間戳獲取方法在包util中,然后我們使用的統(tǒng)計(jì)QPS相關(guān)的實(shí)現(xiàn)在base包中,util包依賴了base包,這個(gè)很好理解,反之,base包也依賴了util包,base包主要也使用了CurrentTimeMillis方法來(lái)獲取當(dāng)前時(shí)間戳,我這里截個(gè)圖,但不止這些,有好幾個(gè)地方都使用到了:

但我寫(xiě)代碼時(shí)是特地繞開(kāi)了循環(huán)依賴,也就是util中調(diào)用base包中的方法是不會(huì)反向依賴回來(lái)形成環(huán)的,為此還單獨(dú)寫(xiě)了個(gè)方法:

使用新方法,就不會(huì)形成依賴環(huán)。但實(shí)際上編譯還是通過(guò)不了,這是因?yàn)镚o在編譯時(shí)就直接禁止了循環(huán)依賴。

那我就好奇了啊,Java是怎么實(shí)現(xiàn)的?

這是com.alibaba.csp.sentinel.util

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

Java也出現(xiàn)了循環(huán)依賴,但它沒(méi)事!

這瞬間勾起了我的興趣,如果我讓它運(yùn)行時(shí)形成依賴環(huán),會(huì)怎么樣呢?

簡(jiǎn)單做個(gè)測(cè)試,搞兩個(gè)包,互相調(diào)用,比如pk1pk2code方法都調(diào)用對(duì)方:

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());
    }
}

編譯可以通過(guò),但運(yùn)行報(bào)錯(cuò)棧溢出了:

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)
	...

這么看來(lái)是Go編譯器做了校驗(yàn),強(qiáng)制不允許循環(huán)依賴。

說(shuō)到這里,其實(shí)Java里也有循環(huán)依賴校驗(yàn),比如:Maven不允許循環(huán)依賴,比如我在sentinel-core模塊中依賴sentinel-benchmark,編譯時(shí)就直接報(bào)錯(cuò)。

再比如SpringBoot2.6.x默認(rèn)禁用循環(huán)依賴,如果想用,還得手動(dòng)打開(kāi)才行。

Java中強(qiáng)制禁止的只有maven,語(yǔ)言層面、框架層面基本都沒(méi)有趕盡殺絕,但Go卻在語(yǔ)言層面強(qiáng)制不讓使用。

這讓我想起了之前在寫(xiě)Go代碼時(shí),Go的鎖不允許重入,經(jīng)常寫(xiě)出死鎖代碼。這擱Java上一點(diǎn)問(wèn)題都沒(méi)有,當(dāng)時(shí)我就沒(méi)想通,為啥Go不支持鎖的重入。

現(xiàn)在看來(lái)可能的原因:一是Go的設(shè)計(jì)者有代碼潔癖,想強(qiáng)制約束大家都有良好的代碼風(fēng)格;二是由于Go有循環(huán)依賴的強(qiáng)制檢測(cè),導(dǎo)致鎖重入的概率變小。

但這終究是理想狀態(tài),往往在實(shí)施起來(lái)的時(shí)候令人痛苦。

反觀Java,一開(kāi)始沒(méi)有強(qiáng)制禁用循環(huán)依賴,導(dǎo)致后面基本不可避免地寫(xiě)出循環(huán)依賴的代碼,SpringBoot認(rèn)為這是不好的,但又不能強(qiáng)制,只能默認(rèn)禁止,但如果你真的需要,也還是可以打開(kāi)的。

但話又說(shuō)回來(lái),循環(huán)依賴真的「丑陋」嗎?我看不一定,仁者見(jiàn)仁,智者見(jiàn)智。

如何解決

問(wèn)題是這么個(gè)問(wèn)題,可能大家都有不同的觀點(diǎn),或是吐槽Go,或是批判Java,這都不是重點(diǎn),重點(diǎn)是我們還得在Go的規(guī)則下解決問(wèn)題。

如何解決Go的循環(huán)依賴問(wèn)題呢?稍微查了一下資料,大概有這么幾種方法:

方法一

將兩個(gè)包合成一個(gè),這是最簡(jiǎn)單的方法,但這里肯定不行,合成一個(gè)這個(gè)PR鐵定過(guò)不了。

方法二

抽取公共底層方法,雙方都依賴這個(gè)底層方法。比如這里,我們把底層方法抽出來(lái)作為common,util和base同時(shí)依賴它,這樣util和base就不互相依賴了。

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

這個(gè)方法也是最常見(jiàn),最正規(guī)的方法。

但在這里,似乎也不好操作。因?yàn)楂@取時(shí)間戳這個(gè)方法已經(jīng)非常底層了,沒(méi)辦法抽出一個(gè)和統(tǒng)計(jì)QPS共用的方法,反正我是沒(méi)能想出來(lái),如果有讀者朋友可以做到,歡迎私聊我,真心求教。

花了很多時(shí)間,還是沒(méi)能搞定。當(dāng)時(shí)的感覺(jué)是,這下翻車了,這題可沒(méi)那么簡(jiǎn)單??!

方法三

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

仔細(xì)看獲取時(shí)間戳的代碼:

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

這里的CurrentClock()是什么?其實(shí)是返回了一個(gè)Clock接口的實(shí)現(xiàn)

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

作者這么寫(xiě)的目的是為了在測(cè)試的時(shí)候,可以靈活地替換真實(shí)實(shí)現(xiàn)

實(shí)際使用時(shí)RealClock,也就是調(diào)用了我們正在調(diào)優(yōu)的時(shí)間戳獲??;MockClock則是測(cè)試時(shí)使用的。

這個(gè)實(shí)現(xiàn)是什么時(shí)候注入的呢?

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

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

在util初始化時(shí),就寫(xiě)死注入了realClock。

這么一細(xì)說(shuō),是不是對(duì)循環(huán)依賴的解決有點(diǎn)眉目了?

我們的realClock實(shí)際上依賴了base,但這個(gè)realClock可以放在util包外,util包內(nèi)只留一個(gè)接口。

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

這樣一改造,編譯就能通過(guò)了,當(dāng)然這代碼只是個(gè)示意,還需要精雕細(xì)琢。

最后

我們發(fā)現(xiàn)就算給你現(xiàn)成的代碼,抄起來(lái)也是比較難的,有點(diǎn)類似「腦子會(huì)了,但手不會(huì)」的尷尬境地。

同時(shí)每個(gè)編程語(yǔ)言都有自己的風(fēng)格,也就是我們通常說(shuō)的,Go代碼要寫(xiě)得更「Go」一點(diǎn),所以語(yǔ)言不止是一個(gè)工具這么簡(jiǎn)單,它的背后也存在著自己的思考方式。

本文其實(shí)是從一個(gè)案例分享了如何解決Go的循環(huán)依賴問(wèn)題,以及一些和Java對(duì)比的思考,更偏向代碼工程。

如果你覺(jué)得還不過(guò)癮,也可以看看這篇文章,也是關(guān)于代碼工程的:

  • 《慘,給Go提的代碼被批麻了》

看完,記得點(diǎn)個(gè)關(guān)注、在看哦,這樣我才有動(dòng)力持續(xù)輸出優(yōu)質(zhì)技術(shù)文章 ~ 我們下期再見(jiàn)吧。


  • 搜索關(guān)注微信公眾號(hào)"捉蟲(chóng)大師",后端技術(shù)分享,架構(gòu)設(shè)計(jì)、性能優(yōu)化、源碼閱讀、問(wèn)題排查、踩坑實(shí)踐。


網(wǎng)頁(yè)名稱:對(duì)不起,我錯(cuò)了,這代碼不好寫(xiě)
本文地址:http://weahome.cn/article/dsoichj.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部