百万用户消息处理

  • 场景:

每条消息内容假设占内存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";
    }

}

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值