分享
  1. 首页
  2. 文章

Go微服务系列 - 第三部分 - 嵌入数据库和JSON

WalkerQiao · · 1409 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

第三部分: Go微服务 - 嵌入数据库和JSON

在第三部分,我们让accountservice做一些有意义的事情。

  • 声明一个Account结构体。
  • 嵌入简单的key-value存储,我们可以在里边存储Account结构。
  • 将结构体序列化为JSON, 然后通过HTTP服务来为/accounts/{accountId}提供服务。

源代码

源代码位置: https://github.com/callistaen...

声明Account结构体

结构体的详细说明可以参照参考链接部分的相关链接查看。

  1. 在我们的项目根目录accountservice下面创建一个名为model的目录。
  2. 在model目录下面创建account.go文件。
package model
type Account struct {
 Id string `json:"id"`
 Name string `json:"name"`
}

Account抽象成包含Id和Name的结构体。结构体的两个属性首字母为大写,表示声明的是全局作用域可见的(标识符首字母大写public, 首字母小写包作用域可见)。

另外结构体中还使用了标签(Tag)。这些标签在encoding/json和encoding/xml中有特殊应用。

假设我们定义结构体的时候没有使用标签,对于结构体通过json.Marshal之后产生的JSON的key使用结构体字段名对应的值。

例如:

type Account struct {
 Id string
 Name string
}
var account = Account{
 Id: 10000,
 Name: "admin",
}

转换为json之后得到:

{
 "Id": 10000,
 "Name": "admin"
}

而这种形式一般不是JSON的惯用形式,我们通常更习惯使用json的key首字母为小写的,那么结构体标签就可以派上用场了:

type Account struct {
 Id string `json:"id"`
 Name string `json:"name"`
}
var account = Account{
 Id: 10000,
 Name: "admin",
}

这个时候转换为JSON的时候,我们就得到如下结果:

{
 "id": 10000,
 "name": "admin"
}

嵌入一个key-value存储

为了简单起见,我们使用一个简单的key-value存储BoltDB, 这是一个Go语言的嵌入式key-value数据库。它主要能为应用提供快速、可信赖的数据库,这样我们无需复杂的数据库,比如MySql或Postgres等。

我们可以通过go get获取它的源代码:

go get github.com/boltdb/bolt

接下来,我们在accountservice目录下面创建一个dbclient的目录,并在它下面创建boltclient.go文件。 为了后续模拟的方便,我们声明一个接口,定义我们实现需要履行的合约:

package dbclient
import (
 "github.com/callistaenterprise/goblog/accountservice/model"
)
type IBoltClient interface() {
 OpenBoltDb()
 QueryAccount(accountId string) (model.Account, error)
 Seed()
}
// 真实实现
type BoltClient struct {
 boltDb *bolt.DB
}
func (bc *BoltClient) OpenBoltDB() {
 var err error
 bc.boltDB, err = bolt.Open("account.db", 0600, nil)
 if err != nil {
 log.Fatal(err)
 }
}

上面代码声明了一个IBoltClient接口, 规定了该接口的合约是具有三个方法。我们声明了一个具体的BoltClient类型, 暂时只为它实现了OpenBoltDB方法。这种实现接口的方法,突然看起来可能感觉有点奇怪,把函数绑定到一个结构体上。这就是Go语言接口实现的特色。其他两个方法暂时先跳过。

我们现在有了BoltClient结构体,接下来我们需要在项目中的某个位置有这个结构体的一个实例。 那么我们就将它放到我们即将使用的地方, 放在我们的goblog/accountservice/service/handlers.go文件中。 我们首先创建这个文件,然后添加BoltClient的实例:

package service
import (
 "github.com/callistaenterprise/goblog/accountservice/dbclient"
)
var DBClient dbclient.IBoltClient

然后更新main.go代码,让它启动的时候打开DB。

func main() {
 fmt.Printf("Starting %v\n", appName)
 initializeBoltClient() // NEW
 service.StartWebServer("6767")
}
// Creates instance and calls the OpenBoltDb and Seed funcs
func initializeBoltClient() {
 service.DBClient = &dbclient.BoltClient{}
 service.DBClient.OpenBoltDb()
 service.DBClient.Seed()
}

这样我们的微服务启动的时候就会打开数据库。但是,这里还是什么都没有做。 我们接下来添加一些代码,让服务启动的时候可以为我们引导一些账号。

启动时填充一些账号

打开boltclient.go代码文件,为BoltClient添加一个Seed方法:

// Start seeding accounts
func (bc *BoltClient) Seed() {
 initializeBucket()
 seedAccounts()
}
// Creates an "AccountBucket" in our BoltDB. It will overwrite any existing bucket of the same name.
func (bc *BoltClient) initializeBucket() {
 bc.boltDB.Update(func(tx *bolt.Tx) error {
 _, err := tx.CreateBucket([]byte("AccountBucket"))
 if err != nil {
 return fmt.Errorf("create bucket failed: %s", err)
 }
 return nil
 })
}
// Seed (n) make-believe account objects into the AcountBucket bucket.
func (bc *BoltClient) seedAccounts() {
 total := 100
 for i := 0; i < total; i++ {
 // Generate a key 10000 or larger
 key := strconv.Itoa(10000 + i)
 // Create an instance of our Account struct
 acc := model.Account{
 Id: key,
 Name: "Person_" + strconv.Itoa(i),
 }
 // Serialize the struct to JSON
 jsonBytes, _ := json.Marshal(acc)
 // Write the data to the AccountBucket
 bc.boltDB.Update(func(tx *bolt.Tx) error {
 b := tx.Bucket([]byte("AccountBucket"))
 err := b.Put([]byte(key), jsonBytes)
 return err
 })
 }
 fmt.Printf("Seeded %v fake accounts...\n", total)
}

上面我们的Seed方法首先使用"AccountBucket"字符串创建一个Bucket, 然后连续创建100个初始化账号。账号id分别依次为10000~10100, 其Name分别为Person_i(i = 0 ~ 100)。

前面我们在main.go中已经调用了Seed()方法,因此这个时候我们可以运行下当前的程序,看看运行情况:

> go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017年01月31日 16:30:59 Starting HTTP service at 6767

很不错!那么我们先暂停执行,使用Ctrl + C让服务先停下来。

添加查询方法

接下来我们可以为boltclient.go中添加一个Query方法来完成DB API。

func (bc *BoltClient) QueryAccount(accountId string) (model.Account, error) {
 // Allocate an empty Account instance we'll let json.Unmarhal populate for us in a bit.
 account := model.Account{}
 // Read an object from the bucket using boltDB.View
 err := bc.boltDB.View(func(tx *bolt.Tx) error {
 // Read the bucket from the DB
 b := tx.Bucket([]byte("AccountBucket"))
 // Read the value identified by our accountId supplied as []byte
 accountBytes := b.Get([]byte(accountId))
 if accountBytes == nil {
 return fmt.Errorf("No account found for " + accountId)
 }
 // Unmarshal the returned bytes into the account struct we created at
 // the top of the function
 json.Unmarshal(accountBytes, &account)
 // Return nil to indicate nothing went wrong, e.g no error
 return nil
 })
 // If there were an error, return the error
 if err != nil {
 return model.Account{}, err
 }
 // Return the Account struct and nil as error.
 return account, nil
}

这个方法也比较简单,根据请求参数accountId在我们之前初始化的DB中查找这个账户的相关信息。如果成功查找到相关账号,返回这个账号的json数据,否则会返回nil。

通过HTTP提供账号服务

让我们修改在/service/routes.go文件中声明的/accounts/{accountId}路由,让它返回我们填充的账号其中一个记录。代码修改如下:

package service
import "net/http"
// Defines a single route, e.g. a human readable name, HTTP method, pattern the function that will execute when the route is called.
type Route struct {
 Name string
 Method string
 Pattern string
 HandlerFunc http.HandlerFunc
}
// Defines the type Routes which is just an array (slice) of Route structs.
type Routes []Route
var routes = Routes{
 Route{
 "GetAccount", // Name
 "GET", // HTTP method
 "/accounts/{accountId}", // Route pattern
 GetAccount,
 },
}

接下来,我们更新下/service/handlers.go,创建一个GetAccount函数来实现HTTP处理器函数签名:

var DBClient dbclient.IBoltClient
func GetAccount(w http.ResponseWriter, r *http.Request) {
 // Read the 'accountId' path parameter from the mux map
 var accountId = mux.Vars(r)["accountId"]
 // Read the account struct BoltDB
 account, err := DBClient.QueryAccount(accountId)
 // If err, return a 404
 if err != nil {
 w.WriteHeader(http.StatusNotFound)
 return
 }
 // If found, marshal into JSON, write headers and content
 data, _ := json.Marshal(account)
 w.Header().Set("Content-Type", "application/json")
 w.Header().Set("Content-Length", strconv.Itoa(len(data)))
 w.WriteHeader(http.StatusOK)
 w.Write(data)
}

上面代码就是实现了处理器函数签名,当Gorilla检测到我们在请求/accounts/{accountId}的时候,它就会将请求路由到这个函数。 下面我们运行一下我们的服务。

> go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017年01月31日 16:30:59 Starting HTTP service at 6767

然后另外开一个窗口,curl请求accountId为10000的请求:

> curl http://localhost:6767/accounts/10000
{"id":"10000","name":"Person_0"}

非常棒,我们微服务现在能够动态提供一些简单的数据了。你可以尝试使用accountId为10000到10100之间的任何数字,得到的JSON都不相同。

占用空间和性能

(FOOTPRINT在这里解释为占用空间, 内存空间)。

第二部分,我们看到在Galtling压测情况下空间占用信息如下:

clipboard.png

同样我们再次对服务做个压测,得到的空间占用情况如下:

clipboard.png

我们可以看到,在增加了boltdb之后,内存占用由2.1MB变成31.2MB, 增加了30MB左右,还不算太差劲。

clipboard.png

每秒1000个请求,每个CPU核大概使用率是10%,BoltDB和JSON序列化的开销不是很明显,很不错!顺便说下,我们之前的Java进程在Galting压测下,CPU使用大概是它的3倍。

clipboard.png

平均响应时间依然小于1毫秒。 可能我们需要使用更重的压测进行测试,我们尝试使用每秒4K的请求?(注意,我们可能需要增加OS级别的可用文件处理数)。

clipboard.png

占用内存变成118MB多,基本上比原来增加到了4倍。内存增加几乎是因为Go语言运行时或者是因为Gorilla增加了用于服务请求的内部goroutine的数量,因此负载增加。

clipboard.png

CPU基本上保持在30%。 我运行在16GB RAM/Core i7的笔记本上的, 我认为I/O或文件句柄比CPU更快成为性能瓶颈。

clipboard.png

平均吞吐量最后上升到95%的请求在1ms~3ms之间。 确实在4k/s的请求时候,吞吐量受到了些影响, 但是个人认为这个小的accountservice服务使用BoltDB,执行还是相当不错的。

最后的话

下一部分,我们会探讨下使用GoConvey和模拟BoltDB客户端来进行单元测试。

参考链接


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:WalkerQiao

查看原文:Go微服务系列 - 第三部分 - 嵌入数据库和JSON

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1409 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏