1 Database Transaction

1.1 What are database transactions

A database transaction is a sequence of database operations that access and may operate on various data items. These operations are either all executed or none executed, and are an indivisible unit of work. A transaction consists of all database operations performed between the beginning of a transaction and the end of a transaction.

It may be a bit abstract, but a database transaction is a guarantee that all operations will either succeed or fail within a single transaction (a series of database operation statements).

1.2 Why use transactions

So why use transactions? Take a classic example, when Xiao Ming transfers 100 yuan to his girlfriend, Xiao Hong, online, he usually first reduces his balance by 100 yuan and then adds 100 yuan to Xiao Hong’s balance. If there is no transaction guarantee or all success, or all failure, it will lead to Xiao Ming reduced 100 yuan, and Xiao Hong’s balance has not changed, it will not cause all kinds of complaints, the system can not be used.

Therefore, transactions are the most common in real life, and let’s take a look at transactions in Spring.

2 Spring Boot transactions

To facilitate the demo project construction, we choose Spring Boot + jdbcTemplate + mysql to illustrate.

2.1 the actual combat

2.1.1 Introducing dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<! Connect to MySQL database -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>
Copy the code

2.1.2 Define the table structure and initialize the data

Table structure:

CREATE TABLE `t_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'name',
  `age` int DEFAULT NULL COMMENT 'age',
  `follows` int DEFAULT NULL COMMENT 'Attention number',
  `fans` int DEFAULT NULL COMMENT 'Number of followers'.PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Copy the code

Initialize data:

INSERT INTO `t_user`(`id`, `name`, `age`, `follows`, `fans`) VALUES (1.'ghj'.18.0.0);
INSERT INTO `t_user`(`id`, `name`, `age`, `follows`, `fans`) VALUES (2.'zhangsan'.19.0.0);
Copy the code

2.1.3 Database Configuration

spring.datasource.url=jdbc:mysql://localhost:3306/test? serverTimezone=GMT%2B8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Copy the code

2.1.4 Compiling database Entity and related Service

Write the entity

@Data // Use the Lombok plug-in
public class User {
    private Long id;
    private String name;
    private Integer age;
    private Integer follows;
    private Integer fans;
}
Copy the code

Write the Service and implementation classes

public interface TransactionService {
    void followUser(a);
}
Copy the code
@Service
public class TransactionServiceImpl  implements TransactionService {
    @Autowired
    JdbcTemplate jdbcTemplate;

	/*** * User 2 pays attention to user 1 */
    @Override
    @Transactional
    public void followUser(a) {
        // User 2 follows + 1
        jdbcTemplate.update("update t_user set follows = 1 where id =2");
        int a = 1/0;
        // User 1 + 1
        jdbcTemplate.update("update t_user set fans = 1 where id =1"); }}Copy the code

2.1.5 Writing Test Cases

@SpringBootTest(classes = DemoApplication.class)
@WebAppConfiguration
public class TransactionServiceTest {
    @Autowired
    TransactionService transactionService;

    @Test
    public void test(a){
        System.out.println("test start");
        transactionService.followUser();
        System.out.println("test end"); }}Copy the code

Run, an error is reported

java.lang.ArithmeticException: / by zero
Copy the code

If you look at the database and all update statements fail, the transaction has taken effect

2.2 is it definitely effective

2.2.1 Catch RuntimeException in code

We will implement the class, catch the exception, and throw the business exception as follows

@Override
@Transactional
public void followUser(a) throws BizException {
    try{
        // User 2 follows + 1
        jdbcTemplate.update("update t_user set follows = 1 where id =2");
        int a = 1/0;
        // User 1 + 1
        jdbcTemplate.update("update t_user set fans = 1 where id =1");
    }catch (Exception e){
        e.printStackTrace();
        throw newBizException(); }}Copy the code

Business exception BizException is just a cover and is not handled in a special way

public class BizException extends Exception{}Copy the code

After the test case is executed, an exception occurs, but the first SQL takes effect, as shown in the following figure

Workaround: Specify rollbackFor as exception.class with the @Transactional annotation

2.2.2 Calling transaction methods internally

We will implement the class code, modified as follows:

@Service
public class TransactionServiceImpl  implements TransactionService {
    @Autowired
    JdbcTemplate jdbcTemplate;

    /*** * User 2 pays attention to user 1 */
    @Override
    public void followUser(a) throws BizException {
        followUser1();
    }

    @Transactional
    public void followUser1(a){
        // User 2 follows + 1
        jdbcTemplate.update("update t_user set follows = 1 where id =2");
        int a = 1/0;
        // User 1 + 1
        jdbcTemplate.update("update t_user set fans = 1 where id =1"); }}Copy the code

By calling the internal transaction method and running the test case, we found that the transaction did not take effect. The first SQL was executed again, and the database was as follows:

Solution: Add the @Transactional annotation to the followUser() method

2.3 the principle

The spring-boot-autoconfigure.jar file contains the EnableAutoConfiguration configuration in the/meta-INF /spring.factories file

As you can see, the spring is TransactionAutoConfiguration – boot initialization entry

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public TransactionManagerCustomizers platformTransactionManagerCustomizers( ObjectProvider
       
        > customizers)
       > {
		return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
	public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
		return TransactionalOperator.create(transactionManager);
	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
	public static class TransactionTemplateConfiguration {

		@Bean
		@ConditionalOnMissingBean(TransactionOperations.class)
		public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
			return newTransactionTemplate(transactionManager); }}@Configuration(proxyBeanMethods = false)
	@ConditionalOnBean(TransactionManager.class)
	@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
	public static class EnableTransactionManagementConfiguration {

		@Configuration(proxyBeanMethods = false)
		@EnableTransactionManagement(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = false)
		public static class JdkDynamicAutoProxyConfiguration {}@Configuration(proxyBeanMethods = false)
		@EnableTransactionManagement(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
		public static class CglibAutoProxyConfiguration {}}}Copy the code

Mainly need to analyze annotations and inner class @ ConditionalOnClass EnableTransactionManagementConfiguration (PlatformTransactionManager. Class) and inner class

@ ConditionalOnClass (PlatformTransactionManager. Class) is introducing the PlatformTransactionManager. This automatic configuration to take effect when the class, Since this class is in spring-tx.jar (the core package for Spring transactions), it was introduced and must have worked.

@EnableTransactionManagement

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

    //proxyTargetClass = false indicates that JDK dynamic proxy supports interface proxy. True indicates that the Cglib proxy supports subclass inheritance proxies.
    boolean proxyTargetClass(a) default false;

    // Transaction notification mode (tangential weaving), default proxy mode (interceptors for methods calling each other in the same class do not take effect), enhanced AspectJ is optional
    AdviceMode mode(a) default AdviceMode.PROXY;

    // Sort when there are multiple notifications on the join point, lowest by default. A larger value indicates a lower priority.
    int order(a) default Ordered.LOWEST_PRECEDENCE;

}
Copy the code

Focus on the class notes @ Import (TransactionManagementConfigurationSelector. Class)

public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {

	/**
	 * Returns {@link ProxyTransactionManagementConfiguration} or
	 * {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY}
	 * and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()},
	 * respectively.
	 */
	@Override
	protected String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			case PROXY:
				return new String[] {AutoProxyRegistrar.class.getName(),
						ProxyTransactionManagementConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {determineTransactionAspectClass()};
			default:
				return null; }}private String determineTransactionAspectClass(a) {
		return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ? TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME : TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME); }}Copy the code

As above, will eventually perform selectImports method into class needs to be loaded and we only see the proxy mode, load the AutoProxyRegistrar, ProxyTransactionManagementConfiguration2 class

AutoProxyRegistrar class

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

	private final Log logger = LogFactory.getLog(getClass());
    
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		boolean candidateFound = false;
		Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
		for (String annType : annTypes) {
			AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
			if (candidate == null) {
				continue;
			}
			Object mode = candidate.get("mode");
			Object proxyTargetClass = candidate.get("proxyTargetClass");
			if(mode ! =null&& proxyTargetClass ! =null && AdviceMode.class == mode.getClass() &&
					Boolean.class == proxyTargetClass.getClass()) {
				candidateFound = true;
				if (mode == AdviceMode.PROXY) {// Proxy mode
					AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
					if ((Boolean) proxyTargetClass) {/ / additional agent
						AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
						return; }}}}if(! candidateFound && logger.isInfoEnabled()) { String name = getClass().getSimpleName(); logger.info(String.format("%s was imported but no annotations were found " +
					"having both 'mode' and 'proxyTargetClass' attributes of type " +
					"AdviceMode and boolean respectively. This means that auto proxy " +
					"creator registration and configuration may not have occurred as " +
					"intended, and components may not be proxied as expected. Check to " +
					"ensure that %s has been @Import'ed on the same class where these " +
					"annotations are declared; otherwise remove the import of %s " +
					"altogether.", name, name, name)); }}}Copy the code

The proxy pattern AopConfigUtils. RegisterAutoProxyCreatorIfNecessary (registry); Final call is org. Springframework. Aop. Config. AopConfigUtils# registerOrEscalateApcAsRequired

private static BeanDefinition registerOrEscalateApcAsRequired( Class<? > cls, BeanDefinitionRegistry registry,@Nullable Object source) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
			BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
			if(! cls.getName().equals(apcDefinition.getBeanClassName())) {int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
				int requiredPriority = findPriorityForClass(cls);
                // If the subscript is greater than an existing internal automatic proxy constructor
				if(currentPriority < requiredPriority) { apcDefinition.setBeanClassName(cls.getName()); }}return null;
		}

		RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
		beanDefinition.setSource(source);
		beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
		beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
		registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
		return beanDefinition;
	}
Copy the code

FindPriorityForClass: APC_PRIORITY_LIST

private static finalList<Class<? >> APC_PRIORITY_LIST =newArrayList<Class<? > > ();/** * Priority increases list */
static {
    APC_PRIORITY_LIST.add(InfrastructureAdvisorAutoProxyCreator.class);
    APC_PRIORITY_LIST.add(AspectJAwareAdvisorAutoProxyCreator.class);
    APC_PRIORITY_LIST.add(AnnotationAwareAspectJAutoProxyCreator.class);
}
Copy the code

Since InfrastructureAdvisorAutoProxyCreator is built, through the postProcessAfterInitialization class enhancements

@Override
	public Object postProcessBeforeInstantiation(Class
        beanClass, String beanName) {
		Object cacheKey = getCacheKey(beanClass, beanName);

		if(! StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
			if (this.advisedBeans.containsKey(cacheKey)) {
				return null;
			}
			if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
				this.advisedBeans.put(cacheKey, Boolean.FALSE);
				return null; }}// Create proxy here if we have a custom TargetSource.
		// Suppresses unnecessary default instantiation of the target bean:
		// The TargetSource will handle target instances in a custom fashion.
		TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
		if(targetSource ! =null) {
			if (StringUtils.hasLength(beanName)) {
				this.targetSourcedBeans.add(beanName);
			}
			Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
			Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		return null;
	}
Copy the code

On face AutoProxyRegistrar finished class analysis, looking at the ProxyTransactionManagementConfiguration class

ProxyTransactionManagementConfiguration class

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

	@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {

		BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
		advisor.setTransactionAttributeSource(transactionAttributeSource);
        // The core transaction interceptor
		advisor.setAdvice(transactionInterceptor);
		if (this.enableTx ! =null) {
			advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
		}
		return advisor;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public TransactionAttributeSource transactionAttributeSource(a) {
		return new AnnotationTransactionAttributeSource();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    // transaction interceptor
	public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
		TransactionInterceptor interceptor = new TransactionInterceptor();
		interceptor.setTransactionAttributeSource(transactionAttributeSource);
		if (this.txManager ! =null) {
			interceptor.setTransactionManager(this.txManager);
		}
		returninterceptor; }}Copy the code

Looking directly at the TransactionInterceptor,

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
    // Work out the target class: may be {@code null}.
    // The TransactionAttributeSource should be passed the target class
    // as well as the method, which may be from an interface.Class<? > targetClass = (invocation.getThis() ! =null ? AopUtils.getTargetClass(invocation.getThis()) : null);

    // Adapt to TransactionAspectSupport's invokeWithinTransaction...
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

@Nullable
protected Object invokeWithinTransaction(Method method, @NullableClass<? > targetClass,final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		TransactionAttributeSource tas = getTransactionAttributeSource();
		finalTransactionAttribute txAttr = (tas ! =null ? tas.getTransactionAttribute(method, targetClass) : null);
		final TransactionManager tm = determineTransactionManager(txAttr);

		if (this.reactiveAdapterRegistry ! =null && tm instanceof ReactiveTransactionManager) {
			ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
				if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
					throw new TransactionUsageException(
							"Unsupported annotated transaction on suspending function detected: " + method +
							". Use TransactionalOperator.transactional extensions instead.");
				}
				ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
				if (adapter == null) {
					throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
							method.getReturnType());
				}
				return new ReactiveTransactionSupport(adapter);
			});
			return txSupport.invokeWithinTransaction(
					method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
		}

		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null| |! (ptminstanceof CallbackPreferringPlatformTransactionManager)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

			Object retVal;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
                // Core method
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
                // Operation after error
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}

			if(retVal ! =null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
				// Set rollback-only in case of Vavr failure matching our rollback rules...
				TransactionStatus status = txInfo.getTransactionStatus();
				if(status ! =null&& txAttr ! =null) {
					retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
				}
			}

			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

		else {
			Object result;
			final ThrowableHolder throwableHolder = new ThrowableHolder();

			// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
			try {
				result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
					TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
					try {
						Object retVal = invocation.proceedWithInvocation();
						if(retVal ! =null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
							// Set rollback-only in case of Vavr failure matching our rollback rules...
							retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
						}
						return retVal;
					}
					catch (Throwable ex) {
						if (txAttr.rollbackOn(ex)) {
							// A RuntimeException: will lead to a rollback.
							if (ex instanceof RuntimeException) {
								throw (RuntimeException) ex;
							}
							else {
								throw newThrowableHolderException(ex); }}else {
							// A normal return value: will lead to a commit.
							throwableHolder.throwable = ex;
							return null; }}finally{ cleanupTransactionInfo(txInfo); }}); }catch (ThrowableHolderException ex) {
				throw ex.getCause();
			}
			catch (TransactionSystemException ex2) {
				if(throwableHolder.throwable ! =null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
					ex2.initApplicationException(throwableHolder.throwable);
				}
				throw ex2;
			}
			catch (Throwable ex2) {
				if(throwableHolder.throwable ! =null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
				}
				throw ex2;
			}

			// Check result state: It might indicate a Throwable to rethrow.
			if(throwableHolder.throwable ! =null) {
				throw throwableHolder.throwable;
			}
			returnresult; }}Copy the code

2.3.1 For catching RuntimeException

Through the above source code, can know the method of transaction rollback is TransactionAspectSupport# completeTransactionAfterThrowing

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
		if(txInfo ! =null&& txInfo.getTransactionStatus() ! =null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
						"] after exception: " + ex);
			}
			if(txInfo.transactionAttribute ! =null && txInfo.transactionAttribute.rollbackOn(ex)) {
				try {
					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
				}
				catch (TransactionSystemException ex2) {
					logger.error("Application exception overridden by rollback exception", ex);
					ex2.initApplicationException(ex);
					throw ex2;
				}
				catch (RuntimeException | Error ex2) {
					logger.error("Application exception overridden by rollback exception", ex);
					throwex2; }}else {
				// We don't roll back on this exception.
				// Will still roll back if TransactionStatus.isRollbackOnly() is true.
				try {
					txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
				}
				catch (TransactionSystemException ex2) {
					logger.error("Application exception overridden by commit exception", ex);
					ex2.initApplicationException(ex);
					throw ex2;
				}
				catch (RuntimeException | Error ex2) {
					logger.error("Application exception overridden by commit exception", ex);
					throwex2; }}}}Copy the code

Can see txInfo. TransactionAttribute. RollbackOn (ex) to true for rollback, and the method

@Override
public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}
Copy the code

Therefore, only errors and RuntimeExceptions are rolled back

2.3.2 For internal invocation

Transactional Transactional methods (AOP) without the @Transactional annotation call methods of the Transactional class that have not been acquired by the Transactional enhanced proxy class and therefore commit directly.

2.4 Other scenarios that do not take effect

  • MyISAM engine does not support transactions. InnoDB is the engine that supports transactions. InnoDB is used to support transactions
  • Transactions are not handed over to Spring for management
  • Set the Spring transaction level to NOT_SUPPORTED not to support transactions and suspend the transaction if one is currently available

3 conclusion

The most important thing is that we need to understand how Spring transactions work, and only if we understand how they work can we know how to solve them if they don’t work anymore.