编程实践精华总结集锦系列2: SpringBoot/Maven/IDEA/Java/Kotlin/Redis等等

Posted 东海陈光剑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编程实践精华总结集锦系列2: SpringBoot/Maven/IDEA/Java/Kotlin/Redis等等相关的知识,希望对你有一定的参考价值。

Go Quick Start 极简教程

Go

官网:https://golang.org/
文档:https://golang.org/doc/

开发环境配置

下载 Go:
(官网)https://golang.org/dl/
(国内镜像)https://golang.google.cn/dl/

IDE :使用 GoLand is a cross-platform IDE built specially for Go developers。
https://www.jetbrains.com/go/

go 命令说明:

go
Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildconstraint build constraints
    buildmode       build modes
    c               calling between Go and C
    cache           build and test caching
    environment     environment variables
    filetype        file types
    go.mod          the go.mod file
    gopath          GOPATH environment variable
    gopath-get      legacy GOPATH go get
    goproxy         module proxy protocol
    importpath      import path syntax
    modules         modules, module versions, and more
    module-get      module-aware go get
    module-auth     module authentication using go.sum
    packages        package lists and patterns
    private         configuration for downloading non-public code
    testflag        testing flags
    testfunc        testing functions
    vcs             controlling version control with GOVCS

Use "go help <topic>" for more information about that topic.

Hello World

源代码文件 hello_world.go :

package main

import "fmt"

func main() {
   fmt.Println("Hello, World!")
}

命令行运行:

go run hello_world.go

输出:

Hello, World!

在 GoLang IDE 中效果如下:

Go 语言简介

概述

Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。

Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。

Go 语言特色

简洁、快速、安全
并行、有趣、开源
内存管理、数组安全、编译迅速

Go 语言用途

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。

对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。

数据类型

Go 有一个相当简单的类型系统:没有子类型(但有类型转换),没有泛型,没有多态函数,只有一些基本的类型:

基本类型:int、int64、int8、uint、float32、float64 等
struct: 结构体
interface:一组方法的集合
map[K, V]:一个从键类型到值类型的映射
[number]Type:一些 Type 类型的元素组成的数组
[]Type:某种类型的切片(具有长度和功能的数组的指针)
chan Type:一个线程安全的队列
指针 *T 指向其他类型
函数:
具名类型:可能具有关联方法的其他类型的别名(LCTT 译注:这里的别名并非指 Go 1.9 中的新特性“类型别名”):

type T struct { foo int }
type T *T
type T OtherNamedType

具名类型完全不同于它们的底层类型,所以你不能让它们互相赋值,但一些操作符,例如 +,能够处理同一底层数值类型的具名类型对象们(所以你可以在上面的示例中把两个 T 加起来)。

映射、切片和信道是类似于引用的类型——它们实际上是包含指针的结构。包括数组(具有固定长度并可被拷贝)在内的其他类型则是值传递(拷贝)。

函数

package main

import "fmt"

func main() {
    fmt.Println(sum(1, 1))
}

func sum(a int, b int) interface{} {
    return a + b
}

分支

package main

import "fmt"

func main() {
    fmt.Println(max(1, 2))
}

func max(a int, b int) interface{} {
    var max int = 0
    if a > b {
        max = a
    } else {
        max = b
    }
    return max
}

循环

package main

import "fmt"

func main() {
    fmt.Println(sum1_n(100))
}

func sum1_n(n int) interface{} {
    var sum = 0
    for i := 1; i <= n; i++ {
        sum += i
    }
    return sum
}

数组

package main

import "fmt"

func main() {
    var a []int = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(sumArray(a))
}

func sumArray(a []int) interface{} {
    var sum = 0
    for i := 0; i < len(a); i++ {
        sum += a[i]
    }
    return sum
}

指针

package main

import "fmt"

func main() {
    var a int = 20
    /* 声明指针变量 */
    var ip *int
    fmt.Println(ip==nil)
    /* 指针变量的存储地址 */
    ip = &a
    fmt.Println(ip==nil)
    fmt.Printf("a 变量的地址是: %x\\n", &a)
    /* 指针变量的存储地址 */
    fmt.Printf("ip 变量储存的指针地址: %x\\n", ip)
    /* 使用指针访问值 */
    fmt.Printf("*ip 变量的值: %d\\n", *ip)
}

//true
//false
//a 变量的地址是: c0000ae008
//ip 变量储存的指针地址: c0000ae008
//*ip 变量的值: 20

结构体 struct

package main

import "fmt"

func main() {
    var b = Book{
        "Go Programming",
        "ken",
        10000,
    }

    fmt.Println(b.title)
    fmt.Println(b.author)
    fmt.Println(b.id)
    //Go Programming
    //ken
    //10000
}

type Book struct {
    title  string
    author string
    id     int
}

集合 Map

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

可以使用内建函数 make 也可以使用 map 关键字来定义 Map:

/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对。

package main

import "fmt"

func main() {
    var ccmap = make(map[string]string)
    ccmap["China"] = "北京"
    ccmap["Italy"] = "罗马"
    ccmap["Germany"] = "柏林"
    for c := range ccmap {
        fmt.Println(c, "Capital is ", ccmap[c])
    }

    c, contains := ccmap["American"]
    if contains {
        fmt.Println("American's capital is ", c)
    } else {
        fmt.Println("Not exist")
    }
}

//Italy Capital is  罗马
//Germany Capital is  柏林
//China Capital is  北京
//Not exist

Go 优势与生态

Go 语言生态

当下使用 Go 语言开发的软件产品非常众多,而且知名的产品也不再少数,足见这门语言的强大:

软件描述链接
docker家喻户晓的容器技术github.com/moby/moby
kubernetes容器编排引擎,google出品github.com/kubernetes/kubernetes
etcd分布式服务注册发现系统github.com/etcd-io/etcd
influxdb时序数据库github.com/influxdata/influxdb
grafana数据监控可视化看板github.com/grafana/grafana
prometheus开源监控系统github.com/prometheus/prometheus
consul分布式服务发现系统github.com/hashicorp/consul
nsq亿级消息队列github.com/nsqio/nsq
TiDB分布式数据库, go + rust 打造github.com/pingcap/tidb

除了上述表格中列举的产品外, Go 语言还涉足于像区块链、微服务等场景,开源的框架也非常多,所以说 Go 语言是一门值得去学习的语言。

性能优势

Go 语言被称为是:"21世纪的C语言",虽然这个帽子戴的有点高,不妨这里给大家解读一下,其实这句话有两层含义:

第一层含义是: Go 语言的语法和C语言类似,如果你会C语言,上手会很快。但如果你不会C语言,其实也不用担心,比起C语言, Go 语言上手其实很简单。

第二层含义是: Go 语言的性能, Go 语言内置强大的并发模型 goroutine,它能让我们快速开发高并发web系统,并且在同样服务器资源的情况下, Go 语言表现出来的性能也是相当的优秀,这也是推荐大家选择选择 Go 语言的原因之一。

部署运维成本低

这是我选择 Go 语言的第二个原因。 Go 语言属于编译型语言,最终部署上线,我们只需要部署项目编译后的二进制文件即可,类比一下像 php 或 python 这种解释性语言在服务器上运行还需要安装相应的运行环境,而使用二进制代码方式使得部署变得更为简单,也不会存在多版本共享环境的兼容性问题,运维也变得非常容易。

编码格式统一

Go 语言官方内置了统一代码风格的工具 gofmt( IDE 一般都会内置集成), 用来规范大家代码风格,这对于需要多人协作项目尤为重要。

比如,现在你需要接手一个外包团队项目,这个外包项目如果项目的编码风格和你不一致,当你接手后,你的心情多半沮丧的,因为这会增加你的改造成本。

更或是,你现在需要接手一个由前前前同事所开发的老项目,如果代码风格不统一,可想而知接下来会发生什么...

Go 语言官方其实也是注意到了这些痛点,也避免了由第三方产生的规范不一致的问题,以官方的角度统一规范,从而降低整个项目的协作运维成本。

测试简单

Go 语言编写测试代码真的非常简单,这个特性真的是刚需啊。我们在项目开发中经常会遇到这样的事情,比如当你负责为项目负责开发一个独立的短信服务模块,这个功能供另个同事在项目里调用,当你开发完成后如何测试功能是否正常呢?

这种测试在 Go 语言中变得相当容易,你只需要在你代码文件同级目录创建一个以 _test.go 结尾的文件,然后在文件里编写针对特定功能测试函数即可,更重要的是,这个测试文件是可以单独运行的,你不需要再去集成整个项目运行环境, Go 语言让单元测试变得非常容易。

Go Projects

https://github.com/moby/moby
https://github.com/docker/docker-ce
https://github.com/kubernetes/kubernetes
https://github.com/etcd-io/etcd
https://github.com/gin-gonic/gin
https://github.com/hashicorp/consul
https://github.com/micro/go-micro
https://github.com/nsqio/nsq
https://github.com/elastic/beats
https://github.com/pingcap/tidb
https://github.com/CodisLabs/codis
https://github.com/baidu/bfe
https://github.com/caddyserver/caddy
https://github.com/cockroachdb/cockroach

参考资料

https://blog.csdn.net/javahongxi/article/details/109771251
https://www.runoob.com/go/go-environment.html
https://golang.org/cmd/go/
https://golang.org/doc/
https://www.cnblogs.com/xingxia/p/golang.html

PS:go内置类型

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

/*
    Package builtin provides documentation for Go's predeclared identifiers.
    The items documented here are not actually in package builtin
    but their descriptions here allow godoc to present documentation
    for the language's special identifiers.
*/
package builtin

// bool is the set of boolean values, true and false.
type bool bool

// true and false are the two untyped boolean values.
const (
    true  = 0 == 0 // Untyped bool.
    false = 0 != 0 // Untyped bool.
)

// uint8 is the set of all unsigned 8-bit integers.
// Range: 0 through 255.
type uint8 uint8

// uint16 is the set of all unsigned 16-bit integers.
// Range: 0 through 65535.
type uint16 uint16

// uint32 is the set of all unsigned 32-bit integers.
// Range: 0 through 4294967295.
type uint32 uint32

// uint64 is the set of all unsigned 64-bit integers.
// Range: 0 through 18446744073709551615.
type uint64 uint64

// int8 is the set of all signed 8-bit integers.
// Range: -128 through 127.
type int8 int8

// int16 is the set of all signed 16-bit integers.
// Range: -32768 through 32767.
type int16 int16

// int32 is the set of all signed 32-bit integers.
// Range: -2147483648 through 2147483647.
type int32 int32

// int64 is the set of all signed 64-bit integers.
// Range: -9223372036854775808 through 9223372036854775807.
type int64 int64

// float32 is the set of all IEEE-754 32-bit floating-point numbers.
type float32 float32

// float64 is the set of all IEEE-754 64-bit floating-point numbers.
type float64 float64

// complex64 is the set of all complex numbers with float32 real and
// imaginary parts.
type complex64 complex64

// complex128 is the set of all complex numbers with float64 real and
// imaginary parts.
type complex128 complex128

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
type int int

// uint is an unsigned integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, uint32.
type uint uint

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

// iota is a predeclared identifier representing the untyped integer ordinal
// number of the current const specification in a (usually parenthesized)
// const declaration. It is zero-indexed.
const iota = 0 // Untyped int.

// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type int

// Type1 is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type1 int

// IntegerType is here for the purposes of documentation only. It is a stand-in
// for any integer type: int, uint, int8 etc.
type IntegerType int

// FloatType is here for the purposes of documentation only. It is a stand-in
// for either float type: float32 or float64.
type FloatType float32

// ComplexType is here for the purposes of documentation only. It is a
// stand-in for either complex type: complex64 or complex128.
type ComplexType complex64

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//  slice = append(slice, elem1, elem2)
//  slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//  slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int

// The delete built-in function deletes the element with the specified key
// (m[key]) from the map. If m is nil or there is no such element, delete
// is a no-op.
func delete(m map[Type]Type1, key Type)

// The len built-in function returns the length of v, according to its type:
//  Array: the number of elements in v.
//  Pointer to array: the number of elements in *v (even if v is nil).
//  Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
//  String: the number of bytes in v.
//  Channel: the number of elements queued (unread) in the channel buffer;
//           if v is nil, len(v) is zero.
// For some arguments, such as a string literal or a simple array expression, the
// result can be a constant. See the Go language specification's "Length and
// capacity" p for details.
func len(v Type) int

// The cap built-in function returns the capacity of v, according to its type:
//  Array: the number of elements in v (same as len(v)).
//  Pointer to array: the number of elements in *v (same as len(v)).
//  Slice: the maximum length the slice can reach when resliced;
//  if v is nil, cap(v) is zero.
//  Channel: the channel buffer capacity, in units of elements;
//  if v is nil, cap(v) is zero.
// For some arguments, such as a simple array expression, the result can be a
// constant. See the Go language specification's "Length and capacity" p for
// details.
func cap(v Type) int

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//  Slice: The size specifies the length. The capacity of the slice is
//  equal to its length. A second integer argument may be provided to
//  specify a different capacity; it must be no smaller than the
//  length. For example, make([]int, 0, 10) allocates an underlying array
//  of size 10 and returns a slice of length 0 and capacity 10 that is
//  backed by this underlying array.
//  Map: An empty map is allocated with enough space to hold the
//  specified number of elements. The size may be omitted, in which case
//  a small starting size is allocated.
//  Channel: The channel's buffer is initialized with the specified
//  buffer capacity. If zero, or the size is omitted, the channel is
//  unbuffered.
func make(t Type, size ...IntegerType) Type

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

// The complex built-in function constructs a complex value from two
// floating-point values. The real and imaginary parts must be of the same
// size, either float32 or float64 (or assignable to them), and the return
// value will be the corresponding complex type (complex64 for float32,
// complex128 for float64).
func complex(r, i FloatType) ComplexType

// The real built-in function returns the real part of the complex number c.
// The return value will be floating point type corresponding to the type of c.
func real(c ComplexType) FloatType

// The imag built-in function returns the imaginary part of the complex
// number c. The return value will be floating point type corresponding to
// the type of c.
func imag(c ComplexType) FloatType

// The close built-in function closes a channel, which must be either
// bidirectional or send-only. It should be executed only by the sender,
// never the receiver, and has the effect of shutting down the channel after
// the last sent value is received. After the last value has been received
// from a closed channel c, any receive from c will succeed without
// blocking, returning the zero value for the channel element. The form
//  x, ok := <-c
// will also set ok to false for a closed channel.
func close(c chan<- Type)

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated with a non-zero exit code. This
// termination sequence is called panicking and can be controlled by the
// built-in function recover.
func panic(v interface{})

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}

// The print built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Print is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func print(args ...Type)

// The println built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Spaces are always added between arguments and a newline is appended.
// Println is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func println(args ...Type)

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

操作系统中的任务调度 & CPU 内存缓存一致性问题

处理器调度(multiprocessor scheduling)

作系统应该如何在多 CPU上调度工作?会遇到什么新问题?

CPU 根据程序寄存器(PC)加载指令,详码,执行,写回,并对程序计数器更新,周而复始。

但是CPU怎么及时响应硬件来的请求呢?
解决方案:中断机制。CPU有一个引脚就是触发中断的。

1、为了接入不同通常会有中断控制器,不同硬件连接到中断控制器上,每个硬件的中断有一个中断编号IRQ。

2、当一个硬件中断发生时,CPU会根据中断编号查到对应的中断服务处理程序,将当前执行上下文压栈,并将程序计数器设为中断处理程序的地址。这样中断处理程序就抢到了CPU。

3、中断处理完成后,再弹栈,跳回到原来的程序继续执行。

计算机中有一种硬件设备叫时钟,用来让程序按照一定的时间间隔执行特定的逻辑。时钟会产生时钟中断。而多任务操作系统把自己的中断服务处理程序注册到了时钟中断上,这样每隔一定间隔。就可以把 CPU从当前的任务手中抢过来,进行上下文切换后,交给另一个任务。这样就支持多任务"同时"执行了。

缓存一致性(cache coherence)问题

在单CPU系统中,存在多级的硬件缓存(hardware cache),一般来说会让处理器更快地执行程序。缓存是很小但很快的存储设备,通常拥有内存中最热的数据的备份。相比之下,内存很大且拥有所有的数据,但访问速度较慢。通过将频繁访问的数据放在缓存中,系统似乎拥有又大又快的内存。

举个例子,假设一个程序需要从内存中加载指令并读取一个值,系统只有一个CPU,拥有较小的缓存(如64KB)和较大的内存。

程序第一次读取数据时,数据在内存中,因此需要花费较长的时间(可能数十或数百纳秒)。处理器判断该数据很可能会被再次使用,因此将其放入CPU缓存中。如果之后程序再次需要使用同样的数据,CPU会先查找缓存。因为在缓存中找到了数据,所以取数据快得多(比如几纳秒),程序也就运行更快。

缓存是基于局部性(locality)的概念,局部性有两种,即时间局部性和空间局部性。时间局部性是指当一个数据被访问后,它很有可能会在不久的将来被再次访问,比如循环代码中的数据或指令本身。而空间局部性指的是,当程序访问地址为x的数据时,很有可能会紧接着访问x周围的数据,比如遍历数组或指令的顺序执行。由于这两种局部性存在于大多数的程序中,硬件系统可以很好地预测哪些数据可以放入缓存,从而运行得很好。

有趣的部分来了:如果系统有多个处理器,并共享同一个内存,如图所示,会怎样呢?

带缓存的单CPU

两个有缓存的CPU共享内存

事实证明,多CPU的情况下缓存要复杂得多。
例如,假设一个运行在CPU 1上的程序从内存地址A读取数据。
1、由于不在CPU 1的缓存中,所以系统直接访问内存,得到值D。程序然后修改了地址A处的值,只是将它的缓存更新为新值D'
2、将数据写回内存比较慢,因此系统(通常)会稍后再做。
3、假设这时操作系统中断了该程序的运行,并将其交给CPU 2,重新读取地址A的数据,由于CPU 2的缓存中并没有该数据,所以会直接从内存中读取,得到了旧值D,而不是正确的值D'

这一普遍的问题称为缓存一致性(cache coherence)问题。

不用被动注入的方式, 主动从 Spring Boot 应用容器中的获取 Bean 的方法

@Component

public class SpringContextUtil implements ApplicationContextAware {

// Spring应用上下文环境

    private static ApplicationContext applicationContext;

/**

* 实现ApplicationContextAware接口的回调方法,设置上下文环境

*

    * @param applicationContext

    */

    public void setApplicationContext(ApplicationContext applicationContext) {

SpringContextUtil.applicationContext = applicationContext;

}

/**

* @return ApplicationContext

*/

    public static ApplicationContext getApplicationContext() {

return applicationContext;

}

/**

* 获取对象

*

    * @param name

    * @return Object

    * @throws BeansException

    */

    public static Object getBean(String name)throws BeansException {

return applicationContext.getBean(name);

}

}

PS: @Component 注解.

从容器中主动获取Bean的使用方式

/**

* Kunlun 编译器实现类

*/

@Service

public class KunlunInterpreterImpl implements KunlunInterpreter {

SpecificationParser specificationParser;

@Override

    public Statement interpret(KunlunRule rule)throws Exception {

// 选择引擎

        selectEngine(rule);

KunlunExpression expression = rule.getExpression();

// BFS 解析嵌套子句

        bfsParseExpression(expression);

// 解析构建 ASTQuery 抽象语法树

        IQuerySpecification specification =specificationParser.parse(rule);

// 物理资源映射,生成真正执行的物理 SQL Statement

        Statement statement =new Statement();

if (specification !=null) {

statement.setSelectSql(specification.genSelectSql());

statement.setCountSql(specification.genSelectCountSql());

statement.setGroupBySql(specification.genSelectGroupBySql());

statement.setKvSql(specification.genSelectSqlForKV());

}

return statement;

}

private void selectEngine(KunlunRule rule) {

// 引擎选择

        if (EngineType.OLAP.equals(rule.getEngineSelector().getEngineType())

&&DriverType.CLICKHOUSE.equals(rule.getEngineSelector().getDriverType())) {

specificationParser = (SpecificationParser)SpringContextUtil.getBean("specificationParserClickHouse");

}else if (EngineType.OLAP.equals(rule.getEngineSelector().getEngineType())

&&DriverType.HIVE.equals(rule.getEngineSelector().getDriverType())) {

specificationParser = (SpecificationParser)SpringContextUtil.getBean("specificationParserHive");

}else {

throw new IllegalArgumentException("engine not supported yet!");

}

}

/**

* BFS 遍历表达式树

*

    * @param kunlunExpression

    * @throws Exception

    */

    private void bfsParseExpression(KunlunExpression kunlunExpression)throws Exception {

Queuequeue =new ArrayDeque<>();

ListexpressionList =new ArrayList<>();

// 根节点入队列

        queue.offer(kunlunExpression);

while (!queue.isEmpty()) {

// 出队列

            KunlunExpression expr =queue.poll();

ListsubExpressionList =expr.getSubExpression();

if (subExpressionList !=null && !subExpressionList.isEmpty()) {

for (KunlunExpression e :subExpressionList) {

// 子节点入队列

                    queue.offer(e);

}

}

expressionList.add(expr);

}

for (KunlunExpression e :expressionList) {

System.out.println(e);

// 解析

            parseExpression(e);

}

}

/**

* 解析表达式

*

    * @param kunlunExpression

    * @throws Exception

    */

    private void parseExpression(KunlunExpression kunlunExpression)throws Exception {

ExpressionLogicOperatorEnum logic = kunlunExpression.getLogic();

if (logic ==null) {

throw new RuntimeException("can't get operator from kunlunExpression:" +JsonUtil.toString(kunlunExpression));

}

FieldCondition fieldCondition = kunlunExpression.getFilterField();

if (fieldCondition !=null) {

// 获取特征值运算符

            this.processFieldFeature(specificationParser, kunlunExpression);

}else {

specificationParser.compose(kunlunExpression);

}

}

private void processFieldFeature(SpecificationParser specificationParser,KunlunExpression kunlunExpression)throws Exception {

FieldCondition fieldCondition = kunlunExpression.getFilterField();

FieldArithmeticOperatorEnum operator =fieldCondition.getOperator();

switch (operator) {

case GREATER_THAN:

specificationParser.greaterThan(kunlunExpression);

break;

case GREATER_EQUAL_THAN:

specificationParser.greaterEqualThan(kunlunExpression);

break;

case LESS_THAN:

specificationParser.lessThan(kunlunExpression);

break;

case LESS_EQUAL_THAN:

specificationParser.lessEqualThan(kunlunExpression);

break;

case EQUAL:

specificationParser.equals(kunlunExpression);

break;

case IN:

specificationParser.in(kunlunExpression);

break;

case BETWEEN:

specificationParser.between(kunlunExpression);

break;

case LIKE:

specificationParser.like(kunlunExpression);

break;

default:

throw new RuntimeException("unsupported operator.");

}

}

}

git 如何删除已经 add 的文件 ( 如何撤销已放入缓存区文件的修改)

使用 git rm 命令即可,有两种选择:

一种是 git rm --cached "文件路径",不删除物理文件,仅将该文件从缓存中删除;

一种是 git rm --f  "文件路径",不仅将该文件从缓存中删除,还会将物理文件删除(不会回收到垃圾桶)。

git --如何撤销已放入缓存区(Index区)的修改

修改或新增的文件通过 git add --all命令全部加入缓存区(index区)之后,使用 git status 查看状态

(git status -s 简单模式查看状态,第一列本地库和缓存区的差异,第二列缓存区和工作目录的差异),

提示使用 git reset HEAD <file> 来取消缓存区的修改。

不添加<file>参数,撤销所有缓存区的修改。

另外可以使用 git rm --cached 文件名 ,可以从缓存区移除文件,使该文件变为未跟踪的状态,

同时下次提交时从本地库中删除。

注:

没有带参数的 git reset 命令,默认执行了 --mixed 参数,即用reset版本库到指定版本,并重置缓存区,在上面的命令中指定的目录版本是HEAD,即当前版本,所以实际上没有任何修改,仅是重置了缓存区。

Maven deploy 怎么配置

在本地的pom文件配置好之后,执行deploy命令,可以将maven所打的jar包上传到远程的repository,便于其他开发者和工程共享。

pom.xml配置

首选,在pom文件中project标签下添加如下代码:

<distributionManagement>

  <repository>

  <id>releases</id>

  <name>Internal Releases</name>

  <url>http://localhost:8081/nexus/content/repositories/thirdparty</url>

  </repository>

  <snapshotRepository>

  <id>releases</id>

  <name>Internal Releases</name>

  <url>http://localhost:8081/nexus/content/repositories/thirdparty</url>

  </snapshotRepository>

</distributionManagement>

此时,执行deploy命令,会返回401错误,则需要用户验证或验证的信息有误。

setting.xml文件配置

在setting配置文件的servers标签下添加如下代码:

  <server>

    <id>releases</id>

    <username>admin</username>

    <password>admin</password>

  </server>

PS:其中此处的id,必须为pom文件中配置的repository的id。

注意事项

一般继承 parent 的version会按照如下格式写:

  <parent>

    <groupId>module.some</groupId>

    <artifactId>module_parent</artifactId>

    <version>${parent.version}</version>

  </parent>

这样写方便统一管理版本信息,但发布到maven私服之后,maven 会试图下载 module_parent 的 ${parent.version} 的 jar。显然,这个jar是不存在的。那么为什么已经指定了 parent.version 的值了却没有解析呢?这是因为deploy 的过程中,parent 标签里的变量是不会解析的,必须是一个常量。

结果

执行maven deploy命令成功之后,登录私服进行查询,即可看到对应的jar包。

maven deploy命令打包到私服

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.zeelan.app</groupId>

  <artifactId>seller-auth</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>jar</packaging>

  <!-- 项目编码 -->

  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <lombok.version>1.16.10</lombok.version>

    <mybatis.paginator.version>1.2.17.2</mybatis.paginator.version>

    <hibernate.validator.version>5.1.2.Final</hibernate.validator.version>

    <validation.api.version>1.1.0.Final</validation.api.version>

  </properties>

  <!-- 远程仓库地址 -->

  <pluginRepositories>

    <pluginRepository>

      <id>nexus</id>

      <name>Team Nexus Repository</name>

      <url>http://192.168.0.126:8081/nexus/content/groups/public</url>

    </pluginRepository>

  </pluginRepositories>

  <!-- 配置远程发布到私服,mvn deploy -->

  <distributionManagement>

    <!-- 定义releases库的坐标 -->

    <repository>

      <id>releases</id>

      <name>Nexus Release Repository</name>

      <url>http://192.168.0.126:8081/nexus/content/repositories/releases/</url>

    </repository>

    <!-- 定义snapshots库 -->

    <snapshotRepository>

      <id>snapshots</id>

      <name>Nexus Snapshot Repository</name>

      <url>http://192.168.0.126:8081/nexus/content/repositories/snapshots/</url>

    </snapshotRepository>

  </distributionManagement>

  <!-- 项目有依赖的所有jar坐标配置 -->

  <dependencies>

    <!-- lombok jar -->

    <dependency>

      <groupId>org.projectlombok</groupId>

      <artifactId>lombok</artifactId>

      <version>${lombok.version}</version>

    </dependency>

    <!-- mybatis分页插件jar -->

    <dependency>

      <groupId>com.github.miemiedev</groupId>

      <artifactId>mybatis-paginator</artifactId>

      <version>${mybatis.paginator.version}</version>

    </dependency>

    <!-- hibernate validator验证jar -->

    <dependency>

      <groupId>org.hibernate</groupId>

      <artifactId>hibernate-validator</artifactId>

      <version>${hibernate.validator.version}</version>

    </dependency>

    <!-- hibernate validtor需要的依赖 jar -->

    <dependency>

      <groupId>javax.validation</groupId>

      <artifactId>validation-api</artifactId>

      <version>${validation.api.version}</version>

    </dependency>

  </dependencies>

  <build>

    <finalName>seller-auth</finalName>

    <!-- 插件库声明 -->

    <plugins>

      <!-- 配置运行环境JDK版本 -->

      <plugin>

        <artifactId>maven-compiler-plugin</artifactId>

        <extensions>true</extensions>

        <configuration>

          <source>1.8</source>

          <target>1.8</target>

        </configuration>

      </plugin>

      <!-- 将源码打包插件 -->

      <plugin>

        <artifactId>maven-source-plugin</artifactId>

        <version>2.1</version>

        <configuration>

          <attach>true</attach>

        </configuration>

        <executions>

          <execution>

            <phase>compile</phase>

            <goals>

              <goal>jar</goal>

            </goals>

          </execution>

        </executions>

      </plugin>

      <!-- deploy时只上传jar包到远程仓库的配置 -->

      <plugin>

        <artifactId>maven-deploy-plugin</artifactId>

        <version>2.7</version>

        <executions>

          <execution>

            <id>default-deploy</id>

            <phase>deploy</phase>

            <goals>

              <goal>deploy</goal>

            </goals>

            <!-- skip默认deploy插件的执行 -->

            <configuration>

              <skip>true</skip>

            </configuration>

          </execution>

          <execution>

            <id>deploy-file</id>

            <phase>deploy</phase>

            <goals>

              <goal>deploy-file</goal>

            </goals>

            <configuration>

              <!-- 开发阶段上传到snapshot仓库,上线阶段上传到release仓库 -->

              <repositoryId>${project.distributionManagement.snapshotRepository.id}</repositoryId>

              <url>${project.distributionManagement.snapshotRepository.url}</url>

              <file>${project.build.directory}/${project.artifactId}.jar</file>

              <groupId>${project.groupId}</groupId>

              <artifactId>${project.artifactId}</artifactId>

              <version>${project.version}</version>

            </configuration>

          </execution>

        </executions>

      </plugin>

    </plugins>

  </build>

</project>

执行 mvn deploy就能打包到私服上了!

mvn -clean配置清除插件,然后在执行命令可以清除target下的文件

mvn-clean package 本地打包,jar/war/等根据<packaging>jar/war</packaging>控制

mvn -e 查看打包过程的错误信息

mvn -v查看mavne版本信息等等.

hive 使用 map 类型字段

创建一个map类型字段

create table test3(field2 map<string,string>) 
row format delimited fields terminated by ',' 
collection items terminated by "|" 
map keys terminated by ":";

map keys terminated by ":"表示键值对之间用":"来分割

str_to_map 方式

同样的,先使用sql方式插入

insert into test3(field2)values(str_to_map("name:zhangsan,age:25")),(str_to_map("name:lisi,age:23"));

再导入文件,文件内容如下:

load data local inpath  '/Users/zhangsheng/hive/note/hive/test.txt' into table test3;

map的访问通过map[key]的形式进行,测试一下

select * from test3;
select field2["name"] as name,field2["age"] as age from test3;
select * from test3 where field2["age"] > 25;

用 map() 函数

select map("pay_order_cnt", 10, "pay_order_amt", 1000)

IDEA 源代码文件大小超出 2.56M : File size exceeds configured limit

File size exceeds configured limit (2560000), code insight features not available

Ask Question

This is built from Álvaro González's answer and How to increase IDE memory limit in IntelliJ IDEA on Mac?

Go to Help > Edit Custom Properties

Add:

idea.max.intellisense.filesize=999999

Restart the IDE.

https://stackoverflow.com/questions/23057988/file-size-exceeds-configured-limit-2560000-code-insight-features-not-available

Schema是什么?

Schema是什么?

schema是元数据的一个抽象集合,包含一套 schema component: 主要是元素与属性的声明、复杂与简单数据类型的定义。这些schema component通常是在处理一批 schema document时被创建。

数据库schema就是 :表, 列,数据类型,视图,存储过程,关系,主键,外键等。

在ISO / IEC 9075-1 SQL标准定义了一个模式作为 描述符的持久性,命名集合。

mysql文档指出,物理,一个模式是与数据库的代名词。因此,模式和数据库是同一件事。

在软件开发中,当讨论模式时,可以讨论概念模式,物理模式,内部模式,外部模式,逻辑模式等。这些每个都有其特定的含义。

XML Schema 是基于 XML 的 DTD 替代者。
XML Schema 可描述 XML 文档的结构。
XML Schema 语言也可作为 XSD(XML Schema Definition)来引用。
XML Schema 的作用是定义 XML 文档的合法构建模块,类似 DTD。
XML Schema:
定义可出现在文档中的元素
定义可出现在文档中的属性
定义哪个元素是子元素
定义子元素的次序
定义子元素的数目
定义元素是否为空,或者是否可包含文本
定义元素和属性的数据类型
定义元素和属性的默认值以及固定值

XML Schema 是 DTD 的继任者
我们认为 XML Schema 很快会在大部分网络应用程序中取代 DTD。
理由如下:
XML Schema 可针对未来的需求进行扩展
XML Schema 更完善,功能更强大
XML Schema 基于 XML 编写
XML Schema 支持数据类型
XML Schema 支持命名空间

ClickHouse 的 Parser与Interpreter

Parser和Interpreter是非常重要的两组接口:

  • Parser分析器负责创建AST对象;

  • Interpreter解释器则负责解释AST,并进一步创建查询的执行管道。

它们与IStorage一起,串联起了整个数据查询的过程。

Parser分析器可以将一条SQL语句以递归下降的方法解析成AST语法树的形式。

不同的SQL语句,会经由不同的Parser实现类解析。例如,有负责解析DDL查询语句的ParserRenameQuery、ParserDropQuery和ParserAlterQuery解析器,也有负责解析INSERT语句的ParserInsertQuery解析器,还有负责SELECT语句的ParserSelectQuery等。

Interpreter解释器的作用就像Service服务层一样,起到串联整个查询过程的作用,它会根据解释器的类型,聚合它所需要的资源。首先它会解析AST对象;然后执行“业务逻辑”(例如分支判断、设置参数、调用接口等);最终返回IBlock对象,以线程的形式建立起一个查询执行管道。

Interpreter模式也类似于Composite模式。Composite模式通常会为单个对象和群组对象定义一个公共接口。不过,Composite模式并不要求支持以不同方式组织的结构,尽管该模式可以支持这些结构。例如,介绍Composite模式时曾描述过ProcessComponent类层次结构,该结构允许生产流程顺序进行,也允许生产流程交替进行。Interpreter模式通常都会涉及不同类型的组合结构(Interpreter模式通常处于Composite模式结构之上)。一个类组成其他组件的方式定义了解释器类实现或解释一个操作的方式。

Interpreter模式的主要意图是可以按照自己定义的组合规则集合来组合可执行对象。

在 Mac OS X 中编译 ClickHouse

ClickHouse 支持在 Mac OS X 10.12 版本中编译。若您在用更早的操作系统版本,可以尝试在指令中使用 Gentoo Prefix 和 clang sl.

通过适当的更改,它应该可以适用于任何其他的 Linux 发行版。

安装 Homebrew

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装编译器,工具库

$ brew install cmake ninja libtool gettext

拉取 ClickHouse 源码

git clone --recursive git@github.com:ClickHouse/ClickHouse.git
# or: git clone --recursive https://github.com/ClickHouse/ClickHouse.git

编译 ClickHouse

$ cd ClickHouse
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_CXX_COMPILER=`which clang++` -DCMAKE_C_COMPILER=`which clang`
$ ninja
$ cd ..

注意事项

若你想运行 clickhouse-server,请先确保增加系统的最大文件数配置。

注意, 可能需要用 sudo

为此,请创建以下文件:

/资源库/LaunchDaemons/limit.maxfiles.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>limit.maxfiles</string>
    <key>ProgramArguments</key>
    <array>
      <string>launchctl</string>
      <string>limit</string>
      <string>maxfiles</string>
      <string>524288</string>
      <string>524288</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>ServiceIPC</key>
    <false/>
  </dict>
</plist>

执行以下命令:

$ sudo chown root:wheel /Library/LaunchDaemons/limit.maxfiles.plist

然后重启。

可以通过 ulimit -n 命令来检查是否生效。

布隆过滤器(Bloom Filter)的原理和实现

什么情况下需要布隆过滤器?

先来看几个比较常见的例子

  • 字处理软件中,需要检查一个英语单词是否拼写正确

  • 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上

  • 在网络爬虫里,一个网址是否被访问过

  • yahoo, gmail等邮箱垃圾邮件过滤功能

这几个例子有一个共同的特点: 如何判断一个元素是否存在一个集合中?

常规思路

  • 数组

  • 链表

  • 树、平衡二叉树、Trie

  • Map (红黑树)

  • 哈希表

虽然上面描述的这几种数据结构配合常见的排序、二分搜索可以快速高效的处理绝大部分判断元素是否存在集合中的需求。但是当集合里面的元素数量足够大,如果有500万条记录甚至1亿条记录呢?这个时候常规的数据结构的问题就凸显出来了。数组、链表、树等数据结构会存储元素的内容,一旦数据量过大,消耗的内存也会呈现线性增长,最终达到瓶颈。有的同学可能会问,哈希表不是效率很高吗?查询效率可以达到O(1)。但是哈希表需要消耗的内存依然很高。使用哈希表存储一亿 个垃圾 email 地址的消耗?哈希表的做法:首先,哈希函数将一个email地址映射成8字节信息指纹;考虑到哈希表存储效率通常小于50%(哈希冲突);因此消耗的内存:8 * 2 * 1亿 字节 = 1.6G 内存,普通计算机是无法提供如此大的内存。这个时候,布隆过滤器(Bloom Filter)就应运而生。在继续介绍布隆过滤器的原理时,先讲解下关于哈希函数的预备知识。

哈希函数

哈希函数的概念是:将任意大小的数据转换成特定大小的数据的函数,转换后的数据称为哈希值或哈希编码。下面是一幅示意图:

可以明显的看到,原始数据经过哈希函数的映射后称为了一个个的哈希编码,数据得到压缩。哈希函数是实现哈希表和布隆过滤器的基础。

布隆过滤器介绍

  • 巴顿.布隆于一九七零年提出

  • 一个很长的二进制向量 (位数组)

  • 一系列随机函数 (哈希)

  • 空间效率和查询效率高

  • 有一定的误判率(哈希表是精确匹配)

布隆过滤器原理

布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k

以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。

布隆过滤器添加元素

  • 将要添加的元素给k个哈希函数

  • 得到对应于位数组上的k个位置

  • 将这k个位置设为1

布隆过滤器查询元素

  • 将要查询的元素给k个哈希函数

  • 得到对应于位数组上的k个位置

  • 如果k个位置有一个为0,则肯定不在集合中

  • 如果k个位置全部为1,则可能在集合中

布隆过滤器实现

下面给出python的实现,使用murmurhash算法

import mmh3
from bitarray import bitarray

# zhihu_crawler.bloom_filter

# Implement a simple bloom filter with murmurhash algorithm.
# Bloom filter is used to check wether an element exists in a collection, and it has a good performance in big data situation.
# It may has positive rate depend on hash functions and elements count.

BIT_SIZE = 5000000

class BloomFilter:

    def __init__(self):
        # Initialize bloom filter, set size and all bits to 0
        bit_array = bitarray(BIT_SIZE)
        bit_array.setall(0)

        self.bit_array = bit_array

    def add(self, url):
        # Add a url, and set points in bitarray to 1 (Points count is equal to hash funcs count.)
        # Here use 7 hash functions.
        point_list = self.get_postions(url)

        for b in point_list:
            self.bit_array[b] = 1

    def contains(self, url):
        # Check if a url is in a collection
        point_list = self.get_postions(url)

        result = True
        for b in point_list:
            result = result and self.bit_array[b]

        return result

    def get_postions(self, url):
        # Get points positions in bit vector.
        point1 = mmh3.hash(url, 41) % BIT_SIZE
        point2 = mmh3.hash(url, 42) % BIT_SIZE
        point3 = mmh3.hash(url, 43) % BIT_SIZE
        point4 = mmh3.hash(url, 44) % BIT_SIZE
        point5 = mmh3.hash(url, 45) % BIT_SIZE
        point6 = mmh3.hash(url, 46) % BIT_SIZE
        point7 = mmh3.hash(url, 47) % BIT_SIZE

        return [point1, point2, point3, point4, point5, point6, point7]

源码地址:https://github.com/cpselvis/zhihu-crawler/blob/master/bloom_filter.py

Redis 亿级用户信息存储实践:bitmap 位图存储

bitmap简介

8 个 bit 组成一个 Byte,所以 bitmap 极大的节省储存空间

你可以把它理解为一个特殊处理过的 字符串
key代表业务属性、标签。一个 bit 位来表示某个元素对应的值或者状态。

bitmap 并不是一种数据结构,实际上它就是字符串,但是可以对字符串的位进行操作。

bitmap有自己的一套命令。可以把bitmap想象成一个以bit为单位的数组,数组的每个单元存储0和1,数组的下标叫做偏移量。

Redis 提供 setbit,getbit,bitcount等几个 bitmap 相关命令。但其实 setbit 等命令只不过是在 set 上的扩展而已。

setbit 命令介绍

指令

SETBIT key offset value

复杂度 O(1)

设置或者清空 key 的 value(字符串)在 offset 处的 bit 值(只能只 0 或者 1)。

空间占用、以及第一次分配空间需要的时间
在一台 2010MacBook Pro 上

offset 为 2^32-1(分配 512MB)需要&#

以上是关于编程实践精华总结集锦系列2: SpringBoot/Maven/IDEA/Java/Kotlin/Redis等等的主要内容,如果未能解决你的问题,请参考以下文章

WinCE 驱动开发问题精华集锦

转程序员面试算法研究编程艺术红黑树机器学习5大经典原创系列集锦与总结

阿里雲 免费人工智能精华课程集锦

程序员面试算法研究编程艺术红黑树数据挖掘5大系列集锦

程序员面试算法研究编程艺术红黑树机器学习5大系列集锦

华为云电子书精华合集华为云云享专家,TOP博主优质电子书合集 2000页+ 限时免费下载 | 云享书库推荐