概述
事务是一组逻辑上的操作,这组操作,要么全部成功,要么全部失败。不会存在一部分操作失败,一部分操作成功的情形。
事务特性
事务的四个属性:原子性、一致性、隔离性、持久性。
- 原子性:是指事务是一个不可分割的工作单位,事务中的操作,要么都发生,要么都不发生。
- 一致性:是指事务前后,数据的完整性必须保持一致。
- 隔离性:是指多个用户并发访问数据库时,一个用户的事务,不能被其他用户的事务所干扰,多个并发事务之间数据要相互隔离。
- 持久性:是指一个事务,一旦被提交,他对数据库中的数据的改变是永久性的,即使数据库发生故障也不应该对其有任何影响。
spring对事务的管理,主要的API在spring-tx-version.jar中,spring提供了三个主要的接口:
- PlatformTransactionManager:事务管理器
- TransactionDefinition:事务定义信息,包括隔离(isolation),传播(propagation),只读(readonly)等
- TransactionStatus:事务运行状态
spring为不同的持久化框架,提供了不同PlatformTransactionManager接口实现。
- 如果我们使用spring-jdbc,他为我们提供的事务管理类是DataSourceTransactionManager。
- 如果我们使用hibernate,他为我们提供的事务管理器类是HibernateTransactionManager。
隔离性是为了解决脏读,不可重复读,幻影读问题的。
- 脏读:一个事务读取了另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的数据是无效的。
- 不可重复读:同一个事务中,多次读取同一数据返回的结果有所不同,与脏读的区别是,它侧重事务提交。
- 幻影读:一个事务读取了几行记录,另一个事务插入一些记录,幻读就发生了,再后来的查询中,第一个事务就会发现有些原来没有的记录,和不可重复读的区别是:不可重复读是数据发生了修改,导致不一样,侧重修改,幻读是数据多了或者少了,侧重新增和删除。
隔离级别分为四种(DEFAULT是衍生出来的):
- DEFAULT:后端数据库默认的隔离级别,mysql和oracle默认隔离级别分别是REPEATABLE_READ和READ_COMMITTED。
- READ_UNCOMMITED:容许你读取还未提交的改变了的数据。会导致隔离级别的三类问题。
- READ_COMMITED:容许在并发事务已经提交后读取,可防止脏读,但幻读和不可重复读仍会发生。
- REPEATTABLE_READ:对相同字段多次读取结果是一样的,除非数据被事务本身改变,可防止脏读,不可重复读,但幻读还是会发生,通过锁定特定的记录行来实现。
- SERIALIZABLE:最严格的隔离级别,完全服从acid原则,确保隔离级别的任何问题都不会发生。它通过锁整张表来实现。
传播行为主要是用来解决多个业务之间方法相互调用时事务管理的问题。
事务的传播行为分为以下几种行为:
- PROPAGATION_REQUIRED:支持当前事务,如果不存在,就新建一个。
- PROPAGATION_SUPPORTS:支持当前事务,如果不存在,就不使用事务。
- PROPAGATION_MANDATORY:支持当前事务,如果不存在,就抛出异常。
- PROPAGATION_REQUIRES_NEW:如果有事务存在,则挂起事务,新建一个事务。
- PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务。
- PROPAGATION_NEVER:以非事务方式运行,如果有事务存在,就抛出异常。
- PROPAGATION_NESTED:如果当前事务存在,则嵌套事务执行。
编程式事务管理是比较贴近jdbc时代,手动控制事务,例如:conn.setAutoCommit(false),conn.rollback()等。这种方式在开发中已经很少使用了,至于原因,看过这个示例,你就能明白。需要事务的地方,需要手动编写事务代码,虽然有可用的模板,使用方式也固定,但是随着业务方法的增加,这种频繁编写事务的代码,实在是不敢想象。
示例
这里使用maven工程,工程结构如下:
pom.xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
jdbc.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///shop?useUnicode=true&useSSL=false
jdbc.username=hadoop
jdbc.password=hadoop
spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="accountDao" class="com.xxx.springtransaction.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="accountService"
class="com.xxx.springtransaction.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="transactionTemplate" ref="transactionTemplate" />
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
</beans>
AccountDao.java
package com.xxx.springtransaction.dao;
public interface AccountDao {
public void transferIn(String id,Double money);
public void transferOut(String id,Double money);
public Double findById(String id);
}
AccountDaoService.java
package com.xxx.springtransaction.dao.impl;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import com.xxx.springtransaction.dao.AccountDao;
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
@Override
public void transferIn(String id, Double money) {
String sql = "update account set money = money + ? where id = ?";
getJdbcTemplate().update(sql, money,id);
}
@Override
public void transferOut(String id, Double money) {
String sql = "update account set money = money - ? where id = ?";
getJdbcTemplate().update(sql, money,id);
}
@Override
public Double findById(String id){
return null;
}
}
AccountService.java
package com.xxx.springtransaction.service;
public interface AccountService {
public void transfer(String out,String in,Double money);
public Double findById(String id);
}
AccountServiceImpl.java
package com.xxx.springtransaction.service.impl;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.xxx.springtransaction.dao.AccountDao;
import com.xxx.springtransaction.service.AccountService;
public class AccountServiceImpl implements AccountService {
private TransactionTemplate transactionTemplate;
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(final String out, final String in, final Double money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus
transactionStatus) {
accountDao.transferOut(out, money);
//int i = 1/0;
//System.out.println(i);
accountDao.transferIn(in, money);
}
});
}
@Override
public Double findById(String id){
return accountDao.findById(id);
}
}
单元测试类:SpringTransactionTest.java
package com.xxx.springtransaction;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.xxx.springtransaction.service.AccountService;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringTransactionTest {
@Resource
private AccountService accountService;
@Test
public void demo1(){
accountService.transfer("1", "2", 200d);
}
}
准备数据,这里假设有一张账户表account,里面有三个账户,分别是aaa,bbb,ccc,均有1000元,这里模拟aaa给bbb转账200元。
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 1000 |
| 2 | bbb | 1000 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
测试验证方法:
1、在没有事务管理或者正常的情况下,aaa给bbb直接转账,会成功,aaa的账户变为800,bbb的账户变为1200
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 800 |
| 2 | bbb | 1200 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
2、在没有事务管理的情况下,打开AccountServiceImpl.java中以下代码注释,让业务方法抛出异常。
int i = 1/0;
System.out.println(i);
这时候转账,aaa的账户会转出200,但是bbb的账户因为异常,不会增加200.
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 600 |
| 2 | bbb | 1200 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
3、给业务代码加上事务管理代码,如下所示,再次转账,抛出异常,事务回滚,aaa的账户钱数不会减少,bbb的不会增加:
@Override
public void transfer(final String out, final String in, final Double money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus
transactionStatus) {
accountDao.transferOut(out, money);
int i = 1/0;
System.out.println(i);
accountDao.transferIn(in, money);
}
});
}
转账结果:
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 600 |
| 2 | bbb | 1200 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set
这个结果说明事务管理生效了。