java抽奖系统(八)

9. 抽奖模块

9.1 抽奖设计

抽奖过程是抽奖系统中最重要的核⼼环节,它需要确保公平、透明且⾼效。以下是详细的抽奖过程设计:

对于前端来说,负责控制抽奖的流程,确定中奖的人员

对于后端来说:

接口1:查询完整的活动信息

接口2: 前端确定好中奖的人员之后奖人员信息发送给后端,后端完整的保存好中奖信息。(扭转活动,奖品,人员状态,有的奖品抽完了,有的人员中奖了就不能参加下一个等级的抽奖了,这个活动是不是已经抽奖结束了)(完成中奖人员通知的操作)

接口三:查询中奖列表。

9.2 查询活动完整信息接口

activityDetailDTO类里面包含抽奖活动所需要的所有参数。

前端:向后端发起请求,从redis中查询相关的数据(如果没有存储,或者redis中的缓存过期,就会查询相关的数据库表,整合所需要的所有数据,再一次存放到redis中),最后奖数据返回给前端。

controller:

java 复制代码
 @RequestMapping("/activity-detail/find")
    public CommonResult<GetActivityDetailResult> getActivityDetail(Long activityId) {
        logger.info("ActivityController getActivityDetail activityId:{}", activityId);
        ActivityDetailDTO detailDTO = activityService.getActivityDetail(activityId);
        return CommonResult.success(convertToGetActivityDetailResult(detailDTO));
    }

    private GetActivityDetailResult convertToGetActivityDetailResult(ActivityDetailDTO detailDTO) {
        if (detailDTO == null) {
            throw new ControllerException(ControllerErrorCodeConstants.GET_ACTIVITY_DETAIL_ERROR);
        }

        GetActivityDetailResult result = new GetActivityDetailResult();
        result.setActivityId(detailDTO.getActivityId());
        result.setActivityName(detailDTO.getActivityName());
        result.setDescription(detailDTO.getDesc());
        result.setValid(detailDTO.valid());
        // 抽奖顺序:一等奖、二、三
        result.setPrizes(
                detailDTO.getPrizeDTOList().stream()
                        .sorted(Comparator.comparingInt(prizeDTO -> prizeDTO.getTiers().getCode()))
                        //按照奖品code1,2,3等奖来进行从小到大的排序
                        .map(prizeDTO -> {
                            GetActivityDetailResult.Prize prize = new GetActivityDetailResult.Prize();
                            prize.setPrizeId(prizeDTO.getPrizeId());
                            prize.setName(prizeDTO.getName());
                            prize.setImageUrl(prizeDTO.getImageUrl());
                            prize.setPrice(prizeDTO.getPrice());
                            prize.setDescription(prizeDTO.getDescription());
                            prize.setPrizeTierName(prizeDTO.getTiers().getMessage());
                            prize.setPrizeAmount(prizeDTO.getPrizeAmount());
                            prize.setValid(prizeDTO.valid());
                            return prize;
                        }).collect(Collectors.toList())
        );
        result.setUsers(
                detailDTO.getUserDTOList().stream()
                        .map(userDTO -> {
                            GetActivityDetailResult.User user = new GetActivityDetailResult.User();
                            user.setUserId(userDTO.getUserId());
                            user.setUserName(userDTO.getUserName());
                            user.setValid(userDTO.valid());
                            return user;
                        }).collect(Collectors.toList())
        );
        return result;
    }

service:

java 复制代码
ActivityDetailDTO getActivityDetail(Long activityId);

@Override
    public ActivityDetailDTO getActivityDetail(Long activityId) {
        if (activityId == null) {
            logger.warn("查询活动详细信息失败,activityId为空!");
            return null;
        }
        // 查询 redis
        ActivityDetailDTO detailDTO = getActivityFromCache(activityId);
        if (detailDTO != null) {
            logger.info("查询活动详细信息成功!detailDTO={}",
                    JacksonUtil.writeValueAsString(detailDTO));
            return detailDTO;
        }
        // 如果redis不存在,查表
        // 活动表
        ActivityDO aDO = activityMapper.selectById(activityId);

        // 活动奖品表
        List<ActivityPrizeDO> apDOList =  activityPrizeMapper.selectByActivityId(activityId);

        // 活动人员表
        List<ActivityUserDO> auDOList = activityUserMapper.selectByActivityId(activityId);

        // 奖品表: 先获取要查询的奖品id
        List<Long> prizeIds = apDOList.stream()
                .map(ActivityPrizeDO::getPrizeId)
                .collect(Collectors.toList());
        List<PrizeDO> pDOList = prizeMapper.batchSelectByIds(prizeIds);
        // 整合活动详细信息,存放redis
        detailDTO = convertToActivityDetailDTO(aDO, auDOList, pDOList, apDOList);
        cacheActivity(detailDTO);
        // 返回
        return detailDTO;
    }

dao:

java 复制代码
@Select("select * from activity_prize where activity_id = #{activityId}")
    List<ActivityPrizeDO> selectByActivityId(@Param("activityId") Long activityId);

@Select("select * from activity_user where activity_id = #{activityId}")
    List<ActivityUserDO> selectByActivityId(@Param("activityId") Long activityId);

postman进行测试:

返回结果如下:

java 复制代码
{
    "code": 200,
    "data": {
        "activityId": 26,
        "activityName": "测试抽奖活动2.0",
        "description": "测试抽奖活动2.0",
        "valid": true,
        "prizes": [
            {
                "prizeId": 23,
                "name": "snh48演唱会门票",
                "imageUrl": "bc10a8f2-eb31-4e1e-8a72-2883b3968f0c.jpg",
                "price": 498.00,
                "description": "演唱会门票",
                "prizeTierName": "一等奖",
                "prizeAmount": 1,
                "valid": true
            },
            {
                "prizeId": 21,
                "name": "奥迪a6",
                "imageUrl": "a1452970-b709-4a51-8343-e969f5ac5c64.jpg",
                "price": 260000.00,
                "description": "一个四轮车子",
                "prizeTierName": "二等奖",
                "prizeAmount": 1,
                "valid": true
            }
        ],
        "users": [
            {
                "userId": 44,
                "userName": "王奕",
                "valid": true
            },
            {
                "userId": 45,
                "userName": "周诗雨",
                "valid": true
            }
        ]
    },
    "msg": ""
}

9.3 抽奖接口分析

抽奖场景:当确定中奖人员信息之后,处于用户体验,需要当前页面马上显示出相关的人员信息,但是前端将相关中奖人确定的信息传入到后端之后,后端要进行(保存中奖信息,扭转对协应的转态,完成通知),之后才会将先关数据发送到前端显示的页面上,这样会极大地消耗时间,这就是同步接口,该接口完全不符合当前的抽奖接口。

异步接口:

前端:确定中间人之后前往后端。

后端:前端来请求之后直接返回数据,对上面三个步骤进行异步处理。

异步处理流程,会另找时间完成后端所要完成的三个步骤。

9.3.1 异步处理业务流程图

9.3.2 业务逻辑分析

1、抽奖请求处理(重要)

随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。

请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加 信息。

消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。

请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。

2、 抽奖结果公布

前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。

3、 抽奖逻辑执⾏(重要)

消 息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。

4、 中奖结果处理(重要)

请求验证:

◦ 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数 限制等)等;

◦ 幂等性:若消息多发,已抽取的内容不能再次抽取

状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取,⼈员是否已中奖等。

结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存。

5、 中奖者通知

通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。

奖品领取:中奖者根据通知中的指引领取奖品。

6、 抽奖异常处理

回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。

补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补 救措施

技术实现细节:

异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂 等性。

活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内 容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。 并发处理:中奖者通知,可能要通知多系统(邮箱和手机两种方式),但相互解耦,可以设计为并发处理,加快抽奖效率 作⽤。

事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要 做好事务处理。

本次的异步处理操作使用rabbitmq工具来实现。

9.4 mq

MQ( Message queue ),从字⾯意思上看,本质是个队列,FIFO先⼊先出,只不过队列中存放的内容 是消息(message)⽽已.消息可以⾮常简单,⽐如只包含⽂本字符串,JSON等,也可以很复杂,⽐如内嵌对象. MQ多⽤于分布式系统之间进⾏通信,系统之间的调⽤通常有两种⽅式:

  1. 同步通信

直接调⽤对⽅的服务,数据从⼀端发出后⽴即就可以达到另⼀端.

  1. 异步通信

数据从⼀端发出后,先进⼊⼀个容器进⾏临时存储,当达到某种条件后,再由这个容器发送给另⼀端. 容器的⼀个具体实现就是MQ( message queue)

RabbitMQ 就是MQ的⼀种实现

9.4.1 MQ主要作用

MQ主要⼯作是接收并转发消息,在不同的应⽤场景下可以展现不同的作⽤:

  1. 异步解耦:

在业务流程中,⼀些操作可能⾮常耗时,但并不需要即时返回结果.可以借助MQ把这些操 作异步化,⽐如⽤⼾注册后发送注册短信或邮件通知,可以作为异步任务处理,⽽不必等待这些操作 完成后才告知⽤⼾注册成功.

  1. 流量削峰:

在访问量剧增的情况下,应⽤仍然需要继续发挥作⽤,但是是这样的突发流量并不常⻅.如 果以能处理这类峰值为标准⽽投⼊资源,⽆疑是巨⼤的浪费.使⽤MQ能够使关键组件⽀撑突发访问压 ⼒,不会因为突发流量⽽崩溃.⽐如秒杀或者促销活动,可以使⽤MQ来控制流量,将请求排队,然后系 统根据⾃⼰的处理能⼒逐步处理这些请求.

  1. 异步通信:

在很多时候应⽤不需要⽴即处理消息,MQ提供了异步处理机制,允许应⽤把⼀些消息放⼊ MQ中,但并不⽴即处理它,在需要的时候再慢慢处理.

  1. 消息分发:

当多个系统需要对同⼀数据做出响应时,可以使⽤MQ进⾏消息分发.⽐如⽀付成功后,⽀ 付系统可以向MQ发送消息,其他系统订阅该消息,⽽⽆需轮询数据库.

  1. 延迟通知:

在需要在特定时间后发送通知的场景中,可以使⽤MQ的延迟消息功能,⽐如在电⼦商务平 台中,如果⽤⼾下单后⼀定时间内未⽀付,可以使⽤延迟队列在超时后⾃动取消订单

9.4.2 rabbitmq核心概念

如下图所示,生产者和消费者是通过网络进行连接的,网络之中是有信道的,发送的消息是存放在队列中遵循先进先出的规则,交换机的作用是用来路由消息的,消息由生产者发送到网络中,由交换机经过路由将消息传递给队列(交换机绑定的队列)。

消费者消费消息,就是去相应对的队列里面进行拿取相应的消息。

多个消费者可以订阅同一个队列。

9.4.3 . web界⾯操作

RabbitMQ Management访问

RabbitMQ 介绍.pdf 基本步骤见文件

9.4.4 rabbitmq的配置与使用

1、引入依赖:

java 复制代码
<!-- RabbitMQ -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

2、application.properties 配置 MQ

## mq ##
spring.rabbitmq.host=49
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=111111
#消息确认机制,默认auto
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#设置失败重试 5次
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5

3、DirectRabbitConfig 配置类

java 复制代码
@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 new Queue(QUEUE_NAME,true);

        // 普通队列绑定死信交换机
//        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);
    }

    /**
     * 死信队列
     *
     * @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);
    }

    /**
     * 死信交换机
     *
     * @return
     */
    @Bean
    DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE_NAME,true,false);
    }

    /**
     * 绑定死信队列与交换机
     *
     * @return
     */
    @Bean
    Binding bindingDlx() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(DLX_ROUTING);
    }



    @Bean
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

在当前生产消费的模块中,生产者是前端的抽奖接口,消费者是处理抽奖流程的方法。

相关推荐
.生产的驴14 分钟前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
我曾经是个程序员16 分钟前
鸿蒙学习记录
开发语言·前端·javascript
爱上语文17 分钟前
宠物管理系统:Dao层
java·开发语言·宠物
王ASC1 小时前
SpringMVC的URL组成,以及URI中对/斜杠的处理,解决IllegalStateException: Ambiguous mapping
java·mvc·springboot·web
是小崔啊1 小时前
开源轮子 - Apache Common
java·开源·apache
因我你好久不见1 小时前
springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率
java·spring boot·ffmpeg
程序员shen1616111 小时前
抖音短视频saas矩阵源码系统开发所需掌握的技术
java·前端·数据库·python·算法
小老鼠不吃猫1 小时前
力学笃行(二)Qt 示例程序运行
开发语言·qt
长潇若雪1 小时前
《类和对象:基础原理全解析(上篇)》
开发语言·c++·经验分享·类和对象
Ling_suu1 小时前
SpringBoot3——Web开发
java·服务器·前端