DB Transaction
White

kiennt viết ngày 16/05/2018

Trong tuần vừa rồi, mình có đọc chương 7 cuốn sách Designing data-intensive applications. Bài viết này nhằm mục đích giúp mình tổng hợp lại những kiến thức đã học được về chương này.

1. Các thuộc tính của Transaction

1.1. Atomic

Nếu một transaction bao gồm nhiều hoạt động khác nhau để thay đổi dữ liệu, thì các hoạt động này sẽ phải hoặc là (1) tất cả đều được thực hiện thành công, hoặc (2) không có hoạt động nào được thực hiện thành công.

Ví dụ nếu như transaction thực hiện 2 hoạt động viết vào DB. Nếu sau khi hoạt động số 1 thành công, DB bị crash, thì transaction sẽ bị coi là thất bại. DB sẽ rollback về trạng thái trước khi hoạt động số 1 thành công.

1.2. Consistency

Consistency có nghĩa là trạng thái của DB trước và sau transaction luôn thỏa mãn một số bất biến về dữ liệu. Bất biến về dữ liệu có thể là các khóa ngoại, hoặc các primary keys, hoặc các constraint, hoặc là các bất biến do tầng ứng dụng yêu cầu.
Các ứng dụng dựa vào các đặc tính khác của DB (như Atomic, Isolation) để đảm bảo tính Consistency của dữ liệu.

Tầng ứng dụng có thể yêu cầu một số bất biến như sau: trong một ứng dụng về notification, nếu chúng ta sử dụng denormalize để add thêm 1 trường unseen_notification_count cho mỗi user, khi user có thểm một notification chưa được đọc, thì trường unseen_notification_count sẽ được tăng lên 1. Ứng dụng notification này sẽ yêu cầu bất biến là: giá trị của unseen_notification_count của mỗi user phải bằng với số lượng hàng trong bàng Notification mà có user_id = id của user và có cột is_seen = false

1.3. Isolation

Cùng một lúc, DB có thể được truy cập bởi nhiều clients. Nếu các clients cùng truy xuất vào một phần dữ liệu, thì sẽ nảy sinh các vấn đề liên quan đến concurency. Ví dụ: trong DB có một biến đếm, có 2 clients cùng muốn tăng biến đếm này lên một. Giả sử giá trị ban đầu của biến đếm là 10, sau khi 2 transactions thực hiện xong, chúng ta mong muốn biến đếm phải nhận giá trị là 12.

Isolation nghĩa là các transaction là được phân tách với nhau. Có nhiều mức độ về việc phân tách (isolate) của dữ liệu. Chúng ta sẽ cùng xem xét một số mức độ ở phần tiếp theo

1.4. Durability

Durability đảm bảo rằng một khi transaction đã được commit thành công, thì dữ liệu được thay đổi bởi transaction này sẽ được lưu trữ an toàn.

Đối với trường hợp của DB trên một máy, điều này có nghĩa là dữ liệu được lưu trữ vào ổ cứng (HDD, hoặc SSD), nó cũng bao gồm việc dữ liệu được lưu vào một WAL, hoặc một cấu trúc dữ liệu tương tự (BTree)

Đối với trường hợp của hệ thống phân tán, durability có nghĩa là dữ liệu sẽ được lưu trữ thành công trên một hoặc một vài máy (replicate)

2. Các mức độ về Isolation

2.1. Read Committed

Read Committed đảm bảo DB sẽ không có 2 hiện tượng Dirty Read và Dirty Write

2.1.1. Dirty Read

Dirty Read là hiện tượng mà transaction đọc thấy dữ liệu chưa commit từ một transaction khác.

Dirty Read sẽ dẫn tới 2 hệ quả:

  • Nếu txn1 cập nhật nhiều bản ghi, txn2 thực hiện cùng sẽ nhìn thấy một số cập nhật của txn1 , điều này sẽ khiến users băn khoăn. Ví dụ, Khi có một notification mới tới, hệ thống phải làm 2 việc:
    1. tạo mới một notification, cập nhật trường is_read trong notification là false
    2. tăng giá trị của trường notification_count trong bảng User

Vì Dirty Read, txn2 sẽ thấy có thêm một notification mới, nhưng notification_count vẫn là 0

figure-1

  • Nếu txn2 đọc dữ liệu từ txn1 nhưng txn1 bị rollback, txn2 sẽ đọc thấy những dữ liệu chưa được rollback của txn1. Ví dụ khi thực hiện việc chuyển tiền từ user A sang user B, txn1 làm 2 việc:
    1. giảm balance của user A
    2. tăng balance của user B

Nếu txn2 đọc dữ liệu, sẽ thấy balance của user B được tăng lên, tuy nhiên do txn1 bị rollback, nên thực chất balance của user B là chưa thay đổi

figure-2

2.1.2. Dirty Write

Khi nhiều transaction cùng cập nhật vào một dữ liệu, thông thường transaction thực hiện sau sẽ ghi đè lên dữ liệu của transaction thực hiện trước. Dirty write là hiện tượng các transaction bao gồm việc cập nhật lên nhiều (> 1) bản ghi, và transaction sau có chứa bản ghi cũng bị ghi đè bời transaction trước.

VD: trong hệ thống bán hàng online, một sản phẩm khi mua sẽ bao gồm 2 hành động:

  • cập nhật trường buyer_id trong bảng listing cho sản phẩm đó
  • cập nhật trường buyer_id trong bảng invoice cho sản phẩm đó

Nếu 2 user A và B cùng thực hiện việc mua hàng thông qua 2 transaction txn1txn2. txn1 thực hiện việc cập nhập listing trước txn2 , nhưng cập nhật invoice sau txn2

Theo quy tắc ghi đè dữ liệu, cuối cùng listing cho sản phẩm sẽ có buyer_id là user B, nhưng invoice cho sản phẩm đó, sẽ có buyer_id là user A

figure3

2.1.3. Cách cài đặt

Để tránh Dirty Write, chúng ta sẽ sử dụng các lock. Mỗi một bản ghi sẽ được cấp một lock. Một transaction khi muốn cập nhật bất cứ bản ghi nào, sẽ phải giữ lock cho bản ghi đó, lock này chỉ được trả lại sau khi transaction đã commit hoăc bị rollback. Nếu lock của bản ghi đang bị giữ bởi transaction khác, transaction hiện tại sẽ phải đợi cho tới khi lock đó được trả lại.

Hình vẽ sau mô tả thuật toán dùng lock để tránh Dirty Write

figure4

Để tránh Dirty Read, chúng ta có 2 giải pháp:

  • Sử dụng cùng lock như đã mô tả ở phần trên. Khi một transaction muốn read dữ liệu tử một bản ghi, transaction này sẽ phải cầm lock của bản ghi đó, sau khi đọc dữ liệu xong (chứ không phải là sau khi transaction commit hoặc rollback như ở phần trên), transaction sẽ trả lại lock. Nếu lock bị giữ bởi transaction khác (do read hoặc write), transaction này sẽ cũng phải đợi cho tới khi lock được trả lại. Giải pháp sử dụng lock như trên khiến cho performance của hệ thống bị giảm. Một thao tác ghi sẽ làm tất cả các thao tác đọc bị ngừng lại, dẫn tới latency cho việc đọc bị tăng lên.

figure5

  • Giải pháp khác có hiệu năng tốt hơn đó là mỗi một bản ghi sẽ có 2 phiên bản: phiên bản trước khi được cập nhật, và phiên bản đang chờ commit. Khi đọc dữ liệu, DB sẽ luôn trả về phiên bản trước khi được cập nhât. Sau khi transaction thực hiên thành công, phiên bản trước khi được cập nhật sẽ được update giá trị mới. Giải pháp này giúp cho các thao tác đọc không cần đợi lock từ thao tác ghi.

figure6

2.2. Snapshot Isolation

Read Commited có thể tránh được các vấn đề của Dirty Read và Dirty Write, tuy nhiên nó vẫn không đảm bảo tính toàn vẹn của dữ liệu trong một số trường hợp. Chúng ta sẽ xem xét các trường hợp lỗi mà Read Committed có thể gây ra dưới đây

2.2.1. Read Skew

Quay trở lại bài toán về Notification được mô tả trong phần 2.1.1. Về phía ứng dụng, chúng ta muốn: số lượng notification chưa đọc = với giá trị của notification_count. Tuy nhiên xét tính huống sau:

figure7

Trong tình huống này chúng ta có 2 transaction: txn1txn2. Ở bước (1), txn1 query vào bảng Notification để lấy ra các unseen notification. Lúc này DB trả về 0.
Ngay sau đó, trong txn2 tại bước (3) và bước (5), một Notification mới được tạo ra, đồng thời notification_counter cũng được cập nhật (tăng lên 1). Sau khi txn2 commit, tại bước (9), trong txn1, nếu user fetch lấy giá trị từ notification_counter, txn1 sẽ thấy giá trị là 1.

Tuy nhiên điều này dẫn tới việc trong txn1, số lượng unseen notification (lấy ra ở bước 1) là khác với giá trị của biến đếm unseen notficiation.

Hiện tượng này được gọi là Read Skew. Nguyên nhân của Read Skew là do dữ liệu một transaction nhìn thấy trong suốt thời gian tồn tại của nó, là bị thay đổi bởi transaction khác thực hiện cùng lúc. Giải pháp cho vấn đề này chính là: mỗi một transaction chỉ được nhìn thấy một snapshot của DB trong suốt thời gian tồn tại, bất cứ dữ liệu nào được thêm mới, là phải do chính bản thân transaction đó.

Chính vì thế, mức độ này còn được gọi là Snapshot Isolation

2.2.2. Sử dụng MVCC để cài đặt Snapshot isolation

Snapshot Isolation còn có tên gọi khác là MVCC (mutliple version concurrent control). Thuật toán này có thể coi là sự tổng quát hóa của thuật toán chống Dirty Read ở mục 2.1.3. Với mỗi bản ghi, thuật toán chống Dirty Read chỉ sử dụng 2 version: old_data, và uncommited_data. Tuy nhiên trong thuật toán MVCC, mỗi một bản ghi sẽ có nhiều phiên bản.

  • Mỗi một transaction khi bắt đầu thực hiện sẽ được gán cho một ID. ID của transaction là duy nhất và tăng dần theo thời gian.

  • Mỗi một bản ghi trong DB sẽ được lưu lại 2 trường: trường created_txn lưu lại ID của transaction tạo ra bản ghi này, và trường deleted_txn lưu lại ID của transaction xóa đi bản ghi này.

  • Mỗi thao tác update bản ghi sẽ được chuyển thành 2 thao tác: thao tác xóa bản ghi cũ và thao tác tạo ra bản ghi mới.

  • Một bản ghi được cho là valid nếu như trường created_txn của bản ghi đó < trường deleted_txn

  • Một transaction với id txn_id, chỉ có thể nhìn thấy các bản ghi valid và đã được commit mà có trường created_txn < txn_id, hoặc là các bản ghi chưa được commit nhưng có created_txn = txn_id. Hay nói cách khác, transaction chỉ nhìn thấy các bản ghi được tạo ra trước, hoặc trong bản thân transaction này.

Biểu đồ sau biểu diễn cách hoạt động của MVCC trên bảng Notification

figure10

2.2.3. Nhược điểm của MVCC

MVCC có một số nhược điểm:

  • Một bản ghi không thực sự bị xóa trong DB trong MVCC. Nó chỉ bị đánh dấu deleted_txn mà thôi. Điều này dẫn tới nhiều bản ghi sẽ bị lưu giữ thừa.

  • Các transaction ID là tăng liên tục, nếu giả sử chúng ta lưu trữ transaction ID bằng một số 32bit, thì hệ thống chỉ có thể xử lý 2^32 transactions. Số lượng transaction ID quá lớn, nó sẽ bị rotate về 0. Tuy nhiên điều này làm cho transaction 0 không nhìn thấy các transaction được thực hiên trước nó?

  • Thêm vào đó, liệu rằng MVCC đã thỏa mãn hết các điều kiện về isolation của Transaction hay chưa? Các trường hợp như Lost Update, hay là Write Skew sẽ ảnh hưởng như thế nào?

Tôi hy vọng có thể trả lời thêm về những vấn đề trên trong bài viết tiếp theo

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

kiennt

30 bài viết.
287 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
92 18
Mọi chuyện bắt đầu từ nắm 2013 trong quá trình xây dựng chức năng login với Facebook, tôi đã tìm ra một cách để tấn công vào các hệ thống login với...
kiennt viết hơn 2 năm trước
92 18
White
29 5
1. Đặt vấn đề Một trong các vấn đề của một hệ thống backend là bài toán điều phối request tới các nguồn dữ liệu. Xét bài toán với một hệ thống bl...
kiennt viết 2 năm trước
29 5
White
25 4
Mở đầu Tôi đang học về (Link) (Link) (hệ thống phân tán). Đây là một lĩnh vực rất hay và khó (đối với tôi). Phần lớn các hệ thống máy tính hiện na...
kiennt viết 2 năm trước
25 4
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
30 bài viết.
287 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!