• 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)
  • 相关阅读:
    指针类型
    集合类型
    VMware打开虚拟机没反应的解决方案(全面汇总)
    redis主从|哨兵|集群模式
    MYSQL一次千万级连表查询优化
    StackExchange.Redis通用封装类分享
    Redis-五种数据类型解析
    Redis并发问题
    C#委托和事件
    Invoke 和 BeginInvoke 的区别
  • 原文地址:https://www.cnblogs.com/architectforest/p/13822505.html
Copyright © 2020-2023  润新知