Transaction - [01] Introduction to Spring transaction management and implementation of Spring boot + Druid + mybatis single data source transaction management

Pre knowledge

  • A brief introduction to google

What is the business?

  • Transaction is a reliable and consistent way to access and operate program units in database

Characteristics of transactions

  • Atomicity: do or not
  • Consistency: consistency refers to the fact that a transaction must transform a database from one consistency state to another
  • Persistence: persistence means that once a transaction is committed, the change to the data in the database is permanent.
  • Isolation: different transaction operations do not interfere with each other

The problem of concurrent transactions

  1. Dirty read: dirty read refers to reading data in another uncommitted transaction in one transaction process, aiming at a certain data
  2. Non repeatable read: multiple queries within a transaction range return different data values because they are modified and submitted by another transaction at the query interval.
  3. Phantom reading: refers to the number of data queried multiple times during the execution of a transaction, which is inconsistent. For example, where is used to filter the data newly inserted by the second transaction more than the first one. Phantom reading is for a batch of data as a whole

Isolation level of transaction

  • Read "uncommitted read uncommitted: can read the data modification content without uncommitted transaction (the lowest isolation level and high concurrency performance)
  • Read "committed read: only transactions and committed data can be read (lock rows being read)
  • Repeatable read: ensure that the same data is read consistently multiple times in a transaction. Not affected by other transactions (lock all rows read)
  • SERIALIZABLE: sequential execution of transactions,

Spring transaction

  • Spring provides a unified transaction API to support different resource types
  • Spring also provides declarative transaction management, decoupled from business code
  • Easy to integrate with Spring ecological framework
  • Support multi resource transaction management and synchronization

1. Transaction abstraction

1.1 PlatformTransactionManager

  • Transaction manager: it mainly provides a unified interface for commit operation, rollback operation and obtaining transaction status according to transaction definition
public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;

}

1.2 TransactionDefinition

  • Transaction definition: some basic information of transaction, such as timeout, isolation level, propagation attribute, etc

Transaction propagation mechanism, that is, how transactions are passed in the calls of multiple methods; for example, two service methods are transaction operations, and how do one transaction method call another transaction method for processing?

public interface TransactionDefinition {

    // Communication mechanism of business
    // If the caller has a transaction, the caller's transaction is used. If no transaction exists, a transaction is created
	int PROPAGATION_REQUIRED = 0;

    // Follow the caller. If the caller has one, use it. If the caller does not have one, use it
	int PROPAGATION_SUPPORTS = 1;

    // The caller's method must run in a transaction. If there is no transaction, an exception will be thrown
	int PROPAGATION_MANDATORY = 2;

    // Whether the caller has transaction execution or not, a new transaction must be initiated by itself. Suspend the original transaction, which only works in jta's transaction manager (nested transactions are not supported)
	int PROPAGATION_REQUIRES_NEW = 3;

    // Even if the caller has a transaction, I will execute it in a non transaction and suspend the original transaction
	int PROPAGATION_NOT_SUPPORTED = 4; 

    // Never in business
	int PROPAGATION_NEVER = 5;

    // Nested transactions (actually, they are not supported. They are implemented by using save points, which are executed by the caller before, and then are similar to opening another transaction for execution. After execution, the save points are recovered and execution continues. JDBC 3.0 and above)
	int PROPAGATION_NESTED = 6;

    // The following defines the transaction isolation mechanism
    // Depending on the default isolation mechanism of the database
	int ISOLATION_DEFAULT = -1;

    // Read not submitted
	int ISOLATION_READ_UNCOMMITTED = 1;  // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

    // Read submitted
	int ISOLATION_READ_COMMITTED = 2;  // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;

    // Repeatable read
	int ISOLATION_REPEATABLE_READ = 4;  // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

    // Serialization
	int ISOLATION_SERIALIZABLE = 8;  // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


    // Default timeout, subject to database settings
	int TIMEOUT_DEFAULT = -1;

    // Get propagation properties of transaction
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}

    // Get isolation level of transaction
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}

    // Get transaction timeout
	default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

    // Whether the transaction is read-only.
	default boolean isReadOnly() {
		return false;
	}

    // Get the name of the transaction
	@Nullable
	default String getName() {
		return null;
	}

    // Returns a default transaction definition
	static TransactionDefinition withDefaults() {
		return StaticTransactionDefinition.INSTANCE;
	}

}

1.3 TransactionStatus

  • Transaction status: some status information of the transaction, such as whether it is a new transaction or whether it has been marked for rollback
// Savepoint provides a savepoint mechanism in Nested propagation mechanism to implement Nested transactions. When there is an error, you can choose to restore to the savepoint
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    // Is there a save point
	boolean hasSavepoint();

	@Override
	void flush();
}
public interface TransactionExecution {
    // Is it a new transaction
	boolean isNewTransaction();

	// Set transaction rollback
	void setRollbackOnly();
    
    // Whether the transaction is rolled back
	boolean isRollbackOnly();
    
    // Get whether the transaction is completed 
	boolean isCompleted();

}

2. Common implementation of platformtransactionmanager

  • DataSourceTransactionManager (for JDBC Template, MyBatis, etc.)
  • Jpatractionmanager (for data JPA, hibernate, etc.)
  • JmsTransactionManager (for message middleware, etc.)
  • JTA transaction manager (mainly used for distributed transactions)

3. Spring single data source transaction case

3.1 environmental description:

  • DataSource: Alibaba Druid
  • Database: MySQL 5.7
  • SpringBoot: 2.2.2.RELEASE
  • ORM: MyBatis
  • GitHub: https://github.com/ilssio/transaction-example/tree/master/one-data-source

3.2 example code

  • This case amount is based on transfer business. PlatformTransactionManager is DataSourceTransactionManager
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDAO accountDAO;
    @Autowired
    PlatformTransactionManager transactionManager;
    /**
     * Declarative transaction
     * propagation = Propagation.REQUIRED (The default value is REQUIRED) if the caller has a transaction, the caller's transaction will be used directly, if not, a new transaction will be created
     * transactionManager = "transactionManager" It's also the default
     * isolation= Isolation.DEFAULT Isolation level
     * There are also parameters such as timeout, which can be checked by yourself. There are instructions in the source code of Transactional
     * @param sourceAccountId Source account
     * @param targetAccountId Target account
     * @param amount Amount of money
     * @return Operation result information
     */
    @Override
    @Transactional(transactionManager = "transactionManager",propagation = Propagation.REQUIRED, rollbackFor = Exception.class, isolation= Isolation.DEFAULT)
    public String transferAnnotation(Long sourceAccountId, Long targetAccountId, BigDecimal amount) {
        AccountDO sourceAccountDO = accountDAO.selectByPrimaryKey(sourceAccountId);
        AccountDO targetAccountDO = accountDAO.selectByPrimaryKey(targetAccountId);
        if (null == sourceAccountDO || null == targetAccountDO) {
            return "Transfer in or transfer out account does not exist";
        }
        if (sourceAccountDO.getBalance().compareTo(amount) < 0) {
            return "Insufficient balance of transfer out account";
        }
        sourceAccountDO.setBalance(sourceAccountDO.getBalance().subtract(amount));
        accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
        // error("annotation error!");
        targetAccountDO.setBalance(targetAccountDO.getBalance().add(amount));
        accountDAO.updateByPrimaryKeySelective(targetAccountDO);
        return "Successful transfer!";
    }

    @Override
    public String transferCode(Long sourceAccountId, Long targetAccountId, BigDecimal amount) {
        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        // Get transaction start business execution
        TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
        try {
            AccountDO targetAccountDO = accountDAO.selectByPrimaryKey(targetAccountId);
            AccountDO sourceAccountDO = accountDAO.selectByPrimaryKey(sourceAccountId);
            if (null == sourceAccountDO || null == targetAccountDO) {
                return "Transfer in or transfer out account does not exist";
            }
            error("code error");
            if (sourceAccountDO.getBalance().compareTo(amount) < 0) {
                return "Insufficient balance of transfer out account";
            }
            sourceAccountDO.setBalance(sourceAccountDO.getBalance().subtract(amount));
            targetAccountDO.setBalance(targetAccountDO.getBalance().add(amount));
            accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
            accountDAO.updateByPrimaryKeySelective(sourceAccountDO);
            // Submission of affairs
            transactionManager.commit(transaction);
            return "Successful transfer!";
        } catch (Exception e) {
            log.error("Transfer error, start to roll back, source: {}, target: {}, amount: {}, errMsg: {}",
                    sourceAccountId, targetAccountId, amount, e.getMessage());
            // Error rollback
            transactionManager.rollback(transaction);
        }
        return "Transfer failure";
    }

    @Override
    public List<AccountDO> listAll() {
        return accountDAO.selectAll();
    }


    private static void error(String msg) {
        throw new RuntimeException(msg);
    }
}

3.3 Transactional annotation implements transactions

  • Using annotated transactions, Spring will implement a proxy class with a proxy,

  • Then get the transaction manager from the context, open a transaction and execute the business code,

  • If the rollback exception conditions you set are met, perform rollback

  • When we call it, we call the Service method directly

  • But when Spring is implemented, it actually calls the Transaction Advisor (for transaction management) through the AOP Proxy (AOP Proxy service), and then processes the service method that calls the annotated transaction

  • Through the interface / API / Account / transfer / annotation? Source = 1 & target = 2 & amount = 123, we trigger the transfer operation of account 1 to account 2 123 yuan
  • Log information (enable DEBUG log):
o.s.web.servlet.DispatcherServlet        : GET "/api/account/transfer/annotation?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.ilss.transaction.onedatasource.web.AccountController#transferAnnotation(Long, Long, String)
o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [io.ilss.transaction.onedatasource.service.impl.AccountServiceImpl.transferAnnotation]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'transactionManager',-java.lang.Exception
o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] for JDBC transaction
o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] to manual commit
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
o.m.s.t.SpringManagedTransaction         : JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] will be managed by Spring
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 1(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 2(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==>  Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==> Parameters: Junior one(String), xiaoyi(String), 123456(String), 877.00(BigDecimal), 2020-01-09T17:04:28(LocalDateTime), 2020-01-09T17:44:33(LocalDateTime), 1(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective  : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==>  Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==> Parameters: Waiter(String), xiaoer(String), 123456(String), 223.00(BigDecimal), 2020-01-09T17:04:40(LocalDateTime), 2020-01-09T17:04:40(LocalDateTime), 2(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective  : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@39615e42]
o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@185f0a96]
o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@185f0a96] after transaction
m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
m.m.a.RequestResponseBodyMethodProcessor : Writing ["Successful transfer!"]
o.s.web.servlet.DispatcherServlet        : Completed 200 OK

  • Process executed successfully
  1. GET requests to the /api/account/transfer/annotation interface and then calls the service method.
  2. Create a new transaction based on the settings of the written Transactional annotation
  3. Select JDBC connection and create SqlSession
  4. Register transaction synchronization for SqlSession
  5. SQL operation
  6. Then commit the transaction
  7. Finally, release resources. Complete method call
  8. Interface return 200
  • It can be seen from the log that we use MyBatis+Druid to configure data source transaction management, which is actually the data source transaction manager

  • Although transaction manager is specified in the code, no Bean registration or other transaction manager code related to DataSource is actually added. This is because Spring helped you do it. I set up the transactionManager because the name of the automatically assembled transactionManager is this. Write it out to remind you of this configuration. As the name implies, this configuration is to configure the manager implementation of this transaction. In short, it is the specification of PlatformTransactionManager. Later, when there are multiple data sources, we will specify different transaction managers to implement transaction management.

  • What will happen if there is an error?

  • Read the log first (open DEBUG log):

o.s.web.servlet.DispatcherServlet        : GET "/api/account/transfer/annotation?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.ilss.transaction.onedatasource.web.AccountController#transferAnnotation(Long, Long, String)
o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [io.ilss.transaction.onedatasource.service.impl.AccountServiceImpl.transferAnnotation]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'transactionManager',-java.lang.Exception
o.s.j.d.DataSourceTransactionManager     : Acquired Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] for JDBC transaction
o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] to manual commit
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
o.m.s.t.SpringManagedTransaction         : JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] will be managed by Spring
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 1(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58] from current transaction
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==>  Preparing: select id, nickname, username, `password`, balance, create_time, update_time from account where id = ?
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : ==> Parameters: 2(Long)
i.i.t.o.d.AccountDAO.selectByPrimaryKey  : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58] from current transaction
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==>  Preparing: update account SET nickname = ?, username = ?, `password` = ?, balance = ?, create_time = ?, update_time = ? where id = ?
i.i.t.o.d.A.updateByPrimaryKeySelective  : ==> Parameters: Junior one(String), xiaoyi(String), 123456(String), 754.00(BigDecimal), 2020-01-09T17:04:28(LocalDateTime), 2020-01-09T17:44:33(LocalDateTime), 1(Long)
i.i.t.o.d.A.updateByPrimaryKeySelective  : <==    Updates: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@66240e58]
o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec]
o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1b6963ec] after transaction
o.s.web.servlet.DispatcherServlet        : Failed to complete request: java.lang.RuntimeException: annotation error!
o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is 
java.lang.RuntimeException: annotation error!
                            .......
o.a.c.c.C.[Tomcat].[localhost]           : Processing ErrorPage[errorCode=0, location=/error]
o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error?source=1&target=2&amount=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, text/html;q=0.8]
o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500

  • Failed execution process
  1. GET requests to the /api/account/transfer/annotation interface and then calls the service method.
  2. Create a new transaction based on the settings of the written Transactional annotation
  3. Select JDBC connection and create SqlSession
  4. Register transaction synchronization for SqlSession
  5. SQL operation: you can see from the log that there is an error in the first update operation
  6. Directly roll back Rolling back JDBC transaction on Connection and print the error log. The second update operation is gone
  7. Method exit abnormally, interface 500

3.4 programming to realize transaction management

  • Main steps of programming transaction
  1. Create a transaction definitiontransactiondefinition = new defaulttransactiondefinition();
  2. Set transaction properties: such as propagation properties, isolation properties, etc
  3. Open transaction through TransactionManager
  4. Execute business code
  5. Commit transaction / process rollback
  • The execution of programming transaction in this instance is basically the same as declarative transaction. You can download the code yourself if you are interested.

//todo: next multi data source transaction management

About me

  • Hangzhou coordinates, general undergraduate in reading, computer science and technology major, graduated in June 20, crazy looking for a job....
  • At present, we are in the rookie stage. Everyone spray lightly. My little brother is learning crazily.
  • Welcome to communicate with me!!!
Published 33 original articles, won praise 7, visited 8080
Private letter follow

Tags: Spring JDBC Mybatis Apache

Posted on Sat, 11 Jan 2020 00:03:25 -0800 by iffo