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配置事务效果是一样的。

相关推荐
纯纯沙口13 分钟前
Qt—用SQLite实现简单的注册登录界面
数据库·sqlite
初次见面我叫泰隆26 分钟前
MySQL——3、数据类型
数据库·mysql
zxrhhm1 小时前
Oracle 中的虚拟列Virtual Columns和PostgreSQL Generated Columns生成列
postgresql·oracle·vr
一叶屋檐1 小时前
Neo4j 图书馆借阅系统知识图谱设计
服务器·数据库·cypher
好吃的肘子2 小时前
MongoDB 应用实战
大数据·开发语言·数据库·算法·mongodb·全文检索
weixin_472339462 小时前
MySQL MCP 使用案例
数据库·mysql
lqlj22333 小时前
Spark SQL 读取 CSV 文件,并将数据写入 MySQL 数据库
数据库·sql·spark
遗憾皆是温柔3 小时前
MyBatis—动态 SQL
java·数据库·ide·sql·mybatis
未来之窗软件服务4 小时前
Cacti 未经身份验证SQL注入漏洞
android·数据库·sql·服务器安全
fengye2071614 小时前
在MYSQL中导入cookbook.sql文件
数据库·mysql·adb