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

Spring.png

6 声明式事务

6.1 Spring中事务的使用

在进行数据操做事,一般会将多条SQL语句做为总体进行操做,这一条或者多条SQL语句就称为数据库事务。数据库事务能够确保该事务范围内的全部操做均可以所有成功或者所有失败。若是事务失败,那么效果就和没有执行这些SQL同样,不会对数据库数据有任何改动。mysql

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

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

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

Spring中能够经过@Transactional注解,实现了对事务的支持。面试

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

@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测试表:sql

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操做数据库,数据库

@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注入进来后端

@Service
public class OrderService {
    @Autowired
	private OrderDao orderDao;
    @Transactional
    public void addOrder(){
    	orderDao.insert();
    	System.out.println("操做完成.........");
    	
    	//int a = 1/0;
    }
}

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

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操做进行了回滚,保证了事务的一致性。app

@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,下面分析这两个组件的功能。

Screen Shot 2020-02-17 at 10.14.19 PM.png

AutoProxyRegistrar.class

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

Screen Shot 2020-02-17 at 10.25.28 PM.png

ProxyTransactionManagementConfiguration.class

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

Screen Shot 2020-02-17 at 11.27.29 PM.png

拦截执行流程

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

事务拦截器是TransactionInterceptor,它也是MethodInterceptor的子类,下面是其执行时的主要逻辑,概括能够分为以下几步:

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

Screen Shot 2020-02-17 at 11.25.32 PM.png

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种隔离级别:

DEFAULT(-1),  ## 数据库默认级别
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);

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

@Transactional(isolation = Isolation.SERIALIZABLE)
public int insertUser(User user){
    return userDao.insertUser(user);
}

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

传播行为

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

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

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方法就是自调用,如:

@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。
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的动态代理了。


参考:


本文由『后端精进之路』原创,首发于博客 http://teckee.github.io/ , 转载请注明出处

搜索『后端精进之路』关注公众号,马上获取最新文章和价值2000元的BATJ精品面试课程

后端精进之路.png

相关文章
相关标签/搜索