乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      源碼分析 Mybatis 的 foreach 為什么會出現(xiàn)性能問題

       印度阿三17 2019-03-17

      背景

      最近在做一個類似于綜合報表之類的東西,需要查詢所有的記錄(數(shù)據(jù)庫記錄有限制),大概有1W條記錄,該報表需要三個表的數(shù)據(jù),也就是根據(jù)這 1W 個 ID 去執(zhí)行查詢?nèi)螖?shù)據(jù)庫,其中,有一條查詢 SQL 是自己寫,其他兩條是根據(jù)別人提供的接口進(jìn)行查詢,剛開始的時候,沒有多想,直接使用 in 進(jìn)行查詢,使用 Mybatis 的 foreach 語句;項目中使用的是 jsonrpc 來請求數(shù)據(jù),在測試的時候,發(fā)現(xiàn)老是請求不到數(shù)據(jù),日志拋出的是 jsonrpc 超時異常,繼續(xù)查看日志發(fā)現(xiàn),是被阻塞在上面的三條SQL查詢中。

      在以前分析 Mybatis 的源碼的時候,了解到,Mybatis 的 foreach 會有性能問題,所以改了下 SQL,直接在代碼中拼接SQL,然后在 Mybatis 中直接使用 # 來獲取,替換?class 測試了下,果然一下子就能查詢出數(shù)據(jù)。

      前提

      這里先不考慮使用 in 好不好,如何去優(yōu)化 in,如何使用?exists 或 inner join 進(jìn)行代替等,這里就只是考慮使用了 in 語句,且使用了 Mybatis 的 foreach 語句進(jìn)行優(yōu)化,其實 foreach 的優(yōu)化很簡單,就是把 in 后面的語句在代碼里面拼接好,在配置文件中直接通過 #{xxx} 或 ${xxx} 當(dāng)作字符串直接使用即可。

      測試

      在分析 foreach 源碼之前,先構(gòu)造個數(shù)據(jù)來看看它們的區(qū)別有多大。

      建表語句:

      CREATE?TABLE?person(
      ????id?int(11)?PRIMARY?KEY?NOT?NULL,
      ????name?varchar(50),
      ????age?int(11),
      ????job?varchar(50));

      插入 1W 條數(shù)據(jù):

      POJO 類:

      @Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructorpublic?class?Person?implements?Serializable?{????private?int?id;????private?String?name;????private?String?job;????private?int?age;
      }

      ?方式一

      通過原始的方式,使用 foreach 語句:

      1. 在 dao 里面定義方法:

      List<Person>?queryPersonByIds(@Param("ids")?List<Integer>?ids);

      2. 配置文件SQL:

      <select?id="queryPersonByIds"?parameterType="list"?resultMap="queryPersonMap">
      	select?*?from?person?where?1=1	<if?test="ids?!=?null?and?ids.size()?>?0">
      		and?id?in		<foreach?collection="ids"?item="item"?index="index"?separator=","?open="("?close=")">
      			#{item}		</foreach>
      	</if></select>

      3. 執(zhí)行 main 方法:

      @RunWith(SpringJUnit4Cla***unner.class)@ContextConfiguration(locations?=?{?"classpath:spring-mybatis.xml"?})public?class?MainTest?{????@Autowired
      ????private?IPersonService?personService;????@Test
      ????public?void?test(){????????//?構(gòu)造?1W?個?ID
      ????????List<Integer>?ids?=?new?ArrayList<>();????????for?(int?i?=?1;?i?<=?10000;?i  )?{
      ????????????ids.add(i);
      ????????}????????long?start?=?System.currentTimeMillis();????????
      ????????//?執(zhí)行三次
      ????????personService.queryPersonByIds(ids);
      ????????personService.queryPersonByIds(ids);
      ????????personService.queryPersonByIds(ids);????????long?end?=?System.currentTimeMillis();
      ????????System.out.println(String.format("耗時:%d",?end?-?start));
      ????}
      }
      結(jié)果:耗時:2853

      可以看到通過 foreach 的方法,大概需要 3s

      方式二

      在代碼中封裝 SQL ,在配置文件中 通過 ${xxx} 來獲?。?/p>

      1. 在 dao 添加方法:

      List<Person>?queryPersonByIds2(@Param("ids")?String?ids);

      2. 配置文件SQL:

      <select?id="queryPersonByIds2"?parameterType="String"?resultMap="queryPersonMap">
      	select?*?from?person?where?1=1	<if?test="ids?!=?null?and?ids?!=?''">
      	??and?id?in?${ids}	</if></select>

      3. 執(zhí)行 main 方法:

      @Testpublic?void?test_3(){	//?拼接?SQL?
      	StringBuffer?sb?=?new?StringBuffer();
      	sb.append("(");	for?(int?i?=?1;?i?<?10000;?i  )?{
      		sb.append(i).append(",");
      	}
      	sb.deleteCharAt(sb.toString().length()?-?1);
      	sb.append(")");????//?最終的?SQL?為?(1,2,3,4,5...)
      
      	long?start2?=?System.currentTimeMillis();????//?執(zhí)行三次
      	personService.queryPersonByIds2(sb.toString());
      	personService.queryPersonByIds2(sb.toString());
      	personService.queryPersonByIds2(sb.toString());	long?end2?=?System.currentTimeMillis();
      	System.out.println(String.format("耗時:%d",?end2?-?start2));
      }
      結(jié)果:耗時:360

      通過拼接 SQL,使用 ${xxx} 的方式,執(zhí)行同樣的 SQL ,耗時大概 360 ms

      方式三

      在代碼中封裝 SQL ,在配置文件中 通過 #{xxx} 來獲?。?/p>

      1. 在 dao 中添加方法:

      List<Person>?queryPersonByIds3(@Param("ids")?String?ids);

      2. 配置文件SQL:

      <select?id="queryPersonByIds3"?parameterType="String"?resultMap="queryPersonMap">
      	select?*?from?person?where?1=1	<if?test="ids?!=?null?and?ids?!=?''">
      		and?id?in?(#{ids})	</if></select>

      3. 執(zhí)行 main 方法:

      @Testpublic?void?test_3(){????//?拼接?SQL
      	StringBuffer?sb2?=?new?StringBuffer();	for?(int?i?=?1;?i?<?10000;?i  )?{
      		sb2.append(i).append(",");
      	}
      	sb2.deleteCharAt(sb2.toString().length()?-?1);????//?最終的SQL為?1,2,3,4,5....
      
      	long?start3?=?System.currentTimeMillis();
      
      	personService.queryPersonByIds3(sb2.toString());
      	personService.queryPersonByIds3(sb2.toString());
      	personService.queryPersonByIds3(sb2.toString());	long?end3?=?System.currentTimeMillis();
      	System.out.println(String.format("耗時:%d",?end3?-?start3));
      }
      結(jié)果:耗時:30

      通過拼接 SQL,使用 #{xxx} 的方式,執(zhí)行同樣的 SQL ,耗時大概 30 ms

      總結(jié)

      通過上面三種方式可以看到,使用不同的方式,耗時的差別還是麻大的,最快的是 拼接 SQL,使用 #{xxx} 當(dāng)作字符串處理,最慢的是 foreach。為什么 foreach 會慢那么多呢,后面再分析源碼的時候再進(jìn)行分析;而這里同樣是拼接 SQL 的方式,#{xxx} 和 ${xxx} 耗時卻相差 10 倍左右;我們知道,Mybatis 在解析 # 和 $ 這兩種不同的符號時,采用不同的處理策略;使用過 JDBC 的都知道,通過 JDBC 執(zhí)行 SQL 有兩種方式:?Statment 對象和PreparedStatment 對象, ?PreparedStatment 表示預(yù)編譯的SQL,包含的SQL已經(jīng)預(yù)編譯過了,SQL 中的參數(shù)部分使用 ?進(jìn)行占位,之后使用 setXXX 進(jìn)行賦值,當(dāng)使用 Statement 對象時,每次執(zhí)行一個SQL命令時,都會對它進(jìn)行解析和編譯。所有?PreparedStatment 效率要高一些。那么 Mybatis 在解析 # 和 $ 的時候,分別對應(yīng)的是這兩種對象,# 被解析成?PreparedStatment 對象,通過 ? 進(jìn)行占位,之后再賦值,而 $ 被解析成?Statement ,通過直接拼接SQL的方式賦值,所以,為什么同樣是通過在代碼中拼接 SQL ,# 和 $ 的耗時不同的原因。

      PS:上面只是介紹了三種方式,應(yīng)該沒有人問,拼接SQL為 (1,2,3,4,5),在配置SQL中通過 #{xxx} 來獲取吧

      foreach 源碼解析

      ?下面來看下 foreach 是如何被解析的,最終解析的 SQL 是什么樣的:

      在 Mybatis 中,foreach 屬于動態(tài)標(biāo)簽的一種,也是最智能的其中一種,Mybatis 每個動態(tài)標(biāo)簽都有對應(yīng)的類來進(jìn)行解析,而 foreach 主要是由?ForEachSqlNode?負(fù)責(zé)解析。

      ForeachSqlNode 主要是用來解析 <foreach> 節(jié)點的,先來看看 <foreach> 節(jié)點的用法:

      <select?id="queryPersonByIds"?parameterType="list"?resultMap="queryPersonMap">
      	select?*?from?person?where?1=1	<if?test="ids?!=?null?and?ids.size()?>?0">
      		and?id?in		<foreach?collection="ids"?item="item"?index="index"?separator=","?open="("?close=")">
      			#{item}		</foreach>
      	</if></select>

      最終被 數(shù)據(jù)庫執(zhí)行的 SQL 為?select? * from person where 1=1 and id in (1,2,3,4,5)

      先來看看它的兩個內(nèi)部類:

      PrefixedContext

      該類主要是用來處理前綴,比如 "(" 等。

      private?class?PrefixedContext?extends?DynamicContext?{???
      ???private?DynamicContext?delegate;????//?指定的前綴
      ????private?String?prefix;????//?是否處理過前綴
      ????private?boolean?prefixApplied;????//?.......
      
      ????@Override
      ????public?void?appendSql(String?sql)?{??????//?如果還沒有處理前綴,則添加前綴
      ??????if?(!prefixApplied?&&?sql?!=?null?&&?sql.trim().length()?>?0)?{
      ????????delegate.appendSql(prefix);
      ????????prefixApplied?=?true;
      ??????}???????//?拼接SQL
      ??????delegate.appendSql(sql);
      ????}
      }

      FilteredDynamicContext

      FilteredDynamicContext 是用來處理 #{} 占位符的,但是并未綁定參數(shù),只是把 #{item} 轉(zhuǎn)換為 #{_frch_item_1} 之類的占位符。

      ??private?static?class?FilteredDynamicContext?extends?DynamicContext?{????private?DynamicContext?delegate;????//對應(yīng)集合項在集合的索引位置
      ????private?int?index;????//?item的索引
      ????private?String?itemIndex;????//?item的值
      ????private?String?item;????//.............
      ????//?解析?#{item}
      ????@Override
      ????public?void?appendSql(String?sql)?{
      ??????GenericTokenParser?parser?=?new?GenericTokenParser("#{",?"}",?new?TokenHandler()?{????????@Override
      ????????public?String?handleToken(String?content)?{??????????//?把?#{itm}?轉(zhuǎn)換為?#{__frch_item_1}?之類的
      ??????????String?newContent?=?content.replaceFirst("^\\s*"? ?item? ?"(?![^.,:\\s])",?itemizeItem(item,?index));???????????//?把?#{itmIndex}?轉(zhuǎn)換為?#{__frch_itemIndex_1}?之類的
      ??????????if?(itemIndex?!=?null?&&?newContent.equals(content))?{
      ????????????newContent?=?content.replaceFirst("^\\s*"? ?itemIndex? ?"(?![^.,:\\s])",?itemizeItem(itemIndex,?index));
      ??????????}??????????//?再返回?#{__frch_item_1}?或?#{__frch_itemIndex_1}
      ??????????return?new?StringBuilder("#{").append(newContent).append("}").toString();
      ????????}
      ??????});??????//?拼接SQL
      ??????delegate.appendSql(parser.parse(sql));
      ????}??private?static?String?itemizeItem(String?item,?int?i)?{????return?new?StringBuilder("__frch_").append(item).append("_").append(i).toString();
      ??}
      }

      ForeachSqlNode?

      了解了 ForeachSqlNode? 它的兩個內(nèi)部類之后,再來看看它的實現(xiàn):

      public?class?ForEachSqlNode?implements?SqlNode?{??public?static?final?String?ITEM_PREFIX?=?"__frch_";??//?判斷循環(huán)的終止條件
      ??private?ExpressionEvaluator?evaluator;??//?循環(huán)的集合
      ??private?String?collectionExpression;??//?子節(jié)點
      ??private?SqlNode?contents;??//?開始字符
      ??private?String?open;??//?結(jié)束字符
      ??private?String?close;??//?分隔符
      ??private?String?separator;??//?本次循環(huán)的元素,如果集合為?map,則index?為key,item為value
      ??private?String?item;??//?本次循環(huán)的次數(shù)
      ??private?String?index;??private?Configuration?configuration;??//?...............
      
      ??@Override
      ??public?boolean?apply(DynamicContext?context)?{????//?獲取參數(shù)
      ????Map<String,?Object>?bindings?=?context.getBindings();????final?Iterable<?>?iterable?=?evaluator.evaluateIterable(collectionExpression,?bindings);????if?(!iterable.iterator().hasNext())?{??????return?true;
      ????}????boolean?first?=?true;????//?添加開始字符串
      ????applyOpen(context);????int?i?=?0;????for?(Object?o?:?iterable)?{
      ??????DynamicContext?oldContext?=?context;??????if?(first)?{????????//?如果是集合的第一項,則前綴prefix為空字符串
      ????????context?=?new?PrefixedContext(context,?"");
      ??????}?else?if?(separator?!=?null)?{????????//?如果分隔符不為空,則指定分隔符
      ????????context?=?new?PrefixedContext(context,?separator);
      ??????}?else?{??????????//?不指定分隔符,在默認(rèn)為空
      ??????????context?=?new?PrefixedContext(context,?"");
      ??????}??????int?uniqueNumber?=?context.getUniqueNumber();??
      ??????if?(o?instanceof?Map.Entry)?{????????//?如果集合是map類型,則將集合中的key和value添加到bindings參數(shù)集合中保存
      ????????Map.Entry<Object,?Object>?mapEntry?=?(Map.Entry<Object,?Object>)?o;????????//?所以循環(huán)的集合為map類型,則index為key,item為value,就是在這里設(shè)置的
      ????????applyIndex(context,?mapEntry.getKey(),?uniqueNumber);
      ????????applyItem(context,?mapEntry.getValue(),?uniqueNumber);
      ??????}?else?{????????//?不是map類型,則將集合中元素的索引和元素添加到?bindings集合中
      ????????applyIndex(context,?i,?uniqueNumber);
      ????????applyItem(context,?o,?uniqueNumber);
      ??????}??????//?調(diào)用?FilteredDynamicContext?的apply方法進(jìn)行處理
      ??????contents.apply(new?FilteredDynamicContext(configuration,?context,?index,?item,?uniqueNumber));??????if?(first)?{
      ????????first?=?!((PrefixedContext)?context).isPrefixApplied();
      ??????}
      ??????context?=?oldContext;
      ??????i  ;
      ????}?????//?添加結(jié)束字符串
      ????applyClose(context);????return?true;
      ??}??private?void?applyIndex(DynamicContext?context,?Object?o,?int?i)?{????if?(index?!=?null)?{
      ??????context.bind(index,?o);?//?key為idnex,value為集合元素
      ??????context.bind(itemizeItem(index,?i),?o);?//?為index添加前綴和后綴形成新的key
      ????}
      ??}??private?void?applyItem(DynamicContext?context,?Object?o,?int?i)?{????if?(item?!=?null)?{
      ??????context.bind(item,?o);
      ??????context.bind(itemizeItem(item,?i),?o);
      ????}
      ??}
      }

      所以該例子:

      <select?id="queryPersonByIds"?parameterType="list"?resultMap="queryPersonMap">
      	select?*?from?person?where?1=1	<if?test="ids?!=?null?and?ids.size()?>?0">
      		and?id?in		<foreach?collection="ids"?item="item"?index="index"?separator=","?open="("?close=")">
      			#{item}		</foreach>
      	</if></select>

      解析之后的 SQL 為:

      select? *? from? person where? 1=1 and id?in (#{__frch_item_0},? #{__frch_item_1}, #{__frch_item_2},?#{__frch_item_3},?#{__frch_item_4})

      之后在通過?PreparedStatment 的 setXXX 來進(jìn)行賦值。

      所以,到這里,知道了 Mybatis?在解析 foreach 的時候,最后還是解析成了?#?的方式,但是為什么還是很慢呢,這是因為需要循環(huán)解析?#{__frch_item_0}?之類的占位符,foreach 的集合越大,解析越慢。既然知道了需要解析占位符,為何不自己拼接呢,所以就可以在代碼中拼接好,而不再使用 foreach 啦。

      所以,Mybatis 在解析 foreach 的時候,底層還是會解析成?#?號的形式而不是?$?的形式,既然知道了這個,如果 需要 foreach 的集合很大,就可以使用代碼拼接 SQL ,使用?(#{xxx})?的方式進(jìn)行獲取,不要再拼接成?(1,2,3,4,5)?再使用??${xxx}?的方式啦。


      來源:http://www./content-1-142251.html

        本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多