一.事务特点
1.Atomicity原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
2.Consistency一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
3.Isolation隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
4.Durability持久性:已被提交的事务对数据库的修改应该永久保存在数据库中。
二.Spring事务管理方式
1.编程式
配置数据源-->配置事务管理器-->配置事务模板-->将业务逻辑放入模板中执行
2.声明式(AOP、注解)
配置数据源-->配置事务管理器-->配置切面|配置注解驱动-->配置切点方法|在方法上加注解
三.Spring隔离级别
1.ISOLATION_DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应
2.ISOLATION_READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读
3.ISOLATION_READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
4.ISOLATION_REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
5.ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
四.脏读、不可重复读和幻读含义和区别
脏读 :脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
-
Notes:
不可重复读的重点是修改 :
同样的条件, 你读取过的数据,再次读取出来发现值不一样了
幻读的重点在于新增或者删除
同样的条件, 第 1 次和第 2 次读出来的记录数不一样解决方案:
解决不可重复读就是锁行就行 for update / for update nowait
解决幻读就锁表(项目中一般用不到)for update / for update nowait:
作用:查询时用,解决不可重复读,即查询时将查询到的数据加行锁,保证不被其他进程修改。
区别:
for update:查询到的数据正在被其他进程调用时,会默默等待,直到数据被释放。
for update nowait:查询到的数据正在被其他进程调用时,不会默默等待,会返回”ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效。”的异常,并退出方法。
五.Spring事务传播行为
描述的是事务与事务之间的调用关系,暂时没研究过,不是很懂,一般用1;
1.PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
2.PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
3.PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
4.PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
5.PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。
6.PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常
7.PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行
六.Spring事务原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
获取连接 Connection con = DriverManager.getConnection()
开启事务con.setAutoCommit(true/false);
执行CRUD
提交事务/回滚事务 con.commit() / con.rollback();
关闭连接 conn.close();
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。
1、配置Spring文件
2、spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据配置的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
七.Spring事务使用注意事项
原因一:是否是数据库引擎设置不对造成的。比如mysql,引擎MyISAM,是不支持事务操作的。需要改成InnoDB才能支持
原因二:入口的方法必须是public,否则事务不起作用(这一点由Spring的AOP特性决定的,理论上而言,不public也能切入,但spring可能是觉得private自己用的方法,应该自己控制,不应该用事务切进去吧)。另外private 方法, final 方法 和 static 方法不能添加事务,加了也不生效
原因三:Spring的事务管理默认只对出现运行期异常(java.lang.RuntimeException及其子类)进行回滚(至于为什么spring要这么设计:因为spring认为Checked的异常属于业务的,coder需要给出解决方案而不应该直接扔该框架)
原因四:确保你的业务和事务入口在同一个线程里,否则事务也是不生效的。
@Transactional
@Override
public void save(User user1, User user2) {
new Thread(() -> {
saveError(user1, user2);
System.out.println(1 / 0);
}).start();
}
原因五:service方法中调用本类中的另一个方法,事务没有生效。
@Override
public void doTransfer(String fromName, String toName, BigDecimal money) {
System.out.println("从"+fromName+"账户转"+money+"元到"+toName+"账户开始....");
// 查询两人账户信息
Account fromAccount = accountDao.findAccountByName(fromName);
Account toAccount = accountDao.findAccountByName(toName);
fromAccount.setMoney(fromAccount.getMoney().subtract(money));
toAccount.setMoney(toAccount.getMoney().add(money));
// 转出账户 money-10000
update(fromAccount);
// 转入账户 money+10000
accountDao.updateAccount(toAccount);
}
private void update(Account source) {
accountDao.updateAccount(source);
int i=1/0;
}
<!-- 配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:method name="up*" propagation="REQUIRED" read-only="true" isolation="READ_COMMITTED"/>
</tx:attributes>
</tx:advice>
doTransfer调用配置了事务的update方法,结果遇到异常没有回滚:转出账户 money-10000
其实写这么多我就是因为对第五个原因感兴趣,初学者对于此很懵逼,因此研究了下。
’类内部调用事务不生效‘原因如下:
回顾下之前的对AOP的理解:将附加功能如事务控制、日志打印等非核心功能交给代理类实现。
AOP开发两步走:1.开发通知2.确定切面。
那么,
对于AOP控制事务而言,此时的通知为事务控制, 切入点为需要事务控制的类或者方法
这些都毫无疑问,但是最重要的一点来了,AOP的核心之一就是底层实现使用了“动态代理”模式。
即,切入点是某个类、某个方法,但是切面却是为某个类创建的代理类。而关于事务控制的通知也是添加在代理类上的。
因此,当类内的一个方法调用本类内的另一个方法时,并没有产生代理类,也就是说此时的AOP是没有意义的、失效的。因此,通过AOP配置的事务控制通知肯定也是无法生效的,自然就无法数据回滚。
下面附上代码,只引一个spring就可以了:
<?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:aop="http://www.springframework.org/schema/aop"
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/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">
<!-- 配置业务层-->
<bean id="accountService" class="com.erayt.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="saleTranDao" ref="saleTranDao"></property>
</bean>
<bean id="saleTranService" class="com.erayt.service.impl.SaleTranServiceImpl">
<property name="saleTranDao" ref="saleTranDao"></property>
<property name="accountService" ref="accountService"></property>
</bean>
<!-- 配置账户的持久层-->
<bean id="accountDao" class="com.erayt.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置交易的持久层-->
<bean id="saleTranDao" class="com.erayt.dao.impl.SaleTranDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>
<property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl"></property>
<property name="username" value="1"></property>
<property name="password" value="1"></property>
</bean>
<!-- Spring中基于XML的申明式事务配置步骤
1、配置事务管理器
2、配置事务的通知
此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop的
使用tx:advice标签配置事务通知
属性:
id:给事务通知起一个唯一标识
transaction-manager:给事务通知提供一个事务管理器引用
3、配置AOP中的通用切入点表达式
4、建立事务通知和切入点表达式的对应关系
5、配置事务的属性
是在事务的通知tx:advice标签的内部
-->
<!-- 配置事务管理器-->
<bean id = "transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务的属性
isolation:用于指定事务的隔离级别,默认值是"DEFALUT",表示使用数据库的默认隔离级别
propagation:用于指定事务的传播行为,默认值是REQUIRED,表示一定会有事务。查询可以用SUPPORTS。
read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写
timeout:用于指定事务的超时时间,默认值是-1表示永不超时。如果指定了数值,以秒为单位。
no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,,产生其他异常时事务回滚、没有默认值。表示任何异常都回滚、
rollback-for:用于指定一个异常,当产生该异常时事务回滚,产生其他异常时,事务不回滚,没有默认值,表示任何异常都回滚。
-->
<tx:attributes>
<!-- <tx:method name="*" propagation="SUPPORTS" read-only="false" />-->
<!-- 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。-->
<tx:method name="deal*" propagation="REQUIRED" read-only="true" isolation="READ_COMMITTED"/><!--优先级高于上面 -->
<tx:method name="upd*" propagation="REQUIRED" read-only="true" isolation="READ_COMMITTED"/><!--优先级高于上面 -->
<tx:method name="do*" propagation="REQUIRED" read-only="true" isolation="READ_COMMITTED"/><!--优先级高于上面 -->
<!-- <!– 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务–>-->
<!-- <tx:method name="do*" propagation="REQUIRED" read-only="true" />-->
<!-- <!– 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。 –>-->
<!-- <tx:method name="mand*" propagation="MANDATORY" read-only="true"/>-->
<!-- <!– 总是非事务地执行,并挂起任何存在的事务。–>-->
<!-- <tx:method name="ns*" propagation="NOT_SUPPORTED" read-only="true"/>-->
<!-- <!– 总是非事务地执行,如果存在一个活动事务,则抛出异常–>-->
<!-- <tx:method name="ntested*" propagation="NEVER" read-only="true"/>-->
<!-- <!– 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行 –>-->
<!-- <tx:method name="ntested*" propagation="NESTED" read-only="true"/>-->
<!-- <!– 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行 –>-->
<!-- <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>-->
</tx:attributes>
</tx:advice>
<!-- 配置AOP 使用aop:config标签表明开始AOP的配置-->
<aop:config>
<!-- 配置切入点表达式()-->
<aop:pointcut id="pt1" expression="execution(* com.erayt.service.impl.*.*(..))"/>
<!-- 建立切入点表达式和事务通知的对应关系-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
</beans>
create table TRANTEST_ACCOUNT
(
id NUMBER(5) not null,
name VARCHAR2(20) not null,
money VARCHAR2(60)
)
insert into TRANTEST_ACCOUNT (id,name,money) values (1,'张三',100);
insert into TRANTEST_ACCOUNT (id,name,money) values (2,'李四',100);
/**
* 账户的实体类
*/
public class Account implements Serializable {
private Integer id;
private String name;
private BigDecimal money;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
/**
* service层
*/
@Override
public void doTransfer(String fromName, String toName, BigDecimal money) {
System.out.println("从"+fromName+"账户转"+money+"元到"+toName+"账户开始....");
// 查询两人账户信息
Account fromAccount = accountDao.findAccountByName(fromName);
Account toAccount = accountDao.findAccountByName(toName);
fromAccount.setMoney(fromAccount.getMoney().subtract(money));
toAccount.setMoney(toAccount.getMoney().add(money));
// 转出账户 money-10000
update(fromAccount);
// 转入账户 money+10000
accountDao.updateAccount(toAccount);
}
private void update(Account source) {
accountDao.updateAccount(source);
int i=1/0;
}
/**
* dao层
*/
@Override
public Account findAccountByName(String accountName) {
List<Account> accounts = super.getJdbcTemplate().query("select * from TRANTEST_ACCOUNT where name = ? for update nowait",new BeanPropertyRowMapper<Account>(Account.class),accountName);
if(accounts.isEmpty()){
return null;
}
if(accounts.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accounts.get(0);
}
@Override
public void updateAccount(Account account) {
super.getJdbcTemplate().update("update TRANTEST_ACCOUNT set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}