文章目录
- 1. Spring 事务简介
- 2. Spring 事务角色
- 3. Spring 事务属性
- 3.1 事务配置
- 3.2 案例:转账业务追加日志
- 3.3 事务传播行为
1. Spring 事务简介
Spring 事务作用:在数据层或业务层保障一系列的数据库操作同成功、同失败。
数据层有事务我们可以理解,为什么业务层也需要处理事务呢?
举个简单的例子:
转账业务会有两次数据层的调用,一次是转入者加钱,一次是转出者减钱;
把事务放在数据层,加钱和减钱就有两个事务;
如果先加钱后减钱,且加钱之后其他地方出现异常,就会出现加钱成功减钱失败的结果。即没办法保证加钱和减钱同时成功或者同时失败;
这个时候就需要将事务放在业务层进行处理。
不用事务时,我们是这样处理的:
Dao接口
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
Service 接口和实现类
public interface AccountService {
/**
* 转账操作
* @param out 转出方
* @param in 转入方
* @param money 余额
*/
void transfer(String out, String in, Double money);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按类型注入
private AccountDao accountDao;
@Override
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
}
}
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() {
accountService.transfer("Tom", "Jerry", 100D);
}
}
上述代码运行时,会执行转账操作,Tom 的账户减少 100,Jerry 的账户增加 100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:
这个时候就模拟了转账过程中出现异常的情况,正确的操作应该是 Tom 和 Jerry 账户中的钱不变。但是真正运行后会发现,并没有像我们想象的那样,Tom账户中的钱变少了,而 Jerry 账户中的钱没变。
当程序出问题后,我们需要让事务回滚,且这个事务应该加在业务层上。Spring 的事务管理就是用来解决这类问题的。
Spring事务管理具体的实现步骤为:
(1) 在需要被事务管理的方法上添加注解
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按类型注入
private AccountDao accountDao;
@Override
@Transactional//开启事务
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
}
}
注意:
@Transactional 可以写在接口类上、接口方法上、实现类上和实现类方法上
写在接口类上,该接口的所有实现类的所有方法都会有事务
写在接口方法上,该接口的所有实现类的该方法都会有事务
写在实现类上,该类中的所有方法都会有事务
写在实现类方法上,该方法上有事务
建议写在实现类或实现类的方法上
(2) 在 JdbcConfig 类中配置事务管理器
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean//把方法的返回值定义成一个bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
//配置事务管理器,mybatis使用的是jdbc事务
@Bean//把方法的返回值定义成一个bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
注意:事务管理器要根据使用技术进行选择,Mybatis 框架使用的是 JDBC 事务,可以直接使用 DataSourceTransactionManager。
(3) 开启事务注解
@Configuration//该类是配置类
@ComponentScan("com.itheima")//扫描这个包下的类,找bean
@PropertySource("jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})//引入这些类中的bean
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
(4) 测试
会发现在转账业务出现错误后,事务就可以控制回滚,保证数据的正确性。
2. Spring 事务角色
本节重点理解两个概念,分别是事务管理员和事务协调员。
(1) 未开启Spring事务之前:
AccountDao 的 outMoney 因为是修改操作,会开启一个事务 T1;
AccountDao 的 inMoney 因为是修改操作,会开启一个事务 T2;
AccountService 的 transfer 没有事务。
运行过程中如果没有抛出异常,则 T1 和 T2 都正常提交,数据正确;
如果在两个方法中间抛出异常,T1 因为执行成功提交事务,T2 因为抛异常不会被执行,就会导致数据出现错误。
(2) 开启 Spring 事务管理后
transfer上添加了 @Transactional 注解,在该方法上就会有一个事务 T。
AccountDao 的 outMoney 方法的事务 T1 加入到 transfer 的事务 T 中;
AccountDao 的 inMoney 方法的事务 T2 加入到 transfer 的事务 T 中。
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
通过上面例子的分析,可以得到如下概念:
事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法(如:transfer 方法)
事务协调员:加入事务方,在 Spring 中通常指代数据层方法(如:outMoney、inMoney 方法),也可以是业务层方法。
注意:
目前的事务管理的前提是 DataSourceTransactionManager 和 SqlSessionFactoryBean 使用的是同一个数据源。
3. Spring 事务属性
3.1 事务配置
上面这些属性都可以在@Transactional
注解的参数上进行设置。
- readOnly:true 只读事务,false 读写事务,增删改要设为 false,查询设为 true。
- timeout:设置超时时间(单位:秒),在多长时间之内事务没有提交成功就自动回滚,-1 表示不设置超时时间。
- rollbackFor:当出现指定异常时,进行事务回滚。
- noRollbackFor:当出现指定异常时,不进行事务回滚。
- rollbackForClassName 等同于 rollbackFor,只不过属性为异常的类全名字符串。
- noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串。
- isolation 设置事务的隔离级别
DEFAULT:默认隔离级别, 会采用数据库的隔离级别
READ_UNCOMMITTED:读未提交
READ_COMMITTED:读已提交
REPEATABLE_READ:重复读取
SERIALIZABLE:串行化
rollbackFor 是当出现指定异常时,进行事务回滚;对于异常事务不应该都回滚么,为什么还要指定?
这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚,导致转出账户减钱成功,转入账户加钱失败。
出现这个问题的原因是:Spring 事务只对 Error 异常和 RuntimeException 异常及其子类进行事务回滚,其他的异常类型不回滚。IOException 就是其他的异常类型,所以不回滚。
此时就可以使用 rollbackFor 属性来设置 IOException 异常回滚:
3.2 案例:转账业务追加日志
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现效果预期:
无论转账操作是否成功,均进行转账操作的日志留痕
该环境是基于转账环境来完成的,下面在其基础上继续往下写:
(1) 创建日志表
create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)
(2) 添加 LogDao 接口
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
(3) 添加 LogService 接口与实现类
public interface LogService {
void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Transactional
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
(4) 在转账的业务中添加记录日志
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按类型注入
private AccountDao accountDao;
@Autowired
private LogService logService;//添加日志相关
@Override
//开启事务,出现IOException时回滚
@Transactional
public void transfer(String out, String in, Double money){
try{
accountDao.outMoney(out,money);
//int i =1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);//添加日志相关
}
}
}
当程序正常运行,tbl_account 表中转账成功,tbl_log 表中日志记录成功;
当转账业务之间出现异常(int i =1/0),转账失败,tbl_account 成功回滚,但 tbl_log 表未添加数据。
这个结果和我们想要的不一样,什么原因?该如何解决?
失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败。
理想效果:无论转账操作是否成功,日志必须保留
3.3 事务传播行为
前面讲过 Spring 事务会把 T1、T2、T3 都加入到事务 T 中。
所以当转账失败后,所有的事务都回滚,导致日志没有记录下来。
这与需求不符,此时就想能不能让 log 方法单独是一个事务呢。
要想解决这个问题,就需要用到事务传播行为。
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
具体如何解决,就需要用到 propagation 属性。
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
经过上面的修改,当转账失败时,tbl_log 表中的日志记录也会成功。但是记录的内容也是 “转账操作由Tom到Jerry,金额:50.0”,这显然不合理。于是 AccountServiceImpl 中做如下修改:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired//按类型注入
private AccountDao accountDao;
@Autowired
private LogService logService;//添加日志相关
@Override
@Transactional
public void transfer(String out, String in, Double money){
try{
accountDao.outMoney(out,money);
//int i = 1/0;
accountDao.inMoney(in,money);
logService.log(out,in,money);//添加日志相关
}catch (Exception e){
logService.log(out,in+"失败",0D);//添加日志相关
throw new RuntimeException();//使事务回滚
}
}
}
这样,转账失败时,记录的日志就变为 “转账操作由Tom到Jerry失败,金额:0.0“。
【注意】当 catch 捕获了异常时,事务不会回滚。如果非得写 catch,需要 catch 后 throw new RuntimeException()
让事务回滚。
事务传播行为的可选值: