百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Spring事务的介绍,以及基于注解@Transactional的声明式事务

ccwgpt 2024-11-25 10:23 81 浏览 0 评论

前言

事务是一个非常重要的知识点,前面的文章已经有介绍了关于SpringAOP代理的实现过程;事务管理也是AOP的一个重要的功能。

事务的基本介绍

数据库事务特性:

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

事务的隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

  • 读未提交(READ UNCOMMITTED)
  • 读已提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 串行化(SERIALIZABLE)

至于为什么会设定数据库的隔离级别,原因是由于在并发操作数据库的时候可能会引起脏读、不可重复读、幻读、第一类丢失更新、第二类更新丢失等现象。

脏读:

事物A读取事物B尚未提交的更改数据,并做了修改;此时如果事物B回滚,那么事物A读取到的数据是无效的,此时就发生了脏读。

不可重复读:

一个事务执行相同的查询两次或两次以上,每次都得到不同的数据。如:A事物下查询账户余额,此时恰巧B事物给账户里转账100元,A事物再次查询账户余额,那么A事物的两次查询结果是不一致的。

幻读:

A事物读取B事物提交的新增数据,此时A事物将出现幻读现象。幻读与不可重复读容易混淆,如何区分呢?幻读是读取到了其他事物提交的新数据,不可重复读是读取到了已经提交事物的更改数据(修改或删除)

第一类丢失更新现象:

撤销一个事务的时候,把其它事务已提交的更新数据覆盖了。这是完全没有事务隔离级别造成的。如果事务1被提交,另一个事务被撤销,那么会连同事务1所做的更新也被撤销。

第二类丢失更新现象:

它和不可重复读本质上是同一类并发问题,通常将它看成不可重复读的特例。当两个或多个事务查询相同的记录,然后各自基于查询的结果更新记录时会造成第二类丢失更新问题。每个事务不知道其它事务的存在,最后一个事务对记录所做的更改将覆盖其它事务之前对该记录所做的更改。

针对以上问题,其实可以有其它的解决方法,设置数据库隔离级别就是其中的一种,简单说一下数据库四个隔离级别的作用,见下表

简单总结:

  • Read Uncommitted存在:脏读、不可重复读、第二类丢失更新和幻读问题。
  • Read committed存在:不可重复读、第二类丢失更新和幻读问题。
  • Repeatable Read存在:幻读问题。
  • Serializable 不存在问题。

接下来我们看一下Spring支持事务的核心接口:

概要图:

TransactionDefinition

  • 看源码(TransactionDefinition.java)
public interface TransactionDefinition {

	/**
	 * 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中。
	 */
	int PROPAGATION_REQUIRED = 0;

	/**
	 * 支持当前事物,如果当前没有事物,则以非事物方式执行。
	 */
	int PROPAGATION_SUPPORTS = 1;

	/**
	 * 使用当前事物,如果当前没有事物,则抛出异常
	 */
	int PROPAGATION_MANDATORY = 2;
	/**
	 * 新建事物,如果当前已经存在事物,则挂起当前事物。
	 */
	int PROPAGATION_REQUIRES_NEW = 3;

	/**
	 * 以非事物方式执行,如果当前存在事物,则挂起当前事物。
	 */
	int PROPAGATION_NOT_SUPPORTED = 4;
	/**
	 * 以非事物方式执行,如果当前存在事物,则抛出异常。
	 */
	int PROPAGATION_NEVER = 5;
	/**
	 * 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同
	 */
	int PROPAGATION_NESTED = 6;

	/**
	 * 使用后端数据库默认的隔离级别。
	 */
	int ISOLATION_DEFAULT = -1;

	/**
	 * READ_UNCOMMITTED 隔离级别
	 */
	int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

	/**
	 *  READ_COMMITTED 隔离级别
	 */
	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;

	/**
	 * SERIALIZABLE 隔离级别
	 */
	int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


	/**
	 * 默认超时时间
	 */
	int TIMEOUT_DEFAULT = -1;

	/**
	 *  获取事物传播特性
	 * @return
	 */
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}
	/**
	 * 获取事物隔离级别
	 * @return
	 */
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}
	/**
	 * 获取事物超时时间
	 * @return
	 */
	default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

	/**
	 * 判断事物是否可读
	 * @return
	 */
	default boolean isReadOnly() {
		return false;
	}

	/**
	 *  获取事物名称
	 * @return
	 */
	@Nullable
	default String getName() {
		return null;
	}

	static TransactionDefinition withDefaults() {
		return StaticTransactionDefinition.INSTANCE;
	}

}

Spring事务传播行为

事务传播行为类型

说明

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY

使用当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER

以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

Spring支持的隔离级别

隔离级别

描述

DEFAULT

使用数据库本身使用的隔离级别 ORACLE(读已提交) MySQL(可重复读)

READ_UNCOMMITTED

读未提交(脏读)最低的隔离级别,一切皆有可能。

READ_COMMITTEN

读已提交,ORACLE默认隔离级别,有幻读以及不可重复读风险。

REPEATABLE_READ

可重复读,解决不可重复读的隔离级别,但还是有幻读风险。MySQL默认隔离级别

SERLALIZABLE

串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了

Spring事务基础结构中的中心接口(PlatformTransactionManager.JAVA)

  • 看源码
/**
 * Spring事务基础结构中的中心接口
 */
public interface PlatformTransactionManager extends TransactionManager {

	/**
	 * 根据指定的传播行为,返回当前活动的事务或创建新事务。
	 * @param definition
	 * @return
	 * @throws TransactionException
	 */
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
	throws TransactionException;

	/**
	 * 就给定事务的状态提交给定事务
	 * @param status
	 * @throws TransactionException
	 */
	void commit(TransactionStatus status) throws TransactionException;
	/**
	 * 执行给定事务的回滚
	 * @param status
	 * @throws TransactionException
	 */
	void rollback(TransactionStatus status) throws TransactionException;

}
  • 源码分析Spring将事务管理委托给底层的持久化框架来完成,因此,Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现类,我们看一下具体有哪些事务管理器:

简单说一下图中标注的几个事务管理器:

事务管理器

描述

DataSourceTransactionManager

提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理

JpaTransactionManager

提供对单个javax.persistence.EntityManagerFactory事务支持,用于集成JPA实现框架时的事务管理

JtaTransactionManager

提供对分布式事务管理的支持,并将事务管理委托给Java EE应用服务器事务管理器

看完事务管理器,我们看一下概览图中的第三部分TransactionStatus事务状态描述接口类

Spring事务状态描述

  • 看源码(TransactionStatus.java)
// 事务状态描述
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

	/**
	 * 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。
	 * @return
	 */
	boolean hasSavepoint();
	/**
	 *  将会话刷新到数据存储区
	 */
	@Override
	void flush();

}



继续看一下它的父类TransactionExecution.java 和SavepointManager.java

public interface TransactionExecution {
	/**
	 * 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行)
	 * @return
	 */
	boolean isNewTransaction();
	/**
	 * 设置事务仅回滚。
	 */
	void setRollbackOnly();
	/**
	 * 返回事务是否已标记为仅回滚
	 * @return
	 */
	boolean isRollbackOnly();
	/**
	 * 返回事物是否已经完成,无论提交或者回滚。
	 * @return
	 */
	boolean isCompleted();

}
public interface SavepointManager {
	/**
	 * 创建一个新的保存点。
	 * @return
	 * @throws TransactionException
	 */
	Object createSavepoint() throws TransactionException;
	/**
	 * 回滚到给定的保存点。
	 *   注意:调用此方法回滚到给定的保存点之后,不会自动释放保存点,
	 *        可以通过调用releaseSavepoint方法释放保存点。
	 * @param savepoint
	 * @throws TransactionException
	 */
	void rollbackToSavepoint(Object savepoint) throws TransactionException;
	/**
	 * 显式释放给定的保存点。(大多数事务管理器将在事务完成时自动释放保存点)
	 * @param savepoint
	 * @throws TransactionException
	 */
	void releaseSavepoint(Object savepoint) throws TransactionException;

}

到这里Spring的事务相关概念已经大概介绍完了,我们先来熟悉一下Spring的编程式事务的应用实例:

Spring编程式事务

package com.vipbbo.spring.transaction;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

public class MyTransaction {

	private JdbcTemplate jdbcTemplate;
	private DataSourceTransactionManager transactionManager;
	private DefaultTransactionDefinition transactionDefinition;
	private String insertSql = "insert into account (balance) values ('100')";

	public void save(){
		// 初始化jdbcTemplate
		DataSource dataSource = getDataSource();
		jdbcTemplate = new JdbcTemplate(dataSource);

		// 创建事务管理器
		transactionManager = new DataSourceTransactionManager();
		transactionManager.setDataSource(dataSource);

		// 定义事务属性
		transactionDefinition = new DefaultTransactionDefinition();
		transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

		// 开启事务
		TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

		// 执行业务逻辑
		try {
			jdbcTemplate.execute(insertSql);
			//int i = 1/0;
			jdbcTemplate.execute(insertSql);
			transactionManager.commit(transactionStatus);
		} catch (DataAccessException e) {
			e.printStackTrace();
		} catch (TransactionException e) {
			transactionManager.rollback(transactionStatus);
			e.printStackTrace();
		}

	}

	public DataSource getDataSource(){
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUrl("jdbc:mysql://192.168.1.100:3306/spring_aop?" +
				"useSSL=false&useUnicode=true&characterEncoding=UTF-8");
		dataSource.setUsername("root");
		dataSource.setPassword("root");
		return dataSource;
	}
}

测试类:

package com.vipbbo.spring.transaction;

import org.junit.jupiter.api.Test;

public class MyTransactionTest {

	@Test
	public void test1() {
		MyTransaction myTransaction = new MyTransaction();
		myTransaction.save();
	}
}

分析

运行测试类,一旦放开int i = 1/0;这段代码,再抛出异常之后手动回滚事务,所以数据库表不会增加记录。

基于@Transactional注解的声明式事务

其底层建立在`AOP`的基础之上,对方法前后进行拦截,然后在目标方法开始之前创建一个或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。通过声明式事务,无需在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相应的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。

非XMl方式配置声明式事务



package com.vipbbo.spring.transaction;


import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(propagation = Propagation.REQUIRED)
public interface AccountByAnnotationService {
	void save() throws RuntimeException;
}

实现类:

package com.vipbbo.spring.transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * 账户接口实现
 *
 * @author paidaxing
 */
@Service("accountByAnnotationService")
public class AccountByAnnotationServiceImpl implements AccountByAnnotationService {


	@Autowired
	private JdbcTemplate jdbcTemplate;
	private static String insertSql = "insert into account(balance) values (100)";

	@Override
	public void save() throws RuntimeException {
		System.out.println("======开始执行sql======");
		jdbcTemplate.execute(insertSql);
		System.out.println("======sql执行结束======");
		System.out.println("======准备抛出异常======");
		throw new RuntimeException("手动抛出异常");
	}

}

注解开启声明式事务

配置类

package com.vipbbo.spring.config;


import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.net.ProtocolException;

@ComponentScan(basePackages = {"com.vipbbo"})
@Configuration
//开启基于注解的声明式事务
@EnableTransactionManagement
public class SpringConfig {


	/**
	 * 注解数据源
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public DataSource dataSource() throws ProtocolException {
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUsername("root");
		dataSource.setPassword("root");
		dataSource.setUrl("jdbc:mysql://192.168.1.100:3306/spring_aop?" +
				"useSSL=false&useUnicode=true&characterEncoding=UTF-8");
		return dataSource;
	}

	/**
	 * 注册JdbcTemplate
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public JdbcTemplate jdbcTemplate() throws ProtocolException{
		// 两种方式获取DataSOurce
		//1. 直接在方法上放置参数 public JdbcTemplate jdbcTemplate(DataSource dataSource)
		// 默认会去容器去取
		// 2. 如下: 调用上面的方法
		//spring对@Configuration类有特殊处理,注册组件的方法多次调用只是在IOC容器中找组件
		return new JdbcTemplate(dataSource());
	}

	/**
	 * 注册事务管理器
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public PlatformTransactionManager transactionManager() throws ProtocolException{
		//需要传入dataSource
		return new DataSourceTransactionManager(dataSource());
	}
}

测试代码类

package com.vipbbo.spring.transaction;

import com.vipbbo.spring.config.SpringConfig;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MyTransactionByAnnotationTest {

	@Test
	public void test(){
		ApplicationContext tx = new AnnotationConfigApplicationContext(SpringConfig.class);
		AccountByAnnotationService annotationService =
				tx.getBean("accountByAnnotationService",AccountByAnnotationService.class);
		annotationService.save();
	}
}

测试类运行截图:


我们在上述实现类中手动抛出了一个异常,Spring会自动回滚事务,我们查看数据库可以知道并没有新增数据。。

注意重中之重

默认情况下Spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果

相关推荐

十分钟让你学会LNMP架构负载均衡(impala负载均衡)

业务架构、应用架构、数据架构和技术架构一、几个基本概念1、pv值pv值(pageviews):页面的浏览量概念:一个网站的所有页面,在一天内,被浏览的总次数。(大型网站通常是上千万的级别)2、u...

AGV仓储机器人调度系统架构(agv物流机器人)

系统架构层次划分采用分层模块化设计,分为以下五层:1.1用户接口层功能:提供人机交互界面(Web/桌面端),支持任务下发、实时监控、数据可视化和报警管理。模块:任务管理面板:接收订单(如拣货、...

远程热部署在美团的落地实践(远程热点是什么意思)

Sonic是美团内部研发设计的一款用于热部署的IDEA插件,本文其实现原理及落地的一些技术细节。在阅读本文之前,建议大家先熟悉一下Spring源码、SpringMVC源码、SpringBoot...

springboot搭建xxl-job(分布式任务调度系统)

一、部署xxl-job服务端下载xxl-job源码:https://gitee.com/xuxueli0323/xxl-job二、导入项目、创建xxl_job数据库、修改配置文件为自己的数据库三、启动...

大模型:使用vLLM和Ray分布式部署推理应用

一、vLLM:面向大模型的高效推理框架1.核心特点专为推理优化:专注于大模型(如GPT-3、LLaMA)的高吞吐量、低延迟推理。关键技术:PagedAttention:类似操作系统内存分页管理,将K...

国产开源之光【分布式工作流调度系统】:DolphinScheduler

DolphinScheduler是一个开源的分布式工作流调度系统,旨在帮助用户以可靠、高效和可扩展的方式管理和调度大规模的数据处理工作流。它支持以图形化方式定义和管理工作流,提供了丰富的调度功能和监控...

简单可靠高效的分布式任务队列系统

#记录我的2024#大家好,又见面了,我是GitHub精选君!背景介绍在系统访问量逐渐增大,高并发、分布式系统成为了企业技术架构升级的必由之路。在这样的背景下,异步任务队列扮演着至关重要的角色,...

虚拟服务器之间如何分布式运行?(虚拟服务器部署)

  在云计算和虚拟化技术快速发展的今天,传统“单机单任务”的服务器架构早已难以满足现代业务对高并发、高可用、弹性伸缩和容错容灾的严苛要求。分布式系统应运而生,并成为支撑各类互联网平台、企业信息系统和A...

一文掌握 XXL-Job 的 6 大核心组件

XXL-Job是一个分布式任务调度平台,其核心组件主要包括以下部分,各组件相互协作实现高效的任务调度与管理:1.调度注册中心(RegistryCenter)作用:负责管理调度器(Schedule...

京东大佬问我,SpringBoot中如何做延迟队列?单机与分布式如何做?

京东大佬问我,SpringBoot中如何做延迟队列?单机如何做?分布式如何做呢?并给出案例与代码分析。嗯,用户问的是在SpringBoot中如何实现延迟队列,单机和分布式环境下分别怎么做。这个问题其实...

企业级项目组件选型(一)分布式任务调度平台

官网地址:https://www.xuxueli.com/xxl-job/能力介绍架构图安全性为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯;调度中心和执...

python多进程的分布式任务调度应用场景及示例

多进程的分布式任务调度可以应用于以下场景:分布式爬虫:importmultiprocessingimportrequestsdefcrawl(url):response=re...

SpringBoot整合ElasticJob实现分布式任务调度

介绍ElasticJob是面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目ElasticJob-Lite和ElasticJob-Cloud组成。它通过弹性调度、资源管控、...

分布式可视化 DAG 任务调度系统 Taier 的整体流程分析

Taier作为袋鼠云的开源项目之一,是一个分布式可视化的DAG任务调度系统。旨在降低ETL开发成本,提高大数据平台稳定性,让大数据开发人员可以在Taier直接进行业务逻辑的开发,而不用关...

SpringBoot任务调度:@Scheduled与TaskExecutor全面解析

一、任务调度基础概念1.1什么是任务调度任务调度是指按照预定的时间计划或特定条件自动执行任务的过程。在现代应用开发中,任务调度扮演着至关重要的角色,它使得开发者能够自动化处理周期性任务、定时任务和异...

取消回复欢迎 发表评论: