Common errors to be aware of when implementing transactions on the Java platform
Transactions are often used in applications to maintain high data integrity and consistency. If you do not care about data quality, you do not need to use transactions. After all, transaction support on the Java platform reduces performance, causes locking and database concurrency problems, and increases application complexity.
About this series
Transactions improve data quality, integrity, and consistency, making applications more robust. It is not easy to implement successful transaction processing in Java applications. design and coding are almost as important. In this new series of articles, Mark Richard will lead you to design an effective transaction strategy that is suitable for use cases from simple applications to high-performance transactions.
But developers who do not care about transactions will be in trouble. Almost all business-related applications require high data quality. The financial investment industry wastes tens of billions of dollars in failed transactions. Poor data is the second biggest cause of such results (see references ). The lack of transaction support is only one of the factors that lead to bad data (but is the main factor), but we can fully think that, billions of dollars are wasted in the financial investment industry due to lack of transaction support or insufficient transaction support.
Ignoring transaction support is another cause of the problem. I often hear the saying "our applications do not require transaction support because these applications never fail. Yes, I know that some applications have very few or never throw exceptions. These applications are based on well-written code, well-written verification routines, and have been fully tested with code coverage support to avoid performance loss and complexity related to transaction processing. This type of application only needs to consider one feature supported by transactions:Atomicity. Atomicity ensures that all updates are considered as a separate unit, either submitted in full or rolled back. However, rollback or simultaneous update is not the only aspect supported by transactions. On the other hand,IsolationIt will ensure that a work unit is independent from other work units. Without proper transaction isolation, other work units can access updates made by an active work unit even if the work unit is not completed. In this way, business decisions are made based on some data, which may lead to failed transactions or other negative (or expensive) results.
Always better when you are late
I started to focus on transaction processing problems in early 2000. At that time, I was studying a client site. I found that one of the projects planned to take precedence over system testing tasks. It is calledImplement transaction support. Of course, it is very easy to add transaction support to a major application when it is almost ready for system testing. Unfortunately, this method is too common. At least this project (different from most projects)IndeedTransaction support is implemented, even when the development cycle ends.
Therefore, considering the high cost and negative impact of bad data and the Importance (and necessity) of transactions, you need to use transactions to process and learn how to handle possible problems. You may encounter many problems after adding transaction support to your application. Transactions do not always work as expected on the Java platform. This article will discuss the causes. I will use code examples to introduce some common transaction traps that I have seen and experienced in this field, most of which are in the production environment.
Although most of the code examples in this article use Spring framework (Version 2.5), the transaction concept is the same as that in the EJB 3.0 specification. In most cases[email protected]
Replace Annotation with Spring framework@Transactional
Just comment. If the two frameworks use different concepts and technologies, I will provide both Spring framework and EJB 3.0 source code examples.
Local transaction trap
It is best to start with the simplest scenario, that is, useLocal transaction, Also knownDatabase transactions. In the early stages of database persistence (such as JDBC), we usually delegate transaction processing to the database. After all, this is what the database should do. A local transaction is a logical unit of work (luw) that executes a single insert, update, or delete statement ). For example, consider the simple JDBC code in Listing 1.TRADE
Insert a stock transaction order in the table:
List 1. Insert a simple database using JDBC
@Statelesspublic 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(); } }}
The JDBC code in Listing 1 does not contain any transaction logic. It is only saved in the database.TRADE
The transaction order in the table. In this example, the database processes the transaction logic.
In luw, this is a good maintenance operation for a single database. But what if I need to update the balance of my account while inserting transaction orders into the database? As shown in List 2:
Listing 2. Execute multiple table updates in the same method
public TradeData placeTrade(TradeData trade) throws Exception { try { insertTrade(trade); updateAcct(trade); return trade; } catch (Exception up) { //log the error throw up; }}
In this example,insertTrade()
AndupdateAcct()
Method to use standard JDBC code without transactions.insertTrade()
After the method is completed, the database saves (and submits) the transaction order. IfupdateAcct()
If the method fails for any reason, the transaction order will still beplaceTrade()
Saved when method endsTRADE
Table, which causes inconsistent data in the database. IfplaceTrade()
The transaction method is used, and both activities are included in a luw. If the Account update fails, the transaction order will be rolled back.
With the popularization of Java Persistence frameworks, such as Hibernate, toplink, and Java persistence APIs (Java persistence API, JPA), we seldom write simple JDBC code. More often, we use the updated object relationship ing (ORM) framework to reduce the workload. That is, we use a few simple methods to call and replace all the troublesome JDBC code. For example, to insert a transaction order with the JDBC sample code in Listing 1, use the Spring framework with JPATradeData
Object ingTRADE
Table, and replace all JDBC code with the JPA code in listing 3:
Listing 3. Simple inserts using JPA
public class TradingServiceImpl { @PersistenceContext(unitName="trading") EntityManager em; public long insertTrade(TradeData trade) throws Exception { em.persist(trade); return trade.getTradeId(); }}
Note: In listing 3EntityManager
Called onpersist()
Method To insert a transaction order. Very simple, right? Actually not. This code is not as expectedTRADE
The table is inserted into the transaction order, and no exception is thrown. It only returns a value.0
As the key of the transaction order, without changing the database. This is one of the main traps of transaction processing:An ORM-based framework requires a transaction to trigger synchronization between the object cache and the database.. After a transaction is committed, the SQL code is generated, and the database performs the required operations (insert, update, and delete ). Without a transaction, the orm will not be triggered to generate the SQL code and save the changes, so only the method will be terminated-no exception, no updates. If an ORM-based framework is used, transactions must be used. You no longer rely on databases to manage connections and submit work.
These simple examples clearly indicate that transactions must be used to maintain data integrity and consistency. However, for the complexity and traps of implementing transactions on the Java platform, these examples only involve the tip of the iceberg.
Back to Top
Spring framework
@Transactional
Annotation trap
You will test the code in listing 3 and findpersist()
Methods cannot work without transactions. Therefore, you can view several links through a simple Web search and find that if you use Spring framework, you need to use@Transactional
Annotations. Add this comment to the code, as shown in Listing 4:
Listing 4. Use
@Transactional
Note
public class TradingServiceImpl { @PersistenceContext(unitName="trading") EntityManager em; @Transactional public long insertTrade(TradeData trade) throws Exception { em.persist(trade); return trade.getTradeId(); }}
Now you can test the code again and find that the above method still cannot work. The problem is that you must tell Spring framework that you are commenting on the transaction management application. This trap is sometimes hard to find unless you perform adequate unit tests. This usually only causes developers to simply add transaction logic in the spring configuration file, without using annotations.
To be used in spring@Transactional
Note: The following lines of code must be added to the spring configuration file:
<tx:annotation-driven transaction-manager="transactionManager"/>
transaction-manager
Property stores a reference to the Transaction Manager bean defined in the spring configuration file. This code tells spring to use@Transaction
Annotations. If it is not available, it will be ignored@Transactional
Note, so that the Code does not use any transactions.
Make basic@Transactional
Commenting in the code in Listing 4 is just the beginning. Note: Use@Transactional
No additional annotation parameters are specified during annotation. I found that many developers are using@Transactional
It does not take time to understand its role during annotation. For example, use it in Listing 4 as I do.@Transactional
When commenting, what is the transaction propagation mode set? What is the read-only flag set? What is the transaction isolation level setting? More importantly, When should a transaction be rolled back? Understanding how to use this annotation is important to ensure that the appropriate transaction support level is available in the application. Answer the question I just raised: Use it separately without any parameters@Transactional
The propagation mode must be setREQUIRED
, Set the read-only flagfalse
, Set the transaction isolation levelREAD_COMMITTED
And the transaction will not roll back against the controlled exception (checked exception.
Back to Top
@Transactional
Read-only flag traps
Spring is a common trap that I often encounter at work.@Transactional
The read-only flag in the comment is not properly used. Here is a quick test method: If the read-only flag is settrue
, Transmission mode is setSUPPORTS
In listing 5@Transactional
What is the role of annotation?
Listing 5. Associate the read-only flag
SUPPORTS
Combined use of the propagation mode-JDBC
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)public long insertTrade(TradeData trade) throws Exception { //JDBC Code...}
When you executeinsertTrade()
Method, guess which of the following results will be obtained:
- Throw a read-only connection exception.
- Insert transaction order correctly and submit data
- Do nothing because the propagation level is set
SUPPORTS
Which one is it? The correct answer is B. The transaction order is correctly inserted into the database, even if the read-only flag is settrue
And the transaction propagation mode is setSUPPORTS
. But how is this done? Because the propagation mode is setSUPPORTS
So it does not start anything, so this method effectively utilizes a local (database) transaction. The read-only flag applies only when the transaction starts. In this example, the read-only flag is ignored because no transaction is started.
If so, in Listing 6@Transactional
Note: When the read-only flag is set and the propagation mode is setREQUIRED
What is its role?
Listing 6. Associate the read-only flag
REQUIRED
Combined use of the propagation mode-JDBC
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)public long insertTrade(TradeData trade) throws Exception { //JDBC code...}
RuninsertTrade()
Which of the following is the result of the method:
- Throw a read-only connection exception.
- Insert transaction order correctly and submit data
- Do nothing, because the read-only flag is set
true
According to the previous explanation, this question should be well answered. The correct answer is. An exception is thrown, indicating that you are attempting to update a read-only connection. Because a transaction (REQUIRED
), So the connection is set to read-only. There is no doubt that when you try to execute an SQL statement, you will get an exception telling you that the connection is a read-only connection.
A strange thing about the read-only flag is that to use it, a transaction must be started. If you only want to read data, do you need a transaction? The answer is no need at all. Starting a transaction to execute read-only operations will increase the overhead of the processing thread and cause shared read locks to the database (depending on the type of the database used and the isolation level set ). In general, it is meaningless to use the read-only flag when obtaining JDBC-based Java persistence, and additional overhead will be added when unnecessary transactions are started.
What if I use an ORM-based framework? According to the test above, if you callinsertTrade()
Method in listing 7@Transactional
What results will the comment get?
Listing 7. Associate the read-only flag
REQUIRED
Combined Use of propagation mode-JPA
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)public long insertTrade(TradeData trade) throws Exception { em.persist(trade); return trade.getTradeId();}
In listing 7insertTrade()
Which of the following is the result of the method:
- Throw a read-only connection exception.
- Insert transaction order correctly and submit data
- Nothing, because
readOnly
Flag is set to true
The correct answer is B. The transaction order is inserted into the database accurately. Note that the previous example shows thatREQUIRED
In propagation mode, a read-only connection exception is thrown. This is the case when JDBC is used. When using an ORM-based framework, the read-only flag only prompts the database and sets the flush mode of object cacheNEVER
Indicates that the object cache should not be synchronized with the database in this work unit. However,REQUIRED
The propagation mode will overwrite all the content and allow the transaction to start and work, just as if the read-only flag is not set.
This reminds me of another major trap that I often encounter. After reading all the preceding content@Transactional
Note the setting of the read-only flag. What results will the code in listing 8 get?
Listing 8. Use the read-only flag-JPA
@Transactional(readOnly = true)public TradeData getTrade(long tradeId) throws Exception { return em.find(TradeData.class, tradeId);}
In listing 8getTrade()
Which of the following operations will be performed?
- Start a transaction, obtain the transaction order, and then submit the transaction.
- Obtain the transaction order, but do not start the transaction
The correct answer is. A transaction is started and committed. Don't forget,@Transactional
The default propagation mode of the annotation isREQUIRED
. This means that the transaction will be started without any need. Based on the database used, this will cause unnecessary shared locks and may cause deadlocks in the database. In addition, starting and stopping transactions consume unnecessary processing time and resources. In general, when using an ORM-based framework, the read-only flag is basically useless and will be ignored in most cases. However, if you stick to it, remember to set the propagation modeSUPPORTS
(As shown in listing 9), the transaction will not be started:
Listing 9. Use the read-only flag and
SUPPORTS
Select the propagation mode
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)public TradeData getTrade(long tradeId) throws Exception { return em.find(TradeData.class, tradeId);}
In addition, avoid using@Transactional
Note, as shown in listing 10:
Listing 10. Delete
@Transactional
Comment to select
public TradeData getTrade(long tradeId) throws Exception { return em.find(TradeData.class, tradeId);}
Back to Top
REQUIRES_NEW
Transaction property trap
Whether using Spring framework or EJB, useREQUIRES_NEW
Transaction properties will get bad results and cause data corruption and inconsistency.REQUIRES_NEW
The transaction attribute always starts a new transaction when the method is started. Many developers mistakenly useREQUIRES_NEW
Attribute to ensure that the transaction starts. Consider the two methods in listing 11:
Listing 11. Use
REQUIRES_NEW
Transaction attributes
@Transactional(propagation=Propagation.REQUIRES_NEW)public long insertTrade(TradeData trade) throws Exception {...}@Transactional(propagation=Propagation.REQUIRES_NEW)public void updateAcct(TradeData trade) throws Exception {...}
Note that both methods in listing 11 are public methods, which means they can be called independently. When usingREQUIRES_NEW
When the attribute is called in the same logical unit through inter-service communication or orchestration, the attribute will be faulty. For example, assume that in listing 11, you can callupdateAcct()
Method, but alsoinsertTrade()
Method callupdateAcct()
Method. If you callupdateAcct()
If an exception is thrown, the transaction order will be rolled back, but the account update will be submitted to the database, as shown in listing 12:
List 12. Use
REQUIRES_NEW
Multiple update of transaction attributes
@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! ...}
This happens becauseupdateAcct()
A new transaction is started inupdateAcct()
After the method is completed, the transaction is committed. UseREQUIRES_NEW
If an existing transaction context exists in the transaction attribute, the current transaction will be suspended and start a new transaction. After the method is completed, the new transaction is committed and the original transaction continues to be executed.
Because of this behavior, only database operations in the called method need to be saved to the database, and should be used no matter when the transaction result is overwritten.REQUIRES_NEW
Transaction attributes. For example, assume that all stock transactions tried must be recorded in an audit database. For verification errors, insufficient funds, or other reasons, this message must be persistent regardless of whether the transaction fails or not. If no audit method is usedREQUIRES_NEW
Properties, audit records will be rolled back together with the transaction to be executed. UseREQUIRES_NEW
Attribute ensures that audit data is saved regardless of the initial transaction result. Note that you must always useMANDATORY
OrREQUIRED
Attribute insteadREQUIRES_NEW
Unless you have enough reasons to use it, similar to those in the audit example.
Back to Top
Transaction rollback trap
I leave the most common transaction traps to the end. Unfortunately, I have been in the production code multiple times ??? To this error. I will first start with Spring framework and then introduce EJB 3.
The code you have studied so far is similar to listing 13:
Listing 13. No rollback support
@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; }}
Assume that the account does not have enough funds to buy the desired stock, or is not ready to buy or sell the stock, and throws a controlled exception (for exampleFundsNotAvailableException
), Will the transaction order be saved in the database? Or will the entire logical unit be rolled back? The answer is unexpected: depending on the controlled exception (whether in Spring framework or in EJB), the transaction commits all the work that it has not yet submitted. Use listing 13, which means that ifupdateAcct()
If a controlled exception is thrown during the process, the transaction order will be saved, but the account will not be updated to reflect the transaction situation.
This may be a major issue of data integrity and consistency when using transactions. A running exception (uncontrolled exception) automatically forces the rollback of the entire logical unit of work, but the controlled exception does not. Therefore, the code in listing 13 is useless from a transaction perspective; although it seems that it uses transactions to maintain atomicity and consistency, it is actually not.
Although this kind of behavior looks strange, it does its own thing. First, not all controlled exceptions are bad; they can be used for event notifications or redirection based on certain conditions. But more importantly, application code will take corrective actions on some types of controlled exceptions, so that all transactions can be completed. For example, consider the following scenario: you are writing code for online book retailers. To complete the order of books, you must send a confirmation letter in the form of email as part of the order processing. If the email server is disabled, you will send some form of SMTP controlled exception, indicating that the email cannot be sent. If a controlled exception causes automatic rollback, the entire book order will be rolled back due to the shutdown of the email server. By disabling automatic rollback of controlled exceptions, you can capture the exception and perform some corrective actions (such as sending messages to the suspended Queue), and then submit the remaining orders.
When using the declarative transaction mode (Part 1 of this series will be described in more detail), you must specify how the container or framework should handle controlled exceptions. In spring framework@Transactional
In the commentrollbackFor
Parameters are specified, as shown in listing 14:
Listing 14. Adding transaction rollback support-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; }}
Note,@Transactional
Used in the AnnotationrollbackFor
Parameters. This parameter accepts a single exception class or a group of exception classes. You can also userollbackForClassName
The parameter specifies the Exception name as JavaString
Type. You can also use the opposite form of this property (noRollbackFor
) Specifies that all exceptions except some exceptions should be forcibly rolled back. Generally, most developers specifyException.class
As a value, it indicates that all exceptions in this method should be forcibly rolled back.
In terms of rollback transactions, EJB works in a slightly different way than spring framework. In the EJB 3.0 specification@TransactionAttribute
The comment does not contain the command for the specified rollback behavior. RequiredSessionContext.setRollbackOnly()
Method to mark the transaction as an execution rollback, as shown in listing 15:
Listing 15. Adding transaction rollback support-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; }}
CallsetRollbackOnly()
After the method, you cannot change your mind. The only possible result is to roll back the transaction after the transaction method is started. The transaction policy described in subsequent articles in this series will show you when and where to use the rollback command and when to use it.REQUIRED
AndMANDATORY
Transaction attributes.
Back to Top
Conclusion
The code used to implement transactions on the Java platform is not too complicated. However, it is complicated to use and configure it. There are many traps in implementing transaction support on the Java platform (including some traps that are not discussed in this article and are not very common ). The biggest problem with most traps is that no compiler warning or runtime error tells you that the transaction implementation is incorrect. In addition, unlike the content in the "Late always do not do well" section at the beginning of this article, implementing transaction support is not just a coding task. Developing a complete transaction policy involves a lot of design work.Transaction PolicyThe rest of the series will show you how to design effective transaction policies for simple applications to high-performance transaction processing cases.
Original article: http://www.ibm.com/developerworks/cn/java/j-ts1.html