複雜業務下向Mysql導入30萬條數據代碼優化的踩坑記錄

從畢業到如今第一次接觸到超過30萬條數據導入MySQL的場景(有點low),就是在順豐公司接入我司EMM產品時須要將AD中的員工數據導入MySQL中,所以樓主負責的模塊connector就派上了用場。在樓主的努力下,線上數據同步代碼經歷了從最初的將近16個小時(而且還出現其餘問題這些問題,等後面慢慢細說),到最終25分鐘的性能優化。css

打個廣告,樓主本身造的輪子,感興趣的請點github.com/haifeiWu/li…mysql

代碼直接Jenkins打包上線

樓主負責的connector模塊以前經歷過的最大的數據量也僅僅是幾千條,固然面對幾千條數據代碼也是跑的及其的快,沒有啥影響,然而當第一次在順豐的正式環境上線時,因爲數據量比較大,樓主的代碼又是串行執行的,事務保持的時間就至關長,也就所以出現了下面的錯誤信息:git

Lock wait timeout exceeded; try restarting transaction
複製代碼

這裏來講一下報這個錯的解決方案,查看MySQL是否有鎖github

show OPEN TABLES where In_use > 0;
複製代碼

另外,在information_schema下面有三張表:INNODB_TRXINNODB_LOCKSINNODB_LOCK_WAITS(解決問題方法),經過這三張表,能夠更簡單地監控當前的事務並分析可能存在的問題。web

比較經常使用的列:sql

  • trx_id: InnoDB存儲引擎內部惟一的事物ID
  • trx_status: 當前事務的狀態
  • trx_status: 事務的開始時間
  • trx_requested_lock_id: 等待事務的鎖ID
  • trx_wait_started: 事務等待的開始時間
  • trx_weight: 事務的權重,反應一個事務修改和鎖定的行數,當發現死鎖須要回滾時,權重越小的值被回滾
  • trx_mysql_thread_id: MySQL中的進程ID,與show processlist中的ID值相對應
  • trx_query: 事務運行的SQL語句

kill 進程ID,發生上面錯誤的根本緣由在業務邏輯代碼對數據庫的操做無視了大數據量的狀況,好比當數據量比較大時就會出現剛剛修改完這條記錄,接着再次修改就會出現上述出現的問題。數據庫

代碼優化過程

使用線程池,併發執行,提升效率

因爲數據量比較大,首先想到的方法是拿到數據後將數據分拆成n份,由多個線程併發執行數導入的操做。由此引出多線程的問題,在處理多線程問題時共享的數據結構像Map,List應該採用jdk提供的current包下的數據結構,另外在涉及到操做數據庫的地方應該加鎖,樓主用的是jdk提供的ReentrantLock。使用jdk自帶的 jvisualvm,進行代碼的監控進而找到最佳線程數,下面是監控的數據, 性能優化

jvisualvm-monitor
jvisualvm-thread

下面是線程池的建立代碼bash

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("demo-pool-%d").build();
ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

singleThreadPool.execute(()-> System.out.println(Thread.currentThread().getName()));
singleThreadPool.shutdown();
複製代碼

果不其然,在使用多線程以後,數據插入效率由原來的十幾個小時降到了三個小時,而後並無達到咱們的預期效果,咱們繼續。session

使用druid監控發現問題的SQL

  • 配置druid監控,在應用的web.xml添加配置,並放開/druid的攔截
<!-- druid 數據庫監控 -->
	<filter>
		<filter-name>DruidWebStatFilter</filter-name>
		<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
		<init-param>
			<param-name>exclusions</param-name>
			<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,*.jsp,/druid/*,/download/*</param-value>
		</init-param>
		<init-param>
			<param-name>sessionStatMaxCount</param-name>
			<param-value>2000</param-value>
		</init-param>
		<init-param>
			<param-name>sessionStatEnable</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>principalSessionName</param-name>
			<param-value>session_user_key</param-value>
		</init-param>
		<init-param>
			<param-name>profileEnable</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>DruidWebStatFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<servlet>
		<servlet-name>DruidStatView</servlet-name>
		<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
		<!--<init-param>
          <param-name>allow</param-name>
          <param-value>*.*.*.*</param-value>
        </init-param>-->
		<init-param>
			<!-- 容許清空統計數據 -->
			<param-name>resetEnable</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<!-- 用戶名 -->
			<param-name>loginUsername</param-name>
			<param-value>druid</param-value>
		</init-param>
		<init-param>
			<!-- 密碼 -->
			<param-name>loginPassword</param-name>
			<param-value>druid</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>DruidStatView</servlet-name>
		<url-pattern>/druid/*</url-pattern>
	</servlet-mapping>
複製代碼
  • 配置數據庫鏈接池
<!-- 數據源配置, 使用 BoneCP 數據庫鏈接池 -->
	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
		<!-- 數據源驅動類可不寫,Druid默認會自動根據URL識別DriverClass -->
		<property name="driverClassName" value="${jdbc.driver}" />

		<!-- 基本屬性 url、user、password -->
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />

		<!-- 配置初始化大小、最小、最大 -->
		<property name="initialSize" value="${jdbc.pool.init}" />
		<property name="minIdle" value="${jdbc.pool.minIdle}" />
		<property name="maxActive" value="${jdbc.pool.maxActive}" />

		<!-- 配置獲取鏈接等待超時的時間 -->
		<property name="maxWait" value="60000" />

		<!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="60000" />

		<!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="300000" />

		<property name="validationQuery" value="${jdbc.testSql}" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="true" />
		<property name="testOnReturn" value="false" />

		<!-- 配置監控統計攔截的filters -->
		<property name="filters" value="stat" />
	</bean>
複製代碼
  • 訪問http://ip/xxx/druid,輸入用戶名,密碼登陸就會看到下面的圖

druid

  • 使用druid的SQL監控發現問題SQL語句,優化SQL

經過命令查看程序的gc狀況

經過命令jstat -gc pid 來查看程序的gc狀況,下面是樓主程序數據同步完成以後的gc狀況

gc
簡單說明:

這裏寫代碼片S0C、S1C、S0U、S1U:Survivor 0/1區容量(Capacity)和使用量(Used)
EC、EU:Eden區容量和使用量
OC、OU:年老代容量和使用量
PC、PU:永久代容量和使用量
YGC、YGT:年輕代GC次數和GC耗時
FGC、FGCT:Full GC次數和Full GC耗時
複製代碼

小結

經過上面的優化過程,樓主的connector的數據同居效率也由小時級降低到了分鐘級,同步30+萬的時間能夠在25分鐘內完成。經此一役,樓主算是收穫滿滿。抱着虔誠的心態學習,不浮不躁,快樂成長。

相關文章
相關標籤/搜索