从零开始写数据库(三)

创建具有网络功能的数据库

修改main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
package main

import (
"fmt"
"net"
"bufio"
"os"
"strings"
)

func main() {
defer Save()

// 监听8080端口
ln, err := net.Listen("tcp", ":8080")
if err != nil {
// handle error
}
// 死循环
for {
// 拿到连接
conn, err := ln.Accept()
if err != nil {
// handle error
continue
}
// 处理函数
go handleConnection(conn)
}


}

func handleConnection(conn net.Conn) {

// Response,返回数据
send := bufio.NewWriter(conn)

// Request,接受数据
scanner := bufio.NewScanner(conn)

// 这里以\n作为分隔符
for scanner.Scan() {
// 命令分隔符,这里以一个空格作为分隔符
arr := strings.Split(scanner.Text(), " ")

command := arr[0]

fmt.Println(command)


switch command {
case "set":
key, value := arr[1], arr[2]
Put(key, value)
send.WriteString("Set success!\n")
case "get":
key := arr[1]

value := Get(key)

if value == "" {
send.WriteString("Get fail, key not found!\n")
} else {
send.WriteString("Get success, this is result: ")
send.WriteString(value)
send.WriteString("\n")
}

case "del":
key := arr[1]
Delete(key)
send.WriteString("Del success!\n")
default:
fmt.Println("Not support command!")
}

send.Flush()
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
}

这里用类似redis的command处理请求

  1. 新增、修改

    1
    set [key] [value]
  2. 读取

    1
    get [key]
  3. 删除

    1
    del [key]

我们使用telnet来测试

1
2
3
4
5
6
7
8
9
10
11
12
13
telnet 127.0.0.1 8080

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
set a b
Set success!
get a
Get success, this is result: b
del a
Del success!
get a
Get fail, key not found!

如预期的一致,这个时候我们的数据具有了处理网络连接的功能,但是显然这样的数据库还是太弱了。我们的最终目标是创建一个类似LevelDB+Redis的数据库

从零开始写数据库(二)

持久化数据

这里使用csv格式保存数据

修改db.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package main

import (
"encoding/csv"
"os"
"log"
"io/ioutil"
"bytes"
)

const FILE = "./db.csv"

// 声明一个map
var m map[string]string


func init() {
// 初始化map
m = make(map[string]string)

// 打开文件
file, err := os.Open(FILE)

if err != nil {
log.Fatal(err)
}

defer file.Close()

// 读取CSV文件
reader := csv.NewReader(file)

// 读取数据,这里data的类型是[][]string
data, err := reader.ReadAll()

if err != nil {
log.Fatal(err)
}

// 循环数组
for i := 0; i < len(data); i++ {
// 获取行
row := data[i]

// 第一个为key
key := row[0]
// 第二个为value
value := row[1]

// 保持到内存
m[key] = value
}
}
// 新建,修改
func Put(key string, value string) {
m[key] = value
}
// 删除
func Delete(key string) {
delete(m, key)
}
// 读取
func Get(key string) string {
return m[key]
}

// 写入文件
func Save() {

// 新建写入流
buf := new(bytes.Buffer)

writer := csv.NewWriter(buf)

log.Print(m)
for k, v := range m {
log.Printf(k, v)
// 写入数据
writer.Write([]string{k, v})
writer.Flush()
}

log.Println(buf)

// 保存到文件
ioutil.WriteFile(FILE, buf.Bytes(), 0777 )
}

我们写个测试类测试

main_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "testing"

func TestPut(t *testing.T) {
Put("hello", "world")
t.Log("insert a record")
value := Get("hello")
if value == "world" {
t.Log("get a record")
} else {
t.Error("insert error")
}

t.Log("save")
defer Save()
t.Log("save")
}

func TestGet(t *testing.T) {
value := Get("hello")
if value == "world" {
t.Log("get a record")
} else {
t.Error("insert error")
}
defer Save()
}

func TestDelete(t *testing.T) {
Delete("hello")
value := Get("hello")
if value == "world" {
t.Error("get a record")
} else {
t.Log("insert error")
}
defer Save()
}

开测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go test

2018/06/30 14:26:29 map[hello:world]
2018/06/30 14:26:29 hello: world
2018/06/30 14:26:29 hello,world

2018/06/30 14:26:29 map[hello:world]
2018/06/30 14:26:29 hello: world
2018/06/30 14:26:29 hello,world

2018/06/30 14:26:29 map[]
2018/06/30 14:26:29
PASS
ok github.com/ixiongdi/leveldb 0.013s

结果如预期的一样,这样我们实现了一个简单的具有数据持久化功能的kv数据库

从零开始写数据库(一)

前言

近些年各种数据库层出不穷,归根结底是因为互联网的快速发展,现有数据库无法满足需求。

数据库技术高深莫测,今天我们就来自己动手实现一个数据库系统。实现语言为Go。

当今数据库可分为几大类型

  1. Relational DBMS,传统的关系型数据库也就是我们常说的以SQL为查询语言的数据库,代表作有MySQL、PostgreSQL
  2. Document store,文档数据库,数据模式自由,代表作有MongoDB
  3. Key-value store,键值存储数据库,代表作有Redis
  4. Search engine,搜索引擎,代表作有Elasticsearch
  5. Wide column store,宽行数据库,代表作有Cassandra

虽然说数据库类型很多,但是其底层都离不开一个存储引擎。这个存储引擎多半是个KV数据库系统

我们以业内比较早,实现清晰的LevelDB作为参考实现

新建项目

在GitHub上新建一个项目LevelGo

新建文件main.go

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Printf("Hello, world!")
}

运行程序

1
go run main.go

输出结果

1
Hello, world!

基于内存的数据库

新建文件db.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

// 声明一个map
var m map[string]string

// 初始化map
func init() {
m = make(map[string]string)
}
// 新建,修改
func Put(key string, value string) {
m[key] = value
}
// 删除
func Delete(key string) {
delete(m, key)
}
// 读取
func Get(key string) string {
return m[key]
}

修改main.go

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

import "fmt"

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

// 插入
Put("hello", "world")

// 读取
fmt.Println(Get("hello"))

// 删除
Delete("hello")

// 读取
fmt.Println(Get("hello"))
}

运行

1
2
3
4
go run *.go

Hello, world!
world

当然一个完整的数据库不可能这么简单

这是一个内存数据库,不能持久化数据。而且支持的存储类型只有string一种

极致性能

前言

这篇文章主要讲目前我自己认为的最优化的性能解决方案

目前来说程序的前后分离越来越明显,而且端也是越来越多

简单点来说前端

  1. 传统的PC Web网页,虽然现在移动端盛行,但是例如企业官网、协同工作、网上商城、资讯类网站仍然需要一个传统的的WEB网站
  2. iOS、Android手机类App,人类已经离不开手机了
  3. 移动端的H5,跨平台的解决方啊,因为小程序的出现破灭了
  4. 小程序,包括微信小程序和支付宝小程序,为了构建自己的商业闭环
  5. 快应用,各大手机厂商联合搞的,类似小程序。为了不被垄断

这么复杂的前端,导致了前后分离的越来越明显,后端越来越只提供API服务,前端SPA(单页应用)

前后联动

一个完整的软件用户体验就是

  1. 用户操作
    例如注册
  2. 反馈到到端
    程序发起网络请求
  3. DNS
    网络请求通常都是域名访问、这一步会经过DNS解析拿到真实的IP地址。当然了直接写IP也行。
    通常来说这一步几乎不存在性能瓶颈DNS会有缓存,这一步的瓶颈更多的是网络延迟。这一步的优化更多的是选择一个好的DNS服务商,节点更多、延迟更低,服务更稳定。目前来说免费的就够用了
  4. API Gateway
    API网关,各大云服务商都有提供LBS,就我个人来说,必要性不是很大。如果说API流量很大,那么LBS目前的定制化功能是不够的,大公司一般会弄自己的LBS。对于小项目来说nginx足以。nginx也不会是性能的瓶颈,单机几百万并发。搞两个做备份绝大多数需求都能满足了
  5. Micro Service
    微服务,目前后端的趋势向微服务或Server Less方向发展。微服务的两大好处就是高可用解决糅合。为啥子呢?高可用的意思就是其中一个服务挂了其他的要正常。保证不全面崩溃。解决糅合就是开发上协作分工,更快交付
  6. Server Less
    目前比较新的概念,无服务开发模式。啥意思?
    目前各大云服务厂商推出了各种云存储、云直播、云分析、云数据库。人们的开发模式发生了很大变化,不再购买服务器、不再购买带宽、不再自建数据库、不再购买磁盘。人们只需专注于产品本身。人们使用云服务厂商提供的API写程序。后来人们发现,既然我全部使用的是云,那么我干嘛程序不也用云呢。于是就诞生了如AMS Lambda这样的服务。但是这个概念太新,和云服务厂商绑定的太死,灵活度不够。目前来说还不太能大规模部署,但是可以小规模试用

那么问题来了,后端性能的瓶颈几乎就落在了microservice上面

首先从语言层面来说,参考The Computer Language Benchmarks Game

  • 性能的第一档C、C++。

性能确实足够优秀,你也找不到更好的语言了。但是并不太适合microservice的开发。开发效率和工具链都不太适合,只能开发一些核心业务。

  • 性能第二档Rust、Go、Swift

这三个语言都是比较新,对人类比较友好的语言。

Rust比较像C++,说实话过于复杂了,性能接近C、C++等原生语言。目前来说网络框架和库比较少。更多的是开发一些系统级的应用,过高的复杂度在开发效率上也得不到保证。

Go比较简单,接近C的语法,各种网络框架和库层出不穷。比较适合微服务的开发

Swift苹果出品,语法上我觉得比较接近scala。目前来说也是网络框架和库比较少,更多还是开发iOS应用

  • 性能第三档Java、C#

Java目前使用最广泛的语言,框架和库数不胜数。JVM经过多年优化性能大概是C的一半。缺点就是比较吃内存,而且GC也十个问题。适合开发大型项目

C#性能上来说和Java差不多,但是框架和库相对Java来说是比较少的。没有Java活跃

  • 性能第四档JavaScript、TypeScript

本来JS应该属于第五档的语言,可是在V8加持下的Node天然的并发特性,使得它的性能和并发都能契合到微服务的开发上来

  • 性能第五档PHP、Python、Ruby

PHP近些年受到HHVM、PHP7的影响,性能有了大幅度的提升。但还是不够,这是语言特性受限。还是比较适合开发传统网站

Python的性能甚至不如PHP,还是适合当工具语言使用

Ruby和Python差不多,2.0以后性能有了些提升,但是和PHP一样,开发网站的利器。

总的来说,第五档的语言由于性能的关系,都不太适合开发微服务。

抉择1

优先选择Go

优点:语法简单、天然并发、性能高、社区成熟
缺点: 缺点就是Go还是不够普及、学习成本还是有的

其次选择Java

优点:成熟的框架和库,门槛低、简单易学、适合团队写作
缺点:GC的延迟、比较吃内存,性能还是比Java差一点,但是不多,能接受

再次选择TypeScript

优点:JavaScript加强版,完全兼容js缺摒弃了js的一些缺点,成熟的社区、高并发,前后端通用
缺点:性能比Go和Java要差的多,大概有几倍的差距

综合看来,我的抉择也是大势所趋。

Java已经有20多年的历史了,经久不衰,经过了时间的考验,近年来高居各大语言排行榜第一名。都说Java已死,目前看来再活个10年完全没有问题

Go自从2009年推出以后就蓬勃发展,很多重量级应用都是Go写的。背后也有谷歌的推动。在中国几乎成为了主流语言

TypeScript微软出品,JS多年来被人诟病,但是即使这样前端也被JS一统天下,当没有缺点的JS【TypeScript】,彻底的统一前后端

抉择2

脱离了语言那就是框架了,参考Web Framework Benchmarks

JSON serialization

在JSON serialization测试环节中前十名都是Java的项目,可见Java20年的沉淀,弄出了多少优秀的框架

Go的fasthttp排在13名,但是性能差别不大,能达到第一名的80%

Js的表现差一些排在23名,但是性能也能达到第一名的44%。这个性能的差距完全是语言本身的性能差距

当然这个测试的意义对于微服务来说更多体现在框架的IO性能和JSON的序列化性能

Single query

这个是单次查询,在实际的测试中肯定是要和数据库交互的,那么这个测试的参考意义还是很大的

这个要做一些删选

排第一的是Java的vertx,go的fasthttp排在第三,能达到第一名的63%,我个人认为这个差距可能是一些实现细节的差距,Js就比较惨了排到了28名,而且只有第一名性能的26%。

Multiple queries

在实际的业务中单次查询是解决不了问题的

排第一的还是vertx,够排在第四,能达到第一名的60%,js只能达到20%

后面的结果都差不多

其实go的性能和java应该算是旗鼓相当。js比较惨的原因可能就是语言性能本身的差距

目前看来Java的可选框架还是比较多的

底层基于Netty和Undertow的性能都不会差,可选的有Spring Framework、Act Framework、rapidoid、vertx

Go来说的话fasthttp一骑绝尘

JS可选业挺多的express、koa、restify

DB

在微服务中、最终落地还是需要喝数据库打交道

SQL

MySQL vs PostgreSQL

传统关系型数据库来说就两个选择mysql和postgresql。这两者的性能相当。就更广泛的通用性来说选择mysql比较好。之前就有人得出结论web服务用mysql,分析用postgresql。简单点来说如果你的微服务是面向大众用户的话选择mysql。如果内部使用选择PostgreSQL

NoSQL

Redis vs Memcache

memcache是个纯缓存数据库,redis不仅可以做缓存还可以持久化

目前来说redis发展迅猛。个人感觉没有必要在新项目中使用memcache

MongoDB

这个似乎没得选

具体业务中使用哪种数据库根据业务类型来判断

RPC vs RUSTful

内部服务建议使用RPC达到最高性能

rpc框架还是比较多的这里比较推荐谷歌开源的gRPC和Facebook开源的thrift,其他的都不推荐,第一是不跨语言,第二就是国内的开源的尿性,要么开源了不维护,要么只开源一部分藏着掖着

对外服务建议使用RESTful达到最广泛的通用性

Go Web Framework比较

前言

Go语言诞生快十年了,已经不能说是一门新语言了。现在涌现出来出来非常多的Web框架。今天就选取其中几个来比较一下

今天选取的是三个FullStack框架,也就是啥都能干的框架

beego

这个是国产的开源框架

先来看看怎么启动的

主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"hello/controllers"

"github.com/astaxie/beego"
)

func main() {
beego.BConfig.RunMode = "prod"
beego.Router("/json", &controllers.JsonController{})
beego.Router("/plaintext", &controllers.PlaintextController{})
beego.Run()
}

主程序主要是设置配置,配置路由和启动服务

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package controllers

type PlaintextController struct {
Base
}

const helloWorldString = "Hello, World!"

var (
helloWorldBytes = []byte(helloWorldString)
)

func (c *PlaintextController) Get() {
c.Ctx.Output.Header("Content-Type", "text/plain")
c.Ctx.Output.Body(helloWorldBytes)
}

控制器负责处理请求

整体看下来,封装的应该比较简洁,但是ctx这块感觉不太雅观

revel

revel比较像Java和Scala的Play Framework

revel这个框架封装的比较深,看官方的例子需要使用提供的revel命令行才能使用,虽然beego也提供了bee工具,但是也可以不使用

1
2
3
4
5
6
# 下载库
go get github.com/revel/revel
# 安装命令行
go get github.com/revel/cmd/revel
# 导出到环境变量
export PATH="$PATH:$GOPATH/bin"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
~
~ revel! http://revel.github.io
~
usage: revel command [arguments]

The commands are:

new create a skeleton Revel application
run run a Revel application
build build a Revel application (e.g. for deployment)
package package a Revel application (e.g. for deployment)
clean clean a Revel application's temp files
test run all tests from the command-line
version displays the Revel Framework and Go version

先新建一个项目

1
2
revel new demo
revel run demo

然后访问http://localhost:9000

这样看revel使用起来比较方便

路由是在单独的文件里面配置的,在/conf/routes,然后revel会自动生成routers.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Routes Config
#
# This file defines all application routes (Higher priority routes first)
#

module:testrunner
# module:jobs


GET / App.Index

# Ignore favicon requests
GET /favicon.ico 404

# Map static resources from the /app/public folder to the /public path
GET /public/*filepath Static.Serve("public")

# Catch all, this will route any request into the controller path
#
# **** WARNING ****
# Enabling this exposes any controller and function to the web.
# ** This is a serious security issue if used online **
#
# For rapid development uncomment the following to add new controller.action endpoints
# without having to add them to the routes table.
# * /:controller/:action :controller.:action

真正的处理请求在app.go

1
2
3
4
5
6
7
8
9
10
11
12
13
package controllers

import (
"github.com/revel/revel"
)

type App struct {
*revel.Controller
}

func (c App) Index() revel.Result {
return c.Render()
}

封装的确实比较深了,但是开发起来会很方便,就是自定义话成都不高

aah

安装工具

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 get -u aahframework.org/tools.v0/aah

aah

‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
aah framework v0.10 - https://aahframework.org
____________________________________________________________________
# Report improvements/bugs at https://github.com/go-aah/aah/issues #

Usage:
aah [global options] command [command options] [arguments...]

Commands:
new, n Create new aah 'web' or 'api' application (interactive)
run, r Run aah framework application (supports hot-reload)
build, b Build aah application for deployment
list, l List all aah projects in GOPATH
clean, c Cleans the aah generated files and build directory
switch, s Switch between aah release and edge version
update, u Update your aah to the latest release version on your GOPATH
generate, g Generates boilerplate code, configurations, complement scripts (systemd, docker), etc.
help, h Shows a list of commands or help for one command

Global Options:
-h, --help show help
-v, --version print aah framework version and go version

这三个框架都提供了命令行工具,方便了开发部署

aah也是通过配置文件来配置路由

config/routes.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
routes {
...
#------------------------------------------------------
# Pick an unique name, it's called `route name`,
# used for reverse URL.
#------------------------------------------------------
index {
# path is used to match incoming requests
# It can contain `:name` - Named parameter and
# `*name` - Catch-all parameter
path = "/"

# HTTP method mapping, It can be multiple `HTTP` methods with comma separated
# Default value is `GET`, it can be lowercase or uppercase
#method = "GET"

# The controller to be called for mapped URL path.
# * `controller` attribute supports with or without package prefix. For e.g.: `v1/User` or `User`
# * `controller` attribute supports both naming conventions. For e.g.: `User` or `UserController`
controller = "AppController"
...

控制类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package controllers

import (
"aahframework.org/aah.v0"
"sample/app/models"
)

// AppController struct application controller
type AppController struct {
*aah.Context
}

// Index method is application home page.
func (a *AppController) Index() {
data := aah.Data{
"Greet": models.Greet{
Message: "Welcome to aah framework - Web Application",
},
}

a.Reply().Ok().HTML(data)
}

Benchmark

这是一个第三方的测试结果

Web Framework JSON Plaintext
beego 334640 638004
revel 249814 215532
aah 181207 215532

从表格中可以看出beego的性能是最好的,不过几个框架的性能差别都不算特别大,性能也不是选择这几个框架的主要原因

总结

共同点

  1. 三个框架都是Fullstack框架,提供一站式解决方案
  2. 都提供了工具开发、测试、部署,但revel和aah集成度似乎更高,不能单独使用
  3. 性能方面相差不大,而且单机查询性能也能上十万,应该不会是瓶颈。go的性能还是有保障的

不同点

  1. beego类似传统的mvc框架
  2. revel和aah比较类似,都学的play framework

选择推荐

  1. 既然选择全栈开发框架,显然是为了满足展开发效率,beego是国人开发具有较好的中文文档,英文不太好的首选beego、
  2. revel提供了更为彻底的一站式解决方案,而且也比较成熟,推荐选择
  3. aah应该来说算是后起之秀,截止本文撰写之日在Github只有304个starts。比较适合尝鲜

使用Spring Data REST

介绍

Spring Data REST是个快速构建REST服务的脚手架,这里整个项目是基于Spring Boot的

下载依赖

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependencies>

实体类映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.List;

@Data
@Entity
public class Student {
@Id
@GeneratedValue
private long id;

private String name;
}

配置数据DAO

1
2
3
4
5
6
7
8
package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
public interface StudentRepository extends JpaRepository<Student, Long> {
}

CRUD

对于集合资源

GET 返回资源,通过page、size和sort来控制返回的数据
POST 创建一个新的资源
HEAD 资源是否可用

除了这些外,其他请求都不支持

对于单个资源

GET 返回单个资源
HEAD 返回资源是否可用
PUT 替换资源
PATCH 部分更新资源
DELETE 删除资源

对于关联的资源

GET 获取资源

Create

新建使用POST

1
2
3
4
5
6
7
8
curl -X POST \
http://localhost:8080/students \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 01b18db0-58a2-4315-8df3-0f783f8d13d0' \
-d '{
"name": "Jobs"
}'

Read

1
2
3
4
读取采用GET
curl -X GET http://localhost:8080/students

curl -X GET http://localhost:8080/students/1

Update

更新使用PUT

1
2
3
4
5
6
7
8
curl -X PUT \
http://localhost:8080/students/1 \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 0b4029f6-42ba-4c79-9c31-b2f1cb884898' \
-d '{
"name": "James"
}'

Delete

1
2
3
4
5
6
7
8
curl -X DELETE \
http://localhost:8080/students/1 \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-H 'Postman-Token: c13c0395-eb18-4b43-b16f-8cea06f5f0b0' \
-d '{
"name": "James"
}'

对于关联资源的CRUD

这个是被关联的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.List;

@Data
@Entity
public class Teacher {
@Id
@GeneratedValue
private Long id;

private String name;
}

这事包含关联资源的类

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
package com.example.demo;

import lombok.Data;
import org.springframework.data.annotation.LastModifiedDate;

import javax.persistence.*;
import java.util.Date;
import java.util.List;

@Data
@Entity
public class Student {
@Id
@GeneratedValue
private long id;
@Version
private long version;
@LastModifiedDate
private Date date;

private String name;

@OneToMany
private List<Teacher> teachers;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.List;

@Data
@Entity
public class Teacher {
@Id
@GeneratedValue
private Long id;

private String name;
}

Create

  1. 先插入两个教师
1
2
curl -X POST http://localhost:8080/teachers -d '{ "name": "James Hadden" }'
curl -X POST http://localhost:8080/teachers -d '{ "name": "Crise Pual" }'

教师的ID分别为1, 2

  1. 新建学生
1
2
3
curl -X POST http://localhost:8080/students -d '{ "name": "James Hadden", "teachers": [
"http://localhost:8080/teachers/1", "http://localhost:8080/teachers/1"
] }'

这个时候会新建成功,并且教师的信息也会关联到学生上

1
2
3
curl -X POST http://localhost:8080/students -d '{ "name": "James Hadden", "teachers": [
"http://localhost:8080/teachers/1", "http://localhost:8080/teachers/1"
] }'

我们再次插入学生信息,这个时候数据库就会报错了,因为@OneToMany注解是一个教师只能属于一个学生。这个时候注解应该改为@ManyToMany,这个时候就能插入了

Update

未完待续!

使用Rancher2.0快速构建Kubernetes

前言

上一篇使用的是Rancher1.6构建的k8s,但是现在Rancher出新版了,并且好像只支持K8s了,看来k8s确实一统天下了

环境准备

Rancher 2.0 推荐使用Ubuntu 16.04构建,其他的好像支持的不是太好,然后节点最好也是Ubuntu 16.04,而且Docker的版本最高也才支持到17.03。

所以我们需要以下服务器资源:

  1. 一台安装了Docker 17.03.2的Ubuntu 16.04主机,用来安装Rancher Server 2.0。内存要大于等于4G,不然很可能跑不起来
  2. 三台Ubuntu16.04主机,这个一般使用API自动安装

开始

  1. 启动一个Ubuntu 16.04虚拟主机

  2. 远程到虚拟主机

这里用户名又是root了,根据镜像和云服务提供商可能有所不同,这里我用的还是DigitalOcean,这是我的优惠码http://www.digitalocean.com/?refcode=f439670561f1

1
ssh root@<server-ip>
  1. 安装Docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 安装依赖
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
# 添加GPG
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# 验证GPG
sudo apt-key fingerprint 0EBFCD88
# 添加源
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
# 更新源
sudo apt-get update
# 查看可用版本
apt-cache madison docker-ce
# 安装版本,这里推荐17.03.2
sudo apt-get install docker-ce=<VERSION>
  1. 安装Rancher 2.0

这里绑定了443端口,也就是开启了https,我没试过不用行不行,如果用了,会被浏览器不信任,添加为信任即可访问了

1
sudo docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher

  1. 访问https://<server-ip>

安装好以后,应该长这个样子,第一次进去会让你输入一个管理员密码的

  1. 新建集群

选择一个你的云主机提供商,我这里是DigitalOcean

新建主机池,这里推荐三个,功能的话全部勾选吧

  1. etcd是个分布式的kv数据库,用来同步集群信息的,这里叫做数据平面
  2. Control是无状态的集群,负责管理集群的,这里叫编排平面
  3. Worker是工作集群,也就是你的应用和服务部署的集群,这里叫计算平面

  1. 集群建好后,默认会进入名为default的命名空间

这个是集群面板,看上去还蛮好看的

忘了说了,默认进去是英文界面,这里可以切换到中文

  1. 新建一个工作负载

这里选用nginx,因为比较简单,部署2个实例

  1. 新建一个负债均衡

这里选择自动生成一个主机名,因为我们还没有启用dns,除非直接使用IP访问,不然访问不到的,xip.io相当于一个免费的域名和dns服务

  1. 访问

等个差不多一分钟,分派好IP以后,就可以直接外网访问了

使用Rancher快速构建Kubernetes

前言

最近在玩Kubernetes,时下最火的容器编排工具,国内外三大云计算平台AWSGoogleAurze
国内阿里腾讯百度无不支持。可以说已经成为了容器编排领域的事实标准了,现在再搞别的纯属浪费时间。可惜Kubernetes这玩意太复杂,要先玩转先得理解其概念,可能大半天就过去了。

踩坑

官网提供minikube的mini单机kube安装方式,首先这玩意在我的windows下不太支持,包括在win10下的linux子系统也不行
其次在我的老款macbook pro下也安装不了,还一个问题,这是个单机版的,无法体验其所有特性,所以放弃了

官网还提供了个多节点的部署方式,但是一看说明文档,现在只是实验性支持,瞬间没了兴趣,坑太多不敢踩。
无意中看到了Rancher这个神器,几乎全程自动安装不需要敲键盘。

开始

这里我使用的DigitalOcean云服务,这里不太推荐在本地环境安装k8s,第一安装一个k8s最少需要四台虚拟机或者服务器,每台机器不低于1GB的内存,而且如果使用Rancher的形式安装,那么可能内存不低于2GB,而且本机环境或者本地虚拟机需要解决硬件和网络问题

所以选择云计算。这里我推荐DigitalOcean,现在注册并认证信用卡就送100美元,有效期2个月。如果没有这个优惠了,可以用我的邀请码,送10美元http://www.digitalocean.com/?refcode=f439670561f1

理论上其他的云服务提供商也是可以的,但是如果是国内的服务器,需要解决网络的问题。推荐走阿里的镜像

  1. 先搞一台机器安装Rancher OS,配置最低2GB内存。不然可能跑不起来

Rancher OS是一个比较纯净的系统,里面自带Docker。理论上安装Ubuntu 16.04也可以的,只是需要自己安装Docker。而且对版本可能也有要求需要17.03

选择镜像

选择规格

  1. 然后进入Rancher OS,注意ssh进入的时候用户名是rancher,不然进不去

远程登陆

1
ssh rancher@<server-ip>
  1. 安装Rancher Server

这里8080端口也可以改为其他端口,国内的运营商可能需要备案才能访问80,443,8080,这个时候可以改为其他端口

1
sudo docker run -d --restart=unless-stopped -p 8080:8080 rancher/server:stable

等待安装成功

  1. 安装成功后进入浏览器输入http://:8080

第一次加载可能有点慢,耐心等待

  1. 然后点击环境管理,添加Kubernetes环境,这个时候需要添加主机,这里最少添加三个主机,不然kubernetes dashboard打不开

添加环境

选择k8s

  1. 三个主机添加好以后,就可以进入UI界面了

  1. 点击工作负载,开始部署一个应用

  1. 这里先部署一个nginx应用,设置三个副本,对外暴露端口80

  1. 等待几分钟后进入overview界面打开链接,这个时候可以看到这个,说明nginx已经成功启动并可以对外提供服务

进入overview

点击链接

Brainfuck语言解释器的Java实现

我们先来看看Brainfuck的定义:

Müller的目标是创建一种简单的、可以用最小的编译器来实现的、匹配图灵完全思想的编程语言。这种语言由八种运算符构成,为Amiga机器编写的编译器(第二版)只有240个字节大小。

就象它的名字所暗示的,brainfuck程序很难读懂。尽管如此,brainfuck图灵机一样可以完成任何计算任务。虽然brainfuck的计算方式如此与众不同,但它确实能够正确运行。

字符 含义
> 指针加一
< 指针减一
+ 指针指向的字节的值加一
- 指针指向的字节的值减一
. 输出指针指向的单元内容(ASCII码)
, 输入内容到指针指向的单元(ASCII码)
[ 如果指针指向的单元值为零,向后跳转到对应的]指令的次一指令处
] 如果指针指向的单元值不为零,向前跳转到对应的[指令的次一指令处

这种语言基于一个简单的机器模型,除了指令,这个机器还包括:一个以字节为单位、被初始化为零的数组、一个指向该数组的指针

(初始时指向数组的第一个字节)、以及用于输入输出的两个字节流。

首先我们要有一个以字节为单位的数组:

1
byte[] memory = new byte[10000];

这个就是通常计算机的内存,现在假设这个内存有10000长度那么大

然后我们需要一个指针:

1
int point = 0;

初始化为0,也就是指向内存的第一个地址

这个语言的源代码只有8个字符,我们假定源码为:

1
String source;

现在我们要读取源码,我们需要一个辅助的属性index来标示我们读到了代码的第几个字符:

1
int index

然后我们来处理这些指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch (b) {
case '>':
point++;
break;
case '<';
point--;
break;
case '+';
memory[point]++;
break;
case '-';
memory[point]--;
break;
case '.'
System.out.print(memory[point]);
break;
case ',';
memory[point] = (byte) System.in.read();
break;
}

其实前面6个指令都很简单,关键在于后面2个指令,这个两个指令实际上就是才是逻辑和循环,这其中的核心就是跳转,就是把源代码的指针index的值进行改变,而这个改变是有条件的,下面就是我对这个逻辑的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (b) {
case '[':
if (memory[point] == 0) {
while (source.charAt(index) != ']') {
index++;
}
}
break;
case ']';
if (memory[point] != 0) {
while (source.charAt(index) != '[') {
index--;
}
}
}

其实就是用while循环来检查代码,然后改变index的值,乍一看好像没毛病,是的,如果是下面这个输出hello, world!的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+++++ +++++             initialize counter (cell #0) to 10
[ use loop to set the next four cells to 70/100/30/10
> +++++ ++ add 7 to cell #1
> +++++ +++++ add 10 to cell #2
> +++ add 3 to cell #3
> + add 1 to cell #4
<<<< - decrement counter (cell #0)
]
> ++ . print 'H'
> + . print 'e'
+++++ ++ . print 'l'
. print 'l'
+++ . print 'o'
> ++ . print ' '
<< +++++ +++++ +++++ . print 'W'
> . print 'o'
+++ . print 'r'
----- - . print 'l'
----- --- . print 'd'
> + . print '!'
> . print '\n'

确实没有毛病,但是对于下面这个乘法

1
2
3
,>,,>++++++++[<------<------>>-]
<<[>[>+>+<<-]>>[<<+>>-]<<<-]
>>>++++++[<++++++++>-],<.>.

就有问题了,因为这里面有个多重循环,如何界定“[”对应的“]”就不准确了,这才是整个程序的难点。

我们先来看看,如何理解对应这个意思,我的理解为:如果字符[]中间有n个[和n个]字符,那么他们是一对,n>=0

按照这个理论,我们开始写代码

先定义一个计数器,出现[就加1,出现]就减1,这和上面那个理解是一个意思,只不过简化为一个变量表示

1
int count = 0;

然后在定义一个index移动的偏移量

1
int offset = 0;

然后开始跳转

1
2
3
4
5
6
7
8
9
10
// 起始偏移量为1,一直到代码结束
for (int offset = 1; index + offset < source.length(); offset++) {
// 找到跳转字符]且n相等,也就是count=0,计算index的值并跳出循环
if (source.charAt(index + offset) == ']' && count == 0) {
index += offset;
break;
}
if (source.charAt(index + offset) == '[') count++;
if (source.charAt(index + offset) == ']' && count != 0) count--;
}

下面是完整的代码:

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
package com.github.xiongdi.study;

public class BrainfuckInterpreter {

private byte[] memory = new byte[1000];

private int point = 0;

public void run(String source) throws Exception {
for (int index = 0; index < source.length(); index++) {
switch (source.charAt(index)) {
case '>':
point++;
break;
case '<':
point--;
break;
case '+':
memory[point]++;
break;
case '-':
memory[point]--;
break;
case '.':
System.out.print((char) memory[point]);
break;
case ',':
memory[point] = (byte) System.in.read();
break;
case '[':
if (memory[point] == 0) {
int count = 0;

for (int offset = 1; index + offset < source.length(); offset++) {
if (source.charAt(index + offset) == ']' && count == 0) {
index += offset;
break;
}
if (source.charAt(index + offset) == '[') count++;
if (source.charAt(index + offset) == ']' && count != 0) count--;
}
}
break;
case ']':
if (memory[point] != 0) {
int count = 0;

for (int offset = 1; index - offset >= 0; offset++) {
if (source.charAt(index - offset) == '[' && count == 0) {
index -= offset;
break;
}
if (source.charAt(index - offset) == ']') count++;
if (source.charAt(index - offset) == '[' && count != 0) count--;
}
}
break;
}
}
}
}

使用xjc工具自动生成DTD格式的JAXB类

文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。

DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用。

我们先来看一下一个典型 的DTD文档定义:

1
2
3
4
5
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>

新建个文件post.dtd

1
touch post.dtd

然后把刚才的文件定义写入文件。

xjc是一个java命令行工具,用户把XML格式定义的数据结构生成生成 JAXB 类。只要你安装了JDK就会有这个工具

我们先来看看xjc具有哪些功能

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
用法: xjc [-options ...] <schema file/URL/dir/jar> ... [-b <bindinfo>] ...
如果指定 dir, 将编译该目录中的所有模式文件。
如果指定 jar, 将编译 /META-INF/sun-jaxb.episode 绑定文件。
选项:
-nv : 不对输入模式执行严格验证
-extension : 允许供应商扩展 - 不严格遵循
JAXB 规范中的兼容性规则和应用程序 E.2
-b <file/dir> : 指定外部绑定文件 (每个 <file> 必须具有自己的 -b)
如果指定目录, 则将搜索 **/*.xjb
-d <dir> : 生成的文件将放入此目录中
-p <pkg> : 指定目标程序包
-httpproxy <proxy> : 设置 HTTP/HTTPS 代理。格式为 [user[:password]@]proxyHost:proxyPort
-httpproxyfile <f> : 作用与 -httpproxy 类似, 但在文件中采用参数来保护口令
-classpath <arg> : 指定查找用户类文件的位置
-catalog <file> : 指定用于解析外部实体引用的目录文件
支持 TR9401, XCatalog 和 OASIS XML 目录格式。
-readOnly : 生成的文件将处于只读模式
-npa : 禁止生成程序包级别注释 (**/package-info.java)
-no-header : 禁止生成带有时间戳的文件头
-target (2.0|2.1) : 行为与 XJC 2.0 或 2.1 类似, 用于生成不使用任何 2.2 功能的代码。
-encoding <encoding> : 为生成的源文件指定字符编码
-enableIntrospection : 用于正确生成布尔型 getter/setter 以启用 Bean 自测 apis
-contentForWildcard : 为具有多个 xs:any 派生元素的类型生成内容属性
-xmlschema : 采用 W3C XML 模式处理输入 (默认值)
-relaxng : 采用 RELAX NG 处理输入 (实验性的, 不支持)
-relaxng-compact : 采用 RELAX NG 简洁语法处理输入 (实验性的, 不支持)
-dtd : 采用 XML DTD 处理输入 (实验性的, 不支持)
-wsdl : 采用 WSDL 处理输入并编译其中的模式 (实验性的, 不支持)
-verbose : 特别详细
-quiet : 隐藏编译器输出
-help : 显示此帮助消息
-version : 显示版本信息
-fullversion : 显示完整的版本信息


扩展:
-Xinject-code : inject specified Java code fragments into the generated code
-Xlocator : enable source location support for generated code
-Xsync-methods : generate accessor methods with the 'synchronized' keyword
-mark-generated : mark the generated code as @javax.annotation.Generated
-episode <FILE> : generate the episode file for separate compilation
-Xpropertyaccessors : Use XmlAccessType PROPERTY instead of FIELD for generated classes

默认情况下xjc是吧.xsd格式的数据生成JAXB类,如果要使用.dtd生成JAXB类,需要使用参数`-dtd`

1
xjc -dtd post.dtd

然后生成了如下两个类:

1
2
generated/Note.java
generated/ObjectFactory.java

我们来看看生成的类Note.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//
// 此文件是由 JavaTM Architecture for XML Binding (JAXB) 引用实现 v2.2.8-b130911.1802 生成的
// 请访问 <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a>
// 在重新编译源模式时, 对此文件的所有修改都将丢失。
// 生成时间: 2017.09.30 时间 04:04:53 PM CST
//


package generated;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;


/**
*
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"to",
"from",
"heading",
"body"
})
@XmlRootElement(name = "note")
public class Note {

@XmlElement(required = true)
protected String to;
@XmlElement(required = true)
protected String from;
@XmlElement(required = true)
protected String heading;
@XmlElement(required = true)
protected String body;

/**
* 获取to属性的值。
*
* @return
* possible object is
* {@link String }
*
*/
public String getTo() {
return to;
}

/**
* 设置to属性的值。
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setTo(String value) {
this.to = value;
}

/**
* 获取from属性的值。
*
* @return
* possible object is
* {@link String }
*
*/
public String getFrom() {
return from;
}

/**
* 设置from属性的值。
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setFrom(String value) {
this.from = value;
}

/**
* 获取heading属性的值。
*
* @return
* possible object is
* {@link String }
*
*/
public String getHeading() {
return heading;
}

/**
* 设置heading属性的值。
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setHeading(String value) {
this.heading = value;
}

/**
* 获取body属性的值。
*
* @return
* possible object is
* {@link String }
*
*/
public String getBody() {
return body;
}

/**
* 设置body属性的值。
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setBody(String value) {
this.body = value;
}

}

这样一个标准的JAXB类就生成好了,可以序列化为xml文档了。