1、什么是事务?
保证业务操作完整性的一种数据库机制(事务由数据库控制,java代码只是对这种事务的调用)
事务的四个特点
- A(原子性)
- C(一致性)
- I(隔离性)
- D(持久性)
如何控制事务
采用不同的持久化技术,控制事务的方式是不同的
- jdbc
- Connection.setAutoCommit(false)`
- Connection.commit();
- Connection.rollback();
- Mybatis:
- Mybatis自动开启事务,你只要关心事务提交和回滚
- sqlSession.commit();
- sqlSession.rollback();
mybatis在事务提交时底层也是封装了Connection对象。
结论
:无论是jdbc,还是mybatis、控制事务的底层都是通过Connection对象来完成事务的。
Spring如何控制事务
Spring是通过aop的方式进行事务开发的。
所以aop开发的四个步骤如下:
原始对象
一个一个service,因为我们的事务是加在service层的。
public class XXXUserServiceImpl(){
private xxxDAO xxxDAO
1.原始对象--->原始方法---->核心功能(业务处理+DAO调用)
2.DAO作为Service的成员变量,依赖注入的方式进行赋值
}
额外功能(这里是环绕通知)
//环绕通知
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("原始方法运行之前 开启事务--------------------");
Object obj = joinPoint.proceed();//执行我们的原始方法,它会返回一个Object
System.out.println("原始方法运行之后 提交事务 --------------------");
return obj;
}
但是如果元素代码抛异常我们需要回滚事务,该怎么做,把它们放在try-cath中就行
//环绕通知
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
try{
System.out.println("原始方法运行之前 开启事务--------------------");
Object obj = joinPoint.proceed();//执行我们的原始方法,它会返回一个Object
System.out.println("原始方法运行之后 提交事务 --------------------");
}catch(Exception e){
}
//回滚事务
}
return obj;
}
这个代码,尽然你我都能想得到,spring自然想得到,所以spring尽然知道你写、我写的都一样,那我spring框架给你封装不就行了。spring就封装了一个类:org.springframework.jdbc.datasource.DataSourceTransactionManager
但是控制事务都是由连接对象Connection完成的,所以DataSourceTransactionManager就需要通过连接对象配合他来完成,所以在用DataSourceTransactionManager时,需要为其注入连接,但是我们为了提高创建连接的效率,我们提供了连接池,所以等效于DataSourceTransactionManager需要注入连接池。
所以在应用的过程中:
1、使用DataSourceTransactionManager类
2、注入连接池
切入点
只需要应用注解@Transactional
这个注解来指定事务这个额外功能加给哪些事务方法:
该注解可以加在两个位置上
- 类上:类中所有的方法都会加入事务
- 方法上:这个方法加入事务
组装切面
切面有两部分组成:1、切入点 2、额外功能
在spring进行事务的控制过程中,它的组装切面是通过标签来进行的。这个标签是txt:annotation-driven tracscation-managers="(获取第二步中定义的额外功能) "
切入点该标签会自动的扫描你标识的@transactional这个注解来识别
2、Spring控制事务的编码?
开发环境
· 首先,导入spring框架的基本开发包,一共5个
-然后,导入mysql数据库驱动jar包
- 接着,咱要在程序中使用jdbcTemplate模板类,所以还需要导入如下jar包。
其中,spring-tx-5.2.6.RELEASE.jar就是Spring用于事务管理时使用到的jar包。 - 紧接着,还要在程序中使用德鲁伊连接池,所以导入德鲁伊连接池所需的jar包
- 在接着,由于咱们接下来会使用到Spring声明式事务管理这种方式来管理事务,所以还得导入Spring AOP开发的jar包。
事务案列
数据库
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
在上面导入过jar包的module中新建com.atguigu.dao、com.atguigu.daoimpl、com.atguigu.service、com.atguigu.serviceimpl、com.atguigu.pojo、com.atguigu.test包。在com.atguigu.dao新建抽象类UserDao,抽象类中有一个抽象方法save()
public interface UserDao {
void save(User user);
}
``
在com.atguigu.daoimpl包中新建一个类叫UserDaoImpl,该类实现接口UserDao,并重写save()
```java
public class UserDaoImpl implements UserDao {
//获取jdbcTemplate对象
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void save(User user) {
//1 创建sql语句
String sql="insert into t_user values(?,?,?)";
int update=jdbcTemplate.update(sql,user.getId(),user.getUserName(),user.getPassword());
System.out.println(update);
}
}
在com.atguigu.service,新建抽象类UserService,里面有一个抽象方法 register()
public interface UserService {
public void register(User user);
}
在com.atguigu.serviceImpl中创建类UserServiceImpl类,实现com.atguigu.service中UserService抽象类的抽象方法register()
//元素对象(核心功能:业务运算+调用dao)
@Transactional//所有方法都有了这个事务
public class UserServiceImpl implements UserService {
private UserDao userDao;//此时dao还是空的,需要在spring文件中进行赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) {
userDao.save(user);
}
}
spring配置文件的配置:
jdbcTemplate的配置见这里,这里不再给出。
根据aop的四个步骤:
1、创建原始对象
<!--1、创建原始对象-->
<bean id="userService" class="com.atguigu.serviceimpl.UserServiceImpl">
<!--给service里的dao层对象赋值-->
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.atguigu.daoimpl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
2、添加额外功能(创建事务管理器)
<!--2、添加额外功能:创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
3、切入点:在类上加transactional注解
4、组装切面
这一步需要引入名称空间(第一个名称空间是引入数据库相关的外部文件的):
<!--4、组装切面:
transaction-manager="transactionManager":关联额外功能
切入点会自动扫描注解
-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
在com.atguigu.test创建一个类TransactionTest类,里面创建test1()的单元测试方法
@Test
public void test1(){
ApplicationContext context=new ClassPathXmlApplicationContext("bean.xml");
//返回的是UserService的代理类对象
UserService userService = context.getBean("userService", UserService.class);
userService.register(new User(null,"shou","123456"));
}
运行之后,数据库多一条记录
至此,我们也不知道事务添加了事务,所以下面验证。
验证事务是否添加
修改com.atguigu.serviceimpl包中的UserServiceImpl类
,把该类中的register方法修改为如下:
//元素对象(核心功能:业务运算+调用dao)
@Transactional//所有方法都有了这个事务
public class UserServiceImpl implements UserService {
private UserDao userDao;//此时dao还是空的,需要在spring文件中进行赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) {
userDao.save(user);
throw new RuntimeException("这里有异常");
}
}
我们给register()加上了事务,里面并添加了一个异常,查看添加到数据库是否成功,来查看我们添加的事务是否成功。
先把上面的数据库清空:
运行刚才单元测试方法:
控制台:
数据库
细节:
事务属性(Transaction Attribute)
1.什么是事务属性
属性:描述物体特征的一系列值(人:性别、身高、体重。。。)
事务属性包括
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
2.如何添加事务属性
语法
如何添加一个隔离属性:
@Transactional(isolation=,)
再添加一个传播属性:
@Transactional(isolation=,propagation=)
…
在异常属性上有两个值:
@Transactional(isolation=,propagation=,readonly=,timeout=,rollbackFors或者noRollbackFors)
@Transactional注解只能加在公共方法上
·
隔离属性
概念:它描述了事务解决并发问题的特征。
引出3个问题:
1、什么是并发?多个事务(用户)同一时间,访问操作了相同的数据。同一时间:也并不是刚好相同时间(比如:0.000几秒,是一个非常小的时间)
2、并发会产生哪些问题
- 脏读
- 不可重复读
- 幻读
3、并发问题的解决—隔离属性:隔离属性中设置不同的值,解决并发处理过程中的问题(本质都是加锁)。
脏读
一个事务,读取了另一个事务中没有提交的数据,会在本事务中产生数据不一致的问题。
你是否会问,事务而怎么会读取到事务一中没有提交的数据呢?
在没有正式提交之前,数据是先更新到日志之中的,只有commit提交之后,才真正再从日志中更新到mysql表中。你不用去纠结为什么能读到它。你只需知道,是事务与事务并发进行时,是具有影响性的,这种影响性的大小被称为隔离级别。那怎么防止未提交读呢?就是加写锁,然后提交之后释放锁,在这个时候,mysql的隔离级別就变成了已提交读。而加锁,释放锁是由mysql自动完成的,你无需关心。所以,其实本质上是锁的应用,最终导致了不同的隔离级别,不同的隔离级别,锁的应用也不同。
解决方案:·@Transaction(isolation=Isolation.READ_COMMITTED)·
不可重复读
一个事务中,多次读取相同的数据,但是读取结果不一样,会咋本事务中产生数据不一致的问题
注意:1 不是脏读 2 一个事务中
解决方案:@Transaction(isolation=Isolation.REPEATABLE_READ)
本质:行锁
幻读
一个事务中,多次对整表进行查询统计,但是结果不一样,会在本事务中产生数据不一致的问题。
解决方案:@Transaction(isolation=Isolation.SERIALIZABLE)
本质:表锁
总结:并发安全 SERIALIZABLE–>REPEATABLE_READ–>READ_COMMITTED
运行效率:READ_COMMITTED–》REPEATABLE_READ—》SERIALIZABLE
数据库对隔离属性的支持
spring使用的是数据库的默认隔离属性:
mysql—》REPEATABLE_READ
oracle—>READ_COMMITTED
推荐使用Spring指定的隔离级别 ,因为未来中的实战中,并发访问情况很低。
如果遇到并发问题,乐观锁。
传播属性(PROPAGATION)
传播属性的概念:它描述了事务解决嵌套问题的特征。
什么叫做事务的嵌套:事务直接的包含关系:
传播属性的核心:在同一时间只有同一个事务。
什么情况下会可能出现事务嵌套呢?
service调用service的情况下,有可能事务嵌套,如下。
问题:大事务中融合了很多小的事务,它们彼此影响,最终会导致外部大的事务,丧失了原子性。—解决:传播属性
传播属性的值及其用法:
1、REQUIERD属性:
先看下面三个业务类
下面是AService中的aMethod方法调用了另外两个service中的两个方法。给aMethod()、bMethod()、cMethod()都加上传播属性:
对应aMethod由于该事务的属性是:Propagation_REQUIRED,所以aMethod外部不存在事务,所以开启新的事务,对于b和c的属性也是Propagation_REQUIRED,而b和c它们外部已经有a事务了,所以b和c融合到a事务中—体现了传播属性的核心:同一时间只有同一个事务。
在实战中的应用:增、删、改方法中用该属性。(在增删改中加了该事务,无论是融合了还是保留自己的事务,它都有事务)
2、SUPPORTS属性:
在实战中,这个属性会应用在查询的业务方法中,因为查询的业务方法,它不需要事务。如果独立应用,像上述的amethod,他就不开了,如果它被别人调用,反正我不用事务,我可以配合外部的事务,融合到外部事务中,如上图第二种情况。
以上两个传播属性解决了我们为来开发中99%的属性设置,因为REQUIRED解决了增、删、改,SUPPORTS解决了查询,而我们大多业务就是增删改。
2、REQUERS_NEW属性:
所以该属性一般是:日志记录的方法中使用。
另外三个属性,99%在开发中及其不常用,看下表的总结,了解一下即可。
总结
上表中只有前两个用的最多,第三个都不多,后面的三个更少。
REQUIRED
是传播属性的默认值
推荐传播属性的使用方式
增、删、该 方法:直接使用默认值REQUIRED
查询操作:使用SUPPORTS
只读属性(readOnly)
针对于只进行查询操作的业务方法,可以加入只读属性,提高运行效率。(加上事务为了保证并发安全,我们就要加各种各样的锁,显然这个锁会把并行操作变成串行操作,当加上了只读属性后,就不会在加额外的锁了)
//元素对象(核心功能:业务运算+调用dao)
@Transactional//所有方法都有了这个事务
public class UserServiceImpl implements UserService {
private UserDao userDao;//此时dao还是空的,需要在spring文件中进行赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) {
userDao.save(user);
throw new RuntimeException("这里有异常");
}
@Override
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
public void login(String name, String password) {
}
}
因为,我们在类上加了 @Transactional,所以每个方法都会加上,又因为每个属性都会使用默认的方法(对于隔离属性就是isolation = Isolation.REPEATABLE_READ、对于传播属性是:Propagation.REQUIRED),而我们的login方法对于隔离属性无所谓,默认的就行,对于传播属性(因为是查询),所以应该使用传播属性的SUPPORTS,而对于查询而言可以增加只读属性提高查询效率,所以我们在方法上加入 @Transactional(propagation = Propagation.SUPPORTS,readOnly = true)进行设置,可以覆盖掉类上加的@Transactional的默认属性值。
只读属性的默认值是:false
超时属性(tiemout)
概念:指定了事务等待的最长时间
1.为什么事务运行等待?
当前事务访问数据时,有可能访问的数据被别的事务进行加锁处理,那么此时本事务就必须进行等待。
2.等待的时间以秒为单位
3、如何应用@Transactional(timeout=2)
代码中的演示:
//元素对象(核心功能:业务运算+调用dao)
@Transactional(timeout = 2)//所有方法都有了这个事务
public class UserServiceImpl implements UserService {
private UserDao userDao;//此时dao还是空的,需要在spring文件中进行赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) {
try {
//设置等待3s
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
userDao.save(user);
// throw new RuntimeException("这里有异常");
}
@Override
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
public void login(String name, String password) {
}
}
4.超时属性的默认值-1(最终由对应的数据库来指定)
5.开发中很少自己指定,一般使用默认值
异常属性(tiemout)
对于如下的类:
//元素对象(核心功能:业务运算+调用dao)
@Transactional()//所有方法都有了这个事务
public class UserServiceImpl implements UserService {
private UserDao userDao;//此时dao还是空的,需要在spring文件中进行赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) {
try {
//设置等待3s
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
userDao.save(user);
throw new RuntimeException("这里有异常");
}
@Override
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
public void login(String name, String password) {
}
}
因为我们手动抛出了异常,事务会进行回滚
测试之前先看看数据库:
下面运行测试方法:
运行之后的数据库:
说明事务进行了回滚操作。
如果把throw new RuntimeException(“这里有异常”),改为
throw new Exception("这有异常呦!");
,就会发生提交。
//元素对象(核心功能:业务运算+调用dao)
@Transactional()//所有方法都有了这个事务
public class UserServiceImpl implements UserService {
private UserDao userDao;//此时dao还是空的,需要在spring文件中进行赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void register(User user) throws Exception {
try {
//设置等待3s
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
userDao.save(user);
//throw new RuntimeException("这里有异常");
throw new Exception("这有异常呦!");
}
@Override
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
public void login(String name, String password) {
}
}
因为我们的register方法抛出了异常,所以Service方法中的抽象方法register也要抛出异常。
准备工作就绪,现在我们在运行测试方法,发现有了数据,说明事务进行了提交。
spring事务处理过程中:
默认—对于RuntimeException及其子类,采用的是回滚的策略
默认—对于Exception以及子类,采用的是提交的策略。
那对于RuntimeException及其子类能不能进行提交的策略?
对于Exception及其子类能不能采用回滚的策略?
使用:
rollbackFor={java.lang.Exception.class,xxx,xxx}//大括号是个数组,里面可以定义多个异常,遇到这些异常都会回滚,但是要加.class
spring的处理逻辑是
:
- spring框架会手写检查方法抛出的异常是不是在rollbackFor的属性值中,如果异常在rollbackFor列表中,不管是RuntimeException异常还是Exception异常,一定回滚。
- 如果你的抛出异常不在rollbackFor列表中,spring会判断异常是不是RuntimeException,如果是一定回滚。
noRollbackFor={{java.lang.RuntimeException,xxx,xxx}};//大括号是个数组,里面可以定义多个异常,遇到这些异常都不回滚。
使用如下:
实战中,我们很少按照上面的自己设置,建议:实战中使用RuntimeException及其子类,使用事务异常属性的默认值。
事务属性常见配置总结
- 隔离属性 默认值
- 传播属性 required(默认值)增删改 SUPPORTS 查询操作
- 只读属性 readOnly 默认值(false) false:增删改 true 查询操作
- 超时属性 默认值-1
- 异常属性 默认值
这个结论记住,以后直接写就行了。
增、删、改 操作 直接:@Transactional
(即隔离属性默认值、传播属性默认值、readonly默认值、超时、异常也是默认值)
查询操作 @Transactional(propagation=Propagation.SUPPORTS,realOnly=true)
基于xml的事务配置方式(事务开发第二种形式)
上面采用的都是基于注解的方式,先来回顾四个步骤。
<!--1、创建原始对象-->
<bean id="userService" class="com.atguigu.serviceimpl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.atguigu.daoimpl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--2、添加额外功能:创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--3、切入点:在类上加transactional注解-->
@Transactional(rollbackFor = {java.lang.Exception.class},noRollbackFor = {java.lang.RuntimeException.class})//所有方法都有了这个事务
public class UserServiceImpl implements UserService {}
<!--4、组装切面:
transaction-manager="transactionManager":关联额外功能
切入点会自动扫描注解
-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
基于xml的配置方式和基于注解的配置方式的区别仅仅在于第三步和第四步
<!--1、创建原始对象-->
<bean id="userService" class="com.atguigu.serviceimpl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.atguigu.daoimpl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--2、添加额外功能:创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--3、事务属性
一定要加tx为结尾的,上面beans里面会有 xmlns:tx="http://www.springframework.org/schema/tx"
id:自定义名称,表示<tx:advice>和</tx:advice>之间的配置内容
transaction-manager:事务管理器对象的id
-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--tx:attributes:配置事务属性-->
<tx:attributes>
<!--tx:method:给具体的方法配置事务属性,可以出现多次,分别给不同 的方法设置事务属性-->
<tx:method name="register" isolation="DEFAULT" propagation="REQUIRED"/>
<tx:method name="login" isolation="DEFAULT" propagation="SUPPORTS"></tx:method>
<!--可以为多个方法进行配置-->
等效于: @Transactional(rollbackFor = {java.lang.Exception.class},noRollbackFor = {java.lang.RuntimeException.class})//所有方法都有了这个事务
public class UserServiceImpl implements UserService {}
</tx:attributes>
</tx:advice>
<!--配置切入点和切面-->
<aop:config>
<aop:pointcut id="pc" expression="execution(* com.atguigu.serviceimpl.UserServiceImpl.login(*,*))"></aop:pointcut>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc"></aop:advisor>
</aop:config>
如果实战中过多的方法,那么就会有上述第三步,需要多个方法
<tx:method name="register" isolation="DEFAULT" propagation="REQUIRED"/>
<tx:method name="login" isolation="DEFAULT" propagation="SUPPORTS"></tx:method>
的冗余。
基于xml的事务配置在实战中的应用方式:
第一、二步一样,
第三步改为:
一般小型项目使用@Transactional注解,如果大型项目可以用上面的第三步的声明,大型项目方法那么多,不可能用注解一个一个的加上