个人邮件和全局邮件实现方案
在游戏服务端开发中,个人邮件和全服邮件最大的区别是发送范围不同,因此存储和投递方式也不同。
一般来说:
- 个人邮件:直接写入玩家的个人邮箱,通常持久化到数据库。
- 全服邮件:通常采用 全局存储 + 玩家登录或查询时懒加载 的方式,避免一次性给所有玩家插入邮件。
下面从实际服务端开发的角度介绍。
个人邮件怎么实现?
个人邮件比较简单,例如:
- 玩家完成任务后收到奖励邮件。
- 系统向某个玩家发送补偿邮件。
- 玩家之间发送邮件。
- 排行榜结算时向指定玩家发放奖励。
1. 数据库设计
假设使用 MySQL,可以设计一张个人邮件表:
CREATE TABLE player_mail (
id BIGINT PRIMARY KEY,
player_id BIGINT NOT NULL,
title VARCHAR(255),
content TEXT,
attachments JSON,
is_read BOOLEAN DEFAULT FALSE,
is_claimed BOOLEAN DEFAULT FALSE,
create_time DATETIME,
expire_time DATETIME,
INDEX idx_player_id (player_id)
);
其中:
id:邮件唯一 ID。player_id:接收邮件的玩家。attachments:邮件附件,例如金币、钻石、道具。is_read:是否阅读。is_claimed:附件是否领取。
2. 发送流程
例如,给玩家 10001 发送一封邮件。
游戏业务模块
|
| 发送邮件请求
v
邮件服务 MailService
|
| 构造邮件数据
v
MySQL 持久化
|
| 玩家在线?
|
+---- 是 ---> 通知玩家有新邮件
|
+---- 否 ---> 不需要立即通知
玩家上线后,可以通过数据库查询:
SELECT *
FROM player_mail
WHERE player_id = 10001
AND expire_time > NOW()
ORDER BY create_time DESC;
这样就可以获取玩家的邮件列表。
需要注意:玩家不在线,也不影响邮件投递。
因为邮件已经持久化到了数据库中。
全服邮件怎么实现?
全服邮件才是值得重点讨论的地方。
例如:
游戏有 100 万玩家,运营需要给所有玩家发送一封补偿邮件。
最直观的方式就是:
for (auto& player : allPlayers) {
SendMail(player.id, mail);
}
这样做虽然简单,但是存在明显问题。
如果游戏有 100 万玩家,就需要:
- 查询 100 万个玩家。
- 生成 100 万条邮件数据。
- 向数据库插入 100 万条记录。
- 可能还需要触发大量通知。
对于大型游戏,这样做会产生较大的数据库写入压力。
因此,全服邮件通常会采用另一种设计。
1. 全局邮件表
单独建立一张全服邮件表:
CREATE TABLE global_mail (
id BIGINT PRIMARY KEY,
title VARCHAR(255),
content TEXT,
attachments JSON,
create_time DATETIME,
expire_time DATETIME
);
当运营发送全服邮件时,只需要向这张表插入一条记录。
例如:
| id | title | content | attachments |
|---|---|---|---|
| 1001 | 停服补偿 | 感谢大家的支持 | 100钻石 |
| 1002 | 版本更新 | 新版本已经上线 | 200金币 |
这样,不管游戏有 10 万玩家还是 100 万玩家,发送一封全服邮件都只需要保存一份邮件内容。
2. 玩家怎么收到全服邮件?
采用懒加载机制。
当玩家登录游戏,或者打开邮箱时:
玩家登录
|
v
加载个人邮件
|
v
查询当前有效的全服邮件
|
v
筛选玩家有资格接收的邮件
|
v
判断哪些邮件尚未领取或处理
|
v
合并个人邮件与全服邮件
|
v
返回客户端
例如:
vector<Mail> GetPlayerMails(int64_t playerId) {
// 1. 获取个人邮件
auto personalMails = GetPersonalMails(playerId);
// 2. 获取有效的全服邮件
auto globalMails = GetValidGlobalMails();
// 3. 获取玩家对全服邮件的处理状态
auto states = GetGlobalMailStates(playerId);
// 4. 合并玩家有资格查看的邮件
return MergeMails(personalMails, globalMails, states);
}
这里需要特别注意:
懒加载不一定意味着真的把全服邮件插入个人邮件表。
实际上有两种方案。
- 延迟投递:发送时只保存模板,玩家登录后再创建个人邮件。减少发送时的集中写入,但仍然需要为登录玩家创建邮件记录。
- 查询时合并:发送时只保存全服邮件,玩家查询时动态合并,只有状态发生变化时才保存个人状态。通常存储开销最低,但查询和状态管理逻辑更复杂。
怎么判断全服邮件是否已经领取?
这里是邮件系统的一个关键问题。
假设全服邮件 ID 为 1001。
玩家 A 领取了附件,玩家 B 没有领取。
由于全服邮件只有一份,因此不能直接修改 global_mail 中的领取状态。
领取状态必须与玩家关联。
可以设计一张状态表:
CREATE TABLE player_global_mail_state (
player_id BIGINT NOT NULL,
mail_id BIGINT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
is_claimed BOOLEAN DEFAULT FALSE,
PRIMARY KEY (player_id, mail_id)
);
例如:
| player_id | mail_id | is_read | is_claimed |
|---|---|---|---|
| 10001 | 1001 | 1 | 1 |
| 10002 | 1001 | 1 | 0 |
| 10003 | 1001 | 0 | 0 |
这样就能够知道:
- 玩家 10001 已经领取奖励。
- 玩家 10002 已经阅读,但没有领取。
- 玩家 10003 还没有阅读。
而全服邮件的内容仍然只保存一份。
通常只需要为玩家已经产生状态变化的邮件保存记录;没有状态记录的邮件可以视为未读、未领取。
邮件附件怎么避免重复领取?
这也是实际开发中非常重要的一点。
假设玩家点击领取 100 钻石。
如果客户端因为网络超时重复发送领取请求:
第一次请求:领取100钻石
第二次请求:领取100钻石
第三次请求:领取100钻石
如果服务端没有做好处理,就可能出现重复发奖。
因此,邮件附件领取必须保证幂等性。
一种实现方式是:
玩家请求领取附件
|
v
校验邮件是否存在、是否过期
|
v
检查附件是否已领取
|
+---- 已领取 ---> 返回已领取
|
v
执行原子领取流程
|
+---- 发放道具
|
+---- 标记邮件已领取
|
v
返回成功
这里必须强调:
不能简单地先判断 is_claimed == false,然后发放奖励。
因为两个并发请求可能同时通过检查。
如果玩家背包和邮件状态位于同一个数据库,可以通过事务、条件更新和唯一约束实现原子领取。
如果背包系统与邮件系统是两个独立服务,则需要通过唯一的奖励发放流水号实现幂等,并结合可靠消息或补偿机制处理跨服务失败。
实际开发中还需要考虑什么?
除了个人邮件和全服邮件的基本实现,还需要考虑:
- 邮件过期:定时清理过期邮件,或者查询时直接过滤。
- 新玩家是否接收历史邮件:通常需要根据角色创建时间、邮件发放时间和运营规则判断。
- 分区分服:全服邮件究竟是当前区服所有玩家,还是整个游戏所有玩家。
- 邮件数量限制:例如个人邮箱最多保存 100 封邮件。
- 离线邮件:玩家离线期间依然能够正常接收邮件。
- 全服邮件缓存:可以将有效全服邮件缓存在 Redis 或服务端内存中,降低数据库查询压力。
- 批量领取:需要保证每封邮件独立幂等,避免重复领取。
- 运营邮件撤回:撤回时需要考虑附件是否已经被玩家领取。

浙公网安备 33010602011771号