基於 GoLang 進行分布式数据库实践

2022-06-09 19:40:04 浏览数 (1)

Hello folks,今天我们介绍一下如何基于 Golang Web 应用程序进行分布式数据库操作,这里我们以 CockroachDB 开源数据库为例。

CockroachDB 是一款开源的分布式数据库,具有 NoSQL 对海量数据的存储管理能力,又保持了传统数据库支持的 ACID 和 SQL 等,还支持跨地域、去中心、高并发、多副本强一致和高可用等特性。支持 OLTP 场景,同时支持轻量级 OLAP 场景。

接下来,我们开始进行部署操作,首先我们检查一下当前的环境信息,具体如下所示:

代码语言:javascript复制
[leonli@192 go ] % go version
go version go1.18.1 darwin/arm64

为了方便起见,我们基于 Docker-compose 新建一个 CockroachD 集群,并将其运行起来,具体如下:

代码语言:javascript复制
[leonli@192 CockroachDB ] % more docker-compose-cockroach.yml 
version: "3.5"

# Reference: https://www.cockroachlabs.com/docs/v21.1/start-a-local-cluster-in-docker-linux
services:
    # Node 1 (main)
    roach1:
        container_name: cockroach-db-1
        image: cockroachdb/cockroach:v21.2.10
        hostname: roach1
        # Assign Container to a Network
        networks: 
            - cockroach-net
        ports:
            - 26257:26257   # Cockroach Database port
            - 9090:8080     # Cockroach UI Panel port
        volumes:
            - ./cockroach-data/roach1:/cockroach/cockroach-data
        command: start --insecure  --join=roach1,roach2,roach3
      
    # Node 2
    roach2:
        container_name: cockroach-db-2
        image: cockroachdb/cockroach:v21.2.10
        hostname: roach2
        # Assign Container to a Network
        networks: 
            - cockroach-net
        volumes:
            - ./cockroach-data/roach2:/cockroach/cockroach-data
        command: start --insecure --join=roach1,roach2,roach3
        
    # Node 3
    roach3:
        container_name: cockroach-db-3
        image: cockroachdb/cockroach:v21.2.10
        hostname: roach3
        # Assign Container to a Network
        networks: 
            - cockroach-net
        volumes:
            - ./cockroach-data/roach3:/cockroach/cockroach-data
        command: start --insecure --join=roach1,roach2,roach3
    
    # First Time Initialization of Cockroach DB
    init-roach1:
        image: cockroachdb/cockroach:latest
        container_name: init-cockroach
        depends_on: 
            - roach1
        restart: "no"
        # Assign Container to a Network
        networks: 
            - cockroach-net
        entrypoint: ["bash", "-c" ,"./cockroach init --insecure --host cockroach-db-1:26257 | wc || exit"]

# Initialize a Network 
networks:
    cockroach-net:
        driver: bridge
代码语言:javascript复制
[leonli@192 CockroachDB ] % docker-compose -f docker-compose-cockroach.yml up -d 

然后,在浏览器中输入:http://localhost:9090/#/overview/list 进行访问,如下所示:

现在,我们进入 CockroachD 集群容器中,进行相关库表的创建,具体如下所示:

代码语言:javascript复制
[leonli@192 CockroachDB ] % docker exec -it 05f9ff283fb8 bash
[[root@roach1 cockroach]] # ls -l
total 212444
-rwxr-xr-x  1 root root 217535728 May  2 17:51 cockroach
drwxr-xr-x 24 root root       768 May 22 05:44 cockroach-data
-rwxr-xr-x  1 root root       120 May  2 17:28 cockroach.sh
代码语言:javascript复制
[[root@roach1 cockroach]] # cockroach sql --insecure
#
# Welcome to the CockroachDB SQL shell.
# All statements must be terminated by a semicolon.
# To exit, type: q.
#
# Server version: CockroachDB CCL v21.2.10 (x86_64-unknown-linux-gnu, built 2022/05/02 17:38:58, go1.16.6) (same version as client)
# Cluster ID: 369395e8-8b75-4d7a-9b5a-b7212c4313bb
#
# Enter ? for a brief introduction.
#
[root@:26257/defaultdb> show databases;
  database_name | owner | primary_region | regions | survival_goal
---------------- ------- ---------------- --------- ----------------
  defaultdb     | root  | NULL           | {}      | NULL
  postgres      | root  | NULL           | {}      | NULL
  system        | node  | NULL           | {}      | NULL
代码语言:javascript复制
[root@:26257/defaultdb> CREATE USER IF NOT EXISTS luga;
CREATE ROLE

Time: 276ms total (execution 272ms / network 3ms)

[root@:26257/defaultdb> CREATE DATABASE books;
CREATE DATABASE

Time: 42ms total (execution 42ms / network 1ms)

[root@:26257/defaultdb> GRANT ALL ON DATABASE books TO luga;
GRANT

Time: 139ms total (execution 138ms / network 1ms)

[root@:26257/defaultdb> use books
                    -> ;
SET

Time: 2ms total (execution 2ms / network 0ms)

[root@:26257/books> show tables;
SHOW TABLES 0

Time: 151ms total (execution 150ms / network 1ms)

[root@:26257/books> CREATE TABLE tblbooks(
id INT PRIMARY KEY,
name VARCHAR,
phone VARCHAR,
email VARCHAR,
stars INT,
category VARCHAR
);
CREATE TABLE

Time: 28ms total (execution 28ms / network 1ms)

[root@:26257/books> show tables;
  schema_name | table_name | type  | owner | estimated_row_count | locality
-------------- ------------ ------- ------- --------------------- -----------
  public      | tblbooks     | table | root  |                   0 | NULL
(1 row)

Time: 120ms total (execution 119ms / network 1ms)

[root@:26257/books> show tables;
  schema_name | table_name | type  | owner | estimated_row_count | locality
-------------- ------------ ------- ------- --------------------- -----------
  public      | tblbooks     | table | root  |                   0 | NULL
(1 row)

Time: 75ms total (execution 74ms / network 1ms)

[root@:26257/books> INSERT INTO tblbooks (id, name, phone, email, stars, category) VALUES
(1,'Lucy','130-0000-0000','lucy13000000000@example.net',8,'life'),
(2,'Green ','125-0456-0102','green1250456@example.com',1,'artity'),
(3,'Linda','857-555-0182','linda857@example.com',3,'moon'),
(4,'Wilium','999-555-000','wilium@example.com',5,'sunset'),
(5,'Lily','234-2123-1231','lily1900@example.com',5,'work'),
(6,'Luga','100-0555-0102','luga100@example.com',3,'golang'),
(7,'Qiuchi','777-5555-0000','qiuchi777@example.com',3,'freedom'),
(8,'walse','000-11111-22222','walse@example.com',5,'tutorial'),
(9,'Jhons','444-4433-1213','jhons444@example.com',5,'project'),
(10,'Leon','122-1214-5678','leon1221214@example.com',4,'confidence');
INSERT 10

Time: 30ms total (execution 30ms / network 1ms)

此時,通過 CockroachD 控制台,我們可以看到之前的相關操作信心,具體如下所示:

基于上述操作,我们新建了 books 数据库,并在此库中,新建 tblbooks 表,并对其进行用户角色、权限的配置。此时,我们开始着手于 Go 應用程序的編寫,首先,我們需要下載涉及的相關依賴,具體如下所示:

代码语言:javascript复制
import ( 

"database/sql" 
"fmt" 
"net/http" 
"log" 
_ "github.com/lib/pq" 
)

下面,我們針對所依賴的代碼包進行簡要解釋,具體如下:

1、要在 Go 中使用 SQL 数据库和 SQL 等数据库,请使用 database/sql。

2、与 Print() 函数类似的是 fmt 函数。它格式化为 I/O。

3、使用 net/http 包对 HTTP 服务器和客户端进行 API 调用。

4、database/sql 需要用于 Go 的 Postgres 驱动程序包 “github.com/lib/pq” 。也可以通过在代码行开头使用下划线 _ 来加载它而不显示代码中的名称。

引入相關依賴包後,我們開始使用 struct 创建匹配数据库表的字段。通常情況下,使用基於 GoLang 的 struct 來创建与表 tbbook 相同的命名属性或字段。如下所示:

代码语言:javascript复制
type Books struct { 
  Id int 
  Name string 
  Phone string 
  Email string 
  Stars int 
  Category string 
}

在成功导入依赖项,使用 GoLang 的 struct 命令创建匹配字段後。 接下来,让我们進行 CockroachDB 数据库連接操作。在 GoLang 語法結構中,通过设置“db”全局包级别变量来获取包内的全局使用情况。即:

代码语言:javascript复制
var db *sql.DB

並使用 init() 函数進行快速无缝建立数据库连接,其最大好處在於:僅需調用一次。具體如下:

代码语言:javascript复制
func init() { 
  var err error 
  connStr := "postgres://luga:password@localhost:26257/books?sslmode=disable" 
  db, err = sql.Open("postgres", connStr) #訪問連接
  if err != nil { 
    panic(err) 
  } 

  if err = db.Ping(); err != nil { 
    panic(err) 
  } 
  fmt.Println("Connected to the database") 
}

基於上述代碼,使用 db 检查任何引发的错误,以及 func main() 來验证数据库连接是否有效。上述代碼簡要描述了 GET /books 路由所需的所有初始化邏輯,現在,让我们为我们的 Web 应用程序編寫 booksIndex() HTTP 处理程序,即所謂的函數(方法)入口。具體如下所示:

代码语言:javascript复制
func main(){ 
  http.HandleFunc("/books", booksIndex)
  http.ListenAndServe(":9090", nil) 
}

当使用方法 http.HandleFunc() 向它传递第二个参数时,可以只传递函数的名称。一般情況下,需要通过消除括号将函数作为回调函数传递。

1、此處声明 HTTP 请求中的函数名称是 booksIndex。

2、“9090”為分配的監聽端口。

最後,我們來看一下最為核心的內容,具體如下所示:

代码语言:javascript复制
func booksIndex(w http.ResponseWriter, r *http.Request) {

        if r.Method != "GET" {
                http.Error(w, http.StatusText(405), http.StatusMethodNotAllowed)
                return
        }

        rows, err := db.Query("SELECT * FROM tblbooks;")

        if err != nil {
                http.Error(w, http.StatusText(500), 500)
                return
        }
        defer rows.Close()

        bks := make([]*Book, 0)

        for rows.Next() {
                bk := new(Book)
                err := rows.Scan(&bk.Id, &bk.Name, &bk.Phone, &bk.Email, &bk.Stars, &bk.Category)
                if err != nil {
                    http.Error(w, http.StatusText(500), 500)
                    return
                }
                bks = append(bks, bk)
        }

        if err = rows.Err(); err != nil {
                http.Error(w, http.StatusText(500), 500)
                return
        }

        for _, bk := range bks {
                fmt.Fprintf(w, "%d %s %s %s %d %sn", bk.Id, bk.Name, bk.Phone, bk.Email, bk.Stars, bk.Category)
        }
}

我們對如上的代碼進行簡要的解釋,具體:

1、db.Query("SELECT * FROM tblbooks") ,基於此命令操作進行數據庫查詢,以返回相關結果。

2、 rows.Close() ,基於此命令延迟释放资源。

3、制作了一个切片并分配了 restos := make([]Restaurant, 0) 变量。

4、使用 rows.Next() 完成迭代。

5、使用 rows.Scan() 复制当前行的列。

基於上述的各階段描述,最終的代碼實現如下所示:

代码语言:javascript复制
package main

import (
        "database/sql"
        "fmt"
        "net/http"
        "log"
        _ "github.com/lib/pq"
)

type Book struct {
        Id int
        Name string
        Phone string
        Email string
        Stars int
        Category string
}

var db *sql.DB

func init() {
        var err error
        db, err = sql.Open("postgres", "postgres://luga:password@127.0.0.1:26257/books?sslmode=disable")
        if err != nil {
            log.Fatal(err)
        }

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

func main() {
    http.HandleFunc("/books", booksIndex)
    http.ListenAndServe(":9090", nil)
}

func booksIndex(w http.ResponseWriter, r *http.Request) {

        if r.Method != "GET" {
                http.Error(w, http.StatusText(405), http.StatusMethodNotAllowed)
                return
        }

        rows, err := db.Query("SELECT * FROM tblbook")

        if err != nil {
                http.Error(w, http.StatusText(500), 500)
                return
        }
        defer rows.Close()

        bks := make([]*Book, 0)

        for rows.Next() {
                bk := new(Book)
                err := rows.Scan(&bk.Id, &bk.Name, &bk.Phone, &bk.Email, &bk.Stars, &bk.Category)
                if err != nil {
                    http.Error(w, http.StatusText(500), 500)
                    return
                }
                bks = append(bks, bk)
        }

        if err = rows.Err(); err != nil {
                http.Error(w, http.StatusText(500), 500)
                return
        }

        for _, bk := range bks {
                fmt.Fprintf(w, "%d %s %s %s %d %sn", bk.Id, bk.Name, bk.Phone, bk.Email, bk.Stars, bk.Category)
        }
}

此時,我們在文件所在的目錄下執行如下命令:

代码语言:javascript复制
[leonli@192 books ] % go run main.go
[leonli@192 books ] %

其結果如下:

代码语言:javascript复制
   Lucy    130-0000-0000   lucy13000000000@example.net     8  life
   Green   125-0456-0102   green1250456@example.com        1  artity   
   Linda   857-555-0182    linda857@example.com            3  moon
   Wilium  999-555-000     wilium@example.com              5  sunset
   Lily    234-2123-1231   lily1900@example.com            5  work
   Luga    100-0555-0102   luga100@example.com             3  golang
   Qiuchi  777-5555-0000   qiuchi777@example.com           3  freedom
   walse   000-11111-22222 walse@example.com               5  tutorial
   Jhons   444-4433-1213   jhons444@example.com            5  project
   Leon    122-1214-5678   leon1221214@example.com         4  confidence

當然,我們也可以通過 http://localhost:9090/books 進行訪問。基於上述結果,我們的操作到此結束。

至此,一个简单的 Demo 先解析到此为止,希望大家有所收获!

Adiós !

- EOF -

0 人点赞