Spring的Transaction实现

事务是什么?

事务是一组操作数据库的动作集合。事务具有ACID特性:
1.A原子性:事务中的操作要么全做,要么全不做
2.C一致性:事务完成操作后,数据库从一种状态到另一种状态
3.I隔离性:一个事务在进行操作不受到另一个事务的影响
4.D永久性:事务一旦提交,将不可撤回


事务传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行Spring定义了七种传播行为:
PROPAGATION_REQUIRED(如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务,这是最常见的选择)
PROPAGATION_SUPPORTS(如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行)
PROPAGATION_MANDATORY(如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常)
PROPAGATION_REQUIRED_NEW(总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起)
PROPAGATION_NOT_SUPPORTED(总是非事务地执行,并挂起任何存在的事务)
PROPAGATION_NEVER(总是非事务地执行,如果存在一个活动事务,则抛出异常。)
PROPAGATION_NESTED(如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事件,则按PROPAGATION_REQUIRED执行)


隔离规则

讲隔离规则之前先要说一下脏读、不可重复读、幻读集中概念。
脏读(Dirty reads): 脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
不可重复读(不可重读): 不可重读读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。
幻读(Phantom read):幻读与不可重读读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。

隔离级别
ISOLATION_DEFAULT:使用后端数据库默认的隔离级别。
ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读,幻读或不可重复读。
ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读,不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的。


其他的事务属性

事务超时:使用timeout来指定强制回滚之前,事务可以占用的时间。默认是30秒。
是否只读:使用readOnly来指定事务是否为只读,它表示这个事务只读取数据不更新数据,这样可以帮助数据库引擎优化事务。若真的是一个只读取数据库值的方法,应设置readOnly=true。


Spring中的事务

Spring中支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理:使用TransactionTemplate或者直接使用底层的PlatformTransactionManager,不过编程式事务管理现在已经很少再使用了(已过时)。
声明式事务管理:建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。


前提资源

建立表格:

1
2
3
4
5
6
7
8
CREATE TABLE `students` (
`student_id` int(11) NOT NULL AUTO_INCREMENT,
`student_name` varchar(20) NOT NULL,
`student_sex` char(1) DEFAULT NULL,
`student_birthday` date DEFAULT NULL,
`group_id` varchar(20) DEFAULT NULL,
PRIMARY KEY (`student_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

POJO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.aowin.testspring.dao.pojo;

import java.io.Serializable;
import java.sql.Date;

public class Students implements Serializable {

private Integer studentid;
private String studentname;
private String studentsex;
private Date studentbirthday;
private String groupid;

public Students() {

}

public Integer getStudentid() {
return studentid;
}

public void setStudentid(Integer studentid) {
this.studentid = studentid;
}

public String getStudentname() {
return studentname;
}

public void setStudentname(String studentname) {
this.studentname = studentname;
}

public String getStudentsex() {
return studentsex;
}

public void setStudentsex(String studentsex) {
this.studentsex = studentsex;
}

public Date getStudentbirthday() {
return studentbirthday;
}

public void setStudentbirthday(Date studentbirthday) {
this.studentbirthday = studentbirthday;
}

public String getGroupid() {
return groupid;
}

public void setGroupid(String groupid) {
this.groupid = groupid;
}

@Override
public String toString() {
return "Students [studentid=" + studentid + ", studentname=" + studentname + ", studentsex=" + studentsex
+ ", studentbirthday=" + studentbirthday + ", groupid=" + groupid + "]";
}

}

DAO接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.aowin.testspring.dao;

import java.util.List;

import com.aowin.testspring.dao.pojo.Students;

public interface IStudentDAO {

String INSERT_STUDENT_SQL = "insert into students (student_name,student_sex,student_birthday,group_id)" +
"values(?,?,?,?)";
String SELECT_STUDENT_ALL_SQL ="select student_id,student_name,student_sex,student_birthday,group_id from students";
String SELECT_STUDENT_COUNT_SQL ="select count(student_id) total from students";
/**
* 添加学生信息
* @param student学生信息
* @return 是否添加成功
*/
public boolean insertStudent(Students student);
}

基础配置方式

编写实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.aowin.testspring.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import com.aowin.testspring.dao.pojo.Students;

public class StudentDAOImpl0 extends JdbcDaoSupport implements IStudentDAO {

public StudentDAOImpl0() {
// TODO Auto-generated constructor stub
}
//实现数据添加方法
public boolean insertStudent(Students student) {
boolean result = false;
//通过update方法执行接口中的SQL语句,并且将SQL语句参数传递进去
int i = super.getJdbcTemplate().update(INSERT_STUDENT_SQL,student.getStudentname(),
student.getStudentsex(),student.getStudentbirthday(),student.getGroupid());
//如果成功添加
if(i > 0)
result = true;
return false;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.aowin.testspring.transcation;

import org.springframework.beans.factory.annotation.Autowired;

import com.aowin.testspring.dao.IStudentDAO;
import com.aowin.testspring.dao.pojo.Students;

public class StudentTranscation {

@Autowired
private IStudentDAO studentDao;
public StudentTranscation() {
// TODO Auto-generated constructor stub
}
//插入两条学生信息
public void insertStudent() {
//初始化第一个学生信息
Students students1 = new Students();
students1.setStudentname("鲁班1号");
students1.setStudentsex("1");
students1.setStudentbirthday(java.sql.Date.valueOf("1990-10-1"));
students1.setGroupid("1810");
//初始化第二个学生信息
Students students2 = new Students();
students2.setStudentname("鲁班2号");
students2.setStudentsex("1");
students2.setStudentbirthday(java.sql.Date.valueOf("2009-10-1"));
students2.setGroupid("1811");
//插入第一个学生信息
studentDao.insertStudent(students1);
//使程序报错,事务回滚
int i = 1/0;
//插入第二个学生信息
studentDao.insertStudent(students2);
}
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!-- 配置数据源 -->
<bean id="dmds"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库连接所必要的数据 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url">
<value>jdbc:mysql://localhost:3306/testmybatis?useUnicode=true&amp;characterEncoding=utf8
</value>
</property>
<property name="username" value="root"></property>
<property name="password" value="123"></property>
</bean>
<!-- 配置JdbcTemplate -->
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg index="0" ref="dmds"></constructor-arg>
</bean>
<!-- 配置studentDao -->
<bean id="studentDao" class="com.aowin.testspring.dao.StudentDAOImpl">
</bean>

<!-- 声明实现类 -->
<bean id="StudentTranscation" class="com.aowin.testspring.transcation.StudentTranscation">
</bean>

<!-- 开启注解 -->
<context:annotation-config />

<!-- aop事务处理 -->
<!-- 配置事务管理器 -->
<bean id="dstm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dmds"></property>
</bean>

<!-- 事务拦截器 -->
<bean id="ti" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<!-- 配置数据源 -->
<property name="transactionManager" ref="dstm"></property>
<!-- 设置事务属性 -->
<property name="transactionAttributes">
<props>
<!-- 配置拦截方法、传播方式、是否只读 -->
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>

<!-- 设置事务自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<!-- 需要加事务的bean -->
<property name="beanNames">
<list>
<value>StudentTranscation</value>
</list>
</property>
<!-- 配置事务拦截器 -->
<property name="interceptorNames">
<list>
<value>ti</value>
</list>
</property>
</bean>

测试类中:

1
2
3
4
5
6
7
@Test
public void test2(){
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("springJDBCTranscation.xml");
StudentTranscation studentTranscation = context.getBean("StudentTranscation",StudentTranscation.class);
studentTranscation.insertStudent();
}

运行结果:

可以看到,学生姓名为‘鲁班1号’的学生信息并未添加到数据库中,事务成功的回滚。

tx标签配置方式

在这里实现类和测试类都不做改动,只需要修改一下配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- 配置数据源 -->
<bean id="dmds"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库连接所必要的数据 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url">
<value>jdbc:mysql://localhost:3306/testmybatis?useUnicode=true&amp;characterEncoding=utf8
</value>
</property>
<property name="username" value="root"></property>
<property name="password" value="123"></property>
</bean>
<!-- 配置JdbcTemplate -->
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg index="0" ref="dmds"></constructor-arg>
</bean>
<!-- 配置studentDao -->
<bean id="studentDao" class="com.aowin.testspring.dao.StudentDAOImpl">
</bean>

<!-- 声明实现类 -->
<bean id="StudentTranscation" class="com.aowin.testspring.transcation.StudentTranscation">
</bean>

<!-- 开启注解 -->
<context:annotation-config />

<!-- 配置事务管理器 -->
<bean id="dstm"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dmds"></property>
</bean>
<!-- 事务通知 -->
<tx:advice transaction-manager="dstm" id="txadvice">
<tx:attributes>
<!--匹配get开头的方法,事务超时时间:无限,隔离级别:READ_COMMITTED 是否只读:是 传播方式:REQUIRED-->
<tx:method name="get*" timeout="-1" isolation="READ_COMMITTED" read-only="true" propagation="REQUIRED"/>
<tx:method name="insert*"/>
</tx:attributes>
</tx:advice>

<!-- 事务自动代理 -->
<aop:config>
<aop:pointcut expression="execution(* com.aowin.testspring.transcation.*.*(..))" id="txpointcut"/>
<aop:advisor advice-ref="txadvice" pointcut-ref="txpointcut"/>
</aop:config>

其中tx:advice标签中可以的子标签tx:attributes中的tx:method中可以设置,事务超时、隔离级别、是否只读、传播方式、方法匹配等熟悉。

测试类和基础配置方式相同,可以看到运行结果也是一样的,事务成功回滚,在这里就不做演示了。

全注解方式

除了基于XML文件的声明式事务配置外,我们也可以采用基于注解式的事务配置方法。直接在Java源代码中声明事务语义的做法让事务声明和将受其影响的代码距离更近了,而且一般来说不会有不恰当的耦合的风险,因为,使用事务性的代码几乎总是被部署在事务环境中。

全注解的方式非常的简单,只需要在你需要添加事务的类或方法上添加@Transactional注解即可。
我们只需要稍微修改一下测试类(添加一个Transactional注解)即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.aowin.testspring.transcation;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import com.aowin.testspring.dao.IStudentDAO;
import com.aowin.testspring.dao.pojo.Students;

@Transactional(timeout=-1,isolation=Isolation.READ_COMMITTED)
public class StudentTranscation {

@Autowired
private IStudentDAO studentDao;
public StudentTranscation() {
// TODO Auto-generated constructor stub
}
//插入两条学生信息
public void insertStudent() {
//初始化第一个学生信息
Students students1 = new Students();
students1.setStudentname("鲁班1号");
students1.setStudentsex("1");
students1.setStudentbirthday(java.sql.Date.valueOf("1990-10-1"));
students1.setGroupid("1810");
//初始化第二个学生信息
Students students2 = new Students();
students2.setStudentname("鲁班2号");
students2.setStudentsex("1");
students2.setStudentbirthday(java.sql.Date.valueOf("2009-10-1"));
students2.setGroupid("1811");
//插入第一个学生信息
studentDao.insertStudent(students1);
//使程序报错,事务回滚
int i = 1/0;
//插入第二个学生信息
studentDao.insertStudent(students2);
}
}

@Transactional注解中可以设置事务有关的属性:
propagation:可选的传播方式设置。枚举型:Propagation
isolation:可选的隔离性级别(默认值:ISOLATION_DEFAULT)。 枚举型:Isolation
readOnly:读写型事务 vs. 只读型事务。布尔类型
timeout:事务超时。int类型
等属性。

修改我们的配置文件(会变得很简单):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 配置数据源 -->
<bean id="dmds"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库连接所必要的数据 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url">
<value>jdbc:mysql://localhost:3306/testmybatis?useUnicode=true&amp;characterEncoding=utf8
</value>
</property>
<property name="username" value="root"></property>
<property name="password" value="123"></property>
</bean>
<!-- 配置JdbcTemplate -->
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg index="0" ref="dmds"></constructor-arg>
</bean>
<!-- 配置studentDao -->
<bean id="studentDao" class="com.aowin.testspring.dao.StudentDAOImpl">
</bean>

<!-- 声明实现类 -->
<bean id="StudentTranscation" class="com.aowin.testspring.transcation.StudentTranscation">
</bean>

<!-- 开启注解 -->
<context:annotation-config />

<!-- 配置事务管理器 -->
<bean id="dstm"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dmds"></property>
</bean>

<!-- 开启事务注解并且注入事务管理器 -->
<tx:annotation-driven transaction-manager="dstm"/>

测试方法和执行结果和之前的是相同的,因为篇幅比较长了,大家可以自行测试一下。

-------------本文结束感谢您的阅读-------------