^__^
(oo)\______
(__)\ )\/\
||----w |
|| ||
一:前言:
活动创建及展示博客链接:Spring项目-抽奖系统(实操项目-用户管理接口)(THREE)-CSDN博客
上一次完成了活动的创建和活动的展示,接下来就是重头戏——抽奖及结果展示。
二:抽奖设计:
首先我们要搞清楚整个业务的流程才能开始:
这张图可以反应整个抽奖流程!
当然一些细节部分还是会有些模糊:
例如究竟是一次性抽出中奖者,还是按等级去抽?还是按奖品去抽?
那么就需要结合UI图和前后端的约定去理解和确定了:
抽奖时序图:
[ 请求 ] /draw-prize POST{"winnerList":[{"userId":15,"userName":" 胡⼀博 "},{"userId":21,"userName":" 范闲 "}],"activityId":23,"prizeId":13,"prizeTiers":"FIRST_PRIZE","winningTime":"2024-05-21T11:55:10.000Z"}[ 响应 ]{"code": 200,"data": true,"msg": ""}
从需求上,我们可以直到每次抽奖都是从等级最高的奖品开始抽,也就是每次前端向后端发送一个表单,包括该奖品中奖名单、活动id、奖品id、奖品等级、中奖时间
画图表示如下:
总结:
前端拿到活动详情,之后进行抽奖行为,每抽一个类型的奖品后将数据传回后端进行处理,后端存储详细信息,返回给前端,前端进行展示!!
2.1:RabbitMq消息队列中间件:
其中我们为了让用户体验更好,每次将抽奖后的处理流程交给RabbitMq消息队列进行进一步的处理!!
RabbitMq起到了异步解耦、流量削峰、消息分发等作用。
对于流量比较大的业务来说,起到了非常大的作用!!
pom.xml文件坐标:
1 <!-- RabbitMQ --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency
properties配置:
## mq ##spring.rabbitmq.host=124.71.229.73spring.rabbitmq.port=5672spring.rabbitmq.username=adminspring.rabbitmq.password=admin# 消息确认机制,默认 autospring.rabbitmq.listener.simple.acknowledge-mode=auto# 设置失败重试 5 次spring.rabbitmq.listener.simple.retry.enabled=truespring.rabbitmq.listener.simple.retry.max-attempts=5
RabbitMq工具类:
@Configuration
public class DirectRabbitConfig {
public static final String QUEUE_NAME = "DirectQueue";
public static final String EXCHANGE_NAME = "DirectExchange";
public static final String ROUTING = "DirectRouting";
/**
* 队列 起名:DirectQueue
*
* @return
*/
@Bean
public Queue directQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启
时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使⽤,⽽且当连接关闭后队列即被
删除。此参考优先级⾼于durable
// autoDelete:是否⾃动删除,当没有⽣产者或者消费者使⽤此队列,该队列会⾃动删除。
// return new Queue("DirectQueue",true,true,false);
// ⼀般设置⼀下队列的持久化就好,其余两个就是默认false
return new Queue(QUEUE_NAME,true);
}
/**
* Direct交换机 起名:DirectExchange
*
* @return
*/
@Bean
DirectExchange directExchange() {
return new DirectExchange(EXCHANGE_NAME,true,false);
}
/**
* 绑定 将队列和交换机绑定, 并设置⽤于匹配键:DirectRouting
*
* @return
*/
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(directQueue())
.to(directExchange())
.with(ROUTING);
}
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
2.2:抽奖请求处理:
2.2.1:controller层:
注意,这里接收到参数以后,不进行任何处理,然后直接抛给RabbitMq去处理!!
@RequestMapping("/draw-prize")
public CommonResult<Boolean> drawPrize(@RequestBody @Valid DrawPrizeParam param) {
log.info("drawPrize DrawPrizeParam:{}", JacksonUtil.writeValueAsString(param));
drawPrizeService.drawPrize(param);
return CommonResult.succcess(true);
}
2.2.2:Service层:
@Service
public interface DrawPrizeService {
void drawPrize(DrawPrizeParam param);
}
serviceImpl:
@Override
public void drawPrize(DrawPrizeParam param) {
//奖中奖信息发送至mq进行处理
String messageId = String.valueOf(UUID.randomUUID());
String messageData = JacksonUtil.writeValueAsString(param);
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:dd:ss"));
Map<String, Object> map = new HashMap<>();
map.put("messageId",messageId);
// 可以加type区分消息类型
map.put("messageData",messageData);
map.put("createTime",createTime);
//将消息携带绑定键值:DirectRouting 发送到交换机DirectExchange
rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING, map);
log.info("发送mq完成!");
}
2.2.3:dao层:
这里只是给出一部分代码,重点是逻辑,完整代码,可以参考码云中的!!
@Mapper
public interface WinningRecordMapper {
Integer batchinsert(@Param("items") List<WinningRecordDO> winningRecordDO);
@Select("select * from winning_record where activity_id = #{activityId}")
List<WinningRecordDO> selectByActivityId(Long activityId);
@Select("select count(1) from winning_record where activity_id = #{activityId} and prize_id = #{prizeId}")
int countByAPId(Long activityId, Long prizeId);
}
2.3:获取活动完整信息:
该接口是在抽奖请求之前需要进行调用,分别获取该活动的人员详情信息,奖品详情信息,
活动详情信息!!
2.3.1::controller层:
@RequestMapping("/activity-detail/find")
public CommonResult<FindActivityDetailListResult> activityDetailFind(Long activityId) {
log.info("activityDetailFind activityId:{}",activityId);
ActivityDetailDTO activityDetailDTO = createActivityService.getActivityDetail(activityId);
return CommonResult.succcess(convertToGetActivityResult(activityDetailDTO));
}
private FindActivityDetailListResult convertToGetActivityResult(ActivityDetailDTO activityDetailDTO) {
if(activityDetailDTO == null) {
throw new ControllerException(ControllerErrorCodeConstants.FIND_ACITVITY_LIST_ERROR);
}
FindActivityDetailListResult findActivityDetailListResult = new FindActivityDetailListResult();
findActivityDetailListResult.setActivityId(activityDetailDTO.getActivityId());
findActivityDetailListResult.setActivityName(activityDetailDTO.getActivityName());
findActivityDetailListResult.setDescription(activityDetailDTO.getDescription());
findActivityDetailListResult.setValid(activityDetailDTO.valid());
findActivityDetailListResult.setPrizes(activityDetailDTO.getActivityPrizeList().stream()
.map(detailDTO->{
FindActivityDetailListResult.Prize prize = new FindActivityDetailListResult.Prize();
prize.setPrizeId(detailDTO.getPrizeId());
prize.setName(detailDTO.getPrizeName());
prize.setImageUrl(detailDTO.getImageUrl());
prize.setPrice(detailDTO.getPrice());
prize.setPrizeAmount(detailDTO.getPrizeAmount());
prize.setDescription(detailDTO.getDescription());
prize.setPrizeTierName(detailDTO.getPrizeTiers().getMessage());
prize.setValid(detailDTO.valid());
return prize;
}).collect(Collectors.toList()));
findActivityDetailListResult.setUsers(activityDetailDTO.getActivityUserList().stream()
.map(detailDTO->{
FindActivityDetailListResult.User user = new FindActivityDetailListResult.User();
user.setUserId(detailDTO.getUserId());
user.setUserName(detailDTO.getUserName());
user.setValid(detailDTO.valid());
return user;}
).collect(Collectors.toList()));
return findActivityDetailListResult;
}
2.3.2:service层:
ActivityDetailDTO getActivityDetail(Long activityId);
serviceimpl:
需要注意的是,之前createActivity时已经将详情信息存入redis缓存,当我们需要时首先从redis中查询相关信息;
如果redis中不存在时,需要从数据库中再次查找,查找出的结果再次存入redis中供以后使用!!
@Override
public ActivityDetailDTO getActivityDetail(Long activityId) {
if(null == activityId) {
throw new ServiceException(ServiceErrorCodeConstatns.ACTIVITY_ID_IS_EMPTY);
}
// 从redis缓存中获取
ActivityDetailDTO activityDetailDTO = getActivityFromCache(activityId);
if (null != activityDetailDTO) {
logger.info("从redis缓存中获取活动信息成功:{}",
JacksonUtil.writeValueAsString(activityDetailDTO));
return activityDetailDTO;
}
// 从数据库获取,并缓存活动数据
activityDetailDTO = getActivityDetailDTO(activityId);
cacheActivity(activityDetailDTO);
logger.info("从数据库中获取活动信息成功:{}",
JacksonUtil.writeValueAsString(activityDetailDTO));
return activityDetailDTO;
}
/**
* 从数据库中获取详细活动信息
* @param activityId
* @return
*/
private ActivityDetailDTO getActivityDetailDTO(Long activityId) {
if(activityId == null) {
log.error("查询活动失败!,activityId为空!");
return null;
}
//查询redis
ActivityDetailDTO activityDetailDTO = getActivityFromCache(activityId);
if(activityDetailDTO != null) {
log.info("查询活动信息成功!:{}",JacksonUtil.writeValueAsString(activityDetailDTO));
return activityDetailDTO;
}
//如果redis中不存在,就在数据库中查
//查询活动信息
ActivityDO activityDO = activityMapper.selectByActivityId(activityId);
if(activityDO == null) {
log.info("getActivityDetailDTO ActivityDO:{}",activityDO);
return null;
}
//查询活动奖品信息
List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.batchByActivityId(activityId);
//查询活动人员信息
List<ActivityUserDO> activityUserDOList = activityUserMapper.batchByActivityId(activityId);
//奖品表:先要查寻关联奖品id
List<Long> prizeIds = activityPrizeDOList.stream()
.map(ActivityPrizeDO::getPrizeId).toList();
List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIds);
//将查询结果打包成ActivityDetail
activityDetailDTO = convertToActivityDetilDTO(activityDO,activityPrizeDOList,activityUserDOList,prizeDOList);
//放入redis
cacheActivity(activityDetailDTO);
return activityDetailDTO;
}
2.4MQ异步抽奖逻辑:
2.4.1:消费类MqReceiver:
@Component
@Slf4j
@RabbitListener(queues = QUEUE_NAME)
public class MqReceive {
@Autowired
private SMSUtil smsUtil;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private DrawPrizeService drawPrizeService;
@Autowired
private ActivityStatusManager activityStatusManager;
@Autowired
private ThreadPoolTaskExecutor asyncServiceExecutor;
@Autowired
private MailUtil mailUtil;
@Autowired
private WinningRecordMapper recordMapper;
@RabbitHandler
public void process(Map<String, String> message) {
log.info("DirectReceiver消费者收到消息 : " + message.toString());
String msgData = message.get("messageData");
DrawPrizeParam param = JacksonUtil.readValue(msgData,
DrawPrizeParam.class);
try {
// 1、核对抽奖信息有效性
drawPrizeService.checkDrawPrizeValid(param);
// 2、扭转活动状态
convertStatus(param);
// 3、保存中奖结果
List<WinningRecordDO> recordDOList =
drawPrizeService.saveWinningRecords(param);
// 4、并发处理后续流程
// 通知中奖者(邮箱、短信)
// 抽奖之后的后续流程,异步(并发)处理
syncExecute(recordDOList);
} catch (ServiceException e) {
log.error("mq消息处理异常:{}", e.getCode(), e);
// 异常回滚中奖结果+活动/奖品状态,保证事务⼀致性
//此消息自动东进入死信队列
rollback(param);
} catch (Exception e) {
log.error("处理 MQ 消息异常!", e);
// 需要保证事务一致性(回滚)
//此消息自动东进入死信队列
rollback(param);
// 抛出异常
throw e;
}
}
2.4.2:请求验证:
接收到信息之后,需要进行对结果的验证操作!!
@Override
public void checkDrawPrizeValid(DrawPrizeParam param) {
//奖品id和活动id对应的奖品活动必须存在
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByActivityIdAndPrizeId(param.getActivityId(),
param.getPrizeId());
//活动id对应的活动必须存在
ActivityDO activityDO = activityMapper.selectByActivityId(param.getActivityId());
if(activityPrizeDO == null || activityDO == null) {
throw new ServiceException(ServiceErrorCodeConstatns.ACTIVITY_OR_PRIZE_NOT_EXIST);
}
//验证活动是否有效
if(activityDO.getStatus().equals(ActivityStatusEnum.COMPLETED.name())) {
throw new ServiceException(ServiceErrorCodeConstatns.ACTIVITY_IS_FAILURE);
}
//验证奖品是否有效
if(activityPrizeDO.getStatus().equals(ActivityPrizeStatusEnum.COMPLETED.name())) {
throw new ServiceException(ServiceErrorCodeConstatns.PRIZE_IS_FAILURE);
}
//验证中奖人数和奖品数量是否一致
if(!(param.getWinnerList().size() == activityPrizeDO.getPrizeAmount())) {
throw new ServiceException(ServiceErrorCodeConstatns.WIINER_PRIZE_AMOUNT_ERROR);
}
}
2.5:状态转换:
验证消息结束之后,就需要对之前的所有状态进行转换!
注意事项:
1.状态转化时应该最后转换的时活动状态!!
2.如果人员\奖品信息全部转换完成以后,才能对活动状态完成转换!
3.如果人员\奖品信息转换失败时需要进行事务的回滚操作!
4.如果日后添加新的模块,也需要等待其他模块状态转换完毕之后,活动状态才可以转换!
综上,面对以上的问题,这里采用两种设计模式合理解决!
责任链模式+策略模式
代码如下:
//map注入常被用在策略模式中
@Autowired
private final Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();
@Autowired
private ActivityService activityService;
@Override
@Transactional(rollbackFor = Exception.class)
public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
// 1、活动状态扭转有依赖性,导致代码维护性差
// 2、状态扭转条件可能会扩展,当前写法,扩展性差,维护性差
if(CollectionUtils.isEmpty(operatorMap)) {
log.warn("operatorMap 为空! 无法处理活动扭转");
return ;
}
Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);
Boolean update;
//先处理人员和奖品
update = processConvertStatus(convertActivityStatusDTO,currMap,1);
//最后处理活动
update = (processConvertStatus(convertActivityStatusDTO,currMap,2) || update);
//更新缓存
if(update) {
log.info("更新缓存成功!");
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
}
注:
1.这里 resquence的设计,就是责任链模式,也就是如果其他的方式没有执行结束,该方法就不能被执行!
2.这里Map的设计就是策略模式,每个模块有自己的处理扭转状态的方式!!
2.5.1:ActivityOperator:
关于活动相关的处理方法:
@Component
@Slf4j
public class ActivityOperator extends AbstractActivityOperator {
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 2;
}
@Override
public Boolean needCovert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
ActivityStatusEnum tagertEnum = convertActivityStatusDTO.getTargetActivityStatus();
if(null == activityId || tagertEnum == null) {
log.error("ActivityOperator needCovert 活动id:{}错误",activityId);
return false;
}
ActivityDO activityDO = activityMapper.selectByActivityId(activityId);
if(activityDO == null) {
log.error("ActivityOperator needCovert 活动信息错误:{}",activityDO);
return false;
}
//判断当前活动状态是否一致
//如果一致就不需要更新
if(activityDO.getStatus().equals(tagertEnum.name())) {
log.error("ActivityOperator needCovert 活动状态错误:{}",activityDO.getStatus());
return false;
}
//需要判断奖品是否全部抽完
//查询INIT状态下奖品的数量
int count = activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name());
if(count>0) {
log.info("ActivityOperator needCovert 奖品还剩:{}",count);
return false;
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//更新数据库状态
try{
activityMapper.updateStatus(convertActivityStatusDTO.getActivityId(),
convertActivityStatusDTO.getTargetActivityStatus().name());
log.info("activityMapper 更新成功!");
return true;
}catch (Exception e) {
return false;
}
}
}
2.5.2:UserOperator:
与人员有关的处理方法:
@Component
@Slf4j
public class UserOperator extends AbstractActivityOperator {
@Autowired
private ActivityUserMapper activityUserMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needCovert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
List<Long> userIds = convertActivityStatusDTO.getUserIds();
ActivityUserStatusEnum activityUserStatusEnum = convertActivityStatusDTO.getTargetUserStatus();
if(userIds == null || activityUserStatusEnum == null ||
activityId == null) {
log.info("所传参数为空 不更新!");
return false;
}
//通过id查询活动人员表
List<ActivityUserDO> activityUserDOList = activityUserMapper.batchSelectByAUIds(activityId,userIds);
if(activityUserDOList == null) {
log.info("人员表为空 不更新!");
return false;
}
//判断当前人员状态是否与转换状态一致
for(ActivityUserDO activityUserDO:activityUserDOList) {
if(activityUserDO.getStatus().equalsIgnoreCase(activityUserStatusEnum.name())) {
log.info("状态一致 不更新!");
return false;
}
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
try {
activityUserMapper.batchUpdateStatus(convertActivityStatusDTO
.getTargetUserStatus().name(),convertActivityStatusDTO.getUserIds(),
convertActivityStatusDTO.getActivityId());
log.info("activityUserMapper 更新成功!");
return true;
}catch (Exception e){
return false;
}
}
}
2.5.3:PrizeOperator:
与奖品状态有关的处理方式:
@Component
@Slf4j
public class PrizeOperator extends AbstractActivityOperator {
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needCovert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
Long prizeId = convertActivityStatusDTO.getPrizeId();
ActivityPrizeStatusEnum activityPrizeStatusEnum = convertActivityStatusDTO.getTargetPrizeStatus();
if(prizeId == null || activityPrizeStatusEnum == null ||
activityId == null) {
log.info("所传参数为空 不更新!");
return false;
}
//通过id查询活动奖品表
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByActivityIdAndPrizeId(activityId,prizeId);
if(activityPrizeDO == null) {
log.info("活动奖品表为哦空 不更新!");
return false;
}
//判断当前奖品状态是否与转换状态一致
if(activityPrizeStatusEnum.name().equals(activityPrizeDO.getStatus())) {
log.info("奖品状态与期望状态一致 不更新!");
return false;
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//更新数据库状态
try{
activityPrizeMapper.updateStatus(convertActivityStatusDTO.getPrizeId(),
convertActivityStatusDTO.getActivityId(),
convertActivityStatusDTO.getTargetPrizeStatus().name());
log.info("activityPrizeMapper 更新成功!");
return true;
}catch (Exception e) {
return false;
}
}
}
2.5:状态回滚:
如果此时我们正在抽奖,发生了以外,例如网络突然断开,或者页面突然关闭等不可预知的操作时!
此时除了我们保存到的数据之外,发生意外后上传的数据应该进行数据回滚操作!!保证事务的统一性,也避免出现不可预知的bug!!
private void rollback(DrawPrizeParam param) {
// 1、回滚状态:活动、奖品、人员
// 状态是否需要回滚
if (!statusNeedRollback(param)) {
// 不需要:return
return;
}
// 需要回滚: 回滚
rollbackStatus(param);
// 2、回滚中奖者名单
// 是否需要回滚
if (!winnerNeedRollback(param)) {
// 不需要:return
return;
}
// 需要: 回滚
rollbackWinner(param);
}
private boolean statusNeedRollback(DrawPrizeParam param) {
// 判断活动+奖品+人员表相关状态是否已经扭转(正常思路)
// 扭转状态时,保证了事务一致性,要么都扭转了,要么都没扭转(不包含活动):
// 因此,只用判断人员/奖品是否扭转过,就能判断出状态是否全部扭转
// 不能判断活动是否已经扭转
// 结论:判断奖品状态是否扭转,就能判断出全部状态是否扭转
ActivityPrizeDO activityPrizeDO =
activityPrizeMapper.selectByActivityIdAndPrizeId(param.getActivityId(), param.getPrizeId());
// 已经扭转了,需要回滚
return activityPrizeDO.getStatus()
.equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
}
private void rollbackWinner(DrawPrizeParam param) {
drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
}
private void rollbackStatus(DrawPrizeParam param) {
// 涉及状态的恢复,使用 ActivityStatusManager
ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
convertActivityStatusDTO.setActivityId(param.getActivityId());
convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
convertActivityStatusDTO.setPrizeId(param.getPrizeId());
convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT);
convertActivityStatusDTO.setUserIds(
param.getWinnerList().stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT);
activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO);
}
步骤:
1.先判断是否需要回滚!
2.如果此时发生意外,抛出异常,此时需要判断一下人员\奖品是否扭转,如果其中之一已经扭转了,那么剩下的奖品\人员与活动状态均需要进行状态扭转
3.接下来判断中奖者名单需不需要扭转(删除)。
2.6:线程池配置:
当活动完成后,如果没有什么异常出现,此时就需要将获奖信息发送给获奖者!!
发送的形式分为两种:
1.短信发送
2.邮箱发送
这里可能获奖人数非常多,也可能使用该产品的用户非常多!
因此为了避免出现卡顿等延迟现象,这里采用多线程的方式进行短信和邮件的发送!!
properties.xml相关配置:
## 线程池 ## ##核心线程数 async.executor.thread.core_pool_size=10 ##最大线程数 async.executor.thread.max_pool_size=20 ##队列容量 async.executor.thread.queue_capacity=20 ##线程前缀 async.executor.thread.name.prefix=async-service-
相关配置说明如下:
- 核心线程数:线程池创建时候初始化的线程数。当线程数超过核心线程数,则超过的线程则进入任务队列。
- 最大线程数:只有在任务队列满了之后才会申请超过核心线程数的线程。不能小于核心线程数。
- 任务队列:线程数大于核心线程数的部分进入任务队列。如果任务队列足够大,超出核心线程数的线程不会被创建,它会等待核心线程执行完它们自己的任务后再执行任务队列的任务,而不会再额外地创建线程。举例:如果有20个任务要执行,核心线程数:10,最大线程数:20,任务队列大小:2。则系统会创建18个线程。这18个线程有执行完任务的,再执行任务队列中的任务。
- 线程的空闲时间:当 线程池中的线程数量 大于 核心线程数 时,如果某线程空闲时间超过 keepAliveTime ,线程将被终止。这样,线程池可以动态的调整池中的线程数。
拒绝策略:如果(总任务数 - 核心线程数 - 任务队列数)-(最大线程数 - 核心线程数)> 0 的话,则会出现线程拒绝。举例:( 12 - 5 - 2 ) - ( 8 - 5 ) > 0,会出现线程拒绝。线程拒绝又分为 4 种策略,分别为:
- CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
- AbortPolicy():直接抛出异常。
- DiscardPolicy():直接丢弃。
- DiscardOldestPolicy():丢弃队列中最老的任务。
2.6.1:异步处理方法:
private void syncExecute(List<WinningRecordDO> recordDOList) {
// 通过线程池 threadPoolTaskExecutor
// 扩展:加入策略模式或者其他设计模式来完成后续的异步操作
// 短信通知
asyncServiceExecutor.execute(()->sendMessage(recordDOList));
//邮箱通知
asyncServiceExecutor.execute(()->sendMail(recordDOList));
}
发送的短信和邮件的内容可以自己确定,当然在使用这两者的同时,还需要引入对应的依赖包和配置相关的配置项!!
2.7:展示每次抽奖中奖信息:
该过程是在每次抽完一种奖品之后需要展示中奖信息:
2.7.1:controller层:
@RequestMapping("/winning-records/show")
public CommonResult<List<WinningRecordResult>> showWinningRecord(
@RequestBody @Validated ShowWinningRecordParam param) {
log.info("showWinningRecord winningRecordDTO:{}",JacksonUtil.writeValueAsString(param));
List<WinningRecordDTO> winningRecordDTOList = drawPrizeService.showWinningRecord(param);
return CommonResult.succcess(convrtToWinningRecordResult(winningRecordDTOList));
}
2.7.2:service层:
List<WinningRecordDTO> showWinningRecord(ShowWinningRecordParam param);
serviceimpl层:
@Override
public List<WinningRecordDTO> showWinningRecord(ShowWinningRecordParam param) {
// 查询redis: 奖品、活动
//可以从奖品维度也可以从活动维度
String key = null == param.getPrizeId()
? String.valueOf(param.getActivityId())
: param.getActivityId() + "_" + param.getPrizeId();
List<WinningRecordDO> winningRecordDOList = getWinningRecords(key);
if (!CollectionUtils.isEmpty(winningRecordDOList)) {
return convertToWinningRecordDTOList(winningRecordDOList);
}
//Redis中数据可能过期
//如果redis不存在,查库
winningRecordDOList = recordMapper.selectByActivityIdOrPrizeId(
param.getActivityId(), param.getPrizeId());
// 存放记录到redis
if (CollectionUtils.isEmpty(winningRecordDOList)) {
log.info("查询的中奖记录为空!param:{}",
JacksonUtil.writeValueAsString(param));
return Arrays.asList();
}
cacheWinningRecords(key, winningRecordDOList);
return convertToWinningRecordDTOList(winningRecordDOList);
}
2.7.3:dao层:
@Select("<script>" +
" select * from winning_record" +
" where activity_id = #{activityId}" +
" <if test=\"prizeId != null\">" +
" and prize_id = #{prizeId}" +
" </if>" +
" </script>")
List<WinningRecordDO> selectByActivityIdOrPrizeId(@Param("activityId") Long activityId,
@Param("prizeId") Long prizeId);
接下来就可以完善diamagnetic,最后进行项目的部署工作啦!!