一、事务属性

1.事务的两种方式

Spring 并不直接支持事务,只有当数据库支持事务时,Spring 才支持事务,Spring 只不过简化了开发人员实现事务的步骤。 Spring 提供了两种方式实现事务:①声明式 ②编程式

2.声明式事务和编程式事务

声明式事务:由Spring自动控制,事务在业务逻辑方法执行前开始,在业务逻辑方法正常结束后提交,在业务逻辑方法抛出异常时回滚
编程式事务:需要编写代码控制事务在哪里开始,哪里提交,哪里回滚。

3.事务的性质

**原子性:**确保动作要么全部完成要么完全不起作用
 **隔离性:**数据和资源就处于一种满足业务规则的一致性状态中
 **一致性:**用户的操作不能混淆
 **持久性:**一旦事务完成, 无论发生什么系统错误, 它的结果都不应该受到影响

4.事务的传播行为

**PROPAGATION_REQUIRED:**如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
 **PROPAGATION_REQUIRES_NEW:**创建一个新的事务,如果当前存在事务,则把当前事务挂起。
 **PROPAGATION_SUPPORTS:**如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
 **PROPAGATION_NOT_SUPPORTED:**以非事务方式运行,如果当前存在事务,则把当前事务挂起。
 **PROPAGATION_NEVER:**以非事务方式运行,如果当前存在事务,则抛出异常。
 **PROPAGATION_MANDATORY:**如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
 **PROPAGATION_NESTED:**如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

5.并发事务所导致的问题

当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题。并发事务所导致的问题可以分为下面三种类型:
1)脏读: 对于两个事物 T1, T2, T1 读取了已经被 T2 更新但 还没有被提交的字段. 之后, 若 T2 回滚, T1读取的内容就是临时且无效的.
**2)不可重复读:**对于两个事物 T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段. 之后, T1再次读取同一个字段, 值就不同了.
**3)幻读:**对于两个事物 T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行. 之后, 如果 T1 再次读取同一个表, 就会多出几行.

6.事务的隔离级别

事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持。事务的隔离级别有以下几种:
ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是ISOLATION_READ_COMMITTED。
**ISOLATION_READ_UNCOMMITTED:**该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
**ISOLATION_READ_COMMITTED:**该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
**ISOLATION_REPEATABLE_READ:**该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
**ISOLATION_SERIALIZABLE:**所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
注:Oracle 支持的 2 种事务隔离级别:READ_COMMITED , SERIALIZABLE,Mysql 支持 4 种事务隔离级别.

7.事务的其他属性

**事务超时:**所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
**事务的只读属性:**事务的只读属性是指,对事务性资源进行只读操作或者是读写操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作,那么我们可以将事务标志为只读的,以提高事务处理的性能。在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。
**事务的回滚规则:**通常情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。这通常也是大多数开发者希望的处理方式,也是 EJB 中的默认处理方式。但是,我们可以根据需要人为控制事务在抛出某些未检查异常时任然提交事务,或者在抛出某些已检查异常时回滚事务。

二、事务的使用

以学生刷卡消费为例,目录结构如下

program:
 -StuCard
 -StuCardDao
 -StuCardDaoImpl
 -StuCardService
 -StuCardServiceImpl
 exception:
 -NoEnoughMoneyException

1.StuCard代码如下

//引入lombok后@Data注解会自动生成get set方法
@Data
public class StuCard {
    private String stuCardNo;

    private BigDecimal money;
}

2.StuCardDao代码如下

public interface StuCardDao {

    public StuCard getStuCardInfoById(String cardNo);
    public void updateStuCardByCardNo(StuCard sc);
}

3.StuCardService代码如下

public interface StuCardService {
    //定义transfer方法,参数为支付卡账号,收款卡账号,转账金额
    public void transaferMoney(String targetCardNo, String sourceCardNo, String money);
}

4.NoEnoughMoneyException代码如下

public class NoEnoughMoneyException extends RuntimeException {
	private static final long serialVersionUID = 3524043619467806059L;
	public NoEnoughMoneyException() {
  		super();
 	}

	public NoEnoughMoneyException(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace) {
  		super(message, cause, enableSuppression, writableStackTrace);
 	}
	public NoEnoughMoneyException(String message, Throwable cause) {
  		super(message, cause);
 	}
	public NoEnoughMoneyException(String message) {
  		super(message);
 	}
	public NoEnoughMoneyException(Throwable cause) {
  		super(cause);
 	}
}

5.StuCardDaoImpl代码如下

public class StuCardDaoImpl implements StuCardDao{
//实现接口StuCardDao
    private JdbcTemplate jt;
    //为了使用JdbcTemplate类中一系列数据库操作方法
    /**
    * 根据学生卡编号查询学生卡信息
    */
    public StuCard getStuCardInfoById(String cardNo) {
    	RowMapper<StuCard> rm = new BeanPropertyRowMapper<StuCard>(StuCard.class);
   	//BeanPropertyRowMapper可以将数据库的查询对象直接转化为java对象
  	return jt.queryForObject("select card_no stuCardNo, money from stu_card where card_no = ?", rm, cardNo);
    }
    
    /**
    * 更新学生卡信息
    */
    public void updateStuCardByCardNo(StuCard sc) {
        jt.update("update stu_card set money = ? where card_no = ?", sc.getMoney(), sc.getStuCardNo());
    }
  
    /**
    * @return the jt
    */
    public JdbcTemplate getJt() {
        return jt;
    }

    /**
    * @param jt the jt to set
    */
    public void setJt(JdbcTemplate jt) {
        this.jt = jt;
   }
}

6.StuCardServiceImpl代码如下

public class StuCardServiceImpl implements StuCardService{

	private StuCardDao scDao;

	//转账支付,当金额不足时,抛出异常
	public void transaferMoney(String targetCardNo, String sourceCardNo, String money) {	
		StuCard sourceCard = scDao.getStuCardInfoById(sourceCardNo);
  		StuCard targetCard = scDao.getStuCardInfoById(targetCardNo);
  		//修改后勤人员信息
  		sourceCard.setMoney(sourceCard.getMoney().subtract(new BigDecimal(money)));
  		scDao.updateStuCardByCardNo(sourceCard);
  
  		targetCard.setMoney(targetCard.getMoney().add(new BigDecimal(money)));
  		if(sourceCard.getMoney().compareTo(new BigDecimal("0")) < 0){
   			throw new NoEnoughMoneyException("账户余额不足,请充值");
  		}
  		scDao.updateStuCardByCardNo(targetCard);
  		System.out.println("支付完成!商品购买成功");
 	}
	public StuCardDao getScDao() {
  		return scDao;
 	}
 	public void setScDao(StuCardDao scDao) {
  		this.scDao = scDao;
 	}
}

总结:执行逻辑如下
方法执行时,首先调用StuServiceImpl对象中的方法transferMoney(),然后创建两个StuCard对象,一个代表支付卡,一个代表收款卡。首先将支付卡的Money属性值减去付款金额,将收款卡的Money加上付款金额。然后验证付款卡的金额是否足够支付。若足够则将两张卡的信息传入数据库,若不足则抛出异常,事务回滚。