专栏首页IT技术小咖Spring Boot 快速入门系列(V)—— 事务管理篇之 @Transactional

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

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

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

前言

《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 账户实体类,代码如下:

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 如下:

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,代码如下:

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 类,代码如下:

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 接口,代码如下:

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 接口,代码如下:

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 接口,代码如下:

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 接口,代码如下:

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 数据:

{
  "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 层捕获异常,会发生什么情况呢,我们一起试试。

代码如下:

@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 层的代码实现如下所示:

@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 <——

本文分享自微信公众号 - AiSmart4J(smart4j)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Django 2.1.7 Session 使用Redis存储

    上一篇Django 2.1.7 Session基本操作,解决 'WSGIRequest' object has no attribute 'session' 问...

    Devops海洋的渔夫
  • 如何保证缓存与数据库的双写一致性?

    只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题

    chinotan
  • POJO、JavaBen、Entity的区别

    POJO (Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans,是为了避免和EJB混淆所创造的简称。其中...

    微醺
  • Django 2.1.7 Session基本操作,解决 'WSGIRequest' object has no attribute 'session' 问题

    上一篇Django 2.1.7 状态保持 - Cookie介绍了Django中关于cookie的基本使用,本篇章继续来看看session的操作。

    Devops海洋的渔夫
  • 如何快速上手腾讯云?】云数据库 MySQL快速入门教程(二)

    小编为大家带来一波新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。

    勤劳的小蜜蜂
  • 从SIEM&AI到SIEM@AI | AI构建下一代企业安全大脑

    SIEM是企业安全的核心中枢,负责收集汇总所有的数据,并结合威胁情报对危险进行准确的判断和预警。但传统的SIEM过度依靠人工定制安全策略,不仅仅增加了人力成本,...

    钱曙光
  • 高并发架构系列:Redis的基本介绍,五种数据类型及应用场景分析

    Redis数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。

    用户5546570
  • ABAP INSERT FROM SELECT

    The database table DEMO_SUMDIST_AGG is filled with aggregated data from the tabl...

    Jerry Wang
  • Runtime Errors - START_CALL_SICK

    |Short Text ...

    Jerry Wang
  • 重复一篇3分左右纯生信文章(第一部分)

    这一次要分享的文章题目是:Five key lncRNAs considered as prognostic targets for predicting pa...

    用户1359560

扫码关注云+社区

领取腾讯云代金券