Spring的事务抽象
Spring提供了一致的事务管理抽象,该抽象能实现为不同的事务API提供一致的编程模型。无视我们使用jdbc、hibernate、mybatis哪种方式来操作数据,无视事务是jta事务还是jdbc事务。
事务
事务(transaction),一般是指要做的或所做的事情。在计算机术语中是指访问或者更新数据库中各项数据项的一个程序执行单元(unit)。事务通常由高级数据库操作语言或编程语言书写的用户程序的执行所引起,并用begin transaction
和 end transaction
语句来界定。
为什么需要事务
事务是为了解决数据安全操作提出的解决方案,事务的控制实际上就是控制数据的安全访问于隔离。举一个简单的例子:如果我们去银行转账,A账户将自己的1000元转账给B,那么业务实现的逻辑首先是将A的余额减少1000,然后往B的余额增加100,假如这个过程中出现意外,导致过程中断,A已经扣款成功,B还没来及增加,就会导致B损失了1000元,所以必须做出控制,要求A账户转帐业务撤销。这才能保证业务的正确性,完成这个操走就需要事务,将A账户资金减少和B账户资金增加放到同一个事务里,要么全部执行成功,要么全部撤销,这样就保证了数据的安全性。
事务的四大特性
- 原子性:事务是数据库的逻辑工作单位,而且必须是原子工作单位,对于其数据修改,要么全部执行,要么全部不执行。
- 一致性:事务在完成时,必须是所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。
- 隔离型:一个事务的执行不能被其他事务所影响。
- 持久性:一个事务一旦提交,事务的操作便永久性的保存在db中。即便是在数据库系统遇到故障的情况下也不会丢失。
Java事务类型
在Java中事务类型有三种:jdbc事务、jta事务以及容器事务
jdbc事务
在jdbc中处理事务,都是通过connection完成,在同一事务中所有的操作,都在使用同一个connection对象完成,jdbc默认是开启事务的,并且默认完成提交操作。而在jdbc中有三种事务有关的操作:
- setAutoCommit:设置是否自动提交事务,如果为true则表示自动提交,每一个sql独立存在一个事务,如果设置为false,则需要手动commit进行提交
- commit:手动提交事务
- rollback:手动回滚结束事务
使用jdbc事务的基本步骤如下:
@Test public void testTX(){ String url = "jdbc:mysql://127.0.0.1:3306/test"; String username = "root"; String password = "1234"; String sourceUserId = "leo"; String desUserId = "xnn"; int money = 500; Connection connection = null; try { //1.加载数据库驱动程序 Class.forName("com.mysql.jdbc.Driver"); //获得数据库连接 connection = DriverManager.getConnection(url, username, password); //开启事务 connection.setAutoCommit(false);//如果为true的话,sql语句会分别执行,修改数据库;如果为false的话,会激活事务 //多条数据操作数据 Statement sql = connection.createStatement(); sql.executeUpdate("UPDATE user_info SET balance = balance-" + money + " WHERE user_id = ‘" + sourceUserId+"‘"); sql.executeUpdate("UPDATE user_info SET balance = balance+" + money + " WHERE user_id = ‘" + desUserId+"‘"); //提交事务 connection.commit(); } catch (SQLException e) { e.printStackTrace(); try{ //回滚 connection.rollback(); }catch (SQLException ex){ } } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } }
从代码中我们可以看出来jdbc事务的缺点:
- 冗长、复杂
- 需要显示事务控制
- 需要显示处理受检查异常
并且jdbc仅仅是为了完成事务操作提供了基础的API支持,通过操作jdbc我们可以将多个sql语句放到同一个事务中,保证acid特性,但是当遇到跨库跨表的sql,简单的jdbc事务就无法满足了,基于这种问题,jta事务出现了。
jta事务
jta(Java transaction API)提供了跨数据库连接的事务管理能力。jta事务管理则由jta容器实现。
jta的构成
在jta中有几个重要的概念:
- 高层应用事务界定接口,供事务客户界定事务边界
- X/Open XA协议(资源之间的一种标准化的接口)的标准Java映射 ,它可以使事务性的资源管理器参与由外部事务管理器控制的事务中
- 高层事务管理器接口,允许应用程序为其管理的程序界定事务的边界范围
jta中的重要接口
jta的重要接口主要位于javax.transaction包中
UserTransaction
:让应用程序得以控制事务的开始、挂起、提交与回滚等操作,由java或者ejb组件调用TransactionManager
:用于应用服务管理事务的状态Transaction
:用于控制执行事务操作XAResource
:用于在分布式事务环境下,协调事务管理器和资源管理器的工作XID
:用来为事务标示的java映射id
需要注意的是前三个接口仅存在于javaee.jar中,在javase中并不存在。
jta事务编程的基本步骤
//配置JTA事务,建立对应的数据源 //1.建立事务:通过创建UserTransaction类的实例来开始一个事务 Context ctx = new InitialContext(p) ; UserTransaction trans = (UserTransaction) ctx.lookup("javax. Transaction.UserTransaction"); //开始事务 trans.begin(); //找到数据源,进行绑定 DataSource ds = (DataSource) ctx.lookup("mysqldb"); //建立数据库连接 Connection mycon = ds.getConnection(); //执行了sql操作 stmt.executeUpdate(sqlS); //提交事务 trans.commit(); //关闭连接 mycon.close();
jta事务的优缺点
可以看出,jta的优点很明显,提供了分布式下的事务解决方案,并且执行严格的acid操作,但是标准的jta事务在日常开发中并不常用,其原因就是jta的缺点导致的,例如jta的实现相当复杂,JTA UserTransaction需要从JNDI获取,即我们如果要实现JTA一般情况下也需要实现JNDI,并且JTA只是个简易的容器,使用复杂,在灵活的需求下很难实现代码复用,因为我们需要一个能给我们进行完成容器事务操作的框架
Spring的事务与事务抽象
Spring给我们封装了一套事务机制,并且提供了完善的事务抽象,将事务所需要的步骤进行抽象划分,并以编程的方式提供一个标准API,如下:
try{ //1.开启事务 //2.执行数据库操作 //3.提交事务 }catch(Exception ex){ //处理异常 //4.回滚事务 }finally{ //关闭连接,资源清理 }
Spring事务抽象的核心接口
Spring的抽象事务模型基于接口PlatformTransactionManager,该接口有不同的多种实现,每一种实现都有对应的一个特定的数据访问技术,大体如下:
PlatformTransactionManager及其相关属性
事务管理器接口PlatformTransactionManager通过getTransaction方法来得到事务,参数为TransactionDefinition类,这个类定义事务类的基本属性:
- 传播行为
- 隔离规则
- 回滚规则
- 事务超时设置
- 事务是否只读
public interface TransactionDefinition { int getPropagationBehavior(); // 返回事务的传播行为 int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据 int getTimeout(); // 返回事务必须在多少秒内完成 boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的 }
其中最重要的是事务的传播行为以及隔离规则
事务的七种传播行为如下所示:
传播性 | 值 | 描述 |
---|---|---|
PROPAGATION_REQUIRED | 0 | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中 |
PROPAGATION_SUPPORTS | 1 | 事务可有可无。有就支持当前事务,没有就以非事务方式执行 |
PROPAGATION_MANDATORY | 2 | 支持当前事务,如果当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 3 | 无论是否有事务都得新建个事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 4 | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 5 | 以非事务方式执行,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 6 | 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行 |
事务的隔离级别如下所示:
在看事务隔离级别前需要先了解下什么是脏读、不可重复读、幻读
- 脏读:脏读就是指当一个事务正在访问数据库,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务获取数据进行操作,结果将刚刚未提交的数据获取到了
- 不可重复读:不可重复读是指在一个事务内,多次读同一数据,前后读取的结果不一致。在事务A还没结束时,另外一个事务B也访问该同一数据。那么,在事务A中的两次读取数据的过程中,由于事务B对当前数据进行修改操作,导致事务A两次读取的数据不一致,因此称为是不可重复读
- 幻读:幻读是指当事务不是独立执行时发生的一种现象,例如事务A对表中的一个数据进行了修改,这种修改涉及到表中的全部数据行。同时事务B也修改了这个表中的数据,这种修改是向表中插入一行新数据。那么就会发生操作事务A的用户发现表中还存在没有修改的数据行,就好像发生了幻觉一样。
为了解决这些问题,事务的隔离级别就出现了,对应的效果如下:
隔离性 | 值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
DEFAULT | -1 | 使用数据库设置的隔离级别 | ||
READ_UNCOMMITTED | 1 | ?? | ?? | ?? |
READ_COMMITTED | 2 | ?? | ?? | ?? |
REPEATABLE_READ | 3 | ?? | ?? | ?? |
SERIALIZABLE | 4 | ?? | ?? | ?? |
Spring和数据库都有事务隔离级别。Spring默认隔离级别是按照数据库的隔离级别处理的。
与不同数据访问技术对应的PlatformTransactionManager实现
TransactionManager类 | 数据库访问技术 |
---|---|
DataSourceTransactionManager | 在仅使用JDBC时适用 |
HibernateTransactionManager | 在适用Hibernate而没有适用JPA时适用。同时在实现时还可能使用JDBC |
JtaTransactionManager | 在使用全局事务时适用 |
可以看到Spring并不是提供了完整的事务操作API,而是提供了多种事务管理器,将事务的职责托管给了Hibernate、JTA等持久化机制平台框架来实现,而仅仅提供一个通用的事务管理器接口org``.``springframework``.``transaction.PlatformTransactionManager
,并且给各大持久化事务平台框架提供了对应的事务管理器,用来限制其通用行为,但是具体事务实现将由各大平台自己去实现:
Public interface PlatformTransactionManager{ // 由TransactionDefinition得到TransactionStatus对象 TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; // 提交 Void commit(TransactionStatus status) throws TransactionException; // 回滚 Void rollback(TransactionStatus status) throws TransactionException; }
在Spring中使用事务
Spring中使用事务,可以直接在业务中以编程的方式使用;也可以通过注解以声明式形式使用。
编程式事务
Spring提供了TransactionTemplate工具类可以很方便的使用编程式事务。默认情况下TransactionTemplate使用的是DataSourceTransactionManager。
package com.lucky.spring; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; /** * Created by zhangdd on 2020/7/26 */ @SpringBootApplication @Slf4j public class Application implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Autowired JdbcTemplate jdbcTemplate; @Autowired TransactionTemplate transactionTemplate; @Override public void run(String... args) throws Exception { log.info("default transaction manage:{}", transactionTemplate.getTransactionManager().getClass().getSimpleName()); log.info("count before transaction:{}", getCount()); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { jdbcTemplate.execute("insert into product(name,description) values (‘spring boot in action‘,‘书籍 spring boot in action‘) "); log.info("count in transaction:{}", getCount()); transactionStatus.setRollbackOnly(); } }); log.info("count after transaction:{}", getCount()); } private long getCount() { return (long) jdbcTemplate.queryForList("select count(*) as cnt from product") .get(0).get("cnt"); } }
打印结果如下:
2020-07-26 18:33:38.839 INFO 41176 --- [ main] com.lucky.spring.Application : default transaction manage:DataSourceTransactionManager 2020-07-26 18:33:38.842 INFO 41176 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2020-07-26 18:33:39.051 INFO 41176 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2020-07-26 18:33:39.071 INFO 41176 --- [ main] com.lucky.spring.Application : count before transaction:7 2020-07-26 18:33:39.082 INFO 41176 --- [ main] com.lucky.spring.Application : count in transaction:8 2020-07-26 18:33:39.087 INFO 41176 --- [ main] com.lucky.spring.Application : count after transaction:7
从打印结果中可以看到:
- TransactionTemplate默认使用的是DataSourceTransactionManager
- TransactionTemplate通过execute方法完成了事务的操作
声明式事务
如上图所示Spring的声明式事务是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中
使用@Transactional注解
@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该注解来覆盖类级别的定义。
虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
@Transactional的rollbackFor属性可以设置一个 Throwable 的数组,用来表明如果方法抛出这些异常,则进行事务回滚。默认情况下如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚。
package com.lucky.spring.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * Created by zhangdd on 2020/7/26 */ @Service public class ProductServiceImpl implements ProductService { @Autowired JdbcTemplate jdbcTemplate; @Override //通过设置@Transactional就开启了事务 @Transactional public void insertRecord() { jdbcTemplate.execute("insert into product(name,description) values (‘spring boot in action 1‘,‘书籍 spring boot in action 1‘) "); } @Override @Transactional(rollbackFor = RuntimeException.class) public void insertThenRollback() throws RuntimeException { jdbcTemplate.execute("insert into product(name,description) values (‘spring boot in action 2‘,‘书籍 spring boot in action 2‘) "); throw new RuntimeException(); } /** * 调用带有事务注解的方法 * 这种形式 事务不会回滚,即数据会插入到数据库里 * * @throws RuntimeException */ @Override public void invokeInsertThenRollback() throws RuntimeException { insertThenRollback(); } /** * 调用的普通方法发生了异常 * <p> * 数据会回滚 */ @Override @Transactional(rollbackFor = RuntimeException.class) public void insertRecordWhenCrash() { jdbcTemplate.execute("insert into product(name,description) values (‘spring boot in action 3‘,‘书籍 spring boot in action 3‘) "); crash(); } private void crash() throws RuntimeException { throw new RuntimeException(); } }
- @Transactional是以代理的形式完成的事务。invokeInsertThenRollback()方法没有被@Transactional修饰,所以即使内部调用insertThenRollback()这个方法事务也没有生效。