准备工作
什么是事务
事务是数据库操作的最基本的单元,一个事务可以包含好几个操作,这些操作在逻辑上看做是一组操作,要么都成功要么都失败。例如最经典的转账事例,一次转账就是一个事务,包括两个操作,一个人少钱,另一个人增加钱,这两个操作,要么都成功要么都失败,不可以一个人少钱另一个人不增加,也不能一个人不少钱,另一个人凭空增加钱。
事务有如下四个特性:
- 原子性:事务中包含的各项操作在一次执行过程中,要么都成功,要么都失败。
- 一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后数据库都必须处于一致性。比如两个账户之间转账,他们的钱数之和必须是一致的。
- 隔离性:多个事务之间相互不影响。
- 持久性:事务一旦提交后,数据库中的数据必须被永久的保存下来。
事务的常见参数
传播行为propagation
事务的传播行为是指多个拥有事务的方法在嵌套调用时的事务控制。主要有如下7种类型:
值 | 解释 |
---|---|
REQUIRED(默认) | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,就加入到这个事务中 |
REQUIRED_NEW | 新建事务,如果当前存在事务,就把事务挂起 |
SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行 |
NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把事务挂起 |
MANDATORY | 使用当前事务,如果当前没有事务,就抛出异常 |
NEVER | 以非事务方式执行,如果当前存在事务,就抛出异常 |
NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则新建一个事务 |
隔离级别isolation
说到隔离级别需要先说明三种数据库的读问题,这些问题是不考虑事务的隔离性而引出的问题
- 脏读:读到了其他事务还没有提交的数据。
- 不可重复读:对某个数据进行读取,发现两次读取的结果不同,也就是说第一次读取和第二次读取之间有其他事务对这个数据进行了修改或删除。
- 幻(虚)读:一个未提交的事务读取到另一个已经提交的添加数据的事务。
spring隔离级别主要有如下取值
值 | 解释 |
---|---|
READ_UNCOMMITTED 读未提交 | 可以读取到未提交事务中的数据。存在脏读、不可重复读和幻读三个问题 |
READ_COMMITTED 读已提交 | 未提交的事务可以读取到已提交事务中的数据。存在不可重复读和幻读两个问题 |
REPEATABLE_READ 可重复读 | 在同一时间里读取的结果是一致的。存在幻读问题 |
SERIALIZABLE 串行化 | 只允许一个事务执行完成后才会执行其他事务。解决了三个问题,但是执行效率最低 |
超时时间timeOut
事务需要在一定时间内提交,如果超出时间还未提交,则事务会回滚。
设置-1表示永不超时,正数相应的数字代表相应的秒数。
是否只读readOnly
是否只读表示是否只能查询数据库中的数据,不能修改数据。有true和false两个取值。
回滚和不会滚
rollbackFor回滚:出现哪些异常会回滚
noRollbackFor不会滚:出现哪些异常不会回滚
它们的取值是相应的异常的class对象。
xml配置
1.创建数据表
create table t_account
(
id varchar(5) not null primary key,
money int null check (`money` >= 0)
);
插入两条数据
创建DAO层
public interface AccountDao {
//增加钱
public int increaseMoney(String id,int money);
//减少钱
public int reduceMoney(String id,int money);
}
public class AccountDaoImpl implements AccountDao{
private JdbcTemplate jt;
public AccountDaoImpl(JdbcTemplate jt) {
this.jt = jt;
}
@Override
public int increaseMoney(String id, int money) {
String sql = "update t_account set money = money + ? where id = ?";
return jt.update(sql,money,id);
}
@Override
public int reduceMoney(String id, int money) {
String sql = "update t_account set money = money - ? where id = ?";
return jt.update(sql,money,id);
}
}
3.创建service层
public interface AccountService {
// 转账
public int transferAccounts(String sourceId,String targetId,int money);
}
public class AccountServiceImpl implements AccountService{
private AccountDao accountDao;
public AccountServiceImpl(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public int transferAccounts(String sourceId, String targetId, int money) {
int count = accountDao.reduceMoney(sourceId,money);
count+=accountDao.increaseMoney(targetId,money);
return count;
}
}
4.创建配置文件
<?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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.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">
<!-- 引入外部配置文件-->
<context:property-placeholder location="db.properties" />
<!-- 数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClassName}" />
<property name="url" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
</bean>
<!-- 配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 注入DAO层和service层-->
<bean id="accountDao" class="chy.spring.jdbc.tx.AccountDaoImpl">
<constructor-arg name="jt" ref="jdbcTemplate" />
</bean>
<bean id="accountService" class="chy.spring.jdbc.tx.AccountServiceImpl">
<constructor-arg name="accountDao" ref="accountDao" />
</bean>
<!-- 1.注册事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 2.配置事务管理器通知-->
<tx:advice id="txm" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置哪些方法开启事务,可以使用通配符*。设置传播行为、隔离级别等属性-->
<tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" timeout="-1" read-only="false"/>
</tx:attributes>
</tx:advice>
<!-- 配置aop,将通知织入-->
<aop:config>
<aop:pointcut id="myPoint" expression="execution(* chy.spring.jdbc.tx.AccountServiceImpl.*(..))"/>
<aop:advisor advice-ref="txm" pointcut-ref="myPoint" />
</aop:config>
</beans>
事务管理会用到aop。
5.测试
1.先测试没有异常产生时
public class TestTx {
@Test
public void testTranscation(){
ApplicationContext app = new ClassPathXmlApplicationContext("txconfig.xml");
AccountService service = app.getBean("accountService",AccountService.class);
int count = service.transferAccounts("小明","小红",100);
System.out.println(count);
}
}
测试前
测试后
2.再测试有异常产生时
在转账方法内添加一个异常
public int transferAccounts(String sourceId, String targetId, int money) {
int count = accountDao.reduceMoney(sourceId,money);
int i = 5/0;
count+=accountDao.increaseMoney(targetId,money);
return count;
}
再次测试
测试前
测试后
数据保持一致
注解配置
xml+注解
1.创建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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="chy.spring.jdbc.tx" />
<!-- 引入外部配置文件-->
<context:property-placeholder location="db.properties" />
<!-- 数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClassName}" />
<property name="url" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
</bean>
<!-- 配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 注册事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 开启事务注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
2.为DAO层和Service层加上注解和自动装配
@Repository
public class AccountDaoImpl implements AccountDao{
private JdbcTemplate jt;
@Autowired
public AccountDaoImpl(JdbcTemplate jt) {
this.jt = jt;
}
@Override
public int increaseMoney(String id, int money) {
String sql = "update t_account set money = money + ? where id = ?";
return jt.update(sql,money,id);
}
@Override
public int reduceMoney(String id, int money) {
String sql = "update t_account set money = money - ? where id = ?";
return jt.update(sql,money,id);
}
}
@Service
public class AccountServiceImpl implements AccountService{
private AccountDao accountDao;
@Autowired
public AccountServiceImpl(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ,timeout = -1,readOnly = false)
public int transferAccounts(String sourceId, String targetId, int money) {
int count = accountDao.reduceMoney(sourceId,money);
count+=accountDao.increaseMoney(targetId,money);
return count;
}
}
在transferAccounts方法加上@Transactional注解,该注解可以放到类或方法上,放到类上表示该类的所有方法都开启事务。
3.测试
先测试未出现异常
@Test
public void testTranscation2(){
ApplicationContext app = new ClassPathXmlApplicationContext("txconfig2.xml");
AccountService service = app.getBean(AccountService.class);
int count = service.transferAccounts("小明","小红",100);
System.out.println(count);
}
测试前
测试后
测试出现异常,
给transferAccounts方法加上异常
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ,timeout = -1,readOnly = false)
public int transferAccounts(String sourceId, String targetId, int money) {
int count = accountDao.reduceMoney(sourceId,money);
int i = 5/0;
count+=accountDao.increaseMoney(targetId,money);
return count;
}
测试
@Test
public void testTranscation2(){
ApplicationContext app = new ClassPathXmlApplicationContext("txconfig2.xml");
AccountService service = app.getBean(AccountService.class);
int count = service.transferAccounts("小明","小红",100);
System.out.println(count);
}
测试前
测试后
完全注解
将上一节的配置文件删掉,建一个配置类来实现相应功能
@Configuration
@ComponentScan("chy.spring.jdbc.tx")
@EnableTransactionManagement
@PropertySource("db.properties")
public class TxConfig {
@Value("${db.driverClassName}")
private String driverClassName;
@Value("${db.url}")
private String url;
@Value("${db.username}")
private String username;
@Value("${db.password}")
private String password;
//配置数据源
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(this.driverClassName);
dataSource.setUrl(this.url);
dataSource.setUsername(this.username);
dataSource.setPassword(this.password);
return dataSource;
}
//配置JdbcTemplate
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//注册和配置事务管理器
@Bean
public TransactionManager getTransactionManager(DataSource dataSource){
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
}
@EnableTransactionManagement为开启事务管理器,让这个注解生效需要我们先注册一个事务管理器。
@PropertySource引入外部配置文件,文件中的信息需要借助@Value引用。
需要注意,别忘记上节的@Transactional注解
在有异常的情况下测试
@Test
public void testTranscation3(){
ApplicationContext app = new AnnotationConfigApplicationContext(TxConfig.class);
AccountService service = app.getBean(AccountService.class);
int count = service.transferAccounts("小明","小红",100);
System.out.println(count);
}
测试前
测试后
事务开启成功