Java Transaction Processing Full Resolution (4) - Successful Case

Keywords: github Java git

stay This series Of Last article In this article, we mentioned that in order to use the same Connection object in the same transaction, we can achieve the goal of sharing by passing the Connection object, but this is ugly. In this article, we will introduce another mechanism (Connection Holder) to complete transaction management.

This is a series of articles on Java transaction processing. Please download the github source code as follows:

git clone https://github.com/davenkin/java_transaction_workshop.git

The working mechanism of Connection Holder is that we put the Connection object in a global common place, and then get the Connection from this place in different operations to achieve the purpose of Connection sharing. This is also a Service Locator pattern, a bit like JNDI. Define a ConnectionHolder class as follows:

public class ConnectionHolder
{
    private Map<DataSource, Connection> connectionMap = new HashMap<DataSource, Connection>();

    public Connection getConnection(DataSource dataSource) throws SQLException
    {
        Connection connection = connectionMap.get(dataSource);
        if (connection == null || connection.isClosed())
        {
            connection = dataSource.getConnection();
            connectionMap.put(dataSource, connection);
        }

        return connection;
    }

    public void removeConnection(DataSource dataSource)
    {
        connectionMap.remove(dataSource);
    }
}

As you can see from the ConnectionHolder class, we maintain a Map with a key of DataSource and a value of Connection, which is mainly used to enable ConnectionHolder to serve multiple DataSources. When the getConnection method is called, a DataSource object is passed in. If the corresponding Connection of the DataSource already exists in the Map, the Connection is returned directly. Otherwise, a new Connection is obtained by calling the getConnection method of the DataSource, which is then added to the Map and finally returned to the Connection. So in the same transaction process, we get the same Connection from ConnectionHolder one after another, unless we call ConnectionHolder's removeConnection method to remove the current Connection or call Connection.close() to close the Connection, and then call Co again in subsequent operations. The getConnection method of nnectionHolder returns a new Connection object, which causes the transaction to fail. You should not do something like remove or close Connection halfway.

However, although we don't manually remove or close Conncetion objects in the middle of the transaction (of course, we should close Conncetion at the end of the transaction), we can't stop other threads from doing so. For example, the ConnectionHolder class can be used in multiple threads at the same time, and these threads use the same DataSource, one thread closes it after using Connection, while another thread is trying to use the Connection, and the problem arises. Therefore, the Connection Holder above is not thread-safe.

In order to obtain thread-safe ConnectionHolder classes, we can introduce the ThreadLocal class provided by Java, which guarantees that instance variables of a class have a separate copy in each thread, thus not affecting instance variables in other threads. Define a SingleThreadConnectionHolder class as follows:

public class SingleThreadConnectionHolder
{
    private static ThreadLocal<ConnectionHolder> localConnectionHolder = new ThreadLocal<ConnectionHolder>();

    public static Connection getConnection(DataSource dataSource) throws SQLException
    {
        return getConnectionHolder().getConnection(dataSource);
    }

    public static void removeConnection(DataSource dataSource)
    {
        getConnectionHolder().removeConnection(dataSource);
    }

    private static ConnectionHolder getConnectionHolder()
    {
        ConnectionHolder connectionHolder = localConnectionHolder.get();
        if (connectionHolder == null)
        {
            connectionHolder = new ConnectionHolder();
            localConnectionHolder.set(connectionHolder);
        }
        return connectionHolder;
    }

}

With a thread-safe SingleThreadConnectionHolder class, we can use this class in the service layer and in various DAO s to get Connection objects:

 Connection connection = SingleThreadConnectionHolder.getConnection(dataSource);

Of course, at this point we need to pass in a DataSource, which can exist as an instance variable of the DAO class, so we don't need to look like Last article In this way, the article passes the Connection object directly to the DAO. Here you might ask, since DataSource can be used as an instance variable, why not use Connection as an instance variable in the previous article, so that it won't create an ugly API? The reason is that using Connection objects as instance variables can also cause thread security problems. When multiple threads use the same DAO class at the same time, one thread closes Connection while the other is using it. This problem is the same as the thread security problem of Connection Holder mentioned above.

The source code for the Bank DAO and Insurance DAO classes is not listed here. They are just different from the previous article in obtaining the Connection object. You can refer to them. github source code.

Next, let's look at the TransactionManager class. In the last few articles, we wrote code directly related to transaction processing in the service class. A better way is to declare that a TransactionManger class centralizes transaction-related work:

public class TransactionManager
{
    private DataSource dataSource;

    public TransactionManager(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }

    public final void start() throws SQLException
    {
        Connection connection = getConnection();
        connection.setAutoCommit(false);
    }

    public final void commit() throws SQLException
    {
        Connection connection = getConnection();
        connection.commit();
    }

    public final void rollback()
    {
        Connection connection = null;
        try
        {
            connection = getConnection();
            connection.rollback();

        } catch (SQLException e)
        {
            throw new RuntimeException("Couldn't rollback on connection[" + connection + "].", e);
        }
    }

    public final void close()
    {
        Connection connection = null;
        try
        {
            connection = getConnection();
            connection.setAutoCommit(true);
            connection.setReadOnly(false);
            connection.close();
            SingleThreadConnectionHolder.removeConnection(dataSource);
        } catch (SQLException e)
        {
            throw new RuntimeException("Couldn't close connection[" + connection + "].", e);
        }
    }

    private Connection getConnection() throws SQLException
    {
        return SingleThreadConnectionHolder.getConnection(dataSource);
    }
}

As you can see, the Transaction Manager object also maintains a DataSource instance variable, and also obtains the Connection object through the SingleThreadConnectionHolder. Then we use the TransactionManager in the service class:

public class ConnectionHolderBankService implements BankService
{
    private TransactionManager transactionManager;
    private ConnectionHolderBankDao connectionHolderBankDao;
    private ConnectionHolderInsuranceDao connectionHolderInsuranceDao;

    public ConnectionHolderBankService(DataSource dataSource)
    {
        transactionManager = new TransactionManager(dataSource);
        connectionHolderBankDao = new ConnectionHolderBankDao(dataSource);
        connectionHolderInsuranceDao = new ConnectionHolderInsuranceDao(dataSource);

    }

    public void transfer(int fromId, int toId, int amount)
    {
        try
        {
            transactionManager.start();
            connectionHolderBankDao.withdraw(fromId, amount);
            connectionHolderInsuranceDao.deposit(toId, amount);
            transactionManager.commit();
        } catch (Exception e)
        {
            transactionManager.rollback();
        } finally
        {
            transactionManager.close();
        }
    }
}

In Connection Holder Bank Service, we use Transaction Manager to manage transactions. Because both Transaction Manger and two DAO classes use SingleThreadConnection Holder to get Connection, they use the same Connection object throughout the transaction process and the transaction is successful. We can also see that the withdraw and deposit methods of the two DAOs do not accept business-independent objects, eliminating API pollution; in addition, using Transaction Manager to manage transactions makes the service layer code concise.

stay Next article In this article, we will talk about using Template mode to complete transaction processing.

Posted by gbrown on Thu, 11 Jul 2019 16:08:15 -0700