美文网首页我爱编程程序员
Spring的事务管理难点剖析

Spring的事务管理难点剖析

作者: 小螺钉12138 | 来源:发表于2018-05-21 23:22 被阅读0次

    1.DAO和事务管理的牵绊

    事务管理的目的是保证数据操作的事务性(原子性、一致性、隔离性、持久性,即所谓的ACID),脱离了事务性,DAO照样可以顺利地进行数据操作

    1.1.JDBC访问数据库

    文档中用到的代码项目地址

    public class UserJdbcWithoutTransManagerService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.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);
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx=new ClassPathXmlApplicationContext("jdbcWithoutTx.xml");
            UserJdbcWithoutTransManagerService userJdbcWithoutTransManagerService= (UserJdbcWithoutTransManagerService) ctx.getBean("userService");
            JdbcTemplate jdbcTemplate= (JdbcTemplate) ctx.getBean("jdbcTemplate");
            BasicDataSource dataSource= (BasicDataSource) jdbcTemplate.getDataSource();
            //检查数据源autoCommit的设置
            System.out.println("autoCommit:"+dataSource.getDefaultAutoCommit());
            //插入一条记录,初始分数为10
            jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10,"+System.currentTimeMillis()+")");
            //调用工作在无事务环境下的服务类方法,将分数添加到20分
            userJdbcWithoutTransManagerService.addScore("tom",20);
            //查看此时用户的分数
            int score=jdbcTemplate.queryForObject("select score from t_user where user_name='tom'",Integer.class);
            System.out.println("score:"+score);
            jdbcTemplate.execute("delete from t_user where user_name='tom'");
        }
    }
    
     <context:component-scan base-package="com"/>
       <context:property-placeholder
                location="classpath:jdbc.properties" ignore-unresolvable="true"/>
    
        <!--apache.commons.dbcp.BasicDataSource-->
        <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
              destroy-method="close"
              p:driverClassName="${jdbc.driverClassName}"
              p:url="${jdbc.url}"
              p:username="${jdbc.username}"
              p:password="${jdbc.password}"/>
    
        <bean id="jdbcTemplate"
              class="org.springframework.jdbc.core.JdbcTemplate"
              p:dataSource-ref="dataSource"/>
    
        <bean id="userService" class="com.servcie.UserJdbcWithoutTransManagerService"/>
    

    在jdbcWithoutTx.xml中没有配置任何事务管理器,但是数据已经成功持久化到数据库中。在默认情况下,dataSource数据源的autoCommit被设置为ture,这也意味着所有通过JdbcTemplate执行的语句马上提交,没有事务。如果将dataSource的defaultAutoCommit设置为false,再次运行UserJdbcWithoutTransManagerService,将抛出错误,原因是新增及更改数据库的操作都没有提交到数据库,所以查询语句无法从数据库中查询匹配大记录而发生异常

    对于强调读速度的应用,数据库本身可能就不支持事务,如使用MyISAM引擎的MySQL数据库。这时,无须在Spring应用中配置事务管理器,因为即使配置了,也没有用处

    1.2.Hibernate访问数据库

    对于Hibernate来说,情况就比较复杂了。因为Hibernate的事务管理拥有其自身的意义,它和Hibernate一级缓存存在密切的关系:在强调Session的save、update等方法时,Hibernate并不直接向数据库发送SQL语句,只在提交事务或flush一级缓存时才真正向数据库发送SQL。所以,即使底层数据库不支持事务,Hibernate的事务管理也是有一定好处的,不会对数据库操作的效率造成负面的影响。所以,如果使用Hiberbate数据访问技术,则没有理由不配置HibernateTransactionManager事务管理器

    但是,如果不使用Hibernate事务管理器,Spring就会采取默认的事务管理器策略搜索(PROPAGATION_REQUIRED,readOnly)。如果有修改操作是不允许的,就会抛出异常

    文档中用到的代码项目地址

    @Service("hiberService")
    public class UserHibernateWithoutTransManagerService {
        
        private HibernateTemplate hibernateTemplate;
    
        @Autowired
        public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
            this.hibernateTemplate = hibernateTemplate;
        }
    
        public void addScore(String userName,int toAdd){
            User user = hibernateTemplate.get(User.class,userName);
            user.setScore(user.getScore()+toAdd);
            //以下语句取消注释后,由于默认事务管理器不支持数据更改将报异常
            //通过Hibernate操作数据库
            hibernateTemplate.update(user);
            
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/withouttx/hiber/hiberWithoutTx.xml");
            UserHibernateWithoutTransManagerService service = (UserHibernateWithoutTransManagerService)ctx.getBean("hiberService");
    
            JdbcTemplate jdbcTemplate = (JdbcTemplate)ctx.getBean("jdbcTemplate");
            BasicDataSource basicDataSource = (BasicDataSource)jdbcTemplate.getDataSource();
    
            //①检查数据源autoCommit的设置
            System.out.println("autoCommit:"+ basicDataSource.getDefaultAutoCommit());
    
            //②插入一条记录,初始分数为10
            jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10,"+System.currentTimeMillis()+")");
    
            //③调用工作在无事务环境下的服务类方法,将分数添加20分
            service.addScore("tom",20);
            
            //④查看此时用户的分数
            int score = jdbcTemplate.queryForObject("SELECT score FROM t_user WHERE user_name ='tom'", Integer.class);
            System.out.println("score:"+score);
            jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
        }
    }
    
    <context:component-scan base-package="com.smart.withouttx.hiber"/>
        <context:property-placeholder location="classpath:jdbc.properties"/>
        <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
            destroy-method="close" 
            p:driverClassName="${jdbc.driverClassName}"
            p:url="${jdbc.url}" 
            p:username="${jdbc.username}"
            p:password="${jdbc.password}"/>
    
        <bean id="jdbcTemplate"
              class="org.springframework.jdbc.core.JdbcTemplate"
              p:dataSource-ref="dataSource"/>
    
        <bean id="sessionFactory"
              class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
              p:dataSource-ref="dataSource">
            <property name="annotatedClasses">
                <list>
                    <value>smart.User</value>
                </list>
            </property>
            <property name="hibernateProperties">
                <props>
                    <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                    <prop key="hibernate.show_sql">true</prop>
                </props>
            </property>
        </bean>
    
        <bean id="hibernateTemplate"
              class="org.springframework.orm.hibernate4.HibernateTemplate"
              p:sessionFactory-ref="sessionFactory"/>
        <bean id="userService" class="smart.withouttx.jdbc.UserJdbcWithoutTransManagerService"/>
        <bean id="hiberService" class="smart.withouttx.hiber.UserHibernateWithoutTransManagerService"/>
    

    2.应用分层的迷惑

    任何项目不一定必须按照web,service和dao三层来进行分层开发,对于一个简单的项目来说,往往只有一些对数据库增、删、改、查的操作,此时,过分强调“面向接口编程”,除了会带来更多的类文件,并不会有什么好处。

    3.事务方法嵌套调用的迷茫

    3.1.Spring事务传播机制回顾

    Spring事务的一个被讹传很广的说法是:一个事务方法不应该调用另一个事务方法,否则会产生两个事务。结果造成开发人员在设计事务方法时束手束脚

    Spring对事务控制的支持统一在TransactionDefinition类中描述,该类有以下几个重要的接口方法

    • int getPropagationBehavior():事务的传播级别
    • int getIsolationLevel():事务的隔离级别
    • int getTimeout():事务的过期时间
    • boolean isReadOnly():事务的读/写特性

    除了事务的传播行为,对于事务的其它特性,Spring是借助底层资源的功能来完成的,Spring无非是充当了一个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能,是Spring提供给开发者最珍贵的礼物。

    所谓事务传播行为,就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持以下7种事务传播行为:

    • PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务;如果已经存在一个事务,就加入到这个事务中。这是最常见的选择
    • PROPAGATION_SUPPORTS:支持当前事务。如果没有事务,就以非事务方式执行
    • PROPAGATION_MANDATORY:使用当前事务。如果没有当前事务,就抛出异常
    • PROPAGATION_REQUIRES_NEW:新建事务。如果当前存在事务,就把当前事务挂起
    • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作。如果当前存在事务,就把当前事务挂起
    • PROPAGATION_NEVER:以非事务方式执行。如果当前存在事务,就抛出异常
    • PROPAGATION_NESTED:如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与PROPAGATION_REQUIRED类似的操作

    spring默认的事务传播行为是PROPAGATION_REQUIRED,它适合绝大多数场景,如果多个Service.methodX均工作在事务环境下(均被spring事务增强),且程序中存在调用链Service1.method1()->Service2.method2()->Service3.method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中

    3.2相互嵌套的服务方法

    文档中用到的代码项目地址

    @Service("userService")
    public class UserService extends BaseService {
    
        private JdbcTemplate jdbcTemplate;
    
        private ScoreService scoreService;
    
        @Autowired
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        @Autowired
        public void setScoreService(ScoreService scoreService) {
            this.scoreService = scoreService;
        }
    
        public void logon(String userName) {
            System.out.println("before userService.updateLastLogonTime...");
            updateLastLogonTime(userName);
            System.out.println("after userService.updateLastLogonTime...");
            
            System.out.println("before scoreService.addScore...");
            scoreService.addScore(userName, 20);
            System.out.println("after scoreService.addScore...");
    
        }
    
        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/smart/nestcall/applicatonContext.xml");
            UserService service = (UserService) ctx.getBean("userService");
    
            JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
            BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();
            //插入一条记录,初始分数为10
            jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");
    
            //调用工作在无事务环境下的服务类方法,将分数添加20分
            System.out.println("before userService.logon method...");
            service.logon("tom");
            System.out.println("after userService.logon method...");
    
            jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
        }
    }
    
    
    @Service("scoreUserService")
    public class ScoreService extends BaseService{
    
        private JdbcTemplate jdbcTemplate;
    
        @Autowired
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.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);
        }
    }
    
    
    

    4.多线程的困惑

    4.1.Spring通过单实例化Bean简化多线程问题

    由于Spring的事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施(Connection实例)的,再结合Ioc和AOP实现高级声明式事务的功能,所以Spring的事务天然地和线程有着千丝万缕的联系

    一个类能够以单实例的方式运行的前提是“无状态”,即一个类不能拥有状态化的成员变量。在传统的编程中,DAO必须持有一个Connection,而Connection就是状态化的对象.所以传统的DAO不能做成单实例的,每次要用时都必须创建一个新的实例。传统的Service由于内部包含了若干有状态的DAO成员变量,所以其本身也是有状态的

    在Spring中,DAO和Service都以单实例的方式存在。Spring通过ThreadLocal将有状态的变量(如Connection等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring不遗余力的将有状态的对象无状态化,就是要达到单实例化Bean的目的

    4.2.启动独立线程调用事务方法

    文档中用到的代码项目地址

    @Service("userService")
    public class UserService extends BaseService {
        private JdbcTemplate jdbcTemplate;
        private ScoreService scoreService;
    
        @Autowired
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        @Autowired
        public void setScoreService(ScoreService scoreService) {
            this.scoreService = scoreService;
        }
    
        public void logon(String userName) {
            System.out.println("before userService.updateLastLogonTime method...");
            updateLastLogonTime(userName);
            System.out.println("after userService.updateLastLogonTime method...");
    
    //      scoreService.addScore(userName, 20);
            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() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("before scoreService.addScor method...");
                scoreService.addScore(userName, toAdd);
                System.out.println("after scoreService.addScor method...");
            }
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/multithread/applicatonContext.xml");
            UserService service = (UserService) ctx.getBean("userService");
    
            JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
            //插入一条记录,初始分数为10
            jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");
    
    
            //调用工作在无事务环境下的服务类方法,将分数添加20分
            System.out.println("before userService.logon method...");
            service.logon("tom");
            System.out.println("after userService.logon method...");
            jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
    
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    
    

    在相同线程中进行相互嵌套使用的事务方法工作在相同的事务中,如果这些相互嵌套调用的方法工作在不同的线程中,则不同线程下的事务方法工作在独立的事务中。

    5.联合军中作战的混乱

    5.1.Spring事务管理器的应对

    Spring抽象的DAO体系兼容多种数据访问技术,比如Hibernate是一个非常优秀的ORM实现方案,但对底层SQL的控制不太方便,而MyBatis则通过模板化技术让用户方便地控制SQL,但没有Hibernate那样高的开发效率;自由度最高的就是直接使用Spring JDBC了,
    但是代码是比较繁复的

    Spring提供的事务管理能力能够很好应对ORM框架和JDBC联合,由于ORM框架的会话是对后者连接的封装,Spring会“足够智能地”在同一事务线程中让前者的会话封装后者的连接。因此,只要采用前者的事务管理器就行了。

    序号 混合数据访问技术框架 事务管理器
    1 Hibernate+Spring JDBC或MyBatis org.springframework.orm.hibernateX.HibernateTransactionManager
    2 JPA+Spring JDBC或MyBatis org.springframework.orm.jpa.JpaTransactionManager
    3 JDO+Spring JDBC或MyBatis org.springframework.orm.jdo.JdoTransactionManager
    5.2.Hibernate+Spring JDBC混合框架的事务管理

    下面展示ORM框架+JDBC框架的情况
    文档中用到的代码项目地址

    @Service("userService")
    public class UserService extends BaseService {
    
        private HibernateTemplate hibernateTemplate;
        private ScoreService scoreService;
    
        @Autowired
        public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
            this.hibernateTemplate = hibernateTemplate;
        }
    
        @Autowired
        public void setScoreService(ScoreService scoreService) {
            this.scoreService = scoreService;
        }
    
        public void logon(String userName) {
            //1、通过Hibernate技术访问数据
            System.out.println("before userService.updateLastLogonTime()..");
            updateLastLogonTime(userName);
            System.out.println("end userService.updateLastLogonTime()..");
            //2、通过JDBC技术访问数据
            System.out.println("before scoreService.addScore()..");
            scoreService.addScore(userName, 20);
            System.out.println("end scoreService.addScore()..");
        }
    
        public void updateLastLogonTime(String userName) {
            User user = hibernateTemplate.get(User.class,userName);
            user.setLastLogonTime(System.currentTimeMillis());
            hibernateTemplate.update(user);
            //将hibernate一级缓存中的内容刷新到数据
            hibernateTemplate.flush();//③请看下文的分析
        }
    
         public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/mixdao/applicationContext.xml");
            UserService service = (UserService) ctx.getBean("userService");
            JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
            //插入一条记录,初始分数为10
            jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10,"+System.currentTimeMillis()+")");
    
            //调用工作在无事务环境下的服务类方法,将分数添加20分
            System.out.println("before userService.logon()..");
            service.logon("tom");
            System.out.println("after userService.logon()..");
             
            int score = jdbcTemplate.queryForObject("SELECT score FROM t_user WHERE user_name ='tom'", Integer.class);
            System.out.println("score:"+score);
            jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
        }
    }
    
     <context:component-scan base-package="com.smart.mixdao"/>
        <context:property-placeholder location="classpath:jdbc.properties"/>
        
        <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
            destroy-method="close" 
            p:driverClassName="${jdbc.driverClassName}"
            p:url="${jdbc.url}" 
            p:username="${jdbc.username}"
            p:password="${jdbc.password}"/>
    
        <bean id="jdbcTemplate"
              class="org.springframework.jdbc.core.JdbcTemplate"
              p:dataSource-ref="dataSource"/>
    
        <bean id="sessionFactory"
              class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
              p:dataSource-ref="dataSource">
            <property name="annotatedClasses">
                <list>
                    <value>com.smart.User</value>
                </list>
            </property>
            <property name="hibernateProperties">
                <props>
                    <prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
                    <prop key="hibernate.show_sql">true</prop>
                </props>
            </property>
        </bean>
    
        <bean id="hibernateTemplate"
              class="org.springframework.orm.hibernate4.HibernateTemplate"
              p:sessionFactory-ref="sessionFactory"/>
    
    <!--使用Hibernate事务管理器-->
        <bean id="hiberManager"
              class="org.springframework.orm.hibernate4.HibernateTransactionManager"
              p:sessionFactory-ref="sessionFactory"/>
    
    <!--使用UserServie及ScoreService共用方法都拥有事务-->
        <aop:config proxy-target-class="true">
            <aop:pointcut id="serviceJdbcMethod"
                          expression="within(com.smart.mixdao.BaseService+)"/>
            <aop:advisor pointcut-ref="serviceJdbcMethod"
                         advice-ref="hiberAdvice"/>
        </aop:config>
        <tx:advice id="hiberAdvice" transaction-manager="hiberManager">
            <tx:attributes>
                <tx:method name="*"/>
            </tx:attributes>
        </tx:advice>
        
    

    在1处使用Hibernate技术操作数据库,而在2处调用addScore()方法,该方法内部使用Spring JDBC技术操作数据库。3处显示调用了flush方法(),将Session中的缓存同步到数据库中(马上向数据库发送一条更新记录的SQL语句)。之所以要显示执行flush()方法,是因为在默认情况下,Hibernate对数据的更新只记录在一级缓存中,要等到事务提交后者显示调用flush()方法时才会将一级缓存中的数据同步到数据库中。

    6.特殊方法成漏网之鱼

    6.1.哪些方法补不能实施Spring AOP事务

    由于Spring事务管理是基于接口代理或动态字节码技术,通过AOP实施事务增强的。对于接口动态代理的AOP事务增强来说,由于接口的方法都必须是public的,这就要求实现类的实现方法也必须是public的,同时不能使用static修饰符。基于CGLib字节码动态代理方案是通过扩展增强类,动态创建其子类的方式进行AOP增强织入的。由于使用final、static、private修饰符的方法都不能被子类覆盖,相应地这些方法将无法实施AOP增强。

    6.2.事务增强遗漏实例

    文档中用到的代码项目地址

    @Service("userService")
    public class UserService implements UserServiceInterface{
       //1、private方法因访问权限的限制,无法被子类覆盖
        private void method1() {
            System.out.println("in method1");
        }
       //2、final方法无法被子类覆盖
        public final void method2() {
            System.out.println("in method2");
        }
       //3、static 是类级别的方法,无法被子类覆盖
        public static void method3() {
            System.out.println("in method3");
        }
       //4、public 方法可以被子类覆盖,因此可以被动态字节码增强
        public void method4() {
            System.out.println("in method4");
        }
       //5、final方法不能被子类覆盖
        public final void method5() {
            System.out.println("in method5");
        }
       //6、protected方法可以被子类覆盖,因此可以被动态字节码增强
        protected void method6(){
            System.out.println("in method6");
        }
    
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/special/applicationContext.xml");
            UserService service = (UserService) ctx.getBean("userService");
    
            System.out.println("before method1");
            service.method1();
            System.out.println("after method1");
    
            System.out.println("before method2");
            service.method2();
            System.out.println("after method2");
    
            System.out.println("before method3");
            service.method3();
            System.out.println("after method3");
    
            System.out.println("before method4");
            service.method4();
            System.out.println("after method4");
    
            System.out.println("before method5");
            service.method5();
            System.out.println("after method5");
    
            System.out.println("before method6");
            service.method6();
            System.out.println("after method6");
    
            //基于接口的动态代理
    //        UserServiceInterface service = (UserServiceInterface) ctx.getBean("userService");
    //        System.out.println("before method4");
    //        service.method4();
    //        System.out.println("after method4");
    //
    //        System.out.println("before method5");
    //        service.method5();
    //        System.out.println("after method5");
        }
    }
    
    
    
    

    这些不能被Spring事务增强的特殊方法并非就不工作在事务环境中,只要他们被外层的事务方法调用了,由于Spring事务管理的传播级别,内部方法也可以工作工作在外部方法所启动的事务上下文中。这些方法不能启动事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依旧可以顺利地传播到这些方法中。换句话说,这些无法启动事务的方法被无事务的上下文方法调用,则他们就工作在无事务的上下文中;反之,如果被有事务上下文的方法调用,则它们就工作在事务上下文中

    7、数据连接泄漏

    7.1、底层连接资源的访问问题

    获取被Spring管控的数据连接,Spring提供了两种方法:其一是使用数据资源获取工具类;其二是对数据源(或衍生品,如Hibernate的SessionFactory)进行代理

    7.2.Spring JDBC数据连接泄漏

    文档中用到的代码项目地址

    @Service("jdbcUserService")
    public class JdbcUserService {
        private JdbcTemplate jdbcTemplate;
    
        @Autowired
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        @Transactional
        public void logon(String userName) {
            try {
                Connection conn = jdbcTemplate.getDataSource().getConnection();
    //            Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
                
                String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
                jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
                Thread.sleep(1000);//②模拟程序代码的执行时间
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
    
        public static void asynchrLogon(JdbcUserService userService, String userName) {
            UserServiceRunner runner = new UserServiceRunner(userService, userName);
            runner.start();
        }
    
        public static void sleep(long time) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        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/smart/connleak/applicatonContext.xml");
            JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");
    
            BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");
            JdbcUserService.reportConn(basicDataSource);
            
            JdbcUserService.asynchrLogon(userService, "tom");
            JdbcUserService.sleep(500);
            JdbcUserService.reportConn(basicDataSource);
    
    
            JdbcUserService.sleep(2000);
            JdbcUserService.reportConn(basicDataSource);
    
    
            JdbcUserService.asynchrLogon(userService, "john");
            JdbcUserService.sleep(500);
            JdbcUserService.reportConn(basicDataSource);
    
    
            JdbcUserService.sleep(2000);
            JdbcUserService.reportConn(basicDataSource);
    
        }
    }
    
    7.3.事务环境下通过DataSourceUtils获取数据连接

    Spring 提供了一个能从当前事务上下文中获取绑定的数据连接的工具类,即DataSourceUtils。Spring强调必须使用DataSourceUtils获取数据连接,Spring的JdbcTemplate内部也是通过DataSourceUtils来获取连接的。DataSourceUtils提供了若干获取和释放数据连接的静态方法,

    • static Connection doGetConnection(DataSource dataSource):首先尝试从事务上
      下文中获取连接,失败后再从数据源获取连接
    • static Connection getConnection(DataSource dataSource):和doGetConnection()
      方法的功能一样,实际上,其内部就是通过调用doGetConnection()方法获取连接的
    • static void doReleaseConnection(Connection conn,DataSource dataSource):释
      放连接,放回连接池中
    • static void releaseConnection(Connection conn,DataSource dataSource):和doR
      eleaseConnection()方法的功能一样,实际上,其内部就是通过调用doReleaseConnection()方法获取连接的
      文档中用到的代码项目地址
    @Service("jdbcUserService")
    public class JdbcUserService {
        private JdbcTemplate jdbcTemplate;
    
        @Autowired
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
    
        @Transactional
        public void logon(String userName) {
            try {
    //            Connection conn = jdbcTemplate.getDataSource().getConnection();
                Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
                
                String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
                jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
                Thread.sleep(1000);//②模拟程序代码的执行时间
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
    
        public static void asynchrLogon(JdbcUserService userService, String userName) {
            UserServiceRunner runner = new UserServiceRunner(userService, userName);
            runner.start();
        }
    
        public static void sleep(long time) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        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/smart/connleak/applicatonContext.xml");
            JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");
    
            BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");
            JdbcUserService.reportConn(basicDataSource);
            
            JdbcUserService.asynchrLogon(userService, "tom");
            JdbcUserService.sleep(500);
            JdbcUserService.reportConn(basicDataSource);
    
    
            JdbcUserService.sleep(2000);
            JdbcUserService.reportConn(basicDataSource);
    
    
            JdbcUserService.asynchrLogon(userService, "john");
            JdbcUserService.sleep(500);
            JdbcUserService.reportConn(basicDataSource);
    
    
            JdbcUserService.sleep(2000);
            JdbcUserService.reportConn(basicDataSource);
    
        }
    }
    
    7.4.无事务环境下通过DataSourceUtils获取数据连接

    如果DataSourceUtils在没有事务上下文的方法中使用getConnection()方法连接,那么依然会造成数据连接泄漏,需要按照下面所示显示释放掉连接
    文档中用到的代码项目地址

     @Transactional
        public void logon(String userName) {
            Connection conn =null;
            try {
    //            Connection conn = jdbcTemplate.getDataSource().getConnection();
                conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
                
                String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
                jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
                Thread.sleep(1000);//②模拟程序代码的执行时间
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //显示使用DataSourceUtils释放连接
                DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());
            }
    
        }
    
    7.5.JdbcTemplate如何做到对连接泄漏的免疫

    通过分析JdbcTemplate的代码,可以清楚的看到它开放的每个数据库操作的方法:首先使用DataSourceUtils获取连接,然后在方法返回之前使用DataSourceUtils释放连接
    文档中用到的代码项目地址

    public <T> T execute(StatementCallback<T> action) throws DataAccessException {
            Assert.notNull(action, "Callback object must not be null");
            //获取连接
            Connection con = DataSourceUtils.getConnection(this.getDataSource());
            Statement stmt = null;
    
            Object var7;
            try {
                Connection conToUse = con;
                if (this.nativeJdbcExtractor != null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
                    conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
                }
    
                stmt = conToUse.createStatement();
                this.applyStatementSettings(stmt);
                Statement stmtToUse = stmt;
                if (this.nativeJdbcExtractor != null) {
                    stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
                }
    
                T result = action.doInStatement(stmtToUse);
                this.handleWarnings(stmt);
                var7 = result;
            } catch (SQLException var11) {
                JdbcUtils.closeStatement(stmt);
                stmt = null;
                DataSourceUtils.releaseConnection(con, this.getDataSource());
                con = null;
                throw this.getExceptionTranslator().translate("StatementCallback", getSql(action), var11);
            } finally {
                JdbcUtils.closeStatement(stmt);
                //显示的关闭Jdbc连接
                DataSourceUtils.releaseConnection(con, this.getDataSource());
            }
    
            return var7;
        }
    
    7.6.使用TransactionAwareDataSourceProxy

    如果不得以要显示获取数据连接,除了可以使用DataSourceUtils获取事务上下文绑定的连接,还可以通过TransactionAwareDataSourceProxy对数据源进行代理。数据源对象被代理后就具有事务上下文感知的能力,通过代理数据源的getConnection()方法获取连接和使用DataSourceUtils.getConnection()方法获取连接是一样的
    文档中用到的代码项目地址

    <context:component-scan base-package="smart.connleak"/>
        <context:property-placeholder location="classpath:jdbc.properties"/>
    
        <bean id="originDataSource" class="org.apache.commons.dbcp.BasicDataSource"
              destroy-method="close"
              p:driverClassName="${jdbc.driverClassName}"
              p:url="${jdbc.url}"
              p:username="${jdbc.username}"
              p:password="${jdbc.password}"/>
        
        <!--<bean id="dataSource"
            class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
            <property name="targetDataSource">
                <bean class="org.apache.commons.dbcp.BasicDataSource"
                      destroy-method="close"
                      p:driverClassName="${jdbc.driverClassName}"
                      p:url="${jdbc.url}"
                      p:username="${jdbc.username}"
                      p:password="${jdbc.password}"/>
            </property>
        </bean> -->   
        
        <bean id="dataSource" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"
            p:targetDataSource-ref="originDataSource"/>
    
    
        <bean id="jdbcTemplate"
              class="org.springframework.jdbc.core.JdbcTemplate"
              p:dataSource-ref="dataSource"/>
    
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
              p:dataSource-ref="dataSource"/>         
        <tx:annotation-driven/>
    

    相关文章

      网友评论

        本文标题:Spring的事务管理难点剖析

        本文链接:https://www.haomeiwen.com/subject/nthcjftx.html