会员到期提醒,在我们生活中,还是比较常见的
腾讯视频会员到期前一个星期提醒
阿里云服务器购买后,到期前一个月,一个星期都会有邮件和短信的提醒
QQ音乐到期前半个月,一个星期,3天也都会有到期的提醒
那么今天,本汪就带大家一起来看一下,如何用Redisson缓存映射MapCache
来实现会员到期前N天提醒
(一)开篇有益
本汪先讲使用Redisson的MapCache的优点
1.Redisson组件,缓存映射MapCache 可以很好地解决DB负载过高的问题,每次进行校验,只需从缓存中查询,不需再查数据库。
2.可以解决定时任务处理不及时的问题,通过实现ApplicationRunner, Ordered两个接口,可以在应用启动和运行期间,不间断监听,并执行我们所需的业务逻辑代码。
3.解决了批次查询的数据量可能过大占用过多的缓存的问题
4.MapCache提供了对其中单独元素的失效功能,同时提供了自动监听Key添加,失效,更新,移除的机制
( 二 ) 思维导图+源码
本汪来了:现在由本汪带大家一起看看他的思维导图,其实结合思维导图和源码,就可以解决60%的问题了
依照惯例,先上思维导图和源码链接
思维导图:
源码链接:
https://github.com/HuskyCorps/distributeMiddleware
强烈建议下载源码,搭建环境进行学习。
(三)上代码,开讲
1.建立会员到期提醒类别的枚举类
First(1)代表是会员到期前进行提醒的,
End(2)代表是会员到期后进行提醒的
因为我们需要对不同类型提醒,发送不同的提醒内容
#用户会员到期提醒
vip.expire.first.subject=会员即将到期提醒【腾讯视频-https://v.qq.com/】
vip.expire.first.content=手机为:%s 的用户,您好!您的腾讯视频会员有效期即将失效,请您前往平台续费~祝您生活愉快【腾讯视频-https://v.qq.com/】
vip.expire.end.subject=会员到期提醒【腾讯视频-https://v.qq.com/】
vip.expire.end.content=手机为:%s 的用户,您好!您的腾讯视频会员有效期已经失效,为了您有更好的体验,请您前往平台继续续费~祝您生活愉快【腾讯视频-https://v.qq.com/】
/**
* 用户会员到期前的多次提醒的标识
*/
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;
}
}
1.controller
/**
* Vip到期提醒Controller
*
* @author Yuezejian
* @date 2020年 09月03日 21:29:43
*/
@RestController
@RequestMapping("user/vip")
public class UserVipController extends AbstractController {
@Autowired
private UserVipService 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);
} catch (Exception e) {
log.error("——————————充值会员-发生异常:",e.fillInStackTrace());
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}
2.service
在service中我们需要做什么呢?
(1)将用户的充值信息先行插入DB
(2) DB插入成功后,再将充值信息放入缓存中,那么怎么放呢?
假设该用户充值了一个月即30天的会员,而我们想要在会员到期的3天进行提醒
我们可以设置这条充值信息的缓存有效时间为 ttl= 30 - 3
这样也就意味着这条缓存信息将会在充值后,第 27 天时失效
通过对缓存失效的监听,进行会员充值提醒的邮件发送就OK了!
/**
* Vip到期提醒Service
*
* @author Yuezejian
* @date 2020年 09月03日 21:31:51
*/
@Service
public class UserVipService {
private static final Logger log = LoggerFactory.getLogger(UserVipService.class);
@Autowired
private RedissonClient redissonClient;
@Autowired
private UserVipMapper userVipMapper;
//充值会员-redisson的MapCache
@Transactional(rollbackFor = Exception.class)
public void addVip(UserVip vip) throws Exception {
vip.setVipTime(DateTime.now().toDate());
int res = userVipMapper.insertSelective(vip);
if (res > 0 ) {
//假设(vipDay = 20,即会员充值20天),20天后失效:第一次提醒 ttl = vipDay - x; 第二次提醒 ttl = vipDay
//1.到期前N天提醒 2.到期后提醒
RMapCache<String,Integer> rMapCache = redissonClient.getMapCache(Constant.RedissonUserVIPKey);
//TODO:第一次提醒,x默认值10,提前10天提醒
//key =vipId_1 过期前提醒,1是枚举类型First(1)
String key = vip.getId() + Constant.SplitCharUserVip + Constant.VipExpireFlg.First.getType();//vipId_1 过期前提醒
Long firstTTL = Long.valueOf(String.valueOf(vip.getVipDay()-Constant.x));
if (firstTTL > 0) {
rMapCache.put(key, vip.getId(), firstTTL, TimeUnit.SECONDS);
}
//TODO:第二次提醒
//key =vipId_2 过期前提醒,1是枚举类型End(2)
key = vip.getId() + Constant.SplitCharUserVip + Constant.VipExpireFlg.End.getType();//vipId_1 过期后提醒
Long secondTTL = Long.valueOf(String.valueOf(vip.getVipDay()));
rMapCache.put(key,vip.getId(),secondTTL, TimeUnit.SECONDS);
}
}
}
3.监听器
缓存失效时,将会被监听到,我们取出失效的数据(key,value)
取出key= vip用户的ID + "_" + 进行提醒的类型(First(1)或End(2))
例如:vip用户id为 3087 ,进行的是到期前提醒First(1),那么我们取出的key,就会是"3087_1"
我们对key,进行拆分, String [] arr = StringUtils.split(key,"_");
第一个数据就是3087,我们用getVipUserById(3087)来取出vip全部数据
第二个数据就是 1, (Constant.VipExpireFlg.First.getType().equals(type)),我们选用到期前提醒模板,进行邮件发送
/**
* vip过期提醒的监听器
*
* @author Yuezejian
* @date 2020年 09月03日 22:20:50
*/
@Component
public class RedissonMapCacheUserVip implements ApplicationRunner, Ordered {
private static final Logger log = LoggerFactory.getLogger(RedissonMapCacheUserVip.class);
@Autowired
private RedissonClient redissonClient;
@Autowired
private Environment env;
@Autowired
private UserVipMapper vipMapper;
@Autowired
private MailService mailService;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("不间断执行自定义操作——————————————————————————————order1");
this.listenUserVip();
}
//设置为1,让她以较高的优先级进行执行
@Override
public int getOrder() {
return 1;
}
//监听会员过期的数据 1.到期前N天提醒 2.到期后的提醒 需要给相应的用户发送通知(邮件)
private void listenUserVip() {
RMapCache<String , Integer> rMapCache = redissonClient.getMapCache(Constant.RedissonUserVIPKey);
//EntryExpiredListener(org.redisson.api.map.event)缓存失效监听
rMapCache.addListener(new EntryExpiredListener<String,Integer>() {
@Override
public void onExpired(EntryEvent<String, Integer> entryEvent) {
//key = 充值记录id -类型
String key = String.valueOf(entryEvent.getKey());
//value = 充值记录id
String value = String.valueOf(entryEvent.getValue());
log.info("————监听用户会员过期信息,监听到数据:key={},value={}",key,value);
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
String [] arr = StringUtils.split(key,Constant.SplitCharUserVip);
Integer id = Integer.valueOf(value);
UserVip vip = vipMapper.selectByPrimaryKey(id);
if (vip != null && 1==vip.getIsActive() && StringUtils.isNotBlank(vip.getEmail())) {
//TODO:区分第几次提醒,发送对应消息
Integer type = Integer.valueOf(arr[1]);
if (Constant.VipExpireFlg.First.getType().equals(type)) {
String content=String.format(env.getProperty("vip.expire.first.content"),vip.getPhone());
mailService.sendSimpleEmail(id.toString(),env.getProperty("vip.expire.first.subject"),content,vip.getEmail());
} else {
//设置数据库內会员信息失效
int res = vipMapper.updateExpireVip(id);
if (res > 0) {
String content=String.format(env.getProperty("vip.expire.end.content"),vip.getPhone());
mailService.sendSimpleEmail(id.toString(),env.getProperty("vip.expire.end.subject"),content,vip.getEmail());
}
}
}
}
}
});
}
}
4.邮件发送组件
import com.google.gson.Gson;
import com.tencent.bigdata.convenience.model.mapper.MsgLogMapper;
import com.tencent.bigdata.convenience.server.controller.AbstractController;
import com.tencent.bigdata.convenience.server.enums.Constant;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
/**
* 邮件service
*
* @author Yuezejian
* @date 2020年 08月24日 20:23:04
*/
@Service
public class MailService extends AbstractController {
@Autowired
private Environment env;
@Autowired
private JavaMailSender mailSender;
@Autowired
private MsgLogMapper msgLogMapper;
//TODO:发送简单的邮件消息
//@Async("threadPoolTaskExecutor")此处切记不可使用AOP注解,方法进入切面后,由于@Around,返回类型boolean会被设为void,会造成异常
//Caused by: org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type
//仍想使用异步线程处理,可以修改ACK实现方法,将返回类型设为void即可
//或对mailSender.send做包装
public Boolean sendSimpleEmail(final String msgId,final String subject,final String content,final String ... tos) throws Exception {
Boolean res = true;
SimpleMailMessage message=new SimpleMailMessage();
message.setSubject(subject);
message.setText(content);
message.setTo(tos);
message.setFrom(env.getProperty("mail.send.from"));
try {
mailSender.send(message);
} catch (MailException e) {
log.error("邮件发送失败, to={}, title={}, e={}", new Gson().toJson(tos), subject, e);
res = false;
throw e;
} finally {
this.updateMsgSendStatus(msgId,res);
}
return res;
}
//TODO:更新消息处理的结果
private void updateMsgSendStatus(final String msgId,Boolean res){
if (StringUtils.isNotBlank(msgId)){
if (res){
msgLogMapper.updateStatus(msgId, Constant.CONSUME_SUCCESS);
}else{
msgLogMapper.updateStatus(msgId, Constant.CONSUME_FALSE);
}
}
}
}