前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Boot 快速入门系列(V)—— 事务管理篇之 @Transactional

Spring Boot 快速入门系列(V)—— 事务管理篇之 @Transactional

作者头像
IT技术小咖
发布2019-07-17 11:47:43
6130
发布2019-07-17 11:47:43
举报
文章被收录于专栏:码上修行码上修行

点击上方蓝色字体关注我吧

一起学习,一起进步,做积极的人!

前言

《Spring Boot 快速入门系列》数据操作篇之 Spring Data JPAJdbcTemplateMyBatis 已经结束,小伙伴们是否了解和掌握了基本的数据库(CRUD)持久化操作。既然数据持久化学习完了,大家知道数据库操作避免不了数据库事务管理,因为存在数据持久化失败的情况,为了保证数据库一致性,必须引入事务管理。记得以前我们使用 SSH 和 SSM 框架都有事务管理,在service 层通过 applicationContext.xml 文件配置,所有 service 层方法都加上事务操作;用来保证一致性,即 service 层方法里的多个dao操作,要么同时成功,要么同时失败;那么今天我们就来演示通过 @Transactional 注解实现 Spring Boot 事务管理。

@Transactional 注解的使用

下面通过一个简单的银行账号转账的示例演示 Spring Boot 下 @Transactional 注解的基本方法。

1)紧接着上一篇(数据操作篇之 MyBatis)项目工程继续。先不使用 @Transactional 注解,演示账户 tom 和账户 jack 之间转账:

2)数据库操作可以参考之前的 3 篇文章(Spring Boot 快速入门系列(II)—— 数据操作篇之 Spring Data JPASpring Boot 快速入门系列(III)—— 数据操作篇之 JdbcTemplateSpring Boot 快速入门系列(IV)—— 数据操作篇之 MyBatis),这里我们使用 Spring JdbcTemplate 模板类实现数据库持久化操作。

在 domain 包下新建 BankAccount 账户实体类,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.domain;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
public class BankAccount {
    /**
     * 账户id
     */
    private String accountId;
    /**
     * 账户名称
     */
    private String userName;
    /**
     * 账户余额
     */
    private double userBalance;

    // 省略 get/set 方法

    @Override
    public String toString() {
        return "BankAccount{" +
                "accountId='" + accountId + '\'' +
                ", userName='" + userName + '\'' +
                ", userBalance=" + userBalance +
                '}';
    }
}

在数据库 db_test 中新增实体类对应的 t_bank_account 表,SQL 如下:

代码语言:javascript
复制
create table t_bank_account (
  account_id int(11) not null auto_increment,
  user_name varchar(20) not null,
  user_balance double default null,
  primary key (account_id)
) engine=innodb default charset=utf8;

insert into t_bank_account (account_id, user_name, user_balance) values (null, 'tom', '1000');
insert into t_bank_account (account_id, user_name, user_balance) values (null, 'jack', '2000');

新建请求对象实体 TransferAccountReqVo,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.domain;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
public class TransferAccountReqVo {
    /**
     * 转出账户
     */
    private int fromAccountId;
    /**
     * 转入账户
     */
    private int toAccountId;
    /**
     * 转账金额
     */
    private double account;

    // 省略 get/set 方法

    @Override
    public String toString() {
        return "TransferAccountReqVo{" +
                "fromAccountId='" + fromAccountId + '\'' +
                ", toAccountId='" + toAccountId + '\'' +
                ", account=" + account +
                '}';
    }
}

controller 层

新增 BankAccountController 类,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.controller;

import cn.giserway.helloworld.domain.TransferAccountReqVo;
import cn.giserway.helloworld.service.IBankAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
@RestController
@RequestMapping("/bankAccount")
public class BankAccountController {
    @Autowired
    private IBankAccountService bankAccountService;

    /**
     * 转账操作
     * @return
     */
    @PostMapping("/transfer")
    public String transferBankAccounts(@RequestBody TransferAccountReqVo transferAccountReqVo){
        try{
            bankAccountService.transferBankAccount(transferAccountReqVo.getFromAccountId(),transferAccountReqVo.getToAccountId(),transferAccountReqVo.getAccount());
            return "SUCCESS";
        }catch(Exception e){
            return "FAIL";
        }
    }
}

注:@RequestBody 注解接收json格式字符串数据,后台直接封装成实体对象

service 层

新增 IBankAccountService 接口,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.service;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
public interface IBankAccountService {
    /**
     * 转账
     * @param formAccountId 用户A
     * @param toAccountId 用户B
     * @param account 金额
     */
    void transferBankAccount(int formAccountId, int toAccountId, double account);
}

新增 BankAccountServiceImpl 类实现 IBankAccountService 接口,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.service.serviceImpl;

import cn.giserway.helloworld.dao.IBankAccountDao;
import cn.giserway.helloworld.domain.BankAccount;
import cn.giserway.helloworld.service.IBankAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
@Service
public class BankAccountServiceImpl implements IBankAccountService {
    @Autowired
    private IBankAccountDao bankAccountDao;

    @Override
    public void transferBankAccount(int formAccountId, int toAccountId, double account) {
        // 扣钱
        BankAccount fromUserAccount = bankAccountDao.queryBankAccountById(formAccountId);
        fromUserAccount.setUserBalance(fromUserAccount.getUserBalance()-account);
        bankAccountDao.updateBankAccount(fromUserAccount);
        // 加钱
        BankAccount toUserAccount = bankAccountDao.queryBankAccountById(toAccountId);
        toUserAccount.setUserBalance(toUserAccount.getUserBalance()+account);
        bankAccountDao.updateBankAccount(toUserAccount);
    }
}

dao 层

新建 IBankAccountDao 接口,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.dao;

import cn.giserway.helloworld.domain.BankAccount;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
public interface IBankAccountDao {
    /**
     * 根据账户id查询账户信息
     * @param accountId
     * @return
     */
    BankAccount queryBankAccountById(int accountId);

    /**
     * 更新账户信息
     * @param bankAccount
     * @return
     */
    int updateBankAccount(BankAccount bankAccount);
}

新增 BankAccountDaoImpl 类实现 IBankAccountDao 接口,代码如下:

代码语言:javascript
复制
package cn.giserway.helloworld.dao.daoImpl;

import cn.giserway.helloworld.dao.IBankAccountDao;
import cn.giserway.helloworld.domain.BankAccount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/**
 * @program: helloworld
 *
 * @author: giserway
 *
 **/
@Repository
public class BankAccountDaoImpl implements IBankAccountDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public BankAccount queryBankAccountById(int accountId) {
        return jdbcTemplate.queryForObject("select account_id,user_name,user_balance from t_bank_account where account_id = ?", new Object[]{ accountId }, new BeanPropertyRowMapper<>(BankAccount.class));
    }

    @Override
    public int updateBankAccount(BankAccount bankAccount) {
        return jdbcTemplate.update("update t_bank_account set user_balance = ? where account_id = ?",bankAccount.getUserBalance(),bankAccount.getAccountId());
    }
}

项目结构如下:

3)声明式事务演示

以 tom 向 jack 转账为例。

I. service 层未加 @Transactional 注解实现如下:

启动工程项目,通过 Postman 发送转账请求:

发送请求前:

发送请求后:

POST 请求地址:localhost:9999/bankAccount/transfer

请求体 json 数据:

代码语言:javascript
复制
{
  "fromAccountId":"1",
  "toAccountId":"2",
  "account":"500"
}

II. 在 service 层代码加入引起异常代码观察数据变化,如下所示:

先将数据恢复到原始状态,即 tom 余额 1000,jack 余额 2000。

重新启动工程,同样使用 Postman 模拟请求,结果如下:

III. 在 service 层代码中,在方法上加 @Transactional 注解,代码如下:

注:pom 文件中引入的 mysql 连接驱动依赖,Spring Boot 会自动注入 DataSourceTransactionManager,即注入了 mysql 数据源事务管理器。因此可以使用 @Transactional 注解使用声明式事务。

先将数据恢复到原始状态,即 tom 余额 1000,jack 余额 2000。

重新启动工程,同样使用 Postman 模拟请求,结果如下:

Postman 请求放回 FAIL,表示后台请求异常,而 tom 和 jack 的余额没有变化,说明 @Transactional 保证的数据库数据的一致性。

IV. 如果在 service 层捕获异常,会发生什么情况呢,我们一起试试。

代码如下:

代码语言:javascript
复制
@Service
public class BankAccountServiceImpl implements IBankAccountService {
    @Autowired
    private IBankAccountDao bankAccountDao;

    @Transactional
    @Override
    public void transferBankAccount(int formAccountId, int toAccountId, double account) {
        try {
            // 扣钱
            BankAccount fromUserAccount = bankAccountDao.queryBankAccountById(formAccountId);
            fromUserAccount.setUserBalance(fromUserAccount.getUserBalance()-account);
            bankAccountDao.updateBankAccount(fromUserAccount);
            int i = 1/0;
            // 加钱
            BankAccount toUserAccount = bankAccountDao.queryBankAccountById(toAccountId);
            toUserAccount.setUserBalance(toUserAccount.getUserBalance()+account);
            bankAccountDao.updateBankAccount(toUserAccount);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

同上重启项目,模拟请求,结果如下:

从上面的结果可以看出,在方法体已捕获异常的情况下,即使方法上加了 @Transactional 注解,事务也没起作用。

V. 对于第 IV 种情况,注解 @Transactional 事务未起作用

下面通过简单的修改注解使用方式 @Transactional(rollbackFor = Exception.class), 代码如下:

此时,测试步骤同上,这里不再演示,@Transactional 注解起到了事务回滚作用,自己动手试一下,体会更深!

注:这是一个坑,在使用注解 @Transactional 来保证数据一致性时,如果后台已经捕获异常,那么此时注解无效;要想注解生效,需要 @Transactional 注解显式指定回滚规则 rollbackFor,即发生什么异常事务回滚,默认抛出 RuntimeException、error 时回滚。该注解默认的事务传播性 PROPAGATION_REQUIRED,如果当前方法已经有事务就沿用该事务,否则没有事务就创建新的事务,这样保证一个事务内要么一起成功、要么一起失败,这是常用的事务传播性。

最后 service 层的代码实现如下所示:

代码语言:javascript
复制
@Service
public class BankAccountServiceImpl implements IBankAccountService {
    @Autowired
    private IBankAccountDao bankAccountDao;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void transferBankAccount(int formAccountId, int toAccountId, double account) {
        try {
            // 扣钱
            BankAccount fromUserAccount = bankAccountDao.queryBankAccountById(formAccountId);
            fromUserAccount.setUserBalance(fromUserAccount.getUserBalance()-account);
            bankAccountDao.updateBankAccount(fromUserAccount);
            int i = 1/0;
            // 加钱
            BankAccount toUserAccount = bankAccountDao.queryBankAccountById(toAccountId);
            toUserAccount.setUserBalance(toUserAccount.getUserBalance()+account);
            bankAccountDao.updateBankAccount(toUserAccount);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

小结

通过转账的场景演示,我们学习掌握了如何使用 @Transactional 注解完成声明式事务管理,简单方便,但是也有一些需要注意的地方,比如 service 层某方法上使用 @Transactional 注解时,业务代码未捕获异常,发生异常时会执行事务回滚;而使用 @Transactional 注解,如果业务代码显式的捕获了异常,那么我们必须显式的声明事务回滚规则 rollbackFor,不然不能保证数据一致性。鉴于此,我们在使用 @Transactional 注解时,都显式的指定 rollbackFor 属性即可,一般设置 rollbackFor = Exception.class,这样所有异常抛出时都会执行事务回滚。

下一篇文章我们将会演示 Spring Boot 快速入门系列(VI)—— 全局异常处理篇。

——> End <——

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上修行 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档