开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

坑爹的问题 如何抽丝剥茧的debug

发表于 2021-11-20

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」

debug犹如警察破案,抽丝剥茧,步步逼近问题的本质。

故事描述

我们客户端接口项目对项目配置参数做了缓存,以保证接口访问速度

管理后台可以对这些配置参数进行修改,当有修改时主动刷新缓存

这么简单且朴素的设计呀

问题描述

我花了不到一个小时就写完了功能,在本地环境测试没有问题,发布到代码到测试环境

测试环境运营妹子测试没有问题,发布到了预发布环境(预发布环境除了代码和生产环境不一致,其他都一致)

问题出现了:管理后台修改配置参数后,客户端没有变

image.png

排查问题

不科学这三个字在我脑海中萦绕

通过打Log的方式,我发现,管理后台主动刷新缓存了,且刷新缓存之后打印的数据是正确的。查看预发布接口,发现数据仍然不对。

活见鬼了哇

于是我再预发布环境,运行脚本刷新缓存,发现生效了,结果是正确的。

既然两个项目刷新缓存都成功了,但是管理后台刷新缓存之后,预发布接口获得的数据仍然不对,那问题就应该是key值不一致。

打印自己的key值,甚至搬出了文件助手比对,发现也一样啊。
(这时候我打印的key值,是我在业务代码中设置的key值)

难道有鬼,于是打开RDS检查一下,终于发现问题了。

发现问题

原因是我们设置的key值除了业务代码中设置的值之外,还会拿到.env配置文件中的APP_NAME在最前面拼接为前缀。

哇咔咔,两个项目的APP_NAME不一致!!!

解决问题

最简单的办法就是把APP_NAME改成一致的,因为管理后台是内部使用,且检查了缓存数据,除了这次的需求,之前没有缓存数据,所以就把管理后台的APP_NAME统一成客户端接口项目的APP_NAME

进一步思考

出现这种问题本身就是很不科学的,两个项目之间互相耦合,出现了相同的代码、配置参数在两个项目中重复定义的问题。

我们可以通过api调用的方式,解决问题,耦合的代码在客户端接口项目中统一管理,提供接口给管理后台调用,避免写重复代码。

我们还能使用更高级的办法,即通过rpc调用,之前写过一篇 go语言RPC调用的demo,大家感兴趣可以阅读。

Go RPC入门指南1:RPC的使用边界在哪里?如何实现跨语言调用?

image.png

总结

写代码1小时,debug2小时,这个小小的问题对我造成了很大的困扰。

但是抽丝剥茧,逐步定位问题的思路让我收益匪浅,分享给大家。

最后

欢迎大家关注我的微信公众号:程序员升级打怪之旅

同样欢迎关注我的专栏服务端从入门到精通,整理了深入浅出的服务端开发总结,包括:Go Java Php

如有问题可以加本人微信交流,微信号:wangzhongyang0601。

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hi,你想要的在线创建架构图都在这儿!(三)

发表于 2021-11-20

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

🤞 个人主页:@青Cheng序员石头

🤞 粉丝福利:加粉丝群 一对一问题解答,获取免费的丰富简历模板、提高学习资料等,做好新时代的卷卷王!

上一篇文章Hi,你想要的在线创建架构图都在这儿!(二),点击穿越过去。

六、Edraw Max

Edraw Max英文名字也许你会很陌生,但是如果说起中文,那么你一定会有一种“原来如此”的感觉。

poster_visualization.svg

它的中文名字就是亿图图示,这个设计软件是一款集办公绘图、工程绘图、图文编辑、彩页设计为一体的设计软件。我身边有很多人在用这个设计软件,特别是一些来自外企的朋友。其自诩为一站式办公绘图利器,相当多的外企都信任Edraw Max。

flow-img1-4.png

亿图图示是一款基于矢量的绘图工具,包含大量的事例库和模板库。通过它可以很方便的绘制各种专业的业务流程图、组织结构图、商业图表、程序流程图、数据流程图、工程管理图、软件设计图、网络拓扑图、商业图表、方向图、UML、线框图、信息图、思维导图、建筑设计等等。

这个产品很多功能都是要收费的,有兴趣的读者可以去体验体验,它的专业性大概率不会让你失望。

七、Cacoo

Cacoo这款工具支持22种语言,适用于全球大多数用户。支持实时团队协作,用户可以同时在线使用这款原型工具。运用它可以制作多种图表,例如:site maps, wire frames, UML和网络图表等。其软件的核心卖点是Communicate visually, collaborate easily。

如果您正在寻找拖拽边界,支持网格以及修订历史的实时合作设计软件,Cacoo是非常棒的。它允许您免费创建25张软件图,并同时支持以PNG格式导出。

collaborate_1000_600.png

screenshot_hero_700_420@2x.png

接下来的在线创建架构图的工具还会有:

  • Lucidchart
  • Creately
  • Coggle
  • Mindmeister
  • yED

朋友,你用的是哪一种呢?评论留言你常用的工具,你认为最好的在线画图工具!


少年,没看够?点击石头的详情介绍,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang 代码中设计:接口的函数实现

发表于 2021-11-20

这个系列文章我会总结一下我在Golang中经常遇到的一些代码设计

这篇我们看看接口的函数实现,对于像我这样从其他语言转过来的开发者,函数类型以及函数类型方法还是需要理解清楚的。工作中对接口的实现也大多是通过自定义结构体作为类型,然后再为它实现接口,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
golang复制代码// alertmanager/notify/notify.go:220
// 约定接口
type Stage interface {
Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error)
}
// 自定义类型实现接口
type RoutingStage map[string]Stage
func (rs RoutingStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
receiver, ok := ReceiverName(ctx)
if !ok {
return ctx, nil, errors.New("receiver missing")
}
s, ok := rs[receiver]
if !ok {
return ctx, nil, errors.New("stage for receiver missing")
}
return s.Exec(ctx, l, alerts...)
}

这是一种很常见也很好理解的设计,约定接口Stage然后自定义类型RoutingStage来实现接口,这样RoutingStage就可以被所有使用Stage的位置使用了,但我们看看下面这段:

1
2
3
4
5
golang复制代码// alertmanager/notify/notify.go:234
type StageFunc func(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context context.Context, []*types.Alert, error)
func (f StageFunc) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context context.Context, []*types.Alert, error) {
return f(ctx, l, alerts...)
}

这里对比类和实例的概念,把StageFunc这样定义函数签名的称为函数类型,这个函数类型对应的函数称为函数值,这段代码中定义了一个函数类型 StageFunc,还为这个函数类型定义了Exec方法,这样的话这个函数类型是满足Stage接口的,在Exec函数中用这个函数值调用Exec方法时接收的参数调用了这个函数值自己。

举个例子,如果一个函数aFunc满足func(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context context.Context, []*types.Alert, error)这样的签名,但这时它还不是StageFunc类型的函数,可以使用bFunc:=StageFunc(aFunc)把这个函数值转为StageFunc类型的函数值,这时候既可以直接调用bFunc(),还可以bFunc.Exec()调用。

这是Golang中一种不是特别常见的设计,当然这种设计的重点也不是对一个函数两种调用方式,而是如何使用函数来实现接口。在net/http的源码中的HandlerFunc也是这种设计方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
golang复制代码// net/http/server.go:2065
// 约定接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// 接口的函数实现
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
// 同样的,在函数类型 HandlerFunc 的 ServeHTTP 方法中
// 用方法接收的参数直接调用函数值
f(w, r)
}

// 使用接口
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

这里的HandlerFunc类似一个转换器,把用户自定义的函数转为特定的HandlerFunc类型,然后通过调用这个类型的方法来调用这个函数,这样相当于开放一个函数签名给用户,用户定义任意满足函数签名的函数就可以被Server使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python中几个非常有趣的模块

发表于 2021-11-20

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

前言

最近学习Python,发现了许多有趣的模块。感觉开启了新世界的大门,因为我也不是对所有模块都熟悉,所以今天不是讲代码。

1、ItChat

这是一个微信自动回复的模块,因为我微信一直无法登陆,所以也没有测试这个模块的功能。这里只是简单介绍一下。

使用流程大致就是:

  1. 登陆微信
  2. 注册监听
  3. 响应监听
  4. 结束

而我就卡在了登陆微信上面,注册监听的话代码也是非常简单的。它可以监听多种数据,文字、图片、视频等…功能也是非常齐全的。也可以调用图灵机器人的API使用(不过图灵机器人是收费的),让自己的微信变成一个智能的小机器人。具体的使用可以参见博客:www.cnblogs.com/dongxiaodon…。

2、WordCloud

我觉得大家应该都看过这种样式的图片:
在这里插入图片描述
就是一大堆关键词形成一张图片,有的是矩形有的是一些特殊形状。而WordCloud的作用,就是制作这种图片。这个模块的使用也是非常方便的,我们需要准备一个文本、一张图片(PNG的)、然后填写一堆参数就好了。
在这里插入图片描述
确实是挺帅的,但是这是网图。大家可以尝试自己做出喜欢的图片。
具体使用可以参见:www.cnblogs.com/jlutiger/p/…

3、Pillow

这是一个图片处理的模块,功能齐全。我们可以用它转换格式、裁剪图片、拼接图片、旋转图片、高斯模糊、颜色通道分离/合并、、、当然了,比p图软件是麻烦多了。不过我还是挺喜欢这个模块的,最近也打算用这个模块做点小东西。下面看一下效果:
在这里插入图片描述

4、Pygame

这个是一个图形界面开发的模块,不过我也只是接触了一下。并没有学习太多,所以不是非常了解它和tkinter模块的区别。

除此之外还有许多有趣的模块,像是处理图标的matplotlib、处理Excel的pandas、处理文本的Jieba等。大家可以自己去多了解一些模块,可以大大减少编程的工作量。

2019、11、3更新


5、wxpy

这个也是用来开发微信机器人的模块,算是建立在itchat之上的一个模块。使用起来非常简单,如下几行代码就可以实现回复功能:

1
2
3
4
5
python复制代码from wxpy import *

bot = Bot(cache_path=True)
my_friend = bot.friends().search('扎克斯', sex=MALE, city="南昌")[0]
my_friend.send('Hello WeChat!')

但是奈何我的微信还是登录不了,用同学的微信做了个小实验。感觉还是非常方便的。在使用过程中我遇到了一个问题,在网上也是找了非常久没解决。(你们应该不会遇到我相同的问题),运行时报了如下异常:

1
python复制代码OSError: [WinError 1155] 没有应用程序与此操作的指定文件有关联。: 'QR.png'

我是万万没想到,是我没有查看图片的软件。在我下了一个“爱奇艺万能播放器”后问题解决了(我真的没打广告)。

6、pynput

这是一个操作鼠标键盘的一个模块,使用起来非常简单。其主要模块有mouse、keyboard。也就是键盘鼠标。操作非常简单,下面给大家看一个实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码from pynput import *

# 创建一个鼠标
my_mouse = mouse.Controller()
# 创建一个键盘
my_keyboard = keyboard.Controller()

# 移动鼠标到指定位置
my_mouse.position = (100, 100)
# 点击鼠标左键
my_mouse.click(mouse.Button.left)

# 用键盘打字
my_keyboard.type('zack')

不过暂时还没用这个模块做出什么好玩的东西。另外最近使用到词云,也写了篇关于词云的博客《WordCloud生成卡卡西忍术词云》希望可以帮到大家。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在微服务团队中高效使用 Git 管理代码?

发表于 2021-11-20

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」

你好,我是看山。

用了 Git 多年,优势和挑战都是深有体会。

话不多说,直接上问题:如何在足够规模团队中高效使用 Git 管理代码?

继续不多话,直接上答案:分支管理。

Git 的分支管理有很多实践,有些是从 SVN 类的集中式版本管理工具继承的,有些是根据 Git 自己的特性总结的,目前市面上比较有名的三种 Git 分支管理模型是:

  • TrunkBased:主干在手,天下我有。所有代码都往主干上招呼,发版也只用主干。
  • GitFlow:严谨、规范、难用,主要是记不住该往哪个分支合并了。
  • AoneFlow:前两种都不行,那就借鉴各自的优点,达到阴阳平衡,中庸也。

TrunkBased

TrunkBased,又叫主干开发,有一个网站专门介绍这种开发方式:Trunk Based Development。

TrunkBased 是持续集成思想所崇尚的工作方式,它由单个主干分支和许多发布分支组成,每个发布分支在特定版本的提交点上从主干创建出来,用来进行上线部署和 Hotfix。在 TrunkBased 模式中,没有显性的特性分支。当然实际上 Git 的分布式特征天生允许每个人有本地分支,TrunkBased 也并非排斥短期的特性分支存在,只不过在说这种模式的时候,大家通常都不会明确强调它罢了。

图片

使用主干开发后,我们的代码库原则上就只能有一个 Trunk 分支即 master 分支了,所有新功能的提交也都提交到 master 分支上,保证每次提交后 master 分支都是可随时发布的状态。没有了分支的代码隔离,测试和解决冲突都变得简单,持续集成也变得稳定了许多。

但是这种方案缺点也比较明显,如果大家都在主干进行开发,当代码提交合并时,将会异常痛苦,一不小心就会出现冲突。而且,这种因为这种方式没有明显的特性分支,想要移除已经上线的特性会变得非常困难。(如果你说把不要的功能注释,重新发版,那就当我什么也没说。)还有一种方案是引入特性开关,通过开关控制特性是否启用和关闭,但是增加开关就引入了复杂性,引入复杂性就引入了出 bug 的风险,毕竟多增加的每行代码都有可能是一个新的 bug。

GitFlow

GitFlow 来源于 Vincent Driessen 提出的 A successful Git branching model,整体来说,是一套完善的版本管理流程。缺点就是太过严格,不太适合喜欢自由而且懒的程序猿。当然,在程序猿这种物种中,没有完美的解决方案,总有那么一小撮人会觉得不好。参考 Ant、Maven 和 Gradle。

先上图:

图片

GitFlow 常用分支:

  • 主干分支( master):最近发布到生产环境代码的分支,这个分支只能从其他分支合并,不能再 Master 分支直接修改。
  • 主开发分支( develop):包含所有要发布到下一个 Release 版本的代码。可以在 Develop 直接开发,也可以将 Feature 的特性代码合并到 Develop 中。 图片
  • 特性分支( feature/*):功能项开发分支,以 feature/开头命名分支,当功能项开发完成,将被合并到主开发分支进入下一个 Release,合并完分支后一般会删掉这个特性分支。 图片
  • 发布分支( release/*):基于主开发分支创建的一个发布分支,以 release/开头命名分支,用于测试、bug 修复及上线。完成后,合并到主干分支和主开发分支,同时在主干分支上打个 Tag 记住 Release 版本号,然后可以删除发布分支。 图片
  • 热修复分支( hotfix/*):用于解决线上 Release 版本出现的 bug,以 hotfix/开头命名分支,修复线上问题,完成后,合并到主干分支和主开发分支,同时在主干分支上打个 tag。 图片

根据上面描述,GitFlow 是一套完整的从开发到生产的管理方式,但是各种分支来回切换及合并,很容易把人搞晕,所以用的人也是越来越少。

AoneFlow

AoneFlow 是阿里内部的一套版本管理模型,兼顾了 TrunkBased 易于持续集成和 GitFlow 易于管理需求的特点,又规避了 GitFlow 分支繁琐的缺点,也就是中庸。

AoneFlow 使用三个分支:主干分支( master)、特性分支( feature/*)、发布分支( release/*),以及三条原则:

规则一,开始工作前,从主干分支创建特性分支。

这条规则借鉴了 GitFlow,每当开始一件新的工作项(比如新的功能或是待解决的问题,可以是一个人完成,或是多个人协作完成)时,从代表最新已发布版本的主干分支上创建一个通常以 feature/前缀命名的特性分支,然后在这个分支上提交代码修改。也就是说,每个工作项对应一个特性分支,所有的修改都不允许直接提交到主干。

图片

特性分支不止承担了新功能,也是待解决问题的分支。对于我们团队,为了避免歧义,会将新功能以 feature/为前缀,待解决问题以 hotfix/为前缀,除了名称外,其他都按照规则一执行。

规则二,通过合并特性分支,形成发布分支。

GitFlow 先将已经完成的特性分支合并回主干分支和主开发分支,然后在主干分支上打 Tag 记录发布信息。TrunkBased 是等所有需要的特性都在主干分支上开发完成,然后从主干分支的特定位置拉出发布分支。而 AoneFlow 的思路是,从主干上拉出一条新分支,将所有本次要集成或发布的特性分支依次合并过去,从而得到发布分支,发布分支通常以 release/前缀命名。

图片

我们可以将每条发布分支与具体的环境相对应, release/test对应部署测试环境, release/prod对应线上正式环境等,并与流水线工具相结合,串联各个环境上的代码质量扫描和自动化测试关卡,将产出的部署包直接发布到相应环境上。

另外,发布分支的特性组成是动态的,调整起来特别容易。在一些市场瞬息万变的互联网企业,以及采用“敏捷运作”的乙方企业经常会遇到这种情况,已经完成就等待上线的需求,随时可能由于市场策略调整或者甲方的一个临时决定,其中某个功能忽然要求延迟发布或者干脆不要了。再或者是某个特性在上线前发现存在严重的开发问题,需要排除。按往常的做法,这时候就要来手工“剔代码”了,将已经合并到开发分支或者主干分支的相关提交一个个剔除出去,做过的同学都知道很麻烦。在 AoneFlow 模式下,重建发布分支,只需要将原本的发布分支删掉,从主干拉出新的同名发布分支,再把需要保留的各特性分支合并过来就搞定,而且代码是干净的,没有包含不必要的特性。

此外,发布分支之间是松耦合的,这样就可以有多个集成环境分别进行不同的特性组合的集成测试,也能方便的管理各个特性进入到不同环境上部署的时机。松耦合并不代表没有相关性,由于测试环境、集成环境、预发布环境、灰度环境和线上正式环境等发布流程通常是顺序进行的,在流程上可以要求只有通过前一环境验证的特性,才能传递到下一个环境做部署,形成漏斗形的特性发布流。当然,这种玩法比较适合有完整开发集成平台的公司,小团队玩不转,比如我们团队就玩不动这种高级玩法。

规则三,发布到线上正式环境后,合并相应的发布分支到主干,在主干添加 Tag,同时删除该发布分支关联的特性分支。

当一条发布分支上的流水线完成了一次线上正式环境的部署,就意味着相应的功能真正的发布了,此时应该将这条发布分支合并到主干。为了避免在代码仓库里堆积大量历史上的特性分支,还应该清理掉已经上线部分特性分支。与 GitFlow 相似,主干分支上的最新版本始终与线上版本一致,如果要回溯历史版本,只需在主干分支上找到相应的版本标签即可。

图片

除了基本规则,还有一些实际操作中不成文的技巧。比如上线后的热修复,正常的处理方法应该是,创建一条新的发布分支,对应线上环境(相当于 Hotfix 分支),同时为这个分支创建临时流水线,以保障必要的发布前检查和冒烟测试能够自动执行。

再啰嗦几句废话

不管哪种方式,既然存在,都会有一定合理性。所以,不管最终翻了哪个牌子,不是因为这个好看,而是因为这个更适合自己。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

alertmanager 源码分析三 流水线

发表于 2021-11-20

前篇说到告警写入后被分发到dispatcher的aggrGroupsPerRoute中的aggrGroup里,然后每个aggrGroup会启动一个自己的goroutine按照group_wait和group_interval两种频率来定时调用dispatcher.stage.Exec方法来处理告警,实际上dispatcher.stage中存储的就是由多种处理函数编排成的一个告警处理流水线,也就是架构图中的下面这部分:

截屏2021-11-19 17.07.37.png

pipeline的构建是在main函数中创建dispatcher的时候,很容易找到,这里不赘述了,我们看看 pipeline 是怎样定义自己的 Exec 方法的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
golang复制代码// pipeline 就是 RoutingStage 类型,
// 它是基于 ctx 中的 receiver 进入这个 receiver 的 Stage
type RoutingStage map[string]Stage

// 看看流水线构建函数是如何为每个 receiver 配置 Stage 的
func (pb *PipelineBuilder) New(
receivers map[string][]Integration,
wait func() time.Duration,
inhibitor *inhibit.Inhibitor,
silencer *silence.Silencer,
muteTimes map[string][]timeinterval.TimeInterval,
notificationLog NotificationLog,
peer Peer,
) RoutingStage {
rs := make(RoutingStage, len(receivers))

ms := NewGossipSettleStage(peer)
is := NewMuteStage(inhibitor)
ss := NewMuteStage(silencer)
tms := NewTimeMuteStage(muteTimes)
// 基于 receiver 的 name 编排了一个包含多个 Stage 对象的 MultiStage
for name := range receivers {
st := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics)
rs[name] = MultiStage{ms, is, tms, ss, st}
}
return rs
}

// 实现了 Exec 方法实际上是实现了 Stage 接口
// 流水线就是由各种 Stage 对象组合成的,后面再说 Stage 的设计,
// 先看看 RoutingStage 做了什么
func (rs RoutingStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
// 从 context 中获取当前路由下配置的告警接收器
receiver, ok := ReceiverName(ctx)
if !ok {
return ctx, nil, errors.New("receiver missing")
}
// 从 RoutingStage 中找到对应的 MultiStage 执行 MultiStage.Exec
s, ok := rs[receiver]
if !ok {
return ctx, nil, errors.New("stage for receiver missing")
}
return s.Exec(ctx, l, alerts...)
}

// 这个时候 aggGroup 经过 RoutingStage
// 为这些 Alerts 找到了 MultiStage
// 我们看看 MultiStage
// MultiStage 就是个包含了多个 Stage 的数组
type MultiStage []Stage

func (ms MultiStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
var err error
// 顺序执行使用数组中的每个 Stage.Exec()
for _, s := range ms {
if len(alerts) == 0 {
return ctx, nil, nil
}
ctx, alerts, err = s.Exec(ctx, l, alerts...)
if err != nil {
return ctx, nil, err
}
}
return ctx, alerts, nil
}

到这里总结一下,Dispatcher下的每个aggGroup先按照自己的receiver.Name通过调用RoutingStage.Exec中找到对应的MultiStage,然后顺序调用其中的每个Stage.Exec,接下来看下Stage的设计:

1
2
3
4
5
6
7
golang复制代码type Stage interface {
Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context context.Context, []*types.Alert, error)
}
// 举几个具体的 Stage 类型
type FanoutStage []Stage
type GossipSettleStage struct { peer Peer }
type MuteStage struct { muter types.Muter }

Stage这里是一个只约定了Exec函数的接口,所以任何一个对象只要定义了相同签名的Exec函数就是Stage类型,你会在源码中很容找到各种Stage,然后在对应的Exec方法中就知道告警在当前Stage中会被怎样处理,Exec的入参数中alerts表示哪些告警进入这个Stage,然后出参中的alerts就是经过当前Stage处理还剩哪些告警,ctx可以很方便各个Stage获取当前流水线上的参数,当然也可以写入参数让后面的Stage使用。

前面RoutingStage.Exec和MultiStage.Exec已经看过了我这里再找几个Stage看看里面的具体行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
golang复制代码// 负责并发的执行一些 Stage
type FanoutStage []Stage
func (fs FanoutStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
var (
wg sync.WaitGroup
me types.MultiError
)
wg.Add(len(fs))
// FanoutStage 和 MultiStage 使用的相同结构 []Stage
// 但是 FanoutStage 是并发的执行
for _, s := range fs {
go func(s Stage) {
if _, _, err := s.Exec(ctx, l, alerts...); err != nil {
me.Add(err)
}
wg.Done()
}(s)
}
wg.Wait()

if me.Len() > 0 {
return ctx, alerts, &me
}
return ctx, alerts, nil
}

func createReceiverStage(
name string,
integrations []Integration,
wait func() time.Duration,
notificationLog NotificationLog,
metrics *Metrics,
) Stage {
// 这个是 FanoutStage 构建时
// 里面是多个可以并发的 MultiStage
var fs FanoutStage
for i := range integrations {
recv := &nflogpb.Receiver{
GroupName: name,
Integration: integrations[i].Name(),
Idx: uint32(integrations[i].Index()),
}
var s MultiStage
s = append(s, NewWaitStage(wait))
s = append(s, NewDedupStage(&integrations[i], notificationLog, recv))
s = append(s, NewRetryStage(integrations[i], name, metrics))
s = append(s, NewSetNotifiesStage(notificationLog, recv))

fs = append(fs, s)
}
return fs
}

再看看静默和抑制的Stage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
golang复制代码type MuteStage struct {
muter types.Muter
}
func NewMuteStage(m types.Muter) *MuteStage {
return &MuteStage{muter: m}
}
func (n *MuteStage) Exec(ctx context.Context, _ log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
var filtered []*types.Alert
// 检查每个 alert 的 labelset 是否跟静默规则或者抑制规则的 labelSet 匹配
// 如果 alert 的 Labels 不匹配 Mute 就保留下来, 传给下一个 stage
for _, a := range alerts {
if !n.muter.Mutes(a.Labels) {
filtered = append(filtered, a)
}
}
return ctx, filtered, nil
}

MuteStage被用来实现SilenceStage和InhibitStage, 它包含了一个 muter,MuteStage.Exec最重要的就是调用muter.Mutes方法,那么muter就是一个包含Mutes方法的接口,Silencer和Inhibitor实现各自的 Mutes方法就可以作为MuteStage,那我们再看看它们各自是怎样实现Mutes方法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
golang复制代码// 这个就是 Inhibitor 实现的 Muter 接口
// 抑制功能设计是这样的:
// 对于 a, b 两个 alert
// 如果 a 满足 SourceMatchers
// b 满足 TargetMatchers
// 则 Equal 成立时, 用 a 抑制 b
// Equal 成立的两个极端情况:
// 1. a 和 b 都没有 Equal 中的 labels, 成立
// 2. a 和 b 都有 Equal 中的 labels, 且都为空值, 成立
// 关于抑制不生效的极端情况:
// 1. a 同时满足 SourceMatchers, TargetMatchers, b 同时满足 SourceMatchers, TargetMatchers, 且 Equal 成立, 不生效
// 抑制不生效的极端情况是为了避免告警的自抑制
// 所以,告警写入阶段 Inhibitor 会通过 Sub 的方式监听新的 alert 并判断 source 侧是否匹配,
// 匹配的话表示这个 alert 可能会抑制其他的 alert, 就会被缓存起来
// 在 Inhibitor 对应的 MuteStage 中取出来检查
func (ih *Inhibitor) Mutes(lset model.LabelSet) bool {
fp := lset.Fingerprint()
// 检查内存中所有 rule 是否匹配 lset
for _, r := range ih.rules {
// target 不匹配就没必要计算了
// 因为我们就是为了抑制 target
if !r.TargetMatchers.Matches(lset) {
continue
}
// target 匹配就检查 source, 如果 source 也匹配
// 那么就需要排除两端都匹配的情况
if inhibitedByFP, eq := r.hasEqual(lset, r.SourceMatchers.Matches(lset)); eq {
ih.marker.SetInhibited(fp, inhibitedByFP.String())
return true
}
}
// 这个位置没传 ids, 那么这个 alert 被置为 "active"
ih.marker.SetInhibited(fp)
return false
}

// 调用这个函数之前, 被检查 alert 已经满足了规则的 target,
// 而 scache 中的 alert 已经满足了规则的 source
// 剩下要确认的是:
// scache 中的 alert 有没有标签和被检查 alert 标签一致的,
// 再避免 alert 自我抑制的场景就可以了
func (r *InhibitRule) hasEqual(lset model.LabelSet, excludeTwoSidedMatch bool) (model.Fingerprint, bool) {
Outer:
for _, a := range r.scache.List() {
// The cache might be stale and contain resolved alerts.
if a.Resolved() {
continue
}

// 检查规则标签
for n := range r.Equal {
if a.Labels[n] != lset[n] {
continue Outer
}
}
// a 在加入 r.scache 的时候已经满足了 r.Source, 如果再通过 target 检查, 那么 scache 中的这个 a 同时满足 source 和 target
// 而 excludeTwoSidedMatch 如果为 true, 表示当前 dispatcher 处理的 alert 在 source 和 target 都满足
// 所以这个条件变成了:
// 如果被检查的 alert 标签还和 a 标签相同, 即抑制规则生效, 而且 a 和被检查的 alert 都同时满足 source 和 target,
// 就忽略 a 对被检查 alert 的抑制, 这里防止了一个告警自己抑制自己情况
if excludeTwoSidedMatch && r.TargetMatchers.Matches(a.Labels) {
continue Outer
}
// 出现一个抑制生效, 剩下的就不继续检查
return a.Fingerprint(), true
}
return model.Fingerprint(0), false
}

// 这个就是 Silencer 实现的 Muter 接口
func (s *Silencer) Mutes(lset model.LabelSet) bool {
fp := lset.Fingerprint()
activeIDs, pendingIDs, markerVersion, _ := s.marker.Silenced(fp)

var (
err error
allSils []*pb.Silence
newVersion = markerVersion
)
// 找出现在正在生效的静默规则
// 用来判断当前 alerts 哪些需要被静默掉
// version 相同表示 fp 标记时的 silences 到现在没有新的静默规则加入
if markerVersion == s.silences.Version() {
totalSilences := len(activeIDs) + len(pendingIDs)
if totalSilences == 0 {
return false
}
allIDs := append(append(make([]string, 0, totalSilences), activeIDs...), pendingIDs...)
allSils, _, err = s.silences.Query(
// 静默规则是用户在 web 端写入
// 这个位置使用 ids 和两种状态来过滤出需要判断的静默规则
// 这个 query 的封装也很特别,我后面会在golang代码设计的文章中聊
QIDs(allIDs...),
QState(types.SilenceStateActive, types.SilenceStatePending),
)
} else {
allSils, newVersion, err = s.silences.Query(
QState(types.SilenceStateActive, types.SilenceStatePending),
QMatches(lset),
)
}
if err != nil {
level.Error(s.logger).Log("msg", "Querying silences failed, alerts might not get silenced correctly", "err", err)
}
if len(allSils) == 0 {
s.marker.SetSilenced(fp, newVersion, nil, nil)
return false
}
activeIDs, pendingIDs = nil, nil
now := s.silences.now()

// 这里仅根据搜索结果的数量就判断是否需要静默当前的 alert
// 并没有计算 silence 的时间区间和当前时间是否重合,
// 因为 silence 有效的计算是在 Maintenance 过程中使用 GC 来维护的
// 所以匹配的一定是现在就生效的
for _, sil := range allSils {
switch getState(sil, now) {
case types.SilenceStatePending:
pendingIDs = append(pendingIDs, sil.Id)
case types.SilenceStateActive:
activeIDs = append(activeIDs, sil.Id)
default:
// Do nothing, silence has expired in the meantime.
}
}
level.Debug(s.logger).Log(
"msg", "determined current silences state",
"now", now,
"total", len(allSils),
"active", len(activeIDs),
"pending", len(pendingIDs),
)
sort.Strings(activeIDs)
sort.Strings(pendingIDs)

// activeIDs 为空且没有 inhibitBy 的话, fp 仍然会是 active 的
// pendingIDs 不会对 fp 的状态有影响
s.marker.SetSilenced(fp, newVersion, activeIDs, pendingIDs)
return len(activeIDs) > 0
}

到这里,流水线的大致情况就介绍的差不多了,总结一下:

  1. 先约定Stage接口,
  2. 再定义一些控制流程的Stage,比如RoutingStage,MultiStage,FanoutStage等
  3. 然后根据需要定义一些对alerts做真正处理的的Stage,比如InhibitStage,SilenceStage,TimeMuteStage等
  4. 最后把这些处理alerts的Stage使用流程控制的Stage进行编排,就成了流水线

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

干货,聊聊Java中String类!!!

发表于 2021-11-20

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」。

java.lang.String 类可能是大家日常用的最多的类,但是对于它是怎么实现的,你真的明白吗?
认真阅读这篇文章,包你一看就明白了。

String 类定义

1
2
java复制代码public final class String implements 
java.io.Serializable, Comparable<String>, CharSequence {}

从源码可以看出,String 是一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建,包含在这个对象中的字符序列是不可改变的,包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串,下面讲解方法时会介绍)。接着实现了 Serializable接口,这是一个序列化标志接口,还实现了 Comparable 接口,用于比较两个字符串的大小(按顺序比较单个字符的ASCII码),后面会有具体方法实现;最后实现了 CharSequence 接口,表示是一个有序字符的集合,相应的方法后面也会介绍。

字段属性

1
2
3
4
5
6
7
8
java复制代码/**用来存储字符串  */
private final char value[];

/** 缓存字符串的哈希码 */
private int hash; // Default to 0

/** 实现序列化的标识 */
private static final long serialVersionUID = -6849794470754667710L;

一个 String 字符串实际上是一个 char 数组。

构造方法

String 类的构造方法很多。可以通过初始化一个字符串,或者字符数组,或者字节数组等等来创建一个 String 对象。   

image-20211119100141712

1
2
3
java复制代码String str1 = "abc";//注意这种字面量声明的区别,文末会详细介绍
String str2 = new String("abc");
String str3 = new String(new char[]{'a','b','c'});

equals(Object anObject) 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}

String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。

hashCode() 方法

1
2
3
4
5
6
7
8
java复制代码public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}

String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:

1、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。

2、31可以被 JVM 优化,31 * i = (i « 5) - i。因为移位运算比乘法运行更快更省性能。

charAt(int index) 方法

1
2
3
4
5
6
7
java复制代码public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}

我们知道一个字符串是由一个字符数组组成,这个方法是通过传入的索引(数组下标),返回指定索引的单个字符。

intern() 方法

这是一个本地方法:

public native String intern();

当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。

这句话什么意思呢?就是说调用一个String对象的intern()方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中);如果没有,则将该对象添加到池中,并返回池中的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码String str1 = "hello";//字面量 只会在常量池中创建对象
String str2 = str1.intern();
System.out.println(str1==str2);//true

String str3 = new String("world");//new 关键字只会在堆中创建对象
String str4 = str3.intern();
System.out.println(str3 == str4);//false

String str5 = str1 + str2;//变量拼接的字符串,会在常量池中和堆中都创建对象
String str6 = str5.intern();//这里由于池中已经有对象了,直接返回的是对象本身,也就是堆中的对象
System.out.println(str5 == str6);//true

String str7 = "hello1" + "world1";//常量拼接的字符串,只会在常量池中创建对象
String str8 = str7.intern();
System.out.println(str7 == str8);//true

关于String类里面的众多方法,这里不一一介绍了,下面我们来深入了解一下,String 类不可变型。

面试精选

分析一道经典的面试题:

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) {
String A = "abc";
String B = "abc";
String C = new String("abc");
System.out.println(A==B);
System.out.println(A.equals(B));
System.out.println(A==C);
System.out.println(A.equals(C));
}

答案是:true、true、false、true

对于上面的题目,我们可以先来看一张图,如下:

image-20211119101936381

首先 String A= “abc”,会先到常量池中检查是否有“abc”的存在,发现是没有的,于是在常量池中创建“abc”对象,并将常量池中的引用赋值给A;第二个字面量 String B= “abc”,在常量池中检测到该对象了,直接将引用赋值给B;第三个是通过new关键字创建的对象,常量池中有了该对象了,不用在常量池中创建,然后在堆中创建该对象后,将堆中对象的引用赋值给C,再将该对象指向常量池。

需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么A.equals(C)返回true的原因了。

注意:看上图红色的箭头,通过 new 关键字创建的字符串对象,如果常量池中存在了,会将堆中创建的对象指向常量池的引用。

再来看一道题目,使用包含变量表达式创建对象:

1
2
3
4
5
6
7
8
java复制代码String str1 = "hello";
String str2 = "helloworld";
String str3 = str1+"world";//编译器不能确定为常量(会在堆区创建一个String对象)
String str4 = "hello"+"world";//编译器确定为常量,直接到常量池中引用

System.out.println(str2==str3);//fasle
System.out.println(str2==str4);//true
System.out.println(str3==str4);//fasle

str3 由于含有变量str1,编译器不能确定是常量,会在堆区中创建一个String对象。而str4是两个常量相加,直接引用常量池中的对象即可。

image-20211120095858404

String 不可变性

String类是Java中的一个不可变类(immutable class)。

简单来说,不可变类就是实例在被创建之后不可修改。

String不可变这个话题应该是老生长谈了,String自打娘胎一出生就跟他们的兄弟姐妹不一样,好好的娃被戴了一个final的帽子,

以至于byte,int,short,long等基本类型的小伙们都不带它玩。

如果你仔细阅读源码注释,你会发现这样一句话:

image-20211118194406592

大致意思就是String是个常量,从一出生就注定不可变。

首先需要补充一个容易混淆的知识点:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。

String 类是用 final 关键字修饰的,所以我们认为其是不可变对象。但是真的不可变吗?

每个字符串都是由许多单个字符组成的,我们知道其源码是由 char[] value 字符数组构成。

1
2
3
4
5
java复制代码/** The value is used for character storage. */
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0

value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static void main(String[] args) throws Exception {
String str = "Hello World";
System.out.println("修改前的str:" + str);
System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
// 获取String类中的value字段
Field valueField = String.class.getDeclaredField("value");
// 改变value属性的访问权限
valueField.setAccessible(true);
// 获取str对象上value属性的值
char[] value = (char[]) valueField.get(str);
// 改变value所引用的数组中的字符
value[3] = '?';
System.out.println("修改后的str:" + str);
System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
}
1
2
3
4
java复制代码修改前的str:Hello World
修改前的str的内存地址1746572565
修改后的str:Hel?o World
修改前的str的内存地址1746572565

通过前后两次打印的结果,我们可以看到 str 值被改变了,但是str的内存地址还是没有改变。但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的。

不可变的好处

首先,我们应该站在设计者的角度思考问题,而不是觉得这不好,那不合理:

  • 可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。
  • 我们的程序中大量使用了String字符串,有可能是出于安全性考虑。
  • 当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

小结

有兴趣的小伙伴也可以去阅读下String的源码,浩浩荡荡的3000+。

String 被new时是要创建对象的,+ 号拼接同理,程序中尽量不要使用 + 拼接,推荐使用StringBuffer或者StringBuilder。

感谢的阅读,希望看完三连一波呀,谢谢啦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis性能测试 前言 性能测试

发表于 2021-11-20

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。

前言

都说 Redis 性能极致,实际到底怎么样呢?我们借助 redis-benchmark 来测试一下。redis-benchmark 是 Redis 自带的测试工具,简直不要太舒服。

性能测试

快速测试

测试命令:redis-benchmark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
erlang复制代码[root@RLKJ-BT ~]# redis-benchmark
====== PING_INLINE ======
100000 requests completed in 1.66 seconds
50 parallel clients
3 bytes payload
keep alive: 1

98.94% <= 1 milliseconds
99.55% <= 2 milliseconds
99.60% <= 5 milliseconds
99.61% <= 6 milliseconds
99.65% <= 10 milliseconds
99.87% <= 11 milliseconds
99.99% <= 12 milliseconds
100.00% <= 12 milliseconds
60240.96 requests per second

====== PING_BULK ======
100000 requests completed in 1.53 seconds
50 parallel clients
3 bytes payload
keep alive: 1

99.25% <= 1 milliseconds
99.94% <= 2 milliseconds
100.00% <= 2 milliseconds
65146.58 requests per second

====== SET ======
100000 requests completed in 1.63 seconds
50 parallel clients
3 bytes payload
keep alive: 1

99.24% <= 1 milliseconds
99.51% <= 2 milliseconds
99.64% <= 3 milliseconds
99.65% <= 5 milliseconds
99.65% <= 6 milliseconds
99.70% <= 10 milliseconds
99.87% <= 11 milliseconds
100.00% <= 11 milliseconds
61462.82 requests per second

====== GET ======
100000 requests completed in 1.65 seconds
50 parallel clients
3 bytes payload
keep alive: 1

98.37% <= 1 milliseconds
99.40% <= 2 milliseconds
99.55% <= 8 milliseconds
99.60% <= 10 milliseconds
99.72% <= 11 milliseconds
99.98% <= 12 milliseconds
100.00% <= 12 milliseconds
60422.96 requests per second

######## 省略 ###########

====== SADD ======
100000 requests completed in 1.58 seconds
50 parallel clients
3 bytes payload
keep alive: 1

99.15% <= 1 milliseconds
99.86% <= 2 milliseconds
99.90% <= 12 milliseconds
99.95% <= 16 milliseconds
99.99% <= 17 milliseconds
100.00% <= 17 milliseconds
63371.36 requests per second

====== HSET ======
100000 requests completed in 1.65 seconds
50 parallel clients
3 bytes payload
keep alive: 1

98.95% <= 1 milliseconds
99.54% <= 2 milliseconds
99.65% <= 5 milliseconds
99.67% <= 6 milliseconds
99.70% <= 10 milliseconds
99.91% <= 11 milliseconds
100.00% <= 11 milliseconds
60642.81 requests per second
######## 省略 ###########

[root@RLKJ-BT ~]#

如上,快速测试出的结果非常全,经过删减还有很多,我们比较关注的就是 100%的时延和 QPS。从上面的结果中我们大致能够得出被测 redis 的时延都在十几毫秒,QPS 都在 6W 多,性能还是很好的。

精简测试

对于快速测试的结果,我们需要去分析挑选那我们需要的数据,其实 redis-benchmark 已经为我们提供了精简测试模式,我们是同-t 参数指定需要测试的操作类型就可以实现对指定操作的性能进行测试。

例:对 set 和 get 进行 1000000 个请求的性能测试。

1
2
3
4
5
sql复制代码[root@RLKJ-BT ~]# redis-benchmark -t set,get -n 1000000 -q
SET: 62774.64 requests per second
GET: 63195.14 requests per second

[root@RLKJ-BT ~]#

如上,我们已经拿到了非常精简的数据,我们可以直接将其放到我们的测试报告中。

Pipline 测试

针对业务场景,我们可以通过 pipline 来模拟业务场景,批量提交命令给 redis server,从而提升性能。

例:我们模拟每个 pipline 执行 10 次命令。

1
2
3
4
5
sql复制代码[root@RLKJ-BT ~]# redis-benchmark -t set,get -n 1000000 -q -P 10
SET: 446229.38 requests per second
GET: 450450.44 requests per second

[root@RLKJ-BT ~]#

同样的请求数,你会发现单次执行多条命令的性能数据要比单次执行一条命令的性能数据高出 7 倍多。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

拦截器的骚操作

发表于 2021-11-20

[TOC]

GitHub:github.com/nateshao/ss…

欢迎关注千羽的公众号

  1. 拦截器概述

什么是拦截器?

​ Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

​ 要使用Spring MVC中的拦截器,就需要对拦截器类进行定义和配置。通常拦截器类可以通过两种方式来定义。

  1. 第一种:通过实现HandlerInterceptor接口,或继承HandlerInterceptor接口的实现类(如HandlerInterceptorAdapter)来定义。
  2. 第二种:通过实现WebRequestInterceptor接口,或继承WebRequestInterceptor接口的实现类来定义。

以实现HandlerInterceptor接口方式为例,自定义拦截器类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class CustomInterceptor implements HandlerInterceptor {
/**
* 该方法会在控制器方法前执行,其返回值表示是否中断后续操作。
* 当其返回值为true时,表示继续向下执行;
* 当其返回值为false时,会中断后续的所有操作。
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("CustomInterceptor...preHandle");
//对拦截的请求进行放行处理
return true;
}

/**
* 该方法会在控制器方法调用之后,且解析视图之前执行。
* 可以通过此方法对请求域中的模型和视图做出进一步的修改。
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("CustomInterceptor...postHandle");
}

/**
* 该方法会在整个请求完成,即视图渲染结束之后执行。
* 可以通过此方法实现一些资源清理、记录日志信息等工作。
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception ex) throws Exception {
System.out.println("CustomInterceptor...afterCompletion");
}
}

要使自定义的拦截器类生效,还需要在Spring MVC的配置文件中进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<mvc:interceptors>
<!-- 全局拦截器,拦截所有请求 -->
<bean class="com.nateshao.interceptor.CustomInterceptor"/>//

<mvc:interceptor>
<!-- **配置,表示拦截所有路径 -->
<mvc:mapping path="/**"/>
<!-- 配置不需要拦截的路径 -->
<mvc:exclude-mapping path=""/>

<bean class="com.nateshao.interceptor.Interceptor1"/>
</mvc:interceptor>
<mvc:interceptor>
<!-- /hello表示拦截所有以“/hello”结尾的路径 -->
<mvc:mapping path="/hello"/>

<bean class="com.nateshao.interceptor.Interceptor2"/>
</mvc:interceptor>
...
</mvc:interceptors>

注意:< mvc:interceptor >中的子元素必须按照上述代码的配置顺序进行编写,否则文件会报错。

  1. 拦截器的执行流程

在运行程序时,拦截器的执行是有一定顺序的,该顺序与配置文件中所定义的拦截器的顺序相关。

单个拦截器,在程序中的执行流程如下图所示:

多个拦截器的执行流程

多个拦截器(假设有两个拦截器Interceptor1和Interceptor2,并且在配置文件中, Interceptor1拦截器配置在前),在程序中的执行流程如下图所示:

  1. 应用案例

案例说明 : 实现用户登录权限验证

案例中,只有登录后的用户才能访问系统中的主页面,如果没有登录系统而直接访问主页面,则拦截器会将请求拦截,并转发到登录页面,同时在登录页面中给出提示信息。如果用户名或密码错误,也会在登录页面给出相应的提示信息。当已登录的用户在系统主页中单击“退出”链接时,系统同样会回到登录页面。

login.jsp

1
2
3
4
5
6
7
8
9
10
11
12
jsp复制代码<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>系统主页</title>
</head>
<body>
当前用户:${USER_SESSION.username}
<a href="${pageContext.request.contextPath }/logout">退出</a>
</body>
</html>

LoginInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码package com.nateshao.interceptor;

import com.nateshao.po.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
* @date Created by 邵桐杰 on 2021/10/22 12:50
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 获取请求的URL
String url = request.getRequestURI();
// URL:除了login.jsp是可以公开访问的,其它的URL都进行拦截控制
if (url.indexOf("/login") >= 0) {
return true;
}
// 获取Session
HttpSession session = request.getSession();
User user = (User) session.getAttribute("USER_SESSION");
// 判断Session中是否有用户数据,如果有,则返回true,继续向下执行
if (user != null) {
return true;
}
// 不符合条件的给出提示信息,并转发到登录页面
request.setAttribute("msg", "您还没有登录,请先登录!");
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp")
.forward(request, response);
return false;
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}

UserController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码package com.nateshao.controller;

import com.nateshao.po.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpSession;

/**
* @date Created by 邵桐杰 on 2021/10/22 12:47
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description:
*/
@Controller
public class UserController {
/**
* 向用户登录页面跳转
*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String toLogin() {
return "login";
}

/**
* 用户登录
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(User user, Model model, HttpSession session) {
// 获取用户名和密码
String username = user.getUsername();
String password = user.getPassword();
// 此处模拟从数据库中获取用户名和密码后进行判断
if (username != null && username.equals("nateshao")
&& password != null && password.equals("123456")) {
// 将用户对象添加到Session
session.setAttribute("USER_SESSION", user);
// 重定向到主页面的跳转方法
return "redirect:main";
}
model.addAttribute("msg", "用户名或密码错误,请重新登录!");
return "login";
}

/**
* 向用户主页面跳转
*/
@RequestMapping(value = "/main")
public String toMain() {
return "main";
}

/**
* 退出登录
*/
@RequestMapping(value = "/logout")
public String logout(HttpSession session) {
// 清除Session
session.invalidate();
// 重定向到登录页面的跳转方法
return "redirect:login";
}
}

main.jsp

1
2
3
4
5
6
7
8
9
10
11
12
jsp复制代码<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>系统主页</title>
</head>
<body>
当前用户:${USER_SESSION.username}
<a href="${pageContext.request.contextPath }/logout">退出</a>
</body>
</html>

验证

浏览器输入:http://localhost:8080/110_springmvc_interceptor_war_exploded/main

输入用户名密码

总结

这一篇文章主要对Spring MVC中的拦截器使用进行了详细讲解。

  1. 首先介绍了如何在Spring MVC项目中定义和配置拦截器,
  2. 然后详细讲解了单个拦截器和多个拦截器的执行流程,
  3. 最后通过一个用户登录权限验证的应用案例演示了拦截器的实际应用。

最后我们可以对Spring MVC中拦截器的定义和配置方式有一定的了解,能够熟悉拦截器的执行流程,并能够掌握拦截器的使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hystrix+Hystrix Dashboard+Turb

发表于 2021-11-20

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」

1、简介

Hystrix Dashboard虽然好用,但是它有一个缺点:一个Hystrix Dashboard只能收集一个微服务的Hystrix流。也就是说对于每个微服务,我们都需要开启一个Hystrix Dashboard来监控其健康情况。可以看到如下Hystrix Dashboard只能输入一个actuator端点地址。

这能忍?反正我是忍不了。

忍不了我们就可以使用Turbine;Netfilx的Turbine项目,提供了将多个微服务的Hystrix流数据聚合到一个流中,并且通过一个Hystrix Dashboard进行展示,这样就可以很nice的同时监控多个微服务的健康状况啦!

Turbine项目的大致架构图如下所示:

没有Turbine之前,每个微服务都需要开启一个Hystrix Dashboard页面来监控当前微服务的健康情况,有了Turbine之后,多个微服务的信息先通过Turbine进行聚合,再统一在一个Hystrix Dashboard页面展示。

2、正文

2.1 快速搭建

服务列表:

我这里一共搭建了六个服务,其中eureka-server为注册中心,turbine-server为turbine服务,hystrix-dashboard-server为hystrix-dashboard服务(这些服务如果有需要你也可以在一个服务中启动),user-server、order-server、message-server三个服务是受hystrix保护的服务。

1
2
3
4
5
6
7
8
xml复制代码<modules>
<module>eureka-server</module>
<module>turbine-server</module>
<module>hystrix-dashboard-server</module>
<module>user-server</module>
<module>order-server</module>
<module>message-server</module>
</modules>

服务依赖:

所有的依赖和版本均在下面,自行在指定服务中选择需要的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

<properties>
<spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
</properties>

<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--turbine-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
<!--hystrix-dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
</dependencies>


<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

eureka-server服务搭建:

application.yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码server:
port: 4010

spring:
application:
name: eureka-server

eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka

服务启动类

1
2
3
4
5
6
7
8
9
less复制代码@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApp {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApp.class, args);
}

}

user-server、order-server、message-server服务搭建:

user-server服务application.yml配置文件(其他两个相似)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码server:
port: 22222

spring:
application:
name: user-server

eureka:
client:
service-url:
defaultZone: http://localhost:4010/eureka

## 开启hystrix.stream端点
management:
endpoints:
web:
exposure:
include: 'hystrix.stream'

user-server服务启动类(其他两个相似)

1
2
3
4
5
6
7
8
9
10
11
less复制代码@SpringBootApplication
@EnableEurekaClient
// 开启Hystrix
@EnableHystrix
public class UserServerApp {

public static void main(String[] args) {
SpringApplication.run(UserServerApp.class, args);
}

}

user-server编写受hystrix保护控制器(其他两个相似)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码@RestController
@RequestMapping(value = "/user")
public class UserController {

@GetMapping
@HystrixCommand(fallbackMethod = "fallback")
public String userDemo() {
return "user-98";
}

// 写两个方法是为了演示方法名称不同,hystrix dashboard会创建不同的circuit
@GetMapping("/demo1")
@HystrixCommand(fallbackMethod = "fallback")
public String userDemo1() {
return "user-98";
}

private String fallback() {
return "user-NaN";
}

}

turbine-server服务搭建:

application.yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码server:
port: 11111

spring:
application:
name: turbine-server

eureka:
client:
service-url:
defaultZone: http://localhost:4010/eureka

turbine:
## eureka中服务名称列表
app-config: order-server,user-server,message-server
## eureka集群名称,默认为default
cluster-name-expression: "'default'"
## 开启主机+端口组合聚合服务,默认情况下turbine会将统一主机下的服务聚合成一个服务来统计
combine-host-port: true

启动类,添加@EnableTurbine启动turbine

1
2
3
4
5
6
7
8
9
10
less复制代码@SpringBootApplication
// 开启turbine
@EnableTurbine
public class TurbineServerApp {

public static void main(String[] args) {
SpringApplication.run(TurbineServerApp.class, args);
}

}

hystrix-dashboard-server服务搭建:

application.yml配置文件

1
2
3
4
5
6
7
8
9
yaml复制代码server:
port: 55555

eureka:
client:
service-url:
defaultZone: http://localhost:4010/eureka
## 不注册
register-with-eureka: false

启动类,添加@EnableHystrixDashboard启动HystrixDashboard

1
2
3
4
5
6
7
8
9
less复制代码@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardServerApp {

public static void main(String[] args) {
SpringApplication.run(HystrixDashboardServerApp.class, args);
}

}

2.2 服务启动

服务启动应先启动注册中心eureka-server再启动user-server、order-server、message-server服务,最后启动turbine-server和hystrix-dashboard-server。

启动完成之后,先查看eureka-server注册中心上服务是否注册正常

http://localhost:4010/

之后访问hystrix-dashboard-server服务的hystrix端点,确保看到如下hystrix dashboard界面

http://localhost:55555/hystrix

在hystrix dashboard中输入turbine-server提供的turbine.stream端点

http://localhost:11111/turbine.stream

初始进入的时候由于各个受hystrix保护的方法并未调用,因此未上报任何数据,所以需要调用各个接口触发数据上报。

之后看到如下界面,说明服务启动成功,整个监控服务整合完毕

2.3 注意事项

hystrix dashboard中展示的circuit数据,会根据方法名来创建,因此不管是不是同一个服务中,只要受hystrix保护的方法,如果方法名相同,将会被聚合到一起展示,这里指的注意哦!

此外不要理解成一个circuit会对应一个thread pools

  • circuit的个数与方法名个数相同
  • thread pools每一个受hystrix保护的方法所在类会创建一个

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…268269270…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%