【抽奖系统-0】Redis 缓存与 RabbitMQ 削峰实战;架构梳理
一、技术栈层面技术选型框架Spring Boot 3.2.6JDKJava 17ORMMyBatis 3.0.3纯注解方式无 XML数据库MySQL 8.x缓存RedisStringRedisTemplate消息队列RabbitMQDirect 交换机 死信队列序列化JacksonJWTjjwt 0.11.5HMAC-SHA工具库Hutool 5.8.25AES 加密、SHA256 摘要短信阿里云 SMS SDK邮件Spring Boot Starter Mail参数校验spring-boot-starter-validation前端纯 HTML/CSS/JS静态页面直接放 resources/static二、分层架构三、数据库设计5张表表名作用关键字段user用户表user_name,email,phone_number(加密存储),password(SHA256摘要),identity(权限ADMIN/NORMAL)prize奖品表name,description,price,image_urlactivity活动表activity_name,description,status(活动状态RUNNING/COMPLETED)activity_prize活动-奖品关联表activity_id,prize_id,prize_amount(奖品总数量),prize_tiers(奖品等级一等奖 / 二等奖 / 三等奖),status(关联状态INIT/COMPLETED)activity_user活动-用户关联表activity_id,user_id,user_name,status(报名/参与状态INIT/COMPLETED)winning_record中奖记录表activity_id,prize_id,winner_id,winner_name,winning_time...四、核心业务流程1. 用户认证模块注册与双模式登录实现了敏感数据的非对称/对称加密并采用 JWT 实现无状态分布式身份鉴权用户注册输入信息 →参数校验 → 密码摘要化使用 SHA256 加盐防撞 → 手机号对称加密使用 AES 算法确保数据库中不直接明文存储隐私数据 →落库并返回自增主键userId密码登录支持邮箱/手机号登录 → 后端提取对应凭证 → 进行SHA256 比对前端明文与数据库密文比对 → 验证通过后由工具类签发 JWT TokenToken 载荷包含userId和identity权限标识ADMIN/NORMAL设置1h严格过期时间短信验证码登录输入手机号 → 接入阿里云 SMS 网关发送 6 位验证码 → 同步缓存至 RedisKey:SMS_CODE_{phone}, TTL: 60s → 用户输入验证码比对 → 验证成功后签发 JWT2.活动创建事务性活动创建涉及多张表的联动变更属于典型的写多于读/强一致性场景创建活动参数校验(用户存在性/奖品存在性/人数≥奖品数/等奖合法性)Transactional闭环控制使用 Spring 声明式事务确保以下四大步骤要么全部成功要么全部回滚步骤 ①向activity表插入活动主记录初始化状态为RUNNING。步骤 ②调用batchInsert批量向activity_prize中间表插入奖品配额状态为INIT。步骤 ③调用batchInsert批量向activity_user关联表导入参与白名单状态为INIT。步骤 ④组装完全体的ActivityDetailDTO对象同步回种至 Redis 缓存Key:ACTIVITY_{id}, 过期时间 3天实现后续抽奖流程的缓存就近读取。在事务开启前进行严格的业务前置检查防止非法请求占用数据库连接资源存在性检查发起创建的用户是否存在、关联的奖品是否存在。业务防呆校验活动报名人数上限是否 $\ge$ 奖品总数。合规检查奖品等阶一/二/三等奖是否合法。3.核心抽奖模块RabbitMQ 异步削峰流量抽奖瞬间流量极高如果直接让请求穿透到 MySQL会导致数据库瞬间瘫痪。因此系统采用了前台快速响应、后台异步消费的 MQ 削峰设计流量异步化Controller 层前端发起抽奖 → Controller 快速接收 → Service 层不做复杂交互直接将请求参数序列化为 JSON 字符串→ 通过RabbitTemplate投递到 Direct 交换机【此时请求已成功脱离本地线程Controller 立刻为前端返回“排队中/处理中”用户页面体验极度流畅响应时间从数百毫秒压缩至数毫秒】异步消费处理MqReceiver 核心逻辑核心参数二次校验校验活动、奖品是否依旧在有效期内。检查活动是否已完成。核心防超卖校验奖品是否已抽完、当前中奖人数是否已达到奖品上限。MqReceiver监听队列并开始执行真正的抽奖重活状态机扭转优雅地控制活动状态由RUNNING→COMPLETED以及关联表状态的变更三方服务解耦优化中奖后的短信通知阿里云和邮件通知Spring Mail属于耗时较长的 IO 操作。系统将其放入独立的自定义线程池中异步执行确保不阻塞 MQ 消费主线程4.中奖记录查询模块高性能快照读用户查询中奖名单 → 优先检索 Redis 缓存 → 若缓存未命中则穿透至 MySQL 数据库查询拿到结果后反向写回 Redis并设置2天 相对短的有效期保证缓存的整体新鲜度五、状态机模式设计模式运用面对活动、奖品、人员三个维度的复杂状态扭转时普通的if-else极难维护六、死信队列抽奖数据涉及真实的资产发放消息如果因异常意外丢失是绝对不可接受的。你的设计中引入了死信交换机来建立兜底机制七、数据加密我们使用Hutool工具包加密引入相关依赖1. 手机号→ AES 对称加密可逆手机号写入 MySQL 之前MyBatis 的 TypeHandler 自动用 AES 对称加密转换成密文存储读取时自动解密回明文------可能会破解密钥进行解密♦涉及的表和字段user 表的 phone_number 字段。用户注册、短信登录、中奖通知发短信时都经过 TypeHandler加密写入或读取存储的密文解密2. 密码→SHA256 哈希不可逆密码不需要还原只需要验证用户输入的是不是同一个密码。用哈希即使数据库被拖库攻击者也反推不出原始密码。------泄露密文也无法解密♦彩虹攻击暴力枚举列出所有字符串的密文一 一对比解出原字符串密码♦涉及的表和字段user 表的 password 字段3.加盐加密密文 Hash(明文密码随机盐)持久化存储将“最终密文”和“随机盐”一起存入数据库的用户表校验用同样的加密算法算出密文对比数据库里的密文4. JWT→HMAC-SHA256 签名不是加密用这个密钥对 {id, identity, exp} 做签名签名只是用来防篡改服务器用密钥验签确保token 没被改过♦服务器把token发给客户端客户端每次会话携带这个token去客户端客户端拿着密钥校验是不是真实用户八、接口输入输出一览表接口路径请求方法输入数据 (Request)输出数据 (Response)/registerPOSTJSON:name,mail,phoneNumber,password,identity{ code: 200, data: { userId: 1 } }/verification-code/sendGETQuery:phoneNumber{ code: 200, data: true }/password/loginPOSTJSON:loginName(邮箱/手机),password,mandatoryIdentity{ code: 200, data: { token: ..., identity: ... } }/message/loginPOSTJSON:loginMobile,verificationCode,mandatoryIdentity{ code: 200, data: { token: ..., identity: ... } }/base-user/find-listGETQuery:identity(ADMIN/NORMAL){ code: 200, data: [{ userId: 1, userName: ..., identity: ... }] }/prize/createPOSTMultipart:param(CreatePrizeParam JSON) prizePic(文件){ code: 200, data: prizeId }/prize/find-listGETQuery:PageParam(分页参数page,size){ code: 200, data: { total: 100, records: [...] } }/pic/uploadPOSTMultipart:MultipartFile(图片文件)http://.../static/image.png(图片URL字符串)/activity/createPOSTJSON:activityName,description,activityUserList,activityPrizeList{ code: 200, data: { activityId: 1 } }/activity/find-listGETQuery:PageParam(分页参数){ code: 200, data: { total: 50, records: [...] } }/activity-detail/findGETQuery:activityId{ code: 200, data: { 活动基础信息: {}, 奖品列表: [], 人员列表: [] } }/draw-prizePOSTJSON:activityId,prizeId,winnerList,winningTime{ code: 200, data: true }/winning-records/showPOSTJSON:activityId,prizeId(可选){ code: 200, data: [{中奖记录1}, {中奖记录2}] }