Spring 事务管理

一、简述

事务是逻辑上的一组操作,要么都执行,要么都不执行。它具有4个属性:

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性: 执行事务前后,数据保持一致;
  • 隔离性: 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

二、事务五大属性

1. 传播行为

​ 传播行为(propagation behavior)定义了客户端与被调用方法之间的事务边界。即何时要创建一个事务,或者何时使用已有的事务:

传播行为 含义
PROPAGATION_MANDATORY 表示该方法必须在事务中进行,如果当前事务不存在,则会 抛出一个异常
PROPAGATION_NESTED 如果当前已存在一个事务,那么该方法在嵌套事务中运行。 嵌套事务可以独立于当前事务进行单独地提交或回滚。如 果当前事务不存在,那么行为和 PROPAGATION_REQUIRED一样
PROPAGATION_NEVER 表示当前方法不运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务上下文中,如果存在当前事务,在该方法运行期间,当前事务会被挂起
PROPAGATION_REQUIRED 表示当前方法必须运行在事务中,如果事务不存在,则启动一个新的事务
PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将启动。如果存在当前事务,当前事务会被挂起
PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行

2. 隔离级别

隔离级别(isolation level)定义了一个事务可能受其他并发事务的影响程度。并发操作相同的数据可能产生一些问题:

  1. 脏读(Dirty reads)------ 发生在一个事务读取了另一个事务改写后但未提交的数据,如果改写被回滚了,那么第一个事务获取的数据就是"脏"的。

  2. 不可重复读(Nonrepeatable read)------ 一个事务执行两次以上相同查询得到不同的数据。这通常是另外一个事务在此期间更新了数据。

  3. 幻读(Phantom read)------ 一个事务读取了几行数据,另一个事务插入了几条数据,当第一个事务再次读取时发现多了几条原本没有的数据。

隔离级别如下表所示:

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的数据表更。可能导致脏读,不可重复读,幻读
ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但不可重复读,幻读仍可能发生
ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果一致,除非数据是本事务自己修改的。可以阻止脏读,不可重复读,但仍可能发生幻读
REPEATABLE_SERIALIZABLE 完全服从事务的ACID原则,避免脏读,不可重复读,幻读

可以看出,ISOLATION_READ_UNCOMMITTED隔离级别是最低的,可能导致脏读,不可重复读,幻读。REPEATABLE_SERIALIZABLE隔离级别最高,但是这会降低数据读取速率,它通常是通过完全锁定事务相关的数据库表来实现的。

3. 事务只读

只读(read-only) 如果事务只读数据库进行读操作,那么设置该属性为true可以给数据库执行优化措施。因为只读是在事务启动,由数据库实施的,所以对那些具备启动一个新的事务的传播行为(PROPAGATION_REQUIREDPROPAGATION_REQUIRED_NEWPROPAGATION_NESTED)才有意义。

设置只读还会使Hibernate的flush模式被设置为FLUSH_NEVER。

4. 事务超时

事务超时是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。假如事务运行时间过长,则会影响效率,所以可以设置超时属性,超时后执行自动回滚。因为超时时钟会在事务开启时启动,所以只有对那些具备启动一个新的事务的传播行为(PROPAGATION_REQUIREDPROPAGATION_REQUIRED_NEWPROPAGATION_NESTED)才有意义。

5. 回滚原则

默认情况遇到运行期异常就回滚。回滚原则可以定义遇到哪些异常不回滚。这些规则定义了哪些异常会导致事务回滚而哪些不会 。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚。

三、XML中定义事务

使用tx命名空间配置:

xml 复制代码
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
         <!--指定哪种规则的方法上面添加事务-->
        <tx:method name="add*" propagation="REQUIRED"/>
        <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
    </tx:attributes>
</tx:advice>

transaction-manager为上述的事务管理器。

<tx:method>元素为某个(某些)name属性指定的方法定义事务参数。

<tx:method>有多个属性来帮助定义方法的事务策略,这些属性对于上述的事务五大属性:

属性 含义
isolation 指定隔离级别
propagation 指定传播原则
read-only 指定事务为只读
回滚原则: rollback-for no-rollback-for rollback-for指定事务为哪些检查型异常回滚 no-rollback-for指定事务对哪些异常继续运行而不回滚
timeout 设定事务超时时间

我们还需设定哪些Bean应该被通知,使用aop定义一个通知器(advisor):

xml 复制代码
<aop:config>
    <aop:advisor
        pointcut="execution(* *..UserDaoImpl.*(..))"	
        advice-ref="txAdvice"/>
</aop:config>

advice-ref属性引用了名为txAdvice的通知。

现在测试添加事务后的addUser()方法:

java 复制代码
public class TestTransaction {
    public static void main(String[] args) {
        ApplicationContext ac
            = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao dao=ac.getBean("userDao",UserDao.class);
        User u=new User();
        u.setName("testTx");
        u.setAge("30");
        u.setPassword("123456");
        dao.addUser(u);
    }
}

控制台抛出异常:

java 复制代码
Exception in thread "main" java.lang.NullPointerException
	at com.spring.dao.UserDaoImpl.addUser(UserDaoImpl.java:51)

查询数据:

sql 复制代码
SQL> select * from lzp.userinfo;
 
USERI NAME       AGE PWD
----- ---------- --- ----------
8     testUser   24  123456
4     SCOTT      25  123456

可以发现testTx并没有被插入。

四、注解定义事务

除了使用XML定义事务,我们还可以注解事务,通过声明:

xml 复制代码
<tx:annotation-driven transaction-manager="transactionManager"/>

<tx:annotation-driven>告诉Spring检查上下文中所有使用@Transaction注解的Bean,不管这个注解是在类级别上还是方法级别上。

使用注解修改UserDaoImpl:

java 复制代码
@Repository("userDao")
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
public class UserDaoImpl implements UserDao {
    //...
    @Transactional(propagation=Propagation.REQUIRED,readOnly=false)
    public void addUser(User user) {
        Map<String, Object> params=new HashMap<String, Object>();
        params.put("name", user.getName());
        params.put("age", user.getAge());
        params.put("pwd", user.getPassword());
        jdbcTemplate.update(SQL_INSERT_USER, params);
        String a=null;
        a.toString();
    }
    //...
}

再次运行testTransaction()方法,抛出异常后查询数据库:

SQL> select * from lzp.userinfo;
 
USERI NAME       AGE PWD
----- ---------- --- ----------
8     testUser   24  123456
4     SCOTT      25  123456

可发现和XML配置事务效果是一样的。

相关推荐
LaoZhangGong12318 分钟前
Linux第95步_Linux内核中的INPUT子系统
linux·运维·数据库·经验分享·stm32·input·stm32mp127
雷神乐乐24 分钟前
MyBatis中的${}和#{}区别
数据库·sql·mybatis·javaweb
Allen Bright1 小时前
如何使用Jedis连接Redis
数据库·redis·缓存
jlting1952 小时前
Flink转换算子——flatMap/map/filter/keyby/reduce综合案例
数据库·flink
锅总2 小时前
GitLab 备份与恢复
数据库·gitlab
sxy1993sxy20183 小时前
多线程下使用数据库 - 20241124
数据库
律队i3 小时前
【数据库原理】创建数据库,SQL语句
数据库·sql
halfbloodghost3 小时前
【数据库】SQL语句简单运用
数据库·sql·oracle
是老余3 小时前
【数据库入门】关系型数据库入门及SQL语句的编写
数据库·sql
ejinxian3 小时前
SQLite 管理工具 SQLiteStudio 3.4.5 发布
数据库·oracle·sqlite