Yêu cầu Thứ Hai, 2:41 CH 55 0 1
  • 55 0 1
0

Cho mình hỏi về việc thay đổi data trong hệ thống microservice

Chia sẻ
  • 55 0 1

Mình cho ví dụ cụ thể như hệ thống ngân hàng. Người dùng ở ATM rút tiền, hệ thống backend yêu cầu phải đáp ứng nhanh, nên sẽ chạy theo mô hình client-server asynchronous

  • Người dùng ra lệnh rút tiền, lệnh được gửi về 1 service trong backend
  • Service đó sẽ kiểm tra tính hợp lệ, nếu hợp lệ thì service gửi 1 message cho service khác xử lý tiếp, cụ thể là yêu cầu update DB
  • Và service đó return kết quả thành công về client (ATM) và không cần chờ kết quả của service kia
  • Như vậy ngay lúc đó, data ở DB hoàn toàn chưa update
  • Nếu sau đó người dùng query lại số dư, thì không thể query trực tiếp từ DB ra được (do chưa update)

Có khả năng mình cần cache cho 1 user đã rút tiền ở trên hay không ? Nghĩa là service đầu tiên sẽ add/update vô cache. Sau đó nếu người dùng query lại số dư, thì sẽ đọc từ cache thay vì đọc từ DB ra ?

1 CÂU TRẢ LỜI


Đã trả lời Thứ Ba, 9:05 SA
0

Đối với các hệ thống ngân hàng, bạn không nên áp dụng cache và Async việc update DB. Bởi vì các giao dịch ngân hàng bắt buộc phải strong consistency. Nếu lỡ như bạn đã gửi lại thông tin cho user là chuyển tiền thành công nhưng việc update DB lỗi hoặc lỗi máy ATM thì thông tin user nhận là sai. Ngoài ra còn gặp những vấn đề liên quan tới race condition nữa. Lưu trên cache cũng gặp rủi ro nếu như redis cache sập thì dữ liệu sẽ mất hết. Cách giải quyết có thể như sau:

Bước 1: Giữ chỗ (Hold/Freeze/Authorize) - Đồng bộ (Synchronous)

  • Ví dụ khi bạn bấm rút 2 triệu: Service tiếp nhận lệnh và gọi đồng bộ xuống DB.
  • Hệ thống sẽ thực hiện một lệnh Lock số dư của User đó (ví dụ: SELECT balance FROM accounts WHERE id = x FOR UPDATE).
  • Kiểm tra số dư. Nếu đủ, hệ thống không trừ tiền ngay, mà chuyển 2 triệu đó vào trạng thái "Hold" (Tạm giữ/Phong tỏa).
  • Lúc này, số dư khả dụng (Available Balance) giảm đi 2 triệu, nhưng số dư thực tế (Current Balance) vẫn giữ nguyên. Thao tác này diễn ra bằng một Transaction trong DB cực kỳ nhanh (vài mili-giây).

Bước 2: Ra lệnh cho ATM (Synchronous) Sau khi đã Hold tiền thành công trong DB, backend trả kết quả Đồng bộ về cho ATM: "Tôi đã giữ tiền an toàn, anh hãy nhả tiền mặt cho khách đi".

Bước 3: Đẩy tiền và Hoàn tất (Settle) hoặc Hủy (Rollback)

  • Kịch bản 1 (Thành công): ATM nhả tiền thành công -> ATM gửi tín hiệu Confirm về Backend -> Backend thực hiện Deduct (Trừ hẳn) 2 triệu đang bị Hold trong DB (Thao tác này có thể chạy Async hoặc Sync tùy kiến trúc kiến tạo).
  • Kịch bản 2 (Thất bại/Kẹt tiền): ATM không nhả được tiền -> Gửi tín hiệu Cancel về Backend -> Backend thực hiện Unhold (Giải phóng) 2 triệu đó lại cho khách hàng.
Chia sẻ
Avatar Lộc @ninja
Thứ Ba, 11:28 SA

Cho mình hỏi thêm, câu lệnh SELECT ... WHERE ... FOR UPDATE nó chỉ tạo exclusive lock cho vài row. Nếu có transaction khác thực thi đồng thời, cũng dùng câu lệnh trên nhưng ở các row khác, thì nó vẫn chạy song song bình thường phải không ?

Thứ Tư, 10:21 SA

SELECT ... WHERE ... FOR UPDATE chỉ tạo exclusive lock cho những row theo điều kiện WHERE. Nếu có transaction khác thực thi đồng thời những row khác thì nó vẫn chạy song song bình thường nhé. Mà dùng cách này thì cẩn thận là ở điều kiện WHERE, cột cần WHERE phải có index. Nếu không có index thì DB sẽ phải quét toàn bộ bảng (Full Table Scan). Dẫn tới database sẽ khóa toàn bộ các dòng mà nó quét qua và khóa sạch cả bảng.

Avatar Lộc @ninja
Thứ Tư, 2:47 CH

@ngoctuyen cho mình hỏi thêm về câu trả lời của bạn, bước 3. Ở kịch bản 1, nếu dùng async, thì khi đó data chưa được update kịp. Và nếu vài phút sau người dùng query balance, thì sẽ đọc data ở đâu để lấy kết quả chính xác (do lúc nãy đã rút 2tr nhưng DB chính vẫn chưa được update) ?

Thứ Năm, 11:28 SA

@ninja Cảm ơn câu hỏi của bạn, có lẽ chúng ta cần thay đổi kiến trúc, không nên update trực tiếp balance. Trong các hệ thống ngân hàng thì chúng ta không update trực tiếp cột balance, vì chúng ta cần lưu trữ lại lịch sử của những lần rút tiền. Cách giải quyết là chúng ta sẽ có một bảng là Holding chỉ để lưu giá trị số dư cần trừ của tài khoản, bảng này sẽ có cột status gồm 2 giá trị là PENDING và COMPLETED Ví dụ tài khoản đang có current_balance = 10 triệu Ở bước 1 khi ATM yêu cầu rút 2 triệu, nó phải được thực hiện đồng bộ, nó sẽ tạo một record trong bảng Holding với số tiền holding là 2 triệu và status là PENDING. Khi người dùng check số dư, backend sẽ tính available_balance = current_balance - holding = 8 triệu. Ở bước 3 sau khi ATM yêu cầu update DB async. Mặc dù DB chưa được update nhưng giá trị available_balance = current_balance - holding = 8 triệu, giá trị trả về cho người dùng luôn đúng. Việc update DB bao gồm update available_balance = 8 triệu, status của holding là COMPLETED 2 tác vụ update phải được đặt trong 1 transaction. Điều này đảm bảo người dùng luôn nhận được số dư chính xác. Tất nhiên với cách tiếp cận này, chúng ta sẽ gặp một vấn đề khác là Dual Write, giải quyết vấn đề này là một bài toán khác. Nếu có gì không hợp lý, bạn có thể hỏi mình, mình nghĩ sẽ còn nhiều vấn đề với cách tiếp cận này. 🤔

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí