百万数据下的控流处理

主体使用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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值