多線程對JSON數據解析

記錄下自己認爲做過的比較重要的東西
場景是這樣的:
數據庫交易表中每天會產生大量的交易數據,數據爲前端系統傳來(從MQ獲取)。數據格式爲json格式。
有問題的難點:

  • 當客戶表存在已有客戶時,需要更新客戶的最新信息,否則需要將客戶信息落地。
  • 需要調用其他系統接口,時間較長。
    目標:保證落地數據可靠性,提高落地速度。

前期數據落地使用單線程方式,速度很慢 。這時的問題主要是調用接口時間過長。可以考慮在調用接口的時候使用多線程方式。

new thread(()->{
	[調用接口程序]
}).start();

上面的這種方式使用與不需要對返回做出處理。

爲了提高解析效率。考慮將單線程的解析方式換成多線程。大體程序如下:

@Component
public class ParseJSONInfoServiceThread implements InitializingBean {
	private static final Logger logger = LoggerFactory.getLogger(ParseJSONInfoServiceThread.class);
	private static int runnerThread = 0;
	private static int maxThreadNumber = 9;
	private static int errorThreadNumber = 0;
	private byte[] bt = new byte[0];
	//很多依賴不寫了
	// ...........
	
    @Override
	public void afterPropertiesSet() throws Exception {
		logger.info("※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※");
		logger.info("※※※※※※   啓動解析交易表中的前端JSON字符串   ※※※※※※");
		logger.info("※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※");
		new Thread(new Runnable() {

			@Override
			public void run() {
				List<UwTransactionStatus> trans = new ArrayList<UwTransactionStatus>();
				// 實時獲取數據庫中未解析的數據
				while (true) {
					logger.info("[MAIN]: ☆※☆※☆※☆※☆※☆※ start ※☆※☆※☆※☆※☆※☆※☆" + runnerThread + "/" + maxThreadNumber + "/" + errorThreadNumber);
					// 解析任務數量
					int size = trans.size();
					// 當前解析任務數量
					int iTask = 0;
					logger.info("[MAIN]: 從交易表[uw_tansaction_status]中獲取待解析報文數: " + size);
					if (size == 0) {
						logger.info("[MAIN]: 交易表中沒有待解析數據, 休眠10s.");
						try {
							Thread.currentThread().sleep(10000);
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
					// 任務數實時可控
					String threadNumber = nbCommonMapper.querySysVar("MQJSONParseMaxThread");
					if (StringUtil.isEmpty(threadNumber) || "0".equals(threadNumber)) {
						// 如果沒有配置數則設置默認數量9
						logger.info("[MAIN]: 設置解析任務默認數量: "+ threadNumber);
						threadNumber = "9";
					}
					maxThreadNumber = Integer.valueOf(threadNumber);

					// 任務沒有下發結束, 則繼續下發, 任務數量和當前執行任務數是一直增長的. 所有任務分配結束則重新獲取任務
					while (iTask < size) {
						// 沒有分配完則分配, 資源不夠則等待
						if (runnerThread < maxThreadNumber) {
							// 獲取下一個待分配的任務
							UwTransactionStatus tran = trans.get(iTask++);
							UwTransactionStatusExample statusExample = new UwTransactionStatusExample();
							// 已經分配的任務重新設置狀態 Working表示處於解析狀態
							tran.setStatus("W");
							statusExample.createCriteria().andTransactioncodeEqualTo(tran.getTransactioncode());
							exceptionBO.update(tran, statusExample);
							logger.info("[MAIN]: 已分配任務:" + tran.getTransactioncode() + ", 當前數量[" + runnerThread + "], 總數量[" + maxThreadNumber + "]");
							addThread(tran);
						} else {
							try {
								Thread.currentThread().sleep(5000);
							} catch (Exception e) {
								e.printStackTrace();
							}
							logger.info("[MAIN]: 有任務尚未分配,當前任務數" + iTask + ", 任務總數" + size + "; 但資源耗盡!" + "當前數量["
									+ runnerThread + "], 總數量[" + maxThreadNumber + "]");
						}
					}

					// 重新添加任務
					trans = regularsConfigBO.selectJsonInfo(threadNumber);

					logger.info("[MAIN]: ☆※☆※☆※☆※☆※☆※ end ※☆※☆※☆※☆※☆※☆※☆" + runnerThread + "/" + maxThreadNumber + "/" + errorThreadNumber+"\n\n");
				}
			}
		}).start();

	}

	private void addThread(UwTransactionStatus tran) {
		synchronized (bt) {
			runnerThread += 1;
		}
		logger.info(
				"新分配,當前數[" + runnerThread + "], 分配任務, 啓動解析 ||||||||||||| 本次交易流水號爲: " + tran.getTransactioncode());
		ThreadMy threadMy = new ThreadMy();
		threadMy.setTran(tran);
		threadMy.start();
	}

	class ThreadMy extends Thread {
		private UwTransactionStatus tran;

		public UwTransactionStatus getTran() {
			return tran;
		}

		public void setTran(UwTransactionStatus tran) {
			this.tran = tran;
		}

		/**
		 * 啓動後對分配的報文進行解析
		 */
		public void run() {

			logger.info("[" + Thread.currentThread().getId() + "]|||||| 正在執行解析: [" + tran.getTransactioncode() + "]");
			String jsonStr = tran.getRescontent();

			DefaultTransactionDefinition def = new DefaultTransactionDefinition();
			def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
			TransactionStatus status = transactionManager.getTransaction(def);
		
			HashMap<String, String> map = null;
			try {
				// 調用方法出現的異常全部拋出, 進catch
				recivePolicies.convertJSONInfo(jsonStr);
				
				transactionManager.commit(status);

				new Thread(()-> {
                    // [調用其他接口]
				}).start();
                saveLog();
				logger.info("[" + Thread.currentThread().getId()
						+ "] #%#%#%%#%#%%#%%#%%#%%#%#%%#%#%# 結束 #%#%#%%#%#%%#%%#%%#%%#%#%%#%#%#");
			} catch (Exception e) {
                errorThreadNumber++;
				transactionManager.rollback(status);
				saveLog();
			}

			// 結束數量減少1
			synchronized (bt) {
				runnerThread -= 1;
				logger.info("[" + Thread.currentThread().getId() + "]落地程序結束。 當前數量[" + runnerThread + "], 異常數量["+errorThreadNumber+"]");
			}
			
			logger.info("[" + Thread.currentThread().getId()+"]執行結束orzorzorzorzorzorzorzorzorzorzorzorzorzorz");

		}

	}

}

這裏我們使用InitializingBean在服務啓動之後開一個線程專門用於解析落地數據。在這個線程裏開多個解析數據的線程,用於數據落地。
這裏有幾個需要注意的變量,全局變量運行線程數量runnerThread,線程總數maxThreadNumber,異常線程數errorThreadNumber。局部變量任務數size,當前任務數iTask
執行過程:
我們只分析while(true)裏面的邏輯。

  1. 每次循環都會重新從數據庫中獲取最新的線程最大數的配置做到可控。
  2. 如果數據庫交易表中沒有交易數據,則線程休眠。
  3. 如果交易表中有數據,則將獲取的數據分配給解析線程,並更新該數據在交易表中的狀態。

這樣做雖然完成了多線程的解析方式,但是因爲有對數據庫CRUD的操作,多線程很容易導致的死鎖問題出現。通過分析幾次死鎖現象。發現在更新客戶信息是出現死鎖問題最多。
但是操作解析數據中出現同一客戶信息這種現象在業務中並不是常見。因此此處我們使用樂觀鎖更新客戶信息。沒有成功實現更新的線程返回0時,我們拋出異常。

		UwNbCustomerExample customerSelect = new UwNbCustomerExample();
		customerSelect.createCriteria().andCustomercodeEqualTo(customercode);
		List<UwNbCustomer> cuses = uwNbCustomerBO.selectByExample(customerSelect);
		
		long updflag = cuses.get(0).getUpdflag();
		UwNbCustomerExample customerExample = new UwNbCustomerExample();
		customerExample.createCriteria()
			.andCustomercodeEqualTo(customer.getCustomercode())
			.andUpdflagEqualTo(updflag);
		customer.setUpdflag(updflag+1);
		// 很可能存在死鎖問題 沒有成功更新的數據不落地
		int success = uwNbCustomerBO.updateByExampleSelective(customer, customerExample);
		if(success == 0) {
			throw new RuntimeException("更新衝突-當前客戶信息已經被其他線程更新, 該保單需重新落地!");
		}

總之,大體思路是,首先要找到落地慢的具體原因,考慮能否將該部分邏輯與解析邏輯分離。

如果要進一步線程問題,最好的方式,是建立與落地數據節點相同的數據表,將數據落地到這些表中,進行其他業務處理前對數據做提取操作,插入到業務表中。之後進行業務處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章