这个其实不怎么推荐,当然她使用的话,也是没有问题的,只是因为用到了定时器
所以不如RMapCache好用,大家可以当作是了解Redisson延时队列来看待这篇博客
源码链接:
https://github.com/HuskyCorps/distributeMiddleware
0.application.properties
#用户会员到期提醒
vip.expire.first.subject=会员即将到期提醒【泰达便民服务平台-http://www.tjxstech.com/】
vip.expire.first.content=手机为:%s 的用户,您好!您的会员有效期即将失效,请您前往平台续费~祝您生活愉快【泰达便民服务平台-http://www.tjxstech.com/】
vip.expire.end.subject=会员到期提醒【泰达便民服务平台-http://www.tjxstech.com/】
vip.expire.end.content=手机为:%s 的用户,您好!您的会员有效期已经失效,为了您有更好的体验,请您前往平台继续续费~祝您生活愉快【泰达便民服务平台-http://www.tjxstech.com/】
1.controller
/**
* Redisson延迟队列DelayedQueue,实现会员到期前N天提醒
*
* @author Yuezejian
* @date 2020年 09月09日 19:57:53
*/
@RestController
@RequestMapping("vip")
public class VipController {
private static final Logger log = LoggerFactory.getLogger(VipController.class);
@Autowired
private VipService vipService;
@RequestMapping(value = "put" ,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)application/json;charset=UTF-8
public BaseResponse putVip(@RequestBody @Validated UserVip userVip, BindingResult result) {
String checkRes = ValidatorUtil.checkResult(result);
if (StringUtils.isNotBlank(checkRes)) {
return new BaseResponse(StatusCode.InvalidParams.getCode(),checkRes);
}
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
vipService.addVip(userVip);
log.info("————成功充值会员 "+userVip.getVipDay()+" 天");
} catch (Exception e) {
log.error("——————————充值会员-发生异常:",e.fillInStackTrace());
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}
2.service
/**
* 基于Redisson延迟队列DelayQueue,实现会员到期前N天提醒
*
* @author Yuezejian
* @date 2020年 09月09日 20:16:35
*/
@Service
public class VipService {
private static final Logger log = LoggerFactory.getLogger(VipService.class);
@Autowired
private UserVipMapper userVipMapper;
@Autowired
private RedissonClient redissonClient;
@Transactional(rollbackFor = Exception.class)
public void addVip(UserVip vip) throws Exception {
vip.setVipTime(DateTime.now().toDate());
int res = userVipMapper.insertSelective(vip);
//TODO:充值成功(现实一般是需要走支付的..在这里以成功插入db为准) - 设置两个过期提醒时间,
//TODO:一个是vipDay后的;一个是在到达vipDay前 x 的时间
//TODO:如,vipDay=10天,x=2,即代表vip到期 前2天 提醒一次,vip到期时提醒一次,即
//TODO:第一次提醒的时间点为:ttl=10-2=8,即距离现在开始的8天后进行第一次提醒;
//TODO:第二次提醒的时间点是:ttl=10,即距离现在开始的10天后进行第二次提醒 -- 以此类推
//TODO: (时间的话,建议转化为s;当然啦,具体业务具体设定即可)
//TODO:基于redisson的延迟队列实现,重点就在于 ttl 的计算
// (为了测试方便,在这里我们以 s 为单位);
//TODO:如果是多次提醒的话,需要做标记
if (res > 0 ) {
RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(Constant.RedissonUserVipQueue);
RDelayedQueue<String> rDelayedQueue = redissonClient.getDelayedQueue(blockingDeque);
//TODO:第一次提醒
//这个value,实际是这样的value=vipId+"-"+"1",{value="14-1"}
// 下面的处理只是为了构架考虑,所以外提了出去。
//为了方便大家理解,本汪把相关代码都加到了注释里
// public static final String SplitCharUserVip="-";
// 用户会员到期前的多次提醒的标识
// public enum VipExpireFlg{
// First(1),
// End(2),
// ;
//
// private Integer type;
//
// VipExpireFlg(Integer type) {
// this.type = type;
// }
//
// public Integer getType() {
// return type;
// }
//
// public void setType(Integer type) {
// this.type = type;
// }
// }
String value = vip.getId()+Constant.SplitCharUserVip+Constant.VipExpireFlg.First.getType();
Long firstTTL = Long.valueOf(String.valueOf(vip.getVipDay()-Constant.x));
if ( firstTTL > 0 ) {
//在firstTTL秒内,把value对象移动到目标队列中去
rDelayedQueue.offer(value,firstTTL,TimeUnit.SECONDS);
}
//TODO:第二次提醒
//这个value,实际是value=vipId+"-"+"2",{value="14-2"}
value = vip.getId()+Constant.SplitCharUserVip+Constant.VipExpireFlg.End.getType();
Long secondTTL = Long.valueOf(vip.getVipDay());
//在secondTTL秒内,把value对象移动到目标队列中去
rDelayedQueue.offer(value,secondTTL,TimeUnit.SECONDS);
//本汪带大家看一下官方的实例
//Java的基于Redis的DelayedQueue对象允许将每个元素以指定的延迟传输到目标队列。
// 对于将消息传递给消费者的指数退避策略可能很有用。目标队列可以是任何队列实现的RQueue接口。
//RBlockingQueue<String> distinationQueue = ...
//RDelayedQueue<String> delayedQueue = getDelayedQueue(distinationQueue);
//--》move object to distinationQueue in 10 seconds
//delayedQueue.offer("msg1", 10, TimeUnit.SECONDS);
//--》move object to distinationQueue in 1 minutes
//delayedQueue.offer("msg2", 1, TimeUnit.MINUTES);
//
//
//--》 msg1 will appear in 15 seconds
//distinationQueue.poll(15, TimeUnit.SECONDS);
//
//--》msg2 will appear in 2 seconds
//distinationQueue.poll(2, TimeUnit.SECONDS);
}
}
}
3.监听器
/**
* Redisson的延时队列DelayQueue,Vip提前N天提醒——Listener
*
* @author Yuezejian
* @date 2020年 09月09日 21:07:25
*/
@Component
public class VipQueueListener {
private static final Logger log = LoggerFactory.getLogger(VipQueueListener.class);
@Autowired
private RedissonClient redissonClient;
@Autowired
private UserVipMapper vipMapper;
@Autowired
private MailService mailService;
@Autowired
private Environment env;//环境变量实例
//实时监听延时队列中的代理消息
@Async("threadPoolTaskExecutor")
@Scheduled(cron = "0/5 * * * * ?")
public void listenExpireVip() {
RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(Constant.RedissonUserVipQueue);
if (blockingDeque != null && !blockingDeque.isEmpty()) {
//本汪说下,此处我们所取到的element,就是我们
//rDelayedQueue.offer(value,secondTTL,TimeUnit.SECONDS);
//放进去的value了,他的格式是“14-1”
String element = blockingDeque.poll();
if (StringUtils.isNotBlank(element)) {
log.info("Vip提前N天提醒,Redisson的延时队列DelayQueue监听器——Listener,监听到 element={}",element);
//这时候,你应该知道为什么把分隔符提出去,就是为了使用的统一
// public static final String SplitCharUserVip="-";
String[] arr = StringUtils.split(element,Constant.SplitCharUserVip);
Integer id = Integer.valueOf( arr[0]);
Integer type = Integer.valueOf(arr[1]);
UserVip vip = vipMapper.selectByPrimaryKey(id);
if (vip != null && 1==vip.getIsActive() && StringUtils.isNotBlank(vip.getEmail())) {
//TODO:区分第几次提醒,发送对应消息
if (Constant.VipExpireFlg.First.getType().equals(type)) {
log.info("Vip提前N天提醒,第一次提醒");
String content=String.format(env.getProperty("vip.expire.first.content"),vip.getPhone());
mailService.sendSimpleEmail(env.getProperty("vip.expire.first.subject"),content,vip.getEmail());
} else {
//设置数据库內会员信息失效,就是把isActive由“1”变为“0”
int res = vipMapper.updateExpireVip(id);
if (res > 0) {
log.info("Vip提前N天提醒,第二次提醒");
String content=String.format(env.getProperty("vip.expire.end.content"),vip.getPhone());
mailService.sendSimpleEmail(env.getProperty("vip.expire.end.subject"),content,vip.getEmail());
}
}
}
}
}
}
4.线程池配置
/**
* 线程池配置类
*
* @author Yuezejian
* @date 2020年 08月22日 22:09:26
*/
@Configuration
public class ThreadConfig {
@Bean("threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor(){
//ThreadPoolTaskExecutor 底层直接使用了一个BlockingQueue,
// 初始容量为2147483647(0x7fffffff,2的31次方-1),即无界队列
//线程池维护线程所允许的空闲时间,默认为60s, keepAliveSeconds = 60
//其内部使用队列:BlockingQueue<Runnable> queue = this.createQueue(this.queueCapacity);
//createQueue()方法底层使用了new LinkedBlockingQueue(内部基于链表来存放元素)
//本汪说下:
//LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,
//添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
/*executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setKeepAliveSeconds(10);
executor.setQueueCapacity(8);*/
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setKeepAliveSeconds(10);
executor.setQueueCapacity(4);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
5.运行结果