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

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.

Mark Richards, Giám đốc và kiến trúc sư kỹ thuật cao cấp, Collaborative Consulting, LLC

Mark Richards là giám đốc và kiến trúc sư kỹ thuật cao cấp ở Collaborative Consulting, LLC. Mark là tác giả của của cuốn Java Message Service (O'Reilly, 2009) và Java Transaction Design Strategies (C4Media Publishing, 2006). Mark có chứng chỉ kiến trúc sư và người phát triển của IBM, Sun, The Open Group, và BEA. Mark cũng là diễn giả thường xuyên ở loạt hội nghị chuyên đề No Fluff Just Stuff và diễn thuyết ở nhiều hội nghị khác và các nhóm người dùng trên khắp thế giới.



04 07 2009

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()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 @Transactionalcủ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 ra lỗi ngoại lệ cảnh báo kết nối chỉ đọc.
  • Chèn một cách chính xác lệnh mua bán và giao kết dữ liệu
  • 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 ra một lỗi ngoại lệ cảnh báo kết nối chỉ đọc
  • Chèn đúng đắn lệnh mua bán và giao kết dữ liệu
  • 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 ra một ngoại lệ cảnh báo kết nối chỉ đọc
  • Chèn chính xác một lệnh mua bán và giao kết dữ liệu
  • 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ẽ:

  • 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
  • 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 @TransactionalREQUIRED. Đ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.

Tài nguyên

Học tập

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=406449
ArticleTitle=Các chiến lược giao tác: : Hiểu những cạm bẫy trong giao tác
publish-date=07042009