Spring Boot 使用事務(wù)

1. 前言

工作中確實(shí)碰到過一些不知道使用事務(wù)的朋友,毫無疑問會(huì)給項(xiàng)目帶來一些風(fēng)險(xiǎn)。

舉個(gè)簡(jiǎn)單的例子吧,網(wǎng)購(gòu)的時(shí)候需要扣減庫(kù)存,同時(shí)生成訂單。如果扣庫(kù)存成功了,沒生成訂單,結(jié)果是庫(kù)存不知道為何變少了;如果生成訂單了,沒扣庫(kù)存,那就有可能賣出去的數(shù)量比庫(kù)存還多。

這兩種情況都是不能接受的,我們必須保證這兩個(gè)對(duì)數(shù)據(jù)庫(kù)的更新操作同時(shí)成功,或者同時(shí)失敗。

事務(wù)就是這樣一種機(jī)制,將對(duì)數(shù)據(jù)庫(kù)的一系列操作視為一個(gè)執(zhí)行單元,保證單元內(nèi)的操作同時(shí)成功,或者當(dāng)有一個(gè)操作失敗時(shí)全部失敗。

2. 實(shí)例場(chǎng)景

在 Spring Boot 中使用事務(wù)非常簡(jiǎn)單,本小節(jié)我們通過商品扣減庫(kù)存、生成訂單的實(shí)例,演示下 Spring Boot 中使用事務(wù)的具體流程。

3. 數(shù)據(jù)庫(kù)模塊實(shí)現(xiàn)

需要有一個(gè)商品表,保存商品的唯一標(biāo)識(shí)、名稱、庫(kù)存數(shù)量,結(jié)構(gòu)如下:

實(shí)例:

CREATE TABLE `goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一標(biāo)識(shí)',
  `name` varchar(255) DEFAULT NULL COMMENT '商品名稱',
  `num` bigint(255) DEFAULT NULL COMMENT '庫(kù)存數(shù)量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

購(gòu)買商品后還需要生成訂單,保存訂單唯一標(biāo)識(shí)、購(gòu)買商品的 id 、購(gòu)買數(shù)量。

實(shí)例:

CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一標(biāo)識(shí)',
  `goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
  `count` bigint(20) DEFAULT NULL COMMENT '購(gòu)買數(shù)量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4. Spring Boot 后端實(shí)現(xiàn)

接下來,我們開始開發(fā) Spring Boot 后端項(xiàng)目,并且使用事務(wù)實(shí)現(xiàn)扣減庫(kù)存、生成訂單功能。數(shù)據(jù)庫(kù)訪問部分使用比較流行的 MyBatis 框架。

4.1 使用 Spring Initializr 創(chuàng)建項(xiàng)目

Spring Boot 版本選擇 2.2.5 ,Group 為 com.5axxw , Artifact 為 spring-boot-transaction,生成項(xiàng)目后導(dǎo)入 Eclipse 開發(fā)環(huán)境。

4.2 引入項(xiàng)目依賴

我們引入熱部署依賴、 Web 依賴、數(shù)據(jù)庫(kù)訪問相關(guān)依賴及測(cè)試相關(guān)依賴,具體如下:

實(shí)例:

		<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驅(qū)動(dòng) -->
		<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>
		<!-- 測(cè)試 -->
		<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 數(shù)據(jù)源配置

修改 application.properties 文件,配置數(shù)據(jù)源信息。

實(shí)例:

# 配置數(shù)據(jù)庫(kù)驅(qū)動(dòng)
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置數(shù)據(jù)庫(kù)url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
# 配置數(shù)據(jù)庫(kù)用戶名
spring.datasource.username=root
# 配置數(shù)據(jù)庫(kù)密碼
spring.datasource.password=Easy@0122

4.4 開發(fā)數(shù)據(jù)對(duì)象類

開發(fā) goods 表對(duì)應(yīng)的數(shù)據(jù)對(duì)象類 GoodsDo ,代碼如下:

實(shí)例:

/**
* 商品類
*/
public class GoodsDo {
   /**
    * 商品id
    */
   private Long id;
   /**
    * 商品名稱
    */
   private String name;
   /**
    * 商品庫(kù)存
    */
   private Long num;
   // 省略 get set
}

然后開發(fā) order 表對(duì)應(yīng)的數(shù)據(jù)對(duì)象類 OrderDo,代碼如下:

實(shí)例:

/**
 * 訂單類
 */
public class OrderDo {
	/**
	 * 訂單id
	 */
	private Long id;
	/**
	 * 商品id
	 */
	private Long goodsId;
	/**
	 * 購(gòu)買數(shù)量
	 */
	private Long count;
	// 省略 get set
}

4.5 開發(fā)數(shù)據(jù)訪問層

首先定義商品數(shù)據(jù)訪問接口,實(shí)現(xiàn)查詢剩余庫(kù)存與扣減庫(kù)存功能。

實(shí)例:

/**
 * 商品數(shù)據(jù)庫(kù)訪問接口
 */
@Repository // 標(biāo)注數(shù)據(jù)訪問組件
public interface GoodsDao {
	/**
	 * 查詢商品信息(根據(jù)id查詢單個(gè)商品信息)
	 */
	public GoodsDo selectForUpdate(Long id);

	/**
	 * 修改商品信息(根據(jù)id修改其他屬性值)
	 */
	public int update(GoodsDo Goods);
}

注意,在查詢商品剩余庫(kù)存時(shí),我們采用面向?qū)ο蟮姆椒ǎ瑢?duì)應(yīng) id 的商品信息全部取出,更加方便點(diǎn)。采用 selectForUpdate 命名,表示該方法使用了 select ... for update 的 SQL 語句查詢方式,以鎖定數(shù)據(jù)庫(kù)對(duì)應(yīng)記錄,規(guī)避高并發(fā)場(chǎng)景下庫(kù)存修改錯(cuò)誤問題。同樣 update 方法也采用了面向?qū)ο蟮姆绞剑鶕?jù) id 修改其他信息,方便復(fù)用。

然后定義訂單數(shù)據(jù)訪問接口,實(shí)現(xiàn)生成訂單的功能。

實(shí)例:

/**
* 訂單數(shù)據(jù)庫(kù)訪問接口
*/
@Repository // 標(biāo)注數(shù)據(jù)訪問組件
public interface OrderDao {
   /**
    * 新增訂單
    */
   public int insert(OrderDo order);
}

然后,我們修改 Spring Boot 配置類,添加 @MapperScan 注解,掃描數(shù)據(jù)訪問接口所在的包。

實(shí)例:

@SpringBootApplication
@MapperScan("com.5axxw.springboottransaction") // 指定MyBatis掃描的包,以便將數(shù)據(jù)訪問接口注冊(cè)為Bean
public class SpringBootTransactionApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringBootTransactionApplication.class, args);
	}
}

4.6 添加 MyBatis 映射文件

編寫 GoodsDao 、 OrderDao 對(duì)應(yīng)的映射文件, 首先我們通過 application.properties 指定映射文件的位置:

實(shí)例:

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

然后在 resources/mapper 目錄下新建 GoodsMapper.xml 文件,該文件就是 goods 表對(duì)應(yīng)的映射文件,內(nèi)容如下:

實(shí)例:

<?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">
<!-- 本映射文件對(duì)應(yīng)GoodsDao接口 -->
<mapper namespace="com.5axxw.springboottransaction.GoodsDao">
   <!-- 對(duì)應(yīng)GoodsDao中的selectForUpdate方法 -->
   <select id="selectForUpdate" resultMap="resultMapBase" parameterType="java.lang.Long">
   	select <include refid="sqlBase" /> from goods where id = #{id} for update
   </select>
   <!-- 對(duì)應(yīng)GoodsDao中的update方法 -->
   <update id="update" parameterType="com.5axxw.springboottransaction.GoodsDo">
   	update goods set name=#{name},num=#{num} where id=#{id}
   </update>
   <!-- 可復(fù)用的sql模板 -->
   <sql id="sqlBase">
   	id,name,num
   </sql>
   <!-- 保存SQL語句查詢結(jié)果與實(shí)體類屬性的映射 -->
   <resultMap id="resultMapBase" type="com.5axxw.springboottransaction.GoodsDo">
   	<id column="id" property="id" />
   	<result column="name" property="name" />
   	<result column="num" property="num" />
   </resultMap>
</mapper>

同樣我們?cè)?resources/mapper 目錄下新建 OrderMapper.xml 文件,該文件是 order 表對(duì)應(yīng)的映射文件,內(nèi)容如下:

實(shí)例:

<?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">
<!-- 本映射文件對(duì)應(yīng)OrderDao接口 -->
<mapper namespace="com.5axxw.springboottransaction.OrderDao">
   <!-- 對(duì)應(yīng)OrderDao中的insert方法 -->
   <insert id="insert" parameterType="com.5axxw.springboottransaction.OrderDo">
   	insert into `order` (goods_id,count) values (#{goodsId},#{count})
   </insert>
</mapper>

4.7 編寫服務(wù)方法

下單這個(gè)操作,可以封裝為一個(gè)服務(wù)方法,不管是手機(jī)端下單還是電腦端下單都可以調(diào)用。

我們新建訂單服務(wù)類 OrderService ,并在其中實(shí)現(xiàn)下單方法 createOrder ,代碼如下:

實(shí)例:

/**
 * 訂單服務(wù)類
 */
@Service // 注冊(cè)為服務(wù)類
public class OrderService {
	@Autowired
	private GoodsDao goodsDao;
	@Autowired
	private OrderDao orderDao;

	/**
	 * 下單
	 * 
	 * @param goodsId 購(gòu)買商品id
	 * @param count   購(gòu)買商品數(shù)量
	 * @return 生成訂單數(shù)
	 */
	@Transactional // 實(shí)現(xiàn)事務(wù)
	public int createOrder(Long goodsId, Long count) {
		// 鎖定商品庫(kù)存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣減庫(kù)存
		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;
	}
}

我們?cè)?createOrder 方法上添加了 @Transactional 注解,該注解為 createOrder 方法開啟了事務(wù),當(dāng)方法結(jié)束時(shí)提交事務(wù)。這樣保證了 createOrder 內(nèi)方法全部執(zhí)行成功,或者全部失敗。

5. 測(cè)試

5.1 構(gòu)造測(cè)試數(shù)據(jù)

在數(shù)據(jù)庫(kù)中構(gòu)造一條測(cè)試數(shù)據(jù)如下:

圖片描述

測(cè)試數(shù)據(jù)

5.2 正常測(cè)試

編寫測(cè)試方法發(fā)起測(cè)試:

實(shí)例:

/**
 * 訂單測(cè)試
 */
@SpringBootTest
class OrderTest {

	@Autowired
	private OrderService orderService;

	/**
	 * 新增一個(gè)商品
	 */
	@Test
	void testCreateOrder() {
		// 購(gòu)買id為1的商品1份
		int affectRows = orderService.createOrder(1L, 1L);
		assertEquals(1, affectRows);
	}
}

運(yùn)行測(cè)試方法后,手機(jī)的庫(kù)存變?yōu)?19 ,且生成一條訂單記錄,測(cè)試通過,具體結(jié)果如下:

圖片描述

正常測(cè)試結(jié)果

5.3 模擬異常測(cè)試

修改下單方法,在扣減庫(kù)存后拋出異常,看看事務(wù)能否回滾到修改全部未發(fā)生的狀態(tài)。為了便于測(cè)試我們將庫(kù)存重新設(shè)為 20 ,然后將下單方法修改如下:

實(shí)例:

	@Transactional // 實(shí)現(xiàn)事務(wù)
	public int createOrder(Long goodsId, Long count) {
		// 鎖定商品庫(kù)存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣減庫(kù)存
		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;
	}

運(yùn)行測(cè)試方法后,拋出異常,查看數(shù)據(jù)庫(kù)發(fā)現(xiàn),庫(kù)存還是 20 ,說明 goodsDao.update(goods); 的修改沒有提交到數(shù)據(jù)庫(kù),具體結(jié)果如下:

圖片描述

模擬異常測(cè)試結(jié)果

6. 使用注意事項(xiàng)

Spring 事務(wù)在一些情況下不能生效,需要特別注意。

6.1 拋出檢查型異常時(shí)事務(wù)失效

首先了解下異常類型:

  • Exception 受檢查的異常:在程序中必須使用 try…catch 進(jìn)行處理,遇到這種異常不處理,編譯器會(huì)報(bào)錯(cuò)。例如 IOException 。
  • RuntimeException 非受檢查的異常:可以不使用 try…catch 進(jìn)行處理。例如常見的 NullPointerException 。

在大多數(shù)人潛意識(shí)中,只要發(fā)生異常,事務(wù)就應(yīng)該回滾,實(shí)際上使用 @Transactional 時(shí),默認(rèn)只對(duì)非受檢查異常回滾。例如:

實(shí)例:

	@Transactional // 實(shí)現(xiàn)事務(wù)
	public int createOrder(Long goodsId, Long count) {
		// 鎖定商品庫(kù)存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣減庫(kù)存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		if (count > goods.getNum()) {
			// 非受檢查異常拋出時(shí),會(huì)回滾
			throw new RuntimeException();
		}
		// 生成訂單
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}

實(shí)例:

   @Transactional // 實(shí)現(xiàn)事務(wù)
   public int createOrder(Long goodsId, Long count) throws Exception {
   	// 鎖定商品庫(kù)存
   	GoodsDo goods = goodsDao.selectForUpdate(goodsId);
   	// 扣減庫(kù)存
   	Long newNum = goods.getNum() - count;
   	goods.setNum(newNum);
   	goodsDao.update(goods);
   	if (count > goods.getNum()) {
   		//注意!此處為受檢查的異常,就算拋出也不會(huì)回滾
   		throw new Exception();
   	}
   	// 生成訂單
   	OrderDo order = new OrderDo();
   	order.setGoodsId(goodsId);
   	order.setCount(count);
   	int affectRows = orderDao.insert(order);
   	return affectRows;
   }

如果想實(shí)現(xiàn)只要拋出異常就回滾,可以通過添加注解 @Transactional(rollbackFor=Exception.class) 實(shí)現(xiàn)。

實(shí)例:

	@Transactional(rollbackFor = Exception.class) // 拋出異常即回滾
	public int createOrder(Long goodsId, Long count) throws Exception {
		// 鎖定商品庫(kù)存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣減庫(kù)存
		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,我們將在測(cè)試類中,將購(gòu)買數(shù)量設(shè)為大于庫(kù)存數(shù)量的 100 ,然后一次測(cè)試上面三種情況,就能驗(yàn)證上面的說法了。

實(shí)例:

/**
 * 訂單測(cè)試
 */
@SpringBootTest
class OrderTest {

	@Autowired
	private OrderService orderService;

	/**
	 * 創(chuàng)建訂單測(cè)試
	 */
	@Test
	void testCreateOrder() throws Exception {
		// 購(gòu)買id為1的商品1份
		int affectRows = orderService.createOrder(1L, 100L);
		assertEquals(1, affectRows);
	}
}

6.2 一個(gè)事務(wù)方法調(diào)用另一個(gè)事務(wù)方法時(shí)失效

先看下面的實(shí)例,我們修改下 OrderService 類,通過一個(gè)事務(wù)方法調(diào)用 createOrder 方法。

實(shí)例:

/**
 * 訂單服務(wù)類
 */
@Service // 注冊(cè)為服務(wù)類
public class OrderService {
	@Autowired
	private GoodsDao goodsDao;
	@Autowired
	private OrderDao orderDao;

	@Transactional // 開啟事務(wù)
	public int startCreateOrder(Long goodsId, Long count) throws Exception {
		return this.createOrder(goodsId, count);
	}

	/**
	 * 下單
	 * 
	 * @param goodsId 購(gòu)買商品id
	 * @param count   購(gòu)買商品數(shù)量
	 * @return 生成訂單數(shù)
	 */
	@Transactional(rollbackFor = Exception.class) // 拋出異常即回滾
	public int createOrder(Long goodsId, Long count) throws Exception {
		// 鎖定商品庫(kù)存
		GoodsDo goods = goodsDao.selectForUpdate(goodsId);
		// 扣減庫(kù)存
		Long newNum = goods.getNum() - count;
		goods.setNum(newNum);
		goodsDao.update(goods);
		if (count > goods.getNum()) {
			// 非受檢查異常拋出時(shí),會(huì)回滾
			throw new Exception();
		}
		// 生成訂單
		OrderDo order = new OrderDo();
		order.setGoodsId(goodsId);
		order.setCount(count);
		int affectRows = orderDao.insert(order);
		return affectRows;
	}
}

此時(shí)我們?cè)跍y(cè)試類中通過 startCreateOrder 方法再去調(diào)用 createOrder 方法,代碼如下:

實(shí)例:

/**
 * 訂單測(cè)試
 */
@SpringBootTest
class OrderTest {

	@Autowired
	private OrderService orderService;

	/**
	 * 創(chuàng)建訂單測(cè)試
	 */
	@Test
	void testCreateOrder() throws Exception {
		// 購(gòu)買id為1的商品1份
		int affectRows = orderService.startCreateOrder(1L, 100L);
		assertEquals(1, affectRows);
	}
}

startCreateOrder 和 createOrder 方法都是事務(wù)方法,且這兩個(gè)方法事務(wù)特性不同 (一個(gè)沒有 rollbackFor=Exception.class),如果我們調(diào)用 startTransaction 方法,則 createOrder 中的事務(wù)并不會(huì)生效。

也就是說,如果在同一個(gè)類中,一個(gè)事務(wù)方法調(diào)用另一個(gè)事務(wù)方法,可能會(huì)導(dǎo)致被調(diào)用的事務(wù)方法的事務(wù)失效!

這是因?yàn)?Spring 的聲明式事務(wù)使用了代理,具體機(jī)制此處不再探討,但是一定要注意規(guī)避這種事務(wù)失效的場(chǎng)景。

7. 小結(jié)

Spring Boot 中的事務(wù)使用非常簡(jiǎn)單,是因?yàn)檫M(jìn)行了高度的封裝。正是由于封裝的很徹底,所以我們一般接觸不到其具體原理和實(shí)現(xiàn)方式,這就需要我們注意一些事務(wù)可能失效的情況,避免因事務(wù)失效帶來風(fēng)險(xiǎn)和損失。

主站蜘蛛池模板: 五十路熟女人妻一区二区 | 国产精品区AV一区二区| 日本精品夜色视频一区二区| 一区免费在线观看| 无码丰满熟妇一区二区| 亚洲AV本道一区二区三区四区| 日本免费一区二区三区最新| 亚洲国产精品一区| 国产AV午夜精品一区二区入口 | 午夜精品一区二区三区在线观看| 国产精品一级香蕉一区| 国产一区二区三区久久| 日韩少妇无码一区二区三区| 日韩美女视频一区| 日韩成人一区ftp在线播放| 亚洲国产精品一区二区三区在线观看| 久久青草国产精品一区| 人妻少妇精品一区二区三区| 精品伦精品一区二区三区视频| 免费国产在线精品一区| 视频一区二区精品的福利| 福利电影一区二区| 国产一区韩国女主播| 国产伦理一区二区三区| 视频一区视频二区在线观看| 女人和拘做受全程看视频日本综合a一区二区视频 | 国产一区二区精品久久凹凸| 无码少妇一区二区浪潮av| 一区二区三区免费看| 久久精品无码一区二区日韩AV | 一区在线免费观看| 久久国产午夜精品一区二区三区| 视频一区二区三区人妻系列| 一区二区福利视频| 亚洲无线码一区二区三区| 无码精品人妻一区二区三区漫画 | 亚洲国产老鸭窝一区二区三区| 一区二区视频在线| 国精产品一区一区三区MBA下载| 日本免费一区二区三区最新 | 日韩免费一区二区三区在线播放 |