什么是事务
事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换成另一种状态。
事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)的缩写。
原子性。即不可分割性,事务要么全部被执行,要么就全部不被执行。如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生转换;如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换。
一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态。
隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应显示给任何其他事务。
持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。
一、事务基本概念
1、事务的基本要素(ACID)
原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱结束前,B不能向这张卡转账。
持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
2、事务并发问题
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
幻读:用户A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是用户B就在这个时候插入了一条具体分数的记录,当修改A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
3、Mysql事务隔离级别
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) 是 是 是
不可重复读(read-committed) 否 是 是
可重复读(repeatable-read) 否 否 是
串行化(serializable) 否 否 否
mysql默认的事务隔离级别为REPEATABLE-READ,我们可以通过sql语句:
sql> select @@tx_isolation;
二、jdbc事务原理
获取链接并设置autocommit(false)
执行sql语句,并commit
出错,rollback
关闭连接,代码如下:
public static void main(String[] args) throws SQLException {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("jdbc-tx.xml");
DataSource dataSource = (DataSource) context.getBean("dataSource");
Connection conn = dataSource.getConnection();
//addStudentWithoutTx(conn);
addStudentWithTx(conn);
context.close();
}
// 没有事务,update语句成功执行,insert重复记录报错
public static void addStudentWithoutTx(Connection conn) {
try {
String sql = "update jdbc_student set name = 'user14--' where id = 14";
Statement stmt = conn.createStatement();
System.out.println("Affected row count: " + stmt.executeUpdate(sql));
//
sql = "insert into jdbc_student(id, name, age) values (14, 'user14---', 30)";
System.out.println("Affected row count: " + stmt.executeUpdate(sql));
} catch(Exception e) {
e.printStackTrace();
} finally {
closeConn(conn);
}
}
// 采用事务,由于insert重复记录报错,所以事务回滚,之前的update失效
public static void addStudentWithTx(Connection conn) {
try {
conn.setAutoCommit(false);
String sql = "update jdbc_student set name = 'user14--' where id = 14";
Statement stmt = conn.createStatement();
System.out.println("Update row count: " + stmt.executeUpdate(sql));
//
sql = "insert into jdbc_student(id, name, age) values (15, 'user15---', 30)";
System.out.println("Insert row count: " + stmt.executeUpdate(sql));
conn.commit();
} catch(Exception e) {
e.printStackTrace();
try {
conn.rollback();
} catch (Exception ee) {
// TODO
}
} finally {
closeConn(conn);
}
}
public static void closeConn(Connection conn) {
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
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
三、spring事务机制
1. spring事务管理器
Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。通过事务管理器PlatformTransactionManager,Spring为各个平台如JDBC、Hibernate等提供了对应的事务管理器,但是具体的实现依赖各自平台的事务实现。接口代码如下:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
1
2
3
4
5
从这里我们可以看到具体的事务管理机制对Spring来说是透明的,所以Spring事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate等。
2. spring事务定义及状态描述
从事务管理器PlatformTransactionManager中可以看出,spring完成事务管理还需2个关键元素:事务定义TransactionDefinition及事务状态TransactionStatus描述。
public interface TransactionDefinition {
int getPropagationBehavior(); //传播行为,默认PROPAGATION_REQUIRED
int getIsolationLevel(); //隔离级别,默认数据库默认级别,如mysql为可重复读
int getTimeout();
boolean isReadOnly(); //是否只读,查询操作可以设置为true
String getName();
}
1
2
3
4
5
6
7
2.1 事务传播行为
有两个含有事务的方法A和B,B调用A时,A是启用新事务还是延续B的事务,这便是事务传播行为,Spring提供了7种事务传播行为:
PROPAGATION_REQUIRED:当前方法必须运行在事务中,如果存在当前事务则加入当前事务,否则开启新事务,spring 默认行为。
PROPAGATION_SUPPORTS:当前方法不需要事务上下文,如果存在当前事务则加入当前事务,否则裸奔。
PROPAGATION_MANDATORY:方法必须在事务中运行,当前事务不存在则报错。
PROPAGATION_REQUIRES_NEW:方法运行在自己的新事务中。
PROPAGATION_NOT_SUPPORTED
PROPAGATION_NEVER
PROPAGATION_NESTED:已存在当前事务则运行在嵌套事务中,否则开启新事务,涉及savepoint。
附2张常见事务传播行为图,来源于spring官网。
图1: PROPAGATION_REQUIRED
图2: PROPAGATION_REQUIRES_NEW
2.2 事务状态描述
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction(); //是否新事务
boolean hasSavepoint(); //是否有恢复点
void setRollbackOnly();
boolean isRollbackOnly();
void flush(); //Flush the underlying session to the datastore, if applicable: for example, all affected Hibernate/JPA sessions.
boolean isCompleted();
}
1
2
3
4
5
6
7
8
可以看出这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,事务管理器在回滚或提交时,需要对应的TransactionStatus。
四、Spring事务的实现方式
编程式事务和声明式事务是spring的2种事务实现方式,其中编程式事务可以直接通过PlatformTransactionManager来实现,也可以通过TransactionTemplate简单实现;声明式事务则可以通过xml配置或@Transactional注解方式实现。
1、编程式事务实现
spring 配置文件 jdbc-tx.xml
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd">
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
数据表 jdbc_student
CREATE TABLE `jdbc_student` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8
1
2
3
4
5
6
TmTxDao.java
package com.marcus.spring.tx;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
/**
* 编程式事务Dao.
*/
public class TmTxDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private TransactionTemplate transactionTemplate;
@SuppressWarnings("rawtypes")
public Map getStudent(int id) {
String sql = "select * from jdbc_student where id = ?";
Map map = jdbcTemplate.queryForMap(sql, id);
System.out.println("student.id=" + id + ": " + map);
return map;
}
public int update(int id, String name){
String sql = "update jdbc_student set name = ? where id = ?";
int count = jdbcTemplate.update(sql, name, id);
System.out.println("sql: " + sql + ", para: " + name + ", " + id);
System.out.println("Affected rows : " + count);
return count;
}
public int insert(int id, int age){
String name = "user" + id;
String sql = "insert into jdbc_student(id, name, age) values (?, ?, ?)";
int count = jdbcTemplate.update(sql, id, name, age);
System.out.println("sql: " + sql);
System.out.println(String.format("para: %d, %s, %d", id, name, age));
System.out.println("Affected rows : " + count);
return count;
}
public void withTx() {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(definition);
System.out.println("withTx start...");
try{
this.getStudent(14);
this.update(14, "user14-tx");
this.insert(15,31);
transactionManager.commit(status);
}catch(Exception e){
transactionManager.rollback(status);
}
//
this.getStudent(14);
this.getStudent(15);
//
System.out.println("withTx end...");
}
public void withoutTx() {
System.out.println("withoutTx start...");
try{
this.getStudent(14);
this.update(14, "user14-tx");
this.insert(15,31);
}catch(Exception e){
System.out.println(e.getMessage());
}
//
this.getStudent(14);
this.getStudent(15);
//
System.out.println("withoutTx end...");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void withTransactionTemplate() {
System.out.println("withTransactionTemplate start...");
final TmTxDao txDao = this;
transactionTemplate.execute(new TransactionCallback() {
@Override
public Object doInTransaction(TransactionStatus status) {
txDao.getStudent(14);
txDao.update(14, "user14-tx");
txDao.insert(15,31);
return null;
}
});
this.getStudent(14);
this.getStudent(15);
System.out.println("withTransactionTemplate end...");
}
}
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
TxApp.java 测试类
package com.marcus.spring.tx;
import java.sql.SQLException;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TxApp {
public static void main(String[] args) throws SQLException {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("jdbc-tx.xml");
TmTxDao tmTxDao = (TmTxDao) context.getBean("tmTxDao");
tmTxDao.withoutTx();
// tmTxDao.withTransactionTemplate();
context.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tmTxDao.withoutTx(),没有启用事务,控制台输出如下
withoutTx start…
student.id=14: {id=14, name=user14, age=28}
sql: update jdbc_student set name = ? where id = ?, para: user14-tx, 14
Affected rows : 1
七月 17, 2018 5:37:50 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
七月 17, 2018 5:37:50 下午 org.springframework.jdbc.support.SQLErrorCodesFactory
信息: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
PreparedStatementCallback; SQL [insert into jdbc_student(id, name, age) values (?, ?, ?)]; Duplicate entry ‘15’ for key ‘PRIMARY’; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry ‘15’ for key ‘PRIMARY’
student.id=14: {id=14, name=user14-tx, age=28}
student.id=15: {id=15, name=user15—, age=30}
withoutTx end…
没有启用事务的情况下,可以发现,insert出错时,update依然生效,id=14 name已经成功改成user14-tx。
tmTxDao.withTx(),启用事务,控制台输出如下
withTx start…
student.id=14: {id=14, name=user14, age=28}
sql: update jdbc_student set name = ? where id = ?, para: user14-tx, 14
Affected rows : 1
七月 17, 2018 5:43:59 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
七月 17, 2018 5:43:59 下午 org.springframework.jdbc.support.SQLErrorCodesFactory
信息: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
student.id=14: {id=14, name=user14, age=28}
student.id=15: {id=15, name=user15—, age=30}
withTx end…
由于启用了事务,insert重复记录时,之前的update被回滚,所以查看id=14的数据时,name依然是user14。
tmTxDao.withTransactionTemplate(),用事务模板实现事务,控制台输出如下
withTransactionTemplate start…
student.id=14: {id=14, name=user14, age=28}
sql: update jdbc_student set name = ? where id = ?, para: user14-tx, 14
Affected rows : 1
七月 17, 2018 5:54:35 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
七月 17, 2018 5:54:35 下午 org.springframework.jdbc.support.SQLErrorCodesFactory
信息: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread “main” org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [insert into jdbc_student(id, name, age) values (?, ?, ?)]; Duplicate entry ‘15’ for key ‘PRIMARY’; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry ‘15’ for key ‘PRIMARY’
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:245)…
由于启用了事务,insert重复记录时,之前的update被回滚,所以查看id=14的数据时,name依然是user14。
2、声明式事务实现
2.1 xml配置方式声明事务
spring配置文件:jdbc-tx-xml.xml,区别与jdbc-tx.xml增加aop事务拦截
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
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
上述TmTxDao.java增加saveWithXmlTx方法,代码如下:
// xml声明式事务,save方法拦截
public void saveWithXmlTx() {
System.out.println("saveWithXmlTx start...");
this.getStudent(14);
this.update(14, "user14-tx");
this.insert(15,31);
//
System.out.println("saveWithXmlTx end...");
}
1
2
3
4
5
6
7
8
9
TxApp.java 测试类修改如下:
public static void main(String[] args) throws SQLException {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("jdbc-tx-xml.xml");
TmTxDao tmTxDao = (TmTxDao) context.getBean("tmTxDao");
tmTxDao.saveWithXmlTx();
context.close();
}
1
2
3
4
5
6
7
8
控制台输出如下:
saveWithXmlTx start…
student.id=14: {id=14, name=user14-tx, age=28}
sql: update jdbc_student set name = ? where id = ?, para: user14-tx, 14
Affected rows : 1
七月 17, 2018 7:20:20 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
七月 17, 2018 7:20:20 下午 org.springframework.jdbc.support.SQLErrorCodesFactory
信息: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread “main” org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [insert into jdbc_student(id, name, age) values (?, ?, ?)]; Duplicate entry ‘15’ for key ‘PRIMARY’; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry ‘15’ for key ‘PRIMARY’
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:245)…
由于save开头方法被aop拦截,自动添加事务管理,因此,insert重复记录时,update被回滚,id=14 name依然是user14。
2.2 注解方式声明事务
spring配置文件:jdbc-tx-anotation.xml,区别于jdbc-tx.xml增加包扫描和tx:annotation-driven
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
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
TmTxDao.java 增加saveWithAnotationTx方法
// @tranactional tx manage
@Transactional
public void saveWithAnotationTx() {
System.out.println("saveWithAnotationTx start...");
this.getStudent(14);
this.update(14, "user14-tx");
this.insert(15,31);
//
System.out.println("saveWithAnotationTx end...");
}
1
2
3
4
5
6
7
8
9
10
测试类TxApp.java修改如下:
public static void main(String[] args) throws SQLException {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("jdbc-tx-anotation.xml");
TmTxDao tmTxDao = (TmTxDao) context.getBean("tmTxDao");
tmTxDao.saveWithAnotationTx();
context.close();
}
1
2
3
4
5
6
7
8
控制台输出如下
saveWithAnotationTx start…
student.id=14: {id=14, name=user14, age=28}
sql: update jdbc_student set name = ? where id = ?, para: user14-tx, 14
Affected rows : 1
七月 17, 2018 7:49:13 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
七月 17, 2018 7:49:13 下午 org.springframework.jdbc.support.SQLErrorCodesFactory
信息: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread “main” org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [insert into jdbc_student(id, name, age) values (?, ?, ?)]; Duplicate entry ‘15’ for key ‘PRIMARY’; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry ‘15’ for key ‘PRIMARY’
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:245)…
由于方法saveWithAnotationTx使用了@Transactional注解,spring自动为该方法添加了事务,因此,insert重复记录时,事务回滚,之前的update失效。
详细了解事务,并自己实现,且用代理模式实现事务横切服务.