一.代码演示,引出事务的概念
(1)Accout类
package com.keen.proxy.domain;
public class Accout {
private int id;
private int banlance;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getBanlance() {
return banlance;
}
public void setBanlance(int banlance) {
this.banlance = banlance;
}
}
(2)IAccoutDAO接口
package com.keen.proxy.dao;
public interface IAccoutDAO {
//转出操作
void transOut(int outId ,int money) ;
//转入操作
void transInt(int inId ,int money);
}
(3)IAccoutDAO实现类
package com.keen.proxy.dao.impl;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import com.keen.proxy.dao.IAccoutDAO;
public class IAccoutDAOImpl implements IAccoutDAO {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource ds) {
this.jdbcTemplate = new JdbcTemplate(ds);
}
public void transOut(int outId, int money) {
jdbcTemplate.update("update accout set balance =balance -? where id = ?",money , outId);
}
public void transInt(int inId, int money) {
jdbcTemplate.update("update accout set balance =balance +? where id = ?",money , inId);
}
}
(4)IAccoutService接口(转账服务)
package com.keen.proxy.service;
public interface IAccoutService {
//做转账事务
void trans(int outId ,int inId ,int money) ;
}
(5) IAccoutSerivce的实现
package com.keen.proxy.service.impl;
import com.keen.proxy.dao.IAccoutDAO;
import com.keen.proxy.service.IAccoutService;
public class IAccoutSerivceImpl implements IAccoutService{
IAccoutDAO dao = null;
public void setIAccoutDAO(IAccoutDAO dao) {
this.dao = dao;
}
public void trans(int outId, int inId, int money) {
dao.transOut(outId, money);
// int i = 1/0; 模仿程序出错,这样就会出现一方转账了,钱减少了,而另一方却因程序中断而收不到转账金额。
dao.transInt(inId, money);
}
}
(6)配置文件
<?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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 从classPath的路径去加载db.properties文件 -->
<context:property-placeholder location="classpath:db.properties" system-properties-mode="NEVER" />
<!-- 配置一个durid的连接池 -->
<bean id = "dataSource" class ="com.alibaba.druid.pool.DruidDataSource"
init-method = "init" destroy-method="close">
<property name = "driverClassName" value ="${dirverClassName}"/>
<property name = "url" value ="${url}"/>
<property name = "username" value ="${username}"/>
<property name = "password" value ="${password}"/>
<property name = "initialSize" value ="${initialSize}"/>
</bean>
<!-- 配置dao -->
<bean id = "accoutDAO" class = "com.keen.proxy.dao.impl.IAccoutDAOImpl">
<property name="dataSource" ref = "dataSource"/>
</bean>
<!-- 配置service -->
<bean id ="service" class = "com.keen.proxy.service.impl.IAccoutSerivceImpl">
<property name = "iAccoutDAO" ref = "accoutDAO"/>
</bean>
</beans>
(7)测试
package com.keen.proxy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.keen.proxy.service.IAccoutService;
@SpringJUnitConfig
public class AutoTest {
@Autowired
private IAccoutService service;
@Test
void testAccout() throws Exception {
service.trans(10086, 10010, 500);
}
}
二.转账过程分析
1.service ,掉调用DAO的转出方法--->发送转出的sql语句
2.service ,再调用DAO的转入方法---->发送转出SQL语句
⚠️:jdbc默认情况下,事务是自动提交的,获取一个connection对象的时候,事务已经打开了,执行完DML语句就自动提交事务
⚠️:因此当上面的程序发现异常就会出现转账不到帐的异常,因为转出装入两个操作根本就不在同一个事务中。
思路修改:
1.获取dataSource对象
2.通过DataSource对象获取数据库连接对象(Connection)
3.取消事务的自动提交机制 connection.setAutoCommit(false)
4.把connection对象绑定在当前线程中
5.在DAO的方法中,从当前线程中获取出connection对象,执行操作
6.如果整个service方法都正常执行,中途没有异常:提交事务 ,connection.commit()
7.如果service方法中出现任何异常: 回滚事务,connection.rollback()
IAccoutSerivce的实现 代码改为:
1.取消事务的自动提交机制
2.获取连接对象
try{
public void trans(int outId, int inId, int money) {
dao.transOut(outId, money);
// int i = 1/0; 模仿程序出错,这样就会出现一方转账了,钱减少了,而另一方却因程序中断而收不到转账金额。
dao.transInt(inId, money);
}
}catch(Expection e){
回滚事务.
}
三.其实在xml配置文件中,我们配置一下事务管理器相关的操作就可以避免这次额问题了。
<!-- *********************看这里!看这里!看这里!**************************** -->
<!--1.what: 配置事务管理器的JDBC,需要一个连接池属性 -->
<bean id = "txManager" class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref = "dataSource"/>
</bean>
<!-- 2.when:配置事务管理器增强 -->
<tx:advice id ="txAdvice" transaction-manager ="txManager">
<tx:attributes>
<tx:method name="trans"/>
</tx:attributes>
</tx:advice>
<!-- 3.where :配置切面 -->
<aop:config>
<aop:pointcut expression="execution(* com.keen.proxy.service.*Service.*(..))" id="txPc"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPc"/>
</aop:config>
//补充说明:
<!-- ****配置一个通用事务配置**** -->
<tx:advice id="crudAdvice" transaction-manager="txManager">
<tx:attributes>
<!-- service中的查询方法 -->
<tx:method name="get" read-only="true" propagation="REQUIRED"/>
<tx:method name="list" read-only="true" propagation="REQUIRED"/>
<tx:method name="query" read-only="true" propagation="REQUIRED"/>
<!-- service中的其他方法(非查询) -->
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
四.事务的回顾
1.何为数据库事务
事务是一系列操作组成的工作单元,该工作单位内的操作是不可分割的,即要么所有的操作都执行,要么所有的操作都不执行
事务必需满足ACID(原子性,一致性,隔离性,和持久性)
原子性(Atomicity):事务是不可分割的最小单位,事务内的操作要么全做,要不都不做;
一致性(Consistency):事务执行前数据处于正确的状态,而事务执行后数据库的数据依然处于正确的状态,即数据完整性约束没有被破坏,如A给B转帐,无论是否转账成功,A和B之间的账户总额和转账前的总额是一致。
隔离性(Isolation):当多个事务处于并发访问同一个数据资源时,事务之间相互影响程度,不同的隔离级别决定了各个事务对数据资源访问的不同行为。
持久性(Durability):事务一旦执行成功,它对数据库的数据的改变是不可逆。
2.数据库并发问题
并发问题类型 产生原因和效果
第一类丢失更新 两个事务更新相同数据,如果一个事务提交,另一个事务回滚,第一个事务的更新就会被回滚
脏读 第二个事务查询到第一个事务未提交的更新数据,第二个事务根据该数据执行,单第一个事务回滚,第二个事务操作脏数据
虚读 一个事务查询到了另一个事务已经提交的新数据,导致多次查询的数据不一致
不可重复读 一个事务查询另一个事务已经修改的数据,导致多次查询的数据不一致
第二类丢失更新 多个事务同时读取相同数据,并完成各自的事务提交,导致最后一个事提交会覆盖前面所有事务对数据的改变
3.事务的隔离级别
为了解决这些并发问题,需要通过数据库级别来解决,在标准SQL规范中定义了四种隔离级别(√表示可能出现的情况)
隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED √ √ √ X √
READ COMMITED X √ √ √ √
REPREATABLE READ X X √ √ √
SERIALIZABLE X X X X X
Mysql支持四种隔离级别,缺省时为 REPREATABLE READ
Oracle支持 READ COMMITED (缺省) SERIALIZABLE
如何选用
隔离级别越高,数据库事务并发执行性能越差,能处理的操作越少。
因此时间项目开发中为了考虑并发性能一般使用 READ COMMITED,它呢避免丢失更新和脏读,尽管不可重复读和幻读不能避免
更多情况下使用悲观锁和乐观锁来解决。
四.事务的类型
1,本地事务和分布式事务
本地事务:就是普通事务,能保证单台数据库上的操作的ACID,被限定在一台数据库
分布式事务:涉及多个数据库源的事务,即跨多台同类或异类数据库的事务(由每台数据库的本地事务组成),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库
2,jdbc事务和jta事务
JDBC事务:就是数据库事务类型中的本地事务,通过Connection对象的控制来管理事务
JTA事务:JTA指(java Transaction API),是java EE数据库事务规范,JTA只提供了事务管理接口,由应用程序服务器厂商提供实现,JTA事务比JDBC更强大,支持分布式事务
3,按是否通过编程实现事务:
编程式事务:通过编写代码来管理事务
声明式事务:通过注解或xml配置来管理事务
五.Spring对事务支持的API
spring的事务管理主要有3个接口:
(1)platformTransactionManager:
根据TransactionDefinition提供的事务属性配置信息,创建事务
(2)TransactionDefinition:
封装事务的隔离级别和超时时间,是否为只读事务和事务的隔离级别和传播规则等事务属性
(3) TransactionStatus:封装了事务的具体运行状态,如是否式新开启事务,是否已经提交事务,设置当前事务为rollback-only等
六.事务传播规则
在一个事务方法中调用其他事务方法,此时事务该如何传播 ,按照什么规则传播,用谁的事务,还是都不用...等等。
情况一:需要遵从当前事务
REQUIRED:必须存在一个事务,如果当前存在一个事务,则加入到该事务中,否则新建一个事务,使用比较多
SUPPORT:支持当前事务,如果当前存在事务,则使用该事务,否则,以非事务形式运行
MANDATORY:必须要存在事务,如果当前存在事务,则使用该事务,否则,出现非法的事务状态异常
情况二:不遵从当前事务
REQUIRES_NEW: 不管当前是否存在事务,都会新开启一个事务,必须是一个新的事务,使用的比较多
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,把当前事务挂起(暂停)
NEVER:不支持事务,如果当前存在事务,抛出一个异常
情况三:寄生事务(外部事务/内部事务/嵌套事务)
NESTED:寄生事务,如果当前存在事务,则在内部事务内执行
如果当前不存在事务,则创建一个新的事务 ,寄生事务通过数据库savePoint(保存点)来实现,寄生事务可以回滚的,但是它回滚不影响外部事务,但是外部事务的回滚回影响寄生事务
寄生事务并不是所以有的事务管理器都支持,比如HibernateTransactionManager默认就不支持,需要手动去开启 JDBC和MyBatis的事务管理器:DataSourceTransactionManager:默认就是支持的