开发环境:
jdk1.8
Idea 2017 :Maven工程、引入父工程
Tomcat:apache-tomcat-8
Spring:5.0.7
一、事务控制
1、概述
事务的概念:
事务是逻辑上一组操作、组成这组操作各个逻辑单元、要么一起成功、要么一起失败。
事务的特性:
原子性:事务不可分割
一致性:事务执行前后数据完整性保持一致
隔离性:一个事务的执行不应该受到其他事条的干扰
持久性:一旦事务结束、数据就持久化到数据库
2、事务并发读问题
脏读:一个事务读取到另一个事务未提交的数据
不可重复读:一个事务读取到另一个事务已提交的update的数据、导致一个事务中多次查询结果不一致
虚读(幻读):一个事务读取到另一个事务已提交的insert的数据、导致一个事务中多次查询结果不一致
解决读问题:设置事务隔离级别:
read uncommitted:未提交读、什么也解决不了
read committed :已提交读、解决脏读、解决不了不可重复读和虚读
repeatable read :重复读、解决脏读和不可重复读
Serializable:串行化、解决所有读问题
3、Spring事务管理的API
3.1、PlatformTransactionManager:平台事务管理器
platformTransactionManager接口提供事务操作的方法包含3个具体的方法:
--获取事务状态信息:TransactionStatus getTransaction(TransactionDefinition definition)
--提交事务:void commit(TransactionStatus status)
--回滚事务:void rollback(TransactionStatus status)
platformTransactionManager实现类:
DataSourceTransactionManager --使用Spring JDBC或iBatis进行持久化数据时使用
3.2、TransactionDefinition:事务定义信息(接口)
事务定义:用于定义事务的相关的信息、隔离级别、超时信息、传播行为、是否可读
有如下方法:
(1)--String getName() -- 获取事务对象名称
(2)--int getlsolationLevel() --获取事务的隔离级别
(3)--int getPropagationBehavior()-- 获取事务传播行为
(4)--int getTimeout() --获取事务超时时间
(5)--boolean isReadOnly() -- 获取事务是否只读
==================================================
--int getlsolationLevel() --获取事务的隔离级别
--int getPropagationBehavior()-- 获取事务传播行为
Spring中提供了七种事务的传播行为:
保证多个操作在同一个事务中
PROPAGATION_REQUIRED
:默认值,如果A中有事务,使用A中的事务,如果A没有,创建一个新的事务,将操作包含进来
PROPAGATION_SUPPORTS
:支持事务,如果A中有事务,使用A中的事务。如果A没有事务,不使用事务。
PROPAGATION_MANDATORY
:如果A中有事务,使用A中的事务。如果A没有事务,抛出异常。
保证多个操作不在同一个事务中
PROPAGATION_REQUIRES_NEW
:如果A中有事务,将A的事务挂起(暂停),创建新事务,只包含自身操作。如果A中没有事务,创建一个新事务,包含自身操作。
PROPAGATION_NOT_SUPPORTED
:如果A中有事务,将A的事务挂起。不使用事务管理。
PROPAGATION_NEVER
:如果A中有事务,报异常。
嵌套式事务
PROPAGATION_NESTED
:嵌套事务,如果A中有事务,按照A的事务执行,执行完成后,设置一个保存点,执行B中的操作,如果没有异常,执行通过,如果有异常,可以选择回滚到最初始位置,也可以回滚到保存点。
--int getTimeout 超时时间、默认值是-1,没有超时限制。如果有,以秒为单位进行设置。
--boolean isReadOnly() -- 建议查询设置为只读
3.3、TransactionStatus:事务的状态(接口)
事务状态:用于记录在事务管理过程中、事务的状态的对象
包含6个具体的方法:
--void flush() --刷新事务
--boolean hasSavePoint() -- 获取是否是存在存储点
--boolean isCompleted() -- 获取事务是否完成
--boolean isNewTransaction() --获取事务是否为新的事务
--boolean isRollbackOnly() --获取事务是否回滚
--void setRollbackOnly() --设置事务回滚
3.4、事务管理的API关系
Spring进行事务管理的时候、首先平台事务管理器根据事务定义信息进行事务的管理、在事务管理过程中、产生各种状态、
将这些状态的信息记录到事务状态的对象中
二、Spring基于xml的声明事条控制
1、创建工程、引入依赖
创建maven子模块、
导入依赖:Ioc依赖、aop依赖、jdbc模板+数据库连接池的依赖、Spring整合Junit单元测试依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<!-- jdbc模板 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
2、编写相关类
domain/Account.java
package com.day04_tx.domain;
import java.io.Serializable;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 8:50 2018/11/15
*/
public class Account implements Serializable {
private Long id;
private String name;
private Double money;
public Account(Long id, String name, Double money) {
this.id = id;
this.name = name;
this.money = money;
}
public Account() {
}
public Long getId() {
return id;
}
public void setId(Long 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;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money='" + money + '\'' +
'}';
}
}
重写RowMapper接口的实现类AccountRowMapper类、把数据库表中的一行数据形成帐户对象
AccouontRowMapper.java
package com.day04_tx.rowmapper;
import com.day04_tx.domain.Account;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
*把数据库表中的一行数据形成帐户对象
* @ Author :ShaoWei Sun.
* @ Date :Created in 8:51 2018/11/15
*/
public class AccountRowMapper implements RowMapper<Account> {
@Override
public Account mapRow(ResultSet resultSet, int row) throws SQLException {
Account account = new Account();
account.setId(resultSet.getLong("id"));
account.setName(resultSet.getString("name"));
account.setMoney(resultSet.getDouble("money"));
return account;
}
}
AccountDao.java接口
package com.day04_tx.dao;
import com.day04_tx.domain.Account;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:36 2018/11/15
*/
public interface AccountDao {
/**
* 根据id查询用户对象
* @param id
* @return
*/
public Account findById(Long id);
/**
* 更新用户对象
* @param account
*/
public void update(Account account);
}
编写AccountDaoImpl实现类、继承JdbcDaoSupport(xml中就不用注入jdbc了)
package com.day04_tx.dao.Impl;
import com.day04_tx.dao.AccountDao;
import com.day04_tx.domain.Account;
import com.day04_tx.rowmapper.AccountRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
/**
* 继承JdbcDaoSupport 使用这个类中的getJdbcTemplate方法
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:38 2018/11/15
*/
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
/**
* 根据id查询用户对象
* @param id
* @return
*/
@Override
public Account findById(Long id) {
Account account = this.getJdbcTemplate().queryForObject("select * from account where id = ?", new AccountRowMapper(), id);
return account;
}
/**
* 更新用户对象
* @param account
*/
@Override
public void update(Account account) {
this.getJdbcTemplate().update("update account set name = ?, money = ? where id =? ",account.getName(),account.getMoney(),account.getId());
}
}
AccountService.java接口
package com.day04_tx.service;
import com.day04_tx.domain.Account;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:50 2018/11/15
*/
public interface AccountService {
/**
* 业务层:转账方法
* @param fromId 转出账户
* @param toId 转入账户
* @param money 转帐金额
*/
public abstract void transfer(Long fromId, Long toId, Double money);
/**
* 根据id查询账户对象
* @param id 用户id
* @return 返回Account
*/
Account findById(Long id);
}
编写AccountServiceImpl实现类 :
package com.day04_tx.service.Imlp;
import com.day04_tx.dao.AccountDao;
import com.day04_tx.domain.Account;
import com.day04_tx.service.AccountService;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:53 2018/11/15
*/
public class AccountServiceImpl implements AccountService {
//依赖注入accountDao到Spring中
private AccountDao accountDao;
//通过set方法把AccountDao注入到Spring容器中
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(Long fromId, Long toId, Double money) {
//查询转出账户
Account fromAccount = accountDao.findById(fromId);
//查询转入帐户
Account toAccount = accountDao.findById(toId);
//转出帐户捡钱
fromAccount.setMoney(fromAccount.getMoney()-money);
//转入帐户加钱
toAccount.setMoney(toAccount.getMoney()+money);
//更新转出账户
accountDao.update(fromAccount);
// int i = 1/0;
//更新转入账户
accountDao.update(toAccount);
}
/**
* 查询功能、现在做了修改操作、但不允许的。
* @param id 用户id
* @return
*/
@Override
public Account findById(Long id) {
Account account = accountDao.findById(id);
account.setMoney(100000d);
accountDao.update(account);
return account;
}
}
3、把类交给Spring容器来管理
创建applicationContext.xml,并引入jdbc.properties,log4j.properties:
log4j.properties
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=D:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=debug, stdout, file
jdbc.properties
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/mybatis
jdbc.username = root
jdbc.password = sswqzx
applicationContext.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"
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">
<!--导入jdbc-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="accountService" class="com.day04_tx.service.Imlp.AccountServiceImpl">
<property name="accountDao" ref="accountDao" > </property>
</bean>
<bean id="accountDao" class="com.day04_tx.dao.Impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
</beans>
4、编写测试类
src/test/java/com.day04_tx.test/TestTx.java
package com.day04_tx.test;
import com.day04_tx.service.AccountService;
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;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 10:15 2018/11/15
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Tx_test {
@Autowired
private AccountService accountService;
//用了注解就不要用set方法了。不会生效
// public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
//事务测试方法
@Test
public void test1(){
accountService.transfer(1L,2L,1000d);
}
//查询测试方法
@Test
public void test2(){
accountService.findById(1L);
}
}
运行Test1
注意:
如果在AccountServiceImpl中加入自定义的异常、张三钱转了。但王五没收到的情况、解决这种情况就要用到事务
5、在Spring中配置事务
上面的案例中、需要据transfer方法套在一个事务里、transfer中所有的操作要么成功、要么全部取消
applicationContext.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"
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">
<!--导入jdbc-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="accountService" class="com.day04_tx.service.Imlp.AccountServiceImpl">
<property name="accountDao" ref="accountDao" > </property>
</bean>
<bean id="accountDao" class="com.day04_tx.dao.Impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务的属性-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--find开头的方法加只读事务、*表示通配符、匹配任意方法-->
<tx:method name="find*" read-only="true"/>
<!--其余方法是加可读写的事务-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--配置事务的切面-->
<aop:config>
<!--配置切入点表达式、告诉框架哪些方法要控制事务-->
<aop:pointcut id="pt" expression="execution(* com.day04_tx.service.Imlp.*.*(..))"/>
<!--将定义好的事务属性应用到上述切入点-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>
</beans>
测试事务是否成功、
再次运行TestTx中的test1方法,控制台依然会报“被零除异常”,但是表中的数据没有发生变化:张三的钱没有转,王五的钱也没有变化。
测试只读事务
在AcountService接口中增加一个findById方法:根据id查询账户对象
package com.day04_tx.service;
import com.day04_tx.domain.Account;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:50 2018/11/15
*/
public interface AccountService {
/**
* 业务层:转账方法
* @param fromId 转出账户
* @param toId 转入账户
* @param money 转帐金额
*/
public abstract void transfer(Long fromId, Long toId, Double money);
/**
* 根据id查询账户对象
* @param id 用户id
* @return 返回Account
*/
Account findById(Long id);
}
在AccountServiceImpl中实现findById方法 :
package com.day04_tx.service.Imlp;
import com.day04_tx.dao.AccountDao;
import com.day04_tx.domain.Account;
import com.day04_tx.service.AccountService;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 9:53 2018/11/15
*/
public class AccountServiceImpl implements AccountService {
//依赖注入accountDao到Spring中
private AccountDao accountDao;
//通过set方法把AccountDao注入到Spring容器中
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(Long fromId, Long toId, Double money) {
//查询转出账户
Account fromAccount = accountDao.findById(fromId);
//查询转入帐户
Account toAccount = accountDao.findById(toId);
//转出帐户捡钱
fromAccount.setMoney(fromAccount.getMoney()-money);
//转入帐户加钱
toAccount.setMoney(toAccount.getMoney()+money);
//更新转出账户
accountDao.update(fromAccount);
int i = 1/0;
//更新转入账户
accountDao.update(toAccount);
}
/**
* 查询功能、现在做了修改操作、但不允许的。
* @param id 用户id
* @return
*/
@Override
public Account findById(Long id) {
Account account = accountDao.findById(id);
account.setMoney(100000d);
accountDao.update(account);
return account;
}
}
在单元测试类TestTx中创建第二个单元测试方法test2,测试findById方法 :
package com.day04_tx.test;
import com.day04_tx.service.AccountService;
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;
/**
* @ Author :ShaoWei Sun.
* @ Date :Created in 10:15 2018/11/15
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Tx_test {
@Autowired
private AccountService accountService;
//用了注解就不要用set方法了。不会生效
// public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
//事务测试方法
@Test
public void test1(){
accountService.transfer(1L,2L,1000d);
}
//查询测试方法
@Test
public void test2(){
accountService.findById(1L);
}
}
运行test2方法,发现控制台报错,异常信息如下: