前言:
我们在实际业务开发中,经常需要往A表插入的数据的同时,还需要同时往B表去插入数据,要不全部成功,要不全部不成功,这就需要事务管理了。事务其实就是指逻辑上的一组操作,组成这组操作的各个单元,要成功都成功,要失败都失败,从而保持数据的一致性,今天就和大家讲解下怎么在实际业务开发中使用事务。
正文:
一、Spring中事务管理分为两种方式
1.编程式事务管理
spring框架提供了两种编程式事务管理方式:使用TransactionTemplate和直接使用PlatformTransactionManager实现
2.声明式事务管理
声明式事务有两种方式,一种是在配置文件中做相关的事务规则声明,另一种是基于@Transactional 注解的方式。
二、两种方式的优缺点
1.声明式事务管理是非侵袭入式的开发方式,使业务代码逻辑不受污染。声明式事务管理不需要通过编程的方式管理事务,这样就不需要在业务代码中掺杂事务管理的代码,只需要在配置文件中做相关的事务规则声明或者通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。
2.声明式事务管理的作用粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是声明式事务管理也可以曲线救国,可以将需要进行事务管理的代码块写成独立的方法。
3.声明式事务管理无法获取事务状态,但是编程式事务管理可以。
所以在实际开发中要结合具体情况选择使用适合自己项目的事务管理方式。
三、如何使用编程式事务和声明式事务
编程式事务举个例子:
银行转账业务:
1.实体类对象
@Data
public class Bank extends Model<Bank> {
private static final long serialVersionUID=1L;
@Id
private int id;
private String cardId;
private String userName;
private Double money;
}
2.Controller层代码
@RestController
@RequestMapping("/bank")
public class BankController {
@Autowired
BankService bankService;
@RequestMapping(value = "/transferMoney", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public String addUser(@RequestBody BankDto bank){
JSONObject result = new JSONObject();
try{
bankService.transferMoney(bank.getFromId(),bank.getToId(),bank.getMoney());
result.put("data","成功");
}catch (Exception e){
return "转账失败";
}
return result.toString();
}
}
3.Service层代码
public interface BankService extends IService<Bank> {
//转账操作
void transferMoney(int fromId,int toId,double money) throws Exception;
}
4.ServiceImpl层代码
@Service
public class BankServiceImpl extends ServiceImpl<BankDao, Bank> implements BankService {
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public void transferMoney(int fromId, int toId, double money) throws Exception {
Bank bankFormOld = baseMapper.selectById(fromId);
if(bankFormOld.getMoney()<=0){
throw new BankException("暂无余额");
}
if(bankFormOld.getMoney()<money){
throw new BankException("余额不足,转账失败");
}
/*
* 执行带有返回值<Object>的事务管理
*/
Boolean b = transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus transactionStatus) {
try {
//减钱
Bank bankForm= new Bank();
bankForm.setId(fromId);
bankForm.setMoney(bankFormOld.getMoney()-money);
baseMapper.updateById(bankForm);
//加钱
Bank bankToOld = baseMapper.selectById(toId);
Bank bankTo= new Bank();
bankTo.setId(toId);
bankTo.setMoney(bankToOld.getMoney()+money);
baseMapper.updateById(bankTo);
return true;
} catch (Exception e) {
//回滚
transactionStatus.setRollbackOnly();
System.out.println("事务回滚");
return false;
}
}
});
if(!b){
throw new BankException("转账失败");
}
}
}
5.数据库
6.这里的事务的写法是,带有返回值的
public Object getObject(String str) {
/*
* 执行带有返回值<Object>的事务管理
*/
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
try {
...
//....... 业务代码
return new Object();
} catch (Exception e) {
//回滚
transactionStatus.setRollbackOnly();
return null;
}
}
});
}
还有种不带返回值的写法
public void update(String str) {
/*
* 执行无返回值的事务管理
*/
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
7.我们可以看到数据库bank表里有两条数据,id分别是1和2,这时候我从1往2转60块,是没问题的 ,但是如果转入的id写个不存在的id,下面的代码就会报空指针异常,事务管理起效的话,数据库的两条数据都不会有变化的。
bankTo.setMoney(bankToOld.getMoney()+money);
8.我们用postman模拟下数据,看下结果
声明式事务举个例子:
银行转账业务:
1.实体类对象
@Data
public class Bank extends Model<Bank> {
private static final long serialVersionUID=1L;
@Id
private int id;
private String cardId;
private String userName;
private Double money;
}
2.Controller层代码
@RestController
@RequestMapping("/bank")
public class BankController {
@Autowired
BankService bankService;
@RequestMapping(value = "/transferMoney", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public String addUser(@RequestBody BankDto bank){
JSONObject result = new JSONObject();
try{
bankService.transferMoney(bank.getFromId(),bank.getToId(),bank.getMoney());
result.put("data","成功");
}catch (Exception e){
return "转账失败";
}
return result.toString();
}
}
3.Service层代码
public interface BankService extends IService<Bank> {
//转账操作
void transferMoney(int fromId,int toId,double money) throws Exception;
}
4.ServiceImpl层代码
@Service
public class BankServiceImpl extends ServiceImpl<BankDao, Bank> implements BankService {
@Transactional
@Override
public void transferMoney(int fromId, int toId, double money) throws Exception {
Bank bankFormOld = baseMapper.selectById(fromId);
if (bankFormOld.getMoney() <= 0) {
throw new BankException("暂无余额");
}
if (bankFormOld.getMoney() < money) {
throw new BankException("余额不足,转账失败");
}
//减钱
Bank bankForm = new Bank();
bankForm.setId(fromId);
bankForm.setMoney(bankFormOld.getMoney() - money);
baseMapper.updateById(bankForm);
//加钱
Bank bankToOld = baseMapper.selectById(toId);
Bank bankTo = new Bank();
bankTo.setId(toId);
bankTo.setMoney(bankToOld.getMoney() + money);
baseMapper.updateById(bankTo);
}
}
5.我们用postman模拟下数据,看下结果
这里需要注意一个事情:
Spring使用声明式事务处理,默认情况下,如果被注解的数据库操作方法中发生了unchecked异常,所有的数据库操作将rollback;如果发生的异常是checked异常,默认情况下数据库操作还是会提交的。
checked异常:
表示无效,不是程序中可以预测的。比如无效的用户输入,文件不存在,网络或者数据库链接错误。这些都是外在的原因,都不是程序内部可以控制的。
必须在代码中显式地处理。比如try-catch块处理,或者给所在的方法加上throws说明,将异常抛到调用栈的上一层。
继承自java.lang.Exception(java.lang.RuntimeException除外)。
unchecked异常:
表示错误,程序的逻辑错误。是RuntimeException的子类,比如IllegalArgumentException, NullPointerException和IllegalStateException。
不需要在代码中显式地捕获unchecked异常做处理。
继承自java.lang.RuntimeException(而java.lang.RuntimeException继承自java.lang.Exception)
简答的说就是:
如果你的业务代码try-catch后,并抛出自定义的异常,这时候spring的事务管理并不起作用,仍会提交数据。
比如这样的写代码:
@Transactional
@Override
public void transferMoney(int fromId, int toId, double money) throws BankException {
Bank bankFormOld = baseMapper.selectById(fromId);
if (bankFormOld.getMoney() <= 0) {
throw new BankException("暂无余额");
}
if (bankFormOld.getMoney() < money) {
throw new BankException("余额不足,转账失败");
}
try {
//减钱
Bank bankForm = new Bank();
bankForm.setId(fromId);
bankForm.setMoney(bankFormOld.getMoney() - money);
baseMapper.updateById(bankForm);
//加钱
Bank bankToOld = baseMapper.selectById(toId);
Bank bankTo = new Bank();
bankTo.setId(toId);
bankTo.setMoney(bankToOld.getMoney() + money);
baseMapper.updateById(bankTo);
}catch (Exception e){
throw new BankException("转账失败");
}
}
postman模拟请求的结果是:
可以看到出现了脏数据,没有保持数据的一致性。
正确的写法,@Transactional指定要回滚checked异常@Transactional(rollbackFor = {BankException.class})
@Transactional(rollbackFor = {BankException.class})
@Override
public void transferMoney(int fromId, int toId, double money) throws BankException {
Bank bankFormOld = baseMapper.selectById(fromId);
if (bankFormOld.getMoney() <= 0) {
throw new BankException("暂无余额");
}
if (bankFormOld.getMoney() < money) {
throw new BankException("余额不足,转账失败");
}
try {
//减钱
Bank bankForm = new Bank();
bankForm.setId(fromId);
bankForm.setMoney(bankFormOld.getMoney() - money);
baseMapper.updateById(bankForm);
//加钱
Bank bankToOld = baseMapper.selectById(toId);
Bank bankTo = new Bank();
bankTo.setId(toId);
bankTo.setMoney(bankToOld.getMoney() + money);
baseMapper.updateById(bankTo);
}catch (Exception e){
throw new BankException("转账失败");
}
}
postman模拟请求的结果是:
数据正常,事务起作用了。
总结:
今日份分享的一句话:
即便成年人的世界中没有“容易二字”,但只要还有在意的事物、爱着的人,生活就仍然值得;而那些在一次次碰壁后仍愿意认真生活的人,总会遇到转机啊。
我是阿达,一名喜欢分享知识的程序员,时不时的也会荒腔走板的聊一聊电影、电视剧、音乐、漫画,这里已经有10455位小伙伴在等你们啦,感兴趣的就赶紧来点击关注我把,哪里有不明白或有不同观点的地方欢迎留言!