一、数据库事务的ACID特性
原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保整个事务的所有操作要么全部完成,要么全部不完成。
一致性(Consistency):事务一旦完成不管成功还是失败,系统始终处于一致的状态。
隔离性(Isolation):不同的事务应该相互隔离,防止数据破坏。
持久性(Durability):在事务完成后,该事务对数据库所做的更改便持久的保存在数据库中,并不会被回滚,无论发生什么系统错误,它的结果都不应该受到影响。
二、Spring事务隔离级别
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITED | 脏读,允许一个事务去读取另一个事务中未提交的数据,可能导致脏、幻、不可重复读。 |
ISOLATION_READ_COMMITTED | 读/写提交,一个事务只能读取另一个事务已经提交的数据。可以防止脏读,但幻读和不可重复读仍可发生 |
ISOLATION_REPEATABLE_READ | 可重复读,对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏、不可能重复读,但是幻读仍可能发生 |
ISOLATION_SERIALIZABLE | 序列化,完全服从ACID的隔离级别,确保不发生脏、幻、不可重复读。这在所有的隔离级别中花费的代价最大的,但是也是最可靠的隔离级别 |
三、Spring事务的传播行为
首先来了解下为什么需要传播行为,通常我们使用service层来调用dao层提供的修改数据的方法,比如下面这样(伪代码)。
//AccountService
class AccountService{
//from 转账给 to money元
public void transfer(String from,String to,int money) {
//from用户减少money
accountDao.transferFrom(from, money);
//to用户增加money
accountDao.transferTo(to, money);
}
}
上面的代码很简单,就是from给to转账,而我们的事务一般都是加在业务层的方法上的,也就是说transfer方法其实就是执行在一个事务中,里面的数据库操作要么都完成,要么都不完成。但如果当我们的事务变得复杂的情况下(如下)
//AccountService
class AccountService{
//from 转账给 to money元
public void transfer(String from,String to,int money) {
//from用户减少money
accountDao.transferFrom(from, money);
//to用户增加money
accountDao.transferTo(to, money);
//这里调用了本类的其他方法
xxx();
}
public void xxx(){
dao1.xxx();
}
}
想象一下,如果这里transfer()方法执行在一个事务中,xxx()方法又执行在另一个事务中,此时我就无法保证事务的一致性,因此我们需要transfer()方法和xxx()方法处于同一事务中。事务的传播行为就可以解决该问题。
事务的传播行为(7种):
PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行 |
PROPAGATION_MANDATORY | 支持当前事务, 如果当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务 |
前3个传播行为定义了,不同的业务层方法调用时(比如说这里的transfer()方法中调用了xxx()方法),他们应该处于同一个事务中,但如果当前没有事务,它们的处理有所不同PROPAGATION_REQUIRED会新建一个事务,PROPAGATION_SUPPORTS会以非事务方式运行,而PROPAGATION_MANDATORY则会抛出异常。而随后的三个传播行为则定义了他们应该处于不同的事务中,最后一个则代表嵌套事务执行。
四、申明式事务(基于xml)
使用申明式事务时,如果业务方法没有发生异常,Sping就会让事务管理器提交事务,如果发生异常,则让事务管理器回滚事务。
开始撸码!!!
dao层
一个与转账有关的接口如下:
public interface AccountDao {
/*
* @param from:转账方
* @param money:转转金额
*/
public void transferFrom(String from,int money);
/*
* @param from:接受方
* @param money:接受金额
*/
public void transferTo(String to,int money);
}
接口的具体实现类
public class AccountDaoImpe implements AccountDao {
//注入JdbcTemplate
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/*
* @param from:转账方
* @param money:转转金额
*/
@Override
public void transferFrom(String from, int money) {
String sql="update bank1 set money=money-? where name=?";
jdbcTemplate.update(sql, new Object[]{money,from});
}
/*
* @param from:接受方
* @param money:接受金额
*/
@Override
public void transferTo(String to, int money) {
String sql="update bank1 set money=money+? where name=?";
jdbcTemplate.update(sql, new Object[]{money,to});
}
}
service层
AccountService接口
public interface AccountService {
//进行转账业务
public void transfer(String from,String to,int money);
}
具体的实现类(调用dao层来进行转账)
public class AccountServiceImpl implements AccountService {
//注入AccountDaoImpe
private AccountDaoImpe accountDao;
public void setAccountDao(AccountDaoImpe accountDao) {
this.accountDao = accountDao;
}
//进行转账业务
@Override
public void transfer(String from,String to,int money) {
accountDao.transferFrom(from, money);
accountDao.transferTo(to, money);
}
}
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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 引入外部属性配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 配置JdbcTemplate 模板类 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置dao -->
<bean id="accountDao" class="dao.AccountDaoImpe">
<!-- 注入jdbcTemplate -->
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!-- 配置service -->
<bean id="accountService" class="service.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
</beans>
初始数据库中又如下数据
现在开始转账,假设张三给李四转100元。
//测试
public static void main(String[] args) {
ApplicationContext ioc=
new ClassPathXmlApplicationContext("application-cfg.xml");
AccountService service=(AccountService) ioc.getBean("accountService");
service.transfer("张三", "李四", 100);
}
结果:查看数据库可以发现成功的转账了。
注意此时我们还没有使用事务。如果在张三转给李四的过程中发生了异常,比如停电了!!!此时会如何?先将数据库恢复初始状态,两人余额都是2000,然后修改一处代码如下。
//进行转账业务
@Override
public void transfer(String from,String to,int money) {
accountDao.transferFrom(from, money);
int a=2/0;//模拟断电
accountDao.transferTo(to, money);
}
执行测试代码,结果如下:
很明显,张三减少100但是随后由于发生了异常李四余额没有增加100,这样问题就很大了。因此引入了事务,这里将事务加在业务层,让整个transfer()方法在同一个事务中执行,确保整个事务的所有操作要么全部完成,要么全部不完成。
修改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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 引入外部属性配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 配置JdbcTemplate 模板类 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置dao -->
<bean id="accountDao" class="dao.AccountDaoImpe">
<!-- 注入jdbcTemplate -->
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!-- 配置service -->
<bean id="accountService" class="service.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 事务增强,相当于切面类 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- method 定义的方法会添加事务管理 -->
<!-- isolation 事务的隔离级别 -->
<!-- propagation 事务的传播行为 ,默认是同一个事务 -->
<!-- read-only 事务是否只读 -->
<!-- timeout="-1" 事务的超时时间,默认值使用数据库的超时时间 -->
<!-- rollback-for: 遇到哪些异常就回滚,其他的都不回滚 -->
<!-- tno-rollback-for:遇到哪些异常不回滚,其他的都回滚。 -->
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED"
read-only="false" timeout="-1" />
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut expression="execution(* service.AccountServiceImpl.transfer(..))"
id="pointcut1" />
<!-- 引入切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"></aop:advisor>
</aop:config>
</beans>
再次测试,首先依然将数据库恢复初始状态,将产生异常的代码注释掉。
正常情况下
没毛病,将数据库恢复初始状态,再来模拟异常情况下(模拟断电)取消产生异常代码的注释。
可以发现这次没有出现总金额少100的情况。因为整个业务方法在一个事务中执行,如果产生异常,事务会回滚。
五、申明式事务——基于注解
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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 引入外部属性配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 配置JdbcTemplate 模板类 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置dao -->
<bean id="accountDao" class="dao.AccountDaoImpe">
<!-- 注入jdbcTemplate -->
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!-- 配置service -->
<bean id="accountService" class="service.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务注解 -->
<tx:annotation-driven/>
</beans>
通过注解来开启事务需要通过<tx:annotation-driven/>来开启
然后在业务层使用注解方式即可
//@Transactional放置在类级的声明中,会使得所有方法都有事务
//@Transactional
public class AccountServiceImpl implements AccountService {
//注入AccountDaoImpe
private AccountDaoImpe accountDao;
public void setAccountDao(AccountDaoImpe accountDao) {
this.accountDao = accountDao;
}
//进行转账业务
//在方法上应用事务,注意方法的@Transactional会覆盖类上面声明的事务
@Transactional
@Override
public void transfer(String from,String to,int money) {
accountDao.transferFrom(from, money);
int a=2/0;//模拟断电
accountDao.transferTo(to, money);
}
}
可以发现通过注解的方式配置事务很简单,同样的注解的方式也可以设置传播行为,隔离级别等信息
@Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED)