一,方法中使用try...catch導致@Transactional事務無效的解決方法
1,問題的描述:
如果一個方法添加了@Transactional注解聲明事務,
而方法內又使用了try catch 捕捉異常,
則方法內的異常捕捉會覆蓋事務對異常的判斷,
從而異致事務失效而不回滾
2, 如何解決?
第一個方法:給@Transactional注解增加:rollbackFor后並手動拋出指定的異常
第二個方法:在捕捉到異常后手動rollback
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息
1,項目地址:
https://github.com/liuhongdi/transactional
2,功能說明:
演示了事務方法中捕捉異常時如何使事務回滾
3,項目結構:如圖:
三,配置文件說明
1,pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis begin--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!--mysql begin--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
2,application.properties
#mysql spring.datasource.url=jdbc:mysql://localhost:3306/store?characterEncoding=utf8&useSSL=false spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis mybatis.mapper-locations=classpath:/mapper/*Mapper.xml mybatis.type-aliases-package=com.example.demo.mapper
3,創建數據表goods的sql
CREATE TABLE `goods` ( `goodsId` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `goodsName` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'name', `subject` varchar(200) NOT NULL DEFAULT '' COMMENT '標題', `price` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '價格', `stock` int(11) NOT NULL DEFAULT '0' COMMENT 'stock', PRIMARY KEY (`goodsId`), UNIQUE KEY `goodsName` (`goodsName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表'
注意goodsName字段上有一個唯一的索引,
后面我們會利用它來引發一個duplicate entry 異常
插入一條數據:
INSERT INTO `goods` (`goodsId`, `goodsName`, `subject`, `price`, `stock`) VALUES (3, '100分電動牙刷', '好用到讓你愛上刷牙', '59.00', 100);
四,java代碼說明
1,LockController.java
@RestController @RequestMapping("/lock") public class LockController { @Resource OrderService orderService; //購買商品,方法內沒有捕捉異常 @GetMapping("/lock") @ResponseBody public String buyLock() { try { int goodsId = 3; orderService.decrementProductStoreLock(goodsId,1); return "success"; } catch (Exception e){ System.out.println("捕捉到了異常 in controller"); e.printStackTrace(); String errMsg = e.getMessage(); return errMsg; } } //購買商品,方法內捕捉異常 @GetMapping("/lockcatch") @ResponseBody public String buyLockCatch() { try { int goodsId = 3; boolean isDecre = orderService.decrementProductStoreLockWithCatch(goodsId,1); if (isDecre) { return "success"; } else { return "false"; } } catch (Exception e){ System.out.println("捕捉到了異常 in controller"); e.printStackTrace(); String errMsg = e.getMessage(); return errMsg; } } //購買商品,方法內捕捉異常 @GetMapping("/lockcatch1") @ResponseBody public String buyLockCatch1() { try { int goodsId = 3; boolean isDecre = orderService.decrementProductStoreLockWithCatch1(goodsId,1); if (isDecre) { return "success"; } else { return "false"; } } catch (Exception e){ System.out.println("捕捉到了異常 in controller"); e.printStackTrace(); String errMsg = e.getMessage(); return errMsg; } } //購買商品,方法內捕捉異常 @GetMapping("/lockcatch2") @ResponseBody public String buyLockCatch2() { try { int goodsId = 3; boolean isDecre = orderService.decrementProductStoreLockWithCatch2(goodsId,1); if (isDecre) { return "success"; } else { return "false"; } } catch (Exception e){ System.out.println("捕捉到了異常 in controller"); e.printStackTrace(); String errMsg = e.getMessage(); return errMsg; } } }
2,GoodsMapper.java
@Repository @Mapper public interface GoodsMapper { Goods selectOneGoods(int goods_id); int updateOneGoodsStock(Goods goodsOne); int insertOneGoods(Goods goodsOne); }
3,Goods.java
public class Goods { //商品id private int goodsId; public int getGoodsId() { return this.goodsId; } public void setGoodsId(int goodsId) { this.goodsId = goodsId; } //商品名字 private String goodsName; public String getGoodsName() { return this.goodsName; } public void setGoodsName(String goodsName) { this.goodsName = goodsName; } //商品庫存數 private int stock; public int getStock() { return this.stock; } public void setStock(int stock) { this.stock = stock; } }
4,OrderService.java
public interface OrderService { public boolean decrementProductStoreLock(int goodsId, int buyNum); public boolean decrementProductStoreLockWithCatch(int goodsId, int buyNum); public boolean decrementProductStoreLockWithCatch1(int goodsId, int buyNum); public boolean decrementProductStoreLockWithCatch2(int goodsId, int buyNum); }
5,OrderServiceImpl.java
@Service public class OrderServiceImpl implements OrderService { @Resource private GoodsMapper goodsMapper; /* * * 減庫存,供其他方法調用 * */ private boolean decrestock(int goodsId, int buyNum) { Goods goodsOne = goodsMapper.selectOneGoods(goodsId); System.out.println("-------------------------當前庫存:"+goodsOne.getStock()+"-------購買數量:"+buyNum); if (goodsOne.getStock() < buyNum || goodsOne.getStock() <= 0) { System.out.println("------------------------fail:buy fail,return"); return false; } int upStock = goodsOne.getStock()-buyNum; goodsOne.setStock(upStock); int upNum = goodsMapper.updateOneGoodsStock(goodsOne); System.out.println("-------------------------success:成交訂單數量:"+upNum); int insNum = goodsMapper.insertOneGoods(goodsOne); System.out.println("-------------------------success:ins數量:"+insNum); return true; } //方法內不做try catch捕捉異常,可以正常回滾 @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public boolean decrementProductStoreLock(int goodsId, int buyNum) { return decrestock(goodsId, buyNum); } //異常時不處理,會導致不回滾 @Override @Transactional(rollbackFor={Exception.class}) public boolean decrementProductStoreLockWithCatch(int goodsId, int buyNum) { try { return decrestock(goodsId, buyNum); } catch (Exception e) { System.out.println("捕捉到了異常in service method"); e.printStackTrace(); String errMsg = e.getMessage(); //throw e; return false; } } //異常時手動拋出異常 @Override @Transactional(rollbackFor={Exception.class}) public boolean decrementProductStoreLockWithCatch1(int goodsId, int buyNum) { try { return decrestock(goodsId, buyNum); } catch (Exception e) { System.out.println("捕捉到了異常in service method"); e.printStackTrace(); String errMsg = e.getMessage(); throw e; } } //異常時手動rollback @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public boolean decrementProductStoreLockWithCatch2(int goodsId, int buyNum) { try { return decrestock(goodsId, buyNum); } catch (Exception e) { System.out.println("捕捉到了異常in service method"); e.printStackTrace(); String errMsg = e.getMessage(); //手動rollback TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return false; } } }
說明:decrestock這個方法中有兩個寫操作,分別是:減庫存和insert一條商品記錄,
后者會引發duplicate entry異常
6,GoodsMapper.xml
<?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"> <mapper namespace="com.transactional.demo.mapper.GoodsMapper"> <select id="selectOneGoods" parameterType="int" resultType="com.transactional.demo.pojo.Goods"> select * from goods where goodsId=#{goodsId} </select> <update id="updateOneGoodsStock" parameterType="com.transactional.demo.pojo.Goods"> UPDATE goods SET stock = #{stock} WHERE goodsId = #{goodsId} </update> <insert id="insertOneGoods" parameterType="com.transactional.demo.pojo.Goods" useGeneratedKeys="true" keyProperty="goodsId"> insert into goods(goodsName) values( #{goodsName}) </insert> </mapper>
五,測試效果
1,測試正常回滾:訪問:
http://127.0.0.1:8080/lock/lock
返回:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '100分電動牙刷' for key 'goodsName'
### The error may involve com.transactional.demo.mapper.GoodsMapper.insertOneGoods-Inline
### The error occurred while setting parameters
### SQL: insert into goods(goodsName) values( ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '100分電動牙刷' for key 'goodsName' ;
Duplicate entry '100分電動牙刷' for key 'goodsName';
nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '100分電動牙刷' for key 'goodsName'
查看數據表:
庫存未減少,說明正常回滾
查看控制台:
-------------------------當前庫存:100-------購買數量:1
-------------------------success:成交訂單數量:1
捕捉到了異常 in controller
2,測試事務無效,不能正常回滾:訪問:
http://127.0.0.1:8080/lock/lockcatch
返回:
false
查看數據表:
回滾失效
查看控制台:
-------------------------當前庫存:100-------購買數量:1
-------------------------success:成交訂單數量:1
捕捉到了異常in service method
3,測試捕捉到異常后手動拋出異常引發回滾:
訪問:
http://127.0.0.1:8080/lock/lockcatch1
返回:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '100分電動牙刷' for key 'goodsName' ### The error may involve com.transactional.demo.mapper.GoodsMapper.insertOneGoods-Inline ### The error occurred while setting parameters ### SQL: insert into goods(goodsName) values( ?) ### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '100分電動牙刷' for key 'goodsName' ; Duplicate entry '100分電動牙刷' for key 'goodsName'; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '100分電動牙刷' for key 'goodsName'
查看數據表:
查看控制台:
-------------------------當前庫存:99-------購買數量:1
-------------------------success:成交訂單數量:1
捕捉到了異常in service method
...
捕捉到了異常 in controller
...
4,測試捕捉到異常后手動回滾
訪問:
http://127.0.0.1:8080/lock/lockcatch2
返回:
false
查看數據表:
查看控制台:
-------------------------當前庫存:99-------購買數量:1
-------------------------success:成交訂單數量:1
捕捉到了異常in service method
5,大家注意最后兩個返回的區別:為什么會不一樣?
手動拋出異常后,會被controller捕捉到,所以沒有返回success或false,
而是返回了異常的提示信息
六,查看spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.4.RELEASE)