Trong quá khứ, 1 database ra đời được thiết kế thường theo tính chất của đại số quan hệ, từ đó cũng nảy sinh các kỹ thuật đặc trưng như tính chất ACID hay các dạng thiết kế normalization (các dạng chuẩn)
Khoảng chục năm trở lại đây, 1 dạng thiết kế khác nổi lên là nosql. Người ta đã nhận ra 1 điều rằng, database có thể thiết kế thiếu quy chuẩn đi 1 chút, nhưng đảm bảo được performance cho hệ thống, serve được 1 lúc nhiều users hơn .. còn quan trọng hơn là 1 database thiết kế quá chặt chẽ, cầu kỳ.
Từ đó, việc hy sinh 1 số tính chất thường thấy trong ACID hoặc các dạng normalization đã càng ngày càng trở nên phổ biến, nhất là khi phải xử lý lượng data lớn.
Tuy vậy, thực tế vẫn có những bussiness buộc phải thiết kế để đảm bảo thỏa mãn được các tính chất mà các DBRM mang lại, như tính ACID trong các transaction chẳn hạn.
Bài viết lấy 1 ví dụ, là công ty bạn cần xây dựng 1 hệ thống ví điểm thưởng, hoặc ví tiền thưởng, phải đảm bảo được là các thao tác chuyển tiền phải ACID.
Như vậy, hệ thống có thể có nhiều services, dùng nhiều db khác nhau, nhưng ít nhất phải có 1 db thỏa ACID. Trong bài viết sẽ propose dùng mysql cho đơn giản.
- Thiết kế db thỏa tính ACID
Để đảm bảo được việc transfer giữa 2 tài khoản thể hiện được tính toàn vẹn và chuẩn xác về mặt dữ liệu. Các thiết kế cho quá trình này phải tuân thủ theo nguyên tắc ACID trong việc lưu trữ và xử lý dữ liệu.
A: atomic => các HQT CSDL đa số đều thực hiện được
C: consistency: tính nhất quán dữ liệu, nó sẽ thể hiện ở việc commit, rollback và locking trong 1 database có hỗ trợ các tác vụ này.
I : isolation: tính cô lập xử lý dữ liệu => có 1 số db dạng quan hệ như mysql, postgre, oracle, mssql server hỗ trợ được tính chất rất quan trọng này.
D : durability : hầu hết các HQT CSDL đều thực hiện được tính chất này.
Ví dụ:
Có 6 tài khoản ví là A,B,C,D,E,F Với số tiền tương ứng như sau
Sau khi mở tài khoản xong, các account này thực hiện các giao dịch chuyển tiền như sau:
Mô hình chuyển tiền giữa các tài khoản:
Để đảo tính ACID trong các giao dịch này, việc thiết kế 1 database hỗ trợ các tính chất này là cần thiết. Vì chữ A và chữ D là mặc định của database sẽ làm việc đó, nên chỉ cần quản tâm tới chữ C và I.
Hình sau sẽ mô tả quá trình chuyển tiền theo các mốc thời gian từ t0 đến t5.
Trường hợp 1: Các tài khoản chuyển lung tung cho nhau gần như cùng 1 lúc.
Quan sát biểu đồ nêu trên, để đảm bảo được tính ACID của dữ liệu, cần phải có cơ chế để khóa tài khoản A lại khi tiến hành giao dịch với B (màu đỏ), sau đó mới được tiếp tục thực hiện tiếp giao dịch với C (màu xanh dương).
Số dư còn lại sau tất cả các giao dịch trên, theo đúng nguyên tắc ACID sẽ là
Trường hợp 2: Từ 1 tài khoản A chuyển đi 1 loạt tk khác gần như cùng 1 lúc (ví dụ từ tài khoản tổng chứa điểm thưởng ck cho các tk trúng thưởng).
Giả sử ban đầu A có 2000 USD. Các tài khoản khác có balance như sau:
Để đảm bảo tính ACID của các giao dịch này. A sẽ bị lock lại và thực hiện lần lượt các giao dịch chuyển tiền cho các ví khác. Hình sau sẽ mô tả quá trình này:
Số dư ví thay đổi sau quá trình này như sau:
2. Lựa chọn các giải pháp để thực hiện việc locking
Ở bước này, việc cần thiết là chọn lựa database có hỗ trợ việc khóa data theo yêu cầu cụ thể của bài toán nêu trên. Để đảm bảo được 2 tính chất Consistency và Isolation thì có 1 số database có hỗ trợ transaction với khả năng lock, unlock, set isolation ..
Vấn đề locking dữ liệu: các database hỗ trợ locking như mysql, oracle .. đều sử dụng giải pháp chủ đạo là 2 phase locking (lock 2 pha) và đây cũng là giải pháp phổ biến nhất hiện nay
Vấn đề về locking trong mysql: có 2 hình thức là shared lock (dành cho đọc data) và execute lock ( ghi data). Việc phối hợp của các giải pháp này cần follow đúng theo tài liệu hiện hữu của mysql trên trang web của oracle, tuyệt đối không được tự ý làm khác. Xem thêm các loại lock trên mysql tại đây https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
Các mức độ locking: để locking ở mức độ record, cần phải lock với câu where có tham gia trong biểu thức là 1 field primary hoặc unique. Vì sao? Vì việc locking record chỉ xảy ra trên 1 clustering index. Mysql sẽ xác định 1 Clustering index theo thứ tự ưu tiên là primary, sau đó là unique index và nếu ko có cả 2 thì xét tới column đầu tiên được sort. Về thiết kế db thông thường, table thường thường có primary index, vì vậy để locking record thì trong câu where phải có 1 mệnh đề có sử dụng field này. Phải xem kỹ các mức độ locking tại đây https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html
Vấn đề deadlock trong mysql: xem thêm tại đây https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks.html
Isolation: đây có thể coi là phần quan trọng nhất của các bussiness locking. Isolation chuẩn giúp ta tránh được 3 lỗi xử lý data là dirty read, nonrepeatable read và phantom row. Các lỗi này cực kỳ nguy hiểm trong bài toán cộng trừ tiền. Xem thêm các mức isolation tại đây https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
3. Ngăn các lỗi isolation
Như đã đề cập tại phần 2, Isolation là phần quan trọng nhất trong việc thiết kế locking. Thiết kế khéo léo sẽ giúp vừa đảm bảo locking, vừa ít bị deadlock khi tải hệ thống lên cao.
Các lỗi về locking trên isolation.
Lỗi 1: lỗi dirty read trong bài toán về xử lý transaction. Lỗi này xảy ra khi 1 transaction đang xử lý data, sau đó rollback về trạng thái ban đầu. Trong đó lại có 1 transaction khác đồng thời lấy data đã xử lý đó trước khi thực hiện rollback.
Ở transaction 1: thực hiện tặng 20USD cho tài khoản A
Ở transaction 2: xem tài khoản A có bao nhiêu tiền, sau đó chi tiêu 10 đồng
Chú ý ở hình trên, tại thời điểm t0, giá trị của tài khoản A đang là 10 USD. Tại thời điểm t1 được tặng 20USD.
Tới thời điểm t2, khi transaction TX1 đang xử lý nghiệp vụ, thì có 1 transaction TX2 khác lấy thông tin của tài khoản A ra thì thấy số dư là 30USD. Lúc này user tiến hành tiêu 10 USD.
Nhưng tới thời điểm t3 thì TX1 bị sự cố phải rollback lại trạng thái trước đó, nghĩa là tài khoản A chỉ còn 10USD.
Thì lúc này TX2 thực hiện xong và số dư chỉ còn 0USD thay vì 10 USD như user này suy nghĩ.
Cách khắc phục: sử dụng ISOLATION READ COMMITED của mysql.
Lỗi 2: lỗi Non Repeatable Read. Tình trạng này xảy ra khi một transaction TX1 vừa thực hiện xong thao tác đọc trên một đơn vị dữ liệu (nhưng chưa commit) thì TX2 lại thay đổi trên đơn vị dữ liệu này. Điều này làm cho lần đọc sau đó của T1 không còn nhìn thấy dữ liệu ban đầu nữa.
Ví dụ, user có 2 ví A và B. Tại thời điểm t0, xảy ra 2 giao dịch: TX1 tiêu 10USD, TX2 tính tổng số tiền các ví của user hiện tại.
Tại thời điểm kết thúc TX1 tại t3, lúc này ví A hết tiền (0USD), nhưng TX2 vẫn không cập nhật được số dư ví A và tính tổng 2 ví lại ra kết quả sai lệch với giá trị đưa vào tính toán ban đầu.
Giải quyết: sử dụng ISOLATION REPEATABLE READ
Lỗi 3: Lỗi Phantom Row. Lỗi này xảy ra khi TX1 đang thực hiện trên 1 tập data, thì tập data đó bị insert thêm vào bởi TX2.
Ví dụ: 1 tài khoản chỉ có thể thực hiện chuyển tối đa 1000 USD/ ngày. Khi thực hiện chuyển tiền, TX1 sẽ tiến hành tính tổng các giá trị giao dịch rồi quyết định có cho transfer không.
Tuy nhiên, sau thời điểm lấy ra các giao dịch (t0) thì tới t1 lại có 1 giao dịch mới được thêm vào ở TX2, dẫn tới việc hàm sum ở t1 vẫn chỉ tính tổng các dữ liệu cũ. Nên sẽ pass qua bước check(s) ở t2. Vì thế, việc chuyển tiền vẫn được xảy ra.
Các khắc phục: dùng ISOLATION SERIALIZABLE
Quay lại bài toán đã trình bày ở bước 1, việc chuyển tiền đảm bảo tính ACID sẽ được thể hiện chi tiết tại 2 bảng sau:
Và kết quả cuối cùng sẽ là:
Trong phần tiếp theo, tôi sẽ hướng dẫn cách implement trên mysql 1 hệ thống tích lũy tiền thưởng, cho phép tạo ví tiền thưởng, nạp tiền, rút tiền và chuyển tiền trên mysql.
Cảm ơn các bạn đã theo dõi.
— — — — — —
Bài viết này của tác giả Quân Rock.