spring事务传递特性-REQUIRES_NEW和NESTED
spring对于事务的实现的确是它的一大优点,节省了程序员不少时间。
关于事务,有许多可以聊的内容,例如实现方式、实现原理、传递特性等。
本文讨论传递特性中的REQUIRES_NEW,NESTED。
如果想了解更多可以看官网和下面这个url: SpringAOP学习--Spring事务简介及原理_程序源程序的博客-CSDN博客_springaop事务实现原理
一、前言
在学习传递性之前,先了解以下内容。
- spring的事务框架以及有哪些事务管理器
- spring如何使用aop实现事务
- 如何配置事务管理器
1.1事务框架和事务管理器
在spring的官网上:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-programmatic
阐述了几个概念:
- 本地事务
- 全局事务
- 声明式事务管理
- 编程式事务管理
大部分时候,我们只考虑本地事务和声明式事务管理。或者说,如果您不需要开发分布式系统,那么一般情况下够了。
下图是spring的事务管理器的类关系图:
从上图可以看出:
- 有两大子类-分别是PlatformTransactionManager和ReactiveTransactionManager
- 我们日常用的是DataSourceTransactionManager
1.2 aop实现事务
官方图,来自于:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-programmatic
关于aop的实现,其原理和拦截器的实现是差不多一个道理:进来前先动一动,结束前再动一动。具体略。
网上有解释这个挺清楚的:SpringAOP学习--Spring事务简介及原理_程序源程序的博客-CSDN博客_springaop事务实现原理
1.3 配置事务管理器(本地)
参考 https://blog.csdn.net/weixin_43868443/article/details/119453063
关键内容就是继承实现:TransactionManagementConfigurer
二、名词解析
2.1 REQUIRES_NEW(需要新事务)
望文生义,就是另外开启一个新的事务。这个事务的成功和失败可以不影响其它事务。
把官方的资料搬过来下:Data Access (spring.io)
PROPAGATION_REQUIRES_NEW, in contrast to PROPAGATION_REQUIRED, always uses an independent physical transaction for each affected transaction scope,
never participating in an existing transaction for an outer scope. In such an arrangement, the underlying resource transactions are different and,
hence, can commit or roll back independently, with an outer transaction not affected by an inner transaction’s rollback status and with an inner transaction’s locks released immediately
after its completion. Such an independent inner transaction can also declare its own isolation level,
timeout, and read-only settings and not inherit an outer transaction’s characteristics.
要点:
- 它是独立的
- 它挂起主事务
- 它可以不影响主事务
2.2 NESTED(嵌套)
关键字:嵌套,保存点(savepoint)。后者通常是rdbms的一个概念,意思是可以回滚到某个标记点。
它和requires_new的主要区别:nested和主事务是一个事务,而requires_new是不同的事务。
它和requires_new的共同点:都可以回滚而不影响主事务(对于nested而言其实是一个)
它们都可以达成一个目的,但是由于requires_new是一个独立的事务,所以可以实现许多特别的业务逻辑。
而nested的由于它的特性,所以如果用于记录日志是一个不错的选择。
三、例子
3.1环境准备
业务简介:有一个仓库表和一个销售记录表。当执行一个销售的业务操作的时候需要:
- 开始日志-记录基本信息
- 销售-检验库存,并增减库存。如果库存不满足要求,则回滚销售的操作
- 结束日志
要求:无论销售是否成功,日志都必须记录在数据库。
表设计:
CREATE TABLE inventory( id INT UNSIGNED NOT NULL AUTO_INCREMENT, NAME VARCHAR(60) NOT NULL COMMENT '物品名称', qty DECIMAL(10,2) NOT NULL, max_qty DECIMAL(10,2) NOT NULL COMMENT '最大库存', unit_name VARCHAR(20), last_optime DATETIME ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(id) ) COMMENT '库存'; ALTER TABLE inventory ADD UNIQUE INDEX uid_inventory_name (NAME); CREATE TABLE sale_info( id INT UNSIGNED NOT NULL AUTO_INCREMENT, NAME VARCHAR(60) NOT NULL, qty DECIMAL(10,2) NOT NULL, price DECIMAL(6,2) NOT NULL, total_amount DECIMAL(16,2) NOT NULL, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id) ) COMMENT '销售记录';
3.2-代码
1.代码-日志的start方法:
@Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.DEFAULT,rollbackFor=Exception.class)
@Override
public int start(SystemLog log) {
String sql="INSERT INTO system_log (\r\n"
+ " operation_userid,\r\n"
+ " module_name,\r\n"
+ " action_name, \r\n"
+ " parmas\r\n"
+ ")\n"
+ "value(?,?,?,?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTp.update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection con) throws SQLException
{
PreparedStatement ps = con.prepareStatement(sql,new String[]{ "id"});
ps.setInt(1, log.getOperationUserid());
ps.setString(2, log.getModuleName());
ps.setString(3, log.getActionName());
ps.setString(4, log.getParmas());
return ps;
}
}, keyHolder);
int id= keyHolder.getKey().intValue();
log.setId(Long.valueOf(id));
return id;
}
2.代码-仓库(为了简介,略去一些不必要的信息):
public interface InventoryService { public int out(String name,BigDecimal qty) throws InvalidDataException; public int in(String name,BigDecimal qty) throws InvalidDataException; } @Service public class InventoryServiceImpl implements InventoryService { @Autowired private JdbcTemplate jdbcTp; @Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.DEFAULT,rollbackFor=Exception.class) @Override public int out(String name, BigDecimal qty) throws InvalidDataException { if (!isEnough(name,qty.abs())) { throw new InvalidDataException("库存不足"); } BigDecimal realQty=qty.abs().multiply(BigDecimal.valueOf(-1)); return updateQty(name,realQty); } }
3.代码-销售记录
public interface SaleInfoService { public int sale(String name,BigDecimal qty, BigDecimal price,BigDecimal totalPrice) throws InvalidDataException; } @Service public class SaleInfoServiceImpl implements SaleInfoService { @Autowired JdbcTemplate jdbcTp; @Autowired private InventoryService inventoryService; @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.DEFAULT, rollbackFor = Exception.class) @Override public int sale(String name, BigDecimal qty, BigDecimal price, BigDecimal totalPrice) throws InvalidDataException { if (qty.compareTo(BigDecimal.valueOf(10000))==1) { throw new InvalidDataException("一次最多销售10000"); } inventoryService.out(name, qty); String sql = "insert into sale_info(name,qty,price,total_amount) values(?,?,?,?)"; return jdbcTp.update(sql, name, qty, price, totalPrice); } ..... 其余略 }
4.代码-销售服务
public interface SaleMainService { public PublicReturn sale(SaleInfo saleInfo) throws InvalidDataException; } @Service public class SaleMainServiceImpl implements SaleMainService { @Autowired LogService logService; @Autowired SaleInfoService saleService; @Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.DEFAULT,rollbackFor=Exception.class) @Override public PublicReturn sale(SaleInfo saleInfo) throws InvalidDataException { /** * 要保证保证核心的业务逻辑即使失败了,也需要记录日志 */ SystemLog log=new SystemLog(1,"销售","销售",JSON.toJSONString(saleInfo)); log.setLogStartime(new Date()); logService.start(log); /** * 新一个事务-无论是否成功不影响主事务 * 出库,添加销售记录 */ boolean isFinished=false; String message=""; try { saleService.sale(saleInfo.getName(), saleInfo.getQty(),saleInfo.getPrice(), saleInfo.getTotalAmount()); isFinished=true; } catch(Exception e) { //这个catch不影响主事务,因为只针对saleService message=e.getMessage(); System.out.println(e.getMessage()); } if (isFinished) { log.setActionResult(GlobalConstant.LOG_RESULT_SUCCESSFUL); } else { log.setActionResult(GlobalConstant.LOG_RESULT_UNSUCCESSFUL); } log.setLogEndtime(new Date()); logService.end(log); if (isFinished) { return PublicReturn.getSuccessful(); } else { return PublicReturn.getUnSuccessful(message); } } }
注:
a.主事务是 SaleMainService.sale,它的传递特性是标准的REQUIRED(如果有用现有的,否则新建一个事务)
b.独立事务是SaleInfoService.sale,它的传递特性是REQUIRES_NEW(独立一个)
c.在主事务中SaleMainService.sale必须使用try..catch来处理独立事务"SaleInfoService.sale"。
当独立事务"SaleInfoService.sale"发生异常的时候,它自己会回滚(spring处理),之后需要捕获这个异常,避免让主事务回滚。
注意:事务代码中,并非不能做异常处理。至于为什么,仔细阅读原生jdbc的事务就明白了。
5.js代码
var settings = { "url": "http://localhost:9999/spring/tran/addSaleInfo", "method": "GET", "timeout": 0, "headers": { "Content-Type": "application/json" }, "data": JSON.stringify({ "name": "大米", "qty": 0.3, "price": 1000 }), }; $.ajax(settings).done(function (response) { console.log(response); });
四、小结
应付绝大部分的应用开发,使用本地事务和声明式事务管理即可,这也是spring官网文档说的。
spring的事务的实现实在是很不错。
强烈建议在开始学习之间,先了解rdbms的事务和保存点等等概念。