精华内容
下载资源
问答
  • redis存后取出却为空(null)

    万次阅读 2019-05-20 11:12:36
    redis存后取出却为空(null) bug分享: 当大家使用redis存的时候,如果出现键值发生如下变化: 存储键值:123456789 但是使用123456789键值取值的时候结果null 经查询redis缓存发现,实际查询存储键值: ...

    redis存值后取出却为空(null)
    bug分享:
    当大家使用redis存值的时候,如果出现键值发生如下变化:
    存储键值为:123456789
    但是使用123456789为键值取值的时候结果为null
    经查询redis缓存发现,实际查询存储键值: “\xac\xed\x00\x05t\x00\x0b123456789”

    遇到以上情况的时候,请记得确认RedisTemplate的注入对象是什么类型的,当存值的位置注入的类型和取值注入的类型不一致的时候会导致键不一致,结果无法取出!
    以上经验,仅供参考,redis出现问题,优先查看影响范围内注入的RedisTemplate对象是否一致!

    展开全文
  • 而我们获取数据源的元信息时,是在Connection连接通道之上,借助于java.sql.DatabaseMetaData进行获取详细信息(比如TABLE_NAME, REMARKS, COLUMN_NAME等等),然而却出现了除REMARKS之外的其...

    大数据平台存在这样一种场景,我们需要根据用户录入的配置信息,进行同步数据源/表相关的信息,此时就需要把数据库、数据表、数据表字段相关信息进行拉取。而我们获取数据源的元信息时,是在Connection连接通道之上,借助于java.sql.DatabaseMetaData进行获取详细信息(比如TABLE_NAME, REMARKS, COLUMN_NAME等等),然而却出现了除REMARKS之外的其他信息都可以正常同步,唯有REMARKS一直为空。

    通过查看相关资料得知,要想获取源数据库表的remarks信息,需要在构建Connection指定两个相关属性:

            Properties connectionProps = new Properties();
            // ...
            /**
             * If load remarks of table using java.sql.DatabaseMetaData, need to set these parameters.
             */
            connectionProps.put("remarks", "true");
            connectionProps.put("useInformationSchema", "true");
            Connection conn = DriverManager.getConnection(url, connectionProps);
    

    如此,问题得以解决。

    【参考文献】
    [1] mysql DatabaseMetaData 获取table remarks为空的解决办法
    [2] DatabaseMetaData的用法

    展开全文
  • //注意这个日志,默认是打印不出来的,程序内部会去判定logWriter是否为空,若为空则不会输出,默认也没有 println("DriverManager.getConnection(\"" + url + "\")"); if (!initialized) {//初始化,不过...

    简单说下,本文是说源码的,但是不会一篇文章就说得很深入,本文是【jdbc源码入口篇】,分别会说明一些源码和使用细节,所提及的源码可能相对于jdbc的源码还是初级看源码,看个大概,细节上还有很多东西,后续有时间会跟进;


    文章会以oracle、mysql jdbc的实现的源码作为说明的依据来参考;

    首先,我们要创建一个链接(连接池是在内部做的),会操作:

    Class.forName("xxx.xxx.xxxx.xxx");//类名通常为jdbc的Dirver;

    oracle的一般是:

    oracle.jdbc.driver.OracleDriver

    而mysql通常是:

    com.mysql.jdbc.Driver

    sql server通常是(本人很少使用SQL Server,所以文章中不会出现SQL server的Driver细节):

    com.microsoft.sqlserver.jdbc.SQLServerDriver

    然后我们才能用:

    DriverManager.getConnection()

    方法来获取链接;我们知道这个是获取一个Cnnection,那么我们首先来看看DriverManager的getConnection到底做了什么?为什么必须要Class.forName才行;

    跟踪DriverManager类进去发现有四个getConnection方法:

    1、所有参数全部通过URL传递过去

    getConnection(String url);

    2、用户名和密码单独传递:

    getConnection(String url , String user , String password);

    3、传递多个参数的K-V结构

    getConnection(String url , Properties properties);

    4、传递多个参数后,还加上指定的ClassLoader,很少用,当跨ClassLoader访问的时候需要使用到,默认使用当前线程的ClassLoader;

    getConnection(String url , Properties properties , ClassLoader callerCL);

    其实无论如何,会调用到最后一个方法,最后一个方法主要代码为:

    private static Connection getConnection(
          String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
    
          java.util.Vector drivers = null;
          synchronized(DriverManager.class) {
              if(callerCL == null) {
                    callerCL = Thread.currentThread().getContextClassLoader();//如果没传递CLassLoader用当前线程的
              }    
          } 
     
          if(url == null) {
               throw new SQLException("The url cannot be null", "08001");//没有传递URL,则抛出异常
          }
    
          //注意这个日志,默认是打印不出来的,程序内部会去判定logWriter是否为空,若为空则不会输出,默认也没有值
          println("DriverManager.getConnection(\"" + url + "\")");
        
          if (!initialized) {//初始化,不过几乎可以忽略
            initialize();
          }
    
    
          synchronized (DriverManager.class){ 
              drivers = readDrivers; // readDrivers为一个类变量,下面的部分我们看看是如何被注册的
          }
    
          SQLException reason = null;
          for (int i = 0; i < drivers.size(); i++) {//循环扫描所有的Drivers
                DriverInfo di = (DriverInfo)drivers.elementAt(i);
                if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {//如果类和类的ClassLoader不匹配,跳过
                   println("    skipping: " + di);
                   continue;
                }
                try {
                     println("    trying " + di);
                     Connection result = di.driver.connect(url, info);//调用对应Driver的connect方法,得到Connection对象
                     if (result != null) {//如果得到了,则返回connection
                          // Success!
                          println("getConnection returning " + di);
                          return (result);
                     }
               } catch (SQLException ex) {
                     if (reason == null) {
                        reason = ex;
                     }
              }
         }
        
         // if we got here nobody could connect.
         if (reason != null)    {
              println("getConnection failed: " + reason);
              throw reason;
         }
        
          println("getConnection: no suitable driver found for "+ url);
          throw new SQLException("No suitable driver found for "+ url, "08001");//没有获取到链接会抛出一个SQL Exception
    }



    先不用看其他的代码,我们首先可以看出一点DriverManager中注册不止一个Driver驱动,如果一些系统中有多个驱动的时候,然后它循环扫描所有的驱动程序,然后通过connect方法获取是否来调用,这个connect方法每次去循环扫描,是不是会造成一些不必要的开销;为此,我们想看看里面到底有那些Driver,以及得到对应的Driver,输出Driver信息:

    我们可以通过以下方式输出循环了那些内容:

    方法1:

    Enumeration<Driver> enumeration = DriverManager.getDrivers();

    然后遍历这个迭代器,就可以得到这些Driver相关信息,可以自己根据实际情况作其他的操作也可以,可以看出Driver就是一个普通java类的实例,只是他有一些基于标准规范,类似于驱动链接的功能而已;

    方法2:

    设置将Log输出,将数据输出到控制台,我们这里简单输出到控制台:

    DriverManager.setLogWriter(new PrintWriter(System.out));

    运行这个getConnection方法、getDriver等方法的时候,就会打印出相应的日志信息在控制台上面;

    例如运行getConnection方法:

    DriverManager.getConnection("jdbc:oracle:thin:@10.20.149.82:1521:fuck")
        trying driver[className=sun.jdbc.odbc.JdbcOdbcDriver,sun.jdbc.odbc.JdbcOdbcDriver@14b9a74]
    *Driver.connect (jdbc:oracle:thin:@10.20.149.82:1521:fuck)
        trying driver[className=com.mysql.jdbc.Driver,com.mysql.jdbc.Driver@1779e93]
        trying driver[className=oracle.jdbc.OracleDriver,oracle.jdbc.OracleDriver@1871e77]
    getConnection returning driver[className=oracle.jdbc.OracleDriver,oracle.jdbc.OracleDriver@1871e77]



    其实DirverManager里面还有一个方法是:

    getDriver(String url)方法,其实更加确切的意思应该叫findDriverByUrl,入口参数为url,也就是jdbc url,它是负责匹配url是不是这个dirver可以接受的,每个driver都需要实现一个方法叫:acceptsURL来返回,当前这个Dirver是否可以接受这个URL;而acceptsURL方法是各个厂商提供的驱动程序自己编写的,也就是自己编写这个方法说明我是否支持这个URL,类似Oracle、Mysql、sql server等等数据库都会有不同的jdbc驱动包,所以他们自然就区分开了;而对应到类里面,就是前面在Class.forName("xxx")所对应的类。


    getConection方法中,会不断尝试的connect方法中,传入的URL也将会被先判定,然后再执行,jdbc通常认为connect方法本身需要判定一次,就不需要再调用acceptsURL判定一次再调用connection方法了,判定成功就直接返回connection,否则就返回null;只是有个问题是,如果有很多Driver,这里需要逐个遍历,所以文章后面我们建议是将自己的Driver保存起来;


    接下来看看每个Driver的connect方法的细节,因为他是负责返回connection的,不过我们可以先看看其中调用解析URL是在哪里调用的,如下:


    对于oracle的connect方法相关部分的源码为(OracleDirver类中):




    mysq相关的源码为(为com.mysql.jdbc.Dirver(你注册的MySQL驱动)类的父类的:NonRegisteringDriver(同一包)中):




    注意parseURL方法就是acceptURL方法调用的下一个目标,内部被隐藏了,例如Mysql jdbc的源码中:

    public boolean acceptsURL(String url) throws SQLException {
       return (parseURL(url, null) != null);
    }


    我们这里分别来说下parseURL的一些细节:

    OracleDriver中:


    private Hashtable parseUrl(String s)
            throws SQLException
        {
            Hashtable hashtable = new Hashtable(5);
            int i = s.indexOf(':', s.indexOf(':') + 1) + 1;//第二个冒号(注意里面还有一个indexOf,所以是第二个)
            int j = s.length();
            if(i == j)
                return hashtable;
            int k = s.indexOf(':', i);//第三个冒号
            if(k == -1)
                return hashtable;
            hashtable.put("protocol", s.substring(i, k));//第二个冒号和第三个冒号之间的认为是协议,比如thin或oci等等
            int l = k + 1;
            int i1 = s.indexOf('/', l);//解析反斜杠
            int j1 = s.indexOf('@', l);//解析@符号的位置
            if(j1 > l && l > i && i1 == -1)//如果既没有反斜杠也没有@则返回null,解析失败
                return null;
            if(j1 == -1)
                j1 = j;
            if(i1 == -1)
                i1 = j1;
            if(i1 < j1)
            {//如果有反斜杠(在@前面),则认为在jdbc URL上传递了用户名和密码,当然可以不传递
                hashtable.put("user", s.substring(l, i1));
                hashtable.put("password", s.substring(i1 + 1, j1));
            }
            if(j1 < j)//如果传递了数据库信息,则将数据库连接串放进去,可以是IP:PORT:SID、describe、TNS等多种格式
                hashtable.put("database", s.substring(j1 + 1));
            return hashtable;
        }



    它将jdbc url解析为一个Hash table,并且将协议、db信息解析出来,并且可以放用户名和密码,这样估计很少有人在oracle jdbc url上方用户名和密码,但是的确是可行的,例如你可以这样写你的oracle jdbc:

    jdbc:oracle:thin:<user>/<passowrd>@ip:port:sid

    然后在DriverManager.getConnection的参数时候就【无需传入用户名和密码】了;

    通常的写法更多是:

    jdbc:oracle:thin:@ip:port:sid

    然后在getConnection的时候,带上用户名和密码

    其实MYSQL也是这样,URL上可以传递,只是Oracle在URL上最多就加这些了,不能再加其他的了,而MySQL很麻烦的就在解析URL上面;


    MySQL的解析部分:

    mysql的解析部分比较复杂,以为内mysql大部分参数都可以通过URL来设置,所以解析很复杂,这里就简单列举部分代码:


    public Properties parseURL(String url, Properties defaults)
        throws java.sql.SQLException {
            Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties();
            
            if (url == null) {//主要看看参数是不是空的
               return null;
            }
    
    
            //这几个常量就是mysql jdbc 的前缀特征,如果一个都不符合,则返回null,标示解析失败
            //REPLICATION_URL_PREFIX "jdbc:mysql:replication://"
            //URL_PREFIX   "jdbc:mysql://"
            //MXJ_URL_PREFIX  "jdbc:mysql:mxj://";
            //LOADBALANCE_URL_PREFIX "jdbc:mysql:loadbalance://"; 集群下使用
            if (!StringUtils.startsWithIgnoreCase(url ,URL_PREFIX) 
                  && !StringUtils.startsWithIgnoreCase(url ,MXJ_URL_PREFIX)
                  && !StringUtils.startsWithIgnoreCase(url , LOADBALANCE_URL_PREFIX)
                  && !StringUtils.startsWithIgnoreCase(url , REPLICATION_URL_PREFIX)) { //$NON-NLS-1$
                 return null;
           }
    
           int beginningOfSlashes = url.indexOf("//");//分析content部分,后面将会是IP、PORT、库名以及扩展串
    
           if (StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)) {
                urlProps.setProperty("socketFactory" , "com.mysql.management.driverlaunched.ServerLauncherSocketFactory");
           }
           int index = url.indexOf("?"); //参数分隔符号
           if (index != -1) {//如果带有非默认参数,则循环
               String paramString = url.substring(index + 1, url.length());
               url = url.substring(0, index);
    
    
               StringTokenizer queryParams = new StringTokenizer(paramString, "&");//拆分每个参数,放在迭代器上
    
    
               while (queryParams.hasMoreTokens()) {//迭代每个参数
                   String parameterValuePair = queryParams.nextToken();
                   int indexOfEquals = StringUtils.indexOfIgnoreCase(0 , parameterValuePair, "=");//拆分K-V,即参数和值
    
                   String parameter = null;
                   String value = null;
    
                   if (indexOfEquals != -1) {
                       parameter = parameterValuePair.substring(0, indexOfEquals);
    
                       if (indexOfEquals + 1 < parameterValuePair.length()) {
                           value = parameterValuePair.substring(indexOfEquals + 1);
                       }
                  }
    
                  //将数据放在urlProps中,为一个当前Driver全局参数配置Properties类型
                  if ((value != null && value.length() > 0) && (parameter != null && parameter.length() > 0)) {
                     try {
                           urlProps.put(parameter, URLDecoder.decode(value , "UTF-8"));
                     } catch (UnsupportedEncodingException badEncoding) {
                           // punt
                           urlProps.put(parameter, URLDecoder.decode(value));
                     } catch (NoSuchMethodError nsme) {
                           // punt again
                           urlProps.put(parameter, URLDecoder.decode(value));
                     }
              }
           }
    }
    ......



    好了,还有些代码没贴,太多了,上面的URL解析知道是那个Driver去解析后,然后getConection就是newXXXConnection的问题,当然如果自己设置Properties这些参数会做相关的设置,例如Oracle的Properties参数中除了正常的参数外,还可以设置(一般情况下保持默认就可以了,后续的文章中我会提到一些参数,并根据源码说明它的默认值):

    database、server、protocol、dll、prefetch、rowPrefetch、defaultRowPrefetch、batch、executeBatch、defaultExecuteBatch、remarks、remarksReporting、synonyms、includeSynonyms、restrictGetTables、fixedString、dataSizeUnits、AccumulateBatchResult

    等等参数;

    如源码所示(还是在connect方法中):


    同时oracle还可能是:oracle.jdbc.ultra.client.Driver(一般用不到,知道有就可以,在源码中有体现)。也有可能是OCI接口(OracleOCIConnection);如果是在我们大多数用thin的模式下,最终会调用:

    neworacle.jdbc.driver.OracleConnection(dbaccess, s1, s2, s3, s4, properties);

    这些参数细节,下一篇文章说,这里要说进去很多。


    mysql的参数都在parserURL里面被设置到Driver的全局变量中(Properties urlProps),在解析后,只需要解析后直接new一个com.mysql.jdbc.Connection就可以了;


    我们现在知道connection通过【对应driver】的【connect】方法如何new出来了,也可以看出每次请求DirverManger.getConnection都会new 一个Connection出来(所以要注意连接池的用途,需要对connection提供调度等策略);

    调用getConection的时候,还会遍历所有【被注册的Dirver】,所以我们也想办法让它尽量【不要被反复遍历】,顺便提及下,大家别被Dirver吓怕了,其实我们看看源码,其实就是一个对象嘛,只是它的功能有点像驱动而已,他提供一些注册和链接的方法,被我们成为Driver;


    回过头来,Driver是在什么时候被注册到DriverManager里面的呢?奇怪,如果未被注册,肯定是获取不到的,看看自己的代码:

    Class.forName("dddd");

    为什么要写这一句,这一句话简单来讲仅仅是用当前的CLassLoader将一个类进行装载的代码,若已经装载,什么也不会做;

    但是DriverManager里面怎么知道的呢?难道所有的类都在里面,那要遍历起来太吓人了,而且我们开始输出过所有的Driver这是错误的,而且Dirver都要求有相应的方法,难道是都implements Driver?有这种可能性,在方法实现上可以做到;

    到底是怎么样的呢?

    其实在Oracle JDBC或MySQL JDBC里面都一个【static匿名块】,static 匿名块是当你【第一次访问这个Class(类)的时候】,会被调用,以后不会被重新初始化,分别如下:

    Orale的oracle.jdbc.driver.OracleDriver类里面:


    可见,他自己调用了DriverManager.registerDirever方法,注册进去,然后new了一个自己;使得自己是全局单例的;

    再看看MySQL的:

    在类:com.mysql.jdbc.Driver类中:


    同样的方式,进行注册了,原来是这样,忽悠了很久,不是什么人名字,而是提供了registerDriver(Dirver driver)方法来注册;

    我们再来看看注册到底做了什么:

    DriverManager.registerDriver()我们看看:


    说明什么,就是做了一个类似于List的东西(内部可以看出它是用Vector实现的,java用vector其实是很早的写法,不过由于这部分代码实际应用中一般不会被经常调用,所以先关的代码也没有做多少修改),然后将数据写进去,原来注册这么简单,当你有一天要来注册某种链接的时候,你要基于一个统一标准来写的时候,就可以用这种方式注册了,例如,你自己写一个某种数据库库的链接对象,或对某些NoSQL做二次包装,这样上面的程序就可以变得比较通用了;


    通过上面的可以看出,放在static匿名块中的代码是在第一次访问这个类的时候被调用,所以不一定非要用Class.forName()因为这样就是访问了这个类;你可以调用这个类里面的一个静态方法,或new一个出来都会访问到这个类,当然driver为了避免重复创建,所以我们一般不用new而已,因为他自己会内部创建一个注册到Driver中,除非调用DriverManager.deregisterDriver(Driver driver)来取消注册,将Driver中被注册的那个对象去掉;


    原理上我们说得差不多了,有些地方其实我们可以节约一些开销,当然不是特别关键的;

    其一:

    我们发现每次通过DriverManager.getConnection都需要去遍历获取Driver然后去调用connect方法,其实我们认为Driver获取一次就可以了,对于相同类型的driver,一般我们的程序就可以认出来是mysql还是oracle;

    所以我们可以通过DriverManager.getDriver(url)只获取一次,你可以放入一个模型就可以,分别表示:

    oracleDriver、mySqlDriver,这样就直接调用对应的driver了;

    例如:

    private static OracleDriver oracleDriver = null;
    
    
    static {
       try {
          //Class.forName("oracle.jdbc.driver.OracleDriver");
          OracleDriver.getCompileTime();//随便访问一个静态方法即可,其实只是想访问下这个类,Class.forName是最简单的访问。
          oracleDriver = (OracleDriver) DriverManager.getDriver("jdbc:oracle:thin:@ip:port:sid");
        } catch (Exception e) {
          e.printStackTrace();
       }
    }


    这样这个OracleDriver就被保存起来,以后就可以直接使用oracleDriver.connect来获取链接了。

     

    其二:

    你也可以new一个driver出来,但是注意这里请保持单利,这样会比较好,一些全局设置可以再全局生效,上面也说了,不推荐,只是说他是一种方法而已;

    例如:

    private final static Driver oracleDriver = new OracleDriver();


    最后DriverManager里面还有些其他的方法,比如一些去掉注册的方法之类的,如果对于注册较多的情况下,需要将一些不需要的去掉,可以再这进行参考;


    下一篇文章,会提及到Connection获取的一些细节,就是具体某个DriverConnection类的实现体,以及PrepareStatement实现,setTimeout实现机制、fetchSize内部运行

    后面还会继续介绍,和数据库交互过程中,反解析数据的过程,中断链接的原理等具体的实现。

    不过肯定不会将所有的源码全部说到,因为有很多不常用,例如存储过程的调用之类的,暂时不是提到。



    展开全文
  • 前端使用ajax获取Response Headers 中的 Set-Cookie 值为null, ![图片说明](https://img-ask.csdn.net/upload/202003/30/1585558466_989791.png) 代码如下: ``` $.ajax({ type: 'HEAD', url:url,...
  • 'Connection': 'keep-alive', 'Referer': 'http://jjhygl.hzfc.gov.cn/webty/gpfy/gpfySelectlist.jsp', 'Cookie': 'ROUTEID=.lb6; JSESSIONID=2E78A1FE8DBC80F1CEEE20264BE96B1F.lb6; Hm_lvt_70e93e4ca4be30a...
  • 经过这番改造,这样我们的count的 永远是0(是不是不符合预期了?) 。 小技巧:此处为了演示我使用sleep方式把问题放大了, 否则可能有时候好使、有时候不好使 把问题放大是debug调试的一个基本技巧~ ...

    前言

    关于Spring的事务,它是Spring Framework中极其重要的一块。前面用了大量的篇幅从应用层面、原理层面进行了比较全方位的一个讲解。但是因为它过于重要,所以本文继续做补充内容:Spring事务的同步机制(后面还有Spring事务的监听机制)

    Spring事务同步机制?我估摸很多小伙伴从来没听过还有这么一说法,毕竟它在平时开发中你可能很少遇到(如果你没怎么考虑过系统性能和吞吐量的话)。

    让我记录本文的源动力是忆起两年前自己在开发、调试过程中遇到这样一个诡异异常:

    java.sql.SQLException: Connection has already been closed

    但是,它不是必现的,重点:它不是必现的。 而一旦出现,任何涉及需要使用数据库连接的接口都有可能报这个错(已经影响正常work了),重启也解决不了问题的根本。

    关于非必现问题,我曾经表达了一个观点:程序中的“软病(非必现问题)”是相对很难解决的,因为定位难度高,毕竟只要问题一旦定位了,从来不差解决方案

    这个异常的字面意思非常简单:数据库连接池连接被关闭了。 可能大多数人(我当然也不例外)看到此异常都会fuck一句:what?我的连接都是交给Spring去管理了,自己从来不会手动close,怎么回事?难道Spring有bug? 敢于质疑“权威”一直以来都是件好事,但是有句话这么说:你对人家还不了解的情况下不要轻易说人家程序有bug。

    可能大多数人对于Spring的事务,只知道怎么使用,比如加个注解啥的,但是底层原理并不清楚,因此定位此问题就会变得非常的困难了~ 由于我之前有研究过Spring事务的同步机制这块,所以忆起这件事之后就迅速定位了问题所在:这和Spring事务的同步机制有关,并不是Spring事务的bug。


    Spring事务极简介绍

    关于Spring事务,我推荐小伙伴看看上面的【相关阅读】,能让你对Spring事务管理有个整体的掌握。但是由于过了有段时间了,此处做个非常简单的介绍:

    Spring有声明式事务编程式事务声明式事务只需要提供@Transactional的注解,然后事务的开启和提交/回滚、资源的清理就都由spring来管控,我们只需要关注业务代码即可; 编程式事务则需要使用spring提供的模板,如TransactionTemplate,或者直接使用底层的PlatformTransactionManager手动控制提交、回滚。

    声明式事务的最大优点就是对代码的侵入性小,只需要在方法上加@Transactional的注解就可以实现事务; 编程式事务的最大优点就是事务的管控粒度较细,可以实现代码块级别的事务。

    前提介绍

    Spring把JDBC 的 Connection或者HibernateSession等访问数据库的链接(会话)都统一称为资源,显然我们知道Connection这种是线程不安全的,同一时刻是不能被多个线程共享的。

    简单的说:同一时刻我们每个线程持有的Connection应该是独立的,且都是互不干扰和互不相同的

    但是Spring管理的Service、Dao等他们都是无状态的单例Bean,怎么破?,如何保证单例Bean里面使用的Connection都能够独立呢? Spring引入了一个类:事务同步管理类org.springframework.transaction.support.TransactionSynchronizationManager来解决这个问题。它的做法是内部使用了很多的ThreadLocal为不同的事务线程提供了独立的资源副本,并同时维护这些事务的配置属性和运行状态信息 (比如强大的事务嵌套、传播属性和这个强相关)。

    这个同步管理器TransactionSynchronizationManager是掌管这一切的大脑,它管理的TransactionSynchronization是开放给调用者一个非常重要的扩展点,下面会有详细介绍~

    TransactionSynchronizationManagerDaoService 类中影响线程安全的所有 “ 状态 ” 都统一抽取到该类中,并用 ThreadLocal 进行封装,这样一来, Dao (基于模板类或资源获取工具类创建的 Dao )和 Service (采用 Spring 事务管理机制)就不用自己来保存一些事务状态了,从而就变成了线程安全的单例对象了,优秀~

    DataSourceUtils

    这里有必要提前介绍Spring提供给我们的这个工具类。

    有些场景比如我们使用MyBatis的时候,某些场景下,可能无法使用 Spring 提供的模板类来达到效果,而是需要直接操作源生API Connection

    那如何拿到这个链接Connection呢???(主意此处打大前提:必须保证和当前MaBatis线程使用的是同一个链接,这样才接受本事务控制嘛,否则就脱缰了~

    这个时候DataSourceUtils这个工具类就闪亮登场了,它提供了这个能力:

    public abstract class DataSourceUtils {
    	...
    	public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { ... }
    	...
    	
    	// 把definition和connection进行一些准备工作~
    	public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) throws SQLException { ...}
    
    	// Reset the given Connection after a transaction,
    	// con.setTransactionIsolation(previousIsolationLevel);和con.setReadOnly(false);等等
    	public static void resetConnectionAfterTransaction(Connection con, @Nullable Integer previousIsolationLevel) { ... }
    
    	// 该JDBC Connection 是否是当前事务内的链接~
    	public static boolean isConnectionTransactional(Connection con, @Nullable DataSource dataSource) { ... }
    
    	// Statement 给他设置超时时间  不传timeout表示不超时
    	public static void applyTransactionTimeout(Statement stmt, @Nullable DataSource dataSource) throws SQLException { ... }
    	public static void applyTimeout(Statement stmt, @Nullable DataSource dataSource, int timeout) throws SQLException { ... }
    
    	// 此处可能是归还给连接池,也有可能是close~(和连接池参数有关)
    	public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) { ... }
    	public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException { ... }
    
    	// 这个是真close
    	public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException { ... }
    	
    	// 如果链接是代理,会拿到最底层的connection
    	public static Connection getTargetConnection(Connection con) { ... }
    }

    getConnection()这个方法就是从TransactionSynchronizationManager里拿到一个现成的Connection(若没有现成的会用DataSource创建一个链接然后放进去~~~),所以这个工具类还是蛮好用的。

    其实Spring不仅为JDBC提供了这个工具类,还为HibernateJPAJDO等都提供了类似的工具类。 org.springframework.orm.hibernate.SessionFactoryUtils.getSession() org.springframework.orm.jpa.EntityManagerFactoryUtils.getTransactionalEntityManager() org.springframework.orm.jdo.PersistenceManagerFactoryUtils.getPersistenceManager()

    问题场景一模拟

    为了更好解释和说明,此处我模拟出这样的一个场景。

    // 此处生路而关于DataSource、PlatformTransactionManager事务管理器等的配置
    @Slf4j
    @Service
    public class HelloServiceImpl implements HelloService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional
        @Override
        public Object hello(Integer id) {
            // 向数据库插入一条记录
            String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
            jdbcTemplate.update(sql);
    
            // 做其余的事情  可能抛出异常
            System.out.println(1 / 0);
            return "service hello";
        }
    }

    如上Demo,这样子的因为有事务,所以最终这个插入都是不会成功的。(这个应该不用解释了吧,初级工程师应该必备的“技能”~)

        @Transactional
        @Override
        public Object hello(Integer id) {
            // 向数据库插入一条记录
            String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
            jdbcTemplate.update(sql);
    
    
            // 根据id去查询获取 总数(若查询到了肯定是count=1)
            String query = "select count(1) from user where id = " + id;
            Integer count = jdbcTemplate.queryForObject(query, Integer.class);
            log.info(count.toString());
    
            return "service hello";
        }

    稍微改造一下,按照上面这么写,我相信想都不用想。count永远是返回1的~~这应该也是我们面向过程编程时候的经典案例:前面insert一条记录,下面是可以立马去查询出来的

    下面我把它改造如下:

        @Transactional
        @Override
        public Object hello(Integer id) {
            // 向数据库插入一条记录
            String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
            jdbcTemplate.update(sql);
    
    
            // 生产环境一般会把些操作交给线程池,此处我只是模拟一下效果而已~
            new Thread(() -> {
                String query = "select count(1) from user where id = " + id;
                Integer count = jdbcTemplate.queryForObject(query, Integer.class);
                log.info(count.toString());
            }).start();
    
    
            // 把问题放大
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            return "service hello";
        }

    经过这番改造,这样我们的count的值永远是0(是不是不符合预期了?)

    小技巧:此处为了演示我使用sleep方式把问题放大了,否则可能有时候好使、有时候不好使 把问题放大是debug调试的一个基本技巧~

    这个现象就是一个非常严重的问题,它可能会出现:刚插入的数据竟然查不到的诡异现象,这个在我们现阶段平时工作中也会较为频繁的遇到,若对这块不了解,它会对的业务逻辑、对mysql binlog的顺序有依赖的相关逻辑全都将会受到影响

    解决方案

    在互联网环境编程中,我们经常为了提高吞吐量、程序性能,会使用到异步的方式进行优化、消峰等等。因此连接池、线程池被得到了大量的应用。我们知道异步的提供的好处不言而喻,能够尽最大可能的提升硬件的利用率和能力,但它带来的缺点只有一个:提升系统的复杂性,很多时候需要深入的了解它才能运用自如,毕竟任何方案都是一把双刃剑,没有完美的~

    比如一个业务处理中,发短信、发微信通知、记录操作日志等等这些非主干需求,我们一般都希望交给线程池去处理而不要干扰主要业务流程,所以我觉得现在多线程方式处理任务的概率已经越来越高了~ 既然如此,我觉得出现上面我模拟的这种现象的可能性还是蛮高的,所以希望小伙伴们能引起重视一些。

    定位到问题的原因是解决问题的关键,这里我先给出直接的解决方案,再做理论分析。 我们的诉求是:我们的异步线程的执行时,必须确保记录已经持久化到数据库了才ok。因此可以这么来做,一招制敌:

    @Slf4j
    @Service
    public class HelloServiceImpl implements HelloService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional
        @Override
        public Object hello(Integer id) {
            // 向数据库插入一条记录
            String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
            jdbcTemplate.update(sql);
    
    
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                // 在事务提交之后执行的代码块(方法)  此处使用TransactionSynchronizationAdapter,其实在Spring5后直接使用接口也很方便了~
                @Override
                public void afterCommit() {
                    new Thread(() -> {
                        String query = "select count(1) from user where id = " + id;
                        Integer count = jdbcTemplate.queryForObject(query, Integer.class);
                        log.info(count.toString());
                    }).start();
                }
            });
    
    
            // 把问题放大
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            return "service hello";
        }
    
    }

    我们使用TransactionSynchronizationManager注册一个TransactionSynchronization然后在afterCommit里执行我们的后续代码,这样就能100%确保我们的后续逻辑是在当前事务被commit后才执行的,完美的问题解决

    它还有个方法afterCompletion()有类似的效果,至于它和afterCommit()有什么区别,我觉得稍微有点技术敏感性的小伙伴都能知晓的~

    TransactionSynchronizationManager

    对它简单的解释为:使用TreadLocal记录事务的一些属性,用于应用扩展同步器的使用,在事务的开启,挂起,提交等各个点上回调应用的逻辑

    // @since 02.06.2003  它是个抽象类,但是没有任何子类  因为它所有的方法都是静态的
    public abstract class TransactionSynchronizationManager {
    
    	// ======保存着一大堆的ThreadLocal 这里就是它的核心存储======
    
    	//  应用代码随事务的声明周期绑定的对象  比如:DataSourceTransactionManager有这么做:
    	//TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
    	// TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
    	// 简单理解为当前线程的数据存储中心~~~~
    	private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
    
    	// 使用的同步器,用于应用扩展
    	// TransactionSynchronization同步器是最为重要的一个扩展点~~~ 这里是个set 所以每个线程都可以注册N多个同步器
    	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
    	
    	// 事务的名称  
    	private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
    	// 事务是否是只读  
    	private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
    	// 事务的隔离级别
    	private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
    	// 事务是否开启   actual:真实的
    	private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
    
    	// 返回的是个只读视图
    	public static Map<Object, Object> getResourceMap() {
    		Map<Object, Object> map = resources.get();
    		return (map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap());
    	}
    
    	public static boolean hasResource(Object key) { ... }
    	public static Object getResource(Object key) { ... }
    	
    	// actualKey:确定的key  拆包后的
    	@Nullable
    	private static Object doGetResource(Object actualKey) {
    		Map<Object, Object> map = resources.get();
    		if (map == null) {
    			return null;
    		}
    		Object value = map.get(actualKey);
    		// Transparently remove ResourceHolder that was marked as void...
    		// 如果ResourceHolder 被标记为了void空白了。此处直接从map里移除掉对应的key 
    		// ~~~~~~~并且返回null~~~~~~~~~~~
    		if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
    			map.remove(actualKey);
    			// Remove entire ThreadLocal if empty...
    			if (map.isEmpty()) {
    				resources.remove();
    			}
    			value = null;
    		}
    		return value;
    	}
    
    	// 逻辑很简单,就是和当前线程绑定一个Map,并且处理ResourceHolder 如果isVoid就抛错
    	public static void bindResource(Object key, Object value) throws IllegalStateException {
    		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    		Assert.notNull(value, "Value must not be null");
    		Map<Object, Object> map = resources.get();
    		// set ThreadLocal Map if none found
    		if (map == null) {
    			map = new HashMap<>();
    			resources.set(map);
    		}
    		Object oldValue = map.put(actualKey, value);
    		// Transparently suppress a ResourceHolder that was marked as void...
    		if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
    			oldValue = null;
    		}
    		if (oldValue != null) {
    			throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
    					actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
    		}
    	}
    
    	public static Object unbindResource(Object key) throws IllegalStateException { ... }
    	public static Object unbindResourceIfPossible(Object key) { ... }
    	
    
    	// 同步器是否是激活状态~~~  若是激活状态就可以执行同步器里的相关回调方法了
    	public static boolean isSynchronizationActive() {
    		return (synchronizations.get() != null);
    	}
    
    	// 如果事务已经开启了,就不能再初始化同步器了  而是直接注册
    	public static void initSynchronization() throws IllegalStateException {
    		if (isSynchronizationActive()) {
    			throw new IllegalStateException("Cannot activate transaction synchronization - already active");
    		}
    		logger.trace("Initializing transaction synchronization");
    		synchronizations.set(new LinkedHashSet<>());
    	}
    
    	// 注册同步器TransactionSynchronization   这个非常重要 下面有详细介绍这个接口
    	// 注册的时候要求当前线程的事务已经是激活状态的  而不是随便就可以调用的哦~~~
    	public static void registerSynchronization(TransactionSynchronization synchronization) throws IllegalStateException {
    		Assert.notNull(synchronization, "TransactionSynchronization must not be null");
    		if (!isSynchronizationActive()) {
    			throw new IllegalStateException("Transaction synchronization is not active");
    		}
    		synchronizations.get().add(synchronization);
    	}
    
    
    	// 返回的是只读视图  并且,并且支持AnnotationAwareOrderComparator.sort(sortedSynchs); 这样排序~~
    	public static List<TransactionSynchronization> getSynchronizations() throws IllegalStateException { ... }
    	public static void clearSynchronization() throws IllegalStateException { ... }
    
    	... // 省略name等其余几个属性的get/set方法  因为没有任何逻辑
    	// 这个方法列出来,应该下面会解释
    	public static void setActualTransactionActive(boolean active) {
    		actualTransactionActive.set(active ? Boolean.TRUE : null);
    	}
    	
    	// 清楚所有和当前线程相关的(注意:此处只是clear清除,和当前线程的绑定而已~~~)
    	public static void clear() {
    		synchronizations.remove();
    		currentTransactionName.remove();
    		currentTransactionReadOnly.remove();
    		currentTransactionIsolationLevel.remove();
    		actualTransactionActive.remove();
    	}
    }

    这里把setActualTransactionActive单独拿出来看一下,以加深对事务执行过程的了解。 在AbstractPlatformTransactionManager.getTransaction()的时候会调用此方法如下:

    TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction()); // 相当于表示事务为开启了

    并且该类的handleExistingTransactionprepareTransactionStatus等等方法都会此标记有调用,也就是说它会参与到事务的声明周期里面去

    备注:以上方法他们统一的判断条件有:TransactionStatus.isNewTransaction()是新事务的时候才会调用这个方进行标记

    另外此类它的suspend暂停的时候会直接的这么调用:

    TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);
    TransactionSynchronizationManager.setActualTransactionActive(false);

    resume恢复的时候:

    TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly);
    TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive);

    大体上可以得出这样的一个处理步骤:

    1. 开启新的事务时初始化。第一次开启事务分为:real首次 或 已存在事务但是REQUIRES_NEW
    2. 在事务的嵌套过程中,TransactionSynchronizationManager属性不断更新最终清除。即外层事务挂起;事务提交,这两个点需要更新TransactionSynchronizationManager属性
    3. 这里面有个内部类AbstractPlatformTransactionManager.SuspendedResourcesHolder它是负责事务挂起时候,保存事物属性的对象,用于恢复外层事务。当恢复外层事务时,根据SuspendedResourcesHolder对象,调用底层事务框架恢复事务属性,并恢复TransactionSynchronizationManager

    DefaultTransactionStatus

    它实现了TransactionStatus接口。 这个是整个事务框架最重要的状态对象,它贯穿于事务拦截器,spring抽象框架和底层具体事务实现框架之间。

    它的重要任务是在新建,挂起,提交事务的过程中保存对应事务的属性。在AbstractPlatformTransactionManager中,每个事物流程都会new创建这个对象

    TransactionSynchronizationUtils

    这个工具类比较简单,主要是处理TransactionSynchronizationManager和执行TransactionSynchronization它对应的方法们,略~



    TransactionSynchronization:事务同步器

    这个类非常的重要,它是我们程序员对事务同步的扩展点:用于事务同步回调的接口,AbstractPlatformTransactionManager支持它。

    注意:自定义的同步器可以通过实现Ordered接口来自己定制化顺序,若没实现接口就按照添加的顺序执行~

    // @since 02.06.2003  实现了java.io.Flushable接口
    public interface TransactionSynchronization extends Flushable {
    
    	int STATUS_COMMITTED = 0;
    	int STATUS_ROLLED_BACK = 1;
    	int STATUS_UNKNOWN = 2;
    
    	// 事务赞提suspend的时候调用此方法
    	// 实现这个方法的目的一般是释放掉绑定的resources 
    	// TransactionSynchronizationManager#unbindResource
    	default void suspend() {
    	}
    	// 事务恢复时候调用
    	// TransactionSynchronizationManager#bindResource
    	default void resume() {
    	}
    
    	// 将基础会话刷新到数据存储区(如果适用) 比如Hibernate/Jpa的session
    	@Override
    	default void flush() {
    	}
    
    	// 在事务提交之前促发。在AbstractPlatformTransactionManager.processCommit方法里 commit之前触发
    	// 事务提交之前,比如flushing SQL statements to the database
    	// 请注意:若此处发生了异常,会导致回滚~
    	default void beforeCommit(boolean readOnly) {
    	}
    	// 在beforeCommit之后,在commit/rollback之前执行
    	// 它和beforeCommit还有个非常大的区别是:即使beforeCommit抛出异常了  这个也会执行
    	default void beforeCompletion() {
    	}
    
    	// 这个就非常重要了,它是事务提交(注意事务已经成功提交,数据库已经持久化完成这条数据了)后执行  注意此处是成功提交而没有异常
    	// javadoc说了:此处一般可以发短信或者email等操作~~因为事务已经成功提交了
    	
    	// =====但是但是但是:======
    	// 事务虽然已经提交,但事务资源(链接connection)可能仍然是活动的和可访问的。
    	// 因此,此时触发的任何数据访问代码仍将“参与”原始事务 允许执行一些清理(不再执行提交操作!)
    	// 除非它明确声明它需要在单独的事务中运行。
    	default void afterCommit() {
    	}
    
    	// 和上面的区别在于:即使抛出异常回滚了  它也会执行的。它的notice同上
    	default void afterCompletion(int status) {
    	}
    }

    我们自定义一个同步器TransactionSynchronization使用得最多的是afterCommitafterCompletion这两个方法,但是上面的note一定一定要注意,下面我用“人的语言”尝试翻译如下:

    1. 事务虽然已经提交,但是我的连接可能还是活动的(比如使用了连接池链接是不会关闭的)
    2. 若你的回调中刚好又使用到了这个链接,它会参与到原始的事务里面去
    3. 这个时候你参与到了原始事务,但是它并不会给你commit提交。(所以你在这里做的update、insert等默认都将不好使)
    4. 回收资源(链接)的时候,因为你使用的就是原始事务的资源,所以Spring事务还会给你回收掉,从而就可能导致你的程序出错

    声明一下:这段白话文"翻译"是我自主的行为,目前还没有得到任何官方、第三方的描述和认可,我是第一个,旨在希望对小伙伴理解这块有所帮助,若有不对的地方请帮忙留言指正,不甚感激~

    依旧为了加强理解,看看源码处是怎么个逻辑:

    public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
    	...
    	private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    		...
    		try {
    			prepareForCommit(status);
    			triggerBeforeCommit(status);
    			triggerBeforeCompletion(status);
    			...
    			doCommit(status);
    			
    			// 事务正常提交后  当然triggerAfterCompletion方法上面回滚里有而有个执行 此处不贴出了
    			try {
    				triggerAfterCommit(status);
    			} finally {
    				triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
    			}
    		} finally {
    			cleanupAfterCompletion(status);
    		}
    	}
    	...
    	// 清楚、回收事务相关的资源~~~  并且恢复底层事务(若需要~)
    	private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    		status.setCompleted();
    		if (status.isNewSynchronization()) {
    			TransactionSynchronizationManager.clear();
    		}
    		if (status.isNewTransaction()) {
    			doCleanupAfterCompletion(status.getTransaction());
    		}
    		if (status.getSuspendedResources() != null) {
    			if (status.isDebug()) {
    				logger.debug("Resuming suspended transaction after completion of inner transaction");
    			}
    			Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
    			resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
    		}
    	}
    }

    从这个代码结构里可以看到,即使triggerAfterCommittriggerAfterCompletion全部都执行了(哪怕是抛错了),最终它一定会做的是:cleanupAfterCompletion(status);这一步会回收资源。

    那这种情况怎么避免被它回收呢?其实上面JavaDoc也说了:首先是可能,其次Spring建议使用一个新事务处理来避免这种可能性发生

    至于什么是新事务?比如上面的new了一个线程,那都别说新事务了,都开新线程,所以肯定是不存在此问题了的。 Spring这里指的是若你还在同一个线程里,同步进行处理的时候,建议新启一个新事务(使用PROPAGATION_REQUIRES_NEW吧~)


    Spring是如何保证事务获取同一个Connection的

    相信这个问题,有了上面的理论支撑,此处不用再大花篇幅了。~以JdbcTemplate为例一笔带过。

    JdbcTemplate执行SQL的方法主要分为updatequery方法,他俩底层最终都是依赖于execute方法去执行(包括存储函数、储存过程),所以只需要看看execute是怎么获取connection链接的?

    public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    	...
    	public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    		...
    		// dataSource就是此JdbcTemplate所关联的数据源,这个在config配置文件里早就配置好了
    		// 显然,这里获取的连接就是事务相关的,和当前想成绑定的connection
    		Connection con = DataSourceUtils.getConnection(obtainDataSource());
    		...
    		finally {
    			JdbcUtils.closeStatement(stmt);
    			DataSourceUtils.releaseConnection(con, getDataSource());
    		}
    	}
    	...
    }

    TransactionSynchronizationManager内部用ThreadLocal<Map<Object, Object>>对象存储资源,key为DataSource、value为connection对应的ConnectionHolder对象。

    以上,就是它保证统一的核心原因,其它持久化框架处理方法都类似~

    TransactionSynchronization的实现类们

    首先就是TransactionSynchronizationAdapter,从明白中就能看出它仅仅是个Adapter适配器而已,并不做实事。但是这个适配器它额外帮我们实现了Ordered接口,所以子类们不用再显示实现了,这样非常利于我们书写匿名内部类去实现它,这一点还是很暖心的~~

    public abstract class TransactionSynchronizationAdapter implements TransactionSynchronization, Ordered {
    	@Override
    	public int getOrder() {
    		return Ordered.LOWEST_PRECEDENCE;
    	}
    	... // 省略空实现们~~~
    }

    其余实现均为内部类实现,比如DataSourceUtils.ConnectionSynchronizationSimpleTransactionScope.CleanupSynchronization。还有后面会碰到的一个相对重要的的内部类实现:ApplicationListenerMethodTransactionalAdapter.TransactionSynchronizationEventAdapter,它和事务监听机制有关~

    问题场景二模拟

    场景一借助TransactionSynchronizationManager解决了“先插入再异步异步线程查询不到”的问题,也就是著名的Spring如何在数据库事务提交成功后进行异步操作问题~~

    case1最多就是丢失部分信息记录,影响甚微(毕竟非常重要的步骤并不建议使用这种异步方式去实现和处理~)。 case2也就是本case最坏情况最终会导致Spring准备好的所有的connection都被close,从而以后再次请求的话拿到的都是已关闭的连接,最终可能导致整个服务的不可用,可谓非常严重。本case主要是为了模拟出上面Spring官方Note的说明,使用时需要注意的点~

    其实如果你在afteCommit里面如果不直接直接使用connection链接,是不会出现链接被关闭问题的。因为现在的高级框架都很好的处理了这个问题

    下面我模拟此场景的代码如下:

    @Slf4j
    @Service
    public class HelloServiceImpl implements HelloService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional
        @Override
        public Object hello(Integer id) {
            // 向数据库插入一条记录
            String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
            jdbcTemplate.update(sql);
    
             TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                // 在事务提交之后执行的代码块(方法)  此处使用TransactionSynchronizationAdapter,其实在Spring5后直接使用接口也很方便了~
                @Override
                public void afterCommit() {
                    String sql = "insert into user (id,name,age) values (" + (id + 1) + ",'fsx',21)";
                    int update = jdbcTemplate.update(sql);
                    log.info(update + "");
                }
            });
            return "service hello";
        }
    
    }

    预期结果:本以为第二个insert是插入不进去的(不是报错,而是持久化不了),但是最终结果是:两条记录都插入成功了。

    what a fuck,有点打我脸,挺疼。,与我之前掌握的理论相悖了,与Spring的javadoc里讲述的也相悖了(其实与Spring的并没有相悖,毕竟人家说的是“可能”,可见话不能说太满的重要性,哈哈)。这勾起了我的深入探索,究竟咋回事呢???

    下面我把我的研究结果直接描述如下:

    afterCommit()内的connection也提交成功的原因分析

    按照AbstractPlatformTransactionManager事务的源码执行处:

    public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
    	...
    	private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    		...
    		try {
    			prepareForCommit(status);
    			triggerBeforeCommit(status);
    			triggerBeforeCompletion(status);
    			...
    			doCommit(status);
    			
    			// 事务正常提交后  当然triggerAfterCompletion方法上面回滚里有而有个执行 此处不贴出了
    			try {
    				triggerAfterCommit(status);
    			} finally {
    				triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
    			}
    		} finally {
    			cleanupAfterCompletion(status);
    		}
    	}
    	...
    	// 清楚、回收事务相关的资源~~~  并且恢复底层事务(若需要~)
    	private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    		status.setCompleted();
    		if (status.isNewSynchronization()) {
    			TransactionSynchronizationManager.clear();
    		}
    		if (status.isNewTransaction()) {
    			doCleanupAfterCompletion(status.getTransaction());
    		}
    		if (status.getSuspendedResources() != null) {
    			if (status.isDebug()) {
    				logger.debug("Resuming suspended transaction after completion of inner transaction");
    			}
    			Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
    			resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
    		}
    	}
    }

    可以明确的看到执行到triggerAfterCommit/triggerAfterCompletion的时候doCommit是执行完成了的,也就是说这个时候事务肯定是已经提交成功了(此时去数据库里查看此记录也确定已经持久化)。

    所以我猜测:后续该connection是不可能再执行connection.commit()方法了的,因为同一个事务只可能被提交一次。从上面理论知道:即使我们在afterCommit()里执行,Spring也保证了我拿到的链接还是当前线程所属事务的Connection 因此我继续猜测:connection的自动提交功能可能是在这期间被恢复了,从而导致了这条SQL语句它的自动提交成功。

    关于Connection的自动提交机制,以及事务对它的“影响干预”,请参与上面的推荐博文了解,有详细的表述

    来到finally里cleanupAfterCompletion方法里有这么一句:

    		// 这里最终都会被执行~~~
    		// doCleanupAfterCompletion方法在本抽象类是一个空的protected方法
    		// 子类可以根据自己的需要,自己去实现事务提交完成后的操作
    		if (status.isNewTransaction()) {
    			doCleanupAfterCompletion(status.getTransaction());
    		}

    我们大都使用的是子类DataSourceTransactionManager,本例也一样使用的是它。因此可以看看它对doCleanupAfterCompletion此方法的实现:

    public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
    		implements ResourceTransactionManager, InitializingBean {
    	...
    	@Override
    	protected void doCleanupAfterCompletion(Object transaction) {
    		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    		// 释放资源~~ Remove the connection holder from the thread, if exposed.
    		if (txObject.isNewConnectionHolder()) {
    			TransactionSynchronizationManager.unbindResource(obtainDataSource());
    		}
    		// Reset connection.
    		Connection con = txObject.getConnectionHolder().getConnection();
    		try {
    			// 这里是关键,在事后会恢复链接的自动提交本能,也就是常用的恢复现场机制嘛~~
    			// 显然这个和isMustRestoreAutoCommit属性的值有关,true就会恢复~~~
    			if (txObject.isMustRestoreAutoCommit()) {
    				con.setAutoCommit(true);
    			}
    			DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
    		}
    		...
    		txObject.getConnectionHolder().clear();
    	}
    	...
    }

    从上注释可知,现在问题的关键的就是DataSourceTransactionObject对象isMustRestoreAutoCommit的属性值了,若它是true,那就完全符合我的猜想。


    DataSourceTransactionObject

    关于DataSourceTransactionObject,它是一个DataSourceTransactionManager的一个私有内部静态类。

    private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {
    
    		// 来自父类
    		@Nullable
    		private ConnectionHolder connectionHolder;
    		@Nullable
    		private Integer previousIsolationLevel;
    		private boolean savepointAllowed = false;
    
    		// 来自本类
    		private boolean newConnectionHolder;
    		private boolean mustRestoreAutoCommit; // 决定是否要恢复自动提交  默认情况下是false的
    		...
    		public void setMustRestoreAutoCommit(boolean mustRestoreAutoCommit) {
    			this.mustRestoreAutoCommit = mustRestoreAutoCommit;
    		}
    		public boolean isMustRestoreAutoCommit() {
    			return this.mustRestoreAutoCommit;
    		}
    }

    这个内部类很简单,就是聚合了一些属性值,此处我们只关注mustRestoreAutoCommit这个属性值是否被设置为true了,若被设置过,就符合我的预期和猜想了。


    通过代码跟踪,发现DataSourceTransactionManagerdoBegin的时候调用了setMustRestoreAutoCommit方法如下:

    	@Override
    	protected void doBegin(Object transaction, TransactionDefinition definition) {
    		...
    		if (con.getAutoCommit()) {
    			txObject.setMustRestoreAutoCommit(true); // 此处设置值为true
    			if (logger.isDebugEnabled()) {
    				logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
    			}
    			con.setAutoCommit(false);
    		}
    		
    	}

    此处代码也就是当开启事务(doBegin)的时候的关键代码,它对DataSourceTransactionObject打入标记,表示最终需要事务它返还给链接自动提交的能力。

    综上所述:上述案例Demo最终成功插入了两条数据的结果是完全正确,且我的猜想都解释通了。

    备注:case2我本想构造的是在afterCommit()里使用connection而最终被错误关闭的情况case,目前来看若使用的是DataSourceTransactionManager这个事务管理器的话,是不用担心这种情况发生的,最终你的SQL都会被成功提交,也不会出现被误close掉的问题~


    总结

    这一篇文章的主旨是讲解Spring事务的同步机制,虽然这以能力可能是Spring提供的小众功能,但正所谓小脾气、大能力描述它就很贴切~

    我认为如果你真的想去了解一门技术的时候,还是不要放过每一个细节,把它融汇贯通,这样再学习一个新的技术就很容易举一反三了。(因为没有一句代码、注释都是无用的,否则它是废代码,就没有存在的必要)

    展开全文
  • Spring MVC @ModelAttribute 获取不到

    千次阅读 2018-02-02 16:07:53
    Spring MVC form表单请求 通过 @ModelAttribute 获取不到问题情况: 1.html form 表单配置错误 参考:http://bbs.csdn.net/topics/390588547 后台代码: @Controller @RequestMapping("/test") public ...
  • }else{alert("data值为0"); $("#divdialog").html("有错误发生,可能是用户名或密码错误!");$("#divdialog").dialog({title:"登陆失败提示",modal:true,buttons:{"关闭":function(){$(this).dialog("close");}}});}})...
  • 4G EPS 中的 PDN Connection

    千次阅读 2019-12-29 15:29:11
    文章目录目录PDNPGWPDN ConnectionAPNAPN-OI、APN-NI 与漫游PDN TypePDN Connection 的选择 PDN PDN(Packet Data Network),即分组数据网络。严格意义上讲,PDN 在移动通信系统中存在内部 PDN 和外部 PDN 之分:...
  • Connection Statement ResultSet

    千次阅读 2014-07-21 19:31:43
    Connection getConnection() 获取对数据库的连接 int getFetchDirection() 获取从数据库表中获取行数据的方向 int getFetchSize() 获取返回的数据库结果集行数 int getMaxFieldSize() 获取返回的数据库结果...
  • SqlCommand.Connection 属性

    千次阅读 2008-04-15 20:01:00
    SqlCommand.Connection 属性获取或设置 SqlCommand 的此实例使用的 SqlConnection。[Visual Basic]Public Property Connection As SqlConnection[C#]public SqlConnection Connection {get; set;}[C++]public
  • OracleConnection.ConnectionString

    千次阅读 2014-11-24 17:14:13
    OracleConnection.ConnectionString 属性 .NET Framework 2.0 其他版本 3(共 4)对本文的评价是有帮助 - 评价此主题 获取或设置用于打开 Oracle 数据库的字符串。 ...
  • C# 获取Access 中表的列

    千次阅读 2011-11-05 22:32:44
    获取所有表 OleDbSchemaGuid 包含的时SQL数据库的架构信息,其中包括 列外键索引主键表视图 并不包含数据信息! string connnectionString = "Provider=Microsoft.Jet.OleDb.4.0;Data Source=" + Xtxx...
  • Java--Connection

    千次阅读 2016-09-10 19:25:32
    接口 Connection 所有超级接口:Wrapper public interface Connectionextends Wrapper 与特定数据库的连接(会话)。在连接上下文中执行 SQL 语句并返回结果。 Connection 对象的数据库能够提供描述其表、...
  • Java中Connection方法笔记

    万次阅读 2019-02-14 13:33:47
    Java中Connection方法笔记  public interface Connectionextends Wrapper   与特定数据库的连接(会话)。在连接上下文中执行 SQL 语句并返回结果。 Connection 对象的数据库能够提供描述其表、所支持的 SQL ...
  • } 经过这番改造,这样我们的count的永远是0(是不是不符合预期了?)。 小技巧:此处为了演示我使用sleep方式把问题放大了,否则可能有时候好使、有时候不好使 把问题放大是debug调试的一个基本技巧~ 这个现象...
  • 判断jsonarray对象是否为空

    万次阅读 2018-10-20 07:25:00
    问题的出发点是我要从数据库读数据写成jsonarray数组(登录验证),进而引发问题如何判断jsonarray数组为空 package dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql....
  • 在cmd命令下运行如下命令(路径是我本机的路径,各个主机不同,需要改自己本机的地址): keystore密码默认:android   由于sdk安装的原因,也有可能得不到MD5的,得到的是 怎样...
  • 连接对象Connection的使用

    千次阅读 2014-12-24 09:52:43
    在C#数据库编程中,Connection对象用来连接数据源,针对不同的数据库连接模式,Connection有以下三种形式: (1)SqlConnection 该对象是专连接Sql Server数据库而设计的 (2)OleDbConnection 该对象可以连接...
  • //获取sqlDataReader对象Read()方法读取流的字段的三种方法。  using(SqlConnection conn = new SqlConnection("data source=凡斌-VAIO;initialcatalog=sales;integrated security=true"))  {
  • muduo::TcpConnection分析

    千次阅读 2015-08-09 23:05:16
    muduo::TcpConnection分析
  • java获取数据库信息

    千次阅读 2019-05-26 11:15:33
    备用: package com.ren.test; import org.junit.Test; import java.sql.*; public class GetInfo { //获取数据库连接 public static Connection getConnection() { Connection conn = null; ...
  • java 获取数据库表中的信息,什么中文字段读出null,其他字段数字都读出来了 ``` JdbcUtil jdbcUtil = null; try{ jdbcUtil = new JdbcUtil(); //获取数据库连接 jdbcUtil.getConnection...
  • Android中连接MySql数据库获取数据的简单实现

    万次阅读 多人点赞 2019-02-20 19:37:37
    但是今天我们就来尝试实现直接连接到MySql数据库获取数据的功能。 代码实现 demo很简单,xml布局文件显示的是一个按钮和一个textView控件。 点击按钮实现连接MySql数据库并获取表中的数据;textView用来显示...
  • 最近在开发一个微信小...而如果我把ctx.state.data的赋值写在connection.query()的响应函数里面,ctx.state.data可以被正确赋值,但返回到前端又显示data是的。不知道是什么原因,希望各位可以提供思路,感激不尽~
  • //此值为前端js获取的文件的Base64字符串,通过post请求提交至后台 当文件小于2M时,一切正常,当文件大于2M时,request拿不到参数,都null. 开始分析问题原因: 1.post请求正常不会限制数据大小,排除。 2....
  • 执行这个程序以后发现输出结果竟然不为空,在网上查找以后发现自己并没有错,关闭数据库连接对象并不等于数据库连接对象为空。 public void testQuery() { //数据库连接信息 String url = "jdbc:mysql://...
  • DriverManager.getConnection一共有四个重载方法,前三个由public修饰,用来获取不同类型的参数,这三个getConnection实际相当于一个入口,他们最终都会return第四个私有化的getConnection方法,最终向第四个私有化...
  • //当前线程的connection为空时,关闭connection. if(conn != null){ try{ conn.close(); //connection关闭之后,要从ThreadLocal的集合中清除Connection connectionHolder.remove(); }catch...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 94,092
精华内容 37,636
关键字:

获取的connection的值为空