登录 |  注册
首页 >  编程技术 >  Spring Boot入门基础教程代码实例 >  Spring Boot注解事务及代码演示

Spring Boot注解事务及代码演示

1. 前言

事务管理是Spring中的一个非常重要的知识。本节介绍Spring的编程式事务管理和声明式事务管理的应用场景和实现方式!希望通过本门课程的学习,使得小伙伴们可以在开发中灵活地应用事务。

2. 实例场景

在 Spring Boot 中使用事务非常简单,本小节我们通过商品扣减库存、生成订单的实例,演示下 Spring Boot 中使用事务的具体流程。

3. 数据库模块实现

需要有一个商品表,保存商品的唯一标识、名称、库存数量,结构如下:

实例:

CREATE TABLE `goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
  `name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  `num` bigint(255) DEFAULT NULL COMMENT '库存数量',
  PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

购买商品后还需要生成订单,保存订单唯一标识、购买商品的 id 、购买数量。

实例:

CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
  `goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
  `count` bigint(20) DEFAULT NULL COMMENT '购买数量',
  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4. Spring Boot 后端实现

接下来,我们开始开发 Spring Boot 后端项目,并且使用事务实现扣减库存、生成订单功能。数据库访问部分使用比较流行的 MyBatis 框架。

4.1 使用 Spring Initializr 创建项目

Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-transaction,生成项目后导入 Eclipse 开发环境。

4.2 引入项目依赖

我们引入热部署依赖、 Web 依赖、数据库访问相关依赖及测试相关依赖,具体如下:

实例:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<!-- 热部署 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
		</dependency>
		<!-- Web支持 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- JDBC -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<!-- MySQL驱动 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<!-- 集成MyBatis -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.2</version>
		</dependency>
		<!-- junit -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- 测试 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

4.3 数据源配置

修改 application.properties 文件,配置数据源信息。

实例:

# 配置数据库驱动spring.datasource.driver-class-name=com.mysql.jdbc.Driver# 配置数据库urlspring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC# 配置数据库用户名spring.datasource.username=root# 配置数据库密码spring.datasource.password=Easy@0122

4.4 开发数据对象类

开发 goods 表对应的数据对象类 GoodsDo ,代码如下:

实例:

/**
* 商品类
*/public class GoodsDo {
   /**
    * 商品id
    */
   private Long id;
   /**
    * 商品名称
    */
   private String name;
   /**
    * 商品库存
    */
   private Long num;
   // 省略 get set}

然后开发 order 表对应的数据对象类 OrderDo,代码如下:

实例:

/**
 * 订单类
 */public class OrderDo {
	/**
	 * 订单id
	 */
	private Long id;
	/**
	 * 商品id
	 */
	private Long goodsId;
	/**
	 * 购买数量
	 */
	private Long count;
	// 省略 get set}

4.5 开发数据访问层

首先定义商品数据访问接口,实现查询剩余库存与扣减库存功能。

实例:

/**
 * 商品数据库访问接口
 */@Repository // 标注数据访问组件public interface GoodsDao {
	/**
	 * 查询商品信息(根据id查询单个商品信息)
	 */
	public GoodsDo selectForUpdate(Long id);

	/**
	 * 修改商品信息(根据id修改其他属性值)
	 */
	public int update(GoodsDo Goods);}

注意,在查询商品剩余库存时,我们采用面向对象的方法,将对应 id 的商品信息全部取出,更加方便点。采用 selectForUpdate 命名,表示该方法使用了 select ... for update 的 SQL 语句查询方式,以锁定数据库对应记录,规避高并发场景下库存修改错误问题。同样 update 方法也采用了面向对象的方式,根据 id 修改其他信息,方便复用。

然后定义订单数据访问接口,实现生成订单的功能。

实例:

/**
* 订单数据库访问接口
*/@Repository // 标注数据访问组件
public interface OrderDao {
   /**
    * 新增订单
    */
   public int insert(OrderDo order);}

然后,我们修改 Spring Boot 配置类,添加 @MapperScan 注解,扫描数据访问接口所在的包。

实例:

@SpringBootApplication
@MapperScan("com.imooc.springboottransaction") // 指定MyBatis扫描的包,以便将数据访问接口注册为Beanpublic class SpringBootTransactionApplication {
public static void main(String[] args) {
		SpringApplication.run(SpringBootTransactionApplication.class, args);
	}}

4.6 添加 MyBatis 映射文件

编写 GoodsDao 、 OrderDao 对应的映射文件, 首先我们通过 application.properties 指定映射文件的位置:

实例:

# 指定MyBatis配置文件位置mybatis.mapper-locations=classpath:mapper/*.xml

然后在 resources/mapper 目录下新建 GoodsMapper.xml 文件,该文件就是 goods 表对应的映射文件,内容如下:

实例:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
                        "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 
<!-- 本映射文件对应GoodsDao接口 -->
<mapper namespace="com.imooc.springboottransaction.GoodsDao">
   <!-- 对应GoodsDao中的selectForUpdate方法 -->
   <select id="selectForUpdate" resultMap="resultMapBase" parameterType="java.lang.Long">
   	select <include refid="sqlBase" /> from goods where id = #{id} for update   
    </select>
    
   <!-- 对应GoodsDao中的update方法 -->
   <update id="update" parameterType="com.imooc.springboottransaction.GoodsDo">
   	update goods set name=#{name},num=#{num} where id=#{id}   
   </update>
   
   <!-- 可复用的sql模板 -->
   <sql id="sqlBase">
   	id,name,num   </sql>
   <!-- 保存SQL语句查询结果与实体类属性的映射 -->
   <resultMap id="resultMapBase" type="com.imooc.springboottransaction.GoodsDo">
   	<id column="id" property="id" />
   	<result column="name" property="name" />
   	<result column="num" property="num" />
   </resultMap></mapper>

同样我们在 resources/mapper 目录下新建 OrderMapper.xml 文件,该文件是 order 表对应的映射文件,内容如下:

实例:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!-- 本映射文件对应OrderDao接口 --><mapper namespace="com.imooc.springboottransaction.OrderDao">
   <!-- 对应OrderDao中的insert方法 -->
   <insert id="insert" parameterType="com.imooc.springboottransaction.OrderDo">
   	insert into `order` (goods_id,count) values (#{goodsId},#{count})   
   </insert>
</mapper>

4.7 编写服务方法

下单这个操作,可以封装为一个服务方法,不管是手机端下单还是电脑端下单都可以调用。

我们新建订单服务类 OrderService ,并在其中实现下单方法 createOrder ,代码如下:

实例:

/**
 * 订单服务类
 */@Service // 注册为服务类public class OrderService {
	@Autowired
	private GoodsDao goodsDao;
	@Autowired
	private OrderDao orderDao;

	/**
	 * 下单
	 * 
	 * @param goodsId 购买商品id
	 * @param count   购买商品数量
	 * @return 生成订单数
	 */
	@Transactional // 实现事务
	public int createOrder(Long goodsId, Long count) {
		// 锁定商品库存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣减库存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		// 生成订单
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}}

我们在 createOrder 方法上添加了 @Transactional 注解,该注解为 createOrder 方法开启了事务,当方法结束时提交事务。这样保证了 createOrder 内方法全部执行成功,或者全部失败。

5. 测试

5.1 构造测试数据

在数据库中构造一条测试数据如下:

图片描述

测试数据

5.2 正常测试

编写测试方法发起测试:

实例:

/**
 * 订单测试
 */@SpringBootTestclass OrderTest {

	@Autowired
	private OrderService orderService;

	/**
	 * 新增一个商品
	 */
	@Test
	void testCreateOrder() {
		// 购买id为1的商品1份
		int affectRows = orderService.createOrder(1L, 1L);
		assertEquals(1, affectRows);
	}}

运行测试方法后,手机的库存变为 19 ,且生成一条订单记录,测试通过,具体结果如下:

图片描述

正常测试结果

5.3 模拟异常测试

修改下单方法,在扣减库存后抛出异常,看看事务能否回滚到修改全部未发生的状态。为了便于测试我们将库存重新设为 20 ,然后将下单方法修改如下:

实例:

	@Transactional // 实现事务
	public int createOrder(Long goodsId, Long count) {
		// 锁定商品库存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣减库存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		// 模拟异常
		int a=1/0;
		// 生成订单
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}

运行测试方法后,抛出异常,查看数据库发现,库存还是 20 ,说明 goodsDao.update(goods); 的修改没有提交到数据库,具体结果如下:

图片描述

模拟异常测试结果

6. 使用注意事项

Spring 事务在一些情况下不能生效,需要特别注意。

6.1 抛出检查型异常时事务失效

首先了解下异常类型:

  • Exception 受检查的异常:在程序中必须使用 try…catch 进行处理,遇到这种异常不处理,编译器会报错。例如 IOException 。

  • RuntimeException 非受检查的异常:可以不使用 try…catch 进行处理。例如常见的 NullPointerException 。

在大多数人潜意识中,只要发生异常,事务就应该回滚,实际上使用 @Transactional 时,默认只对非受检查异常回滚。例如:

实例:

	@Transactional // 实现事务
	public int createOrder(Long goodsId, Long count) {
		// 锁定商品库存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣减库存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		if (count > goods.getNum()) {
			// 非受检查异常抛出时,会回滚
			throw new RuntimeException();
		}
		// 生成订单
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}

实例:

   @Transactional // 实现事务
   public int createOrder(Long goodsId, Long count) throws Exception {
   	// 锁定商品库存
   	GoodsDo goods = goodsDao.selectForUpdate(goodsId);
   	// 扣减库存
   	Long newNum = goods.getNum() - count;
   	goods.setNum(newNum);
   	goodsDao.update(goods);
   	if (count > goods.getNum()) {
   		//注意!此处为受检查的异常,就算抛出也不会回滚
   		throw new Exception();
   	}
   	// 生成订单
   	OrderDo order = new OrderDo();
   	order.setGoodsId(goodsId);
   	order.setCount(count);
   	int affectRows = orderDao.insert(order);
   	return affectRows;
   }

如果想实现只要抛出异常就回滚,可以通过添加注解 @Transactional(rollbackFor=Exception.class) 实现。

实例:

	@Transactional(rollbackFor = Exception.class) // 抛出异常即回滚
	public int createOrder(Long goodsId, Long count) throws Exception {
		// 锁定商品库存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣减库存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		if (count > goods.getNum()) {
			throw new Exception();
		}
		// 生成订单
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}

OK,我们将在测试类中,将购买数量设为大于库存数量的 100 ,然后一次测试上面三种情况,就能验证上面的说法了。

实例:

/**
 * 订单测试
 */@SpringBootTestclass OrderTest {

	@Autowired
	private OrderService orderService;

	/**
	 * 创建订单测试
	 */
	@Test
	void testCreateOrder() throws Exception {
		// 购买id为1的商品1份
		int affectRows = orderService.createOrder(1L, 100L);
		assertEquals(1, affectRows);
	}}

6.2 一个事务方法调用另一个事务方法时失效

先看下面的实例,我们修改下 OrderService 类,通过一个事务方法调用 createOrder 方法。

实例:

/**
 * 订单服务类
 */@Service // 注册为服务类public class OrderService {
	@Autowired
	private GoodsDao goodsDao;
	@Autowired
	private OrderDao orderDao;

	@Transactional // 开启事务
	public int startCreateOrder(Long goodsId, Long count) throws Exception {
		return this.createOrder(goodsId, count);
	}

	/**
	 * 下单
	 * 
	 * @param goodsId 购买商品id
	 * @param count   购买商品数量
	 * @return 生成订单数
	 */
	@Transactional(rollbackFor = Exception.class) // 抛出异常即回滚
	public int createOrder(Long goodsId, Long count) throws Exception {
		// 锁定商品库存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣减库存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		if (count > goods.getNum()) {
			// 非受检查异常抛出时,会回滚
			throw new Exception();
		}
		// 生成订单
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}}

此时我们在测试类中通过 startCreateOrder 方法再去调用 createOrder 方法,代码如下:

实例:

/**
 * 订单测试
 */@SpringBootTestclass OrderTest {

	@Autowired
	private OrderService orderService;

	/**
	 * 创建订单测试
	 */
	@Test
	void testCreateOrder() throws Exception {
		// 购买id为1的商品1份
		int affectRows = orderService.startCreateOrder(1L, 100L);
		assertEquals(1, affectRows);
	}}

startCreateOrder 和 createOrder 方法都是事务方法,且这两个方法事务特性不同 (一个没有 rollbackFor=Exception.class),如果我们调用 startTransaction 方法,则 createOrder 中的事务并不会生效。

也就是说,如果在同一个类中,一个事务方法调用另一个事务方法,可能会导致被调用的事务方法的事务失效!

这是因为 Spring 的声明式事务使用了代理,具体机制此处不再探讨,但是一定要注意规避这种事务失效的场景。

7. 小结

spring事务能力的支撑用到了很多知识,动态代理、AOP、反射、后置处理器等等,总的来说就是应用启动时为需要使用事务的类生成代理类,以及将事务能力(拦截逻辑)织入进去,在实例化的时候调用后置处理器的逻辑,将代理类实例化替代目标类,并放入上下文容器中,在实际调用目标类事务方法的时候,被代理类中ReflectiveMethodInvocation拦截,然后先调用拦截器中的事务逻辑,然后再调用目标类的业务逻辑,最后处理异常回滚和提交,看起来比较简单,但是框架层面提供了非常庞大的基础组件来支撑和实现事务能力,当然这些基础组件大部分都会复用,比如AOP和动态代理,在异步和缓存场景下都会用到,包括我们自己扩展一些能力出来的时候也会用到。

上一篇: Spring Boot整合MyBatis依赖包
下一篇: Spring Boot整合多数据源
推荐文章
  • 在HTML中,如果你想让一个输入框(input元素)不可编辑,你可以通过设置其readonly属性来实现。示例如下:input type="text" value="此处内容不可编辑" readonly在上述代码中,readonly属性使得用户无法修改输入框中的内容。另外,如果你希望输入框完全不可交
  • ASP.NET教程ASP.NET又称为ASP+,基于.NETFramework的Web开发平台,是微软公司推出的新一代脚本语言。ASP.NET是一个使用HTML、CSS、JavaScript和服务器脚本创建网页和网站的开发框架。ASP.NET支持三种不一样的开发模式:WebPages(Web页面)、
  • C# 判断判断结构要求程序员指定一个或多个要评估或测试的条件,以及条件为真时要执行的语句(必需的)和条件为假时要执行的语句(可选的)。下面是大多数编程语言中典型的判断结构的通常形式:判断语句C#提供了以下类型的判断语句。点击链接查看每个语句的细节。语句描述if语句一个 if语句 由一个布尔表达式后跟
  • C#循环有的时候,可能需要多次执行同一块代码。通常情况下,语句是顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。编程语言提供了允许更为复杂的执行路径的多种控制结构。循环语句允许我们多次执行一个语句或语句组,下面是大多数编程语言中循环语句的通常形式:循环类型C#提供了以下几种循环类型
  • C#数组(Array)数组是一个存储相同类型元素的固定大小的顺序集合。数组是用来存储数据的集合,一般认为数组是一个同一类型变量的集合。声明数组变量并不是声明number0、number1、...、number99一个个单独的变量,而是声明一个就像numbers这样的变量,然后使用numbers[0]
  • ASP.NET是一个由微软公司开发的用于构建Web应用程序的框架,它是.NETFramework的一部分。它提供了一种模型-视图-控制器(MVC)架构、Web表单以及最新的ASP.NETCore中的RazorPages等多种开发模式,可以用来创建动态网页和Web服务。以下是一些基础的ASP.NET编
学习大纲