Как транзакции реализованы в Spring Boot

Spring Boot Java

1 Обзор

Я использовал SpringBoot@TransactionalЗаниматься управлением делами, но редко задумываться о том, как реализован SpringBoot, а сегодня из исходников посмотреть@TransactionalКак реализовать транзакции, и, наконец, мы объединяем понимание исходного кода, пишем аналогичную аннотацию, чтобы самостоятельно управлять транзакциями, чтобы помочь нам углубить наше понимание.

Инструкции по прочтению: в этой статье предполагается, что вы знакомы с языком Java и имеете базовое представление о транзакциях и их использовании.

2. Знание дел

Прежде чем приступить к просмотру исходного кода, давайте рассмотрим соответствующие знания о транзакциях.

2.1 Уровень изоляции транзакций

Зачем транзакциям нужны уровни изоляции? Это связано с тем, что в случае параллельных транзакций отсутствие уровня изоляции вызовет следующие проблемы:

  • Грязное чтение: когда транзакция A изменяет данные, но модификация не была отправлена ​​​​в базу данных, транзакция B одновременно обращается к данным. Поскольку нет изоляции, данные, полученные транзакцией B, могут быть отменены транзакцией A, что приводит к несогласованности данных Проблема.

  • Потеряно для изменения: Когда транзакция A получает доступ к данным 100 и изменяет их на 100-1=99, а транзакция B считывает данные 100 и изменяет данные 100-1=99, окончательный результат модификации двух транзакций равен 99, но на самом деле это 98. Данные, измененные транзакцией А, теряются.

  • Неповторимое чтение: Когда транзакция A считывает данные X = 100, транзакция B изменяет данные X = 100 на X = 200. В это время, когда транзакция A считывает данные X во второй раз, она обнаруживает, что X = 200, в результате чего в течение всего периода A транзакции два чтения данных X несовместимы, что является неповторяемым чтением.

  • Фантомное чтение: Фантомные чтения аналогичны неповторяемым чтениям. Таблица фантомного чтения заключается в том, что когда транзакция A читает данные таблицы, есть только записи данных 3. В это время транзакция B вставляет записи данных 2. Когда транзакция A снова читает ее, она обнаруживает, что записей 5, и есть еще 2 записи ни с того ни с сего записи, как галлюцинации.

Неповторяемое чтение против фантомного чтения

Смысл неповторяемого чтения заключается в изменении :Те же условия, данные, которые вы прочитали, прочтите их снова и обнаружите, что значение другое, основное внимание уделяется операции обновления.Смысл фантомного чтения в том, чтобы добавить или удалить: При одинаковых условиях количество считываемых записей в первый и во второй раз разное, а упор делается на операции добавления и удаления.

Поэтому, чтобы избежать вышеуказанных проблем, существует понятие уровня изоляции в транзакциях, и в Spring определены пять констант, представляющих уровни изоляции:

постоянный инструкция
TransactionDefinition.ISOLATION_DEFAULT Уровень изоляции базы данных по умолчанию, уровень изоляции REPEATABLE_READ, принятый MySQL по умолчанию.
TransactionDefinition.ISOLATION_READ_UNCOMMITTED Самый низкий уровень изоляции, позволяющий читать незафиксированные изменения данных,Может вызвать грязное чтение, фантомное чтение или неповторяющееся чтение..
TransactionDefinition.ISOLATION_READ_COMMITTED Позволяет читать данные, которые были зафиксированы параллельными транзакциями,Грязные чтения можно предотвратить, но фантомные или неповторяющиеся чтения все же могут происходить..
TransactionDefinition.ISOLATION_REPEATABLE_READ Результаты многократного чтения одного и того же поля согласуются, если только данные не изменены его собственной транзакцией, ** может предотвратить грязное чтение и неповторяющееся чтение, но фантомное чтение все же может происходить. **В MySQL устранена возможность фантомного чтения на этом уровне изоляции с помощью MVCC.
TransactionDefinition.ISOLATION_SERIALIZABLE уровень изоляции сериализации,Этот уровень предотвращает грязные чтения, неповторяемые чтения и фантомные чтения., но сериализация влияет на производительность.

2.2 Механизм распространения транзакций в Spring

Почему в Spring существует набор механизмов распространения транзакций? Это инструмент улучшения транзакций, который Spring предоставляет нам, в основном для решения проблемы вызовов между методами и способов обработки транзакций. Например, есть метод A, метод B и метод C, а метод B и метод C вызываются в A. Псевдокод выглядит следующим образом:

MethodA{
	MethodB;
	MethodC;
}
MethodB{

}
MethodC{

}

Предполагая, что все три метода открыли свои дела, какая между ними связь?MethodAОткат повлияетMethodBиMethodC? Механизм распространения транзакций в Spring призван решить эту проблему.

В Spring определено семь вариантов поведения распространения транзакций:

тип инструкция
PROPAGATION_REQUIRED Если нет текущей транзакции, создайте новую транзакцию, если уже есть транзакция, присоединяйтесь к транзакции. Это самый распространенный выбор
PROPAGATION_SUPPORTS Поддерживать текущую транзакцию, если текущей транзакции нет, она будет выполнена в нетранзакционном режиме.
PROPAGATION_MANDATORY Используйте текущую транзакцию или создайте исключение, если текущей транзакции нет.
PROPAGATION_REQUIRES_NEW Создать новую транзакцию. Если есть текущая транзакция, приостановить текущую транзакцию.
PROPAGATION_NOT_SUPPORTED Выполнить операцию нетранзакционным способом, приостановив текущую транзакцию, если текущая транзакция существует.
PROPAGATION_NEVER Выполняется без транзакций и выдает исключение, если транзакция уже существует.
PROPAGATION_NESTED Если транзакция уже существует, выполните ее во вложенной транзакции. Если текущих транзакций нет, сделайте что-то похожее на PROPAGATION_REQUIRED.

Как эти семь коммуникационных механизмов влияют на дела, заинтересованные студенты могут прочитатьэта статья.

3. Как реализовать аварийный откат

Изучив соответствующие знания о транзакциях, давайте формально изучим, как передать транзакцию в Spring Boot.@TransactionalЧтобы управлять транзакциями, давайте сосредоточимся на том, как он реализует откат.

веснойTransactionInterceptorиPlatformTransactionManagerЭти два класса являются ядром всего модуля транзакций.TransactionInterceptorОтвечает за перехват выполнения метода и определение необходимости фиксации или отката транзакции.PlatformTransactionManagerЭто интерфейс управления транзакциями в Spring, который действительно определяет, как транзакции откатываются и фиксируются. Мы сосредоточимся на исходном коде этих двух классов.

TransactionInterceptorКода в классе много, упрощу логику для простоты объяснения:

	//以下代码省略部分内容
	public Object invoke(MethodInvocation invocation) throws Throwable {
	//获取事务调用的目标方法
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
	//执行带事务调用
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

Упрощенная логика invokeWithinTransaction выглядит следующим образом:

	//TransactionAspectSupport.class
	//省略了部分代码
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {
			Object retVal;
			try {
			//调用真正的方法体
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// 如果出现异常,执行事务异常处理
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
			//最后做一下清理工作,主要是缓存和状态等
				cleanupTransactionInfo(txInfo);
			}
			//如果没有异常,直接提交事务。
			commitTransactionAfterReturning(txInfo);
			return retVal;
	
	}

Логика аварийного отката транзакцийcompleteTransactionAfterThrowingследующее:

//省略部分代码
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
				//判断是否需要回滚,判断的逻辑就是看有没有声明事务属性,同时判断是不是在目前的这个异常中执行回滚。
			if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
				//执行回滚
					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
			} 
			else {
						//否则不需要回滚,直接提交即可。
					txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
			
			}
		}
	}

Приведенный выше код четко объясняет основные принципы транзакций Spring, как оценивать и выполнять транзакции и как выполнять откат. Теперь к коду, который фактически выполняет логику отката.PlatformTransactionManagerПодкласс интерфейса, в качестве примера возьмем транзакцию JDBC,DataSourceTransactionManagerЭто класс управления транзакциями jdbc. Следуйте приведенному выше кодуrollback(txInfo.getTransactionStatus())Можно обнаружить, что окончательный исполняемый код выглядит следующим образом:

@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 {
		//调用jdbc的 rollback进行回滚事务。
			con.rollback();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
		}
	}

3.1 Резюме

Вот краткое изложение идей реализации транзакций в Spring, Spring в основном опирается наTransactionInterceptorЧтобы перехватить тело метода выполнения, определите, следует ли открывать транзакцию, а затем выполните тело метода транзакции в теле метода.catchЖиви ненормально, потом суди, нужно ли откатывать, а если нужно откатывать, делегируй настоящуюTransactionManagerНапример в JDBCDataSourceTransactionManagerдля выполнения логики отката. То же самое верно и для совершения транзакции.

Вот блок-схема, иллюстрирующая идею:

流程图

4. Напишите аннотацию для реализации отката транзакции

Мы разобрались с процессом выполнения транзакций Spring, теперь мы можем имитировать себя, чтобы написать аннотацию для реализации функции отката при возникновении указанного исключения. Здесь слой сохраняемости берет в качестве примера простейший JDBC. Давайте сначала разберем требования. Во-первых, мы аннотируем, что можем реализовать его на основе АОП Spring. Затем, поскольку это JDBC, нам нужен класс, который поможет нам управлять соединением, чтобы определить, откатывается ли исключение или фиксируется. Дайте высохнуть, когда закончите.

4.1 Первое добавление зависимостей

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

4.2 Добавить примечание

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 17:05
 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
    //指定异常回滚
    Class<? extends Throwable>[] rollbackFor() default {};
}

4.3 Добавлен диспетчер соединений

Этот класс помогает нам управлять соединениями.Основная функция этого класса — привязать извлеченный объект соединения к потоку, что удобно для извлечения, фиксации или отката во время обработки АОП.

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 21:14
 **/
@Component
public class DataSourceConnectHolder {
    @Autowired
    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);
        } 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();
    }
}

4.4 Добавить новый аспект

Эта часть является ядром обработки транзакций.Сначала получите класс исключения в аннотации, затем перехватите исключение выполнения, определите, является ли исключение исключением в аннотации или его подклассом, и если да, выполните откат, в противном случае отправьте.

/**
 * @description:
 * @author: luozhou 
 * @create: 2020-03-29 17:08
 **/
@Aspect
@Component
public class MyTransactionAopHandler {
    @Autowired
    DataSourceConnectHolder connectHolder;
    Class<? extends Throwable>[] es;

    //拦截所有MyTransaction注解的方法
    @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.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();
        } catch (Throwable throwable) {
            //异常处理
            completeTransactionAfterThrowing(throwable);
            throw throwable;
        }
        //直接提交
        doCommit();
        return result;
    }
		/**
		* 执行回滚,最后关闭连接和清理线程绑定
		*/
    private void doRollBack() {
        try {
            connectHolder.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }

    }
		/**
		*执行提交,最后关闭连接和清理线程绑定
		*/
    private void doCommit() {
        try {
            connectHolder.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }
    }
		/**
		*异常处理,捕获的异常是目标异常或者其子类,就进行回滚,否则就提交事务。
		*/
    private void completeTransactionAfterThrowing(Throwable throwable) {
        if (es != null && es.length > 0) {
            for (Class<? extends Throwable> e : es) {
                if (e.isAssignableFrom(throwable.getClass())) {
                    doRollBack();
                }
            }
        }
        doCommit();
    }
}

4.5 Тестовая проверка

Создайте таблицу tb_test со следующей структурой:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_test
-- ----------------------------
DROP TABLE IF EXISTS `tb_test`;
CREATE TABLE `tb_test` (
  `id` int(11) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;

4.5.1 Написать службу

saveTestМетод вызывает 2 оператора вставки при объявлении@MyTransactionаннотация транзакции, обнаруженнаяNullPointerExceptionПросто откатываемся, и, наконец, мы выполняем операцию деления на 0, которая выдаетArithmeticException. Мы используем модульные тесты, чтобы увидеть, будут ли данные откатываться.

/**
 * @description:
 * @author: luozhou kinglaw1204@gmail.com
 * @create: 2020-03-29 22:05
 **/
@Service
public class MyTransactionTest implements TestService {
    @Autowired
    DataSourceConnectHolder holder;
		//一个事务中执行两个sql插入
   @MyTransaction(rollbackFor = NullPointerException.class)
    @Override
    public void saveTest(int id) {
        saveWitharamters(id, "luozhou@gmail.com");
        saveWitharamters(id + 10, "luozhou@gmail.com");
        int aa = id / 0;
    }
		//执行sql
   private void saveWitharamters(int id, String email) {
        String sql = "insert into tb_test values(?,?)";
        Connection connection = holder.getConnection();
        PreparedStatement stmt = null;
        try {
            stmt = connection.prepareStatement(sql);
            stmt.setInt(1, id);
            stmt.setString(2, email);
            stmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
}

4.5.2 Модульное тестирование

@SpringBootTest
@RunWith(SpringRunner.class)
class SpringTransactionApplicationTests {
    @Autowired
    private TestService service;

    @Test
    void contextLoads() throws SQLException {
        service.saveTest(1);
    }

}

Код цифры объявляет о делахNullPointerExceptionИсключение для отката, возникшее во время работыArithmeticExceptionЭто ненормально, поэтому не будет откатываться.Мы обновили базу данных справа и обнаружили, что данные были успешно вставлены нормально, что указывает на то, что они не были отброшены.

Мы меняем класс исключений отката наArithmeticException, чтобы очистить исходные данные и выполнить их снова, появляетсяArithmeticExceptionНенормально, в настоящее время в базу данных не добавлена ​​ни одна запись, что означает откат, что указывает на то, что наши аннотации работают.

5. Резюме

В начале этой статьи мы рассмотрели соответствующие знания о транзакциях, и параллельные транзакции приведут кгрязное чтение,потерянная модификация,неповторяемое чтение,галлюцинации, для решения этих задач в БД вводится уровень изоляции транзакции, а уровень изоляции включает в себя:читать незафиксированные,чтение фиксации,повторяемое чтениеисериализовать.

Концепция транзакции расширена в Spring.Чтобы решить транзакционную связь между методом A, методом B и методом C, введена концепция механизма распространения транзакций.

весной@TransactionalАннотированные транзакции реализуются в основном черезTransactionInterceptorПерехватчик реализуется, перехватывает целевой метод, а затем определяет, является ли исключение целевым исключением, если это целевое исключение, оно будет откатываться, иначе транзакция будет зафиксирована.

Наконец, мы написали свой собственный через JDBC в сочетании с AOP Spring.@MyTransactionalВ аннотации реализована функция отката при возникновении указанного исключения.