一、事务的定义及特性
(一)、事务的定义
1、事务是一个最小的不可再分割的执行单元,它由批量的DML语句构成,这些语句要么全部执行,要不全都不执行。
2、通常一个事务对应一个完整的业务。比如典型的转账业务,从账户A中转账1000元给账户B。操作过程分为两步,第一步从账户A中扣去1000元,第二步往账户B中加上1000元。显然这两个操作要么都得做,要么都不做。如果只做其中一个操作,数据库的数据就会出现问题。
3、注意:只有DML语句才有事务,DDL和DCL没有事务。
(二)、事务的四个特性(ACID)
1、原子性(Atomicity):原子性是指事务操作为最小单位,不可再分。要执行就全部执行,否则都不执行,不能出现执行其中一部分的情况。
2、一致性(Consistency):一致性是指一个事务操作结束后数据库必须仍处于一致性状态。所谓的一致性状态,简单理解就是数据库的数据总体是保持不变的。
比如转账过程中。事务执行前账户A有2000元,账户B有1000元。那么“账户A转账1000元给账户B”这个事务执行完成后,账户A和账户B的金钱总数仍要等于3000元。
3、隔离性(Isolation):隔离性是指并发执行的事务之间互不干扰,相互隔离。
4、持久性(Durability):持久性是指事务一旦被提交,它对数据库数据的改变就是永久性的。
(三)、事务的操作步骤
1、确保数据库支持事务功能
A、查看数据库支持的引擎
mysql语句:show ENGINES;
从查询结果我们会发现数据库支持多种引擎,但其中能提供事务功能的只有InnoDB。
B、查看待操作表的引擎
mysql语句:show create table 表名;
可以看到我的account表执行引擎已经是InnoDB了
C、如果不是InnoDB,则将其改为InnovationDB
mysql语句:ALTER TABLE 表名 ENGINE = INNODB;
2、具体步骤
A、开启事务
start transaction;
B、定义事务的DML语句
insert into account values(50,"test");
此时我们看下account表会发现表中仍然没有money=50,user=test这个记录的。说明这个DML语句还没有真正被执行。
,
C、提交 or 回滚
提交:commit
这时我们再查看下数据库,就会发现新纪录已经被添加到表中了。
回滚:rollback
(需要注意的是每次执行完commit或者rollback后,整个事务就结束了,因此这里我们需要重新开启一个事务)
我们看下account表,会发现DML语句确实没有被执行
二、Spring中JDBC开启事务支持
(一)、Spring的主要配置
定义一个数据源。根据自己的实际情况来配置,笔者这里用的是DBCP的数据源,用其他数据源也是可以的。
<!-- 定义一个使用DBCP实现的数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/library?characterEncoding=UTF8"
p:username="root"
p:password="admin"/>
<!-- 定义JDBC模板Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource"/>
(二)、创建相应的DAO和Service类
1、AccountDAO类
package com.book.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class AccountDao {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void reduce() {
jdbcTemplate.update("update account set money=money-10 where user='红红'");
}
public void add(){
// int i = 2/0;
jdbcTemplate.update("update account set money=money+10 where user='小明'"); }
}
2、AccountService类
package com.book.service;
import com.book.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
private AccountDao accountDao;
@Autowired
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void doAccount() {
accountDao.reduce();
// int i=2/0;
accountDao.add();
}
}
3、测试代码
package com.book.web;
import com.book.service.LendService;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
public class Test {
public static void main(String[] args){
//进行类加载,并实例化对象
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("book-context.xml"));
//获取实例化的对象
LendService bean = (LendService) bf.getBean("lendService");
System.out.println("Done");
try{
bean.doAccount();
}catch (Exception e){
e.printStackTrace();
}
}
}
三、Spring使用事务的两种方式
(一)、基于AOP的注解方式
1、xml文件中添加开启事务的注解
<!-- 第一步配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 第二步 开启事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
2、代码中添加注解
在需要申明为事务的方法前添加@Transactional(rollbackFor = { Exception.class })字段即可,Spring的事务回滚默认只处理RuntimeException,也就是运行时异常,对于一些程序员自己定义的异常并不会触发事务的回滚。rollbackFor = {Exception.class}则指明我们要处理所有类型的异常。如下图
3、运行测试代码
运行前的数据库状态
A、没有异常时运行结果如下:
B、手动构建异常(开启事务的状态)
运行结果如下:
用户“红红”钱没有减少,小明的钱也没有增加。说明事务的回滚生效了。add()操作的异常导致reduce()操作被回滚,没有执行成功。
C、手动构建异常(关闭事务的状态)
运行结果如下,我们会发现账号名为“红红”的用户money少了10,但是账号名为“小明”的用户money却没有变
(二)、基于AOP的xml配置方式
1、xml中添加注解
<!-- 第一步 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入dataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 第二步 配置事务增强 -->
<tx:advice id="txadvice" transaction-manager="transactionManager">
<!-- 做事务操作 -->
<tx:attributes>
<!-- 设置进行事务操作的方法匹配规则 以do开头的方法-->
<tx:method name="do*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 第三步 配置切面 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* com.book.service.AccountService.*(..))" id="pointcut"/>
<!-- 切面 -->
<aop:advisor advice-ref="txadvice" pointcut-ref="pointcut"/>
</aop:config>
2、运行结果如下
当出现异常时两个用户的money都不变,说明事务回滚成功。
三、事务不起作用的常见原因
(一)、确保你所操作的表支持事务机制
最好自己先在mysql中写个简单的事务试试。
(二)、service层处理了异常
1、原因
service层类中使用try catch 去捕获异常后,由于该类的异常并没有抛出,就无法触发事务管理机制。因为在你将doAccount()方法申明为事务方法后,只有当doAccount()方法向上抛出异常时才会触发事务的回滚机制。如果你自己在doAcount方法中用try...catch处理完异常了,事务管理器就不知道出现了异常,自然不会执行回滚操作。
2、测试
如下,我们在AccountService类的doAccount()方法中增加try...catch处理机制进行测试。
我们查看下运行结果,发现事务失效了。
3、解决方案
A、将service层的try..catch提到web层中(web开发通常分为dao、service和web三层,不理解的直接将try...catch去掉即可)。
B、使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()方法手动回滚(不推荐,会增加代码之间的耦合度)
运行结果如下,说明事务起作用了。