举个简单的例子,
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager
commit是用来提交事务,rollback是用来回滚事务。
PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:
从名称上可以看出,我们只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。其内部采用的是JDBC的事务。所以说如果你持久层采用的是JDBC相关的技术,就可以采用这个事务管理器来管理你的事务。而Mybatis内部采用的就是JDBC的事务,Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器,下面会进行介绍。
需求分析: 实现任意两个账户间转账操作
需求微缩: A账户减钱,B账户加钱
为了实现上述的业务需求,我们可以按照下面步骤来实现下:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作
项目结构:
步骤1:准备数据库表
create database spring_db character set utf8;
use spring_db;
create table tbl_account(id int primary key auto_increment,name varchar(35),money double
);
insert into tbl_account values(1,'lsm',1000);
insert into tbl_account values(2,'yxy',1000);
步骤2:创建项目导入jar包
项目的pom.xml添加相关依赖
org.springframework spring-context 5.2.10.RELEASE org.springframework spring-test 5.2.10.RELEASE junit junit 4.12 test com.alibaba druid 1.1.16 mysql mysql-connector-java 5.1.47 org.mybatis mybatis 3.5.6 org.mybatis mybatis-spring 1.3.0 org.springframework spring-jdbc 5.2.10.RELEASE
步骤3:根据表创建模型类
import java.io.Serializable;/*** @Author Mr.Lu* @Date 2022/11/22 17:44* @ClassName Account* @Version 1.0*/
public class Account implements Serializable {private Integer id;private String name;private Double money;public Account() {}public Account(Integer id, String name, Double money) {this.id = id;this.name = name;this.money = money;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Double getMoney() {return money;}public void setMoney(Double money) {this.money = money;}@Overridepublic String toString() {return "Account{" +"id=" + id +", name='" + name + '\'' +", money=" + money +'}';}
}
步骤4:创建Dao接口
import com.itheima.domain.Account;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;/*** @Author Mr.Lu* @Date 2022/11/22 17:45* @ClassName AccountDao* @Version 1.0*/
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);@Select("select * from tbl_account where id = #{id}")Account select(@Param("id") Integer id);
}
步骤5:创建Service接口和实现类
public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*/public void transfer(String out, String in, Double money);/*** 根据id查询账户信息* @param id* @return*/public Account select(Integer id);
}@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Overridepublic void transfer(String out, String in, Double money) throws IOException {accountDao.outMoney(out, money);accountDao.inMoney(in, money);}@Overridepublic Account select(Integer id){return accountDao.select(id);}
}
步骤6:添加jdbc.properties文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
jdbc.username=root
jdbc.password=1234
步骤7:创建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;@Beanpublic DataSource dataSource() {DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;}
步骤8:创建MybatisConfig配置类
public class MybatisConfig {@Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();ssfb.setTypeAliasesPackage("com.itheima.domain");ssfb.setDataSource(dataSource);return ssfb;}@Beanpublic MapperScannerConfigurer mapperScannerConfigurer(){MapperScannerConfigurer msc = new MapperScannerConfigurer();msc.setBasePackage("com.itheima.dao");return msc;}
}
步骤9:创建SpringConfig配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;@Configuration
@ComponentScan("com.itheima")
@PropertySource("jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {
}
步骤10:编写测试类
import com.itheima.config.SpringConfig;
import com.itheima.domain.Account;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.IOException;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {@Autowiredprivate AccountService accountService;@Testpublic void testTransfer() throws IOException {accountService.transfer("lsm", "yxy", 100D);Account account1 = accountService.select(1);System.out.println(account1);Account account2 = accountService.select(2);System.out.println(account2);}@Testpublic void testSelect(){Account account1 = accountService.select(1);System.out.println(account1);Account account2 = accountService.select(2);System.out.println(account2);}
}
testSelect:
testTransfer:
怎么解决呐???????????
上述环境,运行单元测试类,会执行转账操作,lsm
的账户会减少100,yxy
的账户会加100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;public void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i = 1/0;accountDao.inMoney(in,money);}
}
这个时候就模拟了转账过程中出现异常的情况,正确的操作应该是转账出问题了,Tom
应该还是900,Jerry
应该还是1100,但是真正运行后会发现,并没有像我们想象的那样,Tom
账户为800而Jerry
还是1100,100块钱凭空消息了,银行乐疯了。如果把转账换个顺序,银行就该哭了。
不管哪种情况,都是不允许出现的,对刚才的结果我们做一个分析:
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而Spring的事务管理就是用来解决这类问题的。
Spring事务管理具体的实现步骤为:
步骤1:在需要被事务管理的方法上添加注解
public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money) ;
}@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactionalpublic void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i = 1/0;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;@Beanpublic DataSource dataSource(){DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;}//配置事务管理器,mybatis使用的是jdbc事务@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource){DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();transactionManager.setDataSource(dataSource);return transactionManager;}
}
注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager
步骤3:开启事务注解
在SpringConfig的配置类中开启
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
步骤4:运行测试类
会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。
知识点1:@EnableTransactionManagement
名称 | @EnableTransactionManagement |
---|---|
类型 | 配置类注解 |
位置 | 配置类定义上方 |
作用 | 设置当前Spring环境中开启注解式事务支持 |
知识点2:@Transactional
名称 | @Transactional |
---|---|
类型 | 接口注解 类注解 方法注解 |
位置 | 业务层接口上方 业务层实现类上方 业务方法上方 |
作用 | 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务) |
重点介绍事务管理员
和事务协调员
。
通过上面例子的分析,我们就可以得到如下概念:
注意:
目前的事务管理是基于DataSourceTransactionManager
和SqlSessionFactoryBean
使用的是同一个数据源。
重点介绍事务配置
、转账业务追加日志
、事务传播行为
。
上面这些属性都可以在@Transactional
注解的参数上进行设置。
readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
rollbackFor**:当出现指定异常进行事务回滚**
noRollbackFor:当出现指定异常不进行事务回滚
思考:出现异常事务会自动回滚,这个是我们之前就已经知道的
noRollbackFor是设定对于指定的异常不回滚,这个好理解
rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?
public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money) throws IOException;}@Servicepublic class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactionalpublic void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i = 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);}}
出现这个问题的原因是,Spring的事务只会对Error异常
和RuntimeException异常
及其子类进行事务回顾,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚
此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactional(rollbackFor = {IOException.class})public void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i = 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);}}
rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
isolation设置事务的隔离级别
介绍完上述属性后,还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例。
需求分析
在前面的转案例的基础上添加新的需求,完成转账后记录日志。
基于上述的业务需求,我们来分析下该如何实现:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(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 {@Autowiredprivate LogDao logDao;@Transactionalpublic void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}
}
步骤4:在转账的业务中添加记录日志
public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Autowiredprivate LogService logService;@Transactionalpublic void transfer(String out,String in ,Double money) {try{accountDao.outMoney(out,money);accountDao.inMoney(in,money);}finally {logService.log(out,in,money);}}
}
步骤5:运行程序
当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
这个结果和我们想要的不一样,什么原因?该如何解决?
失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
最终效果:无论转账操作是否成功,日志必须保留
对于上述案例的分析:
要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
具体如何解决,就需要用到之前我们没有说的propagation属性
。
修改logService改变事务的传播行为
@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;// propagation设置事务属性:传播行为设置为当前操作需要新事务@Transactional(propagation = Propagation.REQUIRES_NEW)public void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}
}
运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。
事务传播行为的可选值
下一篇:JavaIO流:NIO梳理