Cabbage Java self study room covers core knowledge

1. SpringBoot affairs

Has been using the @ SpringBoot Transactional for transaction management, but few have you thought about SpringBoot is how to realize the transaction management, today, from the perspective of the source code, look at how @ Transactional implementation affairs, finally, we combine the understanding of the source, To help us understand more, write a similar annotation to implement transaction management yourself.

1.1. Isolation level of transactions

Why do transactions need isolation levels? This is because in the case of concurrent transactions, the absence of an isolation level can cause the following problems:

  • Dirty Read: When transaction A makes changes to data that have not yet been committed to the database, and transaction B is accessing the data at the same time, the data acquired by transaction B may be rolled back by transaction A due to the lack of isolation, resulting in data inconsistency.
  • Lost To Modify: when transaction A accesses data 100 and changes it To 100-1=99, and transaction B also reads data 100 and changes data 100-1=99, the final result of the two transactions is 99, but the actual value is 98. The data modified by transaction A was lost.
  • Unrepeatable Read: When transaction A reads data X=100, transaction B changes data X=100 to X=200. At this time, when transaction A reads data X for the second time, it finds that X=200. As A result, the two reads of data X are inconsistent during transaction A, which is called unrepeatable read.
  • Phantom Read: Phantom Read is similar to unrepeatable reading. When transaction A reads the table data, there are only 3 items of data. At this time, transaction B inserts 2 items of data. When transaction A reads the table data again, it finds 5 items of data.

Unrepeatable read vs. phantom read

  • The emphasis of non-repeatable reading is modification: the same condition, you read the data, read again to find a different value, the emphasis is on the update operation.
  • The key of magic reading is to add or delete: under the same conditions, the number of records read for the first time and the second time is not the same, the key is to add and delete operations.

So, to avoid the above problems, there is a concept of isolation level in transactions. In Spring, there are five constant transactionDefinitions that represent isolation level:

  • ISOLATION_DEFAULT: database default isolation level. REPEATABLE_READ isolation level used by MySQL by default.

  • ISOLATION_READ_UNCOMMITTED: Minimum isolation level that allows uncommitted data changes to be read, possibly resulting in dirty reads, illusory reads, or unrepeatable reads.

  • ISOLATION_READ_COMMITTED: Allows concurrent transactions to read data that has already been committed. Dirty reads can be prevented, but phantom or unrepeatable reads can still occur.

  • ISOLATION_REPEATABLE_READ: Multiple reads of the same field are consistent, unless the data is modified by the transaction itself. This can prevent dirty reads and unrepeatable reads, but phantom reads are still possible. The possibility of phantom reads at this isolation level is addressed by MVCC in MySQL.

  • ISOLATION_SERIALIZABLE: Serialization isolation level that prevents dirty, unrepeatable, and phantom reads, but serialization affects performance.

1.2. Transaction propagation mechanism in Spring

Why have a transaction propagation mechanism in Spring? This is a transaction enhancement tool provided by Spring, mainly to solve the problem of how to handle transactions between method calls. For example, there are methods A, B, and C, and methods B and C are called from A. The pseudocode is as follows:

MethodA () {MethodB (); MethodC(); }Copy the code

Suppose each of the three methods has its own transaction opened, what is the relationship between them? Does MethodA rollback affect MethodB and MethodC? The transaction propagation mechanism in Spring addresses this problem.

Seven transaction propagation behaviors are defined in Spring:

  • PROPAGATION_REQUIRED: The current transaction is supported, if one exists. If there are no transactions, start a new one.

  • PROPAGATION_SUPPORTS: Supports the current transaction, if a transaction exists. If there is no transaction, non-transactional execution. However, for transactionally-synchronized transaction managers, PROPAGATION_SUPPORTS is slightly different from not using transactions.

  • PROPAGATION_MANDATORY: The current transaction is supported, if one already exists. If there is no active transaction, an exception is thrown.

  • PROPAGATION_REQUIRES_NEW: Always open a new transaction. If a transaction already exists, suspend the existing transaction.

  • PROPAGATION_NOT_SUPPORTED: Always execute non-transactionally, and suspend any existing transactions.

  • PROPAGATION_NEVER: Always executes non-transactionally, throws an exception if an active transaction exists.

  • PROPAGATION_NESTED: Runs in a nested transaction if an active transaction exists. If there is no active transaction, then the TransactionDefinition. PROPAGATION_REQUIRED properties is carried out.

1.3. How do transactions implement exception rollback in Spring

Now that you’ve reviewed transactions, let’s take a look at how Spring Boot uses @Transactional to manage transactions. Let’s focus on how it implements rollback.

In Spring and TransactionInterceptor PlatformTransactionManager these two classes is the core of the entire transaction module, we focus on the source of these two classes.

  • The TransactionInterceptor intercepts method execution to determine whether a transaction needs to be committed or rolled back.
  • PlatformTransactionManager is the transaction management interface in the Spring, how truly defines transaction rollback and submit.

The TransactionInterceptor class has a lot of code in it, so I’ll simplify the logic.

Public Object invoke(MethodInvocation) throws Throwable {// Get the target method of the transaction invocation Class<? > targetClass = (invocation.getThis() ! = null ? AopUtils.getTargetClass(invocation.getThis()) : null); Return invokeWithinTransaction(Invocation. GetMethod (), targetClass, Invocation ::proceed); }Copy the code

The simplified logic for invokeWithinTransaction is as follows:

Protected Object invokeWithinTransaction(Method Method, @nullable Class<? > targetClass, final InvocationCallback invocation) throws Throwable { Object retVal; Try {/ / call the method that real body retVal = invocation. ProceedWithInvocation (); } the catch (Throwable ex) {/ / if abnormal, perform transactions exception handling completeTransactionAfterThrowing (txInfo, ex); throw ex; } finally {// cleanupTransactionInfo(txInfo) cleanupTransactionInfo(txInfo); } / / if there is no exception, directly to commit the transaction commitTransactionAfterReturning (txInfo); return retVal; }Copy the code

The abnormal rollback transaction logic completeTransactionAfterThrowing is as follows:

/ / the following code omitted parts protected void completeTransactionAfterThrowing (@ Nullable TransactionInfo txInfo, Throwable ex) {/ / determine whether need to rollback, the logic of judgment is to see if there is any statement transaction attribute, and judge whether in the present this exception rollback if (txInfo transactionAttribute! = null && txInfo. TransactionAttribute. RollbackOn (ex)) {/ / rollback txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } else {/ / or you do not need to roll back, directly submit txInfo. GetTransactionManager (), commit (txInfo. GetTransactionStatus ()); }}Copy the code

The above code has explained the basics of Spring transactions, how to determine the execution of transactions and how to roll back. Below the real rollback logic code PlatformTransactionManager interfaces subclass, we use the JDBC transaction, for example, DataSourceTransactionManager is JDBC transaction management class. Tracing the code above the rollback (txInfo getTransactionStatus ()) can be found eventually executed code is as follows:

@Override protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); } try {// call JDBC rollback to rollback the transaction con.rollback(); } catch (SQLException ex) { throw new TransactionSystemException("Could not roll back JDBC transaction", ex); }}Copy the code

Spring mainly relies on the TransactionInterceptor to intercept the execution method body, determine whether to start the transaction, then execute the transaction method body, catch the exception in the method body, and determine whether to roll back. If need to entrust the real rollback the TransactionManager such as JDBC DataSourceTransactionManager to rollback logic. The same goes for committing transactions.

Here’s a flow chart to illustrate the idea:

2. Handwritten annotations implement transaction rollback

Now that we’ve figured out Spring’s transaction execution process, we can emulate ourselves by writing an annotation that rolls back the specified exception. Here, the persistence layer takes JDBC as an example in its simplest form. Let’s start by reviewing the requirements, first by noting that we can implement Spring based AOP, and then since it’s JDBC, we need a class to manage the connection for us and determine whether exceptions are rolled back or committed.

2.1. Maven adds dependencies

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
Copy the code

2.2. Create a new annotation

@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface MyTransaction {// specify the exception rollback Class<? extends Throwable>[] rollbackFor() default {}; }Copy the code

2.3. Create a connection manager

This class helps us manage connections. The core function of this class is to bind fetched connection objects to threads for easy retrieval in AOP processing for commit or rollback operations.

@Component public class DataSourceConnectHolder { @Autowired private DataSource dataSource; ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional Resources "); public Connection getConnection() { Connection con = resources.get(); if (con ! = null) { return con; } try { con = dataSource.getConnection(); Con.setautocommit (false); con.setautocommit (false); } catch (SQLException e) { e.printStackTrace(); } resources.set(con); return con; } public void cleanHolder() { Connection con = resources.get(); if (con ! = null) { try { con.close(); } catch (SQLException e) { e.printStackTrace(); } } resources.remove(); }}Copy the code

2.4. Create a new section

This part is the heart of the transaction. It first gets the exception class on the annotation, then catches the exception executed, determines whether the exception is an exception on the annotation or a subclass of it, rolls back if it is, and commits otherwise.

@Aspect @Component public class MyTransactionAopHandler { @Autowired private DataSourceConnectHolder connectHolder; Class<? extends Throwable>[] es; / / intercept all MyTransaction annotation methods @ org. Aspectj. Lang. The annotation. Pointcut (" @ the annotation (your package path. MyTransaction)") public void Transaction() { } @Around("Transaction()") public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable { Object result = null; Signature signature = proceed.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method == null) { return result; } MyTransaction transaction = method.getAnnotation(MyTransaction.class); if (transaction ! = null) { es = transaction.rollbackFor(); } try { result = proceed.proceed(); } the catch (Throwable Throwable) {/ / exception handling completeTransactionAfterThrowing (Throwable); throw throwable; } // Commit doCommit() directly; return result; * * *} / rollback, and finally close connection and clear the thread binding * / private void doRollBack () {try {connectHolder. GetConnection (). The rollback (); } catch (SQLException e) { e.printStackTrace(); } finally { connectHolder.cleanHolder(); }} / * * * commit, and finally close connection and clear the thread binding * / private void doCommit () {try {connectHolder. GetConnection (), commit (); } catch (SQLException e) { e.printStackTrace(); } finally { connectHolder.cleanHolder(); If the exception is the target exception or a subclass of it, the transaction is rolled back, otherwise the transaction is committed. */ private void completeTransactionAfterThrowing(Throwable throwable) { if (es ! = null && es.length > 0) { for (Class<? extends Throwable> e : es) { if (e.isAssignableFrom(throwable.getClass())) { doRollBack(); } } } doCommit(); }}Copy the code

2.4. Write a Service

The saveTest method invokes two insert statements, declares the @myTransaction transaction annotation, and rolls back an Exception.

@Service public class MyTransactionTest { @Autowired private DataSourceConnectHolder holder; / / a transaction performed in the two SQL insert @ MyTransaction (rollbackFor = NullPointerException. Class) public void saveTest (int id) {save (id, "Cabbage Java Study Room "); Save (id + 10, "Cabbage Java Self-study Room "); throw new RuntimeException(); } private void save(int id, String value) {String SQL = "Insert into test values(? ,?) "; Connection connection = holder.getConnection(); PreparedStatement stmt = null; try { stmt = connection.prepareStatement(sql); stmt.setInt(1, id); stmt.setString(2, value); stmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); }}}Copy the code

We wrote our own @myTransactional annotation via JDBC with Spring AOP to implement rollback when encountering specified exceptions.