-
场景:
每条消息内容假设占内存1KB(约512个汉字),100W用户的消息同时放入内存约占976.56MB。-----大消息
每条消息内容假设占内存0.12KB(约58个汉字),100W用户的消息同时放入内存约占107.56MB。-----本次需求消息(温馨提示:上课提醒有助于养成良好学习习惯哦~\r\n同时为了避免干扰\r\n上课消息提醒只会在每周一、三、五推送哦~)
-
服务器相关:
线上内存配置:
流量产品tomcat最大内存:6G
beanstalk内存:7G
线上cpu个数:
流量产品:4核心CPU
-
线程池关注点:
1、CPU密集型任务:
线程数公式:CPU个数+1
2、IO密集型任务:
线程数公式:CPU核心数/(1- 阻塞系数在0到1范围内)
对于IO密集型任务,需要创建比cpu核心数大N倍的线程数。当一个任务执行IO操作时,线程被阻塞,处理器可以处理其他就绪线程,假如只有少量线程的话,即使有待执行的任务也无法调度处理了。
3、对于本次100W用户数据
按照40个线程数、50个线程数、100个线程数依次测试
4、假设用固定线程池设置100线程
内存占用为100MB,对于服务器来讲没有压力。
5、需要考虑单线程超时时间,避免单线程耗时过长
6、需要考虑线程池中的队列
无界队列,需要保证吞吐量很高,并配合超时时间,否则容易出问题。
-
beanstalk关注点(轻量级MQ):
每分钟扫描一次beanstalk队列中符合条件的消息列表,逐条处理业务代码(同步调用),处理完一条消息,就从beanstalk中移除该条消息。
-
方案1(分页处理+多线程容器)--最终方案:
1、指定时间触发定时任务,
2、通过分页拉取符合条件的数据
3、通过线程池,异步处理业务同时发消息给微信
4、直至没有符合条件的数据,退出任务
优势:内存占用较小,不依赖于外部组件,无消息堆积风险(曾用此方式处理过唯品会线上数据同步,单任务24小时5亿数据,对现有业务和机器没有影响)
-
方案2(定时任务+beanstalk延时队列):
1、每天跑一次需要发送的消息,一次性全部放入beanstalk延时队列
2、beanstalk延时队列触发,获取符合条件的任务列表
3、遍历任务列表,放入线程池中,异步处理消息,使消息快速从延时队列内存中删除,减轻延时队列压力
4、异步处理业务和发消息给微信,成功-直接返回,失败-将消息重新写入延时队列
优势:通过线程池加速消息吞吐能力
劣势:消息堆积过大,超过700w条大消息或7000W条小消息,内存可能会爆掉
-
后期优化方向:
1、扩大tomcat数据库连接池最大连接数
2、扩大定时任务中线程池容器的线程数
3、优化内部微信相关程序(因为流量其实先到公共这块,再由公共这块调用微信接口)
3.1、调整线程数
3.2 、提升吞吐量,对于消息区分类型(同步or异步),异步的直接返回即可
-
核心代码:
/**
* 功 能:用户提醒定时任务
* 作 者:java潇邦
* 时 间:2018/12/19
*/
@Service
public class UserTaskImpl implements UserRemindTask {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource(name = "jedisClusterClient")
JedisClusterClient redisService;
@Resource
private UserMapper userMapper;
@Autowired
private AbstractBatchDataService realBussinessServiceImpl;
/**
* 用户提醒任务
*/
@Scheduled(cron = "0 0 6,7,8,18,19,20 ? * MON,WED,FRI")
@Override
public void userRemind() {
String time = DateUtil.getHh() + ":00";
String redisKey = UserConstants.USER_TASK_MSG_TIME_KEY + DateUtil.getCurrHour();
boolean flag = redisService.setNX(redisKey, time, UserConstants.PHONICS_TASK_TIME);
if (!flag) {
logger.info("用户提醒任务已执行,redisKey={}", redisKey);
return;
}
UserInfo queryParam = new UserInfo(time);
//这里开始是重点
realBussinessServiceImpl.execute(new QueryDataCallBack<UserInfo, Pager>() {
@Override
public int getListCount(UserInfo queryParam) {
return userMapper.getListCount(queryParam);
}
@Override
public List<Map<String, Object>> getList(UserInfo queryParam, Pager pager) {
return userMapper.getList(queryParam, pager);
}
}, queryParam);
}
}
package com.task.service.common.impl;
import com.task.entity.common.Pager;
import com.task.service.common.QueryDataCallBack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicLong;
/**
* 功 能:批处理
* 作 者:java潇邦
* 时 间:2018/12/19
*/
@Component
public abstract class AbstractBatchDataService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public static final int PAGE_SIZE = 1000;
@Autowired
private BatchContainerExecutor batchContainerExecutor;
@Autowired
private BatchContainerExecutorBig batchContainerExecutorBig;
/**
* 这是模板方法内的公用逻辑不用改动
* 1.查询出待处理数据的条数
* 2.将数据分成N页(批次),每页取指定条数
* 3.将每页的数据放入批处理容器
* 4.按照批次顺序依次执行
* 5.直至处理完成
*/
public <T> long execute(QueryDataCallBack queryDataCallBack, T queryParam) {
logger.info("批处理总执行开始");
long begin = System.currentTimeMillis();
int totalCount = queryDataCallBack.getListCount(queryParam);
if (totalCount == 0) {
logger.info("批处理总执行结束,执行耗时[{}]秒,处理[{}]条记录", (System.currentTimeMillis() - begin) / 1000.0, 0);
return totalCount;
}
AtomicLong atomicLong = new AtomicLong(0);
int batchNum = (totalCount % PAGE_SIZE) == 0 ? totalCount / PAGE_SIZE : totalCount / PAGE_SIZE + 1;
for (int pageNum = 1; pageNum <= batchNum; pageNum++) {
int startLine = (pageNum - 1) * PAGE_SIZE;
List<Callable<Object>> threadList = new ArrayList<>(batchNum);
threadList.add(new Callable() {
@Override
public Object call() throws Exception {
long num = executeSub(queryDataCallBack.getList(queryParam, new Pager(startLine, PAGE_SIZE)));
return atomicLong.addAndGet(num);
}
});
batchContainerExecutor.execute(threadList);//加速处理任务
}
logger.info("批处理总执行结束,执行耗时[{}]秒,处理[{}]条记录", (System.currentTimeMillis() - begin) / 1000.0, atomicLong.get());
return atomicLong.get();
}
private long executeSub(List<Map<String, Object>> dataList) {
long begin = System.currentTimeMillis();
logger.info("批处理单次执行开始");
if (CollectionUtils.isEmpty(dataList)) {
logger.info("批处理单次执行结束,执行耗时[{}]秒,处理[{}]条记录", (System.currentTimeMillis() - begin) / 1000.0, 0);
return 0;
}
List<Callable<Object>> threadList = new ArrayList<>(dataList.size());
for (Map<String, Object> dataMap : dataList) {
threadList.add(new Callable() {
@Override
public Object call() throws Exception {
return executeBusiness(dataMap);
}
});
}
int count = dataList.size();
batchContainerExecutorBig.execute(threadList);
logger.info("批处理单次执行结束,执行耗时[{}]秒,处理[{}]条记录", (System.currentTimeMillis() - begin) / 1000.0, count);
dataList = null;
threadList = null;
return count;
}
/**
* 这是一个抽象方法,子类需实现自己的业务逻辑
* 子线程处理每条数据
*/
public abstract Object executeBusiness(Map<String, Object> dataMap);
}
package com.task.service.common;
import java.util.List;
import java.util.Map;
/**
* 功 能:分页查询回调
* 作 者:java潇邦
* 时 间:2018/12/19
*/
public interface QueryDataCallBack<Q, P> {
/**
* 获取总的记录数
* @param queryParam 查询参数
* @return 查询的记录数
*/
int getListCount(Q queryParam) ;
/**
* 获取数据列表,数据格式需要和ExcelExportConstants中配置的数据相对应
* @param queryParam 查询参数
* @param pager 分页信息
* @return 查询的记录列表
*/
List<Map<String,Object>> getList(Q queryParam, P pager);
}
package com.task.entity.common;
import java.io.Serializable;
/**
* 功 能:分页
* 作 者:java潇邦
* 时 间:2018/12/19
*/
public class Pager implements Serializable {
private int pageNum;
private int pageSize;
public Pager() {
}
public Pager(int pageNum, int pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
}
package com.task.service.common.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 描 述:多线程处理容器(尽量不要改动这里)
* 作 者:java潇邦
* 时 间:2018/12/19
*/
@Component
public class BatchContainerExecutor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static ExecutorService pool = Executors.newFixedThreadPool(20);
/**
* 功 能:并发处理taskList中的任务
* 作 者:java潇邦
* 时 间:2018/12/19
*/
public List<Object> execute(List<Callable<Object>> taskList) {
int taskSize = taskList.size();
List<Object> results = new ArrayList<Object>(taskSize);
ExecutorCompletionService<Object> concurrentExecutor = new ExecutorCompletionService<Object>(pool);
for (Callable<Object> callable : taskList) {
concurrentExecutor.submit(callable);
}
for (int i = 0; i < taskSize; i++) {
Object result = getQuenueMsg(concurrentExecutor);
results.add(result);
}
return results;
}
/**
* 功 能:获取任务-超时时间为60秒
* 作 者:java潇邦
* 时 间:2018/12/19
*/
private Object getQuenueMsg(ExecutorCompletionService<Object> concurrentExecutor) {
Object result = null;
try {
result = concurrentExecutor.poll(60, TimeUnit.SECONDS).get(60, TimeUnit.SECONDS);
} catch (Exception e) {
logger.warn("获取线程执行结果异常:", e);
result = e.getMessage();
}
return result;
}
}
package com.task.service.common.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 描 述:多线程处理容器(尽量不要改动这里)
* 作 者:java潇邦
* 时 间:2018/12/19
*/
@Component
public class BatchContainerExecutorBig {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10);
/**
* 功 能:并发处理taskList中的任务
* 作 者:java潇邦
* 时 间:2018/12/19
*/
public List<Object> execute(List<Callable<Object>> taskList) {
int taskSize = taskList.size();
List<Object> results = new ArrayList<Object>(taskSize);
ExecutorCompletionService<Object> concurrentExecutor = new ExecutorCompletionService<Object>(pool);
for (Callable<Object> callable : taskList) {
concurrentExecutor.submit(callable);
}
for (int i = 0; i < taskSize; i++) {
Object result = getQuenueMsg(concurrentExecutor);
results.add(result);
}
return results;
}
/**
* 功 能:获取任务-超时时间为10秒
* 作 者:java潇邦
* 时 间:2018/12/19
*/
private Object getQuenueMsg(ExecutorCompletionService<Object> concurrentExecutor) {
Object result = null;
try {
result = concurrentExecutor.poll(10, TimeUnit.SECONDS).get(10, TimeUnit.SECONDS);
} catch (Exception e) {
logger.warn("获取线程执行结果异常:", e);
result = e.getMessage();
}
return result;
}
}
package com.task.service.phonics.impl;
import com.task.service.common.impl.AbstractBatchDataService;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 功 能:子线程-真正的业务逻辑
* 作 者:java潇邦
* 时 间:2018/12/19
*/
@Component(value = "realBussinessServiceImpl")
public class RealBussinessServiceImpl extends AbstractBatchDataService {
/**
* 业务逻辑
*/
@Override
public Object executeBusiness(Map<String, Object> dataMap) {
//业务逻辑
return "success";
}
}