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

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


  • 首页

  • 归档

  • 搜索

(三)Gateway开发教程之配置路由详解(全)

发表于 2021-11-23

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

前情回顾

上一篇文章,我们已经初步的使用了Gateway组件,并且在SpringCloud框架中进行了集成操作,已经算是入门了,接下来就是要全面的了解下Gateway中各项配置了。

Gateway组件中的配置路由详解

Gateway组件中的配置,在上篇文章中,我们也使用了一些,但是并没有将其他的使用方法全部列出,今天我们就要将其余的一些配置同大家讲述一下。

路由参数

路由Router的使用,我们先看一下之前使用的例子:

1
2
3
4
5
6
7
yaml复制代码routes:
- id: router1
uri: http://www.baidu.com
predicates:
- Path=/baidu/**
filters:
- StripPrefix=1

其中的id参数,是唯一的,如果多个路由的话,id也应该是不同的。

uri:该参数时用来指定匹配后的访问链接,如果匹配成功,那么就去访问百度了。

predicates:断言参数,这个请接着往下看。

filters:过滤器参数。

断言参数

断言参数predicates,是用来匹配路由规则的,比如本次例子中的Path=/baidu/**,意思就是匹配http://localhost/baidu/*/的相关的链接。

断言参数也是Gateway中规则最多的了,下面我们细说一下断言有提供哪些匹配参数。

  1. After

- After=2021-01-01

匹配在2021年一月一日时间之后发生的请求。

  1. Before

- Before=2021-01-01

匹配在2021年一月一日时间之前发生的请求。

  1. Between

- Before=2021-01-01,2021-01-02

匹配在2021年一月一日至2021年一月二日之间发生的请求。

  1. Cookie

- Cookie=demokey, abc

cookie的设置,有两个参数,分别是name和regexp(Java正则),可以匹配到相应名称的Cookie名称,且与正则相匹配的Cookie值的链接。

  1. Header

- Header=X-Request-Id, \d+

Header同样也提供了两个参数,分别是name和regexp(Java正则),可以匹配相应类型的Url,比如127.0.0.1/demo/1,这样就可以进入上述规则。

  1. Host

- Host=**.pxyz.top

Host就比较好理解了,其参数就是匹配相应的ip,或者域名等信息的Url。

  1. Method

- Method=GET

Method就更加熟悉了,GET、POST、PUT、DELETE等都是属于Method中的一类,上述就是匹配GET类的请求。

  1. Path

- Path=/baidu/**

Path:我们最常用的,用于匹配URL相关路径。

  1. Query

- Query=abc

Query:查询条件,用于匹配查询条件是否存在abc条件。

  1. RemoteAddr

- RemoteAddr=192.168.1.1/24

RemoteAddr:指定请求远程地址IPIP,可以设置多个网段等功能

  1. Weight

- Weight= gtoup, 2

利用路由权重来匹配对应的路由规则。

过滤器参数

过滤器参数相对断言参数要少一些,但是同样重要,让我们来细数一下。

路由中的filters参数的设置也是非常广泛的。

而我们平时使用最多的就是

1
2
3
4
5
6
7
yaml复制代码      routes:
- id: demo
uri: http://demo
predicates:
- Path=/demo/**
filters:
- StripPrefix=1

这个参数是从请求中剥离的路径个数,比如http://127.0.0.1/demo/user/list, 就可以得到http://127.0.0.1/demo/list。

总结

如果你能合理的运用这些路由配置,那么就证明已经可以熟练使用Gateway了,但是要想做到熟练掌握,还需要接下来的深入学习才是,加油。

本文转载自: 掘金

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

Yarn ResourceManager启动流程源码解读

发表于 2021-11-23

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

本文的阅读需要一些YARN Service的知识,相关内容请查看:

YARN源码不知从何入手,那是你不知道Service

下面就ResourceManager的启动流程做详细的源码解读

1 serviceInit()

ResourceManager是一个Service,所以ResourceManager的启动当然是要找init()方法,该类本身没有实现Init()方法,所以使用父类方法,父类的Init()方法调用了serviceInit(),并且在ResourceManager 重写,下面详细看ResourceManager .serviceInit()

该方法主要做了下面三件事情:

  1. 创建各种子service实例
1
2
3
> java复制代码new xxx();createxxx();
>
>
  1. 将该实例加入到serviceList集合中
1
2
3
4
> java复制代码addService(service); 
> addIfService(object);
>
>
  1. 执行各个子service的serviceInit()方法
1
2
3
> java复制代码super.init();
>
>

该类的serviceInit方法从上至下依次为:

生成上下文对象

1
java复制代码this.rmContext = new RMContextImpl()

加载配置文件core-site.xml

1
java复制代码loadConfigurationXml(YarnConfiguration.CORE_SITE_CONFIGURATION_FILE);

加载配置文件yarn-site.xml

1
java复制代码loadConfigurationXml(YarnConfiguration.YARN_SITE_CONFIGURATION_FILE);

HA的一些设置和相关服务地址的检查 默认:yarn.resourcemanager.ha.enabled = true

1
java复制代码this.rmContext.setHAEnabled(HAUtil.isHAEnabled(this.conf));

初始化和添加AsyncDispatchen(重点)

1
2
3
java复制代码rmDispatcher = setupDispatcher();
addIfService(rmDispatcher);
rmContext.setDispatcher(rmDispatcher);

adminService

1
2
3
java复制代码adminService = createAdminService();
addService(adminService);
rmContext.setRMAdminService(adminService);

选举相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码if (this.rmContext.isHAEnabled()) {
/**
* yarn.resourcemanager.ha.automatic-failover.emabled = true
* yarn.resourcemanager.ha.automatic-failover.embedde = true
* embedde = true 表明使用原生的zookeeperAPI来执行选举 否则使用curator框架来实现
* 不同点:zookeeperAPI复杂一点
*/
if (HAUtil.isAutomaticFailoverEnabled(conf)
&& HAUtil.isAutomaticFailoverEmbedded(conf)) {
//创建选举器 ActiveStandbyElectorBasedElectorService
// 阻塞的进行最大三次的尝试选举 不成功交给线程
EmbeddedElector elector = createEmbeddedElector();
addIfService(elector);
rmContext.setLeaderElectorService(elector);
}
}

当前节点为active RM才启动的服务 (service)

1
java复制代码createAndInitActiveServices

初始化各种服务(子service) 调用各种service实例的serviceInit( )方法

1
java复制代码super.serviceInit(this.conf);

初始化的内容很多,选取选取几个重要的内容进行详细说明:

2 createEmbeddedElector

下面详细看一下选举初始化的相关内容

createEmbeddedElector()方法作为入口

  1. 默认情况下是使用的zookeeperAPI进行选举,所以走else分支
1
2
3
4
5
6
7
8
java复制代码createEmbeddedElector(){
if (curatorEnabled) {
this.zkManager = createAndStartZKManager(conf);
elector = new CuratorBasedElectorService(this);
} else {
elector = new ActiveStandbyElectorBasedElectorService(this);
}
}
  1. ActiveStandbyElectorBasedElectorService返回了该类型的选举实例,该类也是一个Service,所以该类的初始化找该类下面的serviceInit()方法

该方法主要做下面三件事情:

1
2
3
4
5
> 复制代码1.选举实例的创建
> 2.zookeeper的链接
> 3.创建选举过程中需要创建的路径
>
>

ActiveStandbyElectorBasedElectorService.serviceInit()具体为:

获取zookeeper地址

1
java复制代码String zkQuorum = conf.get(YarnConfiguration.RM_ZK_ADDRESS);

获取cluster id 和rescourcemanager id

1
2
java复制代码String rmId = HAUtil.getRMHAId(conf);
String clusterId = YarnConfiguration.getClusterId(conf);

完成 zookeeper的链接 ,创建一个znode字节数组 localActiveNodeInfo对应zookeeperper znode上面所存储的数据

1
java复制代码localActiveNodeInfo = createActiveNodeInfo(clusterId, rmId);

创建选举过程中需要的路径

zkBasePath = /yarn-leader-election (yarn-leader-election节点)

electionZNode = /yarn-leader-election/clusterId

1
2
java复制代码String zkBasePath = conf.get(YarnConfiguration.AUTO_FAILOVER_ZK_BASE_PATH,YarnConfiguration.DEFAULT_AUTO_FAILOVER_ZK_BASE_PATH);
String electionZNode = zkBasePath + "/" + clusterId;

选举的尝试次数 默认为3次

1
2
3
4
java复制代码int maxRetryNum =
conf.getInt(YarnConfiguration.RM_HA_FC_ELECTOR_ZK_RETRIES_KEY, conf
.getInt(CommonConfigurationKeys.HA_FC_ELECTOR_ZK_OP_RETRIES_KEY,
CommonConfigurationKeys.HA_FC_ELECTOR_ZK_OP_RETRIES_DEFAULT));

创建选举实例

1
java复制代码elector = new ActiveStandbyElector(zkQuorum, (int) zkSessionTimeout,electionZNode, zkAcls, zkAuths, this, maxRetryNum, false);

确保父节点 zkBasePath 存在

1
java复制代码elector.ensureParentZNode();

初始化服务,调用相关子服务的serviceInit()方法

1
java复制代码super.serviceInit(conf)

3 createAndInitActiveServices

createAndInitActiveServices是只要当前RecourceManager是active状态才需要启动的一些服务

核心入口就是下面这句代码,它的内部创建了一个RMActiveServices对象,并且调用了该实例的init)方法。其实init()方法的内部就是调用servicelnit()方法

1
2
3
4
5
java复制代码protected void createAndInitActiveServices(boolean fromActive) {
activeServices = new RMActiveServices(this);
activeServices.fromActive = fromActive;
activeServices.init(conf);
}

接下来看RMActiveServices的serviceInit()方法:

serviceInit()

创建一个选举线程来完成选举

  1. 之前创建了一个 选举实例
  2. 此时创建了一个 选举线程

选举实例,会尝试最多3次选举,如果没有成功, 则启动这个线程来执行选举
最终都是使用选举实例的选举方法实现选举的

1
java复制代码standByTransitionRunnable = new StandByTransitionRunnable();

做token管理 指纹的一些工作

1
2
ini复制代码rmSecretManagerService = createRMSecretManagerService();
addService(rmSecretManagerService);

在初始化方法中有好多好多的Service被创建并且添加到了serviceList列表中,后面就不在明确写出addService()方法


过期处理

当ApplicationMster 收到ResourceManager 新分配到一个container后,必须在一定的时间内(10min)在对应的NM上启动该container,否则就会回收

1
ini复制代码containerAllocationExpirer = new ContainerAllocationExpirer(rmDispatcher);

资源调度策略 resourceScheduler 默认实现的是 CapacityScheduler

1
java复制代码scheduler = createScheduler();

除了有各种各样的Service被创建外,还有很多的eventHandler被注册到AsyncDispatcher

注册方法有两个参数:

  • 第一个参数为: 事件类型
  • 第二个参数为: 事件处理器

如果提交一个事件到 AsyncDispatcher AsyncDispatcher就会找到之前注册的事件所对应的EventHandler 的handle 方法执行事件的处理

EventHandler 有可能是状态机,也可能就是一个简单的 EventHandler

1
2
3
4
5
6
7
8
9
10
java复制代码rmDispatcher.register(SchedulerEventType.class, schedulerDispatcher);
// Register event handler for RmAppEvents
rmDispatcher.register(RMAppEventType.class,
new ApplicationEventDispatcher(rmContext));
// Register event handler for RmAppAttemptEvents
rmDispatcher.register(RMAppAttemptEventType.class,
new ApplicationAttemptEventDispatcher(rmContext));
// Register event handler for RmNodes
rmDispatcher.register(
RMNodeEventType.class, new NodeEventDispatcher(rmContext));

2.4 serviceStart()

接下来看ResourceManager的serviceStart()方法

刚启动当前的节点就成为standby

1
2
3
java复制代码if (this.rmContext.isHAEnabled()) {
transitionToStandby(false);
}

启动用户webapp 用户认证服务

1
2
3
4
5
6
java复制代码startWepApp();
if (getConfig().getBoolean(YarnConfiguration.IS_MINI_YARN_CLUSTER,
false)) {
int port = webApp.port();
WebAppUtils.setRMWebAppPort(conf, port);
}

起初始化创建的所有子Service进行启动

1
java复制代码super.serviceStart();

如果没有启动HA 当前节点直接成为active,只有一台ResourceManager它肯定为active状态

1
2
3
java复制代码if (!this.rmContext.isHAEnabled()) {
transitionToActive();
}

此处注意,当启动了HA模式的时候,该怎么选举active节点呢?

还记得在ResourceManager.serviceInit()方法有下面一段代码

1
2
3
java复制代码EmbeddedElector elector = createEmbeddedElector();
addIfService(elector);
rmContext.setLeaderElectorService(elector);

创建了一个选举的实例并且加入了serviceList中,现在所有的子service启动当然也包含了这个Service的启动,关于HARN HA 的选举机制将会在下一篇文章整理讲述。

本人在学习的路上,上述文章如有错误还请指教批评,谢谢。

本文转载自: 掘金

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

AEJoy —— AE 插件开发中的 全局、序列、帧数据 全

发表于 2021-11-23

全局、序列、帧数据

After Effects 允许插件在三个范围存储数据: 全局,序列和帧。
仔细考虑将信息存储在何处;
选择不当会影响性能,或者让用户感到困惑。

将全局数据用于所有效果实例的公共信息: 静态变量和数据、位图、指向其他 DLL 或外部应用程序的指针。
如果你的效果支持多帧渲染,任何静态或全局变量都必须不存在竞争条件(参见效果线程安全意味着什么?
的更多信息)。

将任何特定于此插件实例的内容( UI 设置、文本字符串和任何未存储在参数中的自定义数据)存储在序列数据或新的多帧渲染计算缓存中。

帧数据用于特定于渲染给定帧的信息。
这已经被废弃了,因为大多数机器一次可以将整个帧加载到内存中。
当然,生成 IMAX 的用户仍然会欣赏您所做的任何优化。

持续性

After Effects 在项目文件中保存序列数据,但不保存全局或帧数据。序列数据中指向外部数据的指针在重新打开项目时很可能无效,必须重新连接。我们称这个过程为序列数据的“平展化”和“非平展化”。

注意:计算缓存不会将其内容存储到项目文件中。必须在渲染期间重新创建存储在缓存中的数据。

验证序列数据

仔细的序列数据验证对于跨时间进行模拟的效果非常重要,因为第 N 帧依赖于第 N-1 帧,并且您在序列数据中使用了计算数据的缓存。
如果一个参数改变了,某些计算的数据可能不再有效,但盲目地在每次更改后重新计算一切也是浪费。

当要求渲染第 N 帧时,假设您已经计算了缓存数据到第 N-1 帧,从 PF_ParamUtilSuite3 调用 PF_GetCurrentState() / PF_AreStatesIdentical() 来查看计算数据的缓存在当前参数设置下是否仍然有效。

所有参数的状态(除了那些设置了 PF_ParamFlag_EXCLUDE_FROM_HAVE_INPUTS_CHANGED 的参数),包括层参数(包括 param[0] )在过去的时间跨度内进行检查。

它能有效地完成,因为更改跟踪是用时间戳完成的。

如果输入没有改变,您可以安全地使用缓存,并且内部缓存系统将假定您对传递的范围(passed range)有时间依赖性。
因此,如果上游(upstream)发生变化,主机的缓存将自动失效。

为了测试它的工作,在每一帧,在一个打上关键帧的参数上应用你的效果。
RAM 预览填充缓存,然后改变其中一个关键帧。
相关联的帧和所有依赖的帧(例如,在模拟的情况下,后面的帧)应该失去它们的缓存标记并需要重新渲染。
类似地,对层参数来源的上游的变化应该会导致缓存的时间选择性(time-selective)失效。

展平化和非展平化的序列数据

如果序列数据引用外部内存(在指针或句柄中),则必须将数据平展化 或 非平展化以实现磁盘安全存储。这类似于创建自己的微型文件格式。

在接收到 PF_Cmd_SEQUENCE_FLATTEN 后,将指针引用的数据放到一个连续块中,稍后可以从这个块中恢复旧的结构。

如果序列数据包含一个指向 long 的指针,分配 4 个字节来存储平展化的数据。您必须处理平台特定的字节排序。

请记住,您的用户(购买了两个插件副本的用户)可能希望在 macOS 和 Windows 上使用相同的项目。

当数据重新加载后,无论是平展化的数据还是非平展化的数据,Effects 都会发送 PF_Cmd_SEQUENCE_RESETUP。

在两个结构的共同偏移量处使用标志来指示数据的状态。

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码typedef struct {
A_char* messageZ;
PF_FpLong big_numF;
void* temp_storage;
} non_flat_data;

typedef struct {
char message[256];
PF_FpLong big_numF;
A_Boolean big_endianB;
} flat_data;

缩放序列数据

在 PF_Cmd_SEQUENCE_SETUP 期间,为特定于你的效果实例的数据分配句柄。

您可以在任何选择器中修改序列数据的内容,但不能修改序列数据的大小。

你只能在以下选择器中调整序列数据句柄的大小:

  • PF_Cmd_AUDIO_SETUP
  • PF_Cmd_AUDIO_SETDOWN
  • PF_Cmd_FRAME_SETUP
  • PF_Cmd_FRAME_SETDOWN
  • PF_Cmd_AUDIO_RENDER
  • PF_Cmd_RENDER
  • PF_Cmd_SEQUENCE_SETUP
  • PF_Cmd_SEQUENCE_SETDOWN
  • PF_Cmd_SEQUENCE_FLATTEN
  • PF_Cmd_SEQUENCE_RESETUP
  • PF_Cmd_DO_DIALOG

在多帧渲染的渲染时刻访问 sequence_data

当对一个效果启用多帧渲染时, sequence_data 对象在渲染期间将是只读的 const ,并且可以通过 PF_EffectSequenceDataSuite1 套件在每个渲染线程上访问。

PF_EffectSequenceDataSuite1

PF_GetConstSequenceData

当启用多帧渲染效果时,检索渲染线程的只读 const sequence_data 对象。

1
2
3
cpp复制代码PF_Err(*PF_GetConstSequenceData)(
PF_ProgPtr effect_ref,
PF_ConstHandle *sequence_data);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cpp复制代码static PF_Err Render(
PF_InData *in_dataP,
PF_OutData *out_dataP,
PF_ParamDef *params[],
PF_LayerDef *output )
{
PF_ConstHandle seq_handle;

AEFX_SuiteScoper<PF_EffectSequenceDataSuite1> seqdata_suite = ///< 获取套件
AEFX_SuiteScoper<PF_EffectSequenceDataSuite1>(
in_dataP,
kPFEffectSequenceDataSuite,
kPFEffectSequenceDataSuiteVersion1,
out_dataP);

PF_ConstHandle const_seq; ///< 只读对象
seqdata_suite->PF_GetConstSequenceData(in_data->effect_ref, &const_seq);
​
// 将 const_seq 转换为存储到 sequence_data 时使用的类型

// rest of render function code...
}

本文转载自: 掘金

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

【力扣-回溯】2、电话号码的字母组合(17) 17 电话号

发表于 2021-11-23

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

17. 电话号码的字母组合

题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

1
2
arduino复制代码输入: digits = "23"
输出: ["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

1
2
ini复制代码输入: digits = ""
输出: []

示例 3:

1
2
arduino复制代码输入: digits = "2"
输出: ["a","b","c"]

解析

回溯法

  • 首先需要一个字符串数组vector<string> result来存放结果,同时还需要一个字符串s来存放叶子结点的值
  • 确定递归函数的参数和返回值
    • 参数:字符串,指向数字的索引(用来统计访问到第几个数,index在这里该表示树的深度)
  • 确定终止条件
    • 如果index 等于输入的数字个数digits.size(),则将s添加到结果集result中
  • 确定单层循环的逻辑
    • 取Index指向的数字,找到对应的字符串
    • for循环处理这个字符串

代码

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
c++复制代码class Solution
{
public:

vector<string> letterCombinations(string digits)
{
// 如果字符串为空,直接返回
if (digits == "")
{
return result;
}
// 进行回溯处理
backtracking(digits,0);
return result;
}

private:
// 结果集
vector<string> result;
// 记录叶子节点
string s;
// 定义数字和字符串的映射
// 采用字符串数组来存储
const string letterMap[10] = {
"", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz", //9

};
// 这里的index不是 组合问题里面的 startIndex
// index指向数字,表示遍历到第几个数字了,还表示树的深度
void backtracking(const string &digits, int index)
{
// index等于字符串的大小时
// 说明找到了匹配的,添加到结果集中
if (index == digits.size())
{
result.push_back(s);
return;
}

// 将index指向的数字转换成int类型
int digit = digits[index] - '0';
// 取出数字对应的字符串
string letter = letterMap[digit];
// 对字符串进行遍历
for (int i = 0; i < letter.size(); i++)
{
s.push_back(letter[i]);
// 下一层处理下一个数字
backtracking(digits, index + 1);
s.pop_back(); // 回溯
}
}
};

本文转载自: 掘金

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

【力扣-回溯】1、组合(77)组合总和III(216) 77

发表于 2021-11-23

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

77. 组合

题目描述

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

1
2
3
4
5
6
7
8
9
10
ini复制代码输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

示例 2:

1
2
lua复制代码输入: n = 1, k = 1
输出: [[1]]

解析

回溯法就是使用暴力法枚举出所有的可能,递归回溯相辅相成,回溯可以看成一棵树。以本题为例,树的深度为 n , 宽度为 k 。所以横向遍历的次数为 k , 纵向遍历的次数为 n 。
回溯三部曲:

  • 1、确定递归函数的参数与返回值
    • 求组合数,所以不考虑元素的顺序
    • 使用一个数组vector<int> path存放每次遍历的结果
    • 使用数组vector<vector<int>> result存放 path
    • 使用int startIndex来标记递归的每一层遍历开始的索引
  • 2、确定终止条件
    • 如果path已经存入了k个元素,则终止此次遍历
  • 3、确定单层递归的逻辑
    • 每层遍历时,将遍历的元素加入到path中
    • 进行回溯

代码

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
c++复制代码class Solution
{
public:
vector<vector<int>> combine(int n, int k)
{
backtracking(n, k, 1);
return result;
}

private:
// 使用两个全局变量存放结果
// 存放最终的结果
vector<vector<int>> result;
// 存放单层递归的结果
vector<int> path;

// 回溯算法
// startIndex用来控制开始遍历的索引,遍历的范围逐渐缩小
void backtracking(int n, int k, int startIndex)
{
// 递归的终止条件
// 当path的 大小等于 k 时,将path数据存入到 结果数组中
if (path.size() == k)
{
result.push_back(path);
return;
}

// 单层递归逻辑
for (int i = startIndex; i <= n; i++)
{
path.push_back(i); // 处理节点
// 递归处理,控制树的纵向遍历
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
}
};

剪枝操作

在单层for循环中,如果剩余的元素不足以和已经选取的元素组成K个,那么后面的遍历就是不必要的

  • 已经选取元素个数:path.size()
  • 还需要选取元素个数: k - path.size()
  • 所以,遍历的起始索引必须满足 startIndex<=n - (k - path.size())+1
  • 修改后的for循环:*
1
2
3
4
c++复制代码for(int i = startIndex;i<=n-(k-path.size())+1;i++)
{
... ...
}

216. 组合总和 III

题目描述

找出所有相加之和为 n 的 k 个数的组合 **。 **组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

示例 1:

1
2
lua复制代码输入: k = 3, n = 7
输出: [[1,2,4]]

示例 2:

1
2
ini复制代码输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

解析

与上面的组合题思路相似,这里是求 k 个数的和等于 n ,在判断条件上稍作改动。具体内容查看代码

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
c++复制代码class Solution
{
public:
vector<vector<int>> combinationSum3(int n, int k)
{
backtracking(n, k, 1);
return result;
}

private:
vector<vector<int>> result;
vector<int> path;

// 使用减法逐步减小n的值
void backtracking(int n, int k, int startIndex)
{
// 剪枝操作
if (n < 0)
{
return;
}
// 如果path的大小满足条件,并且path中的和为 n
if (path.size() == k && n == 0)
{
result.push_back(path);
return;
}

for (int i = startIndex; i <= 9; i++)
{
// 每次遍历到一个数,就将n的值减去这个数
n -= i;
// 将 i 添加到 path 中
path.push_back(i);
// 递归处理
backtracking(n, k, i + 1);
n += i; // 回溯
path.pop_back(); // 回溯
}
}
};

本文转载自: 掘金

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

C# 窗体应用TreeView控件简单使用 前言: 每日一遍

发表于 2021-11-23

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

前言:

TreeView控件在窗体应用里面使用也是频率比较高的,我们在使用TreeView一般是对资源的分层展示,类似数据结构里面树的凹入表示法。很多公司都是使用TreeView控件作为文件显示信息的分级视图,如同Windows里的资源管理器的目录。TreeView控件中的各项信息都有一个与之相关的Node对象。TreeView显示Node对象的分层目录结构,每个Node对象均由一个Label对象和其相关的位图组成。在建立TreeView控件后,可以展开和折叠、显示或隐藏其中的节点。TreeView控件一般用来显示文件和目录结构、文档中的类层次、索引中的层次和其他具有分层目录结构的信息。

每日一遍,防止早恋

image-20211121190703016

1.TreeView控件使用

1.1 TreeView控件常用的属性

1
2
3
4
5
6
7
8
9
10
scss复制代码Index 获取树节点在树节点集合中的位置
Nodes 获取分配给树视图控件的树节点集合
Parent 获取或设置控件的父容器
SelectedNode 获取或设置当前在树视图控件中选定的树节点
ExpandAll 展开所有树节点
Checked 获取或设置一个值,用以指示树节点是否处于选中状态
Text 获取或设置在树节点标签中显示的文本
Expand 展开树节点
Clear 清空树
Remove 从树视图控件中移除当前树节点。

1.2 创建窗体文件并修改名称

image-20211121152657490

2.设计界面

我们采用简单案例来实现效果

image-20211121162951636

3.添加和删除数据

3.1 点击treeView编辑结点

image-20211121163205603

3.2 在TreeView编辑器添加数据

注:我们这里属于静态添加数据,后面做的删除或者是添加也是静态的,后面做到删除是不起作用的

image-20211121163711117

image-20211121164122981

3.3 TreeView控件添加根结点操作

注:博主这里的添加并未添加到数据库,只是做一个静态的展示并未静态写入treeView,存在内存里结束就释放了

image-20211121165100205

3.4 TreeView控件添加子结点操作

我们需要获取我们选中的结点判断它是否为空,不为空就代表有根结点可以添加,没有就返回选择我们需要添加子节点。

image-20211121165636189

3.5 TreeView控件删除操作

这里我们需要判断我们选择的结点的父结点是否为空,为空代表这个结点是根结点需要整个删除,不为空代表为子结点,我们从父结点找到子结点删除。

image-20211121170154992

3.6 TreeView控件清空树操作

我们这里直接使用clear进行删除就可以了。没有其他操作

image-20211121170337000

4.整体效果展示

2021112323

4.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
csharp复制代码using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TreeTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
TreeNode treeNode = new TreeNode(textBox1.Text, 2, 2);
treeView1.Nodes.Add(treeNode);
treeView1.Select();
}

private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
{

}

private void button2_Click(object sender, EventArgs e)
{
TreeNode selectdnode = treeView1.SelectedNode;
if(selectdnode!=null)
{
TreeNode chnode = new TreeNode(textBox2.Text, 2, 2);
selectdnode.Nodes.Add(chnode);
selectdnode.Expand();
treeView1.Select();
}
else
{
MessageBox.Show("请选择需要添加子节点");
}
}

private void button3_Click(object sender, EventArgs e)
{
TreeNode selectnode = treeView1.SelectedNode;
TreeNode parentnode = selectnode.Parent;
if(parentnode == null)
{
treeView1.Nodes.Remove(selectnode);
}
else
{
parentnode.Nodes.Remove(selectnode);
}
treeView1.Select();
}

private void button4_Click(object sender, EventArgs e)
{
treeView1.Nodes.Clear();
}

private void button5_Click(object sender, EventArgs e)
{
this.textBox1.Clear();
this.textBox2.Clear();
}
}
}

总结:

TreeView控件讲解博主并未使用数据库进行操作,而是简单的使用变量来操作,我们在使用数据库是需要使用结点的深度建立,每一层可能会有一个表,最终到最底层才是我们的数据,我们获取每层Name生成树,实现资源的凹入表示法,博主只是简单的介绍使用,比较适合新手上手,就是写一下静态的数据不连接数据库进行操作,如果你需要连接数据库关注博主下一篇文章可能会写,好了,创作不易点赞关注评论收藏,谢谢大佬了!!!!

1235468900

本文转载自: 掘金

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

【力扣-二叉树】23、把二叉搜索树转换成累加树(538) 5

发表于 2021-11-23

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

538. 把二叉搜索树转换为累加树

题目描述

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。

注意: 本题和 1038: leetcode-cn.com/problems/bi… 相同

示例 1:

1
2
csharp复制代码输入: [4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出: [30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]

示例 2:

1
2
ini复制代码输入: root = [0,null,1]
输出: [1,null,1]

示例 3:

1
2
ini复制代码输入: root = [1,0,2]
输出: [3,3,2]

示例 4:

1
2
ini复制代码输入: root = [3,2,4,1]
输出: [7,9,4,10]

提示:

  • 树中的节点数介于 0 和 104 之间。
  • 每个节点的值介于 -104 和 104 之间。
  • 树中的所有值 互不相同 。
  • 给定的树为二叉搜索树。

解析

根据给出的示例可以发现,所谓的累加树,其实就是根据中序遍历的顺序,求出每个节点到最后一个节点值的累加和作为当前节点的值。

法1

  • 使用两次中序遍历:
    • 1、第一次遍历记录每个元素的值,并且求出最大值
    • 2、第二次遍历修改每个元素的值
      • 规律:当前元素的新值等于前驱节点的新值-前驱节点的旧值
      • 所以:使用三个指针来记录值的变化,
        • 其中preOld指针指向前驱节点的旧值
        • preNew指针指向前驱节点的新值
        • tmp 指针记录当前节点修改前的值

代码

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
c++复制代码class Solution
{
public:
TreeNode *convertBST(TreeNode *root)
{
// 第一次遍历,求出所有节点值的累加和
traversal(root);
// 第二次遍历,更新节点的累加和
changeVal(root);
// 返回根节点
return root;
}

private:
int sum; // 记录所有节点值的累加和
TreeNode *preOld = new TreeNode(0); // 记录前驱节点的旧值
TreeNode *preNew = NULL; // 记录前驱节点的新值
TreeNode *tmp = new TreeNode(0); // 临时节点,记录当前节点的旧值

// 第一次中序遍历,求节点值累加和
void traversal(TreeNode *cur)
{
if (cur == NULL)
{
return;
}
// 左
if (cur->left)
{
traversal(cur->left);
}

// 中
sum += cur->val;

// 右
if (cur->right)
{
traversal(cur->right);
}
}

// 第二次中序遍历,修改节点的值
void changeVal(TreeNode *cur)
{
if (cur == NULL)
{
return;
}

// 左
if (cur->left)
{
changeVal(cur->left);
}

// 中
if (preNew == NULL)
{
// 当前驱节点指向空时,找到左子树的最左叶子节点
preOld->val = cur->val; // 记录旧值
cur->val = sum; // 更新值
preNew = cur; // 更新前驱节点指针
}
else
{
tmp->val = cur->val; // 记录当前节点的旧值
cur->val = preNew->val - preOld->val; // 更新当前节点的值
preOld->val = tmp->val; // 更新前驱节点旧值
preNew = cur; // 更新前驱节点
}

// 右
if (cur->right)
{
changeVal(cur->right);
}
}
};

法2

对二叉搜索树进行中序遍历可以得到一个有序数组,数组元素更新的规律可以看做是从后向前累加,所以可以使用逆向中序遍历,即遍历顺序为 右->中->左

  • 使用递归方法
    • 1、确定递归的参数与返回值
      • 参数:当前节点 cur 以及 前驱节点 pre
    • 2、确定递归的终止条件
      • 遇到空节点直接返回
    • 3、确定单层循环的逻辑
      • 先遍历右孩子,
      • 遍历根节点,同时修改节点元素的值
      • 最后左孩子

代码

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
c++复制代码class Solution
{
public:
TreeNode *convertBST(TreeNode *root)
{
traversal(root);
return root;
}

private:
TreeNode *pre = new TreeNode(0); // 前驱节点值初始为0
void traversal(TreeNode *cur)
{
if (cur == NULL)
{
return;
}

// 右
if (cur->right)
{
traversal(cur->right);
}


// 当前节点的值等于前一个节点的值+当前节点的值
cur->val = pre->val + cur->val;
pre = cur;
// 左
if (cur->left)
{
traversal(cur->left);
}
}
};

本文转载自: 掘金

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

laravel的订单管理api 一、订单详情管理

发表于 2021-11-23

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

一、订单详情管理

1.1 创建订单控制器

运行命令php artisan make:controller Web/OrderController创建订单控制器:
在这里插入图片描述
写入订单管理相关的方法:

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
php复制代码<?php

namespace App\Http\Controllers\Web;

use App\Http\Controllers\BaseController;
use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class OrderController extends BaseController
{
/**
* 订单列表
*/
public function index(Request $request) {
// 状态查询
$status = $request->query('status') ;

// 根据标题
$title = $request->query('title');

$order = Order::where('user_id', auth('api')->id())
->when($status, function ($query) use ($status) {
$query->where('status', $status);
})
->when($status, function ($query) use ($title) {
$query->whereHas('goods', function ($query) use($title) {
$query->where('title', 'like', "%{$title}%");
}); // 向关联加入自定义约束
})
->paginate(3);
return $this->response->paginator($order, new OrderTransformer());
}

/**
* 预览订单
*/
public function preview()
{
// 地址数据
// TODO 暂时模拟地址数据
$address = [
['id' => 1, 'name' => '张三', 'address' => '厦门', 'phone' => '123123'],
['id' => 2, 'name' => '李四', 'address' => '厦门', 'phone' => '123123'],
['id' => 3, 'name' => '王武', 'address' => '厦门', 'phone' => '123123'],
];

// 购物车数据
$carts = Cart::where('user_id', auth('api')->id())
->where('is_checked', 1)
->with('goods:id,cove,title')
->get();

// 返回数据
return $this->response->array([
'address' => $address,
'carts' => $carts,
]);
}

/**
* 提交订单
*/
public function store(Request $request)
{
// 验证地址字段
$request->validate([
'address_id' => 'required' // TODO 地址要存在才行 exists:addresses,id
], [
'address_id.required' => ['收获地址不能为空']
]);

// 处理插入的数据
$user_id = auth('api')->id();
$order_no = date('YmdHis') . rand(100000, 999999);
$amount = 0;
$cartsQuery = Cart::where('user_id', $user_id)
->where('is_checked', 1);

$carts = $cartsQuery->with('goods:id,price')->get();

// 要插入的订单详情的数据
$insertData = [];


foreach ($carts as $key => $cart) {
// 如果有商品库存不足,提示用户重新选择
if ($cart->goods->stock < $cart->num) {
return $this->response->errorBadRequest($cart->goods->title.' 库存不足,请重新选择商品!');
}
$insertData[] = [
'goods_id' => $cart->goods_id,
'price' => $cart->goods->price,
'num' => $cart->num
];
$amount += $cart->goods->price * $cart->num;
}

try {
DB::beginTransaction(); // 开启事务
// 生成订单
$order = Order::create([
'user_id' => $user_id,
'order_no' => $order_no,
'address_id' => $request->input('address_id'),
'amount' => $amount
]);

// 生成订单的详情
$order->orderDetails()->createMany($insertData);

// 删除已经结算的购物车数据
$cartsQuery->delete();

// 减去商品对应的库存量
foreach($carts as $cart) {
Good::where('id', $cart->goods_id)->decrement('stock', $cart->num);
}
DB::commit(); // 没有异常才提交
return $this->response->created();
} catch (\Exception $e) {
DB::rollBack(); // 出现异常 事务 数据回滚
throw $e; // 抛出异常
}
}
// 订单详情
public function show(Order $order) {
return $this->response->item($order, new OrderTransformer());
}
/**
* 确认收货
*/
public function confirm(Order $order) {
if ($order->status != 3) {
return $this->response->errorBadRequest('订单状态异常!');
}
try {
DB::beginTransaction();
$order->status = 4;
$order->save();

$orderDetails = $order->orderDetails;

// 增加订单下所有商品的销量
foreach($orderDetails as $orderDetail) {
// 更新商品销量
Good::where('id', $orderDetail->goods_id)->increment('sales', $orderDetail->num);
}

DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
return $this->response->noContent();
}
}

在这里插入图片描述

1、我们这边暂时还没做地址管理api,所以地址暂时用模拟
2、由于创建订单涉及到多表操作,万一一步操作错了,可能会产生未知的bug,所以我们这边使用事务来进行异常捕获并抛出。

由于往订单表中插入数据,所以我们还要往订单表中写入可批量赋值的字段:
在这里插入图片描述
由于也往订单详情表中插入了数据,所以也需要往订单详情表中插入可批量的字段:
在这里插入图片描述


接着在订单模型中写入远程一对多,这样我们就可以跳过中间订单详情表,直接通过订单模型获取到商品数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码    /**
* 订单远程一对多,关联商品
*/
public function goods() {
return $this->hasManyThrough(
Good::class, // 最终关联的模型
OrderDetails::class, // 中间模型
'order_id', // 中间模型和本模型关联的外键
'id', // 最终关联模型的外键
'id', // 本模型和中间模型关联的健
'goods_id' // 中间表和最终模型关联到一个键
);
}

在这里插入图片描述

增加订单OrderTransformer.php的商品数据:

1
2
3
4
5
6
php复制代码    /**
* 商品数据
*/
public function includeGoods(Order $order) {
return $this->collection($order->goods, new GoodTransformer());
}

在这里插入图片描述

1、任务调度订单过时未付取消

由于下单了就会减少库存,为了防止有些人恶意下单,减库存,所以我们可以做个任务调度,做个订单多久失效。
输入命令:crontab -e,输入1回车:
在这里插入图片描述
在编辑器中输入* * * * * /home/vagrant/code/shopProjectApi/artisan schedule:run >> /dev/null 2>&1。你的项目的绝对路径。
然后controller + x输入y保存退出。
然后在 App\Console\Kernel 中输入:

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
php复制代码// 定时检测订单状态,超过10分钟未支付的,作废
// 真是项目一般会采用长链接,因为可以实时长链接,时间一到,直接作废,而任务调度的话不会那么精确的跟前端显示一样的时间
// 任务调度的话 在一定的时间帮我们去执行任务 像备份数据...
$schedule->call(function () {
// info('hellp');
$orders = Order::where('status', 1)
->where('created_at', '<', date('Y-m-d H:i:s', time()-600))
->with('orderDetails.goods') // 拿到关联的商品
->get();

// 循环订单,修改订单状态,还原商品库存
try {
DB::beginTransaction(); // 开启门面

foreach($orders as $order) {
$order->status = 5;
$order->save();

// 还原商品库存
foreach($order->orderDetails as $details) {
$details->goods->increment('stock', $details->num);
}
}

Db::commit();
} catch (\Exception $e) {
DB::rollBack();
Log::error($e);
}

})->everyMinute();

在这里插入图片描述
这样在订单过期时,将会把订单状态设置成5,并且商品数量将回滚:
在这里插入图片描述


1.2 创建订单路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
php复制代码        /**
* 订单
*/
// 订单预览页
$api->get('order/preview', [OrderController::class, 'preview']);

// 提交订单
$api->post('order', [OrderController::class, 'store']);

// 订单详情
$api->get('order/{order}', [OrderController::class, 'show']);

// 订单列表
$api->get('orders', [OrderController::class, 'index']);

// 确认收货
$api->patch('orders/{order}/confirm', [OrderController::class, 'confirm']);

在这里插入图片描述


1.3 测试效果

在这里插入图片描述

测试的时候我们发现报错了,原因是我们之前在建订单表时,将订单单号设置成了integer在这里插入图片描述
所以我们来修改下这个字段的类型。
首先运行命令composer require doctrine/dbal。
在这里插入图片描述
接着运行:php artisan make:migration modify_order_no_column_in_orders_table --table=orders创建修改该字段的迁移文件:
在这里插入图片描述
接着写入:$table->string('order_no')->comment('订单单号')->change();
在这里插入图片描述
好的类型就修改好了。


接下来测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到生成了订单,并且购物车中数据也没了。

在学习的php的路上,如果你觉得本文对你有所帮助的话,那就请关注点赞评论三连吧,谢谢,你的肯定是我写博的另一个支持。

本文转载自: 掘金

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

Go语言搬砖 kafka生产消费消息

发表于 2021-11-23

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

kafka go客户端官方目前没有提供,但在github有2个非常流行的库

  1. 星星较多,网上案例也多 github.com/Shopify/sar…
  2. confluent官网提供的库 github.com/confluentin…

这里使用sarama,因为星星多,案例多,方便快速上手

注意

如果kafka版本在2.2以下,需要在go.mod里面将sarama版本改为 github.com/Shopify/sarama v1.24.1

这是因为sarama只提供最新2个版本+2个月的兼容保证,所以使用低版本kafka是需要避坑

使用非集群集群生产者时,需要自行去创建topic,如果使用集群生产者,集群会自动创建

例子

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

import (
"fmt"
"github.com/Shopify/sarama"
cluster "github.com/bsm/sarama-cluster"
"time"
)

var (
Consumer *cluster.Consumer
producer sarama.SyncProducer
brokers = []string{"ip1:9092","ip2:9092","ip3:9092"}
topic = "testGo"
groupId = "testGo_test1"
)

func initProducer() {
var err error
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForLocal
config.Producer.Retry.Max = 3
config.Producer.Return.Successes = true
brokers := brokers
producer, err = sarama.NewSyncProducer(brokers,config)
if err != nil {
fmt.Printf("生产者初始化失败 -> %v \n", err)
panic(err)
}
fmt.Println("生产者初始化成功。")
}

func initConsumer() {
var err error
config := cluster.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
config.Consumer.Offsets.Initial = sarama.OffsetNewest
Consumer, err = cluster.NewConsumer(brokers,groupId,[]string{topic},config)
if err != nil {
fmt.Printf("消费者初始化失败 -> %v \n", err)
panic(err.Error())
}
if Consumer == nil {
panic(fmt.Sprintf("消费者为空. kafka info -> {brokers:%v, topic: %v, group: %v}", brokers, topic, groupId))
}
fmt.Printf("消费者初始化成功, consumer -> %v, topic -> %v, ", Consumer, topic)
}

func main() {
initProducer()
initConsumer()

//生产消息
for i := 1;i < 100; i ++ {
pid, offset, err := producer.SendMessage(&sarama.ProducerMessage{
Topic: topic,
Key: sarama.StringEncoder(i),
Value: sarama.ByteEncoder("this is test message."),
})
if err != nil {
fmt.Println("发送消息失败, err:", err)
return
}
fmt.Printf("offset: %v\n", offset)
}

time.Sleep(2 * time.Second)

//消费消息
for {
select {
case msg, ok := <-Consumer.Messages():
if ok {
fmt.Printf("kafka msg: %s \n", msg.Value)
}
}
}
}

运行结果如下

image.png

该demo流程如下

  1. 引入单机sarama库和集群sarama库
  2. 定义连接变量
  3. 使用单sarama库实例化一个生产者
  4. 使用集群sarama库实例化一个消费者
  5. 循环100次发送100条消息
  6. 使用sarama自带的生产消息构造器设置消息内容
  7. 使用for让进程一直监听来自kafka的消息

本文转载自: 掘金

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

dart系列之 在dart中使用集合 简介 List的使用

发表于 2021-11-23

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

简介

dart中的集合有三个,分别是list,set和map。dart在dart:core包中提供了对于这三种集合非常有用的方法,一起来看看吧。

List的使用

首先是list的创建,可以创建空的list或者带值的list:

1
2
3
ini复制代码var emptyList =[];

var nameList = ['jack','mac'];

使用List的构造函数来创建:

1
ini复制代码var nameList = List.filled(2, 'max');

向list中添加元素或者list:

1
2
csharp复制代码nameList.add('tony');
nameList.addAll(['lili', 'bruce']);

删除元素:

1
2
ini复制代码nameList.removeAt(0);
nameList.clear();

dart提供了list的排序方法sort(),sort可以接一个比较的函数,用来表示谁在前谁在后:

1
2
3
css复制代码var names = ['jack', 'tony', 'max'];

fruits.sort((a, b) => a.compareTo(b));

list中还可以使用泛型,表示list中固定的类型:

1
2
3
ini复制代码var names = <String>[];

names.add('jack');

Set的使用

Set表示的是不重复的元素的集合。但是set和list不同的是set是无序的,所以你不能用index来定位set中的元素。

来看下set的基本用法:

1
2
3
4
5
6
7
8
9
csharp复制代码//创建一个空的set
var names = <String>{};

// 添加新的元素
names.addAll(['jack', 'tony', 'max']);

//删除元素

names.remove('jack');

或者使用Set的构造函数来构造Set:

1
ini复制代码var names = Set.from(['jack', 'tony', 'max']);

判断集合中元素是否存在:

1
2
3
java复制代码assert(names.contains('jack'));

assert(names.containsAll(['jack', 'tony']));

set还有一个intersection的操作,用来求两个set的交集:

1
2
3
4
5
ini复制代码var name1 = Set<String>();
name1.addAll(['jack', 'tony', 'max']);

var name2 = Set.from(['tony', 'bily']);
var intersection = name1.intersection(name2);

Map的使用

Map是一种key,value的数据类型,也是一种在程序中非常常见的数据类型。

先看下怎么创建Map:

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// 创建map
var studentMap = {
'name': 'jack',
'age': '18',
'class': 'class one'
};


var teacherMap = Map();

var teacherMap2 = Map<String, String>();

添加和删除:

1
2
3
javascript复制代码  var studentMap =Map();
studentMap.putIfAbsent('name', ()=>'jack');
studentMap.remove('name');

判断map中是否包含某个key可以使用containsKey():

1
java复制代码assert(studentMap.containsKey('name'));

常见的集合方法

集合中最常见的方法就是判断集合是否为空:

1
2
scss复制代码assert(studentMap.isEmpty);
assert(studentMap.isNotEmpty);

如果想对集合中的每个元素都进行某个函数操作,则可以使用forEach():

1
2
3
4
5
6
7
8
ini复制代码var names = ['jack', 'bob', 'tom'];

names.forEach((name) => print('the name is $name'));

var nameMap = {};

nameMap.forEach((k, v) {
});

如果是可遍历对象,则有一个map方法,map方法会返回一个新的对象:

1
2
3
ini复制代码var names = ['jack', 'bob', 'mark'];

var names2 = names.map((name) => name.toUpperCase());

注意,map返回的是一个Iterable,它是延时计算的,只有被使用的时候才会进行计算。

如果你想立即计算的话,则可以使用map().toList() 或者 map().toSet():

1
2
ini复制代码var names2 =
names.map((name) => name.toUpperCase()).toList();

可遍历对象还可以进行条件选择。比如使用where()来获得所有匹配的对象,使用any()来判断集合中是否有匹配的对象,使用every()来判断集合中是否全部匹配。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码var names = ['jack', 'bob', 'max'];

bool hasJack(String name) =>
name == 'jack';

var seleteJack =
names.where((name) => hasJack(name));

assert(names.any(hasJack));

assert(!names.every(hasJack));

总结

集合是在程序编写过程中非常常用的一种类型,大家一定要熟练掌握。

本文已收录于 www.flydean.com/15-dart-col…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

1…224225226…956

开发者博客

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