目录
- 一、抽奖流程
- 二、MQ配置
- 三、MQ生产者(抽奖异步接口)
-
- [3.1 抽奖请求处理简介](#3.1 抽奖请求处理简介)
- [3.2 参数列表](#3.2 参数列表)
- [3.3 接口规范](#3.3 接口规范)
- [3.4 controller 层](#3.4 controller 层)
-
- [3.4.1 DrawPrizeController 参数接收类](#3.4.1 DrawPrizeController 参数接收类)
- [3.5 service 层](#3.5 service 层)
-
- [3.5.1 创建接口](#3.5.1 创建接口)
- [3.5.2 实现接口](#3.5.2 实现接口)
- [3.6 测试](#3.6 测试)
- 七、MQ消费者
-
- [7.1 接收消息功能](#7.1 接收消息功能)
- [7.2 校验抽奖请求](#7.2 校验抽奖请求)
-
- [7.2.1 创建接口](#7.2.1 创建接口)
- [7.2.2 实现接口](#7.2.2 实现接口)
- [7.2.3 查表方法](#7.2.3 查表方法)
- [7.3 状态扭转(责任链模式、策略模式)](#7.3 状态扭转(责任链模式、策略模式))
-
- [7.3.1 状态扭转调用](#7.3.1 状态扭转调用)
- [7.3.2 状态扭转具体实现](#7.3.2 状态扭转具体实现)
-
- [7.3.2.1 状态扭转接口](#7.3.2.1 状态扭转接口)
- [7.3.2.2 实现状态扭转接口](#7.3.2.2 实现状态扭转接口)
-
- [7.3.2.2.1 processConvertStatus:扭转状态方法](#7.3.2.2.1 processConvertStatus:扭转状态方法)
- [7.4.2.2.2 控制状态扭转抽象类](#7.4.2.2.2 控制状态扭转抽象类)
- [7.4.2.2.3 ActivityOperator :活动状态扭转控制类](#7.4.2.2.3 ActivityOperator :活动状态扭转控制类)
- [7.4.2.2.4 PrizeOperator :奖品状态扭转控制类](#7.4.2.2.4 PrizeOperator :奖品状态扭转控制类)
- [7.4.2.2.5 UserOperator :中奖人员状态扭转控制类](#7.4.2.2.5 UserOperator :中奖人员状态扭转控制类)
- [7.4.2.2.6 缓存Redis更新](#7.4.2.2.6 缓存Redis更新)
- [7.4.2.2.6.1 接口定义](#7.4.2.2.6.1 接口定义)
- [7.4.2.2.6.2 接口实现](#7.4.2.2.6.2 接口实现)
- [7.3.3 ConvertActivityStatusDTO :抽奖相关需要状态扭转的类](#7.3.3 ConvertActivityStatusDTO :抽奖相关需要状态扭转的类)
- [7.3.4 dao层修改](#7.3.4 dao层修改)
- [7.3.5 更新Redis缓存中的状态](#7.3.5 更新Redis缓存中的状态)
-
- [7.3.5.1 接口创建](#7.3.5.1 接口创建)
- [7.3.5.2 接口实现](#7.3.5.2 接口实现)
- [7.3.6 总结](#7.3.6 总结)
- [7.4 保存中奖者名单](#7.4 保存中奖者名单)
-
- [7.4.1 接口创建](#7.4.1 接口创建)
- [7.4.2 实现接口](#7.4.2 实现接口)
- [7.4.3 WinningRecodesDO 中奖者类](#7.4.3 WinningRecodesDO 中奖者类)
- [7.4.4 缓存方法与获取方法](#7.4.4 缓存方法与获取方法)
- [7.4.5 dao层新增Mapper方法](#7.4.5 dao层新增Mapper方法)
- [7.5 邮箱通知中奖者](#7.5 邮箱通知中奖者)
-
- [7.5.1 添加方法](#7.5.1 添加方法)
- [7.5.2 配置线程池](#7.5.2 配置线程池)
- [7.5.3 异步发送](#7.5.3 异步发送)
- [7.6 抽奖回滚实现](#7.6 抽奖回滚实现)
-
- [7.6.1 判断状态是否需要回滚](#7.6.1 判断状态是否需要回滚)
- [7.6.2 状态回滚方法](#7.6.2 状态回滚方法)
-
- [7.6.2.1 接口创建](#7.6.2.1 接口创建)
- [7.6.2.2 接口实现](#7.6.2.2 接口实现)
- [7.6.3 判断中奖记录是否需要回滚](#7.6.3 判断中奖记录是否需要回滚)
- [7.6.4 中奖记录回滚](#7.6.4 中奖记录回滚)
-
- [7.6.4.1 创建接口](#7.6.4.1 创建接口)
- [7.6.4.2 实现接口](#7.6.4.2 实现接口)
- [7.6.4.3 dao层](#7.6.4.3 dao层)
- [7.7 消费者主流程](#7.7 消费者主流程)
- 八、死信队列消费者

一、抽奖流程

- 参与者注册与奖品建⽴
1.1. 参与者注册:管理员通过管理端新增⽤⼾, 填写必要的信息,如姓名、联系⽅式等。
1.2. 奖品建⽴:奖品需要提前建⽴好 - 抽奖活动设置
2.1. 活动创建:管理员在系统中创建抽奖活动,输⼊活动名称、描述、奖品列表等信息。
2.2. 圈选⼈员: 关联该抽奖活动的参与者。
2.3. 圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等。
2.4. 活动发布:活动信息发布后,系统通过管理端界⾯展⽰活动列表。 - 抽奖请求处理(重要)
3.1. 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。
3.2. 请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加
信息。
3.3. 消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。
请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。 - 抽奖结果公布
4.1. 前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。 - 抽奖逻辑执⾏(重要)
5.1. 消息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。 - 中奖结果处理(重要)
6.1. 请求验证:
6.2. 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数限制等)等;
6.3. 幂等性:若消息多发,已抽取的内容不能再次抽取
6.4. 状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取,⼈员是否已中奖等。
6.5. 结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存。 - 中奖者通知
7.1. 通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。
7.2. 奖品领取:中奖者根据通知中的指引领取奖品。 - 抽奖异常处理
8.1. 回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。
8.2. 补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补救措施
技术实现细节
- 异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂等性。
- 活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。
- 事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要做好事务处理。
二、MQ配置
pom.xml:
xml
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.yml :
java
spring:
rabbitmq: ## mq ##
host: 自己服务器地址
port: 5672
username: admin(自己的)
password: admin(自己的)
listener:
simple:
acknowledge-mode: auto #消息确认机制,默认auto
retry:
enabled: true
max-attempts: 5 #设置失败重试 5次
配置类:
com.yj.lottery_system.common.config 包下:DirectRabbitConfig 类
java
package com.yj.lottery_system.common.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DirectRabbitConfig {
public static final String QUEUE_NAME = "DirectQueue";
public static final String EXCHANGE_NAME = "DirectExchange";
public static final String ROUTING = "DirectRouting";
public static final String DLX_QUEUE_NAME = "DlxDirectQueue";
public static final String DLX_EXCHANGE_NAME = "DlxDirectExchange";
public static final String DLX_ROUTING = "DlxDirectRouting";
/**
* 队列 起名:DirectQueue
*
* @return
*/
@Bean
public Queue directQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使⽤,⽽且当连接关闭后队列即被删除。此参考优先级⾼于durable
// autoDelete:是否⾃动删除,当没有⽣产者或者消费者使⽤此队列,该队列会⾃动删除。
// return new Queue("DirectQueue",true,true,false);
// ⼀般设置⼀下队列的持久化就好,其余两个就是默认false
return QueueBuilder.durable(QUEUE_NAME)
.deadLetterExchange(DLX_EXCHANGE_NAME)
.deadLetterRoutingKey(DLX_ROUTING).build();
}
/**
* 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);
}
/**
* 死信队列 起名:DirectQueue
*
* @return
*/
@Bean
public Queue dlxQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使⽤,⽽且当连接关闭后队列即被删除。此参考优先级⾼于durable
// autoDelete:是否⾃动删除,当没有⽣产者或者消费者使⽤此队列,该队列会⾃动删除。
// return new Queue("DirectQueue",true,true,false);
// ⼀般设置⼀下队列的持久化就好,其余两个就是默认false
return new Queue(DLX_QUEUE_NAME,true);
}
/**
* Direct交换机 起名:DirectExchange
*
* @return
*/
@Bean
DirectExchange dlxExchange() {
return new DirectExchange(DLX_EXCHANGE_NAME,true,false);
}
/**
* 绑定 将死信队列和交换机绑定, 并设置⽤于匹配键:DirectRouting
*
* @return
*/
@Bean
Binding bindingdlx() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with(DLX_ROUTING);
}
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
三、MQ生产者(抽奖异步接口)
3.1 抽奖请求处理简介
时序图:

3.2 参数列表
| 参数名 | 描述 | 类型 | 默认值 | 条件 |
|---|---|---|---|---|
| activityId | 活动id | Long | 必须 | |
| prizeId | 正在抽的奖品id | Long | 必须 | |
| prizeTiers | 正在抽的奖品等级 | String | 必须 | |
| winningTime | 中奖时间 | Date | 必须 | |
| winnerList | 中奖人员列表 | List | 必须 |
3.3 接口规范
java
[请求] /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": ""
}
3.4 controller 层
com.yj.lottery_system.controller 包下:DrawPrizeController 类
- DrawPrizeController 参数接收类
- 前端传的JSON,使用@RequestBody,对参数进行校验使用@Valid
- 打印调用日志
- 调用service
- 返回
java
package com.yj.lottery_system.controller;
import com.yj.lottery_system.common.pojo.CommonResult;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import com.yj.lottery_system.service.IDrawPrizeService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DrawPrizeController {
private static final Logger log = LoggerFactory.getLogger(DrawPrizeController.class);
@Resource
private IDrawPrizeService drawPrizeService;
/**
* 异步抽奖接口
* @param param
* @return
*/
@RequestMapping("/draw-prize")
public CommonResult<Boolean> drawPrize(@RequestBody@Valid DrawPrizeParam param){
//打印日志
log.info("drawPrize DrawPrizeParam : {}", JacksonUtil.writeValueAsString(param));
//调用service
drawPrizeService.drawPrize(param);
return CommonResult.success(true);
}
}
3.4.1 DrawPrizeController 参数接收类
com.yj.lottery_system.controller.param 包下:
- 根据接口规范,有以下参数,并且都是必传参数
java
package com.yj.lottery_system.controller.param;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Data
public class DrawPrizeParam implements Serializable {
//活动id
@NotNull(message = "活动id不能为空")
private Long activityId;
//正在抽的奖品id
@NotNull(message = "奖品id不能为空")
private Long prizeId;
//正在抽的奖品等级
@NotBlank(message = "奖品等级不能为空")
private String prizeTiers;
//中奖时间
@NotNull(message = "中奖时间不能为空")
private Date winningTime;
//中奖人员列表
@NotEmpty(message = "中奖人员不能为空")
@Valid
private List<Winner> winnerList;
@Data
public static class Winner {
//中奖人员id
@NotNull(message = "中奖人员id不能为空")
private Long userId;
//中奖人员名字
@NotBlank(message = "中奖人员名字不能为空")
private String userName;
}
}
3.5 service 层
3.5.1 创建接口
com.yj.lottery_system.service 包下:
创建 抽奖对应的接口类
java
package com.yj.lottery_system.service;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import org.springframework.stereotype.Service;
@Service
public interface IDrawPrizeService {
/**
* 异步抽奖接口
* @param param
*/
void drawPrize(DrawPrizeParam param);
}
3.5.2 实现接口
com.yj.lottery_system.service.impl 包下:
java
package com.yj.lottery_system.service.impl;
import com.yj.lottery_system.common.utils.JacksonUtil;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import com.yj.lottery_system.service.IDrawPrizeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static com.yj.lottery_system.common.config.DirectRabbitConfig.EXCHANGE_NAME;
import static com.yj.lottery_system.common.config.DirectRabbitConfig.ROUTING;
@Service
public class DrawPrizeServiceImpl implements IDrawPrizeService {
private final static Logger log = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 异步抽奖接口
* @param param
*/
@Override
public void drawPrize(DrawPrizeParam param) {
Map<String,String> map = new HashMap<>();
map.put("messageId", String.valueOf(UUID.randomUUID()));
map.put("messageData", JacksonUtil.writeValueAsString(param));
//发消息: 交换机、绑定key、消息体
rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING,map);
log.info("mq消息发送成功: map: {}", JacksonUtil.writeValueAsString(map));
}
}
3.6 测试


七、MQ消费者
时序图:

7.1 接收消息功能
根据前面的发送消息使用的Map<String,String>,接收就也使用。
@RabbitHandler 方法注解来接收队列消息
@RabbitListener(queues = 队列名) 类注解来对应RabbitMQ
java
//成功接收队列消息
log.info("mq成功接收到消息 message:{}",
JacksonUtil.writeValueAsString(message));
String paramString = message.get("messageData");
DrawPrizeParam param = JacksonUtil.readValue(paramString,DrawPrizeParam.class);
7.2 校验抽奖请求
我们单独写一个方法service层方法实现
7.2.1 创建接口
com/yj/lottery_system/service 包下 IDrawPrizeService.java类中
java
/**
* 校验抽奖请求
* @param param
*/
Boolean checkDrawPrizeParam(DrawPrizeParam param);
7.2.2 实现接口
com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java类中
- 我们需要校验活动,奖品是否存在,并且还要校验状态
- 还要校验奖品数和中奖人数是否一样
- 我们在activity_prize 活动奖品关联表中 在创建活动的时候是使用了本地事务,保证了一致性的,所以我们查这张表就可以拿到正确的奖品信息:奖品是否存在 奖品状态 奖品数(用活动id和奖品id组成唯一索引)
java
/**
* 校验抽奖请求
* @param param
*/
@Override
public Boolean checkDrawPrizeParam(DrawPrizeParam param) {
//活动是否存在
ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
//奖品是否存在,从activity_prize表获取,因为使用了本地事务,保持一致性
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
//活动和奖品是否存在
if(null == activityDO || null == activityPrizeDO) {
//throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY);
log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY.getMsg());
return false;
}
//活动是否有效
if(activityDO.getStatus().equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) {
//throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED);
log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.ACTIVITY_COMPLETED.getMsg());
return false;
}
//奖品是否有效
if(activityPrizeDO.getStatus().equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name())) {
//throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED);
log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED.getMsg());
return false;
}
//中奖者人数是否与奖品数量一致
if(activityPrizeDO.getPrizeAmount() != param.getWinnerList().size()) {
//throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR);
log.info("校验抽奖请求失败:"+ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR.getMsg());
return false;
}
return true;
}
7.2.3 查表方法
com/yj/lottery_system/dao/mapper 包下 ActivityPrizeMapper.java 类:
java
/**
* 根据活动id与奖品id组成唯一索引 查活动关联奖品信息
* @param activityId
* @return
*/
@Select("select * from activity_prize where activity_id = #{activityId} and prize_id = #{prizeId}")
ActivityPrizeDO selectByAPId(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);
7.3 状态扭转(责任链模式、策略模式)
我们需要扭转活动状态,活动关联奖品状态,活动关联人员状态
- 活动关联奖品状态:完成一次抽奖动作后,被抽的奖品的状态扭转,INIT ----> COMPLETED
-
- 查询本次抽取的奖品
-
- 虽然前面 校验抽奖请求中 校验了奖品状态必须是INIT,但是为了降低耦合性,还是需要在这判断
-
- 状态为 INIT 扭转
- 活动关联人员状态:完成一次抽奖动作后,中奖人员的状态扭转,INIT ----> COMPLETED
-
- 查询本次中奖人员
-
- 虽然前面 校验抽奖请求中 校验了中奖人员状态必须是INIT,但是为了降低耦合性,还是需要在这判断
-
- 状态为 INIT 扭转
- 活动状态:全部奖品抽完之后,状态扭转,RUNNING----> COMPLETED
-
- 查询活动信息
-
- 判断活动中奖品状态全部为 COMPLETED
- 虽然前面 校验抽奖请求中 校验了活动状态必须是RUNNING,但是为了降低耦合性,还是需要在这判断
-
- 状态为 RUNNING扭转
- 更新Redis缓存
上诉过程存在的问题:
- 活动状态依赖于奖品状态,代码维护性差
- 状态扭转条件如果拓展,代码维护性,扩展性差
解决方法:
- 责任链模式+策略模式:将处理顺序定死,先处理的与后处理的区分开来,状态扭转实现每个类自己的方法。
-
- 我们将要处理的状态都单独拿出来一个类表示,都继承一个抽象类,这个抽象类里面有一个抽象方法返回先后处理的标识,我们处理的时候就根据这个标识来区分处理先后顺序。
-
- 这样就解决了依赖性
- 我们在抽象类中写一个判断状态扭转条件的抽象方法,每一个继承类都实现自己的扭转状态条件。
-
- 这样就解决了代码扩展性与维护性
7.3.1 状态扭转调用
com/yj/lottery_system/service/mq 包下 MqReceiver.java 类中
- 设置状态扭转需要的参数,调用状态扭转实现方法
java
/**
* 状态扭转
* @param param
*/
private void statusConvert(DrawPrizeParam param) {
ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
convertActivityStatusDTO.setActivityId(param.getActivityId());
convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
convertActivityStatusDTO.setPrizeId(param.getPrizeId());
convertActivityStatusDTO.setTargetActivityPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
convertActivityStatusDTO.setUserIds(param.getWinnerList().stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
convertActivityStatusDTO.setTargetActivityUserStatus(ActivityUserStatusEnum.COMPLETED);
//调用状态扭转方法
activityStatusManager.handleEvent(convertActivityStatusDTO);
}
7.3.2 状态扭转具体实现
7.3.2.1 状态扭转接口
com.yj.lottery_system.service.activityStatus 包下:
- 定义状态扭转的接口:
- 封装一下
java
package com.yj.lottery_system.service.activityStatus;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import org.springframework.stereotype.Service;
@Service
public interface IActivityStatusManager {
/**
* 处理活动相关状态转换
* @param convertActivityStatusDTO
*/
void handleEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
}
7.3.2.2 实现状态扭转接口
com.yj.lottery_system.service.activityStatus.impl 包下 ActivityStatusManagerImpl 类
- 实现状态扭转的主逻辑:
- 我们将每一个需要扭转的状态都封装成一个类,类里面实现自己的扭转方法,
- 将需要扭转的状态使用注解注入到一个Map管理起来
- 非空校验
- 调用两次processConvertStatus方法先后处理Map里面需要扭转状态的类,并返回扭转 - true,未扭转 - false
- 如果扭转了,调用缓存更新方法。
java
package com.yj.lottery_system.service.activityStatus.impl;
import com.yj.lottery_system.common.errorcode.ServiceErrorCodeConstants;
import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.service.IActivityService;
import com.yj.lottery_system.service.activityStatus.IActivityStatusManager;
import com.yj.lottery_system.service.activityStatus.operator.AbstractActivityOperator;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@Service
public class ActivityStatusManagerImpl implements IActivityStatusManager {
private static final Logger log = LoggerFactory.getLogger(ActivityStatusManagerImpl.class);
@Autowired
private Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();
@Resource
private IActivityService activityService;
/**
* 处理活动相关状态转换
* @param convertActivityStatusDTO
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void handleEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
if(CollectionUtils.isEmpty(operatorMap)) {
log.warn("operatorMap 为空");
return;
}
//拿到需要处理的
Map<String, AbstractActivityOperator> curMap = new HashMap<>(operatorMap);
//更新标志位
Boolean updateFlag = false;
//先处理
updateFlag = processConvertStatus(convertActivityStatusDTO,curMap,1);
//后处理
updateFlag = processConvertStatus(convertActivityStatusDTO,curMap,2) || updateFlag;
//更新过,更新缓存
if(updateFlag) {
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
}
}
7.3.2.2.1 processConvertStatus:扭转状态方法
- 遍历当前的Map,拿到需要扭转状态的operator,
- 根据sequence 方法判断是否是该当前扭转的
- 调用operator 自己类实现的扭转方法
- 如果扭转了,从当前Map删除
java
/**
* 扭转 curMap 里面 sequence 的状态
* @param convertActivityStatusDTO
* @param curMap
* @param sequence
* @return
*/
private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,
Map<String, AbstractActivityOperator> curMap,
int sequence) {
//更新标志位
Boolean updateFlag = false;
//遍历 curMap
Iterator<Map.Entry<String, AbstractActivityOperator>> iterator = curMap.entrySet().iterator();
while(iterator.hasNext()) {
AbstractActivityOperator operator = iterator.next().getValue();
//判断operator是否需要扭转状态
if(operator.sequence() != sequence
|| !operator.needConvert(convertActivityStatusDTO)) {
continue;
}
//需要扭转 扭转状态
if(!operator.convert(convertActivityStatusDTO)) {
log.error("{} 状态扭转失败 ",operator.getClass().getName());
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR);
}
//在curMap删除当前operator
iterator.remove();
updateFlag = true;
}
//返回
return updateFlag;
}
7.4.2.2.2 控制状态扭转抽象类
com.yj.lottery_system.service.activityStatus.operator :
里面定义三个抽象方法:
- sequence:返回 继承类状态扭转先后顺序 方法
- needConvert:判断继承类是否需要状态扭转
- Convert:继承类自己状态扭转的方法
java
package com.yj.lottery_system.service.activityStatus.operator;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
public abstract class AbstractActivityOperator {
/**
* 控制处理顺序
* @return
*/
public abstract Integer sequence();
/**
* 是否需要状态扭转
* @param convertActivityStatusDTO
* @return
*/
public abstract Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO);
/**
* 转换方法
* @param convertActivityStatusDTO
* @return
*/
public abstract Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO);
}
7.4.2.2.3 ActivityOperator :活动状态扭转控制类
com.yj.lottery_system.service.activityStatus.operator 包下:
- 继承抽象类AbstractActivityOperator
-
- 实现抽象方法 sequence :返回2 代表扭转顺序 后处理
-
- 实现抽象方法 needConvert:判断是否需要扭转状态
-
-
- 校验活动id 和 活动目标扭转状态
-
-
-
- 调用dao,求该活动下 奖品RUNNING状态 的个数
-
-
-
- 个数大于0 不扭转
-
- 实现抽象方法 convert:扭转状态
-
- 调用dao对应方法即可
java
package com.yj.lottery_system.service.activityStatus.operator;
import com.yj.lottery_system.dao.dataObject.ActivityDO;
import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.mapper.ActivityMapper;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class ActivityOperator extends AbstractActivityOperator{
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 2;
}
@Override
public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//活动id
Long activityId = convertActivityStatusDTO.getActivityId();
//活动目标扭转状态
ActivityStatusEnum targetActivityStatus = convertActivityStatusDTO.getTargetActivityStatus();
if (null == activityId || null == targetActivityStatus) {
return false;
}
//调用dao
ActivityDO activityDO = activityMapper.selectById(activityId);
if(null == activityDO) {
return false;
}
//判断当前状态与目标状态
if(targetActivityStatus.name().equals(activityDO.getStatus())) {
return false;
}
//该活动下 奖品INIT状态 的个数
int count = activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name());
if(count > 0) {
return false;
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//调用dao,更新状态
try {
activityMapper.updateStatus( convertActivityStatusDTO.getActivityId(),
convertActivityStatusDTO.getTargetActivityStatus().name());
return true;
}catch (Exception e) {
return false;
}
}
}
7.4.2.2.4 PrizeOperator :奖品状态扭转控制类
com.yj.lottery_system.service.activityStatus.operator 包下:
- 继承抽象类AbstractActivityOperator
-
- 实现抽象方法 sequence :返回1 代表扭转顺序 先处理
-
- 实现抽象方法 needConvert:判断是否需要扭转状态
-
-
- 校验活动id 和 j奖品id 和 奖品目标扭转状态
-
-
-
- 调用dao,拿到被抽取 的奖品
-
-
-
- 判断奖品状态与目标状态,一致返回false,不同返回true
-
- 实现抽象方法 convert:扭转状态
-
- 调用dao对应方法即可
java
package com.yj.lottery_system.service.activityStatus.operator;
import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityPrizeStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class PrizeOperator extends AbstractActivityOperator{
@Resource
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//活动id
Long activityId = convertActivityStatusDTO.getActivityId();
//奖品id
Long prizeId = convertActivityStatusDTO.getPrizeId();
//目标状态
ActivityPrizeStatusEnum targetActivityPrizeStatus = convertActivityStatusDTO.getTargetActivityPrizeStatus();
if(null == activityId || null == prizeId || null == targetActivityPrizeStatus ) {
return false;
}
//拿奖品
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(activityId, prizeId);
if(null == activityPrizeDO) {
return false;
}
//当前奖品状态与目标状态
if(activityPrizeDO.getStatus().equals(targetActivityPrizeStatus.name())) {
return false;
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
try {
activityPrizeMapper.updateStatus(convertActivityStatusDTO.getActivityId(),
convertActivityStatusDTO.getPrizeId(),
convertActivityStatusDTO.getTargetActivityPrizeStatus().name());
return true;
}catch (Exception e) {
return false;
}
}
}
7.4.2.2.5 UserOperator :中奖人员状态扭转控制类
com.yj.lottery_system.service.activityStatus.operator 包下:
- 继承抽象类AbstractActivityOperator
-
- 实现抽象方法 sequence :返回1 代表扭转顺序 先处理
-
- 实现抽象方法 needConvert:判断是否需要扭转状态
-
-
- 校验活动id 和 中奖者 id 列表 和 中奖者目标扭转状态
-
-
-
- 调用dao,拿到中奖者列表
-
-
-
- 判断中奖者状态与目标状态,一致返回false,不同返回true
-
- 实现抽象方法 convert:扭转状态
-
- 调用dao对应方法即可
java
package com.yj.lottery_system.service.activityStatus.operator;
import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.dataObject.ActivityUserDO;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.dao.mapper.ActivityUserMapper;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import com.yj.lottery_system.service.enums.ActivityUserStatusEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Component
public class UserOperator extends AbstractActivityOperator{
@Resource
private ActivityUserMapper activityUserMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//活动id
Long activityId = convertActivityStatusDTO.getActivityId();
//人员id列表
List<Long> userIds = convertActivityStatusDTO.getUserIds();
//目标状态
ActivityUserStatusEnum targetActivityUserStatus = convertActivityStatusDTO.getTargetActivityUserStatus();
if(null == activityId
|| CollectionUtils.isEmpty(userIds)
|| null == targetActivityUserStatus) {
return false;
}
//调用dao
List<ActivityUserDO> activityUserDOList = activityUserMapper.batchSelectByAUId(activityId,userIds);
if(CollectionUtils.isEmpty(activityUserDOList)) {
return false;
}
//判断中奖者状态 与目标状态
for(ActivityUserDO activityUserDO : activityUserDOList ) {
if(activityUserDO.getStatus().equalsIgnoreCase(targetActivityUserStatus.name())) {
return false;
}
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
//活动id
Long activityId = convertActivityStatusDTO.getActivityId();
//人员id列表
List<Long> userIds = convertActivityStatusDTO.getUserIds();
//目标状态
ActivityUserStatusEnum targetActivityUserStatus = convertActivityStatusDTO.getTargetActivityUserStatus();
try{
activityUserMapper.batchUpdateStatus(activityId, userIds,targetActivityUserStatus.name());
return true;
}catch (Exception e) {
return false;
}
}
}
7.4.2.2.6 缓存Redis更新
7.4.2.2.6.1 接口定义
com/yj/lottery_system/service 包下:IActivityService.java类
java
/**
* 缓存活动详细信息(读取表数据
*/
void cacheActivity(Long activityId);
7.4.2.2.6.2 接口实现
7.3.3 ConvertActivityStatusDTO :抽奖相关需要状态扭转的类
com.yj.lottery_system.service.dto 包下:
- 这个类里面的属性,对应这侧抽奖活动需要状态扭转的类对应的id,和扭转后的状态
java
package com.yj.lottery_system.service.dto;
import com.yj.lottery_system.service.enums.ActivityPrizeStatusEnum;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import com.yj.lottery_system.service.enums.ActivityUserStatusEnum;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class ConvertActivityStatusDTO implements Serializable {
//活动id
private Long activityId;
// 活动目标状态
private ActivityStatusEnum targetActivityStatus;
//奖品id
private Long prizeId;
// 奖品目标状态
private ActivityPrizeStatusEnum targetActivityPrizeStatus;
//人员列表
private List<Long> userIds;
// 奖品目标状态
private ActivityUserStatusEnum targetActivityUserStatus;
}
7.3.4 dao层修改
com/yj/lottery_system/dao/mapper 包下ActivityMapper.java类:
java
/**
* 根据活动id,更新目标状态
* @param id 活动id
* @param status 目标状态
*/
@Update("update activity set status = #{status} where id = #{id}")
void updateStatus(@Param("id") Long id, @Param("status")String status);
com/yj/lottery_system/dao/mapper 包下 ActivityPrizeMapper.java 类:
java
/**
*活动activityId下 奖品RUNNING状态 的个数
* @param activityId 活动id
* @param status 奖品状态
* @return
*/
@Select("select count(1) from activity_prize where activity_id = #{activityId} and status = #{status}")
int countPrize(@Param("activityId")Long activityId, @Param("status")String status);
/**
* 更新被抽取奖品状态
* @param activityId 活动id
* @param prizeId 奖品id
* @param status 奖品状态
*/
@Update(("update activity_prize set status = #{status} where activity_id = #{activityId} and prize_id = #{prizeId}"))
void updateStatus(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId, @Param("status")String status);
com/yj/lottery_system/dao/mapper 包下 ActivityUserMapper.java 类:
java
@Select("<script> " +
"select * from activity_user " +
"where activity_id = #{activityId} and " +
"user_id in" +
"<foreach collection = 'userIds' item = 'userId' open='(' separator=',' close=')'> " +
"#{userId}" +
"</foreach>" +
"</script>")
List<ActivityUserDO> batchSelectByAUId(@Param("activityId")Long activityId,
@Param("userIds")List<Long> userIds);
@Update("<script> " +
"update activity_user set status = #{status}" +
"where activity_id = #{activityId} and " +
"user_id in" +
"<foreach collection = 'userIds' item = 'userId' open='(' separator=',' close=')'> " +
"#{userId}" +
"</foreach>" +
"</script>")
void batchUpdateStatus(@Param("activityId")Long activityId,
@Param("userIds")List<Long> userIds,
@Param("status") String status) ;
7.3.5 更新Redis缓存中的状态
7.3.5.1 接口创建
com/yj/lottery_system/service 包下 IActivityService.java 类:
java
/**
* 缓存活动详细信息(读取表数据
*/
void cacheActivity(Long activityId);
7.3.5.2 接口实现
com/yj/lottery_system/service/impl 包下 ActivityServiceImpl.java 类:
跟前面创建活动时的逻辑一样:
java
/**
* 缓存活动详细信息(读取表数据
*/
@Override
public void cacheActivity(Long activityId) {
if(null == activityId) {
log.warn("缓存的活动ID为空");
throw new ServiceException(ServiceErrorCodeConstants.CACHE_ACTIVITY_ID_IS_EMPTY);
}
//查询表数据 活动表 活动奖品表 活动人员表
// 查表 活动表
ActivityDO activityDO = activityMapper.selectById(activityId);
if(null == activityDO) {
log.error("缓存的活动ID有误");
throw new ServiceException(ServiceErrorCodeConstants.CACHE_ACTIVITY_ID_ERROR);
}
// 活动奖品表
List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.selectByActivityId(activityId);
// 活动人员表
List<ActivityUserDO> activityUserDOList = activityUserMapper.selectByActivityId(activityId);
// 奖品表
//先拿奖品id
List<Long> prizeIdList = activityPrizeDOList.stream()
.map(ActivityPrizeDO::getPrizeId)
.collect(Collectors.toList());
List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIdList);
//整合详细信息,存放Redis
ActivityDetailDTO activityDetailDTO = convertActivityDetailDTO(activityDO, activityUserDOList, activityPrizeDOList, prizeDOList);
//存放Redis
cacheActivity(activityDetailDTO);
}
7.3.6 总结
| 模式 | 在哪里用 | 承担的角色 | 带来的好处 |
|---|---|---|---|
| 责任链模式(Chain of Responsibility) | 1. ActivityStatusManagerImpl 按 sequence 两次遍历 Map<String, AbstractActivityOperator> 2. 每个 Operator 自己决定"要不要处理"以及"是否甩锅给后续" |
把 奖品、用户、活动 三类状态节点串成 先 1 后 2 的两条链 | ① 节点顺序可配置 ② 新增/删除状态类型只新增/删除一个 Operator 类,链自动感知 |
| 策略模式(Strategy) | 1. 每个 Operator 实现 AbstractActivityOperator 的 三个策略接口 :sequence() / needConvert() / convert() 2. Spring 启动时把所有具体策略(PrizeOperator/UserOperator/ActivityOperator)注入到 Map------天然 策略注册表 |
让 "判断条件" 和 "执行算法" 各自独立,可互换 | ① 各状态扭转逻辑彻底解耦 ② 单元测试可单独 mock 任一策略 |
7.4 保存中奖者名单
7.4.1 接口创建
com/yj/lottery_system/service 包下:IDrawPrizeService.java类中:
java
/**
* 保存中奖者名单
* @param param
*/
List<WinningRecodesDO> saveWinnerRecodes(DrawPrizeParam param);
7.4.2 实现接口
com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java 类中:
- 查 活动表activity 奖品表prize 用户表user 活动关联奖品表activity_prize,拿到对应数据
- 构造中奖记录
- 缓存奖品维度(activityId_prizeId_ , winningRecodesDOList)
- 活动已完成 缓存活动维度(activityId_ , winningRecodesDOList)
java
/**
* 保存中奖者名单
* @param param
*/
@Override
public List<WinningRecodesDO> saveWinnerRecodes(DrawPrizeParam param) {
//查询活动 奖品 人员信息 活动关联奖品表
//查询活动
ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
//查询人员信息
List<UserDO> userDOList = userMapper.batchSelectByIds(param.getWinnerList().stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList()
));
//查询奖品
PrizeDO prizeDO = prizeMapper.selectById(param.getPrizeId());
//查询 活动关联奖品表
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
//构造中奖者记录
List<WinningRecodesDO> winningRecodesDOList = userDOList.stream()
.map(userDO -> {
WinningRecodesDO winningRecodesDO = new WinningRecodesDO();
winningRecodesDO.setActivityId(activityDO.getId());
winningRecodesDO.setActivityName(activityDO.getActivityName());
winningRecodesDO.setPrizeId(prizeDO.getId());
winningRecodesDO.setPrizeName(prizeDO.getName());
winningRecodesDO.setPrizeTier(activityPrizeDO.getPrizeTiers());
winningRecodesDO.setWinnerId(userDO.getId());
winningRecodesDO.setWinnerName(userDO.getUserName());
winningRecodesDO.setWinnerEmail(userDO.getEmail());
winningRecodesDO.setWinnerPhoneNumber(userDO.getPhoneNumber());
winningRecodesDO.setWinningTime(param.getWinningTime());
return winningRecodesDO;
}).collect(Collectors.toList());
winningRecordMapper.batchInsert(winningRecodesDOList);
//缓存中奖者记录
//缓存奖品维度(activityId_prizeId_ , winningRecodesDOList)
cacheWinningRecords(param.getActivityId()+"_"+param.getPrizeId(),
winningRecodesDOList,
WINNING_RECORDS_TIMEOUT);
//活动已完成 缓存活动维度(activityId_ , winningRecodesDOList)
if(activityDO.getStatus()
.equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) {
//查询活动维度全量中奖记录
List<WinningRecodesDO> allList = winningRecordMapper.selectByActivityId(param.getActivityId());
cacheWinningRecords(String.valueOf(param.getActivityId()),
allList,
WINNING_RECORDS_TIMEOUT);
}
return winningRecodesDOList;
}
7.4.3 WinningRecodesDO 中奖者类
与winner_record表对应
com.yj.lottery_system.dao.dataObject 包下:
java
package com.yj.lottery_system.dao.dataObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
public class WinningRecodesDO extends BaseDO{
//活动id
private Long activityId;
//活动名
private String activityName;
//奖品id
private Long prizeId;
//奖品名称
private String prizeName;
//奖品等级
private String prizeTier;
//中奖者id
private Long winnerId;
//中奖者姓名
private String winnerName;
//中奖者邮箱
private String winnerEmail;
//中奖者电话
private Encrypt WinnerPhoneNumber;
//中奖时间
private Date winningTime;
}
7.4.4 缓存方法与获取方法
com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java 类中:
java
/**
* 缓存中奖记录
* @param key
* @param winningRecodesDOList
* @param time
*/
private void cacheWinningRecords(String key, List<WinningRecodesDO> winningRecodesDOList, Long time) {
try {
if(!StringUtils.hasText(key)
|| CollectionUtils.isEmpty(winningRecodesDOList)) {
log.warn("要缓存的内容为空:key: {}, value: {}",
WINNING_RECORDS_PREFIX+key,
JacksonUtil.writeValueAsString(winningRecodesDOList));
return;
}
redisUtil.set(WINNING_RECORDS_PREFIX+key,
JacksonUtil.writeValueAsString(winningRecodesDOList),
time);
}catch (Exception e) {
log.error("缓存中奖记录异常:key: {}, value: {}",
WINNING_RECORDS_PREFIX+key,
JacksonUtil.writeValueAsString(winningRecodesDOList));
}
}
/**
* 获取缓存中的中奖记录
* @param key
*/
private List<WinningRecodesDO> getWinningRecords(String key) {
try{
if(!StringUtils.hasText(key)) {
log.warn("要查询的缓存中奖记录key为空:key: {}", WINNING_RECORDS_PREFIX+key);
return Arrays.asList();
}
String s = redisUtil.get(WINNING_RECORDS_PREFIX + key);
if (!StringUtils.hasText(s)) {
return Arrays.asList();
}
List<WinningRecodesDO> winningRecodesDOList = JacksonUtil.readListValue(s, WinningRecodesDO.class);
return winningRecodesDOList;
}catch (Exception e) {
log.error("要查询的缓存中奖记录异常:key: {}", WINNING_RECORDS_PREFIX+key);
return Arrays.asList();
}
}
7.4.5 dao层新增Mapper方法
com/yj/lottery_system/dao/mapper 包下 PrizeMapper.java 类中
java
/**
* 根据奖品id查奖品信息
* @param id
* @return
*/
@Select("select * from prize where id = #{id}")
PrizeDO selectById(@Param("id")Long id);
com.yj.lottery_system.dao.mapper 包
java
package com.yj.lottery_system.dao.mapper;
import com.yj.lottery_system.dao.dataObject.WinningRecodesDO;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface WinningRecordMapper {
@Insert("<script> " +
"insert into winning_record " +
"(activity_id,activity_name, " +
"prize_id, prize_name, prize_tier," +
"winner_id,winner_name,winner_email,winner_phone_number,winning_time) " +
"values <foreach item = 'item' collection='items' index='index' separator=',' > " +
" (#{item.activityId},#{item.activityName}," +
"#{item.prizeId},#{item.prizeName},#{item.prizeTier}," +
"#{item.winnerId},#{item.winnerName},#{item.winnerEmail,},#{item.winnerPhoneNumber},#{item.winningTime}) " +
"</foreach>" +
"</script>")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
void batchInsert(@Param("items") List<WinningRecodesDO> winningRecodesDOList);
/**
* 根据活动id 查询全部中奖信息
* @param activityId
* @return
*/
@Select("select * from winning_record where activity_id = #{activityId}")
List<WinningRecodesDO> selectByActivityId(@Param("activityId") Long activityId);
}
7.5 邮箱通知中奖者
7.5.1 添加方法
com/yj/lottery_system/common/utils 包下 EmailCodeUtil.java 添加发送中奖信息的方法:
java
/**
* 发送邮件
* @param to
* @param subject
* @param context
* @return
*/
public Boolean sendSampleMail(String to, String subject, String context) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(context);
try {
mailSender.send(message);
} catch (Exception e) {
log.error("向{}发送邮件失败!", to, e);
return false;
}
return true;
}
7.5.2 配置线程池
application.yml :
yml
## 线程池 ##
async:
executor:
thread:
core_pool_size: 10
max_pool_size: 20
queue_capacity: 20
name:
prefix: async-service-
com.yj.lottery_system.common.config 包:
java
package com.yj.lottery_system.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class ExecutorConfig {
@Value("${async.executor.thread.core_pool_size}")
private int corePoolSize;
@Value("${async.executor.thread.max_pool_size}")
private int maxPoolSize;
@Value("${async.executor.thread.queue_capacity}")
private int queueCapacity;
@Value("${async.executor.thread.name.prefix}")
private String namePrefix;
@Bean(name = "asyncServiceExecutor")
public ThreadPoolTaskExecutor asyncServiceExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new
ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
threadPoolTaskExecutor.setQueueCapacity(queueCapacity);
threadPoolTaskExecutor.setKeepAliveSeconds(3);
threadPoolTaskExecutor.setThreadNamePrefix(namePrefix);
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执⾏任务,⽽是由调⽤者所在的线程来执⾏
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//加载
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
7.5.3 异步发送
我们是在抽奖完成之后异步处理发送,用线程池。
java
//通知
private void syncExecute(List<WinningRecodesDO> winningRecodesDOList) {
//邮件通知
threadPoolTaskExecutor.execute(()->sendMail(winningRecodesDOList));
}
/**
* 发邮件
* @param winningRecodesDOList
*/
private void sendMail(List<WinningRecodesDO> winningRecodesDOList) {
if(CollectionUtils.isEmpty(winningRecodesDOList)) {
log.info("中奖列表为空,不用发邮件");
return;
}
for(WinningRecodesDO winningRecodesDO : winningRecodesDOList) {
String context = "您好, "+winningRecodesDO.getWinnerName()+",恭喜您在"+
winningRecodesDO.getActivityName()+"抽奖活动中获得"+
winningRecodesDO.getPrizeTier()+":"+
winningRecodesDO.getPrizeName()+"!获奖时间是:"+
winningRecodesDO.getWinningTime()+",请尽快领取您的奖品,祝您生活愉快。";
emailCodeUtil.sendSampleMail(winningRecodesDO.getWinnerEmail(),
"中奖通知", context);
}
}
7.6 抽奖回滚实现
为了保证发生异常后,保证数据一致性,恢复处理请求之前的库表状态。
- 回滚状态 :活动表,活动关联奖品表,活动关联人员表
1.1. 状态不需要回滚就不需要回滚中奖者名单 - 回滚中奖者名单
com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:
java
/**
* 处理抽奖异常回滚行为
* @param param
*/
private void rollBack(DrawPrizeParam param) {
//回滚状态:活动表 奖品 人员
//状态不需要回滚直接return
if(!statusNeedRollBack(param)) {
return;
}
rollBackStatus(param);
//回滚中奖者名单
//是否需要回滚,不需要直接返回
if(!winnerNeedRollBack(param)) {
return;
}
rollBackWinner(param);
}
7.6.1 判断状态是否需要回滚
因为我们前面扭转状态时保证了事务一致性,只需要判断 中奖人员 或者 奖品状态 其一就可以。
一个活动多个奖品,多次抽奖,不能使用活动状态判断。
com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:
java
private boolean statusNeedRollBack(DrawPrizeParam param) {
//扭转状态时保证了事务一致性,只需要判断一个状态(不包含活动)
ActivityPrizeDO activityPrizeDO =
activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
//已经扭转需要回滚
return activityPrizeDO.getStatus()
.equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
}
7.6.2 状态回滚方法
com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:
- 全部回滚为初始化的状态:活动 RUNNING 奖品,中奖人员:INIT
java
/**
* 回滚状态
* @param param
*/
private void rollBackStatus(DrawPrizeParam param) {
ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
convertActivityStatusDTO.setActivityId(param.getActivityId());
convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
convertActivityStatusDTO.setPrizeId(param.getPrizeId());
convertActivityStatusDTO.setTargetActivityPrizeStatus(ActivityPrizeStatusEnum.INIT);
convertActivityStatusDTO.setUserIds(
param.getWinnerList().stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
convertActivityStatusDTO.setTargetActivityUserStatus(ActivityUserStatusEnum.INIT);
activityStatusManager.rollBackHandleEvent(convertActivityStatusDTO);
}
7.6.2.1 接口创建
com.yj.lottery_system.service.activityStatus 包下:IActivityStatusManager类中:
java
/**
* 处理活动相关状态回滚
* @param convertActivityStatusDTO
*/
void rollBackHandleEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
7.6.2.2 接口实现
com.yj.lottery_system.service.activityStatus.impl 包下:ActivityStatusManagerImpl类中:
- 这届遍历需要回滚的类,直接调用上面写好的改变状态方法,更新一下缓存即可。
java
/**
* 处理活动相关状态回滚
* @param convertActivityStatusDTO
*/
@Override
public void rollBackHandleEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
//拿到需要处理的
Map<String, AbstractActivityOperator> curMap = new HashMap<>(operatorMap);
for(AbstractActivityOperator operator : curMap.values()) {
operator.convert(convertActivityStatusDTO);
}
//更新缓存
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
7.6.3 判断中奖记录是否需要回滚
com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:
- 直接调用dao层方法,看中奖人数是否更新了,
java
/**
* 判断中奖者名单是否需要回滚,需要-true
* @param param
*/
private boolean winnerNeedRollBack(DrawPrizeParam param) {
//判断活动中的奖品是否存在中奖者
int count = winningRecordMapper.count(param.getActivityId(), param.getPrizeId());
return count > 0;
}
dao层 方法:
com.yj.lottery_system.dao.mapper 包下:WinningRecordMapper类中:
java
/**
* 根据活动id 和 奖品id 查中奖者人数
* @param activityId
* @param prizeId
* @return
*/
@Select("select count(1) from winning_record where activity_id = #{activityId} and prize_id = #{prizeId}")
int count(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);
7.6.4 中奖记录回滚
com/yj/lottery_system/service/mq 包下:MqReceiver.java 类中:
java
/**
* 回滚中奖者名单
* @param param
*/
private void rollBackWinner(DrawPrizeParam param) {
drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
}
7.6.4.1 创建接口
com.yj.lottery_system.service 包下:IDrawPrizeService类:
java
/**
* 根据活动id和奖品id 删除中奖记录
* @param activityId
* @param prizeId
*/
void deleteRecords(Long activityId, Long prizeId);
7.6.4.2 实现接口
com/yj/lottery_system/service/impl 包下 DrawPrizeServiceImpl.java 类中:
java
/**
* 根据活动id 或 奖品id 删除中奖记录
* @param activityId
* @param prizeId
*/
@Override
public void deleteRecords(Long activityId, Long prizeId) {
if(null == activityId ) {
log.warn("要删除中奖记录活动id为空");
}
//删除winner_record表
winningRecordMapper.deleteRecords(activityId,prizeId);
//删除缓存(奖品维度,活动维度)
if(null != prizeId) {
deleteWinningRecodes(activityId+"_"+prizeId);
}
deleteWinningRecodes(String.valueOf(activityId));
}
/**
* 删除缓存
*/
private void deleteWinningRecodes(String key) {
try{
if(redisUtil.hasKey(WINNING_RECORDS_PREFIX+key)) {
redisUtil.del(WINNING_RECORDS_PREFIX+key);
}
}catch (Exception e){
log.error("删除缓存中奖记录异常:key: {}", WINNING_RECORDS_PREFIX+key);
}
}
7.6.4.3 dao层
com.yj.lottery_system.dao.mapper 包下:WinningRecordMapper类中:
java
/**
* 根据活动id 或 奖品id 删除中奖记录
* @param activityId
* @param prizeId
*/
@Delete("<script>" +
"delete from winning_record " +
"where activity_id = #{activityId} " +
"<if test=\"prizeId!=null\">" +
"and prize_id = #{prizeId}" +
"</if>" +
"</script>")
void deleteRecords(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);
7.7 消费者主流程
java
package com.yj.lottery_system.service.mq;
import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.common.utils.EmailCodeUtil;
import com.yj.lottery_system.common.utils.JacksonUtil;
import com.yj.lottery_system.controller.param.DrawPrizeParam;
import com.yj.lottery_system.dao.dataObject.ActivityPrizeDO;
import com.yj.lottery_system.dao.dataObject.WinningRecodesDO;
import com.yj.lottery_system.dao.mapper.ActivityPrizeMapper;
import com.yj.lottery_system.dao.mapper.WinningRecordMapper;
import com.yj.lottery_system.service.IDrawPrizeService;
import com.yj.lottery_system.service.activityStatus.IActivityStatusManager;
import com.yj.lottery_system.service.dto.ConvertActivityStatusDTO;
import com.yj.lottery_system.service.enums.ActivityPrizeStatusEnum;
import com.yj.lottery_system.service.enums.ActivityStatusEnum;
import com.yj.lottery_system.service.enums.ActivityUserStatusEnum;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.yj.lottery_system.common.config.DirectRabbitConfig.QUEUE_NAME;
@RabbitListener(queues = QUEUE_NAME)
@Component
public class MqReceiver {
private static final Logger log = LoggerFactory.getLogger(MqReceiver.class);
@Resource
private IDrawPrizeService drawPrizeService;
@Resource
private IActivityStatusManager activityStatusManager;
@Autowired
@Qualifier("asyncServiceExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private EmailCodeUtil emailCodeUtil;
@Resource
private ActivityPrizeMapper activityPrizeMapper;
@Resource
private WinningRecordMapper winningRecordMapper;
@RabbitHandler
public void process(Map<String,String> message) throws Exception{
//成功接收队列消息
log.info("mq成功接收到消息 message:{}",
JacksonUtil.writeValueAsString(message));
String paramString = message.get("messageData");
DrawPrizeParam param = JacksonUtil.readValue(paramString,DrawPrizeParam.class);
//处理抽奖
try {
//校验抽奖请求
if(!drawPrizeService.checkDrawPrizeParam(param)) {
return;
}
//活动,奖品,人员 状态扭转
statusConvert(param);
//保存中奖者名单
List<WinningRecodesDO> winningRecodesDOList = drawPrizeService.saveWinnerRecodes(param);
//通知中奖者
syncExecute(winningRecodesDOList);
}catch (ServiceException e) {
log.error("处理 MQ 消息异常 {}, {} ",e.getCode(),e.getMsg(),e);
//发生异常,需要回滚 保持事务一致性,
rollBack(param);
// 抛出异常
throw e;
} catch (Exception e) {
log.error("处理 MQ 消息异常 ",e);
//发生异常,需要回滚 保持事务一致性,
rollBack(param);
// 抛出异常
throw e;
}
}
}
八、死信队列消费者
死信队列消费者 实时消费,没有给处理异常消息的时间
java
package com.yj.lottery_system.service.mq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.yj.lottery_system.common.config.DirectRabbitConfig.*;
@Slf4j
@RabbitListener(queues = DLX_QUEUE_NAME)
@Component
public class DlxReceiver {
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitHandler
public void process(Map<String,String> message) {
log.info("开始处理异常消息");
//消息重发
rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING,message);
}
}
- 接收到异常消息,可以将异常消息存放到数据库表中
- 存放后,当前异常消息消费完成,死信队列消息处理完成,但异常消息被我们持久化存储到表中了
- 解决异常
- 完成脚本任务,判断异常消息表中是否存在数据,如果存在,表示有消息未完成,此时处理消息
- 处理消息:将消息发送给普通队列进行处理