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

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


  • 首页

  • 归档

  • 搜索

物联网系列 - EMQ X HTTP认证

发表于 2021-11-10

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

往前文章:

物联网系列 - 初识MQTT

物联网系列 - MQTT协议原理与数据包结构

物联网系列 - EMQ X简介与安装

物联网系列 - EMQ X Dashboard

物联网系列 - EMQ X 认证介绍

物联网系列 - EMQ X Username 认证

物联网系列 - EMQ X Client ID 认证

HTTP 认证使用外部自建 HTTP 应用认证数据源,根据 HTTP API 返回的数据判定认证结果,能够实现复杂的认证鉴权逻辑。启用该功能需要将 emqx_auth_http 插件启用,并且修改该插件的配置文件,在里面指定HTTP认证接口的url。 emqx_auth_http 插件同时还包含了ACL的功能,我们暂时还用不上,通过注释将其禁用。

1:在Dashboard中中开启 emqx_auth_http 插件,同时为了避免误判我们可以停止通过username,clientID 进行认证的插件 emqx_auth_clientid , emqx_auth_username

1. 认证原理

EMQ X 在设备连接事件中使用当前客户端相关信息作为参数,向用户自定义的认证服务发起请求查询权限,
通过返回的 HTTP 响应状态码 (HTTP statusCode) 来处理认证请求。

  • 认证失败:API 返回 4xx 状态码
  • 认证成功:API 返回 200 状态码
  • 忽略认证:API 返回 200 状态码且消息体 ignore

2. HTTP 请求信息

HTTP API 基础请求信息,配置证书、请求头与重试规则。

1
2
3
4
5
6
7
8
9
10
11
shell复制代码# etc/plugins/emqx_auth_http.conf
## 启用 HTTPS 所需证书信息
## auth.http.ssl.cacertfile = etc/certs/ca.pem
## auth.http.ssl.certfile = etc/certs/client-cert.pem
## auth.http.ssl.keyfile = etc/certs/client-key.pem
## 请求头设置
## auth.http.header.Accept = */*
## 重试设置
auth.http.request.retry_times = 3
auth.http.request.retry_interval = 1s
auth.http.request.retry_backoff = 2.0

加盐规则与哈希方法

HTTP 在请求中传递明文密码,加盐规则与哈希方法取决于 HTTP 应用。

3. 认证请求

进行身份认证时,EMQ X 将使用当前客户端信息填充并发起用户配置的认证查询请求,查询出该客户端在
HTTP 服务器端的认证数据。

打开etc/plugins/emqx_auth_http.conf配置文件,通过修改如下内容:修改完成后需要重启EMQX服务

1
2
3
4
5
6
7
8
ini复制代码# etc/plugins/emqx_auth_http.conf
## 请求地址 填写 http 验证服务的地址
auth.http.auth_req = http://192.168.10.20:8991/mqtt/auth
## HTTP 请求方法
## Value: post | get | put
auth.http.auth_req.method = post
## 请求参数
auth.http.auth_req.params = clientid=%c,username=%u,password=%P

HTTP 请求方法为 GET 时,请求参数将以 URL 查询字符串的形式传递;POST、PUT 请求则将请求参数以普通
表单形式提交(content-type 为 x-www-form-urlencoded)。

你可以在认证请求中使用以下占位符,请求时 EMQ X 将自动填充为客户端信息:

  • %u:用户名
  • %c:Client ID
  • %a:客户端 IP 地址
  • %r:客户端接入协议
  • %P:明文密码
  • %p:客户端端口
  • %C:TLS 证书公用名(证书的域名或子域名),仅当 TLS 连接时有效
  • %d:TLS 证书 subject,仅当 TLS 连接时有效
    推荐使用 POST 与 PUT 方法,使用 GET 方法时明文密码可能会随 URL 被记录到传输过程中的服务器日志中。

4. 认证服务开发

创建基于springboot的应用程序: emq-demo

pom文件

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
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>emq-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>emq-demo</name>
<description>emq demo演示</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

创建application.yml配置文件并配置

1
2
3
4
5
yaml复制代码server:
port: 8991
spring:
application:
name: emq-demo

创建Controller

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
kotlin复制代码package com.example.emqdemo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/mqtt")
@Slf4j
public class AuthController {

private Map<String,String> users;

@PostConstruct
public void init(){
users = new HashMap<>();
//实际的密码应该是密文,mqtt的http认证组件传输过来的密码是明文,
// 我们需要自己进行加密验证
users.put("user","123456");
users.put("emq-client2","123456");
users.put("emq-client3","123456");
}

@PostMapping("auth")
public ResponseEntity<?> auth(@RequestParam("clientid") String clientid,
@RequestParam("username") String username,
@RequestParam("password") String password) {
log.info("emqx认证组件调用自定义的认证服务开始认证,clientid={},username={},password= {}",clientid,username,password);
//在此处可以进行复杂也的认证逻辑,但是我们为了演示方便做一个固定操作
String userPassword = users.get(username);
if (StringUtils.isEmpty(userPassword) || !userPassword.equals(password)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(HttpStatus.UNAUTHORIZED);
}
return ResponseEntity.ok(HttpStatus.OK);
}
}

MQTTX客户端验证

http认证.jpg

这个地方的Client-ID随便输入,因为在验证的代码里没有对该字段做校验,之后点连接,发现会连接成功,然
后可以去自定义的认证服务中查看控制台输出,证明基于外部的http验证接口生效了。在实际项目开发过程中,
HTTP接口校验的代码不会这么简单,账号和密码之类的数据肯定会存在后端数据库中,代码会通过传入的数据和
数据库中的数据做校验,如果成功才会校验成功,否则校验失败。

成功后可以在 emqx 控制台看到连接的客户端信息

http认证.jpg

当然EMQ X除了支持我们之前讲过的几种认证方式外,还支持其他的认证方式,比如:MySQL认证、
PostgreSQL认证、Redis认证、MongoDB认证,对于其他这些认证方式只需要开启对应的EMQ X插件并且配置对
应的配置文件,将对应的数据保存到相应的数据源即可。

本文转载自: 掘金

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

租售同体的书屋项目——书籍系统(二)——第二部分(阅读量)

发表于 2021-11-10

一、概述

书籍系统框架如图:

书籍系统.png

文件内容持续更新在GitHub上,可自行查看。

本篇主要是介绍:评论和阅读量中的阅读量统计

二、阅读量

思路

1.通过网页的访问次数来决定;

2.因为次数变化频繁,考虑使用redis做递增,再在某一时间点更新到数据库;

3.防止恶意刷流量,需要有个拦截器,同一个ip在一段时间内不计入次数;

代码

1.拦截器(中间件): 在Expire时间段内不计入次数,使用redis分布式锁进行判定,符合要求的相应文章阅读量则加1

traffic_statistics_middleware.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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
golang复制代码package Middlewares

import (
"WebApi/Svc"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"regexp"
)

var Expire = 10

func TrafficStatisticsMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
ip := c.ClientIP()
url := c.Request.URL
//redis 出错的情况下不记录阅读量并通知工作人员
if repeat, err := IsRepeat(ip + url.String()); err == nil {
if !repeat {
err = TrafficStatistics(url.String())
if err != nil {
fmt.Println(err)
}
}
} else {
fmt.Println(err)
}
c.Next()
}
}

//判斷訪問是否在指定時間內重複 true为重复,反之false
func IsRepeat(key string) (bool, error) {
ok, err := redis.Bool(Svc.SvcContext.Redis.Do("EXISTS", key))
if err != nil {
return false, err
}
if !ok {
_, err = Svc.SvcContext.Redis.Do("SET", key, []byte{}, "NX", "EX", Expire)
if err != nil {
return false, err
}
}
//重复
return ok, nil

}

//在redis记录访问量
func TrafficStatistics(key string) error {
re, err := regexp.Compile("[0-9]+") //解析出来哪本书哪个章节
if err != nil {
fmt.Println(err)
}
res := re.FindAll([]byte(key), -1)

key = "traffic_statistic"
if len(res) == 2 {
member := string(res[0]) + ":" + string(res[1])
_, err := Svc.SvcContext.Redis.Do("ZINCRBY", key, 1, member)
if err != nil {
return err
}
return nil
} else {
return errors.New("url不是正确的格式,无法用正则表达式匹配")
}
}

2.获取访问统计信息:所有访问量都从redis中获取。

ps: 这里我煞笔了,我一开始设想还统计某书某一章的阅读量,其实发现没啥大用,还多做了一层统计。–!

get_traffic_statistic_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
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
golang复制代码package action

import (
"WebApi/Svc"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"net/http"
"strconv"
"strings"
)

func GetTrafficStatisticByBookIdAndChapterNumHandler(c *gin.Context) {
bookId := c.Query("bookId")
chapterNum := c.Query("chapterNum")

//找到redis访问量的内容
key := "traffic_statistic"
res, err := redis.String(Svc.SvcContext.Redis.Do("ZSCORE", key, bookId+":"+chapterNum))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
v, err := strconv.Atoi(res)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, v)
}

func GetAllTrafficStatisticHandler(c *gin.Context) {
//找到redis访问量的内容
key := "traffic_statistic"
res, err := redis.StringMap(Svc.SvcContext.Redis.Do("ZRANGE", key, 0, -1, "WITHSCORES"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, res)
}

func GetAllTrafficStatisticHandlerByBookId(c *gin.Context) {
//通过书籍ID找到redis访问量的内容
bookId := c.Query("bookId")
key := "traffic_statistic"
res, err := redis.StringMap(Svc.SvcContext.Redis.Do("ZRANGE", key, 0, -1, "WITHSCORES"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var count int64
for k, _ := range res {
if strings.Split(k, ":")[0] == bookId {
n, err := strconv.ParseInt(res[k], 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
count += n
}
}

c.JSON(http.StatusOK, count)
}

3.定时让redis和数据库交换数据:使用”github.com/robfig/cron”做定时任务.

cron.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
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
golang复制代码package Utils

import (
"WebApi/Pb/action"
"WebApi/Svc"
"context"
"fmt"
"github.com/gomodule/redigo/redis"
"github.com/robfig/cron"
"strconv"
"strings"
)

func init() {
c := cron.New()

//凌晨5点更新DB中书籍的访问量
if err := c.AddFunc("0 0 5 * * ?", func() {
_ = TrafficStatisticsImportDB()
}); err != nil {
fmt.Println(err)
}
fmt.Println("cron start")
c.Start()
}

//定时在缓存和数据库之间进行访问量的同步
func TrafficStatisticsImportDB() error {
var err error
//找到所有redis访问量的内容
res, err := redis.StringMap(Svc.SvcContext.Redis.Do("ZRANGE", "traffic_statistic", 0, -1, "WITHSCORES"))
if err != nil {
return err
}
fmt.Println(res)
// Update to DB
var bookId, chapterNum, trafficNumber int64
for key, _ := range res {
bookId, err = strconv.ParseInt(strings.Split(key, ":")[0], 10, 64)
if err != nil {
return err
}

chapterNum, err = strconv.ParseInt(strings.Split(key, ":")[1], 10, 64)
if err != nil {
return err
}

trafficNumber, err = strconv.ParseInt(res[key], 10, 64)
if err != nil {
return err
}

rep, err := Svc.SvcContext.Grpc.ActionGrpc.GetTrafficStatisticByBookIdAndChapterNum(context.Background(),
&action.TrafficStatisticReq{
BookId: bookId,
ChapterNum: chapterNum})
fmt.Println(rep, err)

if err != nil {
if err.Error() == "rpc error: code = Unknown desc = sql: no rows in result set" {
_, err := Svc.SvcContext.Grpc.ActionGrpc.CreateTrafficStatistic(context.Background(), &action.TrafficStatisticReq{
BookId: bookId,
ChapterNum: chapterNum,
TrafficNumber: trafficNumber,
})
if err != nil {
return err
}
} else {
return err
}
} else {
if trafficNumber > rep.TrafficNumber {
_, err := Svc.SvcContext.Grpc.ActionGrpc.UpdateTrafficStatistic(context.Background(), &action.TrafficStatisticReq{
Id: rep.Id,
BookId: bookId,
ChapterNum: chapterNum,
TrafficNumber: trafficNumber,
})
if err != nil {
return err
}
}
}
}

//DownLoad to Redis(防止Redis数据丢失)
if len(res) == 0 || res == nil {
resp, err := Svc.SvcContext.Grpc.ActionGrpc.GetAllTrafficStatistics(context.Background(), &action.Request{})
if err != nil {
return err
}
ts := resp.TrafficStatistics
key := "traffic_statistic"
for i, _ := range ts {
_, err = Svc.SvcContext.Redis.Do("ZADD", key, ts[i].TrafficNumber, strconv.FormatInt(ts[i].BookId, 10)+":"+strconv.FormatInt(ts[i].ChapterNum, 10))
if err != nil {
return err
}
}
}
return nil
}

4.结果展示

阅读量统计.png

三、Tips

最近工作中忙了起来,更新可能会比之前慢一些,请多多包涵。

本文转载自: 掘金

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

场景模型驱动自动化测试在盒马的探索及实践 一 引言 二 背景

发表于 2021-11-10

简介: 盒马业务有如下几个特点:线上线下一体化、仓储配送一体化、超市餐饮一体化、经营作业一体化、多业态与平台化。在以上的种种原因,生鲜及物流体验是盒马的特点,但仓储配送一体化作业中,如何能更高效的提升测试效率也是盒马质量团队的重点探索。

作者 | 钦伟

来源 | 阿里技术公众号

一 引言

盒马业务有如下几个特点:线上线下一体化、仓储配送一体化、超市餐饮一体化、经营作业一体化、多业态与平台化。

在以上的种种原因,生鲜及物流体验是盒马的特点,但仓储配送一体化作业中,如何能更高效的提升测试效率也是盒马质量团队的重点探索。

二 背景及待解决问题介绍

1 盒马自动化体系发展新挑战

在盒马,前期业务在狂奔,自动化基础较薄弱,近三年来,经过盒马人的不断突破,已经具备了一定的自动化体系,因为盒马业务的特点,盒马属于麻雀虽小但五脏俱全,有独立App,有自营的物流体系,有自己的供应链体系,因此在自动化方面,我们从最基础的单元测试、到接口测试、再到领域场景自动化及跨领域的自动化以及端的自动化方面都有积累。即便如此,我们的代码覆盖率在超过50%之后很难有比较大的提升,另外,代码的覆盖并不能全部代表业务场景的覆盖,一些线上漏测的问题仍然偶尔发生,因此,对于盒马来说,基于较全场景的测试是必须。

在这种背景下,盒马质量团队进行了较全场景驱动自动化测试的探索,利用线上数据建立测试场景模型。下面会更加详细的进行讲述。

2 业务场景全覆盖的挑战

从业界来说,比较难的也同样是如何用比较简单的手段做到业务的全场景覆盖,对盒马来说也同样。

首先,盒马的业务场景众多,包括inbound与outbound全流程,端到端的全流程多业态,含O2O模式、B2C模式、F2模式、Mini模式、Mall模式、X会员店模式、产地量贩模式、盒马邻里模式等。这么多种业务场景很难一一枚举。

其次,业务场景自动化编写效率也较低,在人工枚举场景,脚本化实现,这种效率比较低,场景难以枚举全,容易遗漏。

业务场景的真实覆盖率也难以度量,人工枚举的业务场景极易有遗漏,线上已频发漏测问题,无法覆盖线上全量场景,同时测试的场景覆盖率难以衡量,需要找到线上场景分母。

传统自动化来说,校验一般基于字段级别,容易遗漏。传统校验方式根据预期逐个字段加断点校验,新增字段或缺失字段极容易造成遗漏。

因此,在这种多种挑战下,我们尝试了基于线上海量数据模型构建全行经模型,同时用场景驱动自动化执行的方案。

3 场景模型驱动自动化的思考

回到本文初心,我们希望通过线上场景来驱动自动化测试的方式进行测试场景的全量覆盖。思路如下:

一、场景业务模型构建:重点在于如何自动化的构建出场景化的模型数据。大致的思路为:1)线上执行过后的订单数据存在诸多特征;2)根据线上落盘数据进行特征值分析;3)构建数据特征集合与对应的样本数据;

二、执行链路构建:重点在于如何自动构建出克执行的系统调用链路。大致思路为:1)基于落盘数据获取线上执行全链路的所有鹰眼;2)根据鹰眼(trace)及系统调用关系构建执行链路;3)执行链路编排构建链路执行能力;

三、执行结果的校验:重点在于如何自动的进行结果数据的一致性校验。大致思路为:1)所有的数据最终会持久化落盘;2)基于持久化数据进行全字段对比;3)忽略规则配置;

三 解决思路

1 模型驱动自动化解决策略

结合上文的背景及思考,推演出本文的模型驱动自动化解决策略包含:特征提取、场景建模、链路执行、结果验证、覆盖率分析、缺陷定位及报告。

2 业务场景建模问题定义

针对业务场景模型,我们首先要思考我们面向的是什么系统,系统的规律是什么,该系统的哪些数据可以被规则化出来的,如何规则出来,最终如何表达,带着这些问题我们一步步介绍我们的解决方案。

业务场景建模-特征提取方法

本节重点介绍特征提取的通常方法,当前阶段,我们是以数据库的全量数据作为特征提取的来源,当然不少团队也在尝试使用接口调用过程中的全量入参数据。具体为:

1)DB全量数据查询:通过odps查询方式获取全量多表关联数据,用以作为分析的数据源。

2)数据的聚合:对于查询的数据进行信息补齐后,字段打平,采用聚类的方式针对每一字段进行聚合,以出现有限数量的字段作为特征字段进行基线特征的沉淀,对于离散型的数据会选择合适的区间进行分段处理。

3)特征推荐:针对上述聚合的内容进行推荐,此部分会将潜在的特征字段全量进行推荐。

4)特征基线沉淀:基于推荐的数据,结合专家经验进行特征字段的选取,并进行标注选择为基线特征。

接下来一一根据细化场景进行介绍。

业务场景的建模-特征提取过程

如下图所示,为数据表数据示例,从数据层面可以看出,有一部分字段是有意义的,如isParent是否主单,businessType业务类型,orderTerminal订单终端类型等等,也有一部分字段是离散且无意义的,如orderId订单ID,itemId商品id,GMTCreate创建时间等。特征提取的过程目标就是自动的提取出有意义的字段,忽略无意义的字段。

实际实践过程中,我们通过不断的迭代以提升特征的精准度与全面度,具体的核心几个提取过程为:

1、特征扩充:元数据中的字段有可能为原始数据,这部分需要关联到具体数据表并找出有意义的字段。

2、特征分类:根据数据的聚合,对于有意义的离散类型数据,比如订单总价,往往我们希望得到零价订单,高值订单及普通订单三类,这三类是未自动打标的,需要我们聚合出范围在特征提取过程中动态识别并分类。

3、特征聚合:依赖于特征的规则,进行所有字段的聚合,最终根据枚举类型字段出现次数进行有效判断,目前我们设定的值为20,这个值可以动态调整,仅仅为参考值而已。

4、特征决策:针对聚合出来的潜在特征,进行基于代码、经验、默认值等多种维度的判断,最终进行特征的推荐,这部分因为业务属性比较重,我们在推荐出来的同时,最终更依赖于专家经验进行字段的最终判断,目前推荐出来和最终采纳的比例约为50%,我们后续会升级算法和参考维度进一步提升采纳率。

接下来,针对以上流程中关键环节会进行一一介绍。

1)特征提取-特征扩充

本文举例商品及仓的场景,对于商品根据商品id关联找到对应商品明细,再将商品明细中有意义的字段,比如:是否是危险品、是否是紧急配送商品、商品的标签、商品的状态等等查询出来关联主数据,对于仓关联查出仓的类型和仓的标签,如此可基于场景的主模型数据进行分支场景的多层级关联,将需要关注到的场景维度值尽可能多的纳入到数据模型中。

2)特征提取-特征聚类

本文举例对于加工时长bomCost字段,对于标品来说是0,对于加工品来说,比如鱼类,需要增加15分钟宰杀作业时间,对于凉拌菜等需要增加10分钟进行制作等等。此处会单独将特定字段进行区间分类,如此将分类后的值进行特征的挖掘基础,即可将离散的值变得有意义。

3)特征提取-特征聚合

将所有数据进行扩充完毕后,将所有相关字段进行打平处理,根据相同字段进行值的聚合,相同值记录次数,不相同时进行归类,如此便可将相关数据进行初始化的数据处理,然后根据聚合出来的数据进行默认值个数的判断进行特征的推荐。

4)特征提取-特征决策

依赖上述聚合出来的全量潜在特征数据,在特征决策模块会基于代码中抽象出的特征字段进行匹配,当然最重要的是依赖于业务领域的测试专家经验进行主动识别标注,最终沉淀出领域的基线特征集合。

以盒马某业务领域为例,下图展示的是最终有效的特征集合,根据基线特征,我们的做法是都标注了具体的含义,如此,便可很容易根据一个领域的业务特征识别出该领域的数据场景。

同样,根据特征情况,也可以刻画出每个领域的特征模型,如下图所示,很轻松的看出领域的全量特征,同时根据每个特征,可以清晰的看出每个特征值的分布占比情况。

业务场景建模-场景提取

有了基线特征后,基于基线特征形成解析规则,再将全量数据基于特征规则匹配处理,对于命中规则的进行打标处理,即可识别出匹配基线特征的数据集合,这些数据集合对于每一条数据代表的特征组我们称之为场景。具体的处理流程如下图所示。

进行特征规则匹配处理后,可识别出场景集,这些场景的集合对我们来说至关重要,因为这些场景集合从一定意义上要代表我们的线上全量场景。如下图所示,除了有场景的推荐,还有场景对应的数据的推荐。这些数据后续我们会进行处理并进行执行链路的驱动。

以下为推荐以驱动链路自动化执行的场景及数据情况。

3 执行链路分析及构建

前文有介绍盒马很多业务领域都是链路式驱动类型,所以对于如何构建出领域的执行链路很关键,我们的思路是通过系统的调用日志及鹰眼trace相结合的方式进行聚合清洗得到领域的大概执行链路推荐,这里面的推荐会有多种情况。整体的思路如下图。

执行链路推荐出来后,这时候的链路还是无法执行的,我们的目标是根据推荐能够自动生成执行链路,只是当前基于进展的考虑,我们先将推荐的链路进行人工链路编排以执行场景模型中的数据。以盒马”履约”领域的系统执行流程链路为例,如下,我们将对所有业态的数据的处理进行统一流程编排,如此,即可更大限度和真实的处理模型数据。

4 数据校验

数据执行完成之后,对于自动化来说一个最关键且有意义的事情是进行结果的校验,为了能够更全面的比对结果,我们将执行链路进行线上生产环境和测试环境的双向执行,对于两套环境的结果数据进行全量的数据比对,可将比对结果精确到像素级别。

5 场景覆盖率分析

针对场景覆盖率,我们将场景模型所转换的测试数据对应的全量场景与线上全量场景进行比较得出场景覆盖率。在今年的OKR目标下,我们也是目标将业务场景覆盖率达到80%以上。

四 产品解决方案

1 产品解决方案架构图

结合以上核心模块的介绍,系统的产品解决方案框图如下所示,基于核心的场景模型驱动自动化执行过程为基准,分为业务场景建模模块、测试数据生产模块、回归策略执行选择模块、链路用例执行、结果校验以及结果推送模块。基于运维视角,包含统计大盘、用例管理等。

在盒马场景探索的场景模型驱动自动化,已经在交易、履约、商品、配送、自提等诸多领域进行了实践,并取得了一定的成果。同时在发布回归、小量数据的常规化压测、系统重构、线上业务巡检等诸多场景中获得了不错的使用。

五 实践结果

基于场景模型驱动自动化的模式,已经在盒马事业群内多个团队进行了推广接入使用,整体上累计沉淀有效的场景化用例2000以上,场景的特征覆盖率均大于90%,各个领域新接入业务的自动化构建成本直线降低80%以上,有效的解决了传统脚本方式的人工依赖过重,引流方式无法覆盖链路场景的问题,也成为团队内重要的发布前的回归可信赖保障手段。

1 在盒马交易领域的实践

盒马交易域是比较早的实践方之一,基于上述方案,交易域进行了整体的方案对接,沉淀了大量的场景及回归用例。尤其交易领域在覆盖率提升方面做了较多的工作。

在交易领域的特征覆盖率的提升经过了以下几个阶段,初始的测试单据(日常测试单据+链路自动化单据)特征覆盖率仅50%左右,覆盖率确实不高,通过覆盖率报告的分析发现,除了确实无单据覆盖的特征,还有一类问题是有单据覆盖但是平台无法匹配上的特殊字符类特征值或不可穷尽枚举类特征值,经过二次处理后,特征覆盖率提高到65%左右;第三阶段,现有的全业态全场景用例构建出来后覆盖率提升到90%左右,剩余的10%特征基本是线上特定时期小概率出现场景阶段性单据没分析到或暂时无法构建数据的特征,经过指定单据有针对性地补充后特征覆盖率达到96%,至此覆盖率达到我们期望的95%以上,基线用例搭建完成。

因此,从最终结果看,交易领域属于基于最终结果反推过程执行,整体接入过程,基于场景构建与链路编排的执行能力,结合自身能力的构建,接入过程约三周,推荐场景累计2000多例,对8种业态进行全面支撑,能够做到快速的回归覆盖,很好的做到需求的高质量持续交付。

2 在盒马其他典型领域的实践

在商品领域,从全新的领域对接,整体投入2天,完成500+线上场景的接入,整体投入成本下降高达90%,代码覆盖率在原有脚本自动化基础上提升了28%,快速达到50%以上。

在履约领域:整体沉淀场景化链路用例1000+,成功率95%以上,特征覆盖率95%以上,链路的仿真精准度90%以上,全面保障履约域的质量。

在配送领域:配送域沉淀场景化链路用例300+,作为作业操作的系统,链路场景的覆盖率97%以上,特征覆盖率100%,新业务的自动化成本下降80%以上。

六 未来展望

基于场景模型驱动自动化实现,只是开始,远没有终结,前期只是针对一部分领域进行了探索实践落地,接下来希望能够扩展更多领域的同时,更加深入的挖掘自动化用例自动生成及测试数据保活的能力,使之能够更好的服务好盒马的各团队业务,也希望通过本文的分享,启发更多的团队一起投入进行探索并做出尝试。

七 结束语

本文的探索方案前后历时近一年半的时间,从0开始投入,团队也累计多人参与,投入虽大,但给我们的质量保障工作带来了不错的回报,接下来的时间里也会继续前行探索,勇于尝试。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

关于PDB的几种启动方式 二、触发器启动 三、SAVE ST

发表于 2021-11-10

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

自从12C开始支持pdb以来,我们多多少少的接触或是使用了ORACLE的CDB+PDB的模式,对于数据库实例开启后,PDB为mount状态,需要再次开启,我想大家应该也觉得不是很方便。

下面就来聊聊关于PDB启动的几种方式:

一、手动启动

打开数据库实例时,默认PDB是mounted状态,需要手动执行命令打开PDB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL MOUNTED

--启动所有PDB
SQL> alter pluggable database all open;

Pluggable database altered.

SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL READ WRITE NO

这个方法,最为常见,但是每次开库都要去手动执行,在当今自动化运维的社会,显得很不自动化,如果为RAC数据库,需要每个实例都手动去开启。

二、触发器启动

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
sql复制代码--创建触发器,cdb启动时,open所有的pdb
CREATE TRIGGER open_all_pdbs
AFTER STARTUP
ON DATABASE
BEGIN
EXECUTE IMMEDIATE 'alter pluggable database all open';
END open_all_pdbs;
7 /

Trigger created.

--查看触发器
SQL> select owner,TRIGGER_NAME,TRIGGER_TYPE from dba_triggers where owner='SYS' and TRIGGER_NAME='OPEN_ALL_PDBS';

OWNER TRIGGER_NAME TRIGGER_TYPE
-------------------- -------------------- ----------------
SYS OPEN_ALL_PDBS AFTER EVENT

SQL> select text from all_source where type='TRIGGER' AND name='OPEN_ALL_PDBS';

TEXT
-----------------------------------------------------------------------------------
TRIGGER open_all_pdbs
AFTER STARTUP
ON DATABASE
BEGIN
EXECUTE IMMEDIATE 'alter pluggable database all open';
END open_all_pdbs;

6 rows selected.

--测试触发器是否生效
SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL READ WRITE NO
--关闭所有pdb
SQL> alter pluggable database all close;

Pluggable database altered.
--查看pdb是否全部关闭
SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL MOUNTED
--关闭数据库实例
SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
--启动数据库实例
SQL> startup
ORACLE instance started.

Total System Global Area 3355441944 bytes
Fixed Size 9141016 bytes
Variable Size 704643072 bytes
Database Buffers 2634022912 bytes
Redo Buffers 7634944 bytes
Database mounted.
Database opened.
--pdb已自动启动
SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL READ WRITE NO

三、SAVE STATE

通过设置视图DBA_PDB_SAVED_STATES来控制PDB的启动模式:

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
bash复制代码--这里我们先DROP触发器
SQL> drop trigger OPEN_ALL_PDBS;

Trigger dropped.
SQL>
SQL> select owner,TRIGGER_NAME,TRIGGER_TYPE from dba_triggers where owner='SYS' and TRIGGER_NAME='OPEN_ALL_PDBS';

no rows selected

SQL> select text from all_source where type='TRIGGER' AND name='OPEN_ALL_PDBS';

no rows selected

--设置state为open
SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL READ WRITE NO
SQL>
--记录当前所有pdb的启动状态
SQL> alter pluggable database all save state;

Pluggable database altered.

SQL> set line222
SQL> col con_name for a20
SQL> col instance_name for a20
SQL> select * from dba_pdb_saved_states;

CON_ID CON_NAME INSTANCE_NAME CON_UID GUID STATE RES
---------- -------------------- -------------------- ---------- -------------------------------- -------------- ---
3 ORCL lucifer 251291369 BF269544BE8B17F9E053AC01A8C0447F OPEN NO

--重启数据库实例
SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> startup
ORACLE instance started.

Total System Global Area 3355441944 bytes
Fixed Size 9141016 bytes
Variable Size 704643072 bytes
Database Buffers 2634022912 bytes
Redo Buffers 7634944 bytes
Database mounted.
Database opened.
--PDB已自动启动
SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL READ WRITE NO
--如何取消自动启动
--先关闭pdb
SQL> alter pluggable database ORCL close;

Pluggable database altered.

SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL MOUNTED

--记录当前关闭状态
SQL> alter pluggable database ORCL save state;

Pluggable database altered.

SQL> select * from dba_pdb_saved_states;

no rows selected

--重启数据库实例
SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> startup
ORACLE instance started.

Total System Global Area 3355441944 bytes
Fixed Size 9141016 bytes
Variable Size 704643072 bytes
Database Buffers 2634022912 bytes
Redo Buffers 7634944 bytes
Database mounted.
Database opened.

--pdb为mounted状态
SQL> show pdbs

CON_ID CON_NAME OPEN MODE RESTRICTED
---------- ------------------------------ ---------- ----------
2 PDB$SEED READ ONLY NO
3 ORCL MOUNTED

总结:

三种方式都可以打开PDB,孰优孰劣大家自可斟酌。

个人建议是第三种方式,从12C开始就可以支持,设置简单,方便快捷,缺点是基于实例的,如果是RAC需要实例都需要去保存一下。不像触发器是基于数据库的,当然触发器可以是万能的。

本文转载自: 掘金

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

Grafana 安装启用和钉钉报警

发表于 2021-11-10

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

Grafana钉钉报警的小卡片点击时无法跳转到Grafana的界面

在Grafana的配置文件.ini里

1
ini复制代码root_url = 'xxxx'

配置上地址重启即可

一、grafana安装与启用

我这里使用的docker方式

官方文档:grafana.com/docs/grafan…

1.数据接入、仪表盘配置展示、各指标含义本篇不详解,请参看这篇:

www.jianshu.com/p/7e7e0d067… by 简书-kang少年

2.直接三挡起步可以fork这个分支:github.com/monitoringa…

里面有很全面很正统的常见数据源grafana模板,下载再倒入就可以了

3.注意模板类型的dashboard只能用于监控和展示,接入警报需要自定义query

二、钉钉机器人创建与配置

钉钉开发者文档:ding-doc.dingtalk.com/doc#/server…

1.创建钉钉群&钉钉机器人

创建一个自定义机器人

2.在“机器人设置”中获得webhook的URL

获得webhook的URL

3.安全设置,这一步是必须的,我选择白名单模式,填入grafana服务器地址

安全设置-白名单

三、grafana设置警报

1.在grafana控制台,左边栏“Alerting”模块,创建一个警报。

Disable Resolve Message 表示健康监测为[OK]时,不发送信息。

2.创建一个测试用的dashboard和panel ,按“E”进入编辑模式,先创建一个query,选择数据源、检测项、实例ID、数据获取间隔;

3.创建一个报警规则

  • Name 自定义警报名称
  • Evaluate every 健康检测频率
  • For 由pending变为alerting状态需要的时间

Send to 警报扳机

  • Message 警报文案

4.设置一个较小的警报阈值用于测试,回到钉钉查看机器人消息

//记得打开Disable Resolve Message标签,这样[OK]状态就不会发警报了

四、其他实施细节

1)注意修改AWS控制台的EC2监控,启用“详细监控”,实际就是数据抓取频率5min → 1min

2)基本沿用测试套路,为常用的server设置报警,可以多个query放到一个panel当中

监控项:CPU负载

健康监测:每分钟计算前5分钟CPU负载平均值,大于80则报警

报警规则:均值大于80变为“pending”状态,pending状态延续3分钟启动Alert

杂项:钉钉群公告、响应人员协调、测试机器人转正、修改机器人头像

五、完善与扩展

grafana接入钉钉机器人只支持link模式,在文中使用link只是当一个文本预览使用,以下是一个link样例

1
2
3
4
5
6
7
8
9
json复制代码{
"msgtype": "link",
"link": {
"text": "这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林",
"title": "时代的火车向前开",
"picUrl": "",
"messageUrl": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI"
}
}

可以修改对应字段,丰富钉钉机器人的功能,比如点击链接直接转到服务控制台、监控仪表盘

本文转载自: 掘金

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

从零开始摸索VUE,配合Golang搭建导航网站(十Gin

发表于 2021-11-10

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

前言

上篇主要实践Gin框架ORM使用,做了一个简单的查询,但是还是有点瑕疵,把所有数据库配置写在了一个单文件中,没有容错处理,今天就这三个方向进行处理一下,暂时没想好项目项目架构,只好把大部分内容暂时都写在一个单文件,因为只有一个接口,并且把它使用Docker运行起来。

优化配置文件

在项目根目录新建一个config.ini文件,输入以下我觉得必要的内容:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码[server]
# debug 开发模式,release 生产模式
AppMode = release
HttpPort = :8080

[database]
Db = mysql
DbHost = ***.***.***.***
DbPort = 3306
DbUser = *****
DbPassWord = *********
DbName = ********

优化框架报错

为了前端更好的处理接口的内容,处理相关报错,统一下接口规范,内容有点多,新建一个文件夹global,再该文件夹新建一个golab.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 global

import (
"net/http"

"github.com/gin-gonic/gin"
)

//Result 暴露
type Result struct {
Ctx *gin.Context
}

//ResultCont 返回的结果:
type ResultCont struct {
Code int `json:"code"` //提示代码
Msg string `json:"msg"` //提示信息
Data interface{} `json:"data"` //数据
}

//NewResult 创建新返回对象
func NewResult(ctx *gin.Context) *Result {
return &Result{Ctx: ctx}
}

//Success 成功
func (r *Result) Success(data interface{}) {
if data == nil {
data = gin.H{}
}
res := ResultCont{}
res.Code = 200
res.Data = data
res.Msg = "成功!"
r.Ctx.JSON(http.StatusOK, res)
}

//Error 出错
func (r *Result) Error(code int, msg string, data interface{}) {
if data == nil {
data = gin.H{}
}
res := ResultCont{}
res.Code = code
res.Msg = msg
res.Data = data
r.Ctx.JSON(http.StatusOK, res)
}

其他优化

在启动文件新增了环境变量的读取,接口规范化:

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

import (
"fmt"
"main/global"

"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"gopkg.in/ini.v1"
)

var db *gorm.DB
var HttpPort string
var AppMode string

type UrlType struct {
ID uint `gorm:"primary_key"`
Name string
UrlLists []UrlList `gorm:"FOREIGNKEY:TypeID;ASSOCIATION_FOREIGNKEY:ID"`
}

type UrlList struct {
ID uint `gorm:"primary_key"`
TypeID uint // 默认外键, 用户Id
Name string
URL string
}

type config struct {
App string
}

func init() {
//配置文件路径
file, err := ini.Load("config.ini")
if err != nil {
fmt.Println("配置文件读取错误,请检查文件路径:", err)
}
//读取项目运行配置
HttpPort = file.Section("server").Key("HttpPort").String()
AppMode = file.Section("server").Key("AppMode").String()
//读取数据配置
DBUser := file.Section("database").Key("DbUser").String()
Password := file.Section("database").Key("DbPassWord").String()
Host := file.Section("database").Key("DbHost").String()
Port := file.Section("database").Key("DbPort").MustInt(3306)
Db := file.Section("database").Key("DbName").String()
connArgs := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", DBUser, Password, Host, Port, Db)
//创建一个数据库的连接
db, err = gorm.Open("mysql", connArgs)
if err != nil {
panic(err)
}
db.SingularTable(true) //去掉数据库自动迁移数据名加的s
db.AutoMigrate(&UrlList{}, &UrlType{}) //自动迁移
}

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
defer db.Close()
r.GET("/index", func(c *gin.Context) {
var list []UrlType
db.Debug().Preload("UrlLists").Find(&list)
result := global.NewResult(c)
result.Success(list)
})
gin.SetMode(AppMode)
r.Run(HttpPort)
}

现在看看接口返回的内容:

image.png
非常的规范化了,成功的接口有code,msg,data三大部分

DockerFile编写

DockerFile目的就是把项目打包起来,方便项目部署,golang是一个编译型的语言,我们看可以采用多段编译优化镜像大小,先编译使用一个基础镜像,再把编译打包出来的二进制文件,放到另外一个镜像运行打包发布。

在项目根目录新建一个文件名为:Dockerfile,注意暴露的端口号,输入以下内容:

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复制代码##
# ---------- building ----------
##
#编译用这个镜像
FROM golang:alpine AS builder

#设置工作路径
WORKDIR /build

#把项目所有文件复制到镜像的根目录文件夹中
ADD . ./

# 设置Go语言的环境变量,打开Go Module模式。设置包下载源,有利于快速下载包
ENV GO111MODULE=on \
GOPROXY=https://goproxy.cn

#下载go.mod里面的包
RUN go mod download

#编译成二进制文件
RUN go build -o my_gin_web .

##
# ---------- run ----------
##
#换一个镜像
FROM alpine:latest

#采用相同的工作目录
WORKDIR /build/

#把编译好的文件复制到到运行镜像的目录下
COPY --from=builder /build .

#打开端口
EXPOSE 8080

#运行二进制文件
ENTRYPOINT ["./my_gin_web"]

然后打包镜像:

1
erlang复制代码docker build -t my_gin_web:v1 .

运行容器:

1
css复制代码docker run -d --name my_gin_web  -p 8080:8080  my_gin_web:v1

在docker 客户端就可以看到运行完成:

image.png
接口也正常:
image.png

总结

今天稍微了优化了Gin框架细节,制作gin镜像,运行容器,下篇准备部署线上环境,做一下相关CI脚本。

本文转载自: 掘金

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

dart系列之 dart类中的构造函数 简介 传统的构造函数

发表于 2021-11-10

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

简介

dart作为一种面向对象的语言,class是必不可少的。dart中所有的class,除了Null都继承自Object class。 要想使用dart中的类就要构造类的实例,在dart中,一个类的构造函数有两种方式,一起来看看吧。

传统的构造函数

和JAVA一样,dart中可以使用和class名称相同的函数作为其构造函数,这也是很多编程语言中首先的构造函数的创建方式,我们以Student类为例,来看看dart中的构造函数是怎么样的:

1
2
3
4
5
6
7
8
9
ini复制代码class Student {
int age = 0;
int id = 0;

Point(int age, int id) {
this.age = age;
this.id = id;
}
}

上面的this表示的是当前类的实例,对dart来说,this是可以忽略的,但是在上面的例子中,因为类变量的名字和构造函数传入参数的名字是一样的,所以需要加上this来进行区分。

上面的代码虽然很简单,但是写起来还是有太多的内容,下面是dart中的一种简写方式:

1
2
3
4
5
6
ini复制代码class Student {
int age = 0;
int id = 0;

Student(this.age, this.id);
}

当然,你也可以不指定构造函数,这样的话dart会为你创建一个默认的无参的构造函数。

命名构造函数

dart和其他语言不同的地方是,还可以使用命名构造函数。命名构造函数的格式是ClassName.identifier,如下所示:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码
class Student {
int age = 0;
int id = 0;

Student(this.age, this.id);

Student.fromJson(Map data) {
print('in Student');
}
}

上面的Student.fromJson就是一个命名构造函数。可以使用该构造函数从Map中生成一个Student对象,有点像是java中的工厂方法。

构造函数的执行顺序

我们知道,dart中的类是可以继承的,那么对于dart中的子类来说,其构造函数的执行顺序是怎么样的呢?

如果不给dart类指定构造函数,那么dart会为类自动生成一个无参的构造函数,如果这个类是子类的话,则会自动调用父类的无参构造函数。

那么对应子类的构造函数来说,初始化的时候有三步:

  1. 调用初始化列表
  2. 调用父类的构造函数
  3. 调用自己的构造函数

在步骤2中,如果父类没有默认的无参构造函数,则需要手动指定具体父类的构造函数。怎么调用呢?可以直接在子类的构造函数后面使用:操作符接父类的构造函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码class Student {
String? firstName;

Student.fromJson(Map data) {
print('in Student');
}
}

class Jone extends Student {

Jone.fromJson(Map data) : super.fromJson(data) {
print('in Jone');
}
}

理解了父类的构造函数之后,我们再看一下什么是初始化列表呢?

初始化列表就是在构造函数执行之前执行的代码,和调用父类的构造函数一样,也使用:操作符,如下所示:

1
2
3
4
5
dart复制代码Point.fromJson(Map<String, double> json)
: x = json['x']!,
y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}

重定向构造函数

如果一个构造函数需要调用另外一个构造函数,而其本身并不进行任何变动,这可以使用重定向构造函数,重定向构造函数也使用:操作符,后面跟的是另外的构造函数:

1
2
3
4
5
6
7
8
9
kotlin复制代码class Point {
double x, y;

// 主构造函数
Point(this.x, this.y);

// 重定向构造函数
Point.alongXAxis(double x) : this(x, 0);
}

Constant构造函数

如果对象中的属性在创建之后,是不会变化的,则可以使用Constant构造函数, 也就是在构造函数前面加上const修饰符,初始化的所有属性都要以final来修饰:

1
2
3
4
5
6
7
arduino复制代码class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);

final double x, y;

const ImmutablePoint(this.x, this.y);
}

工厂构造函数

默认情况下,dart类中的构造函数返回的是该类的新实例,但是我们在实际的应用中可能会对返回的对象做些选择,比如从缓存中返回已经存在的对象,或者返回该类具体的实现子类。

为了实现这样的功能,dart中专门有一个Factory关键字,使用Factory的构造函数叫做工厂构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码class Student {
final String name;

static final Map<String, Student> _studentMap =
<String, Student>{};

factory Student(String name) {
return _studentMap.putIfAbsent(
name, () => Student._newStudent(name));
}

factory Student.fromJson(Map<String, Object> json) {
return Student(json['name'].toString());
}

Student._newStudent(this.name);
}

注意,dart中只能有一个未命名的构造函数,对应命名函数来说,名字不能够重复,否则会报The default constructor is already defined异常。

上面的代码中,factory Student是一个未命名构造函数,而factory Student.fromJson则是一个命名构造函数。

所以如果你再给Student加一个未命名构造函数,如下:

1
kotlin复制代码Student(this.name);

则会报错。

那么问题来了,factory构造函数和普通构造函数到底有什么区别呢?

他们最大的区别就是普通构造函数是没有返回值的,而factory构造函数需要一个返回值。

总结

以上就是dart中各种构造函数,和使用过程中需要注意的问题。

本文已收录于 www.flydean.com/06-dart-cla…

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

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

本文转载自: 掘金

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

k8s series 21 calico初级(简介)

发表于 2021-11-10

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

calico简介

image.png

calico是什么?

Calico 是一个开源网络和网络安全解决方案,适用于容器、虚拟机和基于主机的本地工作负载。Calico支持广泛的平台,包括 Kubernetes、OpenShift。

无论使用Calico的eBPF数据平面还是 Linux 的标准网络管道,Calico都能提供极快的性能和真正的云原生可扩展性

Calico支持公有云和本地运行,支持在单个节点或数千个节点集群中运行

calico优点

  • 支持linux eBPF数据通道,标准的linux网络数据通道,还支持windows HNS数据通道
  • 丰富的网络策略,内置对wireguard加密支持,保护pod-pod数据安全
  • Calico使Linux eBPF或Linux内核高度优化的标准网络管道来提供高性能网络
  • Calico的控制平面和策略引擎做了很好的调整,使占用和使用cpu率最小化
  • 支持超级可扩展性,满足几十节点,到十千个节点的伸缩
  • 支持L2,L3层网络
  • 不对数据进行解包压包,不需要nat和端口映射,性能强悍

calico安装

将calico安装在kubenetes集群中有很多种方式,这里选择一种比较方便的yaml文件方式

1
2
3
4
5
6
js复制代码#3.20版本只支持1.19 1.20  1.21
#版本之间可能不兼容,具体细节需要查看官网说明
wget https://docs.projectcalico.org/v3.20/manifests/calico.yaml
kubectl apply -f calico.yaml
#查看已部署的caclico,默认情况是一个calico控制器和N个calico node
kubectl get pods -A -o wide| grep calico

默认安装的calico,管理的节点不超过50个,且数据存储交由kubenetes api负责,数据真实还是存在etcd中

calico组件

  • Felix: agent进程,以DaemonSet方式安装在每个节点,负责网络接口管理,路由,ARP,ACL的管理,状态上报,同步等
  • calico-controller: calico控制器,监听和变更来源于kubernetes的资源
  • calicoctl:calico的cli工具,方便对calico排错
  • typha: 大于50个节点,使用typha代替apiServer来和ectd交互
  • bird: 从Felix获取路由并分发到网络上的 BGP对等点,以进行主机间路由。在托管 Felix 代理的每个节点上运行,是路由的守护进程
  • confd: 监视Calico的存储,以便查看BGP配置和全局缺省值进行更新。
  • Datastore plugin: 通过减少每个节点对数据存储的影响来增加规模,是Calico CNI插件之一
  • IPAM plugin: 使用 Calico的IP池资源来控制如何将 IP 地址分配给集群中的豆荚。 Calico 默认CNI插件
  • etcd: 分布式键值存府,主要负责网络无数据一致性,确保calico网络状态的准确性,默认情况共用k8s集群的etcd

本文转载自: 掘金

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

MySQL之select、distinct、limit使用

发表于 2021-11-10

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

1、简介

这篇博客将会非常基础,如果有MySQL经验的可以跳过,写这篇博客的原因是给初学者看的。下面将会讲解如何使用select查看指定表的单个列、多个列以及全部列。

首先准备一张表,表结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) NOT NULL COMMENT '用户名',
`age` int(11) NOT NULL COMMENT '年龄',
`sex` smallint(6) NOT NULL COMMENT '性别',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

表数据如下所示:

1
2
3
4
5
6
7
8
sql复制代码INSERT INTO `user` VALUES (1, '李子捌', 18, 1);
INSERT INTO `user` VALUES (2, '张三', 22, 1);
INSERT INTO `user` VALUES (3, '李四', 38, 1);
INSERT INTO `user` VALUES (4, '王五', 25, 1);
INSERT INTO `user` VALUES (5, '六麻子', 13, 0);
INSERT INTO `user` VALUES (6, '田七', 37, 1);

SET FOREIGN_KEY_CHECKS = 1;

注意在MySQL4.1之后,数据库关键字是完全不区分大小写;数据库名、表名、列名默认不区分大小写,但是可以修改(不建议修改)。

2、select

2.1 查询单个列

首先使用use指定需要操作的数据库。

1
2
ini复制代码mysql> use liziba;
Database changed

接着使用select从user表中查询name列,select紧跟着列名称,from后面紧跟着表名称。

select column_name from table_name;

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> select name from user;
+--------+
| name |
+--------+
| 李子捌 |
| 张三 |
| 李四 |
| 王五 |
| 六麻子 |
| 田七 |
+--------+
6 rows in set (0.00 sec)

2.2 查询多个列

查询多个列和单个列的区别在于,select后面紧跟多个列名,用英文逗号分割即可。

select column_name1,column_name2,column_name3 from table_name;

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> select name,age from user;
+--------+-----+
| name | age |
+--------+-----+
| 李子捌 | 18 |
| 张三 | 22 |
| 李四 | 38 |
| 王五 | 25 |
| 六麻子 | 13 |
| 田七 | 37 |
+--------+-----+
6 rows in set (0.00 sec)

2.3 查询所有列

查询所有列有两种方式,第一种是上面两种推导出的方式,列出所有的列名。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> select id,name,age,sex from user;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
| 2 | 张三 | 22 | 1 |
| 3 | 李四 | 38 | 1 |
| 4 | 王五 | 25 | 1 |
| 5 | 六麻子 | 13 | 0 |
| 6 | 田七 | 37 | 1 |
+----+--------+-----+-----+
6 rows in set (0.00 sec)

第二种,也是部分程序员使用的最多的一句SQL,使用 *** 通配符**代替表的所有列。

select * from table_name;

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> select * from user;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
| 2 | 张三 | 22 | 1 |
| 3 | 李四 | 38 | 1 |
| 4 | 王五 | 25 | 1 |
| 5 | 六麻子 | 13 | 0 |
| 6 | 田七 | 37 | 1 |
+----+--------+-----+-----+
6 rows in set (0.00 sec)

提示:*是程序员使用的大忌, 如果我们不需要获取表的所有列且表的列名是移植的,就不应该使用*查询全部数据,而是应该指定数据库列查询,这样可以提升查询的性能。

3、distinct

如果需要查询列值不重复的数据,可以使用distinct关键字去重。

我们在上面的表中插入一条新的数据,数据age和李子捌相等,sex也相同。

1
2
sql复制代码mysql> insert into user (name, age, sex) values('谢礼', 18, 1);
Query OK, 1 row affected (0.01 sec)

此时可以看到年龄列有相等的值

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码mysql> select * from user;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
| 2 | 张三 | 22 | 1 |
| 3 | 李四 | 38 | 1 |
| 4 | 王五 | 25 | 1 |
| 5 | 六麻子 | 13 | 0 |
| 6 | 田七 | 37 | 1 |
| 7 | 谢礼 | 18 | 1 |
+----+--------+-----+-----+
7 rows in set (0.00 sec)

此时我们想获取user表中的用户有哪些年龄。我们可以使用distinct关键字,应用于需要去重的列前面。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> select distinct age from user;
+-----+
| age |
+-----+
| 18 |
| 22 |
| 38 |
| 25 |
| 13 |
| 37 |
+-----+
6 rows in set (0.00 sec)

这里有一个问题需要注意, distinct关键字去重会作用于所有的字段,如果distinct关键字后面跟了多个字段,那么多个字段的值都不相等才算不重复。

比如说user表中不存在age,name同时都不重复的数据,此时distinct关键字并不是没生效,而是本身就不存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码mysql> select distinct age,name from user;
+-----+--------+
| age | name |
+-----+--------+
| 18 | 李子捌 |
| 22 | 张三 |
| 38 | 李四 |
| 25 | 王五 |
| 13 | 六麻子 |
| 37 | 田七 |
| 18 | 谢礼 |
+-----+--------+
7 rows in set (0.00 sec)

如果distinct关键字后跟的字段值都不相等,那么distinct关键字仍然能去重。比如李子捌和谢礼的年龄和性别均相等,此时distinct关键字会过滤一条数据。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> select distinct age,sex from user;
+-----+-----+
| age | sex |
+-----+-----+
| 18 | 1 |
| 22 | 1 |
| 38 | 1 |
| 25 | 1 |
| 13 | 0 |
| 37 | 1 |
+-----+-----+
6 rows in set (0.00 sec)

4、limit

前面的查询会返回满足条件的所有记录,如果我们只需要指定数量的记录,可以使用limit关键字限制返回的行;这种场景多用于数据分页。

limit的取值需大于等于0的整数 ,如果传入负数和小数会报错。

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> select * from user limit 0;
Empty set (0.00 sec)

mysql> select * from user limit 1;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
+----+--------+-----+-----+
1 row in set (0.00 sec)

如果limit给定的值大于表的行记录值,那么将会返回所有数据。比如我们通过select count(1)查询user表的记录数值,一共7条数据,此时我们传入8,并不会报错,MySQL将会放回user表中的所有数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码mysql> select count(1) from user;
+----------+
| count(1) |
+----------+
| 7 |
+----------+
1 row in set (0.01 sec)

mysql> select * from user limit 8;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
| 2 | 张三 | 22 | 1 |
| 3 | 李四 | 38 | 1 |
| 4 | 王五 | 25 | 1 |
| 5 | 六麻子 | 13 | 0 |
| 6 | 田七 | 37 | 1 |
| 7 | 谢礼 | 18 | 1 |
+----+--------+-----+-----+
7 rows in set (0.00 sec)

limit可以跟两个参数分别表示起始值和结束值,闭区间(包含起始值和结束值)。如果跟一个参数,则表示结束值,起始值默认为0。 注意MySQL数据的索引起始值为0。

limit 2, 4表示查询第三条数据到第五条数据,其行号为2到4。

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> select * from user limit 2, 4;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 3 | 李四 | 38 | 1 |
| 4 | 王五 | 25 | 1 |
| 5 | 六麻子 | 13 | 0 |
| 6 | 田七 | 37 | 1 |
+----+--------+-----+-----+
4 rows in set (0.00 sec)

本文转载自: 掘金

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

java和nodejs使用md5算法实现对数据的加密与加盐

发表于 2021-11-10

MD5算法

密码在数据库当中是如何存储的?明文还是密文?
很显然做为一家负责的公司密码应该采用密文在数据库中存储
这样做即使数据库被攻破密码采用了加密也不会得到泄露

MD5算法介绍

MD5是一种哈希算法,用来保证信息的完整性。
一段信息对应一个哈希值,且不能通过哈希值推出这段信息,而且还需要保证不存在任意两段不相同的信息对应同一个哈希值。

java实现使用MD5算法加密

所需要的依赖:commons-codec

1
2
3
4
xml复制代码<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>

实现:

1
2
3
4
java复制代码	String str = "admin";
//使用DigestUtils工具类
String s = DigestUtils.md5Hex(str);
System.out.println("MD5加密结果:"+s);
加盐操作

虽然md5算法加密不可以解密,但是一些简单的,出现频率高的密码还是极有可能被破解记录下来的 如:123456,admin,root等
那么何为加盐?
就是在原要加密的字符串中按照自己的想法把一些规律的不规律的字符串添加进来

例如:加密字符串:123456
加盐:加密字符串变为:123456abcd

java实现加盐操作

1
2
3
4
5
java复制代码	String salter = "加盐字符串";
String str = "admin";
//使用DigestUtils工具类
String s = DigestUtils.md5Hex(str+salter);
System.out.println("MD5加密结果:"+s);

Node.js实现MD5算法加密与加盐

npm 下载crypto

npm install crypto
代码:

1
2
3
4
5
6
7
8
9
10
js复制代码var crypto = require('crypto')

//加盐
let str = "admin"
let salt = 'Node'
str = str + salt
let obj = crypto.createHash('md5')
obj.update(str)
let strHex = obj.digest('hex')
console.log(strHex)

本文转载自: 掘金

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

1…382383384…956

开发者博客

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