Tải bản đầy đủ (.pdf) (21 trang)

Các chiến lược giao tác: : Hiểu những cạm bẫy trong giao tác ppt

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (231.99 KB, 21 trang )

Cá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.
Khô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
trong đặ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

@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;


public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES,
PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}

Đ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

public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}

Trong trường hợp này, các phương thức insertTrade() và updateAcct() đã dùng mã
lệnh JDBC chuẩn mà không có các giao tác. Một khi phương thức insertTrade()
kết thúc, cơ sở dữ liệu sẽ khẳng định (và giao kết (commit)) lệnh mua bán. Nếu
phương thức updateAcct() thất bại bởi bất cứ lý do gì, lệnh mua bán này sẽ vẫn tồn
tại trong bảng TRADE khi kết thúc phương thức placeTrade(), kết quả là dữ liệu
khô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 {
em.persist(trade);
return trade.getTradeId();
}
}

Lưu ý rằng trong Liệt kê 3 ta gọi phương thức persist() trong EntityManager để
chèn một lệnh mua bán. Đơn giản quá, đúng không? Không hẳn thế. Đoạn mã lệnh
này sẽ không chèn lệnh mua bán vào bảng TRADE như ta mong muốn, cũng
không sinh ra ngoại lệ. Nó chỉ đơn giản là trả lại giá trị 0 như là khóa của lệnh
mua 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
public 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
quan 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
Bạ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.
Cá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
Câu trả lời có một chút lắt léo. Trong một số trường hợp thì câu trả lời là C, nhưng
trong hầu hết trường hợp (đặc biệt khi dùng JPA) thì câu trả lời là B. Lệnh mua
bán được chèn vào cơ sở dữ liệu mà không có lỗi. Chờ một tí – ví dụ trước đó chỉ
ra rằng sẽ có một lỗi kết nối chỉ đọc xảy ra khi chế độ lan truyền là REQUIRED.
Điều này là đúng khi bạn dùng JDBC. Tuy nhiên, khi bạn dùng khung công tác
dựa trên ORM thì cờ chỉ đọc sẽ làm việc khác một chút. Khi bạn sinh một khóa
cho thao tác chèn, khung công tác ORM sẽ tới cơ sở dữ liệu để lấy khóa và sau đó
thực hiện thao tác chèn. Với một vài nhà cung cấp sản phẩm, như Hibernate, chế
độ xả (mode flush) được đặt là MANUAL và không có thao tác chèn nào xảy ra
đối với phép chèn mà không sinh khóa. Điều này cũng đúng với các thao tác cập
nhật. Tuy nhiên, các nhà cung cấp sản phẩm khác, như TopLink, sẽ luôn thực thi
phép chèn và cập nhật khi cờ chỉ đọc được đặt là true. Mặc dù việc này là đặc thù
đối với cả nhà cung cấp sản phẩm lẫn phiên bản sản phẩm, điểm đáng nói ở đây là
bạn không thể chắc chắn rằng phép chèn hay cập nhật sẽ không xảy ra khi cờ chỉ
đọc được thiết lập, đặc biệt khi sử dụng JPA vì nó không biết nhà cung cấp sản
phẩm là ai.
Và những điều này đẩy tôi tới một cạm bẫy lớn khác mà tôi thường xuyên gặp.
Với tất cả những gì bạn đã đọc cho đến giờ, bạn cho rằng mã lệnh trong Liệt kê 8
sẽ làm gì nếu bạn chỉ đặt cờ chỉ đọc trong chú giải @Transactional?

Liệt kê 8. Sử dụng cờ chỉ đọc với — JPA


@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}

Phương thức getTrade() trong Liệt kê 8 sẽ:
A. Khởi động một giao tác, nhận một lệnh mua bán, sau đó hoàn tất giao tác
B. Nhận một lệnh mua bán mà không khởi động một giao tác
Đừng bao giờ nói không bao giờ
Một lúc nào đó bạn muốn khởi động một giao tác cho thao tác đọc cơ sở dữ liệu –
ví dụ, khi phân lập các thao tác đọc để đảm bảo nhất quán dữ liệu hay thiết lập một
mức phân lập giao tác cụ thể cho thao tác đọc. Tuy nhiên, những tình huống như
vậy khá hiếm trong các ứng dụng kinh doanh, trừ khi bạn phải đối mặt với vấn đề
như thế, bạn nên tránh khởi động một giao tác cho các thao tác đọc vì chúng
không cần thiết và có thể dẫn tới việc khóa chết luôn cơ sở dữ liệu, hiệu suất thấp
và thông lượng kém.
Câu trả lời chính xác ở đây là A. Một giao tác sẽ được khởi tạo và hoàn tất. Đừng
quên rằng: chế độ lan truyền mặc định của chú giải @Transactional là
REQUIRED. Điều đó có nghĩa là một giao tác được khởi động khi thực tế nó
không cần phải có (xem phần đừng bao giờ nói không bao giờ). Tùy thuộc vào cơ
sở dữ liệu mà ta đang dùng, điều này có thể gây ra việc khóa chia sẻ một cách
không cần thiết, kết quả là có thể gây ra tình trạng khóa chết trong cơ sở dữ liệu.
Thêm vào đó, thời gian và tài nguyên dành cho việc xử lý không cần thiết bị lãng
phí cho việc khởi động và kết thúc một giao tác. Điểm cốt yếu là khi dùng khung
công tác dựa trên ORM, cờ chỉ đọc khá là vô dụng và trong hầu hết trường hợp cờ
này bị bỏ qua. Nhưng nếu bạn vẫn khăng khăng muốn dùng nó thì hãy luôn đặt
chế độ lan truyền là SUPPORTS, như chỉ ra trong Liệt kê 9, như vậy sẽ không có
giao tác nào được khởi động:


Liệt kê 9. Dùng cờ chỉ đọc và chế độ lan truyền SUPPORTS khi thực hiện
thao tác select

@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}

Tốt hơn hết, hãy tránh hoàn toàn việc sử dụng chú giải @Transactional khi thực
hiện thao tác đọc, như chỉ ra trong Liệt kê 10:

Liệt kê 10. Loại bỏ chú giải @Transactional khi thực hiện thao tác select

public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}



Những lỗi thường vấp đối với thuộc tính giao tác REQUIRES_NEW
Dù bạn đang sử dụng khung công tác Spring hay EJB, việc dùng thuộc tính giao
tác REQUIRES_NEW cũng mang lại hậu quả tiêu cực và dẫn tới dữ liệu bị hỏng
và không nhất quán. Thuộc tính REQUIRES_NEW luôn khởi động một giao tác
mới khi thực hiện phương thức, dù đang có hay không có một giao tác khác.
Nhiều người lập trình dùng thuộc tính REQUIRES_NEW không chính xác, nghĩ
rằng đó là cách đúng để đảm bảo chắc chắn khởi động một giao tác. Xem hai
phương thức sau đây trong Liệt kê 11:

Liệt kê 11. Sử dụng thuộc tính REQUIRES_NEW


@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception { }

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception { }

Chú ý rằng trong Liệt kê 11 thì cả hai phương thức đều là công cộng (public), tức
là chúng có thể được gọi độc lập với nhau. Vấn đề xảy ra với thuộc tính
REQUIRES_NEW là khi các phương thức sử dụng nó được gọi trong cùng một
đơn vị công việc logic thông qua giao tiếp liên dịch vụ hoặc qua sự phối hợp. Ví
dụ, giả sử trong Liệt kê 11 bạn gọi phương thức updateAcct() một cách độc lập với
các phương thức khác trong một vài ca sử dụng, nhưng cũng có trường hợp trong
đó phương thức updateAcct() được gọi trong phương thức insertTrade(). Bây giờ
nếu có ngoại lệ xảy ra sau khi gọi phương thức updateAcct(), lệnh mua bán sẽ bị
hủy bỏ nhưng cập nhật tài khoản sẽ vẫn được giao kết vào cơ sở dữ liệu, như ta
thấy trong Liệt kê 12:

Liệt kê 12. Đa cập nhật sử dụng thuộc tính giao tác REQUIRES_NEW

@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
updateAcct(trade);
//exception occurs here! Trade rolled back but account update is not!

}

Điều này xảy ra vì một giao tác mới được khởi động trong phương thức
updateAcct(), như vậy giao tác này sẽ hoàn tất khi phương thức updateAcct() kết
thúc. Khi ta dùng thuộc tính giao tác REQUIRES_NEW, nếu đã có một giao tác

tồn tại rồi, thì giao tác hiện tại sẽ bị buộc tạm dừng và một giao tác mới được khởi
động. Khi phương thức kết thúc thì giao tác mới sẽ được giao kết và giao tác ban
đầu lại phục hồi.
Vì cách hoạt động như thế nên thuộc tính giao tác REQUIRES_NEW chỉ nên
dùng trong trường hợp hành động cơ sở dữ liệu trong phương thức được gọi cần
được ghi lưu vào cơ sở dữ liệu bất chấp kết quả của giao tác phủ ngoài. Ví dụ, giả
sử người ta cố gắng ghi lại tất cả các giao dịch chứng khoán vào trong cơ sở dữ
liệu kiểm toán. Thông tin này cần được ghi lại bền vững bất kể giao dịch thất bại
hay không bởi các lý do như lỗi không hợp lệ, thiếu tiền hay những lý do khác.
Nếu ta không sử dụng thuộc tính REQUIRES_NEW trong phương thức kiểm toán,
bản ghi kiểm toán sẽ bị cuộn lùi trở lại, gỡ bỏ giao dịch định thực hiện. Dùng
thuộc tính REQUIRES_NEW đảm bảo rằng dữ liệu kiểm toán sẽ được ghi lưu bất
chấp kết quả của giao tác khởi tạo. Điểm chính ở đây là ta luôn sử dụng hoặc là
thuộc tính MANDATORY hoặc là thuộc tính REQUIRED thay cho
REQUIRES_NEW trừ phi bạn có lý do gì đó để dùng nó tương tự như trong ví dụ
kiểm toán đã nêu.


Những cái bẫy do cuộn lùi giao tác
Tôi để lại trình bày cuối cùng cái bẫy giao tác phổ biến nhất. Thật không may, tôi
lại thấy nó xuất hiện nhiều hơn trong các mã lệnh chạy sản xuất. Tôi sẽ bắt đầu với
Spring Framework và sau đó chuyển sang EJB 3.
Cho đến giờ, mã lệnh mà chúng ta đã xem xét trông giống như trong Liệt kê 13:

Liệt kê 13. Không hỗ trợ cuộn lùi lại

@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);

updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}

Giả sử số dư tài khoản không đủ tiền để mua chứng khoán đang nói đến hoặc còn
chưa được thiết lập để mua bán chứng khoán và chương trình đưa ra một ngoại lệ
đã kiểm tra (checked exception) (ví dụ, FundsNotAvailableException). Lệnh mua
bán này có được tiếp tục ghi lưu trong cơ sở dữ liệu hay không hay toàn bộ đơn vị
công việc logic bị cuộn lùi lại để hủy bỏ? Câu trả lời, thật đáng ngạc nhiên, là với
ngoại lệ kiểm tra đó (cả trong Spring hay trong EJB), thì giao tác sẽ giao kết bất cứ
công việc nào còn chưa được giao kết. Nhìn vào Đoạn mã lệnh 13, nếu một ngoại
lệ đã kiểm tra xảy ra trong phương thức updateAcct(), lệnh mua bán sẽ vẫn được
ghi lưu bền vững, nhưng tài khoản sẽ không được cập nhật tương ứng để phản ánh
giao dịch mua bán đó.
Có lẽ đây là vấn đề chính về tính toàn vẹn và nhất quán dữ liệu khi sử dụng các
giao tác. Các ngoại lệ thời gian chạy (run-time) (tức là các ngoại lệ không kiểm tra
được (unchecked exceptions) tự động bắt buộc toàn bộ đơn vị công việc logic phải
cuộn lùi để hủy bỏ, nhưng các ngoại lệ đã kiểm tra (checked exceptions) thì
không. Bởi vậy, mã lệnh trong Liệt kê 13 là vô ích trên quan điểm giao tác; mặc
dù nó có vẻ là dùng các giao tác để duy trì tính nguyên tử và tính nhất quán, thực
tế là nó không làm được.
Mặc dù kiểu hành xử này có vẻ lạ, các giao tác hành xử theo cách này là vì một số
lý do thích đáng. Trước hết, không phải tất cả các ngoại lệ đã kiểm tra đều tệ;
chúng có thể được dùng cho việc báo cáo sự kiện hoặc để chuyển hướng việc xử
lý dựa trên những điều kiện nhất định. Nhưng hơn thế nữa, mã lệnh trình ứng dụng
có thể đưa ra những hành động sửa chữa đối với một số dạng ngoại lệ đã kiểm tra,

do đó cho phép giao tác này hoàn tất. Ví dụ, xét một kịch bản trong đó bạn đang
viết chương trình cho một cửa hàng bán lẻ sách trực tuyến. Để hoàn thành một đơn
hàng sách, bạn cần gửi một thư điện tử khẳng định lại như một phần của quá trình
đặt hàng. Nếu máy chủ thư không hoạt động, bạn sẽ cần gửi một kiểu ngoại lệ đã
kiểm tra SMTP chỉ ra rằng thông điệp không được gửi. Nếu ngoại lệ đã kiểm tra
này tự động gây ra cuộn lùi thì toàn bộ đơn hàng mua sách sẽ bị hủy bỏ chỉ vì máy
chủ thư không hoạt động. Bằng cách không tự động cuộn lùi khi các ngoại lệ đã
kiểm tra, bạn có thể bắt được ngoại lệ này và thực hiện một số hành động sửa chữa
(như gửi thông điệp thư điện tử đến hàng đợi chờ xử lý) và giao kết phần còn lại
của đơn đặt hàng.
Khi bạn sử dụng mô hình giao tác khai báo (được mô tả chi tiết trong phần 2 của
loạt bài này), bạn phải chỉ rõ thùng chứa hay khung công tác nên xử lý các ngoại
lệ đã kiểm tra ra sao. Trong Spring Framework bạn chỉ rõ điều này thông qua tham
số rollbackFor trong chú giải @Transactional, như thấy trong Liệt kê 14.

Liệt kê 14. Bổ sung hỗ trợ cuộn lùi giao tác trong Spring

@Transactional(propagation=Propagation.REQUIRED,
rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}


Hãy lưu ý đến việc sử dụng tham số rollbackFor trong chú giải @Transactional.
Tham số này hoặc là nhận chỉ một lớp ngoại lệ hoặc là nhận một mảng các lớp
ngoại lệ, hoặc bạn có thể dùng tham số rollbackForClassName để chỉ rõ tên của
các ngoại lệ dưới dạng một xâu ký tự của Java. Bạn cũng có thể sử dụng dạng phủ
định của thuộc tính (noRollbackFor) để chỉ rõ rằng tất cả các ngoại lệ đều bắt buộc
phải cuộn lùi ngoại trừ những ngoại lệ nhất định. Thường thì hầu hết những người
lập trình chọn giá trị Exception.class, điều này chỉ thị rằng tất cả các ngoại lệ trong
phương thức này bắt buộc phải cuộn lùi.
EJB lại hoạt động khác một chút với Spring Framework trong vấn đề cuộn lùi một
giao tác. Chú giải @TransactionAttribute có trong đặc tả của EJB 3.0 không bao
gồm các chỉ thị để xác định hành vi hủy bỏ. Hơn thế nữa, bạn phải dùng phương
thức SessionContext.setRollbackOnly() để đánh dấu giao tác phải cuộn lùi để hủy
bỏ, như minh họa trong Liệt kê 15:

Liệt kê 15. Thêm hỗ trợ cuộn lùi giao tác trong EJB

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}

Một khi phương thức setRollbackOnly() đã được gọi, bạn không thể thay đổi

quyết định nữa; kết cục duy nhất có thể là cuộn lùi giao tác khi đã hoàn tất phương
thức khởi động giao tác này. Chiến lược giao tác mô tả trong bài viết tiếp theo của
loạt bài này sẽ hướng dẫn khi nào và ở đâu cần cuộn lùi.


Kết luận
Mã lệnh dùng để thực hiện các giao tác trên nền Java không quá phức tạp; tuy
nhiên, cách bạn dùng và cấu hình chúng có thể có chút rắc rối. Nhiều cái bẫy có
liên quan đến việc thực hiện các hỗ trợ giao tác trên nền Java (bao gồm một số cái
bẫy nữa ít phổ biến hơn mà tôi không thảo luận ở đây). Vấn đề lớn nhất với đa số
các cạm bẫy này là không có một cảnh báo biên dịch hay một lỗi khi đang chạy
nào cho ta biết việc thực hiện giao tác là không chính xác. Hơn thế nữa, trái ngược
với giả định phản ánh trong giai thoại "Muộn còn hơn không" ở đầu bài viết, việc
thực hiện hỗ trợ giao tác không chỉ là lao động lập trình. Cần có một nỗ lực thiết
kế rất đáng kể trong việc phát triển một chiến lược giao tác toàn diện. Những bài
còn lại trong loạt bài Các chiến lược giao tác sẽ giúp hướng dẫn bạn về vấn đề
thiết kế một chiến lược giao tác hiệu quả cho các ca sử dụng trong một phạm vi
rộng từ những ứng dụng đơn giản cho đến ứng dụng xử lý giao tác hiệu năng cao.

Mục lục

 Những cạm bẫy trong giao tác cục bộ.
 Bẫy chú giải @Transactional của khung công tác Spring
 Bẫy cờ chỉ đọc của chú giải @Transactional
 Những lỗi thường vấp đối với thuộc tính giao tác REQUIRES_NEW
 Những cái bẫy do cuộn lùi giao tác
 Kết luận

×