主体使用kafka+线程池,加漏斗或令牌桶控流量。
一二三四任选都可以做控制
一:生产端做令牌控制+时间段控制(控)
package com.xx.xx.scheduled.job.give;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import com.alibaba.fastjson.JSONObject;
import com.xx.xx.api.redis.service.RedisService;
import com.xx.x.common.DsmpConst;
import com.xx.xx.common.consts.MmcsRedisCons;
import com.x.x.domain.member.GiveSubServiceRecordVO;
import com.x.x.scheduled.job.AbstractDistributeJob;
import com.x.x.scheduled.mapper.GiveJobMapper;
import com.github.pagehelper.page.PageMethod;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
/**
* 每月处理加赠记录 <b>System:
*/
@Slf4j
public class GiveJob extends AbstractDistributeJob {
@Value("${mmcs.give.pageSize:1000}")
protected Integer pageSize;
@Value("${mmcs.kafka.give.topic:give-topic}")
private String giveTopic;
@Autowired
private GiveJobMapper giveJobMapper;
@Autowired
private RedisService redisService;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Override
protected void execute(String context, int shardingItem) {
//当前时间戳
String executeNo = System.currentTimeMillis() + "";
List<GiveSubServiceRecordVO> list = null;
do {
try {
PageMethod.startPage(1, pageSize);
list = giveJobMapper.listGiveSubServiceRecord(executeNo);
if (list == null || list.isEmpty()) {
break;
}
this.sendKafka(list);
setGiveSubServiceRecord(list,executeNo);
} catch (Exception e) {
log.error("loop query giveSubServiceRecord for give record exception,list:{}", list, e);
sleep(3);
}
} while (list != null && list.size() == pageSize);
}
/**
* 发送kafka执行自动领取逻辑
*
* @param list
*/
private void sendKafka(List<GiveSubServiceRecordVO> list) {
String spermitsPerSecond = (String) redisService.hget(MmcsRedisCons.PARAM_CONFIG_KEY, "mmcs.give.kafka.permitsPerSecond");
double permitsPerSecond = Double.valueOf(spermitsPerSecond);
#创建令牌桶,读取缓存配置设置令牌数量
RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond);
//加赠时间段控制
String allowTime = (String) redisService.hget(MmcsRedisCons.PARAM_CONFIG_KEY, "RateLimiter_give_allow_time");
boolean flag = true;
if (allowTime != null) {
flag = timeControl(allowTime);
}
for (GiveSubServiceRecordVO giveRecord : list) {
try {
if (flag) {
rateLimiter.acquire();
try {
//{"action":"2","data":{ give_sub_service_record 数据}}发送到give_topic
JSONObject jsonObject = new JSONObject();
jsonObject.put("action", DsmpConst.ACTION_ID_STOP);
jsonObject.put("data", giveRecord);
kafkaTemplate.send(giveTopic, this.getPartition(giveRecord.getMsisdn()), null, jsonObject.toJSONString());
} catch (Exception e) {
log.error("sendKafka exception,giveSubServiceRecord:{},msisdn:{}", giveRecord.getId(), giveRecord.getMsisdn(), e);
sleep(1);
}
}else{
submitNotifier(this);
#先获取再挂起,(造成的偶现bug是不是可以用sleep解决?,不需要wait?)
synchronized (this) {
this.wait();
}
log.info("else submitNotifier synchronized this............");
}
} catch (Exception e) {
log.error("GiveSubServiceRecordVO exception,giveSubServiceRecord:{},msisdn:{}", giveRecord.getId(), giveRecord.getMsisdn(), e);
throw new RuntimeException();
}
}
}
/**
* 根据手机号最后一位获取kafka分区ID
*
* @param msisdn
* @return
*/
private int getPartition(String msisdn) {
return Integer.parseInt(msisdn.substring(msisdn.length() - 1));
}
/**
* 更新已处理过的加赠记录
*
* @param list
*/
private void setGiveSubServiceRecord(List<GiveSubServiceRecordVO> list , String executeNo) {
int num = giveJobMapper.setGiveSubServiceRecord(list,executeNo);
log.info("多选, 更新已处理过的加赠记录:{}", num);
}
/**
* 等待秒数
*
* @param seconds
*/
private void sleep(int seconds) {
try {
Thread.sleep(seconds * 1000L);
} catch (Exception e) {
log.error("sleep exception", e);
}
}
public static void submitNotifier(Object rateLimitObject) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
#唤醒。这个地方有误差,在现网中,第二段时间后不再执行,线程被挂起,但有时候又不会,正常执行
log.info("submitNotifier schedule TimerTask run............");
synchronized (rateLimitObject) {
rateLimitObject.notifyAll();
}
}
}, 300000);
}
#时间控制,读取缓存时间段进行时间控制
private boolean timeControl(String allowTime){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd ");
boolean flag = false;
try{
Date nowTmie = new Date();
String nowDay = sdfd.format(nowTmie);
String[] allowTimes = allowTime.split(";");
for(int i = 0; i < allowTimes.length; i++){
String[] aTime = allowTimes[i].split("-");
Date stime = sdf.parse(nowDay+aTime[0]);
Date etime = sdf.parse(nowDay+aTime[1]);
if(nowTmie.after(stime) && nowTmie.before(etime)){
flag = true;
break;
}
}
}catch(ParseException e){
log.error("allowTime 解析失败, allowTime:{}", allowTime, e);
flag = false;
}
return flag;
}
}
缓存插入
INSERT INTO `param_config` (`id`, `code`, `value`, `comments`, `insert_time`) VALUES ('31', 'RateLimiter_give_allow_time', '08:00:00-11:00:00;14:00:00-20:00:00', '主动加赠下发时间段,格式如HH:mm:ss-HH:mm:ss 多个时间段以;隔开', now());
INSERT INTO `param_config` (`id`, `code`, `value`, `comments`, `insert_time`) VALUES ('32', 'mmcs.give.kafka.permitsPerSecond', '20.0', '加赠自动下发速率,n个每秒,double类型,如值为5则1秒下发5次请求', now());
先记录把,比较杂,实际是生产端没做上述控制,因为出现的偶发bug。
二:消费端亦做令牌控制+时间段控制(控)
1、入口
package com.x.xx.app.kafka;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import com.xx.app.service.GiveRecvService;
import lombok.extern.slf4j.Slf4j;
/**
* 加赠消费者 <b>System:</b>x<br/>
x
*/
@Slf4j
@Service
public class GiveConsumer {
@Autowired
private GiveRecvService giveRecvService;
@KafkaListener(id = "giveRecvContainer", topics = {"${mmcs.kafka.month.give.recv.topic:give-topic}"},
groupId = "give-recv-group", concurrency = "${mmcs.kafka.month.give.recv.concurrency:1}")
public void consumer(String message) {
log.info("giveRecvContainer message:{}", message);
giveRecvService.process(message);
}
}
消费控制
package com.x.app.service;
/**
* 加赠领取
*/
@Slf4j
@Service
public class GiveRecvService {
@Autowired
private GiveMapper giveMapper;
@Autowired
protected RedisService redisService;
@Autowired
protected ComRecvService crService;
public void process(String message) {
log.info("give process message, message:{}", message);
JSONObject giveJson = JSON.parseObject(message);
Integer action = giveJson.getInteger("action");
JSONObject dataJson = giveJson.getJSONObject("data");
GiveRecord record = null;
if(action == DsmpConst.ACTION_ID_OPEN){
//message: {"action":"1","data":{"subscriptoinId":"x","serviceId":"x","msisdn":"x"}}
String subscriptoinId = dataJson.getString("subscriptoinId");
String serviceId = dataJson.getString("serviceId");
String msisdn = dataJson.getString("msisdn");
// 查询是否有加赠子业务
List<GiveSubServiceConfig> giveServiceConfigs = giveMapper.queryGiveServiceConfig(serviceId);
if (giveServiceConfigs == null || giveServiceConfigs.size() == 0) {
return;
}
for (GiveSubServiceConfig giveServiceConfig : giveServiceConfigs) {
SubServiceInfoVO giveService = (SubServiceInfoVO) redisService.hget(MmcsRedisCons.SUB_SERVICE_INFO_KEY,
giveServiceConfig.getSubServiceId());
String lockKey = null;
boolean giveLock = false;
try{
if (giveService.getGiveLevel() == 1) {
lockKey = "give:" + msisdn + ":" + giveService.getSubServiceId();
} else if (giveService.getGiveLevel() == 2) {
lockKey = "give:" + subscriptoinId + ":" + giveService.getSubServiceId();
}
giveLock = redisService.setNx(lockKey, "giveLock");
if (!giveLock) {
log.error("give recev lock failed, lockKey:{},lockResult:{}", lockKey, giveLock);
continue;
}
// 加赠记录表是否存在数据,目前赠送完后,再次订也不再赠送
GiveRecord gr = giveMapper.queryGiveRecord(msisdn, subscriptoinId, giveServiceConfig.getSubServiceId(),
giveService.getGiveLevel());
if (gr != null) {
log.error("has been given to, msisdn:{},subscriptoinId:{},subServiceId:{},giveLevel:{}", msisdn,
subscriptoinId, giveServiceConfig.getSubServiceId(), giveService.getGiveLevel());
continue;
}
record = saveGiveRecord(giveServiceConfig, giveService, subscriptoinId, msisdn);
if(giveServiceConfig.getRecvWay() == 1){
log.error("this giveService only support manual, message:{}, giveServiceConfig:{}", message, giveServiceConfig);
continue;
}
//领取
this.giveRecv(record);
}finally{
redisService.del(lockKey);
}
}
}else{
String spermitsPerSecond = (String) redisService.hget(MmcsRedisCons.PARAM_CONFIG_KEY, "mmcs.give.kafka.permitsPerSecond");
double permitsPerSecond = Double.valueOf(spermitsPerSecond);
RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond);
//加赠时间段控制
String allowTime = (String) redisService.hget(MmcsRedisCons.PARAM_CONFIG_KEY, "RateLimiter_give_allow_time");
boolean flag = true;
if (allowTime != null) {
flag = timeControl(allowTime);
}
if (flag) {
#令牌桶
rateLimiter.acquire();
record = JSONObject.parseObject(dataJson.toString(), GiveRecord.class);
//领取
this.giveRecv(record);
}else{
submitNotifier(this);
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
log.error("wait InterruptedException, GiveRecord:{}", record);
}
}
}
}
}
private void giveRecv(GiveRecord giveRecord) {
SubscriptionVO record = new SubscriptionVO();
record.setGiveRecord(giveRecord);
record.setSubscriptionId(giveRecord.getSubscriptionId());
record.setServiceId(giveRecord.getServiceId());
record.setMsisdn(giveRecord.getMsisdn());
record.setRecvType(SubscriptionCons.RecvType.GIVE_RECV);
record.setSelectServiceId(giveRecord.getSubServiceId());
record.setOrderType(SubscriptionCons.OrderType.MONTH);
record.setOrderTime(giveRecord.getUserStartTime());
List<String> subServiceIds = new ArrayList<String>();
subServiceIds.add(giveRecord.getSubServiceId());
crService.recv(record, subServiceIds);
}
private GiveRecord saveGiveRecord(GiveSubServiceConfig giveServiceConfig, SubServiceInfoVO giveService, String subscriptoinId, String msisdn) {
GiveRecord giveRecord = new GiveRecord();
try{
giveRecord.setId(getGiveRecordId());
giveRecord.setGiveConfigId(giveServiceConfig.getId());
giveRecord.setSubscriptionId(subscriptoinId);
giveRecord.setMsisdn(msisdn);
giveRecord.setServiceId(giveServiceConfig.getServiceId());
giveRecord.setSubServiceId(giveServiceConfig.getSubServiceId());
giveRecord.setStatus(1);
giveRecord.setFrequencyUnit(giveServiceConfig.getFrequencyUnit());
giveRecord.setFrequencyAmount(giveServiceConfig.getFrequencyAmount());
giveRecord.setGiveLevel(giveService.getGiveLevel());
giveRecord.setDependSubscription(giveService.getDependSubscription());
giveRecord.setRecvWay(getRecvWay(giveServiceConfig.getRecvWay()));
giveRecord.setUserStartTime(new Date());
giveRecord.setUserEndTime(getUserEndTime(giveServiceConfig));
giveRecord.setNextGiveTime(new Date());
giveRecord.setSuccessNum(0);
giveRecord.setFinishFlag(0);
giveRecord.setExecuteNo(""+System.currentTimeMillis());
giveMapper.insertGiveRecord(giveRecord);
}catch(Exception e){
log.error("saveGiveRecord failed, give_sub_service_record:{}", giveRecord, e);
}
return giveRecord;
}
private Integer getRecvWay(Integer recvWay) {
Integer recordRecvWay = null;
switch(recvWay){
case 1:
recordRecvWay = SubServiceInfoCons.RecvWay.MANUAL;
break;
case 2:
recordRecvWay = SubServiceInfoCons.RecvWay.AUTO;
break;
default:
recordRecvWay = SubServiceInfoCons.RecvWay.ALL;
break;
}
return recordRecvWay;
}
private Date getUserEndTime(GiveSubServiceConfig giveServiceConfig) {
Date userEndTime;
if(giveServiceConfig.getGiveWay()==1){
//需赠送的月数,FrequencyUnit的配置值为换算成的月数,则单位不为月时,不用改代码
int count = giveServiceConfig.getFrequencyUnit() * giveServiceConfig.getFrequencyAmount() * giveServiceConfig.getGiveMax();
//当前月最后一天
Date curtMonthLastDatetime = DateUtil.getMonthLastDatetime();
Calendar calender = Calendar.getInstance();
calender.setTime(curtMonthLastDatetime);
calender.add(Calendar.MONTH, count-1);
userEndTime = calender.getTime();
}else{
userEndTime = giveServiceConfig.getGiveEndTime();
}
return userEndTime;
}
private String getGiveRecordId() {
return redisService.getSeq(SequenceEnum.SEQUNCE_GIVE_RECORD_ID.getKey());
}
public static void submitNotifier(Object rateLimitObject) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
synchronized (rateLimitObject) {
rateLimitObject.notifyAll();
}
}
}, 30000);
}
private boolean timeControl(String allowTime){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd ");
boolean flag = false;
try{
Date nowTmie = new Date();
String nowDay = sdfd.format(nowTmie);
String[] allowTimes = allowTime.split(";");
for(int i = 0; i < allowTimes.length; i++){
String[] aTime = allowTimes[i].split("-");
Date stime = sdf.parse(nowDay+aTime[0]);
Date etime = sdf.parse(nowDay+aTime[1]);
if(nowTmie.after(stime) && nowTmie.before(etime)){
flag = true;
break;
}
}
}catch(ParseException e){
log.error("allowTime 解析失败, allowTime:{}", allowTime, e);
flag = false;
}
return flag;
}
}
单独:线程池处理并发消费(快)
消费端进行多线程消费
package com.xxx.xx.service.give.batch.kafka;
import com.xx.xx.service.give.batch.service.AppointGiveService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class GiveBatchConsumer {
@Autowired
private AppointGiveService agService;
private ThreadPoolExecutor pool;
#这个要注意,四个机器设置8个核心数
@Value("${mmcs.give.batch.core.size:8}")
private int corePoolSize;
@Value("${mmcs.give.batch.max.size:8}")
private int maximumPoolSize;
#注意这个注解,初始化加载,队列类型,和再注意拒绝策略是,若被拒绝则主线程发
@PostConstruct
private void init() {
pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 60L, TimeUnit.SECONDS,
new SynchronousQueue<>(), new KafkaConsumerThreadFactory("kafka-consumer-give-batch-pool"),
new ThreadPoolExecutor.CallerRunsPolicy());
}
#监听消费,启动多线程池
@KafkaListener(id = "giveBatchSpContainer", topics = {"${mmcs.kafka.give-batch.topic:give-batch-topic}"})
public void consumer(String message) {
try {
log.info("give-batch-topic message:{}", message);
#这里原处理agService.process(message)
pool.submit(() -> agService.process(message));
} catch (Exception e) {
log.error("process giveBatchSpContainer exception", e);
}
}
}
#帮助类创建线程池工厂帮助类
package com.xx.xx.service.give.batch.kafka;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* kafka消费线程工厂 <b>System:<br/>
*/
public class KafkaConsumerThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private String namePrefix;
public KafkaConsumerThreadFactory(String namePrefix) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.namePrefix=namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
生产端的时间第二段没控制住的原因
/**
* 发送kafka执行自动领取逻辑
*
* @param list
*/
private void sendKafka(List<GiveSubServiceRecordVO> list) {
String spermitsPerSecond = (String) redisService.hget(MmcsRedisCons.PARAM_CONFIG_KEY, "mmcs.give.kafka.permitsPerSecond");
double permitsPerSecond = Double.valueOf(spermitsPerSecond);
RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond);
//加赠时间段控制
String allowTime = (String) redisService.hget(MmcsRedisCons.PARAM_CONFIG_KEY, "RateLimiter_give_send_time");
boolean flag = true;
for (GiveSubServiceRecordVO giveRecord : list) {
//原因在此循环的时间allowTime 获取有当前时间的动态值,所以线程不会被唤醒,或者锁不会重新获取,要不休眠要不难拿到
if (allowTime != null) {
flag = timeControl(allowTime);
}
try {
if (flag) {
rateLimiter.acquire();
try {
//{"action":"2","data":{ give_sub_service_record 数据}}发送到give_topic
JSONObject jsonObject = new JSONObject();
jsonObject.put("action", DsmpConst.ACTION_ID_STOP);
jsonObject.put("data", giveRecord);
kafkaTemplate.send(giveTopic, this.getPartition(giveRecord.getMsisdn()), null, jsonObject.toJSONString());
} catch (Exception e) {
log.error("sendKafka exception,giveSubServiceRecord:{},msisdn:{}", giveRecord.getId(), giveRecord.getMsisdn(), e);
sleep(1);
}
}else{
sleep(60);
log.info("else submitNotifier synchronized this............");
}
} catch (Exception e) {
log.error("GiveSubServiceRecordVO exception,giveSubServiceRecord:{},msisdn:{}", giveRecord.getId(), giveRecord.getMsisdn(), e);
throw new RuntimeException();
}
}
}
这个可以单独拿出来
缓存工程,redis工程(工具)类+cache类(对所有需要缓存的表做)
再看看redis的配置代码,每当insert数据,就要刷缓存。
package com.aspire.mmcs.service.cache.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.xx.x.common.consts.MmcsRedisCons;
import com.x.x.domain.sysconfig.SysConfigVO;
import com.xx.x.service.cache.mapper.ParamConfigMapper;
/**
* 系统配置参数 <b>System:</b>MMCS<br/>
*/
@Service
public class ParamConfigService extends AbstractService {
@Autowired
private ParamConfigMapper mapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean match(String type) {
return "param_config".equalsIgnoreCase(type);
}
/**
* 数据加载
*/
@Override
public void load() {
Map<String, String> map = this.listAllConfig();
if (map != null) {
redisService.del(MmcsRedisCons.PARAM_CONFIG_KEY);
redisService.hputAll(MmcsRedisCons.PARAM_CONFIG_KEY, map);
}
}
private Map<String, String> listAllConfig() {
List<SysConfigVO> list = mapper.getAllConfig();
if (list == null || list.isEmpty()) {
return null;
}
Map<String, String> map = new HashMap<>((int) (list.size() / 0.75) + 1);
for (SysConfigVO vo : list) {
map.put(vo.getKey(), vo.getValue());
}
return map;
}
}
package com.aspire.mmcs.service.cache.service;
import org.springframework.beans.factory.annotation.Autowired;
import cxx.api.redis.service.RedisService;
*
*/
public abstract class AbstractService {
@Autowired
protected RedisService redisService;
/**
* 是否匹配。用于判断是否需要重新加载缓存
*
* @param type
* @return
*/
public abstract boolean match(String type);
/**
* 数据加载
*/
public abstract void load();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace="com.xx.xx.service.cache.mapper.ParamConfigMapper">
<resultMap id="ResultMap"
type="com.aspire.mmcs.domain.sysconfig.SysConfigVO">
<result column="code" property="key" />
<result column="value" property="value" />
</resultMap>
<select id="getAllConfig" resultMap="ResultMap">
<![CDATA[
select code,value from param_config
]]>
</select>
</mapper>