记录下自己认为做过的比较重要的东西
场景是这样的:
数据库交易表中每天会产生大量的交易数据,数据为前端系统传来(从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)里面的逻辑。
- 每次循环都会重新从数据库中获取最新的线程最大数的配置做到可控。
- 如果数据库交易表中没有交易数据,则线程休眠。
- 如果交易表中有数据,则将获取的数据分配给解析线程,并更新该数据在交易表中的状态。
这样做虽然完成了多线程的解析方式,但是因为有对数据库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("更新冲突-当前客户信息已经被其他线程更新, 该保单需重新落地!");
}
总之,大体思路是,首先要找到落地慢的具体原因,考虑能否将该部分逻辑与解析逻辑分离。
如果要进一步线程问题,最好的方式,是建立与落地数据节点相同的数据表,将数据落地到这些表中,进行其他业务处理前对数据做提取操作,插入到业务表中。之后进行业务处理。