Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >对不起,我错了,这代码不好写

对不起,我错了,这代码不好写

原创
作者头像
龟仙老人
发布于 2022-06-01 12:51:21
发布于 2022-06-01 12:51:21
3280
举报
文章被收录于专栏:捉虫大师捉虫大师

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

前几天不是写了这篇文章《发现一个开源项目优化点,点进来就是你的了》嘛。

文章介绍了Sentinl的自适应缓存时间戳算法,从原理到实现都手把手解读了,而且还发现Sentinel-Go还未实现这个自适应算法,于是我就觉得,这简单啊,把Java代码翻译成Go不就可以混个PR?

甚至在文章初稿中把这个描述为:「有手就可以」,感觉不太妥当,后来被我删掉了。

过了几天,我想去看看有没有人看了我的文章真的去提了个PR,发现仍然是没有,心想,可能是大家太忙(懒)了吧。

于是准备自己来实现一遍,周末我拿出电脑试着写一下这段代码,结果被当头一棒敲醒,原来这代码不好写啊。

如何实现

先简单介绍一下我当时是如何实现的。

首先,定义了系统的四种状态:

代码语言:go
AI代码解释
复制
const (
	UNINITIALIZED = iota
	IDLE
	PREPARE
	RUNNING
)

这里为了让代码更加贴近Go的习惯,用了iota

用了4种状态,第一个状态UNINITIALIZED是Java版里没有的,因为Java在系统初始化时默认就启动了定时缓存时间戳线程。

但Go版本不是这样的,它有个开关,当开关开启时,会调用StartTimeTicker来启动缓存时间戳的协程,所以当没有初始化时是需要直接返回系统时间戳,所以这里多了一个UNINITIALIZED状态。

然后我们需要能够统计QPS的方法,这块直接抄Java的实现,由于不是重点,但又怕你不理解,所以直接贴一点代码,不想看可以往下划。

定义我们需要的BucketWrap:

代码语言:go
AI代码解释
复制
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:

代码语言:go
AI代码解释
复制
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:

代码语言:go
AI代码解释
复制
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逻辑:

代码语言:go
AI代码解释
复制
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的地方:

代码语言:go
AI代码解释
复制
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!

p2.png
p2.png

啥意思呢?循环依赖了!

我们的时间戳获取方法在包util中,然后我们使用的统计QPS相关的实现在base包中,util包依赖了base包,这个很好理解,反之,base包也依赖了util包,base包主要也使用了CurrentTimeMillis方法来获取当前时间戳,我这里截个图,但不止这些,有好几个地方都使用到了:

p3.png
p3.png

但我写代码时是特地绕开了循环依赖,也就是util中调用base包中的方法是不会反向依赖回来形成环的,为此还单独写了个方法:

p4.png
p4.png

使用新方法,就不会形成依赖环。但实际上编译还是通过不了,这是因为Go在编译时就直接禁止了循环依赖。

那我就好奇了啊,Java是怎么实现的?

这是com.alibaba.csp.sentinel.util

p5.png
p5.png

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

p6.png
p6.png

Java也出现了循环依赖,但它没事!

这瞬间勾起了我的兴趣,如果我让它运行时形成依赖环,会怎么样呢?

简单做个测试,搞两个包,互相调用,比如pk1pk2code方法都调用对方:

代码语言:java
AI代码解释
复制
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());
    }
}

编译可以通过,但运行报错栈溢出了:

代码语言:java
AI代码解释
复制
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,编译时就直接报错。

p7.png
p7.png

再比如SpringBoot2.6.x默认禁用循环依赖,如果想用,还得手动打开才行。

Java中强制禁止的只有maven,语言层面、框架层面基本都没有赶尽杀绝,但Go却在语言层面强制不让使用。

这让我想起了之前在写Go代码时,Go的锁不允许重入,经常写出死锁代码。这搁Java上一点问题都没有,当时我就没想通,为啥Go不支持锁的重入。

现在看来可能的原因:一是Go的设计者有代码洁癖,想强制约束大家都有良好的代码风格;二是由于Go有循环依赖的强制检测,导致锁重入的概率变小。

但这终究是理想状态,往往在实施起来的时候令人痛苦。

反观Java,一开始没有强制禁用循环依赖,导致后面基本不可避免地写出循环依赖的代码,SpringBoot认为这是不好的,但又不能强制,只能默认禁止,但如果你真的需要,也还是可以打开的。

但话又说回来,循环依赖真的「丑陋」吗?我看不一定,仁者见仁,智者见智。

如何解决

问题是这么个问题,可能大家都有不同的观点,或是吐槽Go,或是批判Java,这都不是重点,重点是我们还得在Go的规则下解决问题。

如何解决Go的循环依赖问题呢?稍微查了一下资料,大概有这么几种方法:

方法一

将两个包合成一个,这是最简单的方法,但这里肯定不行,合成一个这个PR铁定过不了。

方法二

抽取公共底层方法,双方都依赖这个底层方法。比如这里,我们把底层方法抽出来作为common,util和base同时依赖它,这样util和base就不互相依赖了。

代码语言:txt
AI代码解释
复制
---- util
---- ---- common
---- base
---- ---- common

这个方法也是最常见,最正规的方法。

但在这里,似乎也不好操作。因为获取时间戳这个方法已经非常底层了,没办法抽出一个和统计QPS共用的方法,反正我是没能想出来,如果有读者朋友可以做到,欢迎私聊我,真心求教。

花了很多时间,还是没能搞定。当时的感觉是,这下翻车了,这题可没那么简单啊!

方法三

这个方法比较难想到,我也是在前两个方法怎么都搞不定的情况下咨询了组里的Go大佬才知道。

仔细看获取时间戳的代码:

代码语言:go
AI代码解释
复制
// Returns the current Unix timestamp in milliseconds.
func CurrentTimeMillis() uint64 {
	return CurrentClock().CurrentTimeMillis()
}

这里的CurrentClock()是什么?其实是返回了一个Clock接口的实现

代码语言:go
AI代码解释
复制
type Clock interface {
	Now() time.Time
	Sleep(d time.Duration)
	CurrentTimeMillis() uint64
	CurrentTimeNano() uint64
}

作者这么写的目的是为了在测试的时候,可以灵活地替换真实实现

p8.png
p8.png

实际使用时RealClock,也就是调用了我们正在调优的时间戳获取;MockClock则是测试时使用的。

这个实现是什么时候注入的呢?

代码语言:go
AI代码解释
复制
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包内只留一个接口。

p9.png
p9.png

注入真实的realClock的地方也不能放在util的初始化中,也得放在util包外(比如Sentinel初始化的地方),这样一来,util就不再直接依赖base了。

p10.png
p10.png

这样一改造,编译就能通过了,当然这代码只是个示意,还需要精雕细琢。

最后

我们发现就算给你现成的代码,抄起来也是比较难的,有点类似「脑子会了,但手不会」的尴尬境地。

同时每个编程语言都有自己的风格,也就是我们通常说的,Go代码要写得更「Go」一点,所以语言不止是一个工具这么简单,它的背后也存在着自己的思考方式。

本文其实是从一个案例分享了如何解决Go的循环依赖问题,以及一些和Java对比的思考,更偏向代码工程。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Sentinel-Go 源码系列(二)|初始化流程和责任链设计模式
上节中我们知道了 Sentinel-Go 大概能做什么事情,最简单的例子如何跑起来
龟仙老人
2021/11/09
4830
influxdb 简介与实现(一)
InfluxDB是一个开源的没有外部依赖的时间序列数据库。适用于记录度量,事件及执行分析。
Timu
2018/07/17
1.5K0
influxdb 简介与实现(一)
6.深入TiDB:乐观事务
由于 TiDB 的事务模型沿用了 Percolator 的事务模型。所以先从 Percolator 开始,关于 Percolator 论文没看过的同学看这里:https://www.luozhiyun.com/archives/609 我已经翻译好了
luozhiyun
2021/11/02
6710
更简的并发代码,更强的并发控制
因为没有 泛型 支持,所以才会出现多种类型的原子类支持。以下采用 float64 作为例子:
kevwan
2021/04/19
7880
更简的并发代码,更强的并发控制
发现一个开源项目优化点,点进来就是你的了
最近无聊(摸)闲逛(鱼)github时,发现了一个阿里开源项目可以贡献代码的地方。
龟仙老人
2022/05/25
3200
Sentinel-Go 源码系列(三)滑动时间窗口算法的工程实现
就算现在大厂面试经常要手撕算法,也是更偏向考查代码工程实现的能力,之前在群里看到这样的图片,就觉得很离谱。
龟仙老人
2021/12/20
7260
聊聊zerolog的diode.Writer
github.com/rs/zerolog@v1.20.0/diode/diode.go
code4it
2021/01/05
4520
聊聊zerolog的diode.Writer
victoriaMetrics中的一些Sao操作
victoriaMetrics中有一个fasttime库,用于快速获取当前的Unix时间,实现其实挺简单,就是在后台使用一个goroutine不断以1s为周期刷新表示当前时间的变量currentTimestamp,获取的时候直接原子加载该变量即可。其性能约是time.Now()的8倍。
charlieroro
2022/05/09
7660
Go微服务精讲:Go-Zero全流程实战即时通讯(13章全+电子书)
学习地址自己还原 /s/1p2gVcBMq87ko35yoWzyblQ 提取码: fajt
用户11101800
2024/04/29
4420
Bytom侧链Vapor源码浅析-节点出块过程
在这篇文章中,作者将从Vapor节点的创建开始,进而拓展讲解Vapor节点出块过程中所涉及的源码。
比原链Bytom
2020/08/07
6290
Go程序例子(45):Stateful Goroutines
在前一个示例中,我们使用了显式锁定和互斥锁来同步多个 goroutines 之间对共享状态的访问。另一个选择是使用 goroutines 和通道的内建同步功能来实现相同的结果。这种基于通道的方法与 Go 的理念一致,即通过通信来共享内存,并且每个数据片段只能由一个 goroutine 拥有。
用户11078756
2024/12/19
1140
Go程序例子(45):Stateful Goroutines
golang源码分析:sony/gobreaker
很久以前分析过一款断路器golang 源码分析(39)hystrix-go,最近看了一款类似的断路器sony/gobreaker,https://github.com/sony/gobreaker分析下它的源码,感觉理解又一步加深了,它的源码很简单就一个gobreaker.go,并且它携带了一个例子example/http_breaker.go,我们先从例子入手分析,它主要包含了两个函数,首先是初始化函数:
golangLeetcode
2022/08/03
5490
源码阅读之 pond
前面的是默认值;后面的是错误定义。这是个好习惯,将系统可能的错误枚举地、显示的定义、罗列,方便调用者判断错误,或包装转发错误。
charmer
2025/03/26
910
100 行写一个 go 的协程池 (任务池)
go 的 goroutine 提供了一种较线程而言更廉价的方式处理并发场景, go 使用二级线程的模式, 将 goroutine 以 M:N 的形式复用到系统线程上, 节省了 cpu 调度的开销, 也避免了用户级线程(协程)进行系统调用时阻塞整个系统线程的问题。【1】
会呼吸的Coder
2020/02/17
2.6K0
Golang源码探索----GC的实现原理(5)
gcDrain函数扫描完根对象, 就会开始消费标记队列, 对从标记队列中取出的对象调用scanobject函数:
李海彬
2020/12/03
8280
滑动时间窗口设计
导语:系统做出一系列调度要基于系统运行的统计指标,例如熔断(基于请求数、并发数、请求延迟、异常比例等),本文解析基于滑动时间窗口的统计结构设计办法。
用户8000484
2021/12/04
2.3K0
以太坊虚拟机(下篇)
区块链上的虚拟机(Virtual Machine)是指建立在去中心化的区块链上的代码运行环境,目前市面上比较主流的便是以太坊虚拟机(Ethereum Virtual Machine,EVM)和类以太坊虚拟机,它基于Account账户模型将智能合约代码以对外完全隔离的方式在内部运行,实现了图灵完备的智能合约体系,本篇文章将从源码角度对其工作原理进行简要分析~
Al1ex
2021/07/21
7350
【并发编程】atomic 如何保证原子操作?分别用那几个方法?
之前学习了一些并发原语,已经认为差不多可以应对很多场景了,但是为什么还要学习原子操作呢?原来,在一些场景中,使用并发原语可能更加复杂,为了更轻松地实现底层的优化。
了凡银河系
2022/08/22
1.3K0
Influxdb中TSM文件结构解析之WAL
对应的类型是Value,这是个interface,定义在tsdb/engine/tsm1/encoding.go中
扫帚的影子
2019/01/07
1.6K0
以太坊区块同步
首先根据Synchronise开始区块同步,通过findAncestor找到指定节点的共同祖先,并在此高度进行同步,同时开启多个goroutine同步不同的数据:header、receipt、body,假如同步高度为100的区块,必须先header同步成功同步完成才可以进行body和receipts的同步,而每个部分的同步大致都是由FetchParts来完成的,里面包含了各个Chan的配合,也会涉及不少的回调函数
Al1ex
2021/07/21
4.2K0
以太坊区块同步
相关推荐
Sentinel-Go 源码系列(二)|初始化流程和责任链设计模式
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档