1、建表语句
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`account_id` varchar(30) ,
`account_name` varchar(30),
`balance` decimal(20,2),
PRIMARY KEY (`account_id`)
);
insert into account values ('1','admin','1000.10');
2、以操作账户金额为例,模拟正常操作金额提交事务,以及发生异常回滚事务。
其中控制层代码如下:
package com.sinosoft.demo.controller;
import com.sinosoft.demo.model.Account;
import com.sinosoft.demo.services.AccountService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class AccountController {
@Resource
AccountService accountService;
@GetMapping("/")
public Account getAccount() {
//查询账户
return accountService.getAccount();
}
@GetMapping("/add")
public Object addMoney() {
try {
accountService.addMoney();
} catch (Exception e) {
return "发生异常了:" + accountService.getAccount();
}
return getAccount();
}
}
3、在业务层使用 @Transactional 开启事务,执行数据库操作后抛出异常。具体代码如下:
package com.sinosoft.demo.services;
import com.sinosoft.demo.dao.AccountMapper;
import com.sinosoft.demo.model.Account;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class AccountService {
@Resource
AccountMapper accountMapper;
public Account getAccount() {
return accountMapper.getAccount();
}
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//然后遇到故障
throw new RuntimeException("发生异常了..");
}
}
4、数据库层,通过注解来实现账户数据的查询,具体如下:
package com.sinosoft.demo.dao;
import com.sinosoft.demo.model.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface AccountMapper {
@Select("select * from account where account_id=1")
Account getAccount();
@Update("update account set balance = balance+100 where account_id=1")
void addMoney();
}
5、Account 实体对象如下:
package com.sinosoft.demo.model;
import java.math.BigDecimal;
public class Account {
private String accountId;
private String accountName;
private BigDecimal balance;
@Override
public String toString() {
return "Account{" +
"accountId='" + accountId + '\'' +
", accountName='" + accountName + '\'' +
", balance=" + balance +
'}';
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
6、测试事务,启动应用,访问 http://localhost:8090 ,可以看到账户数据,如下:
然后访问 http://localhost:8090/add ,可以看到账户余额并没有增加,如下: 也就是说事务开启成功,数据得到回滚。
7、容易搞错点
使用事务注解@Transactional 之前,应该先了解它的相关属性,避免在实际项目中踩中各种各样的坑点。
java异常体系结构图。
常见坑点1:遇到非检测异常时,事务不开启,也无法回滚。
例如下面这段代码,账户余额依旧增加成功,并没有因为后面遇到检测异常而回滚!!
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//然后遇到故障
throw new SQLException("发生异常了..");
}
原因分析:因为Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。如果想针对非检测异常进行事务回滚,可以在@Transactional 注解里使用rollbackFor 属性明确指定异常。
例如下面这样,就可以正常回滚:
@Transactional(rollbackFor = Exception.class)
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//然后遇到故障
throw new SQLException("发生异常了..");
}
常见坑点2: 在业务层捕捉异常后,发现事务不生效。
这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。例如:下面这段代码直接导致增加余额的事务回滚没有生效。
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//谨慎:尽量不要在业务层捕捉异常并处理
try {
throw new SQLException("发生异常了..");
} catch (Exception e) {
e.printStackTrace();
}
}
不要小瞧了这些细节,往前暴露异常很大程度上很能够帮我们快速定位问题,而不是经常在项目上线后出现问题,却无法刨根知道哪里报错。
推荐做法:在业务层统一抛出异常,然后在控制层统一处理。
@Transactional
public void addMoney() throws Exception {
//先增加余额
accountMapper.addMoney();
//推荐:在业务层将异常抛出
throw new RuntimeException("发生异常了..");
}