首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >设计高并发抽奖系统

设计高并发抽奖系统

作者头像
果酱带你啃java
发布2026-04-14 13:15:34
发布2026-04-14 13:15:34
1030
举报

引言:为什么需要专业的抽奖系统?

在当今的互联网产品中,抽奖活动已成为提升用户活跃度、增加用户粘性的重要手段。从电商平台的节日促销到社交应用的用户召回,从线下门店的消费激励到企业内部的福利发放,抽奖系统无处不在。

然而,看似简单的 "点击抽奖" 背后,隐藏着复杂的技术挑战:如何确保抽奖过程的公平性?如何应对高并发场景下的系统压力?如何防止恶意刷奖行为?如何灵活配置多样化的抽奖规则?

我将在本文中带您深入探讨抽奖系统的设计与实现,从需求分析到架构设计,从核心算法到代码实现,全方位解析一个专业抽奖系统的构建过程。无论您是需要快速搭建一个简单的抽奖功能,还是计划开发一个支持千万级用户的高并发抽奖平台,本文都将为您提供宝贵的参考。

一、抽奖系统需求分析

在开始设计之前,我们首先需要明确抽奖系统的核心需求。一个完善的抽奖系统应该具备以下功能和特性:

1.1 核心功能需求

  • 奖品管理支持多种类型奖品(实物、虚拟物品、优惠券等)的创建、编辑、上下架管理
  • 抽奖活动管理支持创建多个并行的抽奖活动,每个活动可独立配置
  • 抽奖规则配置支持设置中奖概率、抽奖次数限制、参与条件等
  • 用户抽奖核心功能,支持用户参与抽奖并返回结果
  • 中奖记录记录用户的中奖信息,支持查询和导出
  • 奖品发放支持实物奖品的物流跟踪和虚拟奖品的自动发放
  • 数据统计提供抽奖参与人数、中奖率、奖品发放情况等统计数据

1.2 非功能需求

  • 高并发支持在活动高峰期能够处理大量并发请求
  • 高可用性确保系统稳定运行,避免单点故障
  • 公平性抽奖过程必须公平公正,结果可追溯
  • 安全性防止刷奖、作弊等恶意行为
  • 可扩展性支持业务规则的灵活调整和功能扩展
  • 实时性结果实时反馈,奖品库存实时更新

1.3 业务场景分析

不同的业务场景对抽奖系统有不同的要求,常见的场景包括:

  1. 营销活动抽奖如电商平台的 "每日签到抽奖",用户基数大,并发高,奖品多为优惠券、积分等
  2. 直播互动抽奖主播发起的实时抽奖,要求低延迟,高并发
  3. 企业年会抽奖参与人数相对较少,但对公平性要求极高
  4. 游戏内抽奖如手游中的 "十连抽",通常有复杂的概率算法和保底机制

二、抽奖系统架构设计

基于上述需求分析,我们设计如下抽奖系统架构:

2.1 架构说明

  1. API 网关统一入口,负责请求路由、限流、认证授权等
  2. 核心服务
    • 抽奖服务:核心服务,协调各子服务完成抽奖流程
    • 活动管理服务:管理抽奖活动的生命周期
    • 奖品管理服务:管理奖品信息和分类
    • 中奖记录服务:记录和查询中奖信息
    • 库存服务:管理奖品库存,处理库存扣减
    • 策略服务:实现各种抽奖算法和规则
  3. 支撑服务
    • 通知服务:负责中奖通知(短信、推送等)
    • 奖品发放服务:处理奖品的发放流程
  4. 存储层:
    • 关系型数据库:存储活动、奖品、记录等核心数据
    • Redis 缓存:缓存热点数据、库存计数器、用户抽奖次数等
  5. 中间件:
    • 消息队列:异步处理奖品发放、通知等非核心流程
    • 监控系统:监控系统运行状态和性能指标

2.2 核心流程设计

抽奖系统的核心流程如下:

三、数据库设计

基于 MySQL 8.0 设计如下数据库表结构:

3.1 活动表(lottery_activity)

代码语言:javascript
复制
CREATE TABLE `lottery_activity` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动ID',
  `activity_name` varchar(128) NOT NULL COMMENT '活动名称',
  `activity_desc` varchar(512) DEFAULT NULL COMMENT '活动描述',
  `start_time` datetime NOT NULL COMMENT '开始时间',
  `end_time` datetime NOT NULL COMMENT '结束时间',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-草稿,1-已发布,2-已结束',
  `total_quota` int NOT NULL DEFAULT '0' COMMENT '总参与次数限制,0表示无限制',
  `user_quota` int NOT NULL DEFAULT '0' COMMENT '单用户参与次数限制,0表示无限制',
  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_status_time` (`status`,`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖活动表';
代码语言:javascript
复制

3.2 奖品表(lottery_prize)

代码语言:javascript
复制
CREATE TABLE `lottery_prize` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '奖品ID',
  `prize_name` varchar(128) NOT NULL COMMENT '奖品名称',
  `prize_type` tinyint NOT NULL COMMENT '奖品类型:1-实物,2-虚拟物品,3-优惠券,4-积分',
  `prize_desc` varchar(512) DEFAULT NULL COMMENT '奖品描述',
  `total_count` int NOT NULL COMMENT '总数量',
  `surplus_count` int NOT NULL COMMENT '剩余数量',
  `image_url` varchar(256) DEFAULT NULL COMMENT '奖品图片URL',
  `value` decimal(10,2) DEFAULT NULL COMMENT '奖品价值',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-下架,1-上架',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='奖品表';
代码语言:javascript
复制

3.3 活动奖品关联表(lottery_activity_prize)

代码语言:javascript
复制
CREATE TABLE `lottery_activity_prize` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `activity_id` bigint NOT NULL COMMENT '活动ID',
  `prize_id` bigint NOT NULL COMMENT '奖品ID',
  `prize_weight` int NOT NULL COMMENT '奖品权重,用于计算概率',
  `max_count_per_user` int NOT NULL DEFAULT '0' COMMENT '单用户最大中奖次数,0表示无限制',
  `sort` int NOT NULL DEFAULT '0' COMMENT '排序',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_activity_prize` (`activity_id`,`prize_id`),
  KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动奖品关联表';
代码语言:javascript
复制

3.4 抽奖规则表(lottery_strategy)

代码语言:javascript
复制
CREATE TABLE `lottery_strategy` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '策略ID',
  `activity_id` bigint NOT NULL COMMENT '活动ID',
  `strategy_type` tinyint NOT NULL COMMENT '策略类型:1-概率型,2-固定中奖型,3-阶梯概率型',
  `strategy_config` json DEFAULT NULL COMMENT '策略配置,JSON格式',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖规则表';
代码语言:javascript
复制

3.5 用户抽奖记录表(lottery_record)

代码语言:javascript
复制
CREATE TABLE `lottery_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `activity_id` bigint NOT NULL COMMENT '活动ID',
  `prize_id` bigint DEFAULT NULL COMMENT '奖品ID,NULL表示未中奖',
  `draw_time` datetime NOT NULL COMMENT '抽奖时间',
  `prize_name` varchar(128) DEFAULT NULL COMMENT '奖品名称',
  `status` tinyint NOT NULL COMMENT '状态:1-已抽奖,2-已发奖,3-已取消',
  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_activity` (`user_id`,`activity_id`),
  KEY `idx_activity_time` (`activity_id`,`draw_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户抽奖记录表';
代码语言:javascript
复制

3.6 奖品发放表(prize_delivery)

代码语言:javascript
复制
CREATE TABLE `prize_delivery` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `record_id` bigint NOT NULL COMMENT '抽奖记录ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `prize_id` bigint NOT NULL COMMENT '奖品ID',
  `prize_type` tinyint NOT NULL COMMENT '奖品类型',
  `delivery_status` tinyint NOT NULL COMMENT '发放状态:0-待发放,1-已发放,2-发放失败',
  `delivery_time` datetime DEFAULT NULL COMMENT '发放时间',
  `logistics_info` varchar(512) DEFAULT NULL COMMENT '物流信息,JSON格式',
  `virtual_code` varchar(128) DEFAULT NULL COMMENT '虚拟奖品码',
  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_record` (`record_id`),
  KEY `idx_user_status` (`user_id`,`delivery_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='奖品发放表';
代码语言:javascript
复制

四、核心技术选型

基于需求分析和架构设计,我们选择以下技术栈:

  1. 基础框架Spring Boot 3.2.0
  2. ORM 框架MyBatis-Plus 3.5.5
  3. 数据库MySQL 8.0
  4. 缓存Redis 7.2.3
  5. 消息队列RabbitMQ 3.13.0
  6. API 文档Swagger3 (SpringDoc OpenAPI 2.1.0)
  7. 日志框架SLF4J + Logback
  8. Lombok:1.18.30(简化代码)
  9. JSON 处理Fastjson2 2.0.41
  10. 集合工具Guava 32.1.3-jre
  11. 分布式锁Redisson 3.23.3
  12. 任务调度Quartz 2.3.2
  13. 测试框架JUnit 5 + Mockito 5.8.0

五、项目初始化与配置

5.1 Maven 依赖配置(pom.xml)

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    <groupId>com.jam</groupId>
    <artifactId>lottery-system</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>lottery-system</name>
    <description>高并发抽奖系统</description>

    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <springdoc.version>2.1.0</springdoc.version>
        <lombok.version>1.18.30</lombok.version>
        <fastjson2.version>2.0.41</fastjson2.version>
        <guava.version>32.1.3-jre</guava.version>
        <redisson.version>3.23.3</redisson.version>
        <quartz.version>2.3.2</quartz.version>
    </properties>

    <dependencies>
        <!-- Spring Boot 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- 数据库 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>${redisson.version}</version>
        </dependency>

        <!-- 消息队列 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!-- API文档 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>

        <!-- 工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

        <!-- 任务调度 -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>${quartz.version}</version>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
代码语言:javascript
复制

5.2 核心配置文件(application.yml)

代码语言:javascript
复制
spring:
  profiles:
    active: dev
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/lottery_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual
        concurrency: 5
        max-concurrency: 10
    publisher-confirm-type: correlated
    publisher-returns: true

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: com.jam.lottery.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

springdoc:
  api-docs:
    path: /api-docs
  swagger-ui:
    path: /swagger-ui.html
    operationsSorter: method
  packages-to-scan: com.jam.lottery.controller

redisson:
  single-server-config:
    address: redis://localhost:6379
    connection-pool-size: 16
    connection-minimum-idle-size: 8
    idle-connection-timeout: 10000
    ping-timeout: 1000
    connect-timeout: 10000
    timeout: 3000

logging:
  level:
    root: info
    com.jam.lottery: debug
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
  file:
    name: logs/lottery-system.log

server:
  port: 8080
  servlet:
    context-path: /lottery
  tomcat:
    threads:
      max: 200
    connection-timeout: 30000
代码语言:javascript
复制

六、核心代码实现

6.1 实体类设计

首先,我们定义核心实体类,对应数据库表结构:

6.1.1 活动实体(LotteryActivity.java)
代码语言:javascript
复制
package com.jam.lottery.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 抽奖活动实体类
 *
 * @author 果酱
 */
@Data
@TableName("lottery_activity")
public class LotteryActivity {
    /**
     * 活动ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 活动名称
     */
    private String activityName;

    /**
     * 活动描述
     */
    private String activityDesc;

    /**
     * 开始时间
     */
    private LocalDateTime startTime;

    /**
     * 结束时间
     */
    private LocalDateTime endTime;

    /**
     * 状态:0-草稿,1-已发布,2-已结束
     */
    private Integer status;

    /**
     * 总参与次数限制,0表示无限制
     */
    private Integer totalQuota;

    /**
     * 单用户参与次数限制,0表示无限制
     */
    private Integer userQuota;

    /**
     * 备注
     */
    private String remark;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

6.1.2 奖品实体(LotteryPrize.java)
代码语言:javascript
复制
package com.jam.lottery.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 奖品实体类
 *
 * @author 果酱
 */
@Data
@TableName("lottery_prize")
public class LotteryPrize {
    /**
     * 奖品ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 奖品名称
     */
    private String prizeName;

    /**
     * 奖品类型:1-实物,2-虚拟物品,3-优惠券,4-积分
     */
    private Integer prizeType;

    /**
     * 奖品描述
     */
    private String prizeDesc;

    /**
     * 总数量
     */
    private Integer totalCount;

    /**
     * 剩余数量
     */
    private Integer surplusCount;

    /**
     * 奖品图片URL
     */
    private String imageUrl;

    /**
     * 奖品价值
     */
    private BigDecimal value;

    /**
     * 状态:0-下架,1-上架
     */
    private Integer status;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

6.1.3 活动奖品关联实体(LotteryActivityPrize.java)
代码语言:javascript
复制
package com.jam.lottery.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 活动奖品关联实体类
 *
 * @author 果酱
 */
@Data
@TableName("lottery_activity_prize")
public class LotteryActivityPrize {
    /**
     * ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 活动ID
     */
    private Long activityId;

    /**
     * 奖品ID
     */
    private Long prizeId;

    /**
     * 奖品权重,用于计算概率
     */
    private Integer prizeWeight;

    /**
     * 单用户最大中奖次数,0表示无限制
     */
    private Integer maxCountPerUser;

    /**
     * 排序
     */
    private Integer sort;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

6.1.4 抽奖规则实体(LotteryStrategy.java)
代码语言:javascript
复制
package com.jam.lottery.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 抽奖规则实体类
 *
 * @author 果酱
 */
@Data
@TableName("lottery_strategy")
public class LotteryStrategy {
    /**
     * 策略ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 活动ID
     */
    private Long activityId;

    /**
     * 策略类型:1-概率型,2-固定中奖型,3-阶梯概率型
     */
    private Integer strategyType;

    /**
     * 策略配置,JSON格式
     */
    private String strategyConfig;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

6.1.5 抽奖记录实体(LotteryRecord.java)
代码语言:javascript
复制
package com.jam.lottery.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 用户抽奖记录实体类
 *
 * @author 果酱
 */
@Data
@TableName("lottery_record")
public class LotteryRecord {
    /**
     * 记录ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 活动ID
     */
    private Long activityId;

    /**
     * 奖品ID,NULL表示未中奖
     */
    private Long prizeId;

    /**
     * 抽奖时间
     */
    private LocalDateTime drawTime;

    /**
     * 奖品名称
     */
    private String prizeName;

    /**
     * 状态:1-已抽奖,2-已发奖,3-已取消
     */
    private Integer status;

    /**
     * 备注
     */
    private String remark;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

6.1.6 奖品发放实体(PrizeDelivery.java)
代码语言:javascript
复制
package com.jam.lottery.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 奖品发放实体类
 *
 * @author 果酱
 */
@Data
@TableName("prize_delivery")
public class PrizeDelivery {
    /**
     * ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 抽奖记录ID
     */
    private Long recordId;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 奖品ID
     */
    private Long prizeId;

    /**
     * 奖品类型
     */
    private Integer prizeType;

    /**
     * 发放状态:0-待发放,1-已发放,2-发放失败
     */
    private Integer deliveryStatus;

    /**
     * 发放时间
     */
    private LocalDateTime deliveryTime;

    /**
     * 物流信息,JSON格式
     */
    private String logisticsInfo;

    /**
     * 虚拟奖品码
     */
    private String virtualCode;

    /**
     * 备注
     */
    private String remark;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

6.2 数据访问层(Mapper)

使用 MyBatis-Plus 的 BaseMapper 简化数据访问层代码:

6.2.1 活动 Mapper(LotteryActivityMapper.java)
代码语言:javascript
复制
package com.jam.lottery.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.lottery.entity.LotteryActivity;
import org.apache.ibatis.annotations.Mapper;

/**
 * 抽奖活动Mapper
 *
 * @author 果酱
 */
@Mapper
public interface LotteryActivityMapper extends BaseMapper<LotteryActivity> {
}
代码语言:javascript
复制

其他实体的 Mapper 类实现类似,这里省略。

6.3 服务层设计

服务层是抽奖系统的核心,包含了业务逻辑的实现。我们采用分层设计,将服务分为接口和实现:

6.3.1 抽奖服务接口(LotteryService.java)
代码语言:javascript
复制
package com.jam.lottery.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.lottery.entity.LotteryRecord;
import com.jam.lottery.vo.DrawResultVO;

/**
 * 抽奖服务接口
 *
 * @author 果酱
 */
public interface LotteryService extends IService<LotteryRecord> {
    /**
     * 用户参与抽奖
     *
     * @param userId     用户ID
     * @param activityId 活动ID
     * @return 抽奖结果
     */
    DrawResultVO draw(Long userId, Long activityId);

    /**
     * 查询用户在某个活动中的抽奖次数
     *
     * @param userId     用户ID
     * @param activityId 活动ID
     * @return 抽奖次数
     */
    int countUserDraws(Long userId, Long activityId);

    /**
     * 查询用户在某个活动中某个奖品的中奖次数
     *
     * @param userId     用户ID
     * @param activityId 活动ID
     * @param prizeId    奖品ID
     * @return 中奖次数
     */
    int countUserWins(Long userId, Long activityId, Long prizeId);
}
代码语言:javascript
复制

6.3.2 抽奖服务实现(LotteryServiceImpl.java)
代码语言:javascript
复制
package com.jam.lottery.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.lottery.entity.LotteryActivity;
import com.jam.lottery.entity.LotteryActivityPrize;
import com.jam.lottery.entity.LotteryRecord;
import com.jam.lottery.entity.LotteryStrategy;
import com.jam.lottery.enums.ActivityStatusEnum;
import com.jam.lottery.enums.ErrorCodeEnum;
import com.jam.lottery.enums.PrizeTypeEnum;
import com.jam.lottery.enums.RecordStatusEnum;
import com.jam.lottery.exception.BusinessException;
import com.jam.lottery.mapper.LotteryRecordMapper;
import com.jam.lottery.mq.producer.LotteryMessageProducer;
import com.jam.lottery.service.*;
import com.jam.lottery.strategy.DrawStrategy;
import com.jam.lottery.strategy.StrategyFactory;
import com.jam.lottery.vo.DrawResultVO;
import com.jam.lottery.vo.PrizeVO;
import com.jam.lottery.vo.UserDrawVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 抽奖服务实现类
 *
 * @author 果酱
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LotteryServiceImpl extends ServiceImpl<LotteryRecordMapper, LotteryRecord> implements LotteryService {

    private final LotteryActivityService activityService;
    private final LotteryActivityPrizeService activityPrizeService;
    private final LotteryStrategyService strategyService;
    private final PrizeInventoryService inventoryService;
    private final LotteryMessageProducer messageProducer;
    private final RedissonClient redissonClient;
    private final StrategyFactory strategyFactory;

    /**
     * 用户参与抽奖
     *
     * @param userId     用户ID
     * @param activityId 活动ID
     * @return 抽奖结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public DrawResultVO draw(Long userId, Long activityId) {
        // 参数校验
        validateDrawParams(userId, activityId);

        // 获取分布式锁,防止重复抽奖
        String lockKey = "lottery:draw:" + activityId + ":" + userId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,最多等待3秒,10秒后自动释放
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                log.warn("用户抽奖获取锁失败,userId: {}, activityId: {}", userId, activityId);
                throw new BusinessException(ErrorCodeEnum.DRAW_FREQUENTLY);
            }

            // 检查活动状态和参与条件
            checkDrawConditions(userId, activityId);

            // 获取抽奖策略
            LotteryStrategy strategy = strategyService.getByActivityId(activityId);
            if (ObjectUtils.isEmpty(strategy)) {
                log.error("活动未配置抽奖策略,activityId: {}", activityId);
                throw new BusinessException(ErrorCodeEnum.STRATEGY_NOT_FOUND);
            }

            // 执行抽奖算法
            DrawStrategy drawStrategy = strategyFactory.getStrategy(strategy.getStrategyType());
            Long prizeId = drawStrategy.draw(userId, activityId, strategy);

            // 构建抽奖结果
            DrawResultVO resultVO = new DrawResultVO();
            resultVO.setActivityId(activityId);
            resultVO.setUserId(userId);
            resultVO.setDrawTime(LocalDateTime.now());

            // 记录抽奖结果
            LotteryRecord record = createLotteryRecord(userId, activityId, prizeId);
            resultVO.setRecordId(record.getId());

            if (ObjectUtils.isEmpty(prizeId)) {
                // 未中奖
                resultVO.setWin(false);
                log.info("用户未中奖,userId: {}, activityId: {}", userId, activityId);
                return resultVO;
            }

            // 中奖处理
            resultVO.setWin(true);

            // 检查用户该奖品的中奖次数限制
            LotteryActivityPrize activityPrize = activityPrizeService.getByActivityAndPrize(activityId, prizeId);
            if (!ObjectUtils.isEmpty(activityPrize) && activityPrize.getMaxCountPerUser() > 0) {
                int userWinCount = countUserWins(userId, activityId, prizeId);
                if (userWinCount >= activityPrize.getMaxCountPerUser()) {
                    log.warn("用户该奖品中奖次数已达上限,userId: {}, activityId: {}, prizeId: {}", 
                            userId, activityId, prizeId);
                    resultVO.setWin(false);
                    record.setPrizeId(null);
                    record.setPrizeName(null);
                    baseMapper.updateById(record);
                    return resultVO;
                }
            }

            // 扣减库存
            boolean deductSuccess = inventoryService.deductInventory(activityId, prizeId);
            if (!deductSuccess) {
                log.warn("奖品库存不足,扣减失败,activityId: {}, prizeId: {}", activityId, prizeId);
                resultVO.setWin(false);
                record.setPrizeId(null);
                record.setPrizeName(null);
                baseMapper.updateById(record);
                return resultVO;
            }

            // 查询奖品信息
            PrizeVO prizeVO = activityPrizeService.getPrizeInfo(activityId, prizeId);
            resultVO.setPrize(prizeVO);

            // 发送中奖消息到MQ,异步处理奖品发放
            messageProducer.sendPrizeMessage(buildUserDrawVO(record, prizeVO));

            log.info("用户中奖,userId: {}, activityId: {}, prizeId: {}", userId, activityId, prizeId);
            return resultVO;
        } catch (InterruptedException e) {
            log.error("用户抽奖获取锁中断,userId: {}, activityId: {}", userId, activityId, e);
            Thread.currentThread().interrupt();
            throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR);
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 构建用户抽奖信息VO
     */
    private UserDrawVO buildUserDrawVO(LotteryRecord record, PrizeVO prizeVO) {
        UserDrawVO userDrawVO = new UserDrawVO();
        userDrawVO.setRecordId(record.getId());
        userDrawVO.setUserId(record.getUserId());
        userDrawVO.setActivityId(record.getActivityId());
        userDrawVO.setPrizeId(record.getPrizeId());
        userDrawVO.setPrizeName(record.getPrizeName());
        userDrawVO.setPrizeType(prizeVO.getPrizeType());
        userDrawVO.setDrawTime(record.getDrawTime());
        return userDrawVO;
    }

    /**
     * 创建抽奖记录
     */
    private LotteryRecord createLotteryRecord(Long userId, Long activityId, Long prizeId) {
        LotteryRecord record = new LotteryRecord();
        record.setUserId(userId);
        record.setActivityId(activityId);
        record.setDrawTime(LocalDateTime.now());
        record.setStatus(RecordStatusEnum.DRAWN.getCode());

        if (!ObjectUtils.isEmpty(prizeId)) {
            record.setPrizeId(prizeId);
            PrizeVO prizeVO = activityPrizeService.getPrizeInfo(activityId, prizeId);
            if (!ObjectUtils.isEmpty(prizeVO)) {
                record.setPrizeName(prizeVO.getPrizeName());
            }
        }

        baseMapper.insert(record);
        return record;
    }

    /**
     * 校验抽奖参数
     */
    private void validateDrawParams(Long userId, Long activityId) {
        if (ObjectUtils.isEmpty(userId)) {
            throw new BusinessException(ErrorCodeEnum.USER_ID_NULL);
        }
        if (ObjectUtils.isEmpty(activityId)) {
            throw new BusinessException(ErrorCodeEnum.ACTIVITY_ID_NULL);
        }
    }

    /**
     * 检查抽奖条件
     */
    private void checkDrawConditions(Long userId, Long activityId) {
        // 查询活动信息
        LotteryActivity activity = activityService.getById(activityId);
        if (ObjectUtils.isEmpty(activity)) {
            log.error("活动不存在,activityId: {}", activityId);
            throw new BusinessException(ErrorCodeEnum.ACTIVITY_NOT_FOUND);
        }

        // 检查活动状态
        if (!ActivityStatusEnum.PUBLISHED.getCode().equals(activity.getStatus())) {
            log.error("活动状态异常,activityId: {}, status: {}", activityId, activity.getStatus());
            throw new BusinessException(ErrorCodeEnum.ACTIVITY_STATUS_INVALID);
        }

        // 检查活动时间
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(activity.getStartTime())) {
            log.error("活动未开始,activityId: {}, startTime: {}", activityId, activity.getStartTime());
            throw new BusinessException(ErrorCodeEnum.ACTIVITY_NOT_STARTED);
        }
        if (now.isAfter(activity.getEndTime())) {
            log.error("活动已结束,activityId: {}, endTime: {}", activityId, activity.getEndTime());
            throw new BusinessException(ErrorCodeEnum.ACTIVITY_HAS_ENDED);
        }

        // 检查总参与次数限制
        if (activity.getTotalQuota() > 0) {
            int totalDrawCount = countActivityDraws(activityId);
            if (totalDrawCount >= activity.getTotalQuota()) {
                log.error("活动总参与次数已达上限,activityId: {}, totalQuota: {}", activityId, activity.getTotalQuota());
                throw new BusinessException(ErrorCodeEnum.ACTIVITY_QUOTA_EXHAUSTED);
            }
        }

        // 检查用户参与次数限制
        if (activity.getUserQuota() > 0) {
            int userDrawCount = countUserDraws(userId, activityId);
            if (userDrawCount >= activity.getUserQuota()) {
                log.error("用户参与次数已达上限,userId: {}, activityId: {}, userQuota: {}", 
                        userId, activityId, activity.getUserQuota());
                throw new BusinessException(ErrorCodeEnum.USER_QUOTA_EXHAUSTED);
            }
        }

        // 检查活动是否有可用奖品
        List<LotteryActivityPrize> activityPrizes = activityPrizeService.listByActivityId(activityId);
        if (ObjectUtils.isEmpty(activityPrizes)) {
            log.error("活动未配置奖品,activityId: {}", activityId);
            throw new BusinessException(ErrorCodeEnum.ACTIVITY_NO_PRIZES);
        }
    }

    /**
     * 查询活动总抽奖次数
     */
    private int countActivityDraws(Long activityId) {
        LambdaQueryWrapper<LotteryRecord> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(LotteryRecord::getActivityId, activityId);
        return Math.toIntExact(baseMapper.selectCount(queryWrapper));
    }

    /**
     * 查询用户在某个活动中的抽奖次数
     */
    @Override
    public int countUserDraws(Long userId, Long activityId) {
        LambdaQueryWrapper<LotteryRecord> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(LotteryRecord::getUserId, userId)
                    .eq(LotteryRecord::getActivityId, activityId);
        return Math.toIntExact(baseMapper.selectCount(queryWrapper));
    }

    /**
     * 查询用户在某个活动中某个奖品的中奖次数
     */
    @Override
    public int countUserWins(Long userId, Long activityId, Long prizeId) {
        LambdaQueryWrapper<LotteryRecord> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(LotteryRecord::getUserId, userId)
                    .eq(LotteryRecord::getActivityId, activityId)
                    .eq(LotteryRecord::getPrizeId, prizeId)
                    .ne(LotteryRecord::getStatus, RecordStatusEnum.CANCELLED.getCode());
        return Math.toIntExact(baseMapper.selectCount(queryWrapper));
    }
}
代码语言:javascript
复制

6.4 抽奖策略设计

抽奖策略是抽奖系统的核心算法,我们采用策略模式设计,支持多种抽奖算法:

6.4.1 策略接口(DrawStrategy.java)
代码语言:javascript
复制
package com.jam.lottery.strategy;

/**
 * 抽奖策略接口
 *
 * @author 果酱
 */
public interface DrawStrategy {
    /**
     * 执行抽奖
     *
     * @param userId     用户ID
     * @param activityId 活动ID
     * @param strategy   策略配置
     * @return 中奖奖品ID,null表示未中奖
     */
    Long draw(Long userId, Long activityId, Object strategy);
}
代码语言:javascript
复制

6.4.2 概率型抽奖策略(ProbabilityDrawStrategy.java)
代码语言:javascript
复制
package com.jam.lottery.strategy.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.google.common.collect.Lists;
import com.jam.lottery.entity.LotteryActivityPrize;
import com.jam.lottery.entity.LotteryStrategy;
import com.jam.lottery.service.LotteryActivityPrizeService;
import com.jam.lottery.strategy.DrawStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * 概率型抽奖策略
 * 基于奖品权重计算中奖概率
 *
 * @author 果酱
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ProbabilityDrawStrategy implements DrawStrategy {

    private final LotteryActivityPrizeService activityPrizeService;
    private final Random random = new Random();

    /**
     * 执行抽奖
     *
     * @param userId     用户ID
     * @param activityId 活动ID
     * @param strategy   策略配置
     * @return 中奖奖品ID,null表示未中奖
     */
    @Override
    public Long draw(Long userId, Long activityId, Object strategy) {
        if (!(strategy instanceof LotteryStrategy)) {
            log.error("策略类型错误,activityId: {}", activityId);
            return null;
        }

        LotteryStrategy lotteryStrategy = (LotteryStrategy) strategy;

        // 解析策略配置
        Map<String, Integer> strategyConfig = parseStrategyConfig(lotteryStrategy.getStrategyConfig());

        // 获取活动奖品列表
        List<LotteryActivityPrize> activityPrizes = activityPrizeService.listByActivityId(activityId);
        if (CollectionUtils.isEmpty(activityPrizes)) {
            log.warn("活动未配置奖品,activityId: {}", activityId);
            return null;
        }

        // 计算总权重
        int totalWeight = calculateTotalWeight(activityPrizes, strategyConfig);
        if (totalWeight <= 0) {
            log.warn("奖品总权重为0,无法抽奖,activityId: {}", activityId);
            return null;
        }

        // 生成随机数
        int randomNum = random.nextInt(totalWeight);

        // 根据权重计算中奖奖品
        Long prizeId = determinePrize(activityPrizes, strategyConfig, randomNum);

        log.debug("概率型抽奖结果,userId: {}, activityId: {}, prizeId: {}", userId, activityId, prizeId);
        return prizeId;
    }

    /**
     * 解析策略配置
     */
    private Map<String, Integer> parseStrategyConfig(String configJson) {
        if (ObjectUtils.isEmpty(configJson)) {
            return Maps.newHashMap();
        }
        try {
            return JSON.parseObject(configJson, new TypeReference<Map<String, Integer>>() {});
        } catch (Exception e) {
            log.error("解析策略配置失败,configJson: {}", configJson, e);
            return Maps.newHashMap();
        }
    }

    /**
     * 计算总权重
     */
    private int calculateTotalWeight(List<LotteryActivityPrize> activityPrizes, Map<String, Integer> strategyConfig) {
        int totalWeight = 0;
        for (LotteryActivityPrize prize : activityPrizes) {
            // 从策略配置中获取权重,如果没有则使用数据库配置的权重
            Integer weight = strategyConfig.getOrDefault(prize.getPrizeId().toString(), prize.getPrizeWeight());
            totalWeight += weight;
        }
        return totalWeight;
    }

    /**
     * 根据权重计算中奖奖品
     */
    private Long determinePrize(List<LotteryActivityPrize> activityPrizes, 
                              Map<String, Integer> strategyConfig, int randomNum) {
        int currentWeight = 0;
        for (LotteryActivityPrize prize : activityPrizes) {
            Integer weight = strategyConfig.getOrDefault(prize.getPrizeId().toString(), prize.getPrizeWeight());
            currentWeight += weight;
            if (randomNum < currentWeight) {
                return prize.getPrizeId();
            }
        }
        return null;
    }
}
代码语言:javascript
复制

6.4.3 策略工厂(StrategyFactory.java)
代码语言:javascript
复制
package com.jam.lottery.strategy;

import com.jam.lottery.enums.StrategyTypeEnum;
import com.jam.lottery.strategy.impl.FixedDrawStrategy;
import com.jam.lottery.strategy.impl.ProbabilityDrawStrategy;
import com.jam.lottery.strategy.impl.StageProbabilityDrawStrategy;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * 抽奖策略工厂
 * 用于获取不同类型的抽奖策略
 *
 * @author 果酱
 */
@Component
@RequiredArgsConstructor
public class StrategyFactory {

    private final ProbabilityDrawStrategy probabilityDrawStrategy;
    private final FixedDrawStrategy fixedDrawStrategy;
    private final StageProbabilityDrawStrategy stageProbabilityDrawStrategy;

    /**
     * 根据策略类型获取抽奖策略
     *
     * @param strategyType 策略类型
     * @return 抽奖策略
     */
    public DrawStrategy getStrategy(Integer strategyType) {
        if (StrategyTypeEnum.PROBABILITY.getType().equals(strategyType)) {
            return probabilityDrawStrategy;
        } else if (StrategyTypeEnum.FIXED.getType().equals(strategyType)) {
            return fixedDrawStrategy;
        } else if (StrategyTypeEnum.STAGE_PROBABILITY.getType().equals(strategyType)) {
            return stageProbabilityDrawStrategy;
        } else {
            throw new IllegalArgumentException("不支持的抽奖策略类型: " + strategyType);
        }
    }
}
代码语言:javascript
复制

6.5 库存服务设计

库存管理是抽奖系统的关键环节,需要保证库存的准确性和并发安全性:

6.5.1 库存服务接口(PrizeInventoryService.java)
代码语言:javascript
复制
package com.jam.lottery.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.lottery.entity.LotteryPrize;

/**
 * 奖品库存服务接口
 *
 * @author 果酱
 */
public interface PrizeInventoryService extends IService<LotteryPrize> {
    /**
     * 扣减奖品库存
     *
     * @param activityId 活动ID
     * @param prizeId    奖品ID
     * @return 扣减是否成功
     */
    boolean deductInventory(Long activityId, Long prizeId);

    /**
     * 增加奖品库存
     *
     * @param prizeId 奖品ID
     * @param count   增加数量
     * @return 增加是否成功
     */
    boolean increaseInventory(Long prizeId, int count);

    /**
     * 获取奖品库存数量
     *
     * @param prizeId 奖品ID
     * @return 库存数量
     */
    int getInventoryCount(Long prizeId);

    /**
     * 预热活动奖品库存到缓存
     *
     * @param activityId 活动ID
     */
    void preloadInventoryToCache(Long activityId);
}
代码语言:javascript
复制

6.5.2 库存服务实现(PrizeInventoryServiceImpl.java)
代码语言:javascript
复制
package com.jam.lottery.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.lottery.entity.LotteryActivityPrize;
import com.jam.lottery.entity.LotteryPrize;
import com.jam.lottery.mapper.LotteryPrizeMapper;
import com.jam.lottery.service.LotteryActivityPrizeService;
import com.jam.lottery.service.PrizeInventoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 奖品库存服务实现类
 *
 * @author 果酱
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PrizeInventoryServiceImpl extends ServiceImpl<LotteryPrizeMapper, LotteryPrize> implements PrizeInventoryService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final RedissonClient redissonClient;
    private final LotteryActivityPrizeService activityPrizeService;

    /**
     * 库存缓存前缀
     */
    private static final String INVENTORY_CACHE_PREFIX = "lottery:inventory:";

    /**
     * 扣减奖品库存
     *
     * @param activityId 活动ID
     * @param prizeId    奖品ID
     * @return 扣减是否成功
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deductInventory(Long activityId, Long prizeId) {
        if (ObjectUtils.isEmpty(prizeId)) {
            log.error("奖品ID为空,无法扣减库存");
            return false;
        }

        // 先尝试从缓存扣减
        String cacheKey = INVENTORY_CACHE_PREFIX + prizeId;
        Long remaining = redisTemplate.opsForValue().decrement(cacheKey);

        // 如果缓存扣减成功且剩余库存 >= 0,直接返回成功
        if (remaining != null && remaining >= 0) {
            log.debug("缓存扣减库存成功,prizeId: {}, remaining: {}", prizeId, remaining);

            // 异步更新数据库库存
            asyncUpdateDbInventory(prizeId);
            return true;
        }

        // 缓存扣减失败或库存不足,需要从数据库扣减
        log.warn("缓存扣减库存失败或库存不足,尝试从数据库扣减,prizeId: {}", prizeId);

        // 获取分布式锁,防止并发问题
        String lockKey = "lottery:inventory:lock:" + prizeId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                log.error("获取库存锁失败,prizeId: {}", prizeId);
                return false;
            }

            // 查询数据库库存
            LotteryPrize prize = getById(prizeId);
            if (ObjectUtils.isEmpty(prize) || prize.getSurplusCount() <= 0) {
                log.error("奖品库存不足,prizeId: {}", prizeId);
                // 更新缓存为0
                redisTemplate.opsForValue().set(cacheKey, 0);
                return false;
            }

            // 扣减数据库库存
            int newCount = prize.getSurplusCount() - 1;
            prize.setSurplusCount(newCount);
            boolean updateSuccess = updateById(prize);
            if (updateSuccess) {
                log.debug("数据库扣减库存成功,prizeId: {}, newCount: {}", prizeId, newCount);
                // 更新缓存
                redisTemplate.opsForValue().set(cacheKey, newCount);
                return true;
            } else {
                log.error("数据库扣减库存失败,prizeId: {}", prizeId);
                return false;
            }
        } catch (InterruptedException e) {
            log.error("获取库存锁中断,prizeId: {}", prizeId, e);
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 异步更新数据库库存
     */
    private void asyncUpdateDbInventory(Long prizeId) {
        // 实际项目中可以使用线程池或消息队列异步更新数据库
        // 这里简化处理,直接更新
        baseMapper.decrementSurplusCount(prizeId);
        log.debug("异步更新数据库库存,prizeId: {}", prizeId);
    }

    /**
     * 增加奖品库存
     *
     * @param prizeId 奖品ID
     * @param count   增加数量
     * @return 增加是否成功
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean increaseInventory(Long prizeId, int count) {
        if (ObjectUtils.isEmpty(prizeId) || count <= 0) {
            log.error("参数错误,无法增加库存,prizeId: {}, count: {}", prizeId, count);
            return false;
        }

        // 获取分布式锁
        String lockKey = "lottery:inventory:lock:" + prizeId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                log.error("获取库存锁失败,prizeId: {}", prizeId);
                return false;
            }

            // 更新数据库库存
            int rows = baseMapper.incrementSurplusCount(prizeId, count);
            if (rows > 0) {
                log.debug("增加库存成功,prizeId: {}, count: {}", prizeId, count);
                // 更新缓存
                String cacheKey = INVENTORY_CACHE_PREFIX + prizeId;
                LotteryPrize prize = getById(prizeId);
                redisTemplate.opsForValue().set(cacheKey, prize.getSurplusCount());
                return true;
            } else {
                log.error("增加库存失败,prizeId: {}, count: {}", prizeId, count);
                return false;
            }
        } catch (InterruptedException e) {
            log.error("获取库存锁中断,prizeId: {}", prizeId, e);
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 获取奖品库存数量
     *
     * @param prizeId 奖品ID
     * @return 库存数量
     */
    @Override
    public int getInventoryCount(Long prizeId) {
        if (ObjectUtils.isEmpty(prizeId)) {
            log.error("奖品ID为空,无法获取库存");
            return 0;
        }

        // 先从缓存获取
        String cacheKey = INVENTORY_CACHE_PREFIX + prizeId;
        Object cacheCount = redisTemplate.opsForValue().get(cacheKey);
        if (!ObjectUtils.isEmpty(cacheCount)) {
            try {
                return Integer.parseInt(cacheCount.toString());
            } catch (NumberFormatException e) {
                log.error("解析缓存库存失败,prizeId: {}", prizeId, e);
            }
        }

        // 缓存获取失败,从数据库获取
        LotteryPrize prize = getById(prizeId);
        int count = ObjectUtils.isEmpty(prize) ? 0 : prize.getSurplusCount();

        // 更新缓存
        redisTemplate.opsForValue().set(cacheKey, count);
        log.debug("从数据库获取库存,prizeId: {}, count: {}", prizeId, count);

        return count;
    }

    /**
     * 预热活动奖品库存到缓存
     *
     * @param activityId 活动ID
     */
    @Override
    public void preloadInventoryToCache(Long activityId) {
        if (ObjectUtils.isEmpty(activityId)) {
            log.error("活动ID为空,无法预热库存");
            return;
        }

        // 查询活动关联的奖品
        List<LotteryActivityPrize> activityPrizes = activityPrizeService.listByActivityId(activityId);
        if (CollectionUtils.isEmpty(activityPrizes)) {
            log.warn("活动未关联任何奖品,无需预热库存,activityId: {}", activityId);
            return;
        }

        // 批量查询奖品信息
        List<Long> prizeIds = activityPrizes.stream()
                .map(LotteryActivityPrize::getPrizeId)
                .collect(Collectors.toList());
        List<LotteryPrize> prizes = listByIds(prizeIds);

        if (CollectionUtils.isEmpty(prizes)) {
            log.warn("活动关联的奖品不存在,activityId: {}", activityId);
            return;
        }

        // 预热库存到缓存,设置过期时间为24小时
        for (LotteryPrize prize : prizes) {
            String cacheKey = INVENTORY_CACHE_PREFIX + prize.getId();
            redisTemplate.opsForValue().set(cacheKey, prize.getSurplusCount(), 24, TimeUnit.HOURS);
            log.debug("预热奖品库存到缓存,prizeId: {}, count: {}", prize.getId(), prize.getSurplusCount());
        }

        log.info("活动奖品库存预热完成,activityId: {}", activityId);
    }
}
代码语言:javascript
复制

6.6 控制器设计

控制器负责接收前端请求,调用服务层处理,并返回结果:

6.6.1 抽奖控制器(LotteryController.java)
代码语言:javascript
复制
package com.jam.lottery.controller;

import com.jam.lottery.common.Result;
import com.jam.lottery.service.LotteryService;
import com.jam.lottery.vo.DrawResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 抽奖控制器
 *
 * @author 果酱
 */
@Slf4j
@RestController
@RequestMapping("/api/v1/lottery")
@RequiredArgsConstructor
@Tag(name = "抽奖接口", description = "提供用户参与抽奖的相关接口")
public class LotteryController {

    private final LotteryService lotteryService;

    /**
     * 用户参与抽奖
     */
    @PostMapping("/draw")
    @Operation(summary = "用户参与抽奖", description = "用户参与指定活动的抽奖")
    public Result<DrawResultVO> draw(
            @Parameter(description = "用户ID", required = true)
            @RequestParam Long userId,
            @Parameter(description = "活动ID", required = true)
            @RequestParam Long activityId) {
        log.info("用户参与抽奖,userId: {}, activityId: {}", userId, activityId);
        try {
            DrawResultVO result = lotteryService.draw(userId, activityId);
            return Result.success(result);
        } catch (Exception e) {
            log.error("用户抽奖失败,userId: {}, activityId: {}", userId, activityId, e);
            return Result.fail(e.getMessage());
        }
    }

    /**
     * 查询用户抽奖次数
     */
    @PostMapping("/count")
    @Operation(summary = "查询用户抽奖次数", description = "查询用户在指定活动中的抽奖次数")
    public Result<Integer> countUserDraws(
            @Parameter(description = "用户ID", required = true)
            @RequestParam Long userId,
            @Parameter(description = "活动ID", required = true)
            @RequestParam Long activityId) {
        log.info("查询用户抽奖次数,userId: {}, activityId: {}", userId, activityId);
        try {
            int count = lotteryService.countUserDraws(userId, activityId);
            return Result.success(count);
        } catch (Exception e) {
            log.error("查询用户抽奖次数失败", e);
            return Result.fail(e.getMessage());
        }
    }
}
代码语言:javascript
复制

6.7 消息队列设计

使用 RabbitMQ 处理异步任务,如奖品发放通知:

6.7.1 消息生产者(LotteryMessageProducer.java)
代码语言:javascript
复制
package com.jam.lottery.mq.producer;

import com.alibaba.fastjson2.JSON;
import com.jam.lottery.constant.MqConstant;
import com.jam.lottery.vo.UserDrawVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.UUID;

/**
 * 抽奖消息生产者
 * 负责发送抽奖相关的消息
 *
 * @author 果酱
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class LotteryMessageProducer {

    private final RabbitTemplate rabbitTemplate;

    /**
     * 发送中奖消息
     *
     * @param userDrawVO 用户抽奖信息
     */
    public void sendPrizeMessage(UserDrawVO userDrawVO) {
        if (ObjectUtils.isEmpty(userDrawVO)) {
            log.error("发送中奖消息失败,消息内容为空");
            return;
        }

        try {
            // 生成消息ID
            String messageId = UUID.randomUUID().toString();
            CorrelationData correlationData = new CorrelationData(messageId);

            // 发送消息
            rabbitTemplate.convertAndSend(
                    MqConstant.LOTTERY_EXCHANGE,
                    MqConstant.PRIZE_DELIVERY_ROUTING_KEY,
                    JSON.toJSONString(userDrawVO),
                    correlationData
            );

            log.info("发送中奖消息成功,messageId: {}, userId: {}, prizeId: {}",
                    messageId, userDrawVO.getUserId(), userDrawVO.getPrizeId());
        } catch (Exception e) {
            log.error("发送中奖消息失败,userDrawVO: {}", JSON.toJSONString(userDrawVO), e);
            // 实际项目中可以考虑重试或存入本地消息表
        }
    }
}
代码语言:javascript
复制

6.7.2 消息消费者(PrizeDeliveryConsumer.java)
代码语言:javascript
复制
package com.jam.lottery.mq.consumer;

import com.alibaba.fastjson2.JSON;
import com.jam.lottery.constant.MqConstant;
import com.jam.lottery.entity.PrizeDelivery;
import com.jam.lottery.enums.DeliveryStatusEnum;
import com.jam.lottery.enums.PrizeTypeEnum;
import com.jam.lottery.service.PrizeDeliveryService;
import com.jam.lottery.service.PrizeService;
import com.jam.lottery.vo.UserDrawVO;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.io.IOException;
import java.time.LocalDateTime;

/**
 * 奖品发放消费者
 * 处理中奖消息,负责奖品的发放
 *
 * @author 果酱
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class PrizeDeliveryConsumer {

    private final PrizeDeliveryService deliveryService;
    private final PrizeService prizeService;

    /**
     * 处理中奖消息,发放奖品
     */
    @RabbitListener(queues = MqConstant.PRIZE_DELIVERY_QUEUE)
    public void handlePrizeDeliveryMessage(String message, Channel channel, Message amqpMessage) throws IOException {
        log.info("收到中奖消息,开始处理奖品发放: {}", message);

        try {
            // 解析消息
            UserDrawVO userDrawVO = JSON.parseObject(message, UserDrawVO.class);
            if (ObjectUtils.isEmpty(userDrawVO)) {
                log.error("消息内容解析失败,消息为空");
                // 确认消息已消费
                channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
                return;
            }

            // 检查消息是否已处理
            PrizeDelivery existingDelivery = deliveryService.getByRecordId(userDrawVO.getRecordId());
            if (!ObjectUtils.isEmpty(existingDelivery)) {
                log.warn("消息已处理,无需重复处理,recordId: {}", userDrawVO.getRecordId());
                channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
                return;
            }

            // 创建奖品发放记录
            PrizeDelivery delivery = createPrizeDelivery(userDrawVO);

            // 根据奖品类型发放奖品
            boolean deliverySuccess = false;
            if (PrizeTypeEnum.PHYSICAL.getType().equals(userDrawVO.getPrizeType())) {
                // 实物奖品,需要物流配送
                deliverySuccess = handlePhysicalPrize(delivery, userDrawVO);
            } else if (PrizeTypeEnum.VIRTUAL.getType().equals(userDrawVO.getPrizeType())) {
                // 虚拟物品,如游戏道具等
                deliverySuccess = handleVirtualPrize(delivery, userDrawVO);
            } else if (PrizeTypeEnum.COUPON.getType().equals(userDrawVO.getPrizeType())) {
                // 优惠券
                deliverySuccess = handleCouponPrize(delivery, userDrawVO);
            } else if (PrizeTypeEnum.POINT.getType().equals(userDrawVO.getPrizeType())) {
                // 积分
                deliverySuccess = handlePointPrize(delivery, userDrawVO);
            }

            // 更新发放状态
            updateDeliveryStatus(delivery, deliverySuccess);

            // 确认消息已消费
            channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
            log.info("奖品发放处理完成,recordId: {}, success: {}", userDrawVO.getRecordId(), deliverySuccess);
        } catch (Exception e) {
            log.error("处理中奖消息异常", e);
            // 消息处理失败,拒绝消息并将其重新入队
            channel.basicNack(amqpMessage.getMessageProperties().getDeliveryTag(), false, true);
        }
    }

    /**
     * 创建奖品发放记录
     */
    private PrizeDelivery createPrizeDelivery(UserDrawVO userDrawVO) {
        PrizeDelivery delivery = new PrizeDelivery();
        delivery.setRecordId(userDrawVO.getRecordId());
        delivery.setUserId(userDrawVO.getUserId());
        delivery.setActivityId(userDrawVO.getActivityId());
        delivery.setPrizeId(userDrawVO.getPrizeId());
        delivery.setPrizeType(userDrawVO.getPrizeType());
        delivery.setDeliveryStatus(DeliveryStatusEnum.PENDING.getCode());
        delivery.setCreateTime(LocalDateTime.now());
        deliveryService.save(delivery);
        return delivery;
    }

    /**
     * 处理实物奖品发放
     */
    private boolean handlePhysicalPrize(PrizeDelivery delivery, UserDrawVO userDrawVO) {
        try {
            // 实际项目中,这里会调用物流系统API创建物流单
            // 这里简化处理,模拟物流信息
            String logisticsInfo = "{\"logisticsCompany\":\"顺丰速运\",\"trackingNumber\":\"SF" + System.currentTimeMillis() + "\"}";
            delivery.setLogisticsInfo(logisticsInfo);
            log.info("实物奖品发放处理,recordId: {}, logisticsInfo: {}", userDrawVO.getRecordId(), logisticsInfo);
            return true;
        } catch (Exception e) {
            log.error("处理实物奖品发放失败,recordId: {}", userDrawVO.getRecordId(), e);
            return false;
        }
    }

    /**
     * 处理虚拟物品发放
     */
    private boolean handleVirtualPrize(PrizeDelivery delivery, UserDrawVO userDrawVO) {
        try {
            // 实际项目中,这里会调用对应的虚拟物品发放API
            // 这里简化处理,生成一个虚拟物品码
            String virtualCode = "VIRTUAL" + System.currentTimeMillis() + userDrawVO.getUserId();
            delivery.setVirtualCode(virtualCode);
            log.info("虚拟物品发放处理,recordId: {}, virtualCode: {}", userDrawVO.getRecordId(), virtualCode);
            return true;
        } catch (Exception e) {
            log.error("处理虚拟物品发放失败,recordId: {}", userDrawVO.getRecordId(), e);
            return false;
        }
    }

    /**
     * 处理优惠券发放
     */
    private boolean handleCouponPrize(PrizeDelivery delivery, UserDrawVO userDrawVO) {
        try {
            // 实际项目中,这里会调用优惠券系统API为用户发放优惠券
            // 这里简化处理,生成一个优惠券码
            String couponCode = "COUPON" + System.currentTimeMillis() + userDrawVO.getUserId();
            delivery.setVirtualCode(couponCode);
            log.info("优惠券发放处理,recordId: {}, couponCode: {}", userDrawVO.getRecordId(), couponCode);
            return true;
        } catch (Exception e) {
            log.error("处理优惠券发放失败,recordId: {}", userDrawVO.getRecordId(), e);
            return false;
        }
    }

    /**
     * 处理积分发放
     */
    private boolean handlePointPrize(PrizeDelivery delivery, UserDrawVO userDrawVO) {
        try {
            // 实际项目中,这里会调用积分系统API为用户增加积分
            // 这里简化处理
            log.info("积分发放处理,recordId: {}, userId: {}", userDrawVO.getRecordId(), userDrawVO.getUserId());
            return true;
        } catch (Exception e) {
            log.error("处理积分发放失败,recordId: {}", userDrawVO.getRecordId(), e);
            return false;
        }
    }

    /**
     * 更新发放状态
     */
    private void updateDeliveryStatus(PrizeDelivery delivery, boolean success) {
        if (success) {
            delivery.setDeliveryStatus(DeliveryStatusEnum.COMPLETED.getCode());
            delivery.setDeliveryTime(LocalDateTime.now());
        } else {
            delivery.setDeliveryStatus(DeliveryStatusEnum.FAILED.getCode());
            delivery.setRemark("奖品发放失败,请稍后重试");
        }
        delivery.setUpdateTime(LocalDateTime.now());
        deliveryService.updateById(delivery);
    }
}
代码语言:javascript
复制

七、高并发处理与性能优化

抽奖系统往往面临高并发场景,特别是在营销活动高峰期,需要从多个维度进行优化:

7.1 缓存优化

  1. 热点数据缓存将活动信息、奖品信息、抽奖规则等热点数据缓存到 Redis 中,减少数据库访问
  2. 库存缓存奖品库存优先从 Redis 中读取和扣减,异步更新数据库
  3. 用户抽奖次数缓存用户在活动中的抽奖次数缓存到 Redis,减少数据库计数查询
代码语言:javascript
复制
/**
 * 缓存用户抽奖次数示例
 */
private int getUserDrawCountFromCache(Long userId, Long activityId) {
    String cacheKey = "lottery:user:draw:count:" + activityId + ":" + userId;
    Object countObj = redisTemplate.opsForValue().get(cacheKey);
    if (countObj != null) {
        return Integer.parseInt(countObj.toString());
    }

    // 缓存不存在,从数据库查询并更新缓存
    int count = lotteryRecordMapper.countUserDraws(userId, activityId);
    redisTemplate.opsForValue().set(cacheKey, count, 1, TimeUnit.HOURS);
    return count;
}

/**
 * 递增用户抽奖次数缓存
 */
private void incrementUserDrawCountCache(Long userId, Long activityId) {
    String cacheKey = "lottery:user:draw:count:" + activityId + ":" + userId;
    redisTemplate.opsForValue().increment(cacheKey);
    // 设置过期时间,防止缓存膨胀
    redisTemplate.expire(cacheKey, 1, TimeUnit.HOURS);
}
代码语言:javascript
复制

7.2 异步处理

  1. 非核心流程异步化将奖品发放、通知推送等非核心流程通过消息队列异步处理
  2. 数据库更新异步化库存扣减等操作先更新缓存,再异步更新数据库

7.3 限流与熔断

  1. 接口限流使用 Redis 或网关对抽奖接口进行限流,防止流量过载
  2. 服务熔断使用 Sentinel 或 Resilience4j 实现服务熔断,保护系统稳定
代码语言:javascript
复制
/**
 * 基于Redis的限流实现
 */
@Component
public class RedisRateLimiter {

    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public RedisRateLimiter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 检查是否允许访问
     *
     * @param key      限流键
     * @param maxCount 最大允许次数
     * @param period   时间窗口,单位秒
     * @return 是否允许访问
     */
    public boolean allowAccess(String key, int maxCount, int period) {
        String cacheKey = "rate:limiter:" + key;

        // 使用Redis的INCR命令实现计数器
        Long count = redisTemplate.opsForValue().increment(cacheKey);

        if (count != null && count == 1) {
            // 第一次访问,设置过期时间
            redisTemplate.expire(cacheKey, period, TimeUnit.SECONDS);
        }

        return count != null && count <= maxCount;
    }
}
代码语言:javascript
复制

7.4 数据库优化

  1. 索引优化为频繁查询的字段建立索引,如活动 ID、用户 ID 等
  2. 分库分表对于数据量大的表(如抽奖记录表),采用分库分表策略
  3. 读写分离主库负责写操作,从库负责读操作,提高查询性能

7.5 分布式锁

使用 Redisson 实现分布式锁,解决并发场景下的数据一致性问题,如库存扣减、抽奖次数限制等。

八、系统监控与运维

8.1 监控指标

  1. 业务指标
    • 总抽奖次数
    • 中奖率
    • 各奖品中奖次数
    • 活跃用户数
  2. 技术指标
    • 接口响应时间
    • 接口调用次数
    • 错误率
    • 数据库连接数
    • Redis 内存使用量
    • 消息队列堆积数

8.2 日志管理

  1. 日志分级按照 ERROR、WARN、INFO、DEBUG 等级别记录日志
  2. 日志聚合使用 ELK 栈(Elasticsearch、Logstash、Kibana)进行日志收集和分析
  3. 关键操作日志记录用户抽奖、中奖、奖品发放等关键操作日志,便于问题排查和审计

8.3 告警机制

  1. 异常告警系统异常时通过邮件、短信等方式通知运维人员
  2. 阈值告警当某些指标超过阈值(如接口响应时间过长、错误率过高)时触发告警
  3. 业务告警当奖品库存不足、活动参与人数异常等业务情况时触发告警

九、总结与展望

抽奖系统作为一种常见的用户互动和营销工具,其设计和实现涉及多个技术领域的知识。本文从需求分析、架构设计、数据库设计、核心代码实现到高并发处理,全面介绍了一个专业抽奖系统的构建过程。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言:为什么需要专业的抽奖系统?
  • 一、抽奖系统需求分析
    • 1.1 核心功能需求
    • 1.2 非功能需求
    • 1.3 业务场景分析
  • 二、抽奖系统架构设计
    • 2.1 架构说明
    • 2.2 核心流程设计
  • 三、数据库设计
    • 3.1 活动表(lottery_activity)
    • 3.2 奖品表(lottery_prize)
    • 3.3 活动奖品关联表(lottery_activity_prize)
    • 3.4 抽奖规则表(lottery_strategy)
    • 3.5 用户抽奖记录表(lottery_record)
    • 3.6 奖品发放表(prize_delivery)
  • 四、核心技术选型
  • 五、项目初始化与配置
    • 5.1 Maven 依赖配置(pom.xml)
    • 5.2 核心配置文件(application.yml)
  • 六、核心代码实现
    • 6.1 实体类设计
      • 6.1.1 活动实体(LotteryActivity.java)
      • 6.1.2 奖品实体(LotteryPrize.java)
      • 6.1.3 活动奖品关联实体(LotteryActivityPrize.java)
      • 6.1.4 抽奖规则实体(LotteryStrategy.java)
      • 6.1.5 抽奖记录实体(LotteryRecord.java)
      • 6.1.6 奖品发放实体(PrizeDelivery.java)
    • 6.2 数据访问层(Mapper)
      • 6.2.1 活动 Mapper(LotteryActivityMapper.java)
    • 6.3 服务层设计
      • 6.3.1 抽奖服务接口(LotteryService.java)
      • 6.3.2 抽奖服务实现(LotteryServiceImpl.java)
    • 6.4 抽奖策略设计
      • 6.4.1 策略接口(DrawStrategy.java)
      • 6.4.2 概率型抽奖策略(ProbabilityDrawStrategy.java)
      • 6.4.3 策略工厂(StrategyFactory.java)
    • 6.5 库存服务设计
      • 6.5.1 库存服务接口(PrizeInventoryService.java)
      • 6.5.2 库存服务实现(PrizeInventoryServiceImpl.java)
    • 6.6 控制器设计
      • 6.6.1 抽奖控制器(LotteryController.java)
    • 6.7 消息队列设计
      • 6.7.1 消息生产者(LotteryMessageProducer.java)
      • 6.7.2 消息消费者(PrizeDeliveryConsumer.java)
  • 七、高并发处理与性能优化
    • 7.1 缓存优化
    • 7.2 异步处理
    • 7.3 限流与熔断
    • 7.4 数据库优化
    • 7.5 分布式锁
  • 八、系统监控与运维
    • 8.1 监控指标
    • 8.2 日志管理
    • 8.3 告警机制
  • 九、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档