spring boot:方法中使用try...catch導致@Transactional事務無效的解決(spring boot 2.3.4)


一,方法中使用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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM