前提:接到任务。最近有个三方回调接口超时,这个接口要做的就是对已接入系统的子公司的数据处理,后面的公司会接入更多,接口也会更慢。这个项目本身比较简单,所以就是一个单体项目,也没有引入mq。但也是部署多个节点。
简单的功能优化就分为下面两个:
1:先把每个节点利用起来(之前是接口请求所在节点循环每个公司做数据处理),然后异步执行,不要让回调接口等任务执行完再返回,并且立即返回成功。不太愿意为了这一个功能再去引入mq(申请资源,走流程,运维什么的)
2:开始执行接口和执行结束要收到成功或者失败的消息。
功能1的实现,虽然没有mq,但是有redis。这里就使用了redis做广播消息,每个节点收到消息后,同时去 list 里循环拿数据,放入本地线程池执行。(也可以用redis的 stream 实现)
redis 广播代码比较简单就不解释了。
功能2就麻烦点了。因为多节点,多线程,总体来说得有一个地方记录当前的状态。那就用现成的redis工具 redisson 记录状态呗,于是就有了方式1.
方式1:当时想着,redis记录要有个超时时间,开始执行任务就记录一下 +1 ,退出任务就 -1。是不是想到了弄一个 flag 在redis,然后对它进行操作。但是我当时呢想到懒得自己写,用现成的轮子就行了,就是:RReadWriteLock。redisson 的读写锁。(redisson普通的锁不行,因为只有当前线程可重入)
实现方式就是,开始执行任务时加读锁,因为读锁共享,线程结束先解读锁,然后 尝试加写锁 用 tryLock,由于读写互斥,写锁尝试加锁成功就表示没有读锁了,自然就是所有任务已经结束。
代码我都写了一半,才想通自己犯傻了,如果任务比线程多,正在执行的任务同时结束,还没来得及从队列取下一个任务,这时读锁还是全部解锁了,会误判全部任务结束。比如下面的模拟代码,把线程池改小,会出现多次 下面的这句输出语句:
写锁是否加锁成功: true
@Test
public void testLock() throws InterruptedException {
//这里修改线程数
ExecutorService executorService = Executors.newFixedThreadPool(6);
RReadWriteLock rwLock = redissonClient.getReadWriteLock("test_lock");
RLock rlock = rwLock.readLock();
RLock wlock = rwLock.writeLock();
System.out.println("开始");
for (int i = 0; i < 6; i++) {
executorService.execute(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
rlock.lock(10, TimeUnit.SECONDS);
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
rlock.unlock();
boolean b = wlock.tryLock();
if (b) {
wlock.unlock();
}
System.out.println("写锁是否加锁成功: " + b);
});
}
LockSupport.park();
}
上面的问题,就需要多一个步骤,就是再判断一下线程池的队列和redis的list 是不是还有任务。有的话就不算结束!
但是感觉这样写太麻烦了,所以放弃,下有一个思路
方式2:redis中记录一个falag,广播前就先把flag设置为任务总数。然后每个线程执行完后,flag 减一。不能直接l用下面的两行代码,非原子性(否则加分布式锁)
int coun = redis.get("key");
redis.set(count - 1);
直接使用 redis 的 decr 命令。当flag <= 0 就可以发送消息了。
方式3:服务宕机怎么办?用上面方法的话,一直都不会等于0,导致一直收不到消息。
所以这时我想着是做一个心跳,后台线程定时发送心跳或者每完成一个任务就发送一次心跳。
然后在redis中记录每个节点的 ip 和 端口,以及每个节点完成的任务情况,当前节点全部任务结束后就去检查下是否每个节点都完成,比如设置等待30秒超时,节点完成任务就每5秒循环一次看下是不是所有节点都完成了任务,直到所有节点已完成或者已下线或者到了超时时间,就再用redis做个标记,记录当前节点已发送消息,其他节点发送前判断一下就没必要发消息了。任务重新开始时清理这个标记,最好也设置一个长一点的超时时间,双保险。每个在线的节点都需要做前面描述的判断,这样除非全部下线,,,否者总有一个能有机会发出这个消息。
还需要小心一点的就是,尽量不要用一个json的字符串去保存节点的信息,否则就又出现原子性问题(可以加锁解锁),我想着直接存一个hash,每个节点的每个线程各自更新数据,
所以用 redis 的 hsh 做成大概下面这种数据结。这样每个线程只更新自己的数据,就可以避免并发导致的数据混。
最后发送消息内容展示为:xx数据处理完成,成功xx个,失败xx个,失败原因。。。其中xx节点下线导致xx卫执行。
其他细节,接口和任务的幂等,防重,redis中的数据及时清理等就不写了。