Làm dev đau lưng

Tự trap bằng jsonb trong PostgreSQL

Bài này để nói về lỗi ngớ ngẩn của mình khi làm việc với jsonb trong PostgreSQL, không phải một bài giới thiệu về jsonb. Mình xin phép bắt đầu luôn nha.

jsonb là gì?

Hiểu ngắn gọn là lưu trữ json document trong PostgreSQL, còn chi tiết ở đây nhé

Thao tác với jsonb bằng Golang

  1. Tạo bảng với cột jsonb:
CREATE TABLE example_tbl (
  id SERIAL PRIMARY KEY,
  data jsonb
);
  1. Đẩy dữ liệu vào
data := map[string]any{
    "name":    "Qui Vo",
    "age":     24,
    "country": "Vietnam",
}
query := "INSERT INTO example_tbl (data) VALUES ($1)"
_, err = conn.Exec(ctx, query, data)
if err != nil {
    panic(err)
}
  1. Kiểm tra dữ liệu vừa insert
  1. Oke ngon! Giờ thử thêm một field mới vào jsonb nào
// Retrieve
query := "SELECT id, data FROM example_tbl WHERE id = $1"
var eTbl ExampleTbl
err = pgxscan.Get(ctx, conn, &eTbl, query, 1)
if err != nil {
    panic(err)
}

// Update
eTbl.Data["github"] = "vkhanhqui"
query = "UPDATE example_tbl SET data = $1 WHERE id = $2"
_, err = conn.Exec(ctx, query, eTbl.Data, eTbl.ID)
if err != nil {
    panic(err)
}

Field mới đã vô

Question: Điều gì xảy ra nếu nhiều field update đồng thời? Liệu Atomicity có được đảm bảo?

Vấn đề mình gặp

Nếu thực tế các requests mà chạy được như trên thì đỡ quá. Đoạn code update chắc chắn sẽ xảy ra race condition khi chạy đồng thời. Ví dụ vừa update các social urls, vừa đổi name trong cùng một field jsonb.

Giả lập lại quá trình update:

  1. Retrieve a single record by id
  2. Update a single key value of the jsonb field by record id

Và quá trình này đồng thời cũng chạy song song:

updates := map[string]string{
    "github":   "new vkhanhqui",
    "website":  "new khanhqui.com",
    "linkedin": "new khanhqui",
    "name":     "new Qui Vo",
}

// Run in parallel
var wg sync.WaitGroup
for k, v := range updates {
    wg.Add(1)
    go func() {
        // Retrieve
        eTbl, err := retrieve(ctx, conn, 1)
        if err != nil {
            panic(err)
        }

        // Update
        eTbl.Data[k] = v
        err = update(ctx, conn, 1, eTbl.Data)
        if err != nil {
            panic(err)
        }

        defer wg.Done()
    }()
}
wg.Wait()

Vì bị dính race condition nên chỉ có namegithub được update

Cách xử lý

Đổi qua update từng field trong jsonb thay vì cả cụm

// Run in parallel
var wg sync.WaitGroup
for k, v := range updates {
    wg.Add(1)
    go func() {
        query := fmt.Sprintf(`
        UPDATE example_tbl 
        SET data = jsonb_set(
            COALESCE(data, '{}'), 
            '{%s}', '"%s"'::jsonb
        )
        WHERE id = $1`, k, v)
        _, err = conn.Exec(ctx, query, 1)
        if err != nil {
            panic(err)
        }

        defer wg.Done()
    }()
}
wg.Wait()

Atomicity đã được đảm bảo:

Toàn bộ code của bài này được lưu ở đây. Như thường lệ, hy vọng mọi người không gặp phải sai lầm của mình.