Transactions have four characteristics: Atomicity, Consistency, Isolation and Durability. They stand for ACID. What is sour, lemon (🍋), put them together, it is easy to remember these characteristics. Innodb, the default storage engine for MySQL databases, supports transactions. MyISAM, another common storage engine, does not support transactions. By the way, I have been struggling with how to read MyISAM. I looked it up. In the mysql mailing list, someone asked this question, and the most common way to read MyISAM was [my-i-sam]. I won’t go into the specifics of these four features here, but focus on how to avoid potholes in actual programming.

Properties of a declarative transaction

Spring supports transactions in two ways: programmatic transactions and declarative transactions. Programmatic transactions are intrusive, cumbersome to use, and are rarely used. Declarative transactions are supported by adding the @transactional annotation to which the transaction attributes are added. Several properties of declarative transactions need to be understood.

Transaction propagation level

  1. TransactionDefinition.PROPAGATION_REQUIRED

The @Transactional annotation is propagated by default as REQUIRED, where you join a transaction if there is one. If there is no current transaction, a new one is created.

  1. TransactionDefinition.PROPAGATION_SUPPORTS

If there is a transaction, join the current transaction, if not, then do not start transaction execution. This level of propagation can be used for query methods because the SELECT statement can be executed within or without a transaction;

  1. TransactionDefinition.PROPAGATION_MANDATORY

Indicates that the current transaction must exist and join execution, otherwise an exception will be thrown. This level of propagation can be used for core update logic, such as user balance changes, which are always called by other transactional methods and cannot be called directly by non-transactional methods;

  1. TransactionDefinition.PROPAGATION_REQUIRES_NEW

Indicates that a new transaction execution must be started regardless of whether there is a current transaction. If there is already a transaction, the current transaction will be suspended until a new transaction is completed.

  1. TransactionDefinition.PROPAGATION_NOT_SUPPORTED

This method does not support transactions. If there is a transaction, the current transaction will be suspended until the execution of this method is complete.

  1. TransactionDefinition.PROPAGATION_NEVER

Transactions are not supported and if a transaction is detected, an exception is thrown to reject execution.

  1. TransactionDefinition.PROPAGATION_NESTED

Indicates to start a nested level transaction if there is one, or a new transaction if there is none.

Transaction Isolation level

In the org. Springframework. Transaction. The annotation. The Isolation in the enumeration defines the @ the Isolation level of Transactional support:

  • **DEFAULT ** Use the isolation level specified by the database
  • **READ_UNCOMMITTED ** Read is not committed
  • **READ_COMMITTED ** Read committed
  • **REPEATABLE_READ ** Repeatable read
  • **SERIALIZABLE **SERIALIZABLE

Timeout property

The maximum time that a transaction is allowed to execute. If the time limit is exceeded but the transaction has not completed, the transaction is automatically rolled back. The timeout is represented in TransactionDefinition as an int value in seconds, and the default value is -1. -1 indicates that the default timeout period of the underlying transaction system is used.

Read-only property

For transactions that have only read queries, you can specify the transaction type as readonly, that is, read-only transactions. Read-only transactions do not involve data modification, and the database provides optimizations that are suitable for multiple database query operations. You can check if auto-commit is enabled in the database by using the following statement. If auto-commit is enabled, a statement is a transaction by default.

SHOW VARIABLES LIKE 'autocommit';
Copy the code

In fact, if there is only one query statement in a transaction, there is no need to add the readonly attribute because there is no inconsistency scenario. If there are multiple query statements in a transaction, the results are consistent within the transaction at the repeatable read isolation level. Take, for example, a query on a large batch of data. Instead of fishing out all the data at once, we use paging, counting the number of data, and then using offset and LIMIT batches. If you do not use transactions, the number of batches may not be the same as the number of count queries.

Rollback rules

By default, only runtimeExceptions and errors are rolled back. CheckedException is not rolled back. This can be specified.

Using an example

The @Transactional annotations can be applied to interfaces, classes, and methods. It is common to place annotations on methods to achieve fine-grained control.

// Roll back when an exception occurs, using the default propagation level REQUIRED
@Transactional(rollbackFor = Exception.class)    
// Roll back when MyServiceException occurs, using the propagation level of REQUIRES_NEW
@Transactional(rollbackFor = MyServiceException.class, propagation = Propagation.REQUIRES_NEW)
Copy the code

A spot where potholes are easy to tread

  1. Configured @Transactional on private methods

Declarative transactions use dynamic proxies. Annotations can only be applied to public methods, but common ides can detect this error.

@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @NullableClass<? > targetClass) {
    // Don't allow no-public methods as required.
    if(allowPublicMethodsOnly() && ! Modifier.isPublic(method.getModifiers())) {return null; }... }Copy the code
  1. If a method in the same class calls a method annotated with @transactional, the transaction does not take effect
public class A {
    public void func1(a) {
        func2();
    }
    
    @Transactional(rollbackFor = Exception.class)  
    public void func2(a) {}}Copy the code

In the code above, there are two public methods in class A: func1 and func2. There are transaction annotations on func2. If func1 is the entry point, the transaction annotations on Func2 are not valid for calls to Func1. The reason for this has to do with Spring AOP’s dynamic proxy. For calls using this, they are self-calling and do not use the proxy object to execute the aspect class.

  1. The propagation mode is incorrectly configured, not as expected
public class A {
    @Autowired private B b;
    
    @Transactional(rollbackFor = Exception.class)
    public void func1(a) {
        for (int i = 0; i < 10; i++) {
            try {
       			b.func2(); 
            } catch (Exception ignore) {
            }
        }
        // ...}}public class B {
    @Transactional(rollbackFor = Exception.class)  
    public void func2(a) {
        // If there is an exception, roll back}}Copy the code

In development, there is often a requirement that one of the batch tasks has an exception that can be rolled back without affecting other normal tasks in the same batch. As the code above shows, func1 is called in func2, and the exception is caught, with the expectation that an exception occurs in Func2, that func2 rolls back, and that func1 and other calls in the for loop do not roll. In fact, it’s not what we expect. If func2 rolls back, even if func1 catches the exception, func1 will be rolled back, because func2 propagates as REQUIRED by default, which will be added to the transaction. If func2 throws an exception, The transaction is marked rollbackOnly, causing the entire transaction to be rolled back. Solution:

  • First, remove transaction annotations from Func1;
  • Second, set the transaction propagation mode of Func2 toREQUIRES_NEW
  1. No data source specified

In the beginning we only introduced one library with one data source, so **@Transactional does not need to specify a data source. One more library was introduced, and now you have two data sources. Then @Transactional ** does not know which data source to roll back. Therefore, for systems with multiple data sources, it is important to specify the data source to be rolled back, either through value or transactionManager.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
	@AliasFor("transactionManager")
	String value(a) default "";

	@AliasFor("value")
	String transactionManager(a) default "";
}
Copy the code