1. 事务
1. 什么是事务?
保证业务操作完整性的一种数据库机制
事务的4特点: A C I D
1. A 原子性
2. C 一致性
3. I 隔离性
4. D 持久性
1.2 如何控制事务
JDBC:
Connection.setAutoCommit(false);
Connection.commit();
Connection.rollback();
Mybatis:
Mybatis自动开启事务
sqlSession(Connection).commit();
sqlSession(Connection).rollback();
结论:控制事务的底层 都是Connection对象完成的。
2. Spring控制事务的开发
Spring是通过AOP的方式进行事务开发
2.1 开发步骤
2.1.1. 原始对象
- 原始对象 —》 原始方法 —》核心功能 (业务处理+DAO调用)
- DAO作为Service的成员变量,依赖注入的方式进行赋值
public class XXXUserServiceImpl{
private xxxDAO xxxDAO
set get
}
2.1.2 额外功能
额外功能原本应该通过实现MethodInterceptor接口,在invoke方法里面实现:
public Object invoke(MethodInvocation invocation){
try{
Connection.setAutoCommit(false);
Object ret = invocation.proceed();
Connection.commit();
}catch(Exception e){
Connection.rollback();
}
return ret;
}
或者在类上面加上@Aspect注解 就可以告知Spring这是一个额外功能的实现类,在方法加上@Around注解,就可以在注解里面定义切入点了,不需要实现任何接口
而Spring提供了org.springframework.jdbc.datasource.DataSourceTransactionManager事务管理器来管理事务,会自动帮助我们完成上面invoke方法里面的开启事务, 提交事务和回滚事务,我们只要在使用时给他注入连接池 让dataSourceTransactionManager获取连接 进而通过连接控制事务,完成事务控制的这一额外功能 。
2.1.3 切入点
Spring还提供了@Transactional 这一注解来定义事务额外功能的切入点,他控制了事务的额外功能加入给哪些业务方法。
- 类上:类中所有的方法都会加入事务
- 方法上:这个方法会加入事务
2.1.4 组装切面
还提供了tx:annotation-driven/这个标签支持事务注解,他会告诉spring开启事务注解了,然后我们里面吧额外功能,也就是dataSourceTransactionManager传给transaction-manager这个属性即可
2.2. 编码实现Spring控制事务
2.2.1 搭建开发环境
依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.14.RELEASE</version>
</dependency>
userDAO接口和Mapper映射文件
public interface UserDao {
public void save(User user);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tcgroup.dao.UserDao">
<insert id="save" parameterType="user">
insert into mybatis.users(id,name,pwd) values (#{id},#{name},#{password})
</insert>
</mapper>
2.2.2 接口
public interface UserService {
public void register(User user);
}
2.2.3 实现类(原始类)
//通过@Transactional注解 让这个类所有的方法都实现事务的额外功能
@Transactional
public class UserServiceImpl implements UserService{
private UserDao userDao;
//set get方法让spring通过set吧userDAO注入进来
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) {
userDao.save(user);
}
}
2.2.4 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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 这里吧连接数据库的参数放到db.properties文件里面去了 指定spring读取db.properties配置 -->
<context:property-placeholder location="classpath:db.properties" />
<!-- 连接池配置 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 通过SQLSessionFactoryBean创建SQLSessionFactory -->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 告诉SQLSessionFactoryBean用上面的连接池 -->
<property name="dataSource" ref="dataSource"/>
<!-- 给com.tcgroup.entity这个包下面的类起别名 -->
<property name="typeAliasesPackage" value="com.tcgroup.entity"/>
<!-- 指定Mapper文件路径 -->
<property name="mapperLocations">
<list>
<!-- 这里的value是String类型的文件路径, 也是用/做分隔符 -->
<value>classpath:com/tcgroup/dao/*Mapper.xml</value>
</list>
</property>
</bean>
<!-- 通过MapperScannerConfigurer创建dao对象 -->
<bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 告诉MapperScannerConfigurer 工厂是上面这个sqlSessionFactoryBean -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<!-- 让MapperScannerConfigurer在 com.tcgroup.dao这个包里面找对应的接口,创建对应的dao对象 -->
<property name="basePackage" value="com.tcgroup.dao"/>
</bean>
<!-- 在UserServiceImpl里面注入UserDAO对象 上面scanner已经创建了 首字母小写获取即可 -->
<bean id="userService" class="com.tcgroup.service.UserServiceImpl">
<property name="userDao" value="userDao"/>
</bean>
<!-- 实现事务控制额外功能的类-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入连接池 让dataSourceTransactionManager获取连接 进而通过连接控制事务 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- <tx:annotation-driven/>这个标签 就是支持事务注解的(@Transactional) -->
<!-- transaction-manager 获取额外功能-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>
2.2.5 测试(测试回滚可以在原始方法 UserServiceImpl的register方法里面抛出异常)
public void test2(){
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
//spring生产出来的接口实例默认是首字母小写
UserDao userDao = (UserDao) classPathXmlApplicationContext.getBean("userDao");
User user = new User();
user.setName("黄鹤楼");
user.setPassword("huanghekou");
userDao.save(user);
}
2.3 细节
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/>
通过proxy-target-class这个属性 我们可以进行动态代理底层实现的切换 默认是 false也就是 JDK提供的动态代理模式,如果原始类没有实现接口,可以改成true 改成Cglib继承的方式实现动态代理
3. Spring中的事务属性(Transaction Attribute)
3.1 什么是事务属性
属性:描述物体特征的一系列值
比如: 性别 身高 体重 …
事务属性:描述事务特征的一系列值
- 隔离属性 isloation
- 传播属性 propagation
- 只读属性 readOnly
- 超时属性 timeout
- 异常属性 rollbackFor=,noRollbackFor=,)
3.2 如何添加事务属性
上面实现Spring的事务控制时 我们需要在实现类(原始类)或者方法上面加上@Transactional注解,而在@Transactional注解里面,就可以加上事务的属性
@Transactional(isloation=,propagation=,readOnly=,timeout=,rollbackFor=,noRollbackFor=,)
3.3 事务属性详解
3.3.1 隔离属性 (ISOLATION)
隔离属性的概念:他描述了事务解决并发问题的特征
-
什么是并发
多个事务(用户)在同一时间,访问操作了相同的数据同一时间:0.000几秒 微小前 微小后
-
并发会产生那些问题
1. 脏读
2. 不可重复读
3. 幻影读 -
并发问题如何解决
通过隔离属性解决,隔离属性中设置不同的值,解决并发处理过程中的问题。
事务并发产生的问题
- 脏读
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
例如:张三的卡余额为5000,事务A中把他的余额改为3000,但事务A尚未提交。
与此同时,
事务B正在读取张三的余额,读取到张三的工资为3000。
随后,
事务A发生异常,而回滚了事务。张三的余额又回滚为5000。
最后,
事务B读取到的张三余额为3000的数据即为脏数据,事务B做了一次脏读。
解决方案 @Transactional(isolation=Isolation.READ_COMMITTED)
- 不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
例如:在事务A中,读取到张三的余额为5000,操作没有完成,事务还没提交。
与此同时,
事务B把张三的余额改为8000,并提交了事务。
随后,
在事务A中,再次读取张三的余额,此时余额变为8000。
在一个事务中前后两次读取的结果并不致,导致了不可重复读,注意:1 不可重复读不是脏读,8000也不是脏数据 2 不可重复读发生在一个事务中(事务A)
解决方案 @Transactional(isolation=Isolation.REPEATABLE_READ)
本质: 数据库底层会为我们操作的这个数据加上一把行锁
- 幻影读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
例如:目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。
此时,
事务B插入一条工资也为5000的记录。
这是,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。
解决方案 @Transactional(isolation=Isolation.SERIALIZABLE)
本质:数据库底层会为我们操作的这个表加上一把表锁
Spring 事务的隔离级别:
-
ISOLATION_DEFAULT:
这是一个 PlatfromTransactionManager 默认的隔离级别,
使用数据库默认的事务隔离级别.
另外四个与 JDBC 的隔离级别相对应
MySQL默认 : REPEATABLE_READ
Oracle默认: READ_COMMITTED -
ISOLATION_READ_UNCOMMITTED: 这是事务最低的隔离级别, 它充许令外一个
事务可以看到这个事务未提交的数据。
这种隔离级别会产生脏读,不可重复读和幻像读。 -
ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一
个事务读取。另外一个事务不能读取该事务未提交的数。 -
ISOLATION_REPEATABLE_READ: 这种事务隔离级别可以防止脏读, 不可重复读。
但是可能出现幻像读。
它除了保证一个事务不能读取另一个事务未提交的数据外, 还保证了避免下面的
情况产生(不可重复读)。 -
ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务
被处理为顺序执行,除了防止脏读,不可重复读外,还避免了幻像读。
数据库对于隔离属性的支持
隔离属性的值 | MySQL | Oracle |
---|---|---|
READ UNCOMMITTED | √ | × |
READ COMMITTED | √ | √(默认隔离级别) |
REPEATABLE READ | √(默认隔离级别) | × |
SERIALIZABLE | √ | √ |
Oracle不支持REPEATABLE_READ值 如何解决不可重复读
采用的是多版本比对的方式 解决不可重复读的问题
- 查看数据库默认隔离属性
- MySQL
select @@tx_isolation;
- Oracle
SELECT s.sid, s.serial#,
CASE BITAND(t.flag, POWER(2, 28))
WHEN 0 THEN 'READ COMMITTED'
ELSE 'SERIALIZABLE'
END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr
AND s.sid = sys_context('USERENV', 'SID');
总结
事务的隔离级别和数据库运行效率是成反比的,隔离级别越高,运行效率越低。
隔离级别(从高到底): SERIALIZABLE>REPEATABLE_READ>READ_COMMITTED>READ_UNCOMMITTED
运行效率: READ_UNCOMMITTED<READ_COMMITTED>REPEATABLE_READ>SERIALIZABLE
隔离属性在实战中的建议
推荐使用Spring指定的ISOLATION_DEFAULT
- MySQL repeatable_read
- Oracle read_commited
未来实战中,并发访问情况很低
如果真遇到并发问题,推荐优先用乐观锁解决
Hibernate(JPA) Version
MyBatis 通过拦截器自定义开发
3.3.2 传播属性(PROPAGATION)
事务嵌套
什么叫做事务的嵌套:他指的是一个大的事务中,包含了若干个小的事务
我们在开发一个复杂的系统时可能经常出现这样的场景:比如,A函数中调用了B函数,而A函数和B函数同时都使用了事务,比如Service调用另一个service,这样就出现了事务嵌套
问题:大事务中融入了很多小的事务,他们彼此影响,最终就会导致外部大的事务,丧失了事务的原子性
传播属性的概念
描述了事务解决嵌套问题的特征
传播属性的值及其用法
传播属性的值 | 外部不存在事务 | 外部存在事务 | 用法 | 备注(常用场景) |
---|---|---|---|---|
REQUIRED | 开启新的事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.REQUIRED) | 增删改方法 |
SUPPORTS | 不开启事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.SUPPORTS) | 查询方法 |
REQUIRES_NEW | 开启新的事务 | 挂起外部事务,创建新的事务 | @Transactional(propagation = Propagation.REQUIRES_NEW) | 日志记录方法中 |
NOT_SUPPORTED | 不开启事务 | 挂起外部事务 | @Transactional(propagation = Propagation.NOT_SUPPORTED) | 及其不常用 |
NEVER | 不开启事务 | 抛出异常 | @Transactional(propagation = Propagation.NEVER) | 及其不常用 |
MANDATORY | 抛出异常 | 融合到外部事务中 | @Transactional(propagation = Propagation.MANDATORY) | 及其不常用 |
如果外部存在事务, 内部事务融合到外部事务中去,那么就只有外部事务一个事务,事务嵌套就不存在了
默认的传播属性
REQUIRED是传播属性的默认值
推荐传播属性的使用方式
增删改 方法:直接使用默认值REQUIRED(不需要显示指定值)
查询 操作:显示指定传播属性的值为SUPPORTS
3.3.3 只读属性(readOnly)
针对于只进行查询操作的业务方法,可以加入只读属性,提供运行效率
默认值:false
3.3.4 超时属性(timeout)
指定了事务等待的最长时间
- 为什么事务进行等待?
当前事务访问数据时,有可能访问的数据被别的事务进行加锁的处理,那么此时本事务就必须进行等待。 - 等待时间 单位:秒
- 如何应用
@Transactional(timeout=2) - 超时属性的默认值 -1
等待时间最终由对应的数据库来指定
3.3.5 异常属性
Spring事务处理过程中当程序抛出异常时
默认 对于RuntimeException及其子类 采用的是回滚的策略
默认 对于Exception及其子类 采用的是提交的策略
设置对于Exception及其子类采用回滚策略
rollbackFor = {java.lang.Exception.class}
设置对于RuntimeException及其子类采用提交策略
noRollbackFor = {java.lang.RuntimeException,xxx,xx}
@Transactional(rollbackFor = {java.lang.Exception.class},noRollbackFor = {java.lang.RuntimeException.class})
建议:实战中使用RuntimeExceptin及其子类 使用事务异常属性的默认值也就是不进行设置
3.4 事务属性常见配置总结
- 隔离属性 默认值
- 传播属性 Required(默认值) 增删改 Supports 查询操作
- 只读属性 readOnly false 增删改 true 查询操作
- 超时属性 默认值 -1
- 异常属性 默认值
增删改操作 @Transactional
查询操作 @Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
4. 基于标签的事务配置方式(事务开发的第二种形式)
4.1. 基于注解 @Transaction的事务配置回顾
1. 原始对象
<bean id="userService" class="com.tcgroup.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>
2.额外功能
<!--DataSourceTransactionManager-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
3.通过@Transactional加入切入点 设置事务属性
@Transactional(isolation=,propagation=,...)
public class UserServiceImpl implements UserService {
private UserDAO userDAO;
4.整合切面
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
4.2 基于标签的事务配置
1. 原始对象
<bean id="userService" class="com.tcgroup.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>
2.额外功能
<!--DataSourceTransactionManager-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
3.事务属性
<tx:advice id="txAdvice" transacation-manager="dataSourceTransactionManager">
<tx:attributes>
<tx:method name="register" isoloation="",propagation=""></tx:method>
<tx:method name="login" .....></tx:method>
等效于
@Transactional(isolation=,propagation=,)
public void register(){
}
</tx:attributes>
</tx:advice>
<aop:config>
4.切入点
<aop:pointcut id="pc" expression="execution(* com.tcgroup.service.UserServiceImpl.register(..))"></aop:pointcut>
5.切面组装
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc"></aop:advisor>
</aop:config>
问题:
- 有一个方法, tx:attributes里面就得写一个标签
- 切入点 ,指定了具体的方法,未来有非常多的方法需要加,最好直接用包切入点, 切入service包及其子包
4.3 基于标签的事务配置在实战中的应用方式
<bean id="userService" class="com.tcgroup.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>
<!--DataSourceTransactionManager-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="txAdvice" transacation-manager="dataSourceTransactionManager">
<tx:attributes>
<tx:method name="register"></tx:method>
编程时候 service中负责进行增删改操作的方法 都以modify开头,用*做通配符
<tx:method name="modify*"></tx:method>
把*放在modify*下面,表示除了上面定义过的方法意外的其他方法,一般是除了增删改以外的查询操作 命名无所谓
<tx:method name="*" propagation="SUPPORTS" read-only="true"></tx:method>
</tx:attributes>
</tx:advice>
应用的过程中,service放置到service包中,用包切入点
<aop:config>
<aop:pointcut id="pc" expression="execution(* com.tcgroup.service..*.*(..))"></aop:pointcut>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc"></aop:advisor>
</aop:config>