spring事务控制的原理解析1

一、概述

事务的本质是依赖数据库的支持来实现事务的控制,也就是java应用本身不支持事务,事务是数据库实现的。

简单来说,客户端通过connection和数据库建立连接,并通过connection来执行sql,所以只要是通过同一个connection来执行的sql,数据库就可以控制这些sql同时回滚或者同时提交,也就是事务的原子性。

一、jdbc的事务控制

上边提到事务控制的本质是通过connection来实现的,所以jdbc操作数据库时,只要保证多条sql语句是通过同一个connection来执行的,就可以通过这个connection来提交或者回滚事务。

1.1 简单代码示意

先来看一段简单的示例代码

public static void main(String[] args) throws SQLException {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/transaction_test");

        //获取连接
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        //zs给ls转账的sql
        PreparedStatement sql1 = connection.prepareStatement("UPDATE t_bank SET money=money-100 WHERE ACCOUNT='zs'");
        PreparedStatement sql2 = connection.prepareStatement("UPDATE t_bank SET money=money+100 WHERE ACCOUNT='ls'");
        try {
            //执行sql
            sql1.executeUpdate();
            int i=1/0;
            sql2.executeUpdate();
            //提交事务
            connection.commit();
        } catch (SQLException e) {
            e.printStackTrace();
            //回滚事务
            connection.rollback();
        }

    }

这段代码中先用dataSource获取了一个数据库连接,再利用这个connection来执行两条sql,这俩sql就可以通过这个connection来进行事务控制。

1.2 web应用三层架构中的使用

如果按web应用的三层架构来写,并整合spring,一般可能会这样写。

先有一个BankService,在其中注入两个dao,在transfer方法中调用这两个dao的方法完成转账测试。

@Service
public class BankService {

    @Autowired
    private IncrementDao incrementDao;
    
    @Autowired
    private DecrementDao decrementDao;
    
    public void transfer() throws SQLException {
        incrementDao.increment();
        int i=1/0;
        decrementDao.decrement();
    }

}

然后分别给出两个dao的代码

@Repository
public class IncrementDao {

    @Autowired
    DataSource dataSource;

    public void increment() throws SQLException {
        //获取连接
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        PreparedStatement sql1 = connection.prepareStatement("UPDATE t_bank SET money=money-100 WHERE ACCOUNT='zs'");
        //执行sql
        sql1.executeUpdate();
        //提交事务
        connection.commit();
    }
}

@Repository
public class DecrementDao {

    @Autowired
    private DataSource dataSource;

    public void decrement() throws SQLException {
        //获取连接
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        //zs给ls转账的sql
        PreparedStatement sql2 = connection.prepareStatement("UPDATE t_bank SET money=money+100 WHERE ACCOUNT='ls'");
        sql2.executeUpdate();
        connection.commit();
    }
}

那么这样的写法能控制住事务吗?回想下上边的内容,只有通过同一个connection执行的sql才能被事务控制住,

按上边这样写两个sql是通过两个connection执行的,所以是控制不住事务的。

如果要控制这个transfer方法的事务就要让两个dao中使用同一个connection,当然首先想到的是在service层创建connection然后通过方法参数传到dao层这样就是同一个了,但这种方式也许不够优雅,我们可以尝试通过ThreadLocal来在同一个线程范围内传递connection这样就不用通过方法参数传递了。

我们可以创建一个 MyTransactionSourceManager类来保存ThreadLocal变量

public class MyTransactionSourceManager {

    private static ThreadLocal<Map<Object,Object>> resources = new ThreadLocal<>();
    
    static {
        resources.set(new HashMap<>());
    }

    public static Object getResource(Object key){
        Map<Object, Object> map = resources.get();
        return map.get(key);
    }

    public static void registerRsource(Object key,Object value){
        Map<Object, Object> map = resources.get();
        map.put(key,value);
    }
}

这里的resources属性用来在线程范围内共享变量,内部放一个map是为了能放置多个需要共享的变量,并且提供了获取值和注册值的方法。

现在我们可以在service中先获取数据库连接,然后注册到resources中,在dao层方法中再从resources中取出connection来执行sql,然后在service方法的最后再从resources中取出connection来提交事务或者回滚事务。

下面是经过改进的service

@Service
public class BankService {

    @Autowired
    private IncrementDao incrementDao;

    @Autowired
    private DecrementDao decrementDao;

    @Autowired
    private DataSource dataSource;

    public void transfer() throws SQLException {
        //先获取数据库连接,并放到MyTransactionSourceManager.resources
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        MyTransactionSourceManager.registerRsource(dataSource,connection);
        try {
            incrementDao.increment();
            int i=1/0;
            decrementDao.decrement();
            //取出连接提交事务
            Connection resource = (Connection)MyTransactionSourceManager.getResource(dataSource);
            resource.commit();
        } catch (Exception e) {
            //回滚事务
            Connection resource = (Connection)MyTransactionSourceManager.getResource(dataSource);
            resource.rollback();
            e.printStackTrace();
        }
    }

}

那么dao层也要改进,从MyTransactionSourceManager中获取数据库连接,并且不在dao层提交事务

@Repository
public class IncrementDao {

    @Autowired
    DataSource dataSource;

    public void increment() throws SQLException {
        //获取连接
        Connection connection = (Connection)MyTransactionSourceManager.getResource(dataSource);
        PreparedStatement sql1 = connection.prepareStatement("UPDATE t_bank SET money=money-100 WHERE ACCOUNT='zs'");
        //执行sql
        sql1.executeUpdate();
    }
}

@Repository
public class DecrementDao {

    @Autowired
    private DataSource dataSource;

    public void decrement() throws SQLException {
        //获取连接
        Connection connection = (Connection) MyTransactionSourceManager.getResource(dataSource);
        //zs给ls转账的sql
        PreparedStatement sql2 = connection.prepareStatement("UPDATE t_bank SET money=money+100 WHERE ACCOUNT='ls'");
        sql2.executeUpdate();
    }
}

这样就满足了service方法中的多个dao使用同一个数据库连接来执行sql,所以它的事务是能够被控制的。

1.3 使用aop进行改进

上面这种方式实现了在spring+三层架构中进行事务控制,关键点是保证最终执行sql时使用的是同一个connection。

但这种方式还是不够优雅,我们先关注下service层的代码,事务控制跟获取数据库连接的代码和业务代码柔和在一起,这种状况不利于代码的维护。所以我们可以使用aop来把这部分跟业务不相关的代码抽取出来。

首先我们抽取出一个通知类,注意上边要加@Aspect注解,然后把它配置到spring容器中,可以在此类上加@Component注解,但这样要注意扫描路径要包含这个类,也可以在配置类中用@Bean来添加


@Aspect
public class TransactionAspect {

    @Autowired
    private DataSource dataSource;

    //定义切入点表达式
    @Pointcut("@annotation(com.lyy.transaction_source.config.MyTransaction)")
    public void pt1(){

    }

    //通过环绕通知控制事务
    @Around("pt1()")
    public Object transactionControl(ProceedingJoinPoint point) throws SQLException {
        Object returnValue=null;
        //先获取数据库连接,并放到MyTransactionSourceManager.resources
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        MyTransactionSourceManager.registerRsource(dataSource,connection);
        try {
            System.out.println("代理类执行了");
            Object[] args = point.getArgs();
            point.proceed(args);
            //提交事务
            connection.commit();
        } catch (Throwable e) {
            e.printStackTrace();
            connection.rollback();
            System.out.println("--回滚事务--");
        }
        return returnValue;
    }
}

注意上面的切入点表达式切的是自定义注解,所以我们创建一个自定义注解

//自定义的事务控制的标识注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTransaction {
}

在配置类上开启spring对注解aop的支持,并且配置这个切面类为bean

@Configuration
@ComponentScan(basePackages = {"com.lyy.transaction_source.service","com.lyy.transaction_source.dao"})
@EnableAspectJAutoProxy//开启注解aop
public class SpringConfig {

    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/transaction_test");

        return dataSource;
    }

    //配置事务控制切面类为spring的bean
    @Bean
    public TransactionAspect transactionAspect(){
        return new TransactionAspect();
    

在service层方法上添加上边的自定义注解,删掉事务控制的代码

@MyTransaction
    public void transfer() throws SQLException {
            incrementDao.increment();
            int i=1/0;
            decrementDao.decrement();
    }

然后就会发现这样处理也是可以控制住事务的,这样是不是就优雅多了。

关于spring中是如何实现的,我们下一篇在详细分析