暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

深入理解spring事务底层实现原理

小宇想买乌龟 2018-10-09
147
事务

相信大家都在ATM机取过钱,但是是否有人考虑过它的流程是怎样的呢?

我们都知道,假如我们取300块钱,那么当这三百块钱从ATM机出来时,我们的账户相应的会减少300。这两个过程一定是要同时成功才算成功的。否则就会出现账户少了300.但是钱没出来,对于我们来说,我们损失了300。而如果钱出来了但是账户钱没扣掉的话,银行就会损失300。这两种情况都是不可取的。所以就需要保证要么大家一起成功,有一个失败即表示这个过程失败了,就需要还原现场(钱没出来,就需要把账户扣掉的钱给补上)。这,就是事务。

事务都具有以下四个基本特点:

事务的4个属性

Atomic(原子性):事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败(减款,增款必须一起完成)。

Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。

Isolation(隔离性):事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。

Durability(持久性) :事务完成之后,它对于 系统的影响是永久的,该修改即使出现系统故障也将一直保留,真实的修改了数据库

上面说的是现实生活中的例子,那么在JAVA编程中如何保证对数据库的操作是在一个事务下呢?

首先,要让插入或更新操作在同一事务中,那么就需要保证每次需要进行数据库操作时使用的必须为同一个连接。有人可能会猜到了,用单例,是的,就是单例模式。

spring的事务的确是很强大,且做好了很多封装。如打开连接,关闭连接这种操作我们都不用做了。但是俗话说万变不离其宗,再牛的框架也是要走底层的。原理弄懂了剩下的还难么?

今天我们就来手写一个Spring的底层事务管理。


先看看工程结构。其实也就三个关键类。ConnectionHandler.java,SingleConnectHandler.java,TransactionManager.java。我待会儿会根据代码一个个讲解一下。



首先是连接ConnectionHandler.java


package spring;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

public class ConnectionHandler {
    private Map<DataSource, Connection> map = new HashMap<>();//将数据库连接保存在map中, 以DataSource为键

    public Connection getConnectionByDatabase(DataSource dataSource) throws SQLException{
        Connection conn = map.get(dataSource);
        if(conn == null) {
            conn = dataSource.getConnection();
            map.put(dataSource, conn);
        }
        return conn;
    }
    public void openConnection(DataSource dataSource) throws SQLException {
        Connection conn = map.get(dataSource);
        if(conn.isClosed()) {
            conn = dataSource.getConnection();
            map.put(dataSource, conn);
        }
    }
}

复制


我们都知道,springMVC是可以配置多个数据源的,所以这里我们直接以数据源作为map的键。连接作为值保存起来,每次获取连接时,如果map中不存在连接,则重新获取一个连接。这样就保证了在程序运行周期内,同一个数据源获取到的连接都为同一个。也就完成了我们最重要的第一步,每次进行数据库操作时,使用的都是同一个连接。
如果是在单线程模式下,其实这个类已经足够了的。spring可没那么简单,它是一个多线程的。也就是说在并发的情况下,map是线程不安全的。所以这里我们还得再封装一下。

SingleConnectHandler.java

package spring;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

public class SingleConnectHandler {
    private static ThreadLocal<ConnectionHandler> localThread = new ThreadLocal<>();

    private static ConnectionHandler getConnectionHahdler() {
        ConnectionHandler ch = localThread.get();
        if(ch == null) {
            ch = new ConnectionHandler();
            localThread.set(ch);
        }
        return ch;
    }

    public static Connection getConnection(DataSource dataSource) throws SQLException {
        return getConnectionHahdler().getConnectionByDatabase(dataSource);
    }

    public static void openConnection(DataSource dataSource) throws SQLException {
        getConnectionHahdler().openConnection(dataSource);
    }
}

复制


这个类我们需要一个ThreadLocal类来帮我们保证在多线程下。每个线程能拿到自己变量副本ConnectionHandler。也就是说并发下其实每个线程其实获取到的连接都是不一样的,ThreadLocal这个类。是jdk1.2就有的,我就不多介绍了,有兴趣的朋友可以参考一下这篇文章,我觉得说的蛮好的:
http://www.cnblogs.com/dolphin0520/p/3920407.html

这样我们的项目就可以支持多线程下操作了,完了之后是TransactionManager.java这个类。spring中这个类的功能可强大了。不过我们这个项目毕竟是阉割版的,我就只写了几个常规的方法。如开启事务,关闭事务,提交事务与回滚事务。


package spring;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

public class TransactionManager {
    private static DataSource dataSource;
    private Connection getConnection() throws SQLException {
        return SingleConnectHandler.getConnection(dataSource);
    }
    public TransactionManager(DataSource dataSource) {
        TransactionManager.dataSource = dataSource;
    }
    public void start() throws SQLException {
        Connection conn = getConnection();
        conn.setAutoCommit(false);
    }
    public void close() throws SQLException {
        Connection conn = getConnection();
        conn.close();
    }
    public void commit() throws SQLException {
        Connection conn = getConnection();
        conn.commit();
    }
    public void rollBack() throws SQLException {
        Connection conn = getConnection();
        conn.rollback();
    }
    public boolean isAutoCommit() throws SQLException {
        return getConnection().isClosed();
    }
    public void openConnection() throws SQLException{
        SingleConnectHandler.openConnection(dataSource);
    }
}

复制

这个类完成后到我们的业务层了。userService.java和userDao.java。
userService

package spring;

import java.sql.SQLException;

public interface UserService {
    public void buy() throws SQLException;
    public void addShops() throws SQLException;
}

复制

userDao

package spring;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;

import javax.sql.DataSource;

public class UserDao implements UserService{
    private String sql = "insert into user(account,password) values(?,?)";
    private DataSource dataSource;
    public UserDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void buy() throws SQLException {
        Connection conn = SingleConnectHandler.getConnection(dataSource);
        PreparedStatement ps = conn.prepareStatement(sql);
        ps.setString(1, Thread.currentThread().getName()+"buy");
        ps.setString(2new Date().toString());
        ps.execute();
        System.out.println("方法buy,---当前线程:"+Thread.currentThread()+"-------使用的Connection:"+conn.hashCode());
    }

    @Override
    public void addShops() throws SQLException {
        Connection conn = SingleConnectHandler.getConnection(dataSource);
        PreparedStatement ps = conn.prepareStatement(sql);
        ps.setString(1, Thread.currentThread().getName()+"addShops");
        ps.setString(2new Date().toString());
        ps.execute();
        System.out.println("方法addShops,当前线程:"+Thread.currentThread()+"----使用的Connection:"+conn.hashCode());
    }
}

复制


业务层我模拟了用户添加商品与购买商品操作。我们将这两个数据库操作放在同一个事务中。注意我后面打印的当前线程与连接的hash值。我们都知道,在程序运行周期内,每个对象都有自己唯一的hash。如果两个hash值相等的话,那么这两个对象则相等。
哦,对了,差点忘了我们还有一个自定义的MyDatasource.java


package spring;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

import javax.sql.DataSource;

public class MyDatasource implements DataSource{
    public static final String driverClassName = "com.mysql.jdbc.Driver";
    public static final String password = "root";
    public static final String username = "root";
    public static final String url = "jdbc:mysql://localhost:3306/qq";

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        // TODO Auto-generated method stub

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        // TODO Auto-generated method stub

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public <T> unwrap(Class<T> iface) throws SQLException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Connection getConnection() throws SQLException {
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return conn;
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        // TODO Auto-generated method stub
        return null;
    }

}

复制

这个类实现了DataSource接口,里面的方法我们就直接重写一个getConnection就好了。这里面是JDBC获取数据库连接,都是格式固定的,没什么要讲的。

完成后进入测试:

package spring;

import java.sql.SQLException;

public class Test {
    public static void main(String[] args) throws SQLException, InterruptedException {
        MyDatasource b = new MyDatasource();
        for(int i=0;i<3;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    UserService u = new UserDao(b);
                    TransactionManager t = new TransactionManager(b);
                    try {
                        t.start();
                        u.buy();
                        u.addShops();
                        t.commit();
                        t.close();
                    } catch (Exception e) {
                        try {
                            t.rollBack();
                        } catch (SQLException e1) {
                            e1.printStackTrace();
                        }
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

复制


这个类我新建了三个线程。表示三个用户同时执行添加商品与购买操作。完成后直接run。看结果



总结:

可以看到每个线程的connection都不一样,但是单个线程下使用的connection,包括TransactionManager中的connection都是一致的。这就是一个小型的spring事务管理,其实你翻开spring的源码,基本和这个差不多的。只不过是spring中进行了更多的封装。但是其实有很多是我们不需要的。只有了解了原理,当项目出现问题或者性能瓶颈时,才能更好的发现并解决问题。



文章转载自小宇想买乌龟,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论