聊一聊Spring的事务管理

1,910 阅读8分钟

个人简介:荡不羁,一生所爱。Java耕耘者(微信公众号ID:Java耕耘者),欢迎关注。可获得2000G详细的2020面试题的资料

**事务的定义:**事务是指多个操作单元组成的合集,多个单元操作是整体不可分割的,要么都操作不成功,要么都成功。其必须遵循四个原则(ACID)。

  • 原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做;

  • 一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是应该处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A转帐给B,必须保证A的钱一定转给B,一定不会出现A的钱转了但B没收到,否则数据库的数据就处于不一致(不正确)的状态。

  • 隔离性(Isolation):并发事务执行之间互不影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性;

  • 持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失。

Spring提供以下两种方式管理事务:

  • 声明式事务管理(基于配置方式实现事务控制):底层是建立在Spring AOP的基础上,在方式执行前后进行拦截,并在目标方法开始执行前创建新事务或加入一个已存在事务,最后在目标方法执行完后根据情况提交或者回滚事务。声明式事务的最大优点就是不需要编程,将事务管理从复杂业务逻辑中抽离,只需要在配置文件中配置并在目标方法上添加@Transactional注解即可实现。

  • 编程式事务管理(基于Java编程实现事务控制),不推荐用!:允许用户在实现代码中使用显式的方式调用beginTransaction()开启事务、commit()提交事务、rollback()回滚事务,从而可以达到精确定义事务的边界。

Spring常用的事务类型(传播属性)****,在事务定义的接口TransactionDefinition中定义了7个表示传播行为的常量:

  • PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是默认值。(在方法@Transactional注解时使用方式:@Transactional(propagation=Propagation.REQUIRED,以下类似)

  • PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。

  • PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异

  • PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起

  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

  • PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常

  • PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作。拥有多个可以回滚的保存点,内部回滚不会对外部事务产生影响。只对DataSourceTransactionManager有效。

Spring事务的隔离级别:

Spring事务的隔离级别定义了一个事务可能受到其他并发事务影响的程度,在事务定义接口TransactionDefinition 中定义了五个表示隔离级别的常量,分别是:

  • ISOLATION_DEFAULT:这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,另外四个与JDBC的隔离级别相对应;读取未提交数据(会出现脏读, 不可重复读) 基本不使用。(在方法@Transactional注解时使用方式:@Transactional(isolation = Isolation.READ_UNCOMMITTED),以下类似)(MYSQL: 默认为REPEATABLE_READ级别 SQLSERVER: 默认为READ_COMMITTED)

  • ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据,这种隔离级别会产生脏读,不可重复读和幻像读。 (读取未提交数据(会出现脏读, 不可重复读) 基本不使用)

  • ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据 (读取已提交数据(会出现不可重复读和幻读))

  • ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读,它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读);(可重复读(会出现幻读))

  • ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行,除了防止脏读,不可重复读外,还避免了幻像读。(串行化)

一、声明式事务管理

1、在applicationContext.xml配置文件中使用xml方式配置事务:


<tx:advice id="txAdvice" transaction-manager="txManager">

tx:attributes

<tx:method name="execute" propagation="REQUIRED"/>

<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

<aop:config proxy-target-class="true">
<aop:pointcut id="actionPointcut" expression="within(com.tarena.netctoss.action..*)" />

<aop:advisor advice-ref="txAdvice" pointcut-ref="actionPointcut"/>
</aop:config>

2、在applicationContext.xml配置文件中使用注解方式配置:

step1:定义HibernateTransactionManager(事务管理)Bean组件



step2:开启事务的注解配置

<tx:annotation-driven proxy-target-class="true" transaction-manager="txManager"/>
step3:然后在业务组件的类定义前或方法中使用@Transactional注解即可,例如:
①AddCostAction,在类定义前使用(对类中除了get/set方法外的所有方法,都使用相同的事务管理)
@Transactional(propagation=Propagation.REQUIRED)
public class AddCostAction extends BaseAction{ …… }
②DeleteCostAction,在方法前使用
@Transactional(propagation=Propagation.REQUIRED)
public String execute() throws DAOException{ …… }
③UpdateCostAction,在类定义前使用
@Transactional(propagation=Propagation.REQUIRED)
public class UpdateCostAction extends BaseAction { …… }
④ListCostAction,在方法前使用
@Transactional(readOnly=true,propagation=Propagation.REQUIRED)
public String execute() throws Exception { …… }
注意事项:如果将Action当作目标,需要在tx:annotation-driven添加proxy-target-class="true"属性,表示不管有没有接口,都采用CGLIB方式生成代理类。

二、编程式事务管理

Spring实现编程式事务,依赖于2大类,分别是PlatformTransactionManager,与模版类TransactionTemplate(推荐使用)。
下面分别详细介绍Spring是如何通过该类实现事务管理。

1、事务管理器配置

5 30 10 60 5 0 60 30 true false  

2在业务中使用代码(以测试类展示)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring-public.xml" })
public class test {
@Resource
private PlatformTransactionManager txManager;
@Resource
private DataSource dataSource;
private static JdbcTemplate jdbcTemplate;
Logger logger=Logger.getLogger(test.class);
private static final String INSERT_SQL = "insert into testtranstation(sd) values(?)";
private static final String COUNT_SQL = "select count(*) from testtranstation";
@Test
public void testdelivery(){
//定义事务隔离级别,传播行为,
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//事务状态类,通过PlatformTransactionManager的getTransaction方法根据事务定义获取;获取事务状态后,Spring根据传播行为来决定如何开启事务
TransactionStatus status = txManager.getTransaction(def);
jdbcTemplate = new JdbcTemplate(dataSource);
int i = jdbcTemplate.queryForInt(COUNT_SQL);
System.out.println("表中记录总数:"+i);
try {
jdbcTemplate.update(INSERT_SQL, "1");
txManager.commit(status); //提交status中绑定的事务
} catch (RuntimeException e) {
txManager.rollback(status); //回滚
}
i = jdbcTemplate.queryForInt(COUNT_SQL);
System.out.println("表中记录总数:"+i);
}
}

 3、使用TransactionTemplate

TransactionTemplate模板类使用的回调接口:
TransactionCallback:通过实现该接口的“T doInTransaction(TransactionStatus status) ”方法来定义需要事务管理的操作代码;
TransactionCallbackWithoutResult:继承TransactionCallback接口,提供“void doInTransactionWithoutResult(TransactionStatus status)”便利接口用于方便那些不需要返回值的事务操作代码。

该类继承了接口DefaultTransactionDefinition,用于简化事务管理,事务管理由模板类定义,主要是通过TransactionCallback回调接口或TransactionCallbackWithoutResult回调接口指定,通过调用模板类的参数类型为TransactionCallback或TransactionCallbackWithoutResult的execute方法来自动享受事务管理。

还是以测试类方式展示如何实现:

@Test  public void testTransactionTemplate(){      jdbcTemplate = new JdbcTemplate(dataSource);      int i = jdbcTemplate.queryForInt(COUNT_SQL);        System.out.println("表中记录总数:"+i);      //构造函数初始化TransactionTemplate      TransactionTemplate template = new TransactionTemplate(txManager);      template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);        //重写execute方法实现事务管理      template.execute(new TransactionCallbackWithoutResult() {          @Override          protected void doInTransactionWithoutResult(TransactionStatus status) {              jdbcTemplate.update(INSERT_SQL, "饿死");   //字段sd为int型,所以插入肯定失败报异常,自动回滚,代表TransactionTemplate自动管理事务          }}      );      i = jdbcTemplate.queryForInt(COUNT_SQL);        System.out.println("表中记录总数:"+i);  } 

PS:数据库知识扩展:

1、注意事务和锁:

事务是数据库应用中和重要的工具,它有原子性、一致性、隔离性、持久性这四个属性,很多操作我们都需要利用事务来保证数据的正确性。在使用事务中我们需要做到尽量避免死锁、尽量减少阻塞。

具体以下方面需要特别注意:
A、事务操作过程要尽量小,能拆分的事务要拆分开来。
B、 事务操作过程不应该有交互,因为交互等待的时候,事务并未结束,可能锁定了很多资源。
C、 事务操作过程要按同一顺序访问对象。
D、提高事务中每个语句的效率,利用索引和其他方法提高每个语句的效率可以有效地减少整个事务的执行时间。
E、 尽量不要指定锁类型和索引,SQL SERVER允许我们自己指定语句使用的锁类型和索引,但是一般情况下,SQL SERVER优化器选择的锁类型和索引是在当前数据量和查询条件下是最优的,我们指定的可能只是在目前情况下更有,但是数据量和数据分布在将来是会变化的。
F、 查询时可以用较低的隔离级别,特别是报表查询的时候,可以选择最低的隔离级别(未提交读)。

2、数据库分为本地事务跟全局事务

本地事务:普通事务,独立一个数据库,能保证在该数据库上操作的ACID。
分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库;