事务
保证业务操作完整性的数据库技术。
一个业务操作中的多个步骤,要么同时成功,要么同时失败,而且不能产生相互的影响。
事务是数据库保证的,java 中的事务操作仅仅是对数据库中 api 的调用。
控制事务
不同的持久化技术事务控制的方式是不一样的
JDBC:
开启事务 Connection.setCommit(true);
提交事务 Connection.commit();
回滚事务 Connection.rollback();
Mybatis: Mybatis 中 SqlSession 底层封装的也是 Connection
自动开启事务
提交事务 SqlSession.commit();
回滚事务 SqlSession.rollback();
结论: 无论是JDBC,Mybatis,还是其他持久成技术,控制事务的底层都是 Connection 对象来完成的
Spring 控制事务的开发
事务是业务层中的额外功能,既然是额外功能,最好是通过 AOP 的方式进行开发,而 Spring 就是通过 AOP 的方式进行事务开发的。
AOP 四步
1、原始对象
2、额外功能
3、切入点
4、组装切面
1、原始对象
public class ServiceImpl{
原始对象->原始方法->核心功能(业务操作+ Dao 调用)
Dao 调用以成员变量的形式注入进来
}
2、额外功能
org.springframework.jdbc.datasource.DatasourceTransactionManager: Spring 框架封装的控制事务代码,实际上就是如下 MethodIntercept中的事务操作
DataSourceTransactionManager对象对于事务的操作是通过连接对象实现的,所以需要将连接对象注入给它。现阶段开发为了提高连接对象的使用效率,通常通过引入连接池进行
连接获取。所以等效于DataSourceTransactionManager需要连接池,此时我们只需要为它注入连接池,它会根据连接池获取连接进而进行事务控制。
1、MethodIntercept
原始方法运行之前,开启事务,运行之后提交事务,出现异常回滚事务
public void invoke(MethodInvocation invocation){
try{
Connection.setAutoCommit(true);
Object res = invocation.process(); // 原始方法运行
Connection.commit();
}catch(Exception e){
Connection.rollback();
}
return res;
}
2、@Aspect
@Around
3、切入点
@Transactional,可指定事务这个额外功能要加给哪些方法。可加在类上,意味着该类中所有方法都加入事务。可加在方法上,意味着本方法加入事务。
4、组装切面
1、切入点
2、额外功能
Spring 框架中通过一个标签来体现额外功能,通过该标签指定TransactionManager,进而可执行相应的事务操作
<tx:annotation-driven transacationManager=""/>
Spring 框架中通过注解扫描的方式,扫描@Transactional注解,进而找到对应切入点,也就是需要事务操作的方法
细节
<tx:annotation-driven transacationManager="datasourceTransactionManager",proxy-target-class="true"/>
proxy-target-class: 进行动态代理底层实现的切换
默认是 false,通过JDK动态代理(动态代理的目标必须要有接口实现)
true,通过Cglib动态代理(继承目标类,创建它的子类,在子类中重写父类同名的方法,进而实现功能的修改)
Spring 的事务属性
1、什么是事务属性
1、什么是属性
描述物体特征的一些列值
2、事务属性:描述事务特征的一些列值
五个维度:
隔离属性
传播属性
只读属性
超时属性
异常属性
2、如何添加事务属性
@Transactional(isolation=,propagation=,readOnly=,timeOut=,rollbackFor=,noRollbackFor=)
隔离属性:isolation
传播属性:propagation
只读属性:readOnly
超时属性:timeOut
异常属性:rollbackFor/noRollbackFor
3、事物属性详解
隔离属性:描述了事务解决并发问题的特征
1、什么是并发
多个事务(用户)在同一时间访问并操作了相同的数据
2、并发出现了什么问题
脏读
不可重复读
幻读
3、并发问题如何解决
通过隔离属性解决,隔离属性中设置不同的值,解决并发处理过程中的问题
账户表:
id |
name |
password |
balance |
1 |
a |
123 |
1000 |
2 |
b |
123 |
2000 |
3 |
c |
123 |
1000 |
4 |
d |
123 |
1000 |
t1,t2 两个用户同一时间(并不是严格意义的同一时间)访问了 id 为1的数据,产生了并发
事务并发产生的问题
一个事务读取了另外一个事务没有提交的数据,最终会在本事务中产生数据不一致的问题
eg:
用户A,用户B同时访问了id为1的数据,用户A将balance值减去300,在用户A没有提交事务的前提下,用户B他看到的balance值仍然是1000,
他将Balance的值减去400.然后用户B提交事务了。此时用户A不想执行这个操作了,他选择了回滚,然后他提交了事务,在用户A的想法中,现在balance仍然为1000
在用户B的想法中balance的值为600。当有一天两人交谈时会发现数据不一致,出现了脏读。
解决方案:
@Transacational(isolation = Isolation.READ_COMMITTED) 读已提交
一个事务读取另一个事务提交后的数据
一个事务中多次读取相同的数据,但是读取的数据结果不一样,这样就会在本事务中产生数据不一致的问题
eg:
用户A,用户B同时访问了 id 为1的数据,用户A以0.00001秒的差距先一步进行访问,并开启了事务,他做了一次查询,查询到balance的值为1000,
然后在本次事务(用户A)中,用户A去做其他业务操作了,并没有提交事务。此时用户B开始进行访问Id为1的数据,他更新的balance值,将其变成了800,
然后用户B提交了事务。当用户A做完业务操作后,他又一次对 id 为1的数据进行了一次查询,此时balance值为800,出现多次访问中数据不一致的问题。
解决方案:
@Transacational(isolation = Isolation.REPEATABLE_READ) 可重复读
实际上是为该条数据加了行锁,当用户A先一步访问数据时,它会为这条数据加一把行锁,只要用户A事务没有结束,锁没有释放,那用户B永远都要等着
一个事务中多次对整表数据进行查询(如聚合balance值),但是多次查询数据不一致
eg:
用户A,用户B同时访问账户表数据,用户A先一步进行了访问,并开启了事务,他查询了账户表中的总金额数(sum(balance)),然后就去做其他业务操作了,
本事务并没有提交,此时用户B开发访问账户表数据,他新增了一条数据,id为5,该数据balance值为2000,然后提交事务了。此时用户A又一次访问账户表的总balance值
发现值发生了变化,表中多了一条id为5的数据,就像影子一样。数据产生不一致。
解决方案:
@Transacational(isolation = Isolation.SERIALIZABLE) 可序列化的
实际上是为账户表加了表锁,当用户A访问账户表时,会为该表加一把表锁,只有用户A事务提交,并释放表锁,用户B才能访问账户表
总结
并发安全上来说,一定是序列化方式安全性最高(SERIALIZABLE),之后依次是可重复读(REPEATABLE_READ),读已提交(READ_COMMITTED)
效率上来说,顺序相反,READ_COMMITTED未加任何锁
数据库对于隔离属性的知识
隔离属性的值 |
mysql |
oracle |
READ_COMMITTED |
支持 |
支持 |
REPEATABLE_READ |
支持 |
不支持 |
SERIALIZABLE |
支持 |
支持 |
oracle 不支持可重复读的隔离属性,它是通过多版本对比的方式来解决这个问题。
Spring 默认的隔离属性
ISOLATION_DEFAULT,代表它会调用不同数据库所设置的隔离性属性
Mysql: REPEATABLE_READ
Oracle: READ_COMMITTED
隔离属性在实战中的建议
推荐使用Spring指定的默认值 ISOLATION_DEFAULT
如果连接不同数据库,则使用的是数据库默认隔离级别
如果并发量高,也不推荐使用隔离属性解决,因为隔离属性中加锁是物理锁,对系统性能影响较大,一般会使用乐观锁的方式解决并发高的场景
传播属性(propagation)
事务的嵌套
TA事务中包含TB数据,TB事务中包含TC事务,等。
事务嵌套会出现的问题:
大事务TA中融入了多个小事务TB、TC,小事务TC出现异常回滚,TB事务正常提交,此时会出现,TA,TC回滚,而TB并没有回滚的现象。影响了事务的原子性
事务嵌套如何解决:
使用传播属性来解决
传播属性的值
传播属性的值 |
外部不存在事务 |
外部存在事务 |
用法 |
备注 |
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) |
不常用 |
融合到外部事务意味着放弃本事务,使用外部事务
默认的传播属性
增删改: 直接使用 Spring 默认的传播属性 REQUIRED
查询: 显示的指定传播属性为 SUPPORTS
只读属性
针对于只查询的业务方法,可以加入只读属性,可提高运行效率(当事务为只读属性时,不会为其加各种各样的锁,也就意味着性能会得到释放)
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
默认值:false
超时属性(timeout)
指定了事务等待的最长时间
为什么事务要进行等待呢
因为在当前事务访问数据时,有可能访问的数据被别的事务加锁,那么本事务就需要等待了,等待的时间以秒为单位
超过 timeout 就会抛出异常
@Transactional(timeout=2)
默认值: -1 最终由对应的数据库来指定
异常属性
spring 事务处理过程中
默认 对于RuntimeException及其子类采用的是回滚的策略
默认 对于Exception及其子类采用的是提交的策略
rollbackFor = {} 回滚
noRollbackFor = {} 提交
@Transactional(rollbackFor=java.lang.Exception.class) 当遇到Exception及其子类的异常时,进行回滚
@Transactional(noRollbackFor=java.lang.RuntimeException.class) 当遇到RuntimeException及其子类的异常时,进行提交
事务属性常见配置总结
隔离属性 使用默认
传播属性 增删改: 默认 查询: SUPPOETS
只读属性 增删改: 默认 false 查询: true
超时属性 使用默认
异常属性 使用默认
增删改 : @Transactionl
查询: @Transactional(propagation=Propagation.SUPPORTS,readOnly=true)