Mysql, Redis và Mongo đều là những cửa hàng rất nổi tiếng và mỗi cửa hàng đều có những ưu điểm riêng. Trong các ứng dụng thực tế, người ta thường sử dụng nhiều cửa hàng cùng một lúc và việc đảm bảo tính nhất quán của dữ liệu trên nhiều cửa hàng trở thành một yêu cầu.
Bài viết này đưa ra một ví dụ về việc triển khai một giao dịch phân tán trên nhiều công cụ cửa hàng, Mysql, Redis và Mongo. Ví dụ này dựa trên Khung giao dịch phân tán https://github.com/dtm-labs/dtm và hy vọng sẽ giúp giải quyết vấn đề của bạn về tính nhất quán dữ liệu trên các dịch vụ nhỏ.
Khả năng kết hợp linh hoạt nhiều công cụ lưu trữ để tạo thành một giao dịch phân tán lần đầu tiên được đề xuất bởi DTM và không có khung giao dịch phân tán nào khác có khả năng như thế này.
Tình huống sự cố
Hãy xem xét kịch bản vấn đề trước. Giả sử bây giờ người dùng tham gia một chương trình khuyến mãi:có số dư, nạp tiền vào hóa đơn điện thoại, chương trình khuyến mãi sẽ tặng điểm trung tâm. Số dư được lưu trữ trong Mysql, hóa đơn được lưu trữ trong Redis, các điểm mua sắm được lưu trữ trong Mongo. Vì thời gian khuyến mãi có hạn nên có khả năng không tham gia được, vì vậy cần hỗ trợ khôi phục.
Đối với trường hợp sự cố trên, bạn có thể sử dụng giao dịch Saga của DTM và chúng tôi sẽ giải thích giải pháp chi tiết bên dưới.
Chuẩn bị dữ liệu
Bước đầu tiên là chuẩn bị dữ liệu. Để giúp người dùng nhanh chóng bắt đầu với các ví dụ này dễ dàng hơn, chúng tôi đã chuẩn bị dữ liệu liên quan tại en.dtm.pub, bao gồm Mysql, Redis và Mongo, đồng thời có thể tìm thấy tên người dùng và mật khẩu kết nối cụ thể tại https:// github.com/dtm-labs/dtm-examples.
Nếu bạn muốn tự chuẩn bị môi trường dữ liệu cục bộ, bạn có thể sử dụng https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml để khởi động Mysql, Redis, Mongo; và sau đó thực thi các tập lệnh trong https://github.com/dtm-labs/dtm/tree/main/sqls để chuẩn bị dữ liệu cho ví dụ này, trong đó
busi.*
là dữ liệu kinh doanh vàbarrier.*
là bảng phụ được DTM sử dụng
Viết Mã Doanh nghiệp
Hãy bắt đầu với mã doanh nghiệp cho Mysql quen thuộc nhất.
Đoạn mã sau đây nằm trong Golang. Các ngôn ngữ khác như C #, PHP, Java có thể được tìm thấy tại đây:DTM SDKs
func SagaAdjustBalance(db dtmcli.DB, uid int, amount int) error {
_, err := dtmimp.DBExec(db, "update dtm_busi.user_account set balance = balance + ? where user_id = ?" , amount, uid)
return err
}
Mã này chủ yếu thực hiện việc điều chỉnh số dư của người dùng trong cơ sở dữ liệu. Trong ví dụ của chúng tôi, phần mã này không chỉ được sử dụng cho hoạt động chuyển tiếp của Saga mà còn cho hoạt động bồi thường, trong đó chỉ cần chuyển một số tiền âm để được bồi thường.
Đối với Redis và Mongo, mã doanh nghiệp được xử lý tương tự, chỉ cần tăng hoặc giảm số dư tương ứng.
Cách đảm bảo Tính lý tưởng
Đối với mô hình giao dịch Saga, khi chúng tôi gặp lỗi tạm thời trong dịch vụ giao dịch phụ, hoạt động không thành công sẽ được thử lại. Lỗi này có thể xảy ra trước hoặc sau khi giao dịch phụ cam kết, vì vậy hoạt động giao dịch phụ cần phải được thực hiện một cách lý tưởng.
DTM cung cấp các bảng trợ giúp và các chức năng trợ giúp để giúp người dùng đạt được hiệu quả lý tưởng một cách nhanh chóng. Đối với Mysql, nó sẽ tạo một bảng bổ trợ barrier
trong cơ sở dữ liệu kinh doanh, khi người dùng bắt đầu giao dịch để điều chỉnh số dư, trước tiên nó sẽ chèn Gid
trong barrier
bàn. Nếu có một hàng trùng lặp, thì việc chèn sẽ không thành công và sau đó bỏ qua điều chỉnh số dư để đảm bảo giá trị trung tâm. Mã sử dụng hàm trợ giúp như sau:
app.POST(BusiAPI+"/SagaBTransIn", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
return SagaAdjustBalance(tx, TransInUID, reqFrom(c).Amount, reqFrom(c).TransInResult)
})
}))
Mongo xử lý Idempotency theo cách tương tự như Mysql, vì vậy tôi sẽ không đi vào chi tiết một lần nữa.
Redis xử lý Idempotency khác với Mysql, chủ yếu là do sự khác biệt trong nguyên tắc giao dịch. Các giao dịch của Redis chủ yếu được đảm bảo bằng cách thực thi nguyên tử của Lua. chức năng trợ giúp DTM sẽ điều chỉnh cân bằng thông qua tập lệnh Lua. Trước khi điều chỉnh số dư, nó sẽ truy vấn Gid
trong Redis. Nếu Gid
tồn tại, nó sẽ bỏ qua việc điều chỉnh số dư; nếu không, nó sẽ ghi lại Gid
và thực hiện điều chỉnh số dư. Mã được sử dụng cho chức năng trợ giúp như sau:
app.POST(BusiAPI+"/SagaRedisTransOut", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), -reqFrom(c).Amount, 7*86400)
}))
Cách thực hiện bồi thường
Đối với Saga, chúng tôi cũng cần giải quyết hoạt động bồi thường, nhưng bồi thường không chỉ đơn giản là điều chỉnh ngược lại và có rất nhiều cạm bẫy cần phải lưu ý.
Một mặt, việc bồi thường cần phải tính đến tính ưu việt, bởi vì lỗi và các lần thử lại được mô tả trong tiểu mục trước cũng tồn tại trong phần bù. Mặt khác, việc bồi thường cũng cần phải tính đến "bồi thường vô hiệu", vì hoạt động chuyển tiếp của Saga có thể trả về lỗi, có thể đã xảy ra trước hoặc sau khi điều chỉnh dữ liệu. Đối với những sai hỏng mà việc điều chỉnh đã được cam kết, chúng tôi cần thực hiện điều chỉnh ngược lại; nhưng đối với những lỗi mà việc điều chỉnh chưa được cam kết, chúng ta cần bỏ qua thao tác ngược lại.
Trong bảng trợ giúp và các hàm trợ giúp do DTM cung cấp, một mặt, nó sẽ xác định xem phần bù có phải là phần bù rỗng hay không dựa trên Gid được chèn bởi thao tác chuyển tiếp và mặt khác, nó sẽ chèn lại Gid + 'bù' để xác định xem phần bù có phải là một hoạt động trùng lặp hay không. Nếu có hoạt động bù bình thường thì sẽ thực hiện điều chỉnh dữ liệu về nghiệp vụ; nếu có khoản bồi thường vô hiệu hoặc khoản bồi thường trùng lặp, nó sẽ bỏ qua việc điều chỉnh doanh nghiệp.
Mã Mysql như sau.
app.POST(BusiAPI+"/SagaBTransInCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
return SagaAdjustBalance(tx, TransInUID, -reqFrom(c).Amount, "")
})
}))
Mã cho Redis như sau.
app.POST(BusiAPI+"/SagaRedisTransOutCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), reqFrom(c).Amount, 7*86400)
}))
Mã dịch vụ bồi thường gần như giống với mã trước đó của hoạt động chuyển tiếp, ngoại trừ số tiền được nhân với -1. Chức năng trợ giúp DTM tự động xử lý tính thiếu hụt và bù null đúng cách.
Các trường hợp ngoại lệ khác
Khi viết các phép toán chuyển tiếp và các phép toán bồi thường, thực tế có một ngoại lệ khác được gọi là "Đình chỉ". Giao dịch toàn cầu sẽ quay trở lại khi hết thời gian chờ hoặc quá trình thử lại đạt đến giới hạn đã định cấu hình. Trường hợp bình thường là hoạt động kỳ hạn được thực hiện trước khi thực hiện bù, nhưng trong trường hợp tạm dừng quy trình, khoản bồi thường có thể được thực hiện trước hoạt động kỳ hạn. Vì vậy, hoạt động chuyển tiếp cũng cần xác định xem phần bù đã được thực hiện hay chưa và trong trường hợp đã thực hiện, việc điều chỉnh dữ liệu cũng cần được bỏ qua.
Đối với người dùng DTM, những trường hợp ngoại lệ này đã được xử lý khéo léo và đúng cách và bạn, với tư cách là người dùng, chỉ cần tuân theo MustBarrierFromGin(c).Call
cuộc gọi được mô tả ở trên và không cần quan tâm đến chúng cả. Nguyên tắc để DTM xử lý các trường hợp ngoại lệ này được mô tả chi tiết tại đây:Các trường hợp ngoại lệ và các rào cản giao dịch phụ
Bắt đầu giao dịch phân tán
Sau khi viết các dịch vụ giao dịch phụ riêng lẻ, các đoạn mã sau sẽ bắt đầu một giao dịch toàn cầu của Saga.
saga := dtmcli.NewSaga(dtmutil.DefaultHTTPServer, dtmcli.MustGenGid(dtmutil.DefaultHTTPServer)).
Add(busi.Busi+"/SagaBTransOut", busi.Busi+"/SagaBTransOutCom", &busi.TransReq{Amount: 50}).
Add(busi.Busi+"/SagaMongoTransIn", busi.Busi+"/SagaMongoTransInCom", &busi.TransReq{Amount: 30}).
Add(busi.Busi+"/SagaRedisTransIn", busi.Busi+"/SagaRedisTransOutIn", &busi.TransReq{Amount: 20})
err := saga.Submit()
Trong phần này của mã, một giao dịch toàn cầu của Saga được tạo bao gồm 3 giao dịch phụ.
- Chuyển 50 từ Mysql
- Chuyển khoản 30 đến Mongo
- Chuyển 20 đến Redis
Trong suốt giao dịch, nếu tất cả các giao dịch phụ hoàn tất thành công, thì giao dịch toàn cầu sẽ thành công; nếu một trong các giao dịch phụ trả lại lỗi kinh doanh, thì giao dịch toàn cầu sẽ quay trở lại.
Chạy
Nếu bạn muốn chạy một ví dụ hoàn chỉnh ở trên, các bước như sau.
- Chạy DTM
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
- Chạy một ví dụ thành công
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
- Chạy một ví dụ không thành công
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback
Bạn có thể sửa đổi ví dụ để mô phỏng các lỗi tạm thời khác nhau, các tình huống đền bù rỗng và nhiều trường hợp ngoại lệ khác trong đó dữ liệu nhất quán khi toàn bộ giao dịch toàn cầu kết thúc.
Tóm tắt
Bài viết này đưa ra một ví dụ về giao dịch phân tán trên Mysql, Redis và Mongo. Nó mô tả chi tiết các vấn đề cần được xử lý và các giải pháp.
Các nguyên tắc trong bài viết này phù hợp với tất cả các công cụ lưu trữ hỗ trợ giao dịch ACID và bạn có thể nhanh chóng mở rộng nó cho các công cụ khác như TiKV.
Chào mừng bạn đến thăm github.com/dtm-labs/dtm. Đây là một dự án chuyên dụng để làm cho các giao dịch phân tán trong microservices trở nên dễ dàng hơn. Nó hỗ trợ nhiều ngôn ngữ và nhiều mẫu như tin nhắn 2 pha, Saga, Tcc và Xa.