事务是什么?
在操作数据库时(增删改),如果同时操作多次数据,我们从业务希望,要不全部成功,要不全部失败。这种情况称为事务处理。
A转账给B。
第一步,扣除A君账号要转的金额
第二步,增加B君账号的金额
事务:指单个逻辑操作单元的集合
Spring事务控制我们要明确的
1.JavaEE体系进行分层开发,事务处理位于业务层,所以,一般情况下我们使用事务代理,一般放在分层设计业务层。
2.spring框架为我们提供了一组事务控制的应用程序接口(API)。
3.spring的事务控制都是基于AOP的,它既可以使用编程的方式实现,也可以使用配置的方式实现。我们学习的重点是使用配置的方式实现
案例引出问题:
A转账给B。
需求:从A君账户给B账户转账1000元钱。
第一步,扣除A君账号要转的金额
第二步,增加B君账号的金额
案列分析:按正常来说,完成这两步才算成功,但假如第一步完成,开始第二步时系统出错了,也就是整个转账只扣了A的钱,然后出了错,却没给B加钱,1000元就在系统错误中消失了,这要怎么办了?
这就需要事务管理了,要么转账两步成功执行,要么都不执行
结论: 根据上述案例分析, 事务管理应该是在Service层处理
模拟转账代码:(事务在Dao层的代码)
Dao层:接口和实现类
package cn.sxt.mapper; public interface UserMapper { /** * 转出方法 * @param outId * @param money */ void updateIn(Integer inId,Integer money); /** * 转入方法 * @param inId * @param money */ void updateOut(Integer outId,Integer money); }
package cn.sxt.mapper.Impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import cn.sxt.mapper.UserMapper; @Repository public class UserMapperImpl implements UserMapper { @Autowired private JdbcTemplate template; @Override public void updateIn(Integer inId, Integer money) { String sql = "update t_account set balance = balance - ? where id = ?"; template.update(sql, money, inId); } @Override public void updateOut(Integer outId, Integer money) { String sql = "update t_account set balance = balance + ? where id = ?"; template.update(sql, money, outId); } }
service层接口和实现类
package cn.sxt.service; public interface UserService { /** * 转账业务方法 * @param outId 转出账号 * @param inId 转入账号 * @param money 金额 */ void trans(Integer outId,Integer inId,Integer money); }
package cn.sxt.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import cn.sxt.mapper.UserMapper; import cn.sxt.service.UserService; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper mapper; /* * 真正的业务方法 * 一个业务方法有多个 功能(多个dao层方法)组成 */ @Override public void trans(Integer outId, Integer inId, Integer money) { mapper.updateIn(inId, money); System.out.println(1/0); mapper.updateOut(outId, money); } }
测试类:
package cn.sxt.test; 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; import cn.sxt.service.UserService; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class test01 { @Autowired private UserService Service; @Test public void testName() throws Exception { Service.trans(2, 1, 500); } }
配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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 "> <!-- 配置组件包扫描的位置 --> <context:component-scan base-package="cn.sxt" /> <!-- 读取db.properties配置文件到Spring容器中 --> <context:property-placeholder location="classpath:db.properties" /> <!-- 配置 阿里巴巴的 druid 数据源(连接池) --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- SpringEL 语法 ${key} --> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <!-- ${username}如果key是username,name 默认spring框架调用的当前操作系统的账号 解决方案:可以统一给key加一个前缀 --> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxActive" value="${jdbc.maxActive}"/> </bean> <!-- 配置jdbcTemplate| --> <bean id="template" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入数据源(连接池) --> <property name="dataSource" ref="dataSource"></property> </bean> </beans>
jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT jdbc.username=root jdbc.password=gzsxt jdbc.maxActive=10
导入的包:
数据库并发问题
什么是数据库并发问题?
并发: 多个客户端同时同时访问数据库中某一条数据(秒杀)
数据库可以拥有多个访问客户端,若多个客户端并发地访问数据库中相同的资源,如果没有采取必要的隔离措施,则会导致各种并发问题,破坏数据的完整性。
这些问题归结为5类
包括3类数据读问题(脏读,不可重复读,幻读)
和2类数据更新问题(第一类丢失更新,第二类丢失更新)。 看图
数据库事务的隔离级别
问题:上述问题理论上如果出现了应该如何解决?
答:一般情况,数据库都会处理一些事务并发的问题,数据库提供了不同的事务隔离级别来处理不同的事务并发问题,事务隔离级别定义如下:
隔离级别 |
说明 |
READ_UNCOMMITED |
允许你读取还未提交的改变了的数据。可能导致脏、幻、不可重复读(相当于没有做任何事务隔离) |
READ_COMMITTED |
允许在并发事务已经提交后读取。可防止脏读,但幻读和 不可重复读仍可发生(ORACLE默认级别) |
REPEATABLE_READ |
对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏、不可重复读,但幻读仍可能发生。(MYSQL默认级别) |
SERIALIZABLE |
完全服从ACID的隔离级别,确保不发生脏、幻、不可重复读。这在所有的隔离级别中是最慢的,它是典型的通过完全锁定在事务中涉及的数据表来完成的。(ORACLE支持)
|
针对不同隔离级别可以解决的的如下五类问题
解决丢失更新的方案
数据库表数据加锁
1,悲观锁
(1) 在操作当前数据的事务开启事务就使用for update 锁住当前数据
(2) Hibernate和MyBatis都有悲观锁对应的解决方案
2,乐观锁
(1) 为表添加一个version字段。当前事务操作的时候都会比对当前事务情况的多次操作的版本号是否一致,如果不一致认为数据已经被更新
(2) Hibernate和MyBatis都有乐观锁对应的解决方案
Spring对事务的支持
- 为什么需要使用Spring是事务
答:使用Spring是事务代理,可以配置一次,不用重复编写事务处理代码!!
Spring框架针对事务处理提供专门的解决方案
Spring的事务管理主要包括3个接口
Spring事务处理接口 |
描述 |
TransactionDefinition |
封装事务的隔离级别超时时间,是否为只读事务和事务的隔离级别和传播规则等事务属性,可通过XML配置具体信息 |
PlatformTransactionManager |
根据TransactionDefinition提供的事务属性配置信息,创建事务。事物管理器 |
TransactionStatus |
封装了事务的具体运行状态。比如,是否是新开启事务,是否已经提交事务,设置当前事务为rollback-only等 |
TransactionDefinition
该接口主要定义了:事务的传播行为(规则),事务的隔离级别,获得事务信息的方法。所以在配置事务的传播行为,事务的隔离级别已经需要获得事务信息时,可以通过查阅该类的代码获得相关信息。
TransactionDefinition源码
public interface TransactionDefinition {
//事务的传播行为 int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4; int PROPAGATION_NEVER = 5; int PROPAGATION_NESTED = 6; //事务的隔离级别 int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE; //事务超时管理 int TIMEOUT_DEFAULT = -1;
//获得事务信息 int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
String getName();
}
|
事务传播规则
Spring在TransactionDefinition接口中定义了七种事务传播规则,规定了事务方法和事务方法发生嵌套调用时事务该如何进行传播
事务传播规则类型 |
描述 |
PROPAGATION_REQUIRED |
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务(最常用操作)-Spring默认使用的就是此规则 |
PROPAGATION_REQUIRES_NEW |
创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
PROPAGATION_SUPPORTS |
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
PROPAGATION_NOT_SUPPORTED |
以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
PROPAGATION_NEVER |
以非事务方式运行,如果当前存在事务,则抛出异常 |
PROPAGATION_MANDATORY |
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
PROPAGATION_NESTED |
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED |
PlatformTransactionManager事务管理器
Spring的事务管理:
1,PlatformTransactionManager:接口统一抽象处理事务操作相关的方法;
1):TransactionStatus getTransaction(TransactionDefinition definition):
根据事务定义信息从事务环境中返回一个已存在的事务,或者创建一个新的事务,并用TransactionStatus描述该事务的状态。
ü 2):void commit(TransactionStatus status):
根据事务的状态提交事务,如果事务状态已经标识为rollback-only,该方法执行回滚事务的操作。
ü 3):void rollback(TransactionStatus status):
将事务回滚,当commit方法抛出异常时,rollback会被隐式调用
2,在使用spring管理事务的时候,首先得告诉spring使用哪一个事务管理器,使用不同的框架(JdbcTemplate,MyBatis,Hibernate/JPA )使用事务管理器都不同
PlatformTransactionManager事物管理器的继承体系图
常用的事务管理器:
DataSourceTransactionManager:使用JDBC,MyBatis的事务管理器;
事物方法的属性细节配置
事务方法属性:
事务方法属性 |
描述 |
name |
匹配到的方法模式 |
read-only |
如果为true,开启一个只读事务,只读事务的性能较高,但是不能再只读事务中,使用DML |
isolation |
代表数据库事务隔离级别(就使用默认) DEFAULT:让spring使用数据库默认的事务隔离级别; |
no-rollback-for |
如果遇到的异常是匹配的异常类型,就不回滚事务 |
rollback-for |
如果遇到的异常是指定匹配的异常类型,才回滚事务;spring默认情况下,spring只会去回滚RuntimeException及其子类,不会回滚Exception和Thowable |
propagation |
事务的传播方式(当一个方法已经在一个开启的事务当中了,应该怎么处理自身的事务) 1,REQUIRED(默认的传播属性):如果当前方法运行在一个没有事务环境的情况下,则开启一个新的事务,如果当前方法运行在一个已经开启了的事务里面,把自己加入到开启的那个事务中 2,REQUIRES_NEW:不管当前方法是否运行在一个事务空间之内,都要开启自己的事务
|
timeout |
事务处理的超时时间,默认事物管理自动处理 ,可以手动配置
|
Spring事务的配置
Spring支持编程式事务管理和声明式事务管理。
- 编程式事务管理:事务和业务代码耦合度太高。
- 声明式事务管理:侵入性小,把事务从业务代码中抽离出来,使用AOP配置到配置文件中,提高维护性。
声明式事务管理-xml方式配置(新的xml配置解决了上面的问题)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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:component-scan base-package="cn.zj.spring"/> <!-- 读取db.properties配置文件到Spring容器中 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置 阿里巴巴的 druid 数据源(连接池) --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- SpringEL 语法 ${key} --> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <!-- ${username}如果key是username,name 默认spring框架调用的当前操作系统的账号 解决方案:可以统一给key加一个前缀 --> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxActive" value="${jdbc.maxActive}"/> </bean> <!-- 配置jdbcTemplate| --> <bean id="template" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入数据源(连接池) --> <property name="dataSource" ref="dataSource"/> </bean> <!-- 开始Spring的事务配置 配置思路 1. 配置事务管理器 2.配置事务细节 把事务管理器配置好事务管理中 3.把事务管理器 使用AOP切入Service层 --> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据源(连接池) --> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置事务细节 ,事务增强/通知 transaction-manager :要增强事务管理器 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <!-- 属性配置 --> <tx:attributes> <!-- 具体要增强的方法 (JointPoint) 事务一般对业务方法增强 当前例子:是对service层的转账方法增强 method :要增强的方法名 当前场景 :trans 方法 method实际开发一般可以分为两类 DML :增删改 DQL :查询 我们可以使用 * 星号 作为通配符来配置 这两方法 isolation: 事务的隔离级别,DEFAULT 默认就是当前数据库默认级别 propagation :事务传播规则(行为) timeout : 设置事务超时时间 read-only :是否是只读事务 DML :非只读事务 DQL :只读事务(执行效率高) --> <!-- <tx:method name="trans" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="5" read-only="false" /> --> <!-- DQL : 只读事务 --> <tx:method name="select*" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="5" read-only="true"/> <tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="5" read-only="true"/> <tx:method name="query*" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="5" read-only="true"/> <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="5" read-only="true"/> <!-- 其他操作 :非只读事务 --> <tx:method name="*" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="5" read-only="false"/> </tx:attributes> </tx:advice> <!-- 使用aop把事务切入到service层 --> <aop:config> <!-- 配置切入点 --> <aop:pointcut expression="execution(* cn.zj.spring.service..*.*(..))" id="pt"/> <!-- 配置切面 = 切入点 +通知 <aop:aspect></aop:aspect> 一般开发者自定义aop功能使用 <aop:advisor advice-ref=""/> spring事务管理器配置时候使用 advice-ref :通知引用 底层默认使用的环绕通知 pointcut-ref :切入点引用 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/> <!-- 织入,Spring完成 --> </aop:config> </beans>
声明式事务管理-基于注解配置
applicationContext.xml配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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:component-scan base-package="cn.sxt" /> <!-- 读取db.properties配置文件到Spring容器中 --> <context:property-placeholder location="classpath:db.properties" /> <!-- 配置 阿里巴巴的 druid 数据源(连接池) --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- SpringEL 语法 ${key} --> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <!-- ${username}如果key是username,name 默认spring框架调用的当前操作系统的账号 解决方案:可以统一给key加一个前缀 --> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="maxActive" value="${jdbc.maxActive}" /> </bean> <!-- 配置jdbcTemplate| --> <bean id="template" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入数据源(连接池) --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开始Spring的事务配置 配置思路 1. 配置事务管理器 2.配置事务细节 把事务管理器配置好事务管理中 3.把事务管理器 使用AOP切入Service层 --> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据源(连接池) --> <property name="dataSource" ref="dataSource" /> </bean> <!-- 开启事务注解驱动 transaction-manager: 是万物管理器 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
service类
package cn.sxt.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Propagation; import cn.sxt.mapper.UserMapper; import cn.sxt.service.UserService; /* * * @Transactional * 开启注解事务配置,一个 @Transactional只能对当前类有效,当前类下面所有方法配置一样 * isolation=Isolation.REPEATABLE_READ, 隔离级别 propagation=Propagation.REQUIRED, 传播行为(规则) timeout=5, 超时时间 readOnly=true 是否是只读事务 * */ @Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED, timeout = 5, readOnly = false) @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper mapper; /* * 真正的业务方法 一个业务方法有多个 功能(多个dao层方法)组成 */ @Override public void trans(Integer outId, Integer inId, Integer money) { mapper.updateIn(inId, money); System.out.println(1 / 0); mapper.updateOut(outId, money); } @Transactional(readOnly = true) public void selectByPrimaryKey(Integer id) { } @Transactional(readOnly = true) public void selectList() { } }
事物配置的注解和XML配置的选择
Xml配置 : 代码清晰,配置量少,容易维护
注解配置 : 侵入代码,每个Service层类都得配置,针对不同的方法还得配置事务细节:如查询方法配置只读操作,不容易维护
建议选择xml配置
小结Spring框架:
1.1. Spring 核心作用
解耦 (降低程序代码与代码的直接依赖关系)
1.2. 核心功能
Spring容器-下面所有操作都在Spring 容器中完成,Spring就是项目对象的管家
- IOC :控制反转(将对象的创建权交给Spring管理)
(1) Xml 配置
① <bean id/name=’xxx’ class = “类的全限定名” scope=’作用范围’ init-mehtod=’初始化方法’destory-mehtod=’销毁方法’>
② 从Spring容器中读取bean对象
(2) 注解配置 -贴在类上即可
① @Component 通用组件扫描创建
② @Controller 表现层(SpringMVC 专用)
③ @Service 业务层专用
④ @Repository 持久层/DAO/Mapper 专用
⑤ @Scope 作用范围
⑥ @PostConstrcut 初始化方法
⑦ @PreDestory 销毁方法
- DI : 依赖注入(将对象的属性赋值,对象依赖关系维护交给Spring)
(1) XML配置
① <property name=’’ value/ref=’’> 属性注入
② <constructor-arg name=’参数名’ value/ref=’’ >
③ P 命名空间
1) <bean id =’xx’ class =”Xxxx” p:属性名=’值’ p:属性名-ref=’引用类型’>
(2) 注解配置
① Spring框架制定
1) @Autowired 默认按照类型注入
- 可以贴在 字段(成员编写),set方法,构造函数
2) @Qualifier 从多个相同类型中通过id指定唯一的那个bean
② JavaEE 官方指定
1) @Resource(name=’bean的id’)
- AOP :面向切面编程
(1) AOP 底层原理-Java动态代理
① JDK动态代理 : 只能有接口的类型进行代理
② CGLIB代理 :即可代理有接口类,有可以代理没有接口的
(2) 专业术语
① JoinPoint 连接点(方法)
② PointCut 切入点 (某一类方法,规则)
③ Advice 通知/增强
④ Aspect 切面 = 切入点+通知
⑤ Weaving 织入(Spring自动完成)
- Spring事务管理
(1) 配置事务管理器
① JDBC/MyBatis ---->DataSourceTransactionManager
(2) 事务的隔离级别 4个
(3) 事务的传播规则(行为) 7个
(4) 是否是只读事务
(5) 使用AOP 将事务切入到 业务层
- Spring-Jdbc (了解)
(1) jdbcTemplate 模板类
① Update 更新方法(update,insert,delete)
② queryForObject : 单行查询
③ Query : 多行查询
- Spring-test
- SpringMVC