原文引自:http://www.bea.com.cn/support_pattern/JDBC_Causes_Server_Hang_Pattern.html
問題描述 在通過由應(yīng)用程序或 WebLogic Server 本身使用的 JDBC 連接進(jìn)行調(diào)用時(shí),此連接會(huì)在整個(gè)調(diào)用期間內(nèi)阻塞一個(gè) WebLogic Server 執(zhí)行線程。盡管在 SQL 查詢上阻塞的線程需要等待,但 JVM 將通過其線程調(diào)度機(jī)制確保 CPU 獲得可運(yùn)行線程。但是,由 JDBC 調(diào)用占用的線程將保留給應(yīng)用程序使用,直至該調(diào)用從 SQL 查詢返回。 即使事務(wù)超時(shí)也不會(huì)終止由在此事務(wù)中登記的資源完成的任何操作,或者使其超時(shí)。這些操作將正常地運(yùn)行,而不會(huì)出現(xiàn)中斷。事務(wù)超時(shí)只是在事務(wù)上設(shè)置一個(gè)標(biāo)記,將其標(biāo)記為回滾,這樣提交此事務(wù)的任何后續(xù)請(qǐng)求都將失敗,系統(tǒng)拋出 TimedOutException 或 RollbackException。但是,如前所述,長時(shí)間運(yùn)行的 JDBC 調(diào)用會(huì)導(dǎo)致 WebLogic Server 執(zhí)行線程阻塞,如果所有線程均被阻塞,沒有能夠處理傳入請(qǐng)求執(zhí)行線程,則最終可導(dǎo)致實(shí)例掛起。 最新版本的 WebLogic Server 具有健全性檢查功能,能夠定期檢查線程不響應(yīng)的時(shí)間是否達(dá)到特定時(shí)長(缺省值為 600 秒)。如果是這樣,系統(tǒng)會(huì)向日志文件輸出一條如下的錯(cuò)誤消息: |
####<Nov 6, 2004 1:42:30 PM EST> <Warning> <WebLogicServer> <mydomain> <myserver> <CoreHealthMonitor> <kernel identity> <> <000337> <ExecuteThread: '64' for queue: 'default' has been busy for "740" seconds working on the request "Scheduled Trigger", which is more than the configured time (StuckThreadMaxTime) of "600" seconds.> |
這并不會(huì)中斷線程,而只是提供給管理員的一項(xiàng)通知。阻塞線程恢復(fù)為正常狀態(tài)的唯一途徑是等待它正處理的請(qǐng)求完成。這種情況下,WebLogic Server 日志文件中將出現(xiàn)一條如下的消息: |
####<Nov 7, 2004 4:17:34 PM EST> <Info> <WebLogicServer><mydomain> <myserver> <ExecuteThread: '66' for queue: 'default'> <kernel identity> <> <000339> <ExecuteThread: '66' for queue: 'default' has become "unstuck".> |
健康檢查功能的時(shí)間間隔是可以配置的。請(qǐng)檢查 config.xml 文件 <Server> 標(biāo)記中的 StuckThreadMaxTime 屬性:http://e-docs.bea.com/wls/docs81/config_xml/Server.html#StuckThreadMaxTime (English) 或 WebLogic Server 管理控制臺(tái)幫助中的“Detecting stuck threads”一節(jié):http://e-docs.bea.com/wls/docs81/perform/WLSTuning.html#stuckthread (English)。 返回頁首 故障排除 不同的編程技術(shù)或 JDBC 連接池配置可以造成死鎖或長時(shí)間運(yùn)行的 JDBC 調(diào)用,進(jìn)而使 WebLogic Server 實(shí)例掛起。常規(guī)服務(wù)器掛起模式中提供了有關(guān)如何排除和分析掛起 WebLogic Server 實(shí)例的一般信息。 本模式討論導(dǎo)致服務(wù)器掛起的 JDBC 調(diào)用,并從 JDBC 角度討論導(dǎo)致 WebLogic Server 實(shí)例掛起的常見問題的原因。本模式中引用的其它支持模式位于 WebLogic Server Support Patterns Site (English)。 快速鏈接 為什么發(fā)生此問題? 以下是在 JDBC 調(diào)用時(shí)導(dǎo)致 WebLogic Server 實(shí)例掛起的各種可能原因:
同步的 DriverManager.getConnection() 舊版本的 JDBC 應(yīng)用程序代碼有時(shí)使用 DriverManager.getConnection() 調(diào)用來通過特定驅(qū)動(dòng)程序取得數(shù)據(jù)庫連接。不建議使用此項(xiàng)技術(shù),因?yàn)樗赡軙?huì)導(dǎo)致死鎖,或者至少相對(duì)降低連接請(qǐng)求的性能。究其原因,是因?yàn)樗?DriverManager 調(diào)用都采用類同步模式,也就是說一個(gè)線程中的一個(gè) DriverManager 調(diào)用將阻塞一個(gè) WebLogic Server 實(shí)例內(nèi)任何其它線程中的所有其它 DriverManager 調(diào)用。 此外,SQLException 的構(gòu)造器會(huì)構(gòu)造一個(gè) DriverManager 調(diào)用,而且大多數(shù)驅(qū)動(dòng)程序使用 DriverManager.println() 調(diào)用進(jìn)行日志記錄,這其中的任何一項(xiàng)都會(huì)阻塞發(fā)出 DriverManager 調(diào)用的所有其它線程。 DriverManager.getConnection() 在返回為數(shù)據(jù)庫建立的物理連接之前可能會(huì)需要相對(duì)較長的時(shí)間。即使不發(fā)生死鎖,所有其它調(diào)用也需要等到這個(gè)線程獲得連接。在像 WebLogic Server 這樣的多線程系統(tǒng)中,這不是最佳的方式。 |
此處的信息來自 http://forums.bea.com/bea//thread.jspa?forumID=2022&threadID=200063365&message ID=202311284&start=-1#202311284 (English)。 |
此外,我們的文檔也明確指出不應(yīng)使用 DriverManager.getConnection():http://e-docs.bea.com/wls/docs81/faq/jdbc.html#501044 (English)。 如果愿意在 JDBC 代碼中使用 JDBC 連接,應(yīng)使用 WebLogic Server JDBC 連接池,為其定義一個(gè)數(shù)據(jù)源,并從此數(shù)據(jù)源獲得連接。這樣您將享有池的所有優(yōu)點(diǎn)(資源共享、連接重用、數(shù)據(jù)庫關(guān)閉后的連接刷新等等)。它還將幫助您避免 DriverManager 調(diào)用可能發(fā)生的死鎖。有關(guān)如何使用 JDBC 連接池、數(shù)據(jù)源及 WebLogic Server 中的其它 JDBC 對(duì)象的詳細(xì)信息,請(qǐng)參閱:http://e-docs.bea.com/wls/docs81/jdbc/intro.html#1036718 (English) 和 http://e-docs.bea.com/wls/docs81/jdbc/programming.html#1054307 (English)。 在 DriverManager.getConnection() 調(diào)用中阻塞的典型線程如下: |
"ExecuteThread-39" daemon prio=5 tid=0x401660 nid=0x33 waiting for monitor entry [0xd247f000..0xd247fc68] ? at java.sql.DriverManager.getConnection(DriverManager.java:188) ? at com.bla.updateDataInDatabase(MyClass.java:296) ? at javax.servlet.http.HttpServlet.service(HttpServlet.java:865) ? at weblogic.servlet.internal.ServletStubImpl.invokeServlet (ServletStubImpl.java:120) ? at weblogic.servlet.internal.ServletContextImpl.invokeServlet (ServletContextImpl.java:945) ? at weblogic.servlet.internal.ServletContextImpl.invokeServlet (ServletContextImpl.java:909) ? at weblogic.servlet.internal.ServletContextManager.invokeServlet (ServletContextManager.java:269) ? at weblogic.socket.MuxableSocketHTTP.invokeServlet (MuxableSocketHTTP.java:392) ? at weblogic.socket.MuxableSocketHTTP.execute(MuxableSocketHTTP.java:274) ? at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:130) |
返回頁首 長時(shí)間運(yùn)行的 SQL 查詢 長時(shí)間運(yùn)行的 SQL 查詢?cè)谄鋱?zhí)行期間將阻塞執(zhí)行線程,直至它們將結(jié)果返回給發(fā)出調(diào)用的應(yīng)用程序。這就意味著,需要修改 WebLogic Server 實(shí)例的配置來處理應(yīng)用程序負(fù)載要求的足夠多的調(diào)用。這種情況的限制因素是執(zhí)行線程數(shù)和 JDBC 連接池中的連接數(shù)。一般的經(jīng)驗(yàn)方法是將池中的連接數(shù)設(shè)置為等于執(zhí)行線程數(shù),以便能夠?qū)崿F(xiàn)最優(yōu)的資源利用。如果使用 JTS,則池中的可用連接應(yīng)更多一些,因?yàn)槟承┻B接可能會(huì)保留給實(shí)際處于非活動(dòng)狀態(tài)的事務(wù)。 對(duì)于在長時(shí)間運(yùn)行的 SQL 調(diào)用期間掛起的線程,其在 Thread Dump 中的堆棧與掛起的數(shù)據(jù)庫的堆棧十分相似。有關(guān)詳細(xì)信息,請(qǐng)對(duì)比下一小節(jié)的內(nèi)容。 掛起的數(shù)據(jù)庫 對(duì)于依賴于數(shù)據(jù)庫的應(yīng)用程序來說,良好的數(shù)據(jù)庫性能是其性能的關(guān)鍵。因此,掛起的數(shù)據(jù)庫可能會(huì)阻塞 WebLogic Server 實(shí)例中許多或所有可用的執(zhí)行線程并最終導(dǎo)致服務(wù)器掛起。要診斷這一問題,應(yīng)從掛起的 WebLogic Server 實(shí)例獲得 5 到 10 個(gè) Thread Dump,并檢查您的執(zhí)行線程(在缺省隊(duì)列或您的應(yīng)用程序線程隊(duì)列中)當(dāng)前是否在 SQL 調(diào)用之中并在等待來自數(shù)據(jù)庫的結(jié)果。當(dāng)前發(fā)出 SQL 查詢的線程的典型堆棧跟蹤如下例所示: |
"ExecuteThread: '4' for queue: 'weblogic.kernel.Default'" daemon prio=5 tid=0x8e93c8 nid=0x19 runnable [e137f000..e13819bc] ? at java.net.SocketInputStream.socketRead0(Native Method) ? at java.net.SocketInputStream.read(SocketInputStream.java:129) ? at oracle.net.ns.Packet.receive(Unknown Source) ? at oracle.net.ns.DataPacket.receive(Unknown Source) ? at oracle.net.ns.NetInputStream.getNextPacket(Unknown Source) ? at oracle.net.ns.NetInputStream.read(Unknown Source) ? at oracle.net.ns.NetInputStream.read(Unknown Source) ? at oracle.net.ns.NetInputStream.read(Unknown Source) ? at oracle.jdbc.ttc7.MAREngine.unmarshalUB1(MAREngine.java:931) ? at oracle.jdbc.ttc7.MAREngine.unmarshalSB1(MAREngine.java:893) ? at oracle.jdbc.ttc7.Oall7.receive(Oall7.java:375) ? at oracle.jdbc.ttc7.TTC7Protocol.doOall7(TTC7Protocol.java:1983) ? at oracle.jdbc.ttc7.TTC7Protocol.fetch(TTC7Protocol.java:1250) ? - locked <e8c68f00> (a oracle.jdbc.ttc7.TTC7Protocol) ? at oracle.jdbc.driver.OracleStatement.doExecuteQuery(OracleStatement.java:2529) ? at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:2857) ? at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate (OraclePreparedStatement.java:608) ? - locked <e5cc44d0> (a oracle.jdbc.driver.OraclePreparedStatement) ? - locked <e8c544c8> (a oracle.jdbc.driver.OracleConnection) ? at oracle.jdbc.driver.OraclePreparedStatement.executeQuery (OraclePreparedStatement.java:536) ? - locked <e5cc44d0> (a oracle.jdbc.driver.OraclePreparedStatement) ? - locked <e8c544c8> (a oracle.jdbc.driver.OracleConnection) ? at weblogic.jdbc.wrapper.PreparedStatement.executeQuery(PreparedStatement.java:80) ? at myPackage.query.getAnalysis(MyClass.java:94) ? at jsp_servlet._jsp._jspService(__jspService.java:242) ? at weblogic.servlet.jsp.JspBase.service(JspBase.java:33) ? at weblogic.servlet.internal.ServletStubImpl$ServletInvocationAction.run (ServletStubImpl.java:971) ? at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:402) ? at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:305) ? at weblogic.servlet.internal.RequestDispatcherImpl.includ e(RequestDispatcherImpl.java:607) ? at weblogic.servlet.internal.RequestDispatcherImpl.include (RequestDispatcherImpl.java:400) ? at weblogic.servlet.jsp.PageContextImpl.include(PageContextImpl.java:154) ? at jsp_servlet._jsp.__mf1924jq._jspService(__mf1924jq.java:563) ? at weblogic.servlet.jsp.JspBase.service(JspBase.java:33) ? at weblogic.servlet.internal.ServletStubImpl$ServletInvocationAction.run (ServletStubImpl.java:971) ? at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:402) ? at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:305) ? at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run (WebAppServletContext.java:6350) ? at weblogic.security.acl.internal.AuthenticatedSubject.doAs (AuthenticatedSubject.java:317) ? at weblogic.security.service.SecurityManager.runAs(SecurityManager.java:118) ? at weblogic.servlet.internal.WebAppServletContext.invokeServlet (WebAppServletContext.java:3635) ? at weblogic.servlet.internal.ServletRequestImpl.execute(ServletRequestImpl.java:2585) ? at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:197) ? at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:170) |
線程將處于運(yùn)行狀態(tài)。您應(yīng)比較不同 Thread Dump 中的線程,查看它們是否及時(shí)接收 SQL 調(diào)用的返回結(jié)果或者它們是否在此同一調(diào)用中長時(shí)間掛起。如果 Thread Dump 似乎指示 SQL 調(diào)用的響應(yīng)時(shí)間較長,則應(yīng)檢查相應(yīng)的數(shù)據(jù)庫日志,查看是不是數(shù)據(jù)庫中的問題導(dǎo)致這種執(zhí)行速度緩慢或掛起的狀況。 返回頁首 低速網(wǎng)絡(luò) WebLogic Server 與數(shù)據(jù)庫之間的通信依賴于性能良好且可靠的網(wǎng)絡(luò),來及時(shí)地處理請(qǐng)求。因此,網(wǎng)絡(luò)性能低下可導(dǎo)致正在等待 SQL 查詢結(jié)果的執(zhí)行線程被掛起或阻塞。相關(guān)的堆棧跟蹤將與上面掛起的數(shù)據(jù)庫小節(jié)中的示例相似。僅僅通過分析 WebLogic Server Thread Dump 不可能找到掛起或 SQL 查詢速度低下的根本原因。它們給出 SQL 調(diào)用的性能存在問題的第一個(gè)提示。下一步是檢查是否存在導(dǎo)致 SQL 調(diào)用性能不佳的數(shù)據(jù)庫網(wǎng)絡(luò)或網(wǎng)絡(luò)問題。 死鎖 應(yīng)用程序級(jí)的死鎖與數(shù)據(jù)庫級(jí)的死鎖都可導(dǎo)致線程掛起。您應(yīng)檢查 Thread Dump,查看是否存在應(yīng)用程序級(jí)的死鎖。有關(guān)如何執(zhí)行這一操作的信息在服務(wù)器掛起 - 應(yīng)用程序死鎖模式中提供。數(shù)據(jù)庫死鎖可以在數(shù)據(jù)庫日志中檢測,或者通過可在 WebLogic Server 日志文件中找到的“SQL 異常”檢測。下面是相關(guān)“SQL 異常”的一個(gè)示例: |
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resource ? at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:170) ? at oracle.jdbc.oci8.OCIDBAccess.check_error(OCIDBAccess.java:1614) ? at oracle.jdbc.oci8.OCIDBAccess.executeFetch(OCIDBAccess.java:1225) ? at oracle.jdbc.oci8.OCIDBAccess.parseExecuteFetch(OCIDBAccess.java:1338) ? at oracle.jdbc.driver.OracleStatement.executeNonQuery(OracleStatement.java:1722) ? at oracle.jdbc.driver.OracleStatement.doExecuteOther(OracleStatement.java:1647) ? at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout (OracleStatement.java:2167) ? at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate (OraclePreparedStatement.java:404) |
數(shù)據(jù)庫檢測死鎖并通過回滾引發(fā)死鎖的一個(gè)或多個(gè)事務(wù)來解決死鎖這一過程通常需要一些時(shí)間,因此在回滾結(jié)束之前,會(huì)有一個(gè)或多個(gè)執(zhí)行線程被阻塞。 RefreshMinutes 或 TestFrequencySeconds 如果發(fā)現(xiàn)數(shù)據(jù)庫性能低下、SQL 調(diào)用速度緩慢或連接高峰的時(shí)期反復(fù)出現(xiàn),其原因可能在于 JDBC 連接池中 RefreshMinutes 或 TestFrequencySeconds配置屬性的設(shè)置。有關(guān)內(nèi)容在探查 JDBC 故障模式中詳細(xì)介紹。除非 WebLogic Server 實(shí)例與數(shù)據(jù)庫之間沒有防火墻,否則您應(yīng)禁用此功能。 池收縮 數(shù)據(jù)庫的物理連接是應(yīng)當(dāng)打開一次并盡可能長時(shí)間保持打開的資源,因?yàn)樾碌倪B接請(qǐng)求對(duì)于數(shù)據(jù)庫、操作系統(tǒng)內(nèi)核及 WebLogic Server 而言是一個(gè)相當(dāng)大的資源開銷。因此,應(yīng)在生產(chǎn)系統(tǒng)中禁用池收縮,使這一開銷保持最小程度。如果啟用池收縮,一旦對(duì)該池發(fā)出的連接請(qǐng)求不能得到滿足,空閑的池連接就將被關(guān)閉,然后重新打開。 這些活動(dòng)可能會(huì)需要一些時(shí)間,因此相關(guān)應(yīng)用程序請(qǐng)求需要的時(shí)間可能會(huì)異常的長,使用戶以為系統(tǒng)掛起。有關(guān)如何優(yōu)化 JDBC 連接池配置的信息在探查 JDBC 故障模式中提供。 返回頁首 分析掛起的 WebLogic Server 實(shí)例 有關(guān)如何分析掛起的 WebLogic Server 實(shí)例的一般信息在常規(guī)服務(wù)器掛起模式中提供。 大多數(shù)情況下,首先從掛起的系統(tǒng)獲得 Thread Dump 對(duì)于了解進(jìn)展情況(例如不同的線程在做些什么以及它們?yōu)槭裁磼炱穑┦欠浅S幸娴摹Mǔ#梢栽谏a(chǎn)系統(tǒng)上獲得 Thread Dump,但是對(duì)于很早以前的 JVM 版本 (<1.3.1_09) 則應(yīng)小心,因?yàn)樗鼈兛赡軙?huì)在 Thread Dump 期間崩潰。此外,如果 WebLogic Server 實(shí)例有大量線程,則意味著完成 Thread Dump 需要一段時(shí)間,而其余線程將被阻塞。 請(qǐng)進(jìn)行多個(gè) Thread Dump(5 到 10 個(gè)),這些 Thread Dump 彼此之間有若干秒鐘的延遲。這使得您可以檢查不同進(jìn)程的進(jìn)度情況。而且,它還將指示系統(tǒng)是否確實(shí)掛起(根本沒有進(jìn)度)或者吞吐速度是否極低,看起來像是系統(tǒng)已掛起。 有關(guān)如何進(jìn)行 Thread Dump 的信息在“常規(guī)服務(wù)器掛起”支持模式或我們的文檔中提供:http://e-docs.bea.com/wls/docs81/cluster/trouble.html (English)。 此外,還請(qǐng)檢查是整個(gè) WebLogic Server 實(shí)例掛起還是應(yīng)用程序掛起。“常規(guī)服務(wù)器掛起”支持模式也包括此信息。 分析 Thread Dump 可以指示出實(shí)例的掛起是否確實(shí)是由于前一小節(jié)為什么發(fā)生此問題?中提到的某一種原因。例如,如果所有線程都在一個(gè) DriverManager 方法(如 getConnection())中,則您已經(jīng)確定出掛起的根本原因,并需要更改應(yīng)用程序,以使用數(shù)據(jù)源或 Driver.connect() 來代替 DriverManager.getConnection()。 Samurai 是一個(gè)非常有用的工具,可用于分析 Thread Dump 并監(jiān)視不同 Thread Dump 之間線程的進(jìn)度。可從 dev2dev 下載此工具,網(wǎng)址:http://dev2dev.bea.com/resourcelibrary/utilitiestools/adminmgmt.jsp (English)。 dev2dev 上有關(guān)分析 Thread Dump 的白皮書:http://dev2dev.bea.com/products/wlplatform81/articles/thread_dumps.jsp (English) 也有助于深入探究 Thread Dump,以了解有關(guān)服務(wù)器掛起的更多內(nèi)容。 返回頁首 優(yōu)化 JDBC 代碼和 JDBC 連接池配置的技巧 對(duì)于開發(fā) JDBC 代碼和配置 JDBC 連接池,有一些最佳的慣例,可以幫助避免常見問題并優(yōu)化資源利用,避免服務(wù)器實(shí)例掛起。 JDBC 編程 為了優(yōu)化 WebLogic Server 中資源的利用,節(jié)約數(shù)據(jù)庫資源,應(yīng)使用 JDBC 連接池進(jìn)行應(yīng)用程序的 JDBC 調(diào)用。在應(yīng)用程序代碼中建立和破壞的連接會(huì)造成不必要的開銷,應(yīng)當(dāng)避免。要獲得 JDBC 編程的一般文檔,請(qǐng)參閱:http://e-docs.bea.com/wls/docs81/jdbc/rmidriver.html#1028977 (English)。此外,有關(guān) JDBC 性能調(diào)整的詳細(xì)信息位于:http://e-docs.bea.com/wls/docs81/jdbc/performance.html#1027791 (English)。 可以在 dev2dev Java Database Connectivity (English) 頁上查看有關(guān) JDBC 的綜合性信息,它有助于優(yōu)化 JDBC 代碼和 JDBC 資源的利用,網(wǎng)址為:http://dev2dev.bea.com/technologies/jdbc/index.jsp (English)。 JDBC 連接池配置 關(guān)于如何針對(duì)生產(chǎn)環(huán)境配置連接池的建議,請(qǐng)參閱探查 JDBC 故障模式。為避免出現(xiàn)掛起或者性能不佳的情況,應(yīng)考慮這些配置技巧。 |
返回頁首 已知問題 您可以定期查看所用 WLS 版本的“Release Notes”,了解 Service Pack 中的“Known Issues”或“Resolved Issues”的詳細(xì)信息及瀏覽與 JDBC 服務(wù)器掛起有關(guān)的問題。方便起見,下面提供了這些發(fā)行說明的鏈接: 請(qǐng)注意,WLS 8.1 SP3 中已經(jīng)進(jìn)行一些更改來解決 CR134921,其中對(duì)于特定的 JDBC 連接,用于回滾事務(wù)的調(diào)用沒有立即處理,因?yàn)轵?qū)動(dòng)程序必須等待任何當(dāng)前正執(zhí)行的語句返回。 使用搜索功能也可以搜索到“Release Notes”,還可以搜索到其它支持解決辦法及與 CR 有關(guān)的信息,如需要更多幫助?中所提到的內(nèi)容。如果客戶簽訂了技術(shù)支持合同,則可以登錄 http://support.bea.com/,登錄后會(huì)看到為 Solutions 和 Bug Central 提供的 Browse portlet,可在其中按產(chǎn)品版本瀏覽最新提供的 CR。 |
需要更多幫助? 如果您已經(jīng)理解這個(gè)模式,但仍需要更多幫助,您可以:
|