Spring基础-事务

1、什么是事务

事务是数据库保证原子性的一种机制,所有的操作要么都成功,要么都失败。了解事务,一定绕不开的他的四个特点:ACID

  • A(代表了事务的原子性)
  • C(代表了事务的一致性)
  • I(代表了事务的隔离性)
  • D(代表了事务的持久性)

2、控制事务原理

使用JDBC的事务,我们控制事务的手段是Connection.setAutoCommit(false)开启事务,Connection.commit()提交事务,Connection.rollback()回滚事务

我们使用Mybatis用SqlSession.commit(),和SqlSession.rollback(),其实这都是MyBatis的SqlSession对Connection的封装,他的底层还是调用的Connection.commit()方法

控制事务的底层原理,就是使用Connection来进行操作

3、Spring如何控制事务

事务,其实和核心业务没有太大的关系,是一个可有可无的功能(对于业务)。所以,我们可以使用AOP来做这件事情,Spring也是通过AOP来实现这个控制业务的

既然是AOP,还是那四个步骤:

  • 原始对象

原始对象就是我们具体的service业务逻辑的实现类

public class UserServiceImpl implements UserService {

    private UserDao userDao;
    //get、set方法,通过spring的注入,将userDao注入进来

    public void save(User user) {
        userDao.save(user);
    }
}
  • 额外功能

额外功能有两种实现的方法,一种是实现MethodInterceptor接口,实现invoke方法;一种是通过@Aspect注解来实现

public class Around implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Object proceed ;
        try {
            //开启事务
            proceed = methodInvocation.proceed();
            //提交事务
        }catch (RuntimeException e){
            //回滚事务
        }
        return proceed ;
    }
}

 这一段代码,Spring也给我们封装了一个类,DataSourceTransactionManager,这个类就是帮助我们控制事务的!因为事务的底层是用Connection来完成的,所以我们需要注入连接池。

  • 切入点

Spring为我们提供了一个注解作为切入点@Transactional,这个注解可以放在类上,也可以放在方法上。

  • 组装切面

切面 = 额外功能 + 切入点

<tx:annotation-driven transaction-manager=""></tx:annotation-driven>

4、Spring控制事务代码演示

引入maven依赖

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
   <version>5.1.14.RELEASE</version>
</dependency>

接口

public interface UserService {

    void save(User user);
}

原始类(实现类) 

public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(User user) {
        userDao.save(user);
    }
}

因为userDao这个对象,我们需要通过spring来进行注入,配置文件

    <bean id="userSerivce" class="com.wx.mybatis.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

额外功能

Spring提供了DataSourceTransactionManager类,所以我们直接注入DataSource连接池即可

    <!--    额外功能-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

切入点

直接在类上添加@Transactional注解,作为切入点

@Transactional
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(User user) {
        userDao.save(user);
    }
}

切面(切面 = 切入点 + 额外功能)

    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>

完整的applicationContext.xml配置文件

<?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: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/tx https://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--    连接池-->
    <bean id="dataSource"
          class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver">
        </property>
        <property name="url" value="jdbc:mysql://localhost:3306/user?useSSL=false"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
    <!--创建SqlsessionFactoryBean-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <!--        执行实体对应的包-->
        <property name="typeAliasesPackage" value="com.wx.mybatis.entity"></property>
        <property name="mapperLocations">
            <list>
                <value>classpath*:mapper/*Mapper.xml</value>
            </list>
        </property>
    </bean>

    <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
        <!--设置dao所在的包路径-->
        <property name="basePackage" value="com.wx.mybatis.dao"></property>
    </bean>

    <!--    额外功能-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <bean id="userSerivce" class="com.wx.mybatis.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
</beans>

测试

2021-04-29 22:50:19 DEBUG SqlSessionUtils:99 - Creating a new SqlSession
2021-04-29 22:50:19 DEBUG SqlSessionUtils:130 - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:50:19 DEBUG SpringManagedTransaction:89 - JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] will be managed by Spring
2021-04-29 22:50:19 DEBUG save:137 - ==>  Preparing: insert into t_user(name, password) values (?, ?)
2021-04-29 22:50:19 DEBUG save:137 - ==> Parameters: zhaoliu(String), 123(String)
2021-04-29 22:50:19 DEBUG save:137 - <==    Updates: 1
2021-04-29 22:50:19 DEBUG SqlSessionUtils:188 - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:50:19 DEBUG SqlSessionUtils:286 - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:50:19 DEBUG SqlSessionUtils:312 - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:50:19 DEBUG SqlSessionUtils:317 - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:50:19 DEBUG DataSourceTransactionManager:743 - Initiating transaction commit
2021-04-29 22:50:19 DEBUG DataSourceTransactionManager:326 - Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@51c668e3]
2021-04-29 22:50:19 DEBUG DataSourceTransactionManager:384 - Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] after transaction

Process finished with exit code 0

通过输出的日志,可以看出插入成功了,DataSourceTransactionManager类也确实帮助我们提交了事务

那么我们再来证明下,我们的事务是在UserServiceImpl的save()方法上,而不是dao层中。我们可以在service层中抛出一个异常,看是否会回滚事务

@Transactional
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(User user) {
        userDao.save(user);
        throw new RuntimeException("异常了");
    }
}
2021-04-29 22:56:54 DEBUG SqlSessionUtils:99 - Creating a new SqlSession
2021-04-29 22:56:54 DEBUG SqlSessionUtils:130 - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:56:54 DEBUG SpringManagedTransaction:89 - JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] will be managed by Spring
2021-04-29 22:56:54 DEBUG save:137 - ==>  Preparing: insert into t_user(name, password) values (?, ?)
2021-04-29 22:56:54 DEBUG save:137 - ==> Parameters: zhaoliu(String), 123(String)
2021-04-29 22:56:54 DEBUG save:137 - <==    Updates: 1
2021-04-29 22:56:54 DEBUG SqlSessionUtils:188 - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:56:54 DEBUG SqlSessionUtils:312 - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:56:54 DEBUG SqlSessionUtils:317 - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8]
2021-04-29 22:56:54 DEBUG DataSourceTransactionManager:836 - Initiating transaction rollback
2021-04-29 22:56:54 DEBUG DataSourceTransactionManager:341 - Rolling back JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@51c668e3]
2021-04-29 22:56:54 DEBUG DataSourceTransactionManager:384 - Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] after transaction
Exception in thread "main" java.lang.RuntimeException: 异常了

我们还可以设置我们的DataSourceTransactionManager的代理方式,是采用默认的jdk还是cglib代理

<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="false"></tx:annotation-driven>

5、Spring的事务属性

5.1、什么是事务属性

事务的五大特性:

  • 隔离属性
  • 传播属性
  • 只读属性
  • 超时属性
  • 异常属性

5.2、添加事务

Spring的@Transactional注解就可以帮助我们添加事务

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    //传播属性
    Propagation propagation() default Propagation.REQUIRED;
    //隔离属性
    Isolation isolation() default Isolation.DEFAULT;
    //超时属性
    int timeout() default -1;
    //只读属性
    boolean readOnly() default false;
    //异常属性
    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};
    //异常属性
    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

6、隔离属性

隔离属性主要是为了解决并发下的问题,那么并发下会出现哪些问题呢?

脏读,幻读,不可重复读

那么出现这些问题,我们应该如何处理呢?这是时候我们就需要设置我们的隔离级别,来解决这个问题

6.1脏读

一个事务,读取到了另一个事务没有提交的数据 

时间事务A事务B
T1开始事务 
T2 开始事务
T3 查询余额(余额1000元)
T4 取出余额1000元(余额0元)
T5查询余额(余额0元) 
T6 撤销事务,回滚余额(余额100元)
T7存入500元(月500元) 
T8提交事务

 

我们可以看到,最后的结果是500元,而不是1500元。这个结果显然是错误的。造成这个问题的原因是,事务A读取到了事务B还没有提交的数据,这个就是脏读

解决脏读的办法,可以在@Transactional注解中加上@Transactional(isolation = Isolation.READ_COMMITTED),将隔离级别设置为提交读,避免脏数据的发生。

6.2不可重复读

同一个事务中,多次读取相同的数据,但是读取到的结果不一样,在本事务中产生的数据不一致的问题

时间事务A事务B
T1开始事务 
T2 开始事务
T3 查询余额(余额100元)
T4查询余额(余额1000元) 
T5 取出1000元(余额0元)
T6 提交事务
T7查询余额(余额0元) 

 事务A就查询了两次余额,其他的什么也没有干,但是两次查询出来的数据却不相同。因为事务B取出钱了后,将事务提交了,所以这个并不是脏数据,因为他无法回滚

解决不可重复读的办法,可以在@Transactional注解中加上@Transactional(isolation = Isolation.REPEATABLE_READ),将隔离级别设置为不可重复读,避免这种问题的发生。底层原理是对这行数据加上一把行级锁。保证在同一个事务中多次查询同一条数据,结果是相同的。

6.3幻读

一个事务中,多次对整表进行统计,但是结果不一样

时间事务A事务B
T1开始事务 
T2 开始事务
T3统计总存款1000元 
T4 存入100元
T5 提交事务
T6统计总存款1100元 

事务A对表中多条数据继续统计,两次的结果不一致,这是由于其他的线程提交了事务导致的。

解决幻读的办法,可以在@Transactional注解中加上@Transactional(isolation = Isolation.SERIALIZABLE),将隔离级别设置为序列化。底层的原理是使用表锁。一个线程访问,就会将表进行锁住,其他线程都无法操作,只能等到锁释放才可以操作。

6.4默认隔离级别

我们使用注解的默认隔离级别

默认的隔离级别,是根据数据库的不同而设置不同

所以,这个离级别是和数据库相关的!

小结:

  • 推荐使用spring设置的默认隔离级别,连接不同的数据库,使用的是不同的隔离级别
  • 真的遇到了并发的情况,可以采用乐观锁,乐观锁是应用层面的解决方案,如果是隔离级别,属于物理隔离,会大大影响我们的数据库效率

7、传播属性

传播属性,用来解决事务嵌套问题

事务嵌套:是指线程A中嵌套着线程B这种情况

比如:我们的service业务类中,调用了其他service业务类中的方法。而这两个方法都加上了注解事务,这就形成了事务嵌套

业务场景:TA中调用了TB和TC两个方法,TB正常执行结束;并且提交了事务,TC出现了异常,进行了回滚,但是TB已经提交了事务,无法进行回滚。这就是典型的事务嵌套,一个大事务中包含了若干个小事务,从而无法保证大事务的原子性。

8、传播属性的值与用法

传播属性的值外部不存在事务外部存在事务用法备注
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
  • 查询:显示指定默认值Supports

9、只读属性

针对于只是查询的业务逻辑,我们可以添加上只读属性,来提高效率

之前我们为了保证并发的安全性,选择了事务,底层一定会使用到各种各样的锁,只要使用锁,就会影响我们的性能。那加上了只读属性,就不会添加各种各样的锁,从而提高我们的效率

@Transactional(propagation = Propagation.MANDATORY, readOnly = true)
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(User user) {
        userDao.save(user);
        // throw new RuntimeException("异常了");
    }
}

如果不加这个注解,默认值是false 

10、超时属性

如果线程A占用了数据库行锁,线程B就需要等待线程A释放锁,那么这个等待的时间,就是超时等待

我们设置超时属性为2s,我们在执行save的时候睡眠3s

@Transactional(timeout = 2)
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(User user) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        userDao.save(user);
        // throw new RuntimeException("异常了");
    }
}

演示效果:

如果不指定这个值,spring给的默认值是-1,表示这个值最后由数据库来决定。我们一般不会去设置这个值,就用spring的默认值即可

11、异常属性

在spring的事务异常中,只要是RuntimeException异常,就会进行回滚

@Transactional(timeout = 2)
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save(User user) throws Exception {
        userDao.save(user);
        throw new Exception("eee");
    }
}

现象

我们可以看到Exception异常,直接提交了

  • 想让RuntimeException不提交,直接回滚,就这么设置 rollbackFor = {java.lang.Exception,xxx,xxx}
  • 想让Exception提交,不回滚,这么设置 noRollbackFor = {java.lang.RuntimeException,xxx,xx}

实际开发过程中,使用默认值即可

12、总结

隔离属性使用默认值

传播属性增删改使用Required(默认值),查询使用Supports

只读属性onlyread=false增删改,true为查询

超时属性默认值-1

异常属性默认值RuntimeException回滚

增删改@Transactional()

查询@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值