• Spring单实例、多线程安全、事务解析


    引言:

        在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的:
        DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。
        上述观点对了一半。对的是“每个DAO都要包含一个不同的Connection对象实例”这句话,错的是“DAO对象就不能是单实例”。
        其实Spring在实现Service和DAO对象时,使用了ThreadLocal这个类,这个是一切的核心! 如果你不知道什么事ThreadLocal,请看 深入研究java.lang.ThreadLocal类》 :。请放心,这个类很简单的。
        要弄明白这一切,又得明白事务管理在Spring中是怎么工作的,所以本文就对Spring中多线程、事务的问题进行解析。

    Spring使用ThreadLocal解决线程安全问题:

        Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。 
        参考下面代码,这个是《Spring3.x企业应用开发实战中的例子》,本文后面也会多次用到该书中例子(有修改)。
    public class SqlConnection {
        //①使用ThreadLocal保存Connection变量
        privatestatic ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();
        publicstatic Connection getConnection() {
           // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
           // 并将其保存到线程本地变量中。
           if (connThreadLocal.get() == null) {
               Connection conn = getConnection();
               connThreadLocal.set(conn);
               return conn;
           } else {
               return connThreadLocal.get();
               // ③直接返回线程本地变量
           }
        }
        public voidaddTopic() {
           // ④从ThreadLocal中获取线程对应的Connection
           try {
               Statement stat = getConnection().createStatement();
           } catch (SQLException e) {
               e.printStackTrace();
           }
        }
    }
        这个是例子展示了不同线程使用TopicDao时如何使得每个线程都获得不同的Connection实例副本,同时保持TopicDao本身是单实例。

    事务管理器:

        事务管理器用于管理各个事务方法,它产生一个事务管理上下文。下文以SpringJDBC的事务管理器DataSourceTransactionManager类为例子。
        我们知道数据库连接Connection在不同线程中是不能共享的,事务管理器为不同的事务线程利用ThreadLocal类提供独立的Connection副本。事实上,它将Service和Dao中所有线程不安全的变量都提取出来单独放在一个地方,并用ThreadLocal替换。而多线程可以共享的部分则以单实例方式存在。

    事务传播行为:

        当我们调用Service的某个事务方法时,如果该方法内部又调用其它Service的事务方法,则会出现事务的嵌套。Spring定义了一套事务传播行为,请参考。这里我们假定都用的REQUIRED这个类型:如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到的当前事务。参考下面例子(代码不完整):
    @Service( "userService")
    public class UserService extends BaseService {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Autowired
        private ScoreService scoreService;
       
        public void logon(String userName) {
            updateLastLogonTime(userName);       
            scoreService.addScore(userName, 20);
        }
    
        public void updateLastLogonTime(String userName) {
            String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
            jdbcTemplate.update(sql, System. currentTimeMillis(), userName);
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/nestcall/applicatonContext.xml" );
            UserService service = (UserService) ctx.getBean("userService" );
            service.logon( "tom");
    
        }
    }
    
    @Service( "scoreUserService" )
    public class ScoreService extends BaseService{
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public void addScore(String userName, int toAdd) {
            String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
            jdbcTemplate.update(sql, toAdd, userName);
        }
    }
        同时,在配置文件中指定UserService、ScoreService中的所有方法都开启事务。
        上述例子中UserService.logon()执行开始时Spring创建一个新事务,UserService.updateLastLogonTime()和ScoreService.addScore()会加入这个事务中,好像所有的代码都“直接合并”了!

    多线程中事务传播的困惑:

        还是上面那个例子,加入现在我在UserService.logon()方法中手动新开一个线程,然后在新开的线程中执行ScoreService.add()方法,此时事务传播行为会怎么样?飞线程安全的变量,比如Connection会怎样?改动之后的UserService 代码大体是:
    @Service( "userService")
    public class UserService extends BaseService {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Autowired
        private ScoreService scoreService;
    
        public void logon(String userName) {
            updateLastLogonTime(userName);
            Thread myThread = new MyThread(this.scoreService , userName, 20);//使用一个新线程运行
            myThread .start();
        }
    
        public void updateLastLogonTime(String userName) {
            String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
            jdbcTemplate.update(sql, System. currentTimeMillis(), userName);
        }
    
        private class MyThread extends Thread {
            private ScoreService scoreService;
            private String userName;
            private int toAdd;
            private MyThread(ScoreService scoreService, String userName, int toAdd) {
                this. scoreService = scoreService;
                this. userName = userName;
                this. toAdd = toAdd;
            }
    
            public void run() {
                scoreService.addScore( userName, toAdd);
            }
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/multithread/applicatonContext.xml" );
            UserService service = (UserService) ctx.getBean("userService" );
            service.logon( "tom");
           }
    }
        这个例子中,MyThread会新开一个事务,于是UserService.logon()和UserService.updateLastLogonTime()会在一个事务中,而ScoreService.addScore()在另一个事务中,需要注意的是这两个事务都被事务管理器放在事务上下文中。
        结论是:在事务属性为REQUIRED时,在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果互相嵌套调用的事务方法工作在不同线程中,则不同线程下的事务方法工作在独立的事务中。

    底层数据库连接Connection访问问题

        程序只要使用SpringDAO模板,例如JdbcTemplate进行数据访问,一定没有数据库连接泄露问题!如果程序中显式的获取了数据连接Connection,则需要手工关闭它,否则就会泄露!
        当Spring事务方法运行时,事务会放在事务上下文中,这个事务上下文在本事务执行线程中对同一个数据源绑定了唯一一个数据连接,所有被该事务的上下文传播的放发都共享这个数据连接。这一切都在Spring控制下,不会产生泄露。Spring提供了数据资源获取工具类DataSourceUtils来获取这个数据连接.
    @Service( "jdbcUserService" )
    public class JdbcUserService {
        @Autowired
        private JdbcTemplate jdbcTemplate;
       
        @Transactional
        public void logon(String userName) {
            try {
                Connection conn = jdbcTemplate.getDataSource().getConnection();           
                String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
                jdbcTemplate.update(sql, System. currentTimeMillis(), userName);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
        public static void asynchrLogon(JdbcUserService userService, String userName) {
            UserServiceRunner runner = new UserServiceRunner(userService, userName);
            runner.start();
        }
    
        public static void reportConn(BasicDataSource basicDataSource) {
            System. out.println( "连接数[active:idle]-[" +
                           basicDataSource.getNumActive()+":" +basicDataSource.getNumIdle()+ "]");
        }
    
        private static class UserServiceRunner extends Thread {
            private JdbcUserService userService;
            private String userName;
    
            public UserServiceRunner(JdbcUserService userService, String userName) {
                this. userService = userService;
                this. userName = userName;
            }
    
            public void run() {
                userService.logon( userName);
            }
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/connleak/applicatonContext.xml" );
            JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService" );
            JdbcUserService. asynchrLogon(userService, "tom");
        }
    }
        在这个例子中,main线程拿到一个UserService实例,获取一个Connection的副本,它会被Spring管理,不会泄露。UserServiceRunner 线程手动从数据源拿了一个Connection但没有关闭因此会泄露。
        如果希望使UserServiceRunner能拿到UserService中那个Connection们就要使用DataSourceUtils类,DataSourceUtils.getConnection()方法会首先查看当前是否存在事务管理上下文,如果存在就尝试从事务管理上下文拿连接,如果获取失败,直接从数据源中拿。在获取连接后,如果存在事务管理上下文则把连接绑定上去。
        实际上,上面的代码只用改动一行,把login()方法中获取连接那行改成就可以做到:
    Connection conn = DataSourceUtils.getConnection( jdbcTemplate .getDataSource());    
       需要注意的是:如果DataSourceUtils在没有事务上下文的方法中使用getConnection()获取连接,依然要手动管理这个连接!
        此外,开启了事务的方法要在整个事务方法结束后才释放事务上下文绑定的Connection连接,而没有开启事务的方法在调用完Spring的Dao模板方法后立刻释放。

    多线程一定要与事务挂钩么?

        不是!即便没有开启事务,利用ThreadLocal机制也能保证线程安全,Dao照样可以操作数据。但是事务和多线程确实纠缠不清,上文已经分析了在多线程下事务传播行为、事务对Connection获取的影响。

    结论:

    • Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。 
    • 在事务属性为REQUIRED时,在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果互相嵌套调用的事务方法工作在不同线程中,则不同线程下的事务方法工作在独立的事务中。
    • 程序只要使用SpringDAO模板,例如JdbcTemplate进行数据访问,一定没有数据库连接泄露问题!如果程序中显式的获取了数据连接Connection,则需要手工关闭它,否则就会泄露!
    • 当Spring事务方法运行时,就产生一个事务上下文,它在本事务执行线程中对同一个数据源绑定了一个唯一的数据连接,所有被该事务上下文传播的方法都共享这个连接。要获取这个连接,如要使用Spirng的资源获取工具类DataSourceUtils。
    • 事务管理上下文就好比一个盒子,所有的事务都放在里面。如果在某个事务方法中开启一个新线程,新线程中执行另一个事务方法,则由上面第二条可知这两个方法运行于两个独立的事务中,但是:如果使用DataSourcesUtils,则新线程中的方法可以从事务上下文中获取原线程中的数据连接!






















        
  • 相关阅读:
    [剑指offer] 7. 斐波那契数列
    [剑指offer] 6. 旋转数组的最小数字
    [剑指offer] 5. 用两个栈实现队列
    [剑指offer] 4. 重建二叉树
    [剑指offer] 3. 从头到尾打印链表
    vue.js从输入中的contenteditable元素获取innerhtml
    CSS3 ------- object-fit属性
    mouseenter和mouseover区别
    元素scroll系列属性
    淘宝flexible.js源码分析
  • 原文地址:https://www.cnblogs.com/snake-hand/p/3161414.html
Copyright © 2020-2023  润新知