问题场景
设计一个支持多账号合并计算的抽奖系统,需满足以下要求:
-
同一自然人的多次参与需合并计算权重(不同账号使用相同手机号+身份证视为同一人)
-
每个自然人仅有一次中奖机会
-
支持管理员动态设置中奖人数
-
保证高并发下的数据一致性
核心挑战
-
身份识别:跨账号的同一自然人识别
-
权重计算:合并不同账号的参与次数
-
随机公平性:参与次数越多中奖概率越高
-
数据一致性:抽奖过程需原子化操作
技术实现方案
1. 数据分组策略
关键逻辑:
var groupedData = allRecords
.GroupBy(r => new { r.Mobile, r.IdentityNumber })
.Select(g => new {
UniqueKey = g.Key,
TotalEntries = g.Count(),
RelatedRecords = g.ToList()
});
效果:
-
合并不同账号但相同手机号+身份证的记录
-
每组权重=该自然人的总参与次数
2. 加权随机算法
实现步骤:
-
计算总权重值:
-
生成随机数:
-
动态遍历累加:
while (需要选择更多中奖者 && 剩余可选组存在): 生成随机数 累加权重直到超过随机数 选中当前组 更新总权重(排除已选组)
特点:
-
时间复杂度O(n)
-
动态调整权重分布
3. 事务处理设计
操作流程:

4. 关键异常处理
| 异常类型 | 处理方案 |
|---|---|
| 并发冲突 | 数据库事务隔离级别设置为RepeatableRead |
| 无效输入 | 前置验证:中奖人数>0、活动存在性检查 |
| 数据空值 | 返回明确错误码:"NO_ELIGIBLE_CANDIDATES" |
架构优化建议
1. 数据层优化
-
建立组合索引:
(Mobile, IdentityNumber) -
缓存预热:活动开始前加载基础数据到Redis
-
读写分离:抽奖记录写入主库,统计查询走从库
2. 业务逻辑解耦
public interface ILotteryStrategy {
List<Winner> SelectWinners(List<Group> groups, int count);
}
public class WeightedRandomStrategy : ILotteryStrategy { ... }
public class FairDrawStrategy : ILotteryStrategy { ... }
3. 安全防护
-
数据脱敏:返回结果隐藏身份证中间字段
-
防重复提交:客户端Token机制
-
SQL防护:全参数化查询
4. 监控指标
| 指标名称 | 监控方式 | 阈值预警 |
|---|---|---|
| 抽奖成功率 | Prometheus计数器 | <95% |
| 事务耗时 | ELK日志分析 | >500ms |
| 内存占用 | APM监控 | >70% JVM |
典型问题排查指南
Q1:出现重复中奖记录怎么办?
-
检查分组逻辑是否包含全部识别字段
-
验证事务隔离级别是否合理
-
检查是否有未启用软删除的历史数据
Q2:加权随机分布不均匀?
-
验证权重计算是否正确
-
测试随机数生成器的分布特性
-
检查动态权重调整时的数值更新
Q3:高并发时性能下降?
-
添加数据库连接池监控
-
检查是否缺少(Mobile, IdentityNumber)索引
-
分析慢查询日志
设计原则总结
-
唯一性准则:以手机号+身份证为自然人唯一标识
-
权重可累加:合并不同账号的参与记录
-
原子性保证:抽奖过程全程事务保护
-
可扩展架构:策略模式支持算法替换
核心代码实现(部分脱敏)
1. 数据模型
// Models/Entities/Campaign.cs
public class Campaign : BaseEntity
{
public string Title { get; set; }
public bool IsActive { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
// Models/Entities/Participant.cs
public class Participant : BaseEntity
{
public string CampaignId { get; set; }
public string UserId { get; set; }
public string Mobile { get; set; }
public string IdentityNumber { get; set; }
public int EntriesCount { get; set; }
}
// Models/Entities/Winner.cs
public class Winner : BaseEntity
{
public string CampaignId { get; set; }
public string ParticipantId { get; set; }
}
2. 服务层接口
// Services/ILotteryService.cs
public interface ILotteryService
{
Task<LotteryResult> GenerateWinnersAsync(LotteryRequest request);
}
3. 核心算法实现
// Services/LotteryService.cs
public class LotteryService : ILotteryService
{
private readonly IParticipantRepository _participantRepo;
private readonly IWinnerRepository _winnerRepo;
public LotteryService(
IParticipantRepository participantRepo,
IWinnerRepository winnerRepo)
{
_participantRepo = participantRepo;
_winnerRepo = winnerRepo;
}
public async Task<LotteryResult> GenerateWinnersAsync(LotteryRequest request)
{
// 参数验证
if (request.RequiredWinners <= 0)
throw new ArgumentException("Invalid winner count");
using var transaction = await _participantRepo.BeginTransactionAsync();
try
{
// 清理历史数据
await _winnerRepo.ClearCampaignWinnersAsync(request.CampaignId);
// 获取有效参与者
var participants = await _participantRepo
.GetValidParticipantsAsync(request.CampaignId);
// 按自然人分组
var grouped = participants
.GroupBy(p => new { p.Mobile, p.IdentityNumber })
.Select(g => new ParticipantGroup(
key: g.Key,
entries: g.Sum(p => p.EntriesCount),
records: g.ToList()))
.ToList();
// 执行抽选
var selectedGroups = SelectWinners(grouped, request.RequiredWinners);
// 生成中奖记录
var winners = selectedGroups.Select(g =>
new Winner {
CampaignId = request.CampaignId,
ParticipantId = SelectRepresentativeRecord(g.Records).Id
}).ToList();
// 持久化数据
await _winnerRepo.AddRangeAsync(winners);
await transaction.CommitAsync();
return BuildResult(winners);
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
private List<ParticipantGroup> SelectWinners(
List<ParticipantGroup> groups,
int requiredCount)
{
var rnd = new Random();
var result = new List<ParticipantGroup>();
var remainingGroups = new List<ParticipantGroup>(groups);
var totalWeight = remainingGroups.Sum(g => g.TotalEntries);
while (result.Count < requiredCount && remainingGroups.Any())
{
var randomValue = rnd.Next(totalWeight);
var accumulated = 0;
foreach (var group in remainingGroups.ToList())
{
accumulated += group.TotalEntries;
if (accumulated > randomValue)
{
result.Add(group);
totalWeight -= group.TotalEntries;
remainingGroups.Remove(group);
break;
}
}
}
return result;
}
private Participant SelectRepresentativeRecord(IEnumerable<Participant> records)
{
// 选择策略示例:取最早参与的记录
return records.OrderBy(r => r.CreatedTime).First();
}
}
4. 辅助类型
// 分组结构定义
public record ParticipantGroup(
object Key, // 包含Mobile+IdentityNumber
int TotalEntries, // 总参与次数
List<Participant> Records
);
// 请求/响应模型
public class LotteryRequest
{
[Required]
public string CampaignId { get; set; }
[Range(1, 1000)]
public int RequiredWinners { get; set; }
}
public class LotteryResult
{
public IEnumerable<WinnerInfo> Winners { get; set; }
public int ActualWinnerCount { get; set; }
public string CampaignTitle { get; set; }
}
public class WinnerInfo
{
public string Name { get; set; }
public string MaskedMobile { get; set; }
public string MaskedIdNumber { get; set; }
}
使用示例
var request = new LotteryRequest
{
CampaignId = "campaign_001",
RequiredWinners = 10
};
var result = await _lotteryService.GenerateWinnersAsync(request);
Console.WriteLine($"成功生成 {result.ActualWinnerCount} 名中奖者");
foreach (var winner in result.Winners)
{
Console.WriteLine($"{winner.Name} | {winner.MaskedMobile}");
}
8245

被折叠的 条评论
为什么被折叠?



