Spring MVC系列-(6) 声明式事务

2023-10-19 20:20:56 浏览数 (1)

6 声明式事务

6.1 Spring中事务的使用

在进行数据操作事,通常会将多条SQL语句作为整体进行操作,这一条或者多条SQL语句就称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。

事务是恢复和并发控制的基本单位。

事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

  • 原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
  • 一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

Spring中可以通过@Transactional注解,实现了对事务的支持。

首先定义配置类,配置类中创建了数据源,封装了jdbcTemplate和事务管理器。

代码语言:javascript复制
@Configuration
@ComponentScan("com.enjoy.cap11")
@EnableTransactionManagement  //开启事务管理功能,对@Transactional起作用
public class Cap11MainConfig {
	//创建数据源
	@Bean
	public DataSource dataSource() throws PropertyVetoException{
		//这个c3p0封装了JDBC, dataSource接口的实现
		ComboPooledDataSource dataSource = new ComboPooledDataSource();
		dataSource.setUser("root");
		dataSource.setPassword("xxxxx");
		dataSource.setDriverClass("com.mysql.jdbc.Driver");
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/Spring?useSSL=false");
		return dataSource;
	}
	
	//jdbcTemplate能简化增删改查的操作
	@Bean
	public JdbcTemplate jdbcTemplate() throws PropertyVetoException{
		return new JdbcTemplate(dataSource());
	}
	//注册事务管理器
	@Bean
	public PlatformTransactionManager platformTransactionManager() throws PropertyVetoException{
		return new DataSourceTransactionManager(dataSource());
	}
}

新建Order测试表:

代码语言:javascript复制
CREATE TABLE `order` (
  `orderid` int(11) DEFAULT NULL,
  `ordertime` datetime DEFAULT NULL,
  `ordermoney` decimal(20,0) DEFAULT NULL,
  `orderstatus` char(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8

新建OrderDao操作数据库,

代码语言:javascript复制
@Repository
public class OrderDao {
	@Autowired
	private JdbcTemplate jdbcTemplate;
	//操作数据的方法
	public void insert(){
		String sql = "insert into `order` (ordertime, ordermoney, orderstatus) values(?,?,?)";
		jdbcTemplate.update(sql,new Date(),20,0);
	}
}

新建 OrderService类,将orderDao注入进来

代码语言:javascript复制
@Service
public class OrderService {
    @Autowired
	private OrderDao orderDao;
    @Transactional
    public void addOrder(){
    	orderDao.insert();
    	System.out.println("操作完成.........");
    	
    	//int a = 1/0;
    }
}

在下面的测试用例中,正常的向数据库中插入一条数据,查询数据库可以发现插入正常。

代码语言:javascript复制
public class Cap11Test {
	@Test
	public void test01(){
		AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(Cap11MainConfig.class);
		
		OrderService bean = app.getBean(OrderService.class);
		bean.addOrder();
		
		app.close();
	}
}

但是接着测试,在addOrder方法中手动设置一个异常,下面的代码中,在运行时会抛出除数为0的异常。从运行结果可以看到,这种情况下数据库的插入操作没有成功,说明Spring对insert操作进行了回滚,保证了事务的一致性。

代码语言:javascript复制
@Service
public class OrderService {
    @Autowired
	private OrderDao orderDao;
    @Transactional
    public void addOrder(){
    	orderDao.insert();
    	System.out.println("操作完成.........");
    	
    	int a = 1/0;
    }
}

6.2 Spring事务原理分析

在上面的例子中,为了使事务能够生效,需要加上@EnableTransactionManagement注解,整个源码实现和AOP原理一致,在注册Bean时对对象进行包装,生成增强的Bean,返回代理对象。在执行阶段,利用事务拦截器来运行有事务注解的代码,当出现异常时进行回滚。

通过@EnableTransactionManagement引入的class可以看到,默认PROXY模式下,会引入AutoProxyRegistrar.classProxyTransactionManagementConfiguration.class,下面分析这两个组件的功能。

AutoProxyRegistrar.class

从下面的代码可以看到,和AOP类似,该组件会往容器中注册InfrastructureAdvisorAutoProxyCreator,利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用。

ProxyTransactionManagementConfiguration.class

事务增强器要用事务注解的信息,AnnotationTransactionAttributeSource解析事务注解。

拦截执行流程

和AOP类似,在拦截执行的时候,首先会获取拦截链,然后依次执行拦截器的proceed方法。

事务拦截器是TransactionInterceptor,它也是MethodInterceptor的子类,下面是其执行时的主要逻辑,归纳可以分为如下几步:

  1. 先获取事务相关的属性
  2. 再获取PlatformTransactionManager,如果事先没有添加指定任何transactionmanger 最终会从容器中按照类型获取一个PlatformTransactionManager;
  3. 执行目标方法
  • 如果异常,获取到事务管理器,利用事务管理回滚操作;
  • 如果正常,利用事务管理器,提交事务。

6.3 Spring的事务隔离级别与传播性

隔离级别

隔离性(Isolation)作为事务特性的一个关键特性,它要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,在数据库层面都是使用锁来实现。

在辨析不同的隔离级别之前,引入几个基本概念:

1. 脏读 :脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

2. 不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。简单来讲就是,事务 A 读取了事务 B 已提交的更改数据。

3. 幻读 : 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。简单来讲就是,事务 A 读取了事务 B 已提交的新增数据。

事务的隔离级别从低到高有以下四种:

  • READ UNCOMMITTED(未提交读):这是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。READ UNCOMMITTED是一种危险的隔离级别,在实际开发中基本不会使用,主要是由于它会带来脏读问题。

脏读对于要求数据一致性的应用来说是致命的,目前主流的数据库的隔离级别都不会设置成READ UNCOMMITTED。不过脏读虽然看起来毫无用处,但是它主要优点是并发能力高,适合那些对数据一致性没有要求而追求高并发的场景。

  • READ COMMITTED(读写提交): 它是指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。READ COMMITTED会带来不可重复读的问题。

一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库如(ORACLE,SQL SERVER)将其默认隔离级别设置为READ COMMITTED,允许不可重复读的现象。

  • REPEATABLE READ (可重复读):对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生。
  • SERIALIZABLE(串行化):数据库最高的隔离级别,它要求所有的SQL都会按照顺序执行,这样可以克服上述所有隔离出现的各种问题,能够完全包住数据的一致性。

Spring中可以配置5种隔离级别:

代码语言:javascript复制
DEFAULT(-1),  ## 数据库默认级别
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);

可以使用类似下面的注解,很方便的配置隔离级别:

代码语言:javascript复制
@Transactional(isolation = Isolation.SERIALIZABLE)
public int insertUser(User user){
    return userDao.insertUser(user);
}

上面的代码中我们使用了串行化的隔离级别来包住数据的一致性,这使它将阻塞其他的事务进行并发,所以它只能运用在那些低并发而又需要保证数据一致性的场景下。

传播行为

在Spring中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。

在Spring的事务机制中对数据库存在7种传播行为,通过枚举类Propagation定义。

代码语言:javascript复制
public enum Propagation {
    /**
     * 需要事务,默认传播性行为。
     * 如果当前存在事务,就沿用当前事务,否则新建一个事务运行子方法
     */
    REQUIRED(0),
    /**
     * 支持事务,如果当前存在事务,就沿用当前事务,
     * 如果不存在,则继续采用无事务的方式运行子方法
     */
    SUPPORTS(1),
    /**
     * 必须使用事务,如果当前没有事务,抛出异常
     * 如果存在当前事务,就沿用当前事务
     */
    MANDATORY(2),
    /**
     * 无论当前事务是否存在,都会创建新事务允许方法
     * 这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立
     */
    REQUIRES_NEW(3),
    /**
     * 不支持事务,当前存在事务时,将挂起事务,运行方法
     */
    NOT_SUPPORTED(4),
    /**
     * 不支持事务,如果当前方法存在事务,将抛出异常,否则继续使用无事务机制运行
     */
    NEVER(5),
    /**
     * 在当前方法调用子方法时,如果子方法发生异常
     * 只回滚子方法执行过的SQL,而不回滚当前方法的事务
     */
    NESTED(6);
}

日常开发中基本只会使用到REQUIRED(0),REQUIRES_NEW(3),NESTED(6)三种。

NESTEDREQUIRES_NEW是有区别的。NESTED传播行为会沿用当前事务的隔离级别和锁等特性,而REQUIRES_NEW则可以拥有自己独立的隔离级别和锁等特性。

NESTED的实现主要依赖于数据库的保存点(SAVEPOINT)技术,SAVEPOINT记录了一个保存点,可以通过ROLLBACK TO SAVEPOINT来回滚到某个保存点。如果数据库支持保存点技术时就启用保存点技术;如果不支持就会新建一个事务去执行代码,也就相当于REQUIRES_NEW。

Transactional自调用失效

如果一个类中自身方法的调用,我们称之为自调用。如一个订单业务实现类OrderServiceImpl中有methodA方法调用了自身类的methodB方法就是自调用,如:

代码语言:javascript复制
@Transactional
public void methodA(){
    for (int i = 0; i < 10; i  ) {
        methodB();
    }
}
    
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int methodB(){
    ......
}

在上面方法中不管methodB如何设置隔离级别和传播行为都是不生效的。即自调用失效。

这主要是由于@Transactional的底层实现原理是基于AOP实现,而AOP的原理是动态代理,在自调用的过程中是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,于是就发生了自调用失败的现象。

要克服这个问题,有2种方法:

  • 编写两个Service,用一个Service的methodA去调用另外一个Service的methodB方法,这样就是代理对象的调用,不会有问题;
  • 在同一个Service中,methodA不直接调用methodB,而是先从Spring IOC容器中重新获取代理对象OrderServiceImpl,获取到后再去调用methodB。说起来有点乱,还是show you the code。
代码语言:javascript复制
public class OrderServiceImpl implements OrderService,ApplicationContextAware {
    private ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Transactional
    public void methodA(){
        OrderService orderService = applicationContext.getBean(OrderService.class);
        for (int i = 0; i < 10; i  ) {
            orderService.methodB();
        }
    }

    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    public int methodB(){
        ......
    }
}

上面代码中我们先实现了ApplicationContextAware接口,然后通过applicationContext.getBean()获取了OrderService的接口对象。这个时候获取到的是一个代理对象,也就能正常使用AOP的动态代理了。


参考:

  • https://juejin.im/post/5d909a98518825092046e83b

0 人点赞