游戏奖励系统完整示例:3步搞定项目级代码,告别教程焦虑
看了一堆教程还是不会写项目?别怪你,是那些碎片化文章没给你完整示例。今天不扯虚的,直接上代码,从零搭建一个生产级的游戏奖励系统。
项目目标:从玩具到生产
很多新手写的奖励系统,发个金币就完事了。但真实场景下,你要处理并发领取、防重复刷取、奖励过期、库存扣减。本文目标是实现一个具备以下能力的核心模块:
- 原子性操作:防止高并发下超发。
- 幂等性设计:同一请求多次调用结果一致。
- 审计日志:所有变动可追溯,符合合规要求。
- 配置化奖励:支持不同活动、不同用户的差异化奖励。
这不是一个简单的if-else,而是一个涉及数据库事务、分布式锁、消息队列的完整闭环。
目录结构:清晰分层是关键
项目采用标准分层架构,避免业务逻辑耦合。
game-reward-system/
├── src/
│ ├── main/
│ │ ├── java/com/game/reward/
│ │ │ ├── config/ # 配置类
│ │ │ ├── controller/ # 接口层
│ │ │ ├── service/ # 业务逻辑层
│ │ │ ├── repository/ # 数据访问层
│ │ │ ├── entity/ # 数据库实体
│ │ │ ├── dto/ # 数据传输对象
│ │ │ └── exception/ # 自定义异常
│ │ └── resources/
│ │ ├── application.yml # 配置文件
│ │ └── db/migration/ # 数据库迁移脚本
│ └── test/ # 单元测试
├── pom.xml # Maven依赖
└── README.md
关键点:repository层只负责CRUD,service层处理所有业务规则。这种分离让后续替换Redis或数据库时,改动范围可控。
核心代码实现:逐行拆解
1. 数据库设计:别只存金币数
奖励系统最忌“黑盒”。我们需要记录每一笔变动。
-- 奖励记录表
CREATE TABLE reward_record (id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',user_id BIGINT NOT NULL COMMENT '用户ID',reward_type VARCHAR(50) NOT NULL COMMENT '奖励类型: COIN, EXP, ITEM',quantity INT NOT NULL COMMENT '数量',biz_code VARCHAR(100) NOT NULL COMMENT '业务唯一码,用于幂等',status TINYINT DEFAULT 0 COMMENT '0-待发放, 1-已发放, 2-已过期',created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_biz_code (biz_code), -- 关键:唯一索引防重INDEX idx_user_id (user_id)
) COMMENT='奖励发放记录';
注意:biz_code是幂等性的核心。它由用户ID + 活动ID + 时间戳生成,确保同一业务场景下只生成一条记录。
2. Service层:事务与幂等
这是最容易出Bug的地方。很多教程直接insert,并发下就炸了。
@Service
public class RewardService {@Autowiredprivate RewardRepository rewardRepository;@Autowiredprivate StringRedisTemplate redisTemplate;/*** 发放奖励 - 核心方法* @param userId 用户ID* @param rewardType 奖励类型* @param quantity 数量* @param bizCode 业务唯一码*/@Transactional(rollbackFor = Exception.class)public boolean grantReward(Long userId, String rewardType, int quantity, String bizCode) {// 1. 幂等检查:先查库,再查Redisif (rewardRepository.existsByBizCode(bizCode)) {log.info("重复请求,忽略: {}", bizCode);return false;}// 2. Redis预占位,防止数据库唯一索引冲突(可选优化)String lockKey = "reward:lock:" + bizCode;Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);if (Boolean.FALSE.equals(locked)) {throw new BusinessException("并发冲突,请重试");}try {// 3. 插入记录RewardRecord record = new RewardRecord();record.setUserId(userId);record.setRewardType(rewardType);record.setQuantity(quantity);record.setBizCode(bizCode);record.setStatus(0); // 待发放rewardRepository.save(record);// 4. 实际发放逻辑(此处省略,调用积分中心/背包服务)doActualGrant(record);// 5. 更新状态record.setStatus(1);rewardRepository.save(record);return true;} catch (Exception e) {// 6. 异常回滚,释放Redis锁redisTemplate.delete(lockKey);throw e;}}
}
逐行解析:
@Transactional:确保数据库操作原子性。setIfAbsent:利用Redis原子操作做初步防重,减轻DB压力。try-catch:无论成功失败,必须释放Redis锁,防止死锁。existsByBizCode:数据库层面的最终防线。即使Redis挂了,DB的唯一索引也能兜底。
3. 配置化奖励:告别硬编码
不同活动奖励不同,不能写死在代码里。
# application.yml
game:reward:config:- activityId: "newbie"rewardType: "COIN"quantity: 100expireHours: 24- activityId: "vip_weekly"rewardType: "ITEM"quantity: 1itemId: "sword_001"expireHours: 168
通过@ConfigurationProperties绑定到对象,动态加载配置。这样运营改奖励,不需要发版。
运行与测试:MockMvc实战
不要只靠Postman点按钮。用JUnit+MockMvc写接口测试。
@SpringBootTest
@AutoConfigureMockMvc
class RewardControllerTest {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Testvoid testGrantReward_Success() throws Exception {// 1. 准备数据RewardRequest request = new RewardRequest();request.setUserId(1001L);request.setActivityId("newbie");request.setBizCode("test_biz_001");// 2. 执行请求mockMvc.perform(post("/api/reward/grant").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(request))).andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)).andExpect(jsonPath("$.data.success").value(true));// 3. 验证数据库// 可注入Repository进行断言}
}
测试要点:
- 模拟并发请求,验证幂等性。
- 模拟Redis宕机,验证DB唯一索引兜底。
- 模拟奖励过期,验证状态流转。
优化扩展:生产级考量
- 异步发放:高并发下,同步发放会拖慢主流程。将
doActualGrant改为发送MQ消息,由消费者异步处理。 - 缓存策略:用户当前余额/背包数据缓存在Redis,DB仅做持久化。注意双写一致性。
- 限流降级:使用Sentinel对
grantReward接口限流,防止恶意刷接口。 - 监控告警:埋点监控
bizCode重复率、发放失败率。当失败率>1%时触发告警。
避坑指南:
- 不要用
System.currentTimeMillis()做bizCode的一部分,高并发下可能重复。建议加UUID或雪花算法ID。 - 事务中不要做远程调用:
doActualGrant如果调用了远程服务,会导致事务长时间持有,连接池耗尽。务必改为异步或本地消息表。
小结:从代码到思维
这个完整示例不是让你抄代码,而是让你理解:
- 幂等性是分布式系统的生命线。
- 数据库唯一索引是最后的防线,不要过度依赖缓存。
- 分层架构让你能从容应对技术栈变更。
游戏奖励系统只是冰山一角。类似的思路适用于订单支付、库存扣减、积分兑换等几乎所有涉及“资源变动”的场景。
当你下次遇到“并发超卖”或“重复发奖”的Bug时,想想今天的bizCode和唯一索引。
还有什么不懂的?评论区留言挨个回。 特别是关于Redis锁与DB事务一致性的高阶问题,欢迎探讨。