Sau hết, việc hỗ trợ giao tác trên nền Java có thể hủy hoại hiệu năng, sinh ra vấn đề về khóa và các vấn đề tương tranh trong cơ sở dữ liệu, và do vậy gây thêm phức tạp cho trình ứng dụn
Trang 1Các chiến lược giao tác: : Hiểu những cạm bẫy trong giao tác
Đề phòng các lỗi thường gặp khi triển khai thực hiện giao tác trên nền Java
Mark Richards, Giám đốc và kiến trúc sư kỹ thuật cao cấp, Collaborative
Consulting, LLC
Tóm tắt: Xử lý giao tác phải đạt được tính toàn vẹn và nhất quán của dữ liệu ở
mức cao Bài viết này, là bài đầu tiên trong một loạt bài viết về phát triển một chiến lược giao tác hiệu quả trên nền Java, sẽ giới thiệu những cạm bẫy thường gặp để ngăn bạn khỏi mắc vào Dùng những ví dụ là các đoạn mã lệnh trong
Spring Framework và đặc tả EnterPrise JavaBeans (EJB) 3.0, tác giả Mark
Richards sẽ giải thích những lỗi quá thông thường ấy
Lý do chung nhất khi sử dụng các giao tác trong một ứng dụng là để duy trì tính toàn vẹn và nhất quán của dữ liệu ở mức cao Nếu bạn không quan tâm đến chất lượng dữ liệu của mình, thì bạn cũng không cần quan tâm đến các giao tác Sau hết, việc hỗ trợ giao tác trên nền Java có thể hủy hoại hiệu năng, sinh ra vấn đề về khóa và các vấn đề tương tranh trong cơ sở dữ liệu, và do vậy gây thêm phức tạp cho trình ứng dụng của bạn
Về loạt bài này
Các giao tác làm tăng chất lượng, tính toàn vẹn và tính nhất quán của dữ liệu của bạn, và khiến cho các trình ứng dụng của bạn vững chãi hơn Việc triển khai thể hiện thành công các xử lý giao tác trong các ứng dụng Java không phải là một công việc tầm thường, và đây là nói về việc thiết kế cũng quan trọng ngang với nói
về viết mã lệnh Trong loạt bài mới này, Mark Richards sẽ hướng dẫn chúng ta thiết kế một chiến lược giao tác hiệu quả cho một loạt các trường hợp từ các trình ứng dụng đơn giản cho đến xử lý giao tác hiệu năng cao
Nhưng những người phát triển lại không bận tâm đến những giao tác gây thiệt hại cho mình như thế Hầu hết các ứng dụng có liên quan đến kinh doanh đều yêu cầu chất lượng dữ liệu ở mức cao Chỉ riêng ngành kinh doanh đầu tư tài chính đã mất mười tỉ đô la cho các hoạt động thương mại thất bại, mà dữ liệu tồi là nguyên nhân thứ hai dẫn tới tình trạng này (xem Tài nguyên) Mặc dù việc thiếu các hỗ trợ giao tác chỉ là một tác nhân dẫn đến tình trạng dữ liệu tồi (vẫn là nguyên nhân chính), một kết luận chắc chắn là hàng tỷ đô la đã bị lãng phí chỉ riêng trong lĩnh vực kinh doanh đầu tư tài chính là hậu quả của việc thiếu hụt hoặc không có các hỗ trợ giao tác
Trang 2Không biết gì về các hỗ trợ giao tác là nguyên nhân khác của vấn đề Rất thường xuyên tôi đã nghe những tuyên bố theo kiểu “chúng tôi không cần hỗ trợ giao tác trong các trình ứng dụng của chúng tôi đâu, bởi vì chúng chả bao giờ lỗi cả.” Đúng Tôi đã từng chứng kiến một số trình ứng dụng trong thực tế cực hiếm hoặc không bao giờ đưa ra các báo lỗi Những trình ứng dụng ấy trông cậy vào việc có
mã lệnh viết rất tốt, có các thủ tục kiểm tra dữ liệu hợp lệ được viết tốt và việc hỗ trợ kiểm soát mã và kiểm thử đầy đủ để giảm chi phí thực thi và những phức tạp liên quan đến xử lý giao tác Vấn đề của cách suy nghĩ như thế là ở chỗ nó chỉ tính
đến một đặc trưng của hỗ trợ giao tác: tính nguyên tử Tính nguyên tử đảm bảo
rằng mọi cập nhật sẽ được xem như một đơn vị công việc duy nhất và, hoặc là tất
cả được giao kết hoặc là tất cả bị hủy bỏ Nhưng sự hủy bỏ hoặc phối hợp các cập
nhật không phải là khía cạnh duy nhất của hỗ trợ giao tác Một khía cạnh khác, sự phân lập, sẽ đảm bảo rằng mỗi đơn vị công việc được tách biệt khỏi các đơn vị
khác Nếu không có sự phân lập giao tác thích hợp, các đơn vị công việc khác có thể truy nhập vào các cập nhật được tạo ra bởi một đơn vị công việc đang chạy, mặc dù đơn vị này chưa hoàn thành xong việc của mình Và kết quả là các quyết định kinh doanh có thể được đưa ra dựa trên dữ liệu chưa hoàn chỉnh, gây ra
những giao dịch kinh doanh thất bại hoặc những hậu quả tiêu cực
Muộn còn hơn không
Tôi bắt đầu đánh giá đúng các vấn đề trong xử lý giao tác từ đầu năm 2000, khi làm việc cho khách hàng tôi để ý đến một mục trong bản kế hoạch dự án ngay bên
trên nhiệm vụ kiểm thử hệ thống Dòng đó là thực hiện hỗ trợ giao tác Chắc chắn
rồi, khá dễ dàng bổ sung các hỗ trợ giao tác vào trình ứng dụng chính khi nó gần như đã đến giai đoạn sẵn sàng để kiểm thử hệ thống có phải không? Thật không may, cách tiếp cận này quá chung chung Ít nhất thì dự án này, không giống như
hầu hết những dự án khác, đã thực thi các hỗ trợ giao tác, mặc dù ở giai đoạn cuối
của chu kỳ phát triển
Vậy thì khi đã biết rằng chi phí cao và ảnh hưởng xấu của dữ liệu tồi và các hiểu biết cơ bản về giao tác là quan trọng (và cần thiết), bạn cần sử dụng các giao tác và học cách giải quyết các vấn đề nảy sinh Bạn gấp rút bổ sung hỗ trợ giao tác vào trình ứng dụng của mình Và đây chính là chỗ mà các vấn đề thường nảy sinh Các giao tác hình như thường không hoạt động như hứa hẹn trên nền Java Bài viết này
sẽ khảo sát tỉ mỉ lý do tại sao như thế Cùng với sự trợ giúp của các đoạn mã ví dụ, tôi sẽ giới thiệu những cạm bẫy phổ biến trong giao tác mà tôi thường thấy và kinh nghiệm trong lĩnh vực này, hầu hết trường hợp là trong các môi trường sản xuất
Mặc dù hầu hết các đoạn mã ví dụ trong bài viết này sử dụng khung công tác Spring (Spring Framework) phiên bản 2.5, khái niệm giao tác là tương tự như trong đặc tả EJB 3.0 Trong đa số các trường hợp, chỉ đơn giản là ta thay thế lời chú giải @Transactional của khung công tác Spring bằng @TransactionAttribute
Trang 3trong đặc tả của EJB 3.0 Những chỗ mà hai bộ khung này khác nhau về khái niệm
và kỹ thuật, tôi sẽ đưa ra cả hai ví dụ mã nguồn của khung công tác Spring và EJB
3
Những cạm bẫy trong giao tác cục bộ
Cách tốt nhất để khởi đầu là bằng một kịch bản dễ nhất: việc sử dụng các giao tác cục bộ, cũng thường được gọi là giao tác cơ sở dữ liệu Thời kỳ đầu mới xuất hiện
cơ sở dữ liệu bền vững (ví dụ JDBC), chúng ta thường giao phó việc xử lý giao tác cho cơ sở dữ liệu Rốt cuộc thì đây có phải chính là cái mà cơ sở dữ liệu cần phải làm? Các giao tác cục bộ làm việc tốt với các đơn vị công việc logic (LUW), tức là thực hiện các câu lệnh đơn như chèn, cập nhật hoặc xóa Ví dụ, xét đoạn mã lệnh JDBC đơn giản trong Liệt kê 1, đoạn mã lệnh này thực hiện thao tác chèn một lệnh mua bán chứng khoán vào bảng TRADE:
Liệt kê 1 Thao tác chèn đơn giản vào một cơ sở dữ liệu sử dụng JDBC
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
Trang 5Đoạn mã lệnh JDBC trong Liệt kê 1 không có logic giao tác, nó một mực đưa lệnh mua bán vào bảng TRADE trong cơ sở dữ liệu Trong trường hợp này, cơ sở dữ liệu điều khiển logic giao tác
Điều này là tốt và hợp lý đối với trường hợp chỉ có một hành động duy trì cơ sở dữ liệu trong đơn vị công việc lô gic (LUW) Nhưng giả sử rằng bạn cần cập nhật số
dư tài khoản cùng thời điểm với việc bạn chèn một lệnh mua bán vào cơ sở dữ liệu, như ta thấy trong Liệt kê 2:
Liệt kê 2 Thực hiện nhiều cập nhật bảng trong cùng một phương thức
} catch (Exception up) {
//log the error
Trang 6không nhất quán trong cơ sở dữ liệu Nếu phương thức placeTrade() sử dụng các giao tác, cả hai hoạt động này sẽ nằm trong cùng một LUW và lệnh mua bán này
sẽ bị hủy nếu việc cập nhật tài khoản bị thất bại
Với sự phổ biến rộng rãi của các khung công tác bền vững của Java như
Hibernate, TopLink và Java Persistence API (JPA) đang phát triển, chúng ta hiếm khi viết thẳng các đoạn mã lệnh JDBC nữa Phổ biến hơn là chúng ta dùng các khung công tác ánh xạ quan hệ - đối tượng (ORM) mới hơn để làm cho công việc
dễ dàng hơn bằng cách thay thế tất cả các đoạn mã lệnh JDBC khó chịu này bằng một vài lời gọi phương thức đơn giản Ví dụ, để chèn một lệnh mua bán từ ví dụ đoạn mã lệnh JDBC trong Liệt kê 1, sử dụng khung công tác Spring với JPA, bạn
sẽ ánh xạ đối tượng TradeData vào bảng TRADE và thay thế toàn bộ đoạn mã lệnh JDBC này bằng đoạn mã lệnh JPA trong Liệt kê 3:
Liệt kê 3 Thao tác chèn đơn giản dùng JPA
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
Trang 7mua bán này mà chẳng biến đổi gì cơ sở dữ liệu cả Đây là cạm bẫy chủ yếu đầu
tiên của xử lý giao tác: các khung công tác dựa trên nền ORM yêu cầu phải có một giao tác để kích hoạt một quá trình đồng bộ hóa giữa đối tượng nhớ sẵn (cache object) và cơ sở dữ liệu Chính là thông qua việc giao kết một giao tác mà mã SQL
sẽ được sinh ra và tác động đến cơ sở dữ liệu với các hành động mong muốn (như chèn, cập nhật, xóa) Không có một giao tác ở đây thì không thể kích hoạt một quá trình trên ORM để sinh mã lệnh SQL và thực hiện các thay đổi, như vậy phương thức đơn giản chỉ kết thúc– không có lỗi ngoại lệ, không có cập nhật Nếu bạn đang dùng khung công tác dựa trên ORM, bạn phải dùng sử dụng các giao tác Bạn không còn có thể dựa vào cơ sở dữ liệu để quản lý các kết nối và hoàn tất công việc
Những ví dụ đơn giản này biểu thị rõ ràng rằng giao tác là cần thiết để duy trì dữ liệu toàn vẹn và nhất quán Nhưng đây mới chỉ là bề ngoài của những rắc rối và những cạm bẫy thường vấp phải khi thực thi các giao tác trên nền Java
Bẫy chú giải @Transactional của khung công tác Spring
Như vậy bạn đã kiểm thử mã lệnh trong Liệt kê 3 và khám phá ra rằng phương thức persist() không thực hiện khi thiếu giao tác Kết quả là bạn thấy vài đường liên kết nhờ một thao tác tìm kiếm đơn giản trên Internet và biết rằng với khung công tác Spring, ta cần dùng chú giải @Transactional Bởi thế bạn thêm chú giải vào mã lệnh như trong Liệt kê 4:
Liệt kê 4 Sử dụng chú giải @Transactional
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
@Transactional
Trang 8public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
Kiểm thử lại mã lệnh và bạn sẽ nhận thấy chương trình vẫn không hoạt động Vấn
đề là bạn phải thông báo với SpringFramework rằng bạn đang sử dụng các chú giải để quản lý giao tác Trừ phi bạn đang thực hiện kiểm thử đơn vị toàn bộ, đôi khi cái bẫy này khá là khó tìm ra Thông thường nó dẫn người phát triển đến chỗ chỉ đơn giản thêm vào các logic giao tác trong tệp cấu hình Spring mà không nghĩ tới các chú giải
Khi sử dụng chú giải @Transactional trong Spring, ta phải thêm dòng mã sau vào tệp cấu hình Spring:
<tx:annotation-driven transaction-manager="transactionManager"/>
Thuộc tính transaction-manager lưu giữ một tham chiếu đến bean quản lý giao tác được định nghĩa trong tệp cấu hình Spring Dòng mã này báo cho Spring sử dụng chú giải @Transaction khi áp dụng bộ chặn giao tác Nếu không có đoạn mã này, chú giải @Transactional sẽ bị bỏ qua, kết quả là không có giao tác nào được sử dụng trong mã lệnh
Việc làm cho chú giải cơ sở @Transactional có tác dụng trong mã lệnh ở Liệt kê 4 chỉ là sự khởi đầu Lưu ý rằng Liệt kê 4 sử dụng chú giải @Transactional mà không định rõ bất cứ tham số chú giải bổ sung nào Tôi nhận thấy nhiều người dùng chú giải @Transactional mà không bỏ thời gian tìm hiểu đầy đủ xem nó làm
gì Ví dụ, khi sử dụng chú giải @Transactional không tham số như ta đã làm trong Liệt kê 4, chế độ lan truyền giao tác sẽ được thiết lập là gì? Cờ báo chỉ đọc được đặt là gì? Mức phân lập giao tác được đặt là gì? Quan trọng hơn, khi nào thì giao tác sẽ bị cuộn lùi trở lại? Hiểu chú giải giao tác được sử dụng như thế nào là rất
Trang 9quan trọng để đảm bảo bạn có mức độ hỗ trợ giao tác thích hợp trong trình ứng dụng của mình Và đây là trả lời những câu hỏi tôi vừa đặt ra: khi sử dụng chú giải
@Transactional không có bất kỳ tham số nào, chế độ lan truyền được đặt là
REQUIRED, cờ báo chỉ đọc đặt là false, mức phân lập giao tác đặt giá trị mặc định của cơ sở dữ liệu (thường là READ_COMMITTED), và giao tác sẽ không bị cuộn lùi trở lại khi ngoại lệ đã được kiểm tra
Bẫy cờ chỉ đọc của chú giải @Transactional
Một cạm bẫy phổ biến nhất mà tôi thường xuyên gặp là dùng sai cờ chỉ đọc trong chú giải Spring @Transactional Ở đây có một câu hỏi nhanh dành cho bạn: Khi dùng mã lệnh JDBC chuẩn của Java bền vững, chú giải @Transactional trong Liệt
kê 5 thực hiện công việc gì khi cờ chỉ đọc được thiết lập giá trị true và chế độ lan truyền đặt là SUPPORTS?
Liệt kê 5 Sử dụng cờ chỉ đọc với chế độ lan truyền JDBC là SUPPORTS
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
//JDBC Code
}
Khi thi hành phương thức insertTrade() trong Liệt kê 5, nó sẽ:
A Đưa ra lỗi ngoại lệ cảnh báo kết nối chỉ đọc
B Chèn một cách chính xác lệnh mua bán và giao kết dữ liệu
C Không làm gì vì mức lan truyền đặt là SUPPORTS
Trang 10Bạn đầu hàng? Câu trả lời chính xác là B Lệnh mua bán được chèn một cách chính xác vào cơ sở dữ liệu, thậm chí cả khi cờ chỉ đọc được thiết lập giá trị true
và lan truyền giao tác được đặt là SUPPORTS Nhưng tại sao lại có thể như thế? Không có giao tác nào được khởi động vì phương thức truyền dẫn là SUPPORTS, như vậy là phương thức này thực sự dùng giao tác cục bộ (của cơ sở dữ liệu) Cờ chỉ đọc chỉ được áp dụng nếu một giao tác được khởi động Trong trường hợp này, không có giao tác nào thực hiện nên cờ chỉ đọc bị bỏ qua
Được thôi, nếu đúng là như thế thì chú giải @Transactional sẽ có tác dụng gì trong liệt kê 6 khi cờ chỉ đọc có giá trị true và phương thức truyền dẫn là REQUIRED?
Liệt kê 6 Sử dụng cờ chỉ đọc với phương thức truyền dẫn REQUIRED của
— JDBC
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code
}
Khi chạy, phương thức insertTrade() trong liệt kê 6 sẽ làm gì:
A Đưa ra một lỗi ngoại lệ cảnh báo kết nối chỉ đọc
B Chèn đúng đắn lệnh mua bán và giao kết dữ liệu
C Không làm gì cả vì cờ chỉ đọc được thiết đặt giá trị true
Câu hỏi rất dễ trả lời vì đã có các giải thích lúc trước Câu trả lời chính xác là A
Sẽ có một ngoại lệ được đưa ra, chỉ báo rằng bạn đang cố thực hiện một thao tác cập nhật trên kết nối chỉ đọc Vì một giao tác sẽ được khởi động (REQUIRED), kết nối này sẽ được thiết đặt là chỉ đọc Chắc chắn, khi bạn thử thực hiện câu lệnh SQL ấy, bạn sẽ nhận được một ngoại lệ thông báo rằng kết nối là chỉ đọc
Trang 11Cái dở của cờ chỉ đọc là bạn cần khởi động một giao tác nó mới có tác dụng Tại sao bạn cần một giao tác nếu như bạn chỉ đọc dữ liệu? Câu trả lời là bạn không cần Việc khởi động một giao tác để thực thi hành động chỉ đọc thêm gánh nặng cho luồng xử lý và có thể gây ra khóa việc chia sẻ khi đọc dữ liệu trong cơ sở dữ liệu (phụ thuộc vào kiểu cơ sở dữ liệu mà bạn đang dùng và mức phân lập được thiết đặt) Điểm cốt yếu là cờ chỉ đọc là sẽ hơi vô nghĩa khi bạn dùng nó trong Java bền vững dựa trên JDBC và sinh thêm chi phí khi phải khởi tạo một giao tác không cần thiết
Tình hình sẽ thế nào khi bạn dùng khung công tác dựa trên ORM? Vẫn với những câu hỏi nhanh như trên, bạn có thể đoán kết quả của chú giải @Transactional trong Liệt kê 7 là gì nếu phương thức insertTrade() được gọi khi sử dụng JPA với
Hibernate?
Liệt kê 7 Sử dụng cờ chỉ đọc với chế độ lan truyền là REQUIRED của — JPA
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
Phương thức insertTrade() trong Liệt kê 7 sẽ:
A Đưa ra một ngoại lệ cảnh báo kết nối chỉ đọc
B Chèn chính xác một lệnh mua bán và giao kết dữ liệu
C Không làm gì vì cờ readOnly được thiết đặt là true