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

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


  • 首页

  • 归档

  • 搜索

持续发烧,试试Dart语言的异步操作,效率提升500%

发表于 2021-06-29

前言

昨天发了篇文章《Dart开发服务端,我是不是发烧(骚)了》,承蒙小编看得起上了首页。

今天持续发烧,再来写写如何使用 Dart 语言的异步操作。说起异步操作,玩 NodeJS 的同学会心一笑,这就是我们的看家本领啊。玩 PHP, JAVA 的同学也就看看,表示我们光看不说话。

代码演示之前,我们先假设一个场景。假设我有一些漂亮妹妹,我别出心裁的想发电子邮件给他们,表达爱心。在这里个过程中,代码需要做的事情:

  1. 接收请求
  2. 保存我的邮件内容到数据库
  3. 还需要把邮件内容发送到她们的邮箱。
  4. 返回结果

这个过程中,我关心的是发一封邮件需要多长时间,因为我妹妹太多了,一封邮件的时间太长的话,我就没办法照顾到其他人了。那上面4个步骤里,哪些步骤会耗时呢?

很显然,1 和 4 基本绝对不耗时,2 需要点时间,但是时间很短,3 需要的时间最长,因为涉及到网络传输,不可控因素太多。

同步代码是什么样的

我们先用同步代码的方式来模拟上面的流程。

假设保存信息到数据库需要 1 秒,发送邮件到对方邮箱需要 5 秒,总体应该是 6 点多。

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
dart复制代码import 'dart:io';

main() {
acceptRequest(); //接受请求
saveToDb(); //保存到数据库,不太耗时, 假设需要1秒
sendLetter(); //发送邮件到对方邮箱,非常耗时, 假设需要5秒
returnRes(); //返回结果
}

void acceptRequest() {
print(DateTime.now().toString() + ' 接受请求');
}

void saveToDb() {
sleep(Duration(seconds: 1));
print(DateTime.now().toString() + ' 保存数据库成功');
}

void sendLetter() {
sleep(Duration(seconds: 5));
print(DateTime.now().toString() + ' 发送邮件成功');
}

void returnRes() {
print(DateTime.now().toString() + ' 返回结果');
}

执行它,得到打印的结果

1
2
3
4
bash复制代码2021-06-29 16:40:44.993785 接受请求
2021-06-29 16:40:46.000240 保存数据库成功
2021-06-29 16:40:51.002400 发送邮件成功
2021-06-29 16:40:51.002400 返回结果

简单计算一下,从接受请求到返回结果,总共耗时 6 秒左右,符合预期。

异步代码又是什么样子

刚才说了,我有好多漂亮妹妹,则一封邮件都要那么长时间,那么多妹妹得多长时间啊,能不能快点呢?

当然可以了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dart复制代码main() async {
acceptRequest(); //接受请求
await saveToDb(); //保存到数据库,不太耗时, 需要1秒
sendLetter(); //发送邮件到对方邮箱,非常耗时, 需要5秒
returnRes(); //返回结果
}

void acceptRequest() {
print(DateTime.now().toString() + ' 接受请求');
}

void saveToDb() async {
await Future.delayed(Duration(seconds: 1));
print(DateTime.now().toString() + ' 保存数据库成功');
}

void sendLetter() async {
await Future.delayed(Duration(seconds: 5));
print(DateTime.now().toString() + ' 发送邮件成功');
}

void returnRes() {
print(DateTime.now().toString() + ' 返回结果');
}

执行它,得到打印结果

1
2
3
4
bash复制代码2021-06-29 16:47:46.931323 接受请求
2021-06-29 16:47:47.956545 保存数据库成功
2021-06-29 16:47:47.959539 返回结果
2021-06-29 16:47:52.960542 发送邮件成功

这个结果,可需要注意看了。有两点需要特别注意:

  1. 从接收请求到返回结果,总共消耗了1秒左右
  2. 发送邮件成功,竟然出现在返回结果得后面,间隔5秒

为什么是这样? 实际上这就是 Dart语言异步操作得魅力所在。

Dart默认情况下是按照代码顺序来执行任务。

但是当执行到 sendLetter 得时候,发现它是一个 async 异步的操作,并且暂时不用等待它,然后就直接跳过他,执行了后面 returnRes ,因此打印出了 返回结果

返回结果之后,如果是浏览器请求的话,那么这个浏览器请求就直接结束了。但是事情并没有结束,Dart继续执行了刚刚跳过的 sendLetter, 所以最后打印出了 发送邮件成功

整体下来,我这次发邮件,只用了 1 秒钟,而之前是 6 秒啊,这个效率提升,足足有 500%

嗯嗯,真是太棒了,可以照顾到更多妹妹了。

await async 究竟是个啥

眼尖的同学估计看出来了,上面的代码中

1
2
3
4
5
6
dart复制代码main() async {
acceptRequest(); //接受请求
await saveToDb(); //保存到数据库,不太耗时, 需要1秒
sendLetter(); //发送邮件到对方邮箱,非常耗时, 需要5秒
returnRes(); //返回结果
}

saveToDb 保存数据库 与 sendLetter 发送油价都是耗时操作,为什么 saveToDb 前面加了 await ?

这是因为, saveToDb 也是异步操作,如果不加 await ,它就会像 sendLetter 发送邮件一样,先被跳过,浏览器返回结果后,才被执行。这样会产生一个问题,如果写入数据库失败了,但是你已经告诉用户成功了,这不尴尬了吗?

所以, saveToDb 前面加了 await, 告诉 Dart 这段代码虽然是异步的,你要同步执行。

总结

当一个操作非常耗时的话,我们就可以将其设置成异步 async,先给用户返回信息,再慢慢处理。

如果想把某异步操作变为同步的话, 可以加关键字 await, 表示我愿意等待这个异步结果。

Dart 提供了异步操作的机制,我们可以很方便的来使用他们。

玩 NodeJS 的哭了,看家本领被人给偷了。

本文转载自: 掘金

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

微服务实战Go Micro v3 系列(五)- 注册和配置中

发表于 2021-06-29

这一篇就来讲讲,go-micro v3 如何进行配置consul注册中心和操作配置中心

源码地址

  • 源码地址
  • 爱租房微服务综合项目

前言

go-micro框架为服务注册发现提供了标准的接口Registry。只要实现这个接口就可以定制自己的服务注册和发现。不过官方已经为主流注册中心提供了官方的接口实现,大多数时候我们不需要从头写起。

代码仓库

示例代码

Docker 安装 Consul

这里使用 consul 演示,因为consul自带UI界面,方便操作

1
bash复制代码docker pull consul # 默认拉取latest

运行单机版consul

1
ini复制代码docker run -d -p 8500:8500 --restart=always --name=consul consul:latest agent -server -bootstrap -ui -node=1 -client='0.0.0.0'

参数解释如下:

  • agent: 表示启动 Agent 进程。
  • server:表示启动 Consul Server 模式
  • client:表示启动 Consul Client 模式。
  • bootstrap:表示这个节点是 Server-Leader ,每个数据中心只能运行一台服务器。技术角度上讲 Leader 是通过 Raft 算法选举的,但是集群第一次启动时需要一个引导 Leader,在引导群集后,建议不要使用此标志。
  • ui:表示启动 Web UI 管理器,默认开放端口 8500,所以上面使用 Docker 命令把 8500 端口对外开放。
  • node:节点的名称,集群中必须是唯一的,默认是该节点的主机名。
  • client:consul服务侦听地址,这个地址提供HTTP、DNS、RPC等服务,默认是127.0.0.1所以不对外提供服务,如果你要对外提供服务改成0.0.0.0
  • join:表示加入到某一个集群中去。 如:-json=192.168.0.11。

运行成功后,访问 http://localhost:8500
就可以到consul自带的UI界面了,如图:

关键代码

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
go复制代码package main

import (
"github.com/asim/go-micro/plugins/registry/consul/v3"
"github.com/asim/go-micro/v3"
"github.com/asim/go-micro/v3/logger"
"github.com/asim/go-micro/v3/registry"
"go-micro-examples/registerConfiguration/handler"
pb "go-micro-examples/registerConfiguration/proto"
)

func main() {
// Register consul
reg := consul.NewRegistry(func(options *registry.Options) {
options.Addrs =[]string{"127.0.0.1:8500"}
})

// Create service
srv := micro.NewService(
micro.Name("go.micro.srv.registerconfiguration"),
micro.Version("latest"),
// 注册consul中心
micro.Registry(reg),
)

// Register handler
if err := pb.RegisterRegisterConfigurationHandler(srv.Server(), new(handler.RegisterConfiguration)); err != nil {
logger.Fatal(err)
}

// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

go-micro v3 提供 plugins,只需要引入并创建实例之后,使用 micro.Registry 注册即可

运行后效果图如下:

配置中心

点击 Key/Value 创建目录 micro/config,然后在config目录分别创建 mysql、redis、logger、server 四个目录,如下图所示:

以其中mysql为例,输入一下信息:

1
2
3
4
5
6
7
json复制代码{
"host": "192.168.0.65", // 主机地址
"user": "root", // 用户名
"pwd": "123456", // 密码
"database": "go-shop-b2b2c", // 数据库
"port": 3306 // 端口
}

然后在 registerConfiguration 创建 config 目录,并创建 config.go、mysql.go文件,分别编写其中代码

config.go

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
go复制代码package config

import (
"github.com/asim/go-micro/plugins/config/source/consul/v3"
"github.com/asim/go-micro/v3/config"
"strconv"
)

const (
Host = "192.168.0.65"
Port = 8500
Prefix = "/micro/config"
)

// GetConsulConfig 设置配置中心
func GetConsulConfig() (config.Config, error) {
//添加配置中心
//配置中心使用consul key/value 模式
consulSource := consul.NewSource(
//设置配置中心地址
consul.WithAddress(Host+":"+strconv.FormatInt(Port, 10)),
//设置前缀,不设置默认为 /micro/config
consul.WithPrefix(Prefix),
//是否移除前缀,这里设置为true 表示可以不带前缀直接获取对应配置
consul.StripPrefix(true),
)
//配置初始化
conf, err := config.NewConfig()
if err != nil {
return conf, err
}
//加载配置
err = conf.Load(consulSource)
return conf, err
}

mysql.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package config

import "github.com/asim/go-micro/v3/config"

// MysqlConfig 创建结构体
type MysqlConfig struct {
Host string `json:"host"`
User string `json:"user"`
Pwd string `json:"pwd"`
Database string `json:"database"`
Port int64 `json:"port"`
}

// GetMysqlFromConsul 获取mysql的配置
func GetMysqlFromConsul(config config.Config, path ...string) (*MysqlConfig, error) {
mysqlConfig := &MysqlConfig{}
//获取配置
err := config.Get(path...).Scan(mysqlConfig)
if err != nil {
return nil, err
}
return mysqlConfig, nil
}

main.go

在启动服务之前就可以获取配置中心的配置信息,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// 配置中心
consulConfig, err := config.GetConsulConfig("127.0.0.1", 8500, "/micro/config")
if err != nil {
logger.Fatal(err)
}

// Mysql配置信息
mysqlInfo, err := config.GetMysqlFromConsul(consulConfig, "mysql")
if err != nil {
logger.Fatal(err)
}

logger.Info("Mysql配置信息:", mysqlInfo)

运行后入如下图,可以看到,已经成功获取刚才输入的配置信息:

本文转载自: 掘金

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

从 LiveData 迁移到 Kotlin 数据流

发表于 2021-06-29

LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了。

DeadData?

LiveData 对于 Java 开发者、初学者或是一些简单场景而言仍是可行的解决方案。而对于一些其他的场景,更好的选择是使用 Kotlin 数据流 (Kotlin Flow)。虽说数据流 (相较 LiveData) 有更陡峭的学习曲线,但由于它是 JetBrains 力挺的 Kotlin 语言的一部分,且 Jetpack Compose 正式版即将发布,故两者配合更能发挥出 Kotlin 数据流中响应式模型的潜力。

此前一段时间,我们探讨了 如何使用 Kotlin 数据流 来连接您的应用当中除了视图和 View Model 以外的其他部分。而现在我们有了 一种更安全的方式来从 Android 的界面中获得数据流,已经可以创作一份完整的迁移指南了。

在这篇文章中,您将学到如何把数据流暴露给视图、如何收集数据流,以及如何通过调优来适应不同的需求。

数据流: 把简单复杂化,又把复杂变简单

LiveData 就做了一件事并且做得不错: 它在 缓存最新的数据 和感知 Android 中的生命周期的同时将数据暴露了出来。稍后我们会了解到 LiveData 还可以 启动协程 和 创建复杂的数据转换,这可能会需要花点时间。

接下来我们一起比较 LiveData 和 Kotlin 数据流中相对应的写法吧:

#1: 使用可变数据存储器暴露一次性操作的结果

这是一个经典的操作模式,其中您会使用协程的结果来改变状态容器:

△ 将一次性操作的结果暴露给可变的数据容器 (LiveData)

△ 将一次性操作的结果暴露给可变的数据容器 (LiveData)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Kotlin复制代码<!-- Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 -->

class MyViewModel {
private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
val myUiState: LiveData<Result<UiState>> = _myUiState

// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}

如果要在 Kotlin 数据流中执行相同的操作,我们需要使用 (可变的) StateFlow (状态容器式可观察数据流):

△ 使用可变数据存储器 (StateFlow) 暴露一次性操作的结果

△ 使用可变数据存储器 (StateFlow) 暴露一次性操作的结果

1
2
3
4
5
6
7
8
9
10
11
12
Kotlin复制代码class MyViewModel {
private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
val myUiState: StateFlow<Result<UiState>> = _myUiState

// 从挂起函数和可变状态中加载数据
init {
viewModelScope.launch {
val result = ...
_myUiState.value = result
}
}
}

StateFlow 是 SharedFlow 的一个比较特殊的变种,而 SharedFlow 又是 Kotlin 数据流当中比较特殊的一种类型。StateFlow 与 LiveData 是最接近的,因为:

  • 它始终是有值的。
  • 它的值是唯一的。
  • 它允许被多个观察者共用 (因此是共享的数据流)。
  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。

当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。

#2: 把一次性操作的结果暴露出来

这个例子与上面代码片段的效果一致,只是这里暴露协程调用的结果而无需使用可变属性。

如果使用 LiveData,我们需要使用 LiveData 协程构建器:

△ 把一次性操作的结果暴露出来 (LiveData)

△ 把一次性操作的结果暴露出来 (LiveData)

1
2
3
4
5
6
Kotlin复制代码class MyViewModel(...) : ViewModel() {
val result: LiveData<Result<UiState>> = liveData {
emit(Result.Loading)
emit(repository.fetchItem())
}
}

由于状态容器总是有值的,那么我们就可以通过某种 Result 类来把 UI 状态封装起来,比如加载中、成功、错误等状态。

与之对应的数据流方式则需要您多做一点配置:

△ 把一次性操作的结果暴露出来 (StateFlow)

△ 把一次性操作的结果暴露出来 (StateFlow)

1
2
3
4
5
6
7
8
9
Kotlin复制代码class MyViewModel(...) : ViewModel() {
val result: StateFlow<Result<UiState>> = flow {
emit(repository.fetchItem())
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000), //由于是一次性操作,也可以使用 Lazily
initialValue = Result.Loading
)
}

stateIn 是专门将数据流转换为 StateFlow 的运算符。由于需要通过更复杂的示例才能更好地解释它,所以这里暂且把这些参数放在一边。

#3: 带参数的一次性数据加载

比方说您想要加载一些依赖用户 ID 的数据,而信息来自一个提供数据流的 AuthManager:

△ 带参数的一次性数据加载 (LiveData)

△ 带参数的一次性数据加载 (LiveData)

使用 LiveData 时,您可以用类似这样的代码:

1
2
3
4
5
6
7
8
Kotlin复制代码class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()

val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
liveData { emit(repository.fetchItem(newUserId)) }
}
}

switchMap 是数据变换中的一种,它订阅了 userId 的变化,并且其代码体会在感知到 userId 变化时执行。

如非必须要将 userId 作为 LiveData 使用,那么更好的方案是将流式数据和 Flow 结合,并将最终的结果 (result) 转化为 LiveData。

1
2
3
4
5
6
7
Kotlin复制代码class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.asLiveData()
}

如果改用 Kotlin Flow 来编写,代码其实似曾相识:

△ 带参数的一次性数据加载 (StateFlow)

△ 带参数的一次性数据加载 (StateFlow)

1
2
3
4
5
6
7
8
9
10
11
Kotlin复制代码class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
repository.fetchItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}

假如说您想要更高的灵活性,可以考虑显式调用 transformLatest 和 emit 方法:

1
2
3
4
5
6
7
8
Kotlin复制代码val result = userId.transformLatest { newUserId ->
emit(Result.LoadingData)
emit(repository.fetchItem(newUserId))
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser //注意此处不同的加载状态
)

#4: 观察带参数的数据流

接下来我们让刚才的案例变得更具交互性。数据不再被读取,而是被观察,因此我们对数据源的改动会直接被传递到 UI 界面中。

继续刚才的例子: 我们不再对源数据调用 fetchItem 方法,而是通过假定的 observeItem 方法获取一个 Kotlin 数据流。

若使用 LiveData,可以将数据流转换为 LiveData 实例,然后通过 emitSource 传递数据的变化。

△ 观察带参数的数据流 (LiveData)

△ 观察带参数的数据流 (LiveData)

1
2
3
4
5
6
7
8
Kotlin复制代码class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: LiveData<String?> =
authManager.observeUser().map { user -> user.id }.asLiveData()

val result = userId.switchMap { newUserId ->
repository.observeItem(newUserId).asLiveData()
}
}

或者采用更推荐的方式,把两个流通过 flatMapLatest 结合起来,并且仅将最后的输出转换为 LiveData:

1
2
3
4
5
6
7
8
Kotlin复制代码class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }

val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.asLiveData()
}

使用 Kotlin 数据流的实现方式非常相似,但是省下了 LiveData 的转换过程:

△ 观察带参数的数据流 (StateFlow)

△ 观察带参数的数据流 (StateFlow)

1
2
3
4
5
6
7
8
9
10
11
12
Kotlin复制代码class MyViewModel(authManager..., repository...) : ViewModel() {
private val userId: Flow<String?> =
authManager.observeUser().map { user -> user?.id }

val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.LoadingUser
)
}

每当用户实例变化,或者是存储区 (repository) 中用户的数据发生变化时,上面代码中暴露出来的 StateFlow 都会收到相应的更新信息。

#5: 结合多种源: MediatorLiveData -> Flow.combine

MediatorLiveData 允许您观察一个或多个数据源的变化情况,并根据得到的新数据进行相应的操作。通常可以按照下面的方式更新 MediatorLiveData 的值:

1
2
3
4
5
6
7
8
9
10
11
Kotlin复制代码val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...

val result = MediatorLiveData<Int>()

result.addSource(liveData1) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}

同样的功能使用 Kotlin 数据流来操作会更加直接:

1
2
3
4
Kotlin复制代码val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...

val result = combine(flow1, flow2) { a, b -> a + b }

此处也可以使用 combineTransform 或者 zip 函数。

通过 stateIn 配置对外暴露的 StateFlow

早前我们使用 stateIn 中间运算符来把普通的流转换成 StateFlow,但转换之后还需要一些配置工作。如果现在不想了解太多细节,只是想知道怎么用,那么可以使用下面的推荐配置:

1
2
3
4
5
6
Kotlin复制代码val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)

不过,如果您想知道为什么会使用这个看似随机的 5 秒的 started 参数,请继续往下读。

根据文档,stateIn 有三个参数:‍

1
2
3
4
5
6
7
less复制代码@param scope 共享开始时所在的协程作用域范围

@param started 控制共享的开始和结束的策略

@param initialValue 状态流的初始值

当使用 [SharingStarted.WhileSubscribed] 并带有 `replayExpirationMillis` 参数重置状态流时,也会用到 initialValue。

started 接受以下的三个值:

  • Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。
  • Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。
  • WhileSubscribed: 这种情况有些复杂 (后文详聊)。

对于那些只执行一次的操作,您可以使用 Lazily 或者 Eagerly。然而,如果您需要观察其他的流,就应该使用 WhileSubscribed 来实现细微但又重要的优化工作,参见后文的解答。

WhileSubscribed 策略

WhileSubscribed 策略会在没有收集器的情况下取消上游数据流。通过 stateIn 运算符创建的 StateFlow 会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。

WhileSubscribed 接受两个参数:

1
2
3
4
Kotlin复制代码public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)

超时停止

根据其文档:

stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止)。

这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。

liveData 协程构建器所使用的方法是 添加一个 5 秒钟的延迟,即如果等待 5 秒后仍然没有订阅者存在就终止协程。前文代码中的 WhileSubscribed (5000) 正是实现这样的功能:

1
2
3
4
5
6
7
8
9
Kotlin复制代码class MyViewModel(...) : ViewModel() {
val result = userId.mapLatest { newUserId ->
repository.observeItem(newUserId)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)
}

这种方法会在以下场景得到体现:

  • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
  • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。

数据重现的过期时间

如果用户离开应用太久,此时您不想让用户看到陈旧的数据,并且希望显示数据正在加载中,那么就应该在 WhileSubscribed 策略中使用 replayExpirationMillis 参数。在这种情况下此参数非常适合,由于缓存的数据都恢复成了 stateIn 中定义的初始值,因此可以有效节省内存。虽然用户切回应用时可能没那么快显示有效数据,但至少不会把过期的信息显示出来。

replayExpirationMillis 配置了以毫秒为单位的延迟时间,定义了从停止共享协程到重置缓存 (恢复到 stateIn 运算符中定义的初始值 initialValue) 所需要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE (表示永远不将其重置)。如果设置为 0,可以在符合条件时立即重置缓存的数据。

从视图中观察 StateFlow

我们此前已经谈到,ViewModel 中的 StateFlow 需要知道它们已经不再需要监听。然而,当所有的这些内容都与生命周期 (lifecycle) 结合起来,事情就没那么简单了。

要收集一个数据流,就需要用到协程。Activity 和 Fragment 提供了若干协程构建器:

  • Activity.lifecycleScope.launch : 立即启动协程,并且在本 Activity 销毁时结束协程。
  • Fragment.lifecycleScope.launch : 立即启动协程,并且在本 Fragment 销毁时结束协程。
  • Fragment.viewLifecycleOwner.lifecycleScope.launch : 立即启动协程,并且在本 Fragment 中的视图生命周期结束时取消协程。

LaunchWhenStarted 和 LaunchWhenResumed

对于一个状态 X,有专门的 launch 方法称为 launchWhenX。它会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。对此,需要注意对应的协程只有在它们的生命周期所有者被销毁时才会被取消。

△ 使用 launch/launchWhenX 来收集数据流是不安全的

△ 使用 launch/launchWhenX 来收集数据流是不安全的

当应用在后台运行时接收数据更新可能会引起应用崩溃,但这种情况可以通过将视图的数据流收集操作挂起来解决。然而,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源。

这么说来,目前我们对 StateFlow 所进行的配置都是无用功;不过,现在有了一个新的 API。

lifecycle.repeatOnLifecycle 前来救场

这个新的协程构建器 (自 lifecycle-runtime-ktx 2.4.0-alpha01 后可用) 恰好能满足我们的需要: 在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程。

△ 不同数据流收集方法的比较

△ 不同数据流收集方法的比较

比如在某个 Fragment 的代码中:

1
2
3
4
5
6
7
javascript复制代码onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}

当这个 Fragment 处于 STARTED 状态时会开始收集流,并且在 RESUMED 状态时保持收集,最终在 Fragment 进入 STOPPED 状态时结束收集过程。如需获取更多信息,请参阅: 使用更为安全的方式收集 Android UI 数据流。

结合使用 repeatOnLifecycle API 和上面的 StateFlow 示例可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能。

△ 该 StateFlow 通过 WhileSubscribed(5000) 暴露并通过 repeatOnLifecycle(STARTED) 收集

△ 该 StateFlow 通过 WhileSubscribed(5000) 暴露并通过 repeatOnLifecycle(STARTED) 收集

注意: 近期在 Data Binding 中加入的 StateFlow 支持 使用了 launchWhenCreated 来描述收集数据更新,并且它会在进入稳定版后转而使用 repeatOnLifecyle。

对于数据绑定,您应该在各处都使用 Kotlin 数据流并简单地加上 asLiveData() 来把数据暴露给视图。数据绑定会在 lifecycle-runtime-ktx 2.4.0 进入稳定版后更新。

总结

通过 ViewModel 暴露数据,并在视图中获取的最佳方式是:

  • ✔️ 使用带超时参数的 WhileSubscribed 策略暴露 StateFlow。[示例 1]
  • ✔️ 使用 repeatOnLifecycle 来收集数据更新。[示例 2]

如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费:

  • ❌ 通过 WhileSubscribed 暴露 StateFlow,然后在 lifecycleScope.launch/launchWhenX 中收集数据更新。
  • ❌ 通过 Lazily/Eagerly 策略暴露 StateFlow,并在 repeatOnLifecycle 中收集数据更新。

当然,如果您并不需要使用到 Kotlin 数据流的强大功能,就用 LiveData 好了 :)

向 Manuel、Wojtek、Yigit、Alex Cook、Florina 和 Chris 致谢!

本文转载自: 掘金

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

《高可用系列》- 你来说说什么是限流? 这里是重点!!!

发表于 2021-06-29

停更了很久的《面试补习》 ,随着最近的校招来临,也要提上日程了,在梳理八股文的同时,也能加深自己的理解,希望对各位童鞋有所帮助~


概述
–

在最近一期的文章 给几位小朋友面试辅导后,我发现了一些问题! 中,有提到面试中,真的童鞋们的项目经验提出了比较多的问题,也不知道有没有人看 orz


主要列了一下项目中的这些问题:

  • 去理解为什么你要做秒杀系统?
  • 秒杀系统适合什么场景,不适合什么场景
  • 思考你的系统还有哪些欠缺的地方
  • 掌握你系统的每一个点,包括功能,性能,数据流和部署架构
  • 技术选型,为什么你要用 redis ,为什么要用MQ?
  • 技术风险,引用了这些中间件,对你的系统带来的收益和风险
  • 怎么去容灾,怎么监控

今天写的这片关于限流文章,也是属于秒杀系统中的一个关键技术点. 会从: 技术原理,技术选型,使用场景等多方面来介绍,让你在面试中,肆意发挥。

什么是限流

讲一个大家都懂的例子: 三峡大坝排水

  • 三峡水库的存水:可以理解是我们秒杀活动的用户
  • 放闸 : 活动开始
  • 排水 :秒杀成功的用户

如果没有 闸口 在, 受到的影响是啥? 下游的村庄经受洪水灾难,而对应你的系统也是一样的崩溃!

可能大家有疑问,如果我没有做这个蓄水的动作(三峡没有那么多水),我是不是就不需要做限流了呢? 其实不然,我们都知道 三峡解决了多少历史上造成的洪灾问题,这里找了个科普链接。

那对应到我们的秒杀系统上, 我们怎么知道我们的系统会在哪个时间点来一波用户暴增呢?如果这时候你没做好准备,是不是就造成了这批用户的流失?而且系统瘫痪,对存量用户也有影响。双输

1
复制代码我要这铁棒有何用~

所以,限流就是我们系统的定海神针, 让我们的系统风平浪静。

最后再以一批数据来说明一下限流的实际场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码1个商品
1秒内
100个名额
5000个用户
1000个进入下单页面
4000个超时页面

100个下单
900个库存不足

结果:
100个成功下单
4900个抢单失败

限流量: 1000

思考题

1
复制代码求:我这个服务最大并发量多少?

怎么限流

image.png

简单画了个调用链路

H5/客户端 -> Nginx -> Tomcat -> 秒杀系统 -> DB

简单梳理为

  • 网关限流
    • Nginx 限流
    • Tomcat 限流
  • 服务端限流
    • 单机限流
    • 分布式限流

网关限流

Nginx 限流

Nginx自带了两个限流模块:

  • 连接数限流模块 ngx_http_limit_conn_module
  • 漏桶算法实现的请求限流模块 ngx_http_limit_req_module

1、ngx_http_limit_conn_module

主要用于限制脚本攻击,如果我们的秒杀活动开始,一个黑客(假装有,毕竟我们的系统要做大做强!)写了脚本来攻击,会造成我们带宽被浪费,大量无效请求产生,对于这类请求, 我们可以通过对 ip 的连接数进行限制。

我们可以在nginx_conf的http{}中加上如下配置实现限制:

1
2
3
4
5
6
7
8
9
ini复制代码#限制每个用户的并发连接数,取名one
limit_conn_zone $binary_remote_addr zone=one:10m;

#配置异常日志,和状态码
limit_conn_log_level error;
limit_conn_status 503;

# 在server{} 限制用户并发连接数为1
limit_conn one 1;

2、ngx_http_limit_req_module

上面说的 是 ip 的连接数, 那么如果我们要控制请求数呢? 限制的方法是通过使用漏斗算法,每秒固定处理请求数,推迟过多请求。如果请求的频率超过了限制域配置的值,请求处理会被延迟或被丢弃,所以所有的请求都是以定义的频率被处理的。

1
2
3
ini复制代码limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
#设置每个IP桶的数量为5
limit_req zone=one burst=5;

3、怎么理解 连接数,请求数限流

  • 连接数限流(ngx_http_limit_conn_module)
    每个IP,我们只会接待一个,只有当这个IP 处理结束了, 我才会接待下一位。(单位时间内,只有一个连接在处理)

有味道的解读:厕所(IP)限制只有一个坑了,只有当我上完了,才能下一个人上。

  • 请求数限流(ngx_http_limit_req_module)
    通过 漏桶算法 ,按照单位时间放行请求,也不管你服务器能不能处理完,我就放,哎,就是放!

有味道的解读:厕所有五个坑,我一分钟放5个人进去,下一分钟再放5个人进去。 里面可能有5个人,也可能有10个人,我也不清楚。


4、怎么选择?

可能面试官在听到你对 nginx 的限流那么了解后,会问你在什么情况下使用哪种限流策略

  • IP限流:可以在活动开始前进行配置,也可以用于预防脚本攻击(IP代理的情况另说)
  • 请求数限流: 日程可以配置,保护我们的服务器在突发流量造成的崩溃

漏桶算法


漏桶算法的主要概念如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;
  • 如果桶是空的,则不需流出水滴;
  • 可以以任意速率流入水滴到漏桶;
  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

Tomcat 限流

这个其实不太好用,但是也了解一下吧~

可能现在的童鞋,对 Tomcat 也不太了解了,毕竟 SpringBoot 里面封装了 Tomcat ,让开发者越来越懒惰了,但是人类进化,根本原因就是懒,所以也未尝不是一件好事。

在 Tomcat 的配置文件中, 有一个 maxThreads

1
2
xml复制代码<Connector port="8080"   connectionTimeout="30000" protocol="HTTP/1.1"
maxThreads="1000" redirectPort="8000" />

这个好像没啥好介绍的了,如果你碰到你压测的时候,并发上不去,可以检查一下这个配置。

之前面试的时候,面试官有问过我 Tomcat 的问题:

1
2
3
复制代码Tomcat 默认最大连接数是多少?
你们服务器的线程数设置了多少?
线程占用内存是多少?

总结

结合我们的 秒杀系统 ,那么在介绍我们系统的时候,我们可以说,在限流这块,从网关角度,我们可以使用了 Nginx 的 ngx_http_limit_conn_module 模块,针对 IP 在单位时间内只允许一个请求,避免用户多次请求,减轻服务的压力。在进入到订单界面后,在单位时间内,会产生多次请求, 可以使用 ngx_http_limit_req_module 模块,针对请求数做限流,避免由于 IP 限制,导致订单丢失。

除此之外,在服务上线前,我们针对服务器进行了最大并发的压测(如200并发),因此在 Tomcat 允许的最大请求中,设置为(300,稍微上调,有其他请求)。

服务器限流

单机限流

如果我们的系统部署,是只有一台机器,那我们可以直接使用 单机限流的方案(毕竟你一台机器还要用分布式限流,是不是有点过了~)

image.png

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    public static void main(String[] args) throws InterruptedException {
// 每秒产生 1 个令牌
RateLimiter rt = RateLimiter.create(1, 1, TimeUnit.SECONDS);
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());

System.out.println("-------------分隔符-----------------");

}

RateLimiter.tryAcquire() 和 RateLimiter.acquire() 两个方法都通过限流器获取令牌,

1、tryAcquire

支持传入等待时间,通过 canAcquire 判断最早一个生成令牌时间,判断是否进行等待下一个令牌的获取。

1
2
3
4
5
java复制代码public boolean tryAcquire(int permits, long timeout, TimeUnit unit);

private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
java复制代码    public static void main(String[] args) throws InterruptedException {
// 每秒产生 1 个令牌
RateLimiter rt = RateLimiter.create(1, 3, TimeUnit.SECONDS);
System.out.println("try acquire token: " + rt.tryAcquire(1,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(5,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
Thread.sleep(10000);
System.out.println("-------------分隔符-----------------");
System.out.println("try acquire token: " + rt.tryAcquire(1,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(1,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());

}

输出结果:

image.png

2、acquire

acquire 为阻塞等待获取令牌,通过查看源码可以看出同步加锁操作:

image.png

示例代码:

1
2
3
4
5
6
7
8
9
java复制代码        RateLimiter rt = RateLimiter.create(1);
// 每秒产生 1 个令牌
for (int i = 0; i < 11; i++) {
new Thread(() -> {
// 获取 1 个令牌
rt.acquire();
System.out.println("try acquire token success,time:" +System.currentTimeMillis() + " ThreaName:"+Thread.currentThread().getName());
}).start();
}

输出结果:

image.png

令牌算法

上面说到了几个概念, 在nignx 我们提到的是 漏斗算法 ,在 RateLimiter 这里我们提到的是令牌算法

image.png

我们可以通过上面这个图来进行解释,有一个容量有限的桶,令牌以固定的速率添加到这个桶里面。由于桶的容量是有限的,所以不可能无限制的往里面添加令牌,如果令牌到达桶的时候,桶是满的,那么这个令牌就被抛弃了。每次请求,n个数量的令牌从桶里面被移除,如果桶里面的令牌数少于n,那么该请求就会被拒绝或阻塞。

这里有几个关键的属性

1
2
3
4
5
6
java复制代码  /** The currently stored permits. */
double storedPermits; //目前令牌数量

/** The maximum number of stored permits. */
double maxPermits; //最大令牌数量
private long nextFreeTicketMicros = 0L; //下一个令牌获取时间

在获取令牌前,会有一个判断规则,判断当前获取令牌时间,是否满足上一次令牌时间获取 - 生产令牌时间,

比如
:我这次获取令牌时间为 100 秒,令牌生成时间为 10秒 一个,那么当我 105秒过来拿的时候, 不管令牌桶有没有令牌,我都没办法获取到令牌。

1
2
3
java复制代码  private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}

这里是重点!!!

那么令牌桶当中的令牌数量(存量)到底有什么用呢? 针对不同的请求,我们可以设定需要不同数量的令牌,优先级高的,只需要1个令牌即可;优先级低的,则需要多个令牌。 那么当获取令牌时间到了之后, 进行下一层判断,令牌数是否足够, 优先级高的请求(需要令牌数量比较少的),可以马上放行!!!!!


在 RateLimit 中刷新令牌的算法:

1
2
3
4
5
6
7
8
java复制代码  void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}

集群限流

随着我们秒杀系统做大做强,一台机器肯定不能满足我们的诉求了,那么我们的部署架构就会衍生成为下面这个架构图(简版)

image.png

在将集群限流前,提个思考问题:

集群部署我们就不能用单机部署的方案了吗?

答案肯定是可以的, 我们可以将单机限流 的方案拓展到集群每一台机器,那么每天机器都是复用了相同的一套限流代码(RateLimit 实现)。

那么这个方案存在什么问题呢?

  • 流量分配不均
  • 误限,错限
  • 更新不及时

主要讲一下 误限 , 我们服务端接收到的请求,都是有 nginx 进行分发,如果某个时间段,由于请求的分配不均(60,30,10比例分配,限流50qps),会触发第一台机器的限流,而对于集群而言,我的整体限流阀值为 150 qps,现在 100qps 就限流了, 那肯定不行哇!

Redis 实现

参考文档: juejin.cn/post/684490…

我们可以借助 Redis 的有序集合 ZSet 来实现时间窗口算法限流,实现的过程是先使用 ZSet 的 key 存储限流的 ID,score 用来存储请求的时间,每次有请求访问来了之后,先清空之前时间窗口的访问量,统计现在时间窗口的个数和最大允许访问量对比,如果大于等于最大访问量则返回 false 执行限流操作,负责允许执行业务逻辑,并且在 ZSet 中添加一条有效的访问记录。

此实现方式存在的缺点有两个:

  • 使用 ZSet 存储有每次的访问记录,如果数据量比较大时会占用大量的空间,比如 60s 允许 100W 访问时;
  • 此代码的执行非原子操作,先判断后增加,中间空隙可穿插其他业务逻辑的执行,最终导致结果不准确。

限流中间件

Sentinel 是阿里中间件团队研发的面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。

限流中间件的原理是在太有东西了,我这里简单裂了一下他们之间的一些区别,后续会单独写一篇文章来分享 Sentinel 的实现原理! 目前可以比较容易理解的就是,底层是基于滑动窗口的方式实现

image.png

滑动窗口算法


在 Sentinel 和 Hystrix 的底层实现,都是采用了滑动窗口,这里接简单来描述一下什么是滑动窗口,在1S 内, 我允许通过 5个请求, 分别处于 0~200ms,200~400ms以此类推,当时间点来到1.2s 的时候,我们的时间区间变成了 200ms ~ 1200ms。 那么第一个请求,就不在统计的区间范围内了, 我们目前总的 请求数为 4, 因此能够再接受一个新的请求进来处理!

总结

想闲扯一下,在我画的那张图中,我列出了 Hystrix(豪猪),Sentinel(哨兵)和蚂蚁内源的Guardian(守卫)。他们都有一个共性: 保护。豪猪有坚硬的刺保护柔软的身体,哨兵和守卫则保护着身后的家人。

当面试官问你为什么要使用限流的时候, 你应该第一反应就是保护系统,保护系统不受伤害!这才是你为什么要用到限流的各种策略的根本原因。

在讨论到高可用的时候,我们会想到,削峰,限流和熔断。 他们的目标都是为了保护我们的系统,提升系统的可用率,我们常说的系统可用率 几个9,这些数据都是由各种高可用的策略来保护的。


后续的计划:

  • 熔断,结合 Sentinel 的原理来介绍一下,秒杀系统使用熔断的场景
  • 削峰,结合 RocketMQ 讲一下,削峰的优缺点,引入MQ带来的成本和风险

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,我后面会每周都更新几篇高质量的大厂面试和常用技术栈相关的文章。感谢大伙能看到这里,如果这个文章写得还不错, 求三连!!!
创作不易,感谢各位的支持和认可,我们下篇文章见!

我是 九灵 ,有需要交流的童鞋可以 加我wx,Jayce-K,关注公众号:Java 补习课,掌握第一手资料!

如果本篇博客有任何错误,请批评指教,不胜感激 !

本文转载自: 掘金

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

对话面试官-动态代理是如何实现的?JDK Proxy 和 C

发表于 2021-06-29

这是我参与更文挑战的第20天,活动详情查看: 更文挑战


90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。

面试题:如何实现动态代理?JDK Proxy 和 CGLib 有什么区别?

回答

动态代理的常用实现方式是反射。反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。

但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。

JDK Proxy 和 CGLib 的区别主要体现在以下几个方面

  • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;
  • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
  • JDK Proxy 是通过拦截器加反射的方式实现的;
  • JDK Proxy 只能代理继承接口的类;
  • JDK Proxy 实现和调用起来比较简单;
  • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;
  • CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。

考点分析

面试题考察的是对反射、动态代理及 CGLib 的了解,很多人经常会把反射和动态代理划为等号,但从严格意义上来说,这种想法是不正确的,真正能搞懂它们之间的关系,也体现了你扎实 Java 的基本功。和这个问题相关的知识点,还有以下几个:

  • 对 JDK Proxy 和 CGLib 的掌握程度。
  • Lombok 是通过反射实现的吗?
  • 动态代理和静态代理有什么区别?
  • 动态代理的使用场景有哪些?
  • Spring 中的动态代理是通过什么方式实现的?

知识扩展

1.JDK Proxy 和 CGLib 的使用及代码分析

JDK Proxy 动态代理实现

JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示:

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
java复制代码/**
 * JDK Proxy 相关示例
 */
public class ProxyExample {
    static interface Car {
        void running();
    }

    static class Bus implements Car {
        @Override
        public void running() {
            System.out.println("The bus is running.");
        }
    }

    static class Taxi implements Car {
        @Override
        public void running() {
            System.out.println("The taxi is running.");
        }
    }

    /**
     * JDK Proxy
     */
    static class JDKProxy implements InvocationHandler {
        private Object target; // 代理对象

        // 获取到代理对象
        public Object getInstance(Object target) {
            this.target = target;
            // 取得代理对象
            return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(), this);
        }

        /**
         * 执行代理方法
         * @param proxy  代理对象
         * @param method 代理方法
         * @param args   方法的参数
         * @return
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws InvocationTargetException, IllegalAccessException {
            System.out.println("动态代理之前的业务处理.");
            Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理)
            return result;
        }
    }

    public static void main(String[] args) {
        // 执行 JDK Proxy
        JDKProxy jdkProxy = new JDKProxy();
        Car carInstance = (Car) jdkProxy.getInstance(new Taxi());
        carInstance.running();
}
}

以上程序的执行结果是:

1
2
java复制代码动态代理之前的业务处理.
The taxi is running.

可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下:

1
2
3
4
java复制代码public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。

CGLib 的实现

在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置:

1
2
3
4
5
java复制代码<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

CGLib 实现代码如下:

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
java复制代码public class CGLibExample {

    static class Car {
        public void running() {
            System.out.println("The car is running.");
        }
    }

    /**
     * CGLib 代理类
     */
    static class CGLibProxy implements MethodInterceptor {
        private Object target; // 代理对象

        public Object getInstance(Object target) {
            this.target = target;
            Enhancer enhancer = new Enhancer();
            // 设置父类为实例类
            enhancer.setSuperclass(this.target.getClass());
            // 回调方法
            enhancer.setCallback(this);
            // 创建代理对象
            return enhancer.create();
        }

        @Override
        public Object intercept(Object o, Method method,
                                Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("方法调用前业务处理.");
            Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
            return result;
        }
    }

    // 执行 CGLib 的方法调用
    public static void main(String[] args) {
        // 创建 CGLib 代理类
        CGLibProxy proxy = new CGLibProxy();
        // 初始化代理对象
        Car car = (Car) proxy.getInstance(new Car());
        // 执行方法
        car.running();
}
}

以上程序的执行结果是:

1
2
java复制代码方法调用前业务处理.
The car is running.

可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。

2.Lombok 原理分析

在开始讲 Lombok 的原理之前,先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效的解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,向这种方法都可以使用 Lombok 注解来完成。

例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Person {
    private Integer id;
    private String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

在使用 Lombok 之后,代码是这样的:

1
2
3
4
5
java复制代码@Data
public class Person {
private Integer id;
private String name;
}

可以看出 Lombok 让代码简单和优雅了很多。

tips:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示:

RUyZHP.png

接下来讲讲 Lombok 的原理。

Lombok 的实现和反射没有任何关系,前面我们说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢?

回到刚才 Setter/Getter 的方法,当打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码竟然是这样的:

RUymAf.png

可以看出 Lombok 是在编译期就为我们生成了对应的字节码。

其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下:

RUyVBt.png

从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。

3.动态代理知识点扩充

当面试官问动态代理的时候,经常会问到它和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。

动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。

Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-class=”true”/> 即可。

小结

本文介绍了 JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。

除了 JDK Proxy 和 CGLib 之外,还讲了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式,希望本文可以帮助到你。

本文转载自: 掘金

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

效率利器:SpringBoot整合Mybatis-Plus

发表于 2021-06-29

一、什么是Mybatis-Plus

官网地址:baomidou.com/

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

二、简单的使用案例

​ 只需要添加Mybatis-Plus依赖(无需添加mybatis依赖,mybatis-plus的依赖已经包含),编写表映射的实体类和Mapper语句(继承BaseMapper)即可实现对数据库的访问,BaseMapper已经包含了基本的sql方法,无需编写Mapper.xml。

数据库表结构

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`avatar` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `UK_ob8kqyqqgmefl0aco34akdtpe` (`email`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
  1. 引入依赖

  • 引入Springboot Starter 父工程
1
2
3
4
5
6
xml复制代码    <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
  • 引入依赖

只需要引入Spring Boot、MyBatis-Plus、数据库(Mysql)依赖。

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
xml复制代码        <!-- springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
  • 完整依赖文件
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-mybatis-plus</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<!-- springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
</dependencies>
</project>
  1. 实体类

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
arduino复制代码@Data
@Accessors(chain = true)
public class User implements Serializable {
/**
* id
* */
private Integer id;
/**
* 头像
* */
private String avatar;
/**
* 邮箱
* */
private String email;
/**
* 账号
* */
private String name;
/**
* 密码
* */
private String password;
/**
* 用户名
* */
private String username;
}
  1. UserMapper

​ 这里需要继承BaseMapper接口,BaseMapper提供的

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
java复制代码public interface BaseMapper<T> extends Mapper<T> {
//插入
int insert(T entity);

//通过主键删除
int deleteById(Serializable id);
//通过Map字段对应值删除
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
//通过条件Wrapper删除
int delete(@Param("ew") Wrapper<T> wrapper);
//批量删除
int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);

//更新 通过ID匹配
int updateById(@Param("et") T entity);
//更新 通过更新条件匹配
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

//查询通过主键
T selectById(Serializable id);
//查询通过批量
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
//查询通过Map
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
//通过查询条件构造器查询,返回实体
T selectOne(@Param("ew") Wrapper<T> queryWrapper);
//通过查询条件构造器查询行数
Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
//通过查询条件构造器
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);

List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);

List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
//分页查询
<E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);

<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}

​ UserMapper.java基础了BaseMapper,如果启动类使用了MapperScan注解扫描到Mapper所在的路径可以不用使用@Mapper。

1
2
3
4
java复制代码@Mapper
public interface UserMapper extends BaseMapper<User> {

}

​

​ 启动类@MapperScan("com.stopping.mapper")

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@MapperScan("com.stopping.mapper")
public class MybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class,args);
}
}
  1. 配置文件

​ 配置数据源信息

1
2
3
4
5
6
yml复制代码spring:
datasource:
password: root
username: root
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
  1. 测试

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootTest(classes = MybatisPlusApplication.class)
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void selectUser(){
userMapper.selectList(null).stream().forEach(System.out::println);
}
}

结果

1
2
3
less复制代码User(id=1, avatar=null, email=1064076070@qq.com, name=stopping, password=$2a$10$HMoRS.lxhl0mQ1D0uKVeFeMl7nQ1ZykhI/8N3z0AiND1HUMNCZk/y, username=admin)
User(id=2, avatar=null, email=1111@cc.com, name=tom, password=123456, username=tom)
User(id=3, avatar=null, email=1123@cc.com, name=job, password=123456, username=job)

三、代码生成器

​ AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码。

​ 通过数据库链接反向生成数据库映射的实体类,以及 Entity、Mapper、Mapper XML、Service、Controller 。这些生成文件存在在什么路径都是可以通过配置实现的。

  1. 新增依赖

​ MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
  1. 使用代码生成器生成

​ 现在通过代码生成器生成Test数据库中的category表相关的代码

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`user_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `FKpfk8djhv5natgshmxiav6xkpu` (`user_id`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

​ 生成代码在下面,下图是生成后的代码。

image-20210628235919800

  1. 测试

1
2
3
4
5
6
7
8
9
10
11
java复制代码@SpringBootTest(classes = MybatisPlusApplication.class)
class UserMapperTest {
@Resource
private UserMapper userMapper;
@Resource
private CategoryService categoryService;
@Test
public void selectUser(){
categoryService.lambdaQuery().list().forEach(System.out::println);
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码Category(id=2, createTime=null, name=默认, userId=1)
Category(id=3, createTime=2020-08-20T01:05:05, name=计算机视觉, userId=1)
Category(id=4, createTime=2020-08-20T01:05:15, name=Spring, userId=1)
Category(id=5, createTime=2020-08-20T01:05:24, name=Mybatis, userId=1)
Category(id=6, createTime=2020-08-20T01:05:36, name=数据库, userId=1)
Category(id=7, createTime=2020-08-20T01:05:50, name=设计模式, userId=1)
Category(id=8, createTime=2020-08-20T01:13:39, name=代码编辑器, userId=1)
Category(id=9, createTime=2020-08-20T01:21:44, name=服务器, userId=1)
Category(id=10, createTime=2020-08-20T01:23:06, name=Hibernate, userId=1)
Category(id=11, createTime=2020-08-20T01:24:38, name=技术相关, userId=1)
Category(id=12, createTime=2020-08-20T03:16:06, name=前端, userId=1)
Category(id=13, createTime=2020-09-16T03:29:30, name=java, userId=1)

示例代码生成器

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
141
142
java复制代码/**
* @Description GeneratorCode
* @Author stopping
* @date: 2021/6/28 23:33
*/
public class GeneratorCode {
/**
* 数据库连接
* */
private static final String dbUrl = "jdbc:mysql://localhost:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8";
/**
* 数据库账号
* */
private static final String username = "root";
/**
* 数据库密码
* */
private static final String password = "root";
/**
* 模块名
* */
private static final String moduleName = "/spring-mybatis-plus";

/**
* <p>
* 读取控制台内容
* @param
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}

public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
String module = scanner("请输入模块名称");
// 全局配置
GlobalConfig gc = new GlobalConfig();
//D:/code/springboot-orm/spring-mybatis-plus
String projectPath = System.getProperty("user.dir")+moduleName;
System.out.println(projectPath);
//设置文件路径和模块文件生成
gc.setOutputDir(projectPath+"/src/main/java");
gc.setAuthor("stopping");
//生成类名限制
gc.setMapperName("%sMapper");
gc.setServiceName("%sService");
gc.setServiceImplName("%sServiceImp");
gc.setControllerName("%sController");
gc.setXmlName("%sMapper");
gc.setIdType(IdType.AUTO);
gc.setOpen(false);
//是否覆盖
gc.setFileOverride(true);
//实体属性 Swagger2 注解
gc.setSwagger2(false);
mpg.setGlobalConfig(gc);

// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(dbUrl);
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername(username);
dsc.setPassword(password);
mpg.setDataSource(dsc);

// 包配置:生成文件
PackageConfig pc = new PackageConfig();
//包路径
pc.setParent("com.stopping");
//包路径下的子包名称
pc.setMapper("mapper."+module);
pc.setController("controller."+module);
pc.setService("service."+module);
pc.setServiceImpl("service."+module+".imp");
pc.setEntity("model.entity");
pc.setXml("Mapper");
mpg.setPackageInfo(pc);

// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};

// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";

// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
// Mapper 文件输出
String xmlUrl = projectPath + "/src/main/resources/mapper/" + module
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
System.out.println("xml生成路径:"+xmlUrl);
return xmlUrl;
}
});

cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);

// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);

// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 写于父类中的公共字段
//strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
//是否生成注解
strategy.setEntityTableFieldAnnotationEnable(true);
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}

本文转载自: 掘金

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

《蹲坑也能进大厂》多线程系列 - 非公平锁和公平锁详解

发表于 2021-06-28

这是我参与更文挑战的第 17 天,活动详情查看:更文挑战

作者:花Gie

微信公众号:Java开发零到壹

前言

多线程系列我们前面已经更新过很多章节,强烈建议小伙伴按照顺序学习:

《蹲坑也能进大厂》多线程系列文章目录

前一章介绍了悲观锁、乐观锁以及CAS等概念,今天就继续介绍另外的两种常见的锁—-非公平锁和公平锁。

正文

锁的总览

锁可以从不同的角度进行分类,这些分类并不是互斥的,比如一个人既可以是医生,又可以是父亲,也可以是一样女人。分类总览如图:

Java锁分类

公平锁与非公平锁

  • 概念

公平锁:多个线程申请获取锁,会按照先后顺序进入队列,排在队首的线程会率先获取锁。

非公平锁:多个线程申请锁时,首先会尝试获取锁,如果获取不到,再进行排队,如果能够直接获取到锁,那么直接获取。

这里举个生活中的常见例子,这样大家就很容易理解。

小伙伴们应该都去过火车站排队买票吧,现在估计都会在网上直接购票,那排队就是一个很公平的,先到先得,这就是公平锁的案例。

image.png

如果这时有个乘客1买完票了,乘客2正在掏身份证,此时有个乘客4上来插队,问了售票员一句:去铁岭的票还有吗?这个时候就可以看做是非公平锁了,因为乘客4没有按照先来后到而是直接见缝插针,抢先一步,这就是非公平锁的一种案例。

image.png

说完了生活实例,我们接下来看一下这两种锁的原理实现。

这就涉及到我们前面文章经常提到的ReentrantLock,它可实现公平锁和非公平锁两种模式,而ReentrantLock的实现是基于其内部类Sync。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;

/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;

Sync又有两个子类,FairSync(公平锁)和NonFairSync(非公平锁)。

image.png

实现原理

公平锁与非公平锁的原理差距不大,主要是在获取锁时存在差异,公平锁在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁。两种获取锁的方式对应源码如下:

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
java复制代码//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors:用于检查是否有等待队列的。
//有线程来抢占锁的时候,都会检查一遍有没有等待队列
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//无需判断是否有等待队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

从上面两个方法源码可以看到,非公平锁在尝试获取锁时,不会调用hasQueuedPredecessors(判断当前的线程是否位于同步队列的首位,是就返回true,否则返回false)。

1
2
3
4
5
6
7
8
9
10
java复制代码public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

对于非公平锁,只要线程进入了等待队列,队列里面依然是先进先出的原则,和公平锁的顺序是一样的。因为公平锁与非公平锁在释放锁部分代码是共用AQS的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒队列头的线程
LockSupport.unpark(s.thread);
}

代码实现

这里写一个多线程打印日志的案例

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复制代码public class FairLockDemo {

public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
//1.创建5个线程
for (int i = 0; i < 5; i++) {
thread[i] = new Thread(new Job(printQueue));
}
//2.启动这5个线程
for (int i = 0; i < 5; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Job implements Runnable {
PrintQueue printQueue;

public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}

@Override
public void run() {
//3.线程启动完成后,未获取到锁的线程阻塞到printJob前,等待获取锁
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob();
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}

//打印类
class PrintQueue {
private Lock queueLock = new ReentrantLock(false);

public void printJob() {
queueLock.lock();
try {
//4.模拟打印,调用sleep休眠随机秒数
int duration = new Random().nextInt(5) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
//5.当前线程释放锁后,立马再次尝试获取锁
queueLock.lock();
try {
//6.模拟打印,调用sleep休眠随机秒数
int duration = new Random().nextInt(5) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}

根据上面6个步骤,我们分别尝试ReentrantLock实现公平锁和非公平锁模式,会出现什么结果呢。

  • 使用公平锁模式,即ReentrantLock(true),每个线程按照先后顺序,插入到队列中。如下图按照Thread-0、1、2、3、4打印一遍之后,再次按照该顺序打印第二遍,不会因为步骤5出现线程插队现象:

image.png

  • 使用非公平锁模式,即ReentrantLock(false),当一个线程执行完步骤4后,会立即尝试获取锁(步骤5),因此该线程会抢先队列中的线程,从而获取到锁。但是整体又是按照Thread-0、1、2、3、4进行执行,这也符合我们上面对非公平锁的分析:

image.png

总结

上面是公平锁和非公平锁的介绍,文中涉及到AQS、ReentrantLock,可能小伙伴们不是很理解,不过不用担心,这里我们先熟悉概念,这几张介绍锁的概念也是在为后面AQS进行铺垫。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜 Java开发零到壹 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

参考链接:

blog.csdn.net/qq_29479041…

www.imooc.com/article/302…

segmentfault.com/a/119000001…

本文转载自: 掘金

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

微服务实战Go Micro v3 系列(三)-启动HTTP服

发表于 2021-06-28

这篇就是使用 go-micro 的 http 创建一个可以调用接口的微服务HTTP

源码地址

  • 源码地址
  • 爱租房微服务综合项目

httpServer

这里我们使用 gin 框架结合 go-micro 来进行编写

首先 创建一个 http 目录,并在该目录下创建 main.go,写入下面代码

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
go复制代码package main

import (
httpServer "github.com/asim/go-micro/plugins/server/http/v3"
"github.com/asim/go-micro/v3"
"github.com/asim/go-micro/v3/logger"
"github.com/asim/go-micro/v3/registry"
"github.com/asim/go-micro/v3/server"
"github.com/gin-gonic/gin"
"go-micro-examples/http/handler"
)

const (
ServerName = "go.micro.web.DemoHTTP" // server name
)

func main() {
// Create service
srv := httpServer.NewServer(
server.Name(ServerName),
server.Address(":8080"),
)

gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(gin.Recovery())

// register router
demo := handler.NewDemo()
demo.InitRouter(router)

hd := srv.NewHandler(router)
if err := srv.Handle(hd); err != nil {
logger.Fatal(err)
}

// Create service
service := micro.NewService(
micro.Server(srv),
micro.Registry(registry.NewRegistry()),
)
service.Init()

// Run service
if err := service.Run(); err != nil {
logger.Fatal(err)
}
}

使用 gin 进行初始化路由

在 http 目录创建 handler\handler.go

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
go复制代码package handler

import (
"context"
"github.com/asim/go-micro/v3"
"github.com/gin-gonic/gin"
helloworld "go-micro-examples/helloworld/proto"
)

//demo
type demo struct{}

func NewDemo() *demo {
return &demo{}
}

func (a *demo) InitRouter(router *gin.Engine) {
router.POST("/demo", a.demo)
}

func (a *demo) demo(c *gin.Context) {
// create a service
service := micro.NewService()
service.Init()

client := helloworld.NewHelloworldService("go.micro.srv.HelloWorld", service.Client())

rsp, err := client.Call(context.Background(), &helloworld.Request{
Name: "world!",
})
if err != nil {
c.JSON(200, gin.H{"code": 500, "msg": err.Error()})
return
}

c.JSON(200, gin.H{"code": 200, "msg": rsp.Msg})
}

postman测试

在启动两个微服务之后,如下图:

使用 postman 进行测试,调用成功并返回 “hello world!”

本文转载自: 掘金

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

微服务实战Go Micro v3 系列(二)- HelloW

发表于 2021-06-28

首先从我们最最熟悉的 helloworld 例子在入手,对 go-micro 有一个初步的了解

源码地址

  • 源码地址
  • 爱租房微服务综合项目

ProtoBuf

简介

protocol buffers (ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

json\xml都是基于文本格式,protobuf是二进制格式。

你可以通过 ProtoBuf 定义数据结构,然后通过 ProtoBuf 工具生成各种语言版本的数据结构类库,用于操作 ProtoBuf 协议数据

本教程介绍的是最新的protobuf proto3版本的语法。

使用ProtoBuf的例子

创建 .proto 文件,定义数据结构

使用 ProtoBuf ,首先需要通过 ProtoBuf 语法定义数据结构(消息),这些定义好的数据结构保存在.proto为后缀的文件中。

例子:

文件名: response.proto

1
2
3
4
5
6
7
8
ini复制代码// 指定protobuf的版本,proto3是最新的语法版本
syntax = "proto3";

// 定义数据结构,message 你可以想象成java的class,c语言中的struct
message Response {
string data = 1; // 定义一个string类型的字段,字段名字为data, 序号为1
int32 status = 2; // 定义一个int32类型的字段,字段名字为status, 序号为2
}

说明:proto文件中,字段后面的序号,不能重复,定义了就不能修改,可以理解成字段的唯一ID。

安装ProtoBuf编译器

protobuf的github发布地址: github.com/protocolbuf…

protobuf的编译器叫protoc,在上面的网址中找到最新版本的安装包,下载安装。

这里下载的是:protoc-3.9.1-win64.zip , windows 64位系统版本的编译器,下载后,解压到你想要的安装目录即可。

提示:安装完成后,将 [protoc安装目录]/bin 路径添加到PATH环境变量中

打开cmd,命令窗口执行protoc命令,没有报错的话,就已经安装成功。

更多protoc的教程请点击这里

grpc

gRPC 是一个高性能、跨平台、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C/C++、Java、Python、Ruby、C#、PHP、Node.js、Go 语言等版本,几乎你想到的语言都支持了.

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

下面先介绍grpc相关概念

grpc是什么?

在 gRPC 里客户端应用可以像调用本地方法一样直接调用另一台机器上服务端应用的方法,这样我们就很容易创建分布式应用和服务。跟其他 RPC 系统类似,gRPC 也是基于以下理念:首先定义一个服务,定义能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个方法,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根,这个存根就是长得像服务端一样的方法(但是没有具体实现),客户端通过这个存根调用服务端的方法。

grpc工作原理,如下图:

grpc使用的协议

gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据的序列化机制,当然也可以使用其他数据格式如 JSON,不过通常都使用protocol buffers这种灵活、高效的数据格式,如果不了解protobuf语法,点击这里学习 protocol buffers入门教程。

服务定义

使用gprc,首先需要定义服务, 指定其可以被远程调用的方法及其参数和返回类型。

服务,你可以理解成服务端api接口的集合,对外提供一些功能。

通过protobuf定义服务的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码// 定义一个叫HelloService的服务
service HelloService {
// 定义一个叫SayHello的方法,这个方法接受HelloRequest消息作为参数,返回HelloResponse消息
rpc SayHello (HelloRequest) returns (HelloResponse);
}

// 定义HelloRequest消息
message HelloRequest {
string greeting = 1;
}

// 定义HelloResponse消息
message HelloResponse {
string reply = 1;
}

如果你把service和message关键词当成class,是不是跟类定义很像!

gRPC 允许你定义四类服务方法,下面分别介绍如何定义,以及客户端和服务端的交互方式。

单向RPC

即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

1
2
scss复制代码rpc SayHello(HelloRequest) returns (HelloResponse){
}

服务端流式 RPC

即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

通俗的讲就是客户端请求一次,服务端就可以源源不断的给客户端发送消息。

1
2
3
scss复制代码// 注意stream关键词在什么地方
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

客户端流式 RPC

即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

通俗的讲就是请求一次,客户端就可以源源不断的往服务端发送消息。

1
2
3
scss复制代码// 注意stream关键词在什么地方
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

双向流式 RPC

即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

类似tcp通信,客户端和服务端可以互相发消息。

1
2
3
scss复制代码// 注意stream关键词在什么地方
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

更多关于grpc教程请点击这里

编写 go-micro HTTP服务

安装 gofast插件

1
2
arduino复制代码//gofast
go get -u -v github.com/gogo/protobuf/protoc-gen-gofast

开始编写 go-micro HTTP服务

创建 go-micro-examples 目录,然后在该目录下创建 helloworld目录

在 helloworld 目录下,创建 proto 和 handler,并创建 main.go 和使用 go mod init 初始化项目,如下图所示

其它文件暂时忽略,后面会解释

在 proto 目录创建 helloworld.proto,并写入下面代码

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
ini复制代码syntax = "proto3";

package helloworld;

option go_package = "proto;helloworld";

service Helloworld {
rpc Call(Request) returns (Response) {}
rpc Stream(StreamingRequest) returns (stream StreamingResponse) {}
rpc PingPong(stream Ping) returns (stream Pong) {}
}

message Message {
string say = 1;
}

message Request {
string name = 1;
}

message Response {
string msg = 1;
}

message StreamingRequest {
int64 count = 1;
}

message StreamingResponse {
int64 count = 1;
}

message Ping {
int64 stroke = 1;
}

message Pong {
int64 stroke = 1;
}

进入该目录并输入以下命令

并生成 helloworld.pb.go、helloworld.pb.micro.go

在 main.go,创建服务并注册一下 handler,运行服务

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
go复制代码package main

import (
"github.com/asim/go-micro/v3"
"github.com/asim/go-micro/v3/logger"
"go-micro-examples/helloworld/handler"
pb "go-micro-examples/helloworld/proto"
)

const (
ServerName = "go.micro.srv.HelloWorld" // server name
)

func main() {
// Create service
service := micro.NewService(
micro.Name(ServerName),
micro.Version("latest"),
)

// Register handler
if err := pb.RegisterHelloworldHandler(service.Server(), new(handler.Helloworld)); err != nil {
logger.Fatal(err)
}

// Run service
if err := service.Run(); err != nil {
logger.Fatal(err)
}
}

client 调用 service

创建 client 目录并创建client.go,写入一下代码

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
go复制代码package main

import (
"context"
"fmt"
"github.com/asim/go-micro/v3"
helloworld "go-micro-examples/helloworld/proto"
)

func main() {
// create a new service
service := micro.NewService()

// parse command line flags
service.Init()

// Use the generated client stub
cl := helloworld.NewHelloworldService("go.micro.srv.HelloWorld", service.Client())

// Make request
rsp, err := cl.Call(context.Background(), &helloworld.Request{
Name: "John",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(rsp.Msg)
}

效果

先启用 go.micro.srv.HelloWorld 服务,然后再启动 client 调用,效果如下

启动 client 之后,输出 Hello World!

参考链接

  • ProtoBuf 入门教程
  • grpc 框架教程

本文转载自: 掘金

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

微服务实战Go Micro v3 系列(一)- 基础篇

发表于 2021-06-28

最近完成了公司租房微服务项目,由于使用 go-micro v3遇到了不少的问题,在这里利用一个实战项目帮助大家融会贯通的使用 go-micro v3

另外,由于 Micro3.0直接放弃维护 go-micro,所以网上很多文章都是已经过时的了。

源码地址

  • 源码地址
  • 爱租房微服务综合项目

Go Micro 简介与设计理念

Go Micro 是一个基于 Go 语言编写的、用于构建微服务的基础框架,提供了分布式开发所需的核心组件,包括 RPC 和事件驱动通信等。

它的设计哲学是「可插拔」的插件化架构,其核心专注于提供底层的接口定义和基础工具,这些底层接口可以兼容各种实现。例如 Go Micro 默认通过 consul 进行服务发现,通过 HTTP 协议进行通信,通过 protobuf 和 json 进行编解码,以便你可以基于这些开箱提供的组件快速启动,但是如果需要的话,你也可以通过符合底层接口定义的其他组件替换默认组件,比如通过 etcd 或 zookeeper 进行服务发现,这也是插件化架构的优势所在:不需要修改任何底层代码即可实现上层组件的替换。

Go Micro 基础架构介绍

Go Micro 框架的基础架构如下,由 8 个核心接口组成,每个接口都有默认实现:

它的设计哲学是「可插拔」的插件化架构,其核心专注于提供底层的接口定义和基础工具,这些底层接口可以兼容各种实现。例如 Go Micro 默认通过 consul 进行服务发现,通过 HTTP 协议进行通信,通过 protobuf 和 json 进行编解码,以便你可以基于这些开箱提供的组件快速启动,但是如果需要的话,你也可以通过符合底层接口定义的其他组件替换默认组件,比如通过 etcd 或 zookeeper 进行服务发现,这也是插件化架构的优势所在:不需要修改任何底层代码即可实现上层组件的替换。

  • 最顶层的 Service 接口是构建服务的主要组件,它把底层的各个包需要实现的接口,做了一次封装,包含了一系列用于初始化 Service 和 Client 的方法,使我们可以很简单的创建一个 RPC 服务;
  • Client 是请求服务的接口,从 Registry 中获取 Server 信息,然后封装了 Transport 和 Codec 进行 RPC 调用,也封装了 Brocker 进行消息发布,默认通过 RPC 协议进行通信,也可以基于 HTTP 或 gRPC;
  • Server 是监听服务调用的接口,也将以接收 Broker 推送过来的消息,需要向 Registry 注册自己的存在与否,以便客户端发起请求,和 Client 一样,默认基于 RPC 协议通信,也可以替换为 HTTP 或 gRPC;
  • Broker 是消息发布和订阅的接口,默认实现是基于 HTTP,在生产环境可以替换为 Kafka、RabbitMQ 等其他组件实现;
  • Codec 用于解决传输过程中的编码和解码,默认实现是 protobuf,也可以替换成 json、mercury 等;
  • Registry 用于实现服务的注册和发现,当有新的 Service 发布时,需要向 Registry 注册,然后 Registry 通知客户端进行更新,Go Micro 默认基于 consul 实现服务注册与发现,当然,也可以替换成 etcd、zookeeper、kubernetes 等;
  • Selector 是客户端级别的负载均衡,当有客户端向服务端发送请求时,Selector 根据不同的算法从 Registery 的主机列表中得到可用的 Service 节点进行通信。目前的实现有循环算法和随机算法,默认使用随机算法,另外,Selector 还有缓存机制,默认是本地缓存,还支持 label、blacklist 等方式;
  • Transport 是服务之间通信的接口,也就是服务发送和接收的最终实现方式,默认使用 HTTP 同步通信,也可以支持 TCP、UDP、NATS、gRPC 等其他方式。

Go Micro 官方创建了一个 Plugins 仓库,用于维护 Go Micro 核心接口支持的可替换插件:

各个组件接口之间的关系可以通过下图串联:

小结

通过上述介绍,可以看到,Go Micro 简单轻巧、易于上手、功能强大、扩展方便,是基于 Go 语言进行微服务架构时非常值得推荐的一个 RPC 框架,基于其核心功能及插件,我们可以轻松解决之前讨论的微服务架构引入的需要解决的问题:

  • 服务接口定义:通过 Transport、Codec 定义通信协议及数据编码;
  • 服务发布与调用:通过 Registry 实现服务注册与订阅,还可以基于 Selector 提高系统可用性;
  • 服务监控、服务治理、故障定位:通过 Plugins Wrapper 中间件来实现。

接下来,我们将基于 Go Micro 微服务框架演示如何基于 Go 落地微服务架构。

参考链接

  • Micro 不能用了?关于 Go 语言微服务框架 Micro 的一些情况说明
  • go-micro 到底是个啥?

本文转载自: 掘金

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

1…627628629…956

开发者博客

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