Cài đặt thư viện client Spanner cho Go và Python
Trước khi viết code, bạn cần cài đặt các thư viện client chính thức của Google Spanner vào môi trường Ubuntu 24.04. Thư viện Go sử dụng module cloud.google.com/go/spanner và Python sử dụng google-cloud-spanner.
Đối với Go, ta sử dụng go get để tải dependency. Đối với Python, ta dùng pip để cài đặt package trong virtual environment để tránh xung đột hệ thống.
Cài đặt cho Go (Golang)
Bạn cần tạo một project Go mới và thêm dependency vào file go.mod. Command này sẽ tự động tạo file nếu chưa có và tải thư viện.
Tại sao: Để đảm bảo ứng dụng Go có thể import và sử dụng các API của Spanner.
Kết quả mong đợi: Thư viện cloud.google.com/go/spanner xuất hiện trong go.mod và được cache trong go/pkg/mod.
mkdir -p ~/spanner-apps/go-client && cd ~/spanner-apps/go-client
go mod init spanner-go-client
go get cloud.google.com/go/spanner
Cài đặt cho Python
Tạo virtual environment và cài đặt thư viện google-cloud-spanner. Bạn cần đảm bảo Python 3.10 trở lên đang chạy trên Ubuntu 24.04.
Tại sao: Isolate dependencies của project và đảm bảo version của thư viện tương thích với Spanner Local Emulator.
Kết quả mong đợi: Package google-cloud-spanner được cài đặt thành công trong venv.
cd ~/spanner-apps
python3 -m venv python-client
source python-client/bin/activate
pip install google-cloud-spanner
Viết mã nguồn kết nối ứng dụng đến Spanner Local
Bước quan trọng nhất là cấu hình kết nối. Khi chạy với Spanner Local (Emulator), ta không dùng credential file (JSON) mà phải chỉ định SPANNER_EMULATOR_HOST trong môi trường hoặc code.
Tại sao: Emulator chạy trên localhost (thường là port 9010), nếu không cấu hình đúng, client sẽ cố gắng kết nối đến dịch vụ Cloud thực tế và bị lỗi authentication.
Kết quả mong đợi: Client có thể tạo connection object thành công mà không cần key file.
Code kết nối cho Go
File: ~/spanner-apps/go-client/main.go. Nội dung dưới đây thiết lập connection string và override endpoint để trỏ về emulator.
package main
import (
"context"
"fmt"
"log"
"os"
"cloud.google.com/go/spanner"
"google.golang.org/api/option"
)
func main() {
ctx := context.Background()
// Cấu hình môi trường cho Emulator
os.Setenv("SPANNER_EMULATOR_HOST", "localhost:9010")
// Instance và Database ID (phải trùng với phần 3)
instanceID := "my-instance"
databaseID := "my-database"
connectionString := fmt.Sprintf("projects/%s/instances/%s/databases/%s", "test-project", instanceID, databaseID)
// Khởi tạo client với option Emulator
client, err := spanner.NewClient(ctx, connectionString, option.WithoutAuthentication())
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
fmt.Println("Go client connected to Spanner Local successfully!")
}
Code kết nối cho Python
File: ~/spanner-apps/python-client/main.py. Tương tự Go, ta set environment variable và sử dụng client manager.
import os
from google.cloud import spanner
def main():
# Cấu hình môi trường cho Emulator
os.environ["SPANNER_EMULATOR_HOST"] = "localhost:9010"
instance_id = "my-instance"
database_id = "my-database"
# Khởi tạo client
client = spanner.Client(project="test-project")
instance = client.instance(instance_id)
database = instance.database(database_id)
print("Python client connected to Spanner Local successfully!")
return database
if __name__ == "__main__":
main()
Verify kết quả kết nối
Chạy code để đảm bảo không có lỗi Connection refused hoặc Permission denied.
cd ~/spanner-apps/go-client && go run main.go
cd ~/spanner-apps/python-client && python main.py
Kết quả mong đợi: Console in ra thông điệp "connected successfully" cho cả hai ngôn ngữ.
Thực hiện các thao tác CRUD (Create, Read, Update, Delete)
Sau khi kết nối, ta cần xây dựng logic CRUD. Spanner yêu cầu tạo bảng trước khi chèn dữ liệu. Giả sử bảng Singers đã tồn tại (từ Phần 3) với schema: id INT64, FirstName STRING(100), LastName STRING(100), SingerInfo BYTES(MAX), LastUpdate TIMESTAMP.
Thao tác Create (Insert) trong Go
Sử dụng Mutation để chèn dữ liệu. Trong Spanner, việc chèn dữ liệu phải nằm trong transaction hoặc batch mutation để đảm bảo tính nguyên tử.
Tại sao: Spanner là DB phân tán, mọi ghi dữ liệu đều phải qua cơ chế transaction để đảm bảo ACID.
// Tiếp tục trong main.go, thêm hàm insertSinger
func insertSinger(client *spanner.Client, id int64, firstName, lastName string) error {
ctx := context.Background()
// Tạo mutation
m := spanner.Mutation{
Operation: spanner.Insert,
Table: "Singers",
Columns: []string{"SingerId", "FirstName", "LastName", "SingerInfo", "LastUpdate"},
Values: []interface{}{id, firstName, lastName, []byte{}, spanner.CommitTimestamp()},
}
// Thực hiện mutation
_, err := client.Apply(ctx, []*spanner.Mutation{&m})
return err
}
Thao tác Create (Insert) trong Python
Python sử dụng BatchTransaction hoặc Commit để thực hiện insert.
# Tiếp tục trong main.py, thêm hàm insert_singer
from google.cloud.spanner_v1 import TransactionOptions, CommitTimestamp
def insert_singer(database, singer_id, first_name, last_name):
mutation = spanner.Mutation(
table="Singers",
columns=["SingerId", "FirstName", "LastName", "SingerInfo", "LastUpdate"],
values=[singer_id, first_name, last_name, b"", CommitTimestamp()]
)
# Thực hiện batch
database.apply([mutation])
print(f"Inserted singer with ID: {singer_id}")
Thao tác Read (Select) trong Go
Sử dụng Single để đọc một dòng theo Primary Key, hoặc Query để quét toàn bộ bảng.
func readSinger(client *spanner.Client, id int64) error {
ctx := context.Background()
// Đọc theo Primary Key (Single)
row, err := client.Single().ReadRow(ctx, "Singers", spanner.Key{id}, []string{"SingerId", "FirstName", "LastName"})
if err != nil {
return err
}
var singerId int64
var firstName, lastName string
err = row.Columns(&singerId, &firstName, &lastName)
if err != nil {
return err
}
fmt.Printf("Found Singer: ID=%d, Name=%s %s\n", singerId, firstName, lastName)
return nil
}
Thao tác Update và Delete trong Python
Update dùng Update mutation (chỉ cần key và các cột cần sửa), Delete dùng Delete mutation.
def update_singer(database, singer_id, new_first_name):
mutation = spanner.Mutation(
table="Singers",
columns=["FirstName"],
values=[new_first_name],
key=[singer_id]
)
database.apply([mutation])
print(f"Updated singer {singer_id} to {new_first_name}")
def delete_singer(database, singer_id):
mutation = spanner.Mutation(
table="Singers",
columns=[],
values=[],
key=[singer_id],
operation=spanner.MutationOperation.DELETE
)
database.apply([mutation])
print(f"Deleted singer {singer_id}")
Verify kết quả CRUD
Chạy script test kết hợp Insert -> Read -> Update -> Read -> Delete.
# Tạo file test_crud.go trong thư mục go-client
# Tạo file test_crud.py trong thư mục python-client
# Chạy lệnh:
cd ~/spanner-apps/go-client && go run test_crud.go
cd ~/spanner-apps/python-client && python test_crud.py
Kết quả mong đợi: Output hiển thị đúng thông tin của Singer mới tạo, sau đó là Singer đã cập nhật, và cuối cùng là xác nhận xóa.
Xử lý transaction và đảm bảo tính nhất quán ACID
Spanner hỗ trợ transactions mạnh mẽ (Serializable Isolation). Khi thực hiện nhiều thao tác cùng lúc (ví dụ: chuyển tiền, update inventory), ta phải đóng gói chúng vào một Transaction.
Transaction trong Go
Sử dụng client.RunInTransaction. Hàm callback sẽ thực thi các mutation. Nếu có lỗi xảy ra, transaction tự động rollback.
Tại sao: Đảm bảo tính nguyên tử (Atomicity). Nếu bước 2 lỗi, bước 1 cũng bị hủy, giữ dữ liệu không bị vỡ.
func runTransaction(client *spanner.Client) error {
ctx := context.Background()
_, err := client.RunInTransaction(ctx, func(ctx context.Context, txn *spanner.ReadOnlyTransaction) error {
// Trong thực tế, RunInTransaction dùng cho Read-Write transaction
// Dùng func(ctx context.Context, txn *spanner.ReadWriteTransaction) error
// Ví dụ: Cập nhật 2 dòng trong cùng 1 transaction
m1 := spanner.Mutation{
Operation: spanner.Update,
Table: "Singers",
Columns: []string{"FirstName"},
Values: []interface{}{"UpdatedName1"},
Key: spanner.Key{int64(1)},
}
m2 := spanner.Mutation{
Operation: spanner.Update,
Table: "Singers",
Columns: []string{"FirstName"},
Values: []interface{}{"UpdatedName2"},
Key: spanner.Key{int64(2)},
}
return txn.BufferWrite([]*spanner.Mutation{&m1, &m2})
})
return err
}
Transaction trong Python
Sử dụng database.commit hoặc database.run_in_transaction.
def run_transaction(database):
def transaction(txn):
mutation1 = spanner.Mutation(
table="Singers",
columns=["FirstName"],
values=["UpdatedName1"],
key=[1]
)
mutation2 = spanner.Mutation(
table="Singers",
columns=["FirstName"],
values=["UpdatedName2"],
key=[2]
)
txn.buffer_write([mutation1, mutation2])
print("Transaction buffered, ready to commit")
database.run_in_transaction(transaction)
print("Transaction committed successfully")
Verify tính nhất quán ACID
Thực hiện một transaction giả lập có lỗi (ví dụ: cố cập nhật ID không tồn tại) để xem liệu các mutation trước đó có bị rollback không.
# Thêm logic vào test_crud.go và test_crud.py
# Tạo transaction với mutation hợp lệ + mutation sai key
# Chạy lại và kiểm tra dữ liệu trong DB
Kết quả mong đợi: Nếu mutation thứ 2 lỗi, dữ liệu từ mutation thứ 1 không được ghi vào DB (rollback hoàn toàn).
Quản lý connection pool trong ứng dụng đa luồng
Spanner Client (cả Go và Python) đã tích hợp sẵn connection pooling. Tuy nhiên, trong môi trường sản xuất hoặc load cao, ta cần cấu hình đúng để tránh tạo quá nhiều connection hoặc cạn kiệt resource.
Quản lý trong Go
Go Client giữ một pool connection nội bộ. Khi gọi client.Apply hoặc client.Read từ nhiều goroutine, client tự động phân phối request. Ta chỉ cần tạo một instance client cho toàn ứng dụng (singleton) và đóng nó khi ứng dụng dừng.
Tại sao: Việc tạo mới client cho mỗi request sẽ gây overhead lớn và làm quá tải server Spanner.
var globalClient *spanner.Client
func initClient() {
// Chỉ gọi 1 lần khi start app
client, err := spanner.NewClient(context.Background(), connectionString, option.WithoutAuthentication())
if err != nil {
log.Fatal(err)
}
globalClient = client
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Sử dụng globalClient từ nhiều goroutine (go routine)
go func(id int64) {
err := readSinger(globalClient, id)
if err != nil {
fmt.Println("Error in goroutine", id, err)
}
}(r.Context().Value("id"))
w.Write([]byte("Processing..."))
}
Quản lý trong Python
Python Client cũng sử dụng pool. Khi chạy đa luồng (threading), ta chia sẻ một instance Database. Lưu ý rằng Python GIL có thể ảnh hưởng hiệu năng, nên ưu tiên multiprocessing nếu cần tính toán nặng, nhưng I/O của Spanner vẫn có thể chia sẻ connection.
import threading
from concurrent.futures import ThreadPoolExecutor
# Global database instance
db_instance = None
def init_db():
global db_instance
# Code khởi tạo database như phần trên
# db_instance = client.instance(...).database(...)
def worker(singer_id):
# Đọc từ nhiều thread
read_singer(db_instance, singer_id)
def run_concurrent():
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(worker, i) for i in range(1, 11)]
for future in futures:
future.result()
Verify hiệu năng và Pool
Chạy script tạo 50-100 thread/goroutine cùng lúc thực hiện Read operation.
# Chạy script stress test
cd ~/spanner-apps/go-client && go run stress_test.go
cd ~/spanner-apps/python-client && python stress_test.py
Kết quả mong đợi: Tất cả request hoàn thành thành công, không có lỗi Too many connections hoặc timeout do hết connection pool. Log hiển thị thời gian xử lý trung bình ổn định.
Điều hướng series:
Mục lục: Series: Triển khai Database NewSQL với Google Spanner và Ubuntu 24.04
« Phần 3: Cấu hình kết nối và quản lý cơ sở dữ liệu Spanner
Phần 5: Tối ưu hiệu năng và mở rộng quy mô Spanner trên Ubuntu »