manghe/抽奖算法优化说明.md
2025-04-06 18:08:04 +00:00

11 KiB
Raw Blame History

抽奖算法优化说明

优化背景

原有抽奖系统在处理大量库存和高并发抽奖场景时存在性能瓶颈和内存占用过高的问题。同时,在宝箱类型奖品和特殊奖品处理上存在一些不一致性。为了提升系统性能、优化用户体验并确保数据一致性,我们对抽奖核心算法进行了全面优化。

系统中的抽奖算法汇总

app/api/controller/Notify.php文件中,系统实现了多种不同类型的抽奖算法,用于处理不同抽奖场景:

方法名称 行号 功能描述
drawprize_notice 483 常规抽奖入口方法,处理普通订单的抽奖逻辑
ordinary_prize_notice 597 处理普通奖品的开奖逻辑
ordinary_prize_notice_box 632 处理普通奖品中的宝箱类型奖品开奖
weightedRandom 737 加权随机算法,用于按权重随机选择奖品
ordinary_prize_notice_flw 756 处理福利屋类型的开奖逻辑
special_first_notice 797 特殊FIRST赏开奖逻辑
special_prize_notice 864 特殊奖品LAST赏、最终赏、全局赏开奖逻辑
infinite_drawprize_notice 1015 无限赏抽奖入口方法
infinite_drawprize 1100 无限赏开奖主逻辑
infinite_drawprize_box 1158 无限赏宝箱开奖逻辑
infinite_shangchengshang 1252 商城赏开奖逻辑
rage 1331 擂台赏相关逻辑
ling_zhu 1372 领主赏相关逻辑
lian_ji 1429 连击赏相关逻辑
cardextractor_drawprize_notice 1484 抽卡机抽奖入口方法
cardextractor_drawprize 1542 抽卡机开奖主逻辑
draw_drawprize_notice 1793 抽奖机抽奖入口方法
draw_drawprize 1830 抽奖机开奖主逻辑
infinite_shangchengshang_notice 1952 商城赏抽奖入口方法

本次优化主要针对ordinary_prize_notice_box方法,该方法是多种抽奖类型中处理宝箱奖品的核心算法,同时增强了special_prize_notice对宝箱奖品的支持。

无限赏现有算法分析

无限赏抽奖使用了基于概率区间的随机算法,其核心实现在infinite_drawprize_box方法中:

// 计算总概率
$totalProbability = array_sum(array_column($goodslist, 'real_pro'));

// 构建概率区间
$probabilityRanges = [];
$currentRange = 0;
foreach ($goodslist as $good) {
    $rangeStart = $currentRange;
    $currentRange += $good['real_pro'];
    $probabilityRanges[] = [
        'id' => $good['id'],
        'start' => $rangeStart,
        'end' => $currentRange
    ];
}

// 生成随机数
$maxRand = (int) ($totalProbability * 100000);
$random = mt_rand(0, $maxRand) / 100000;

// 查找中奖奖品
$prize_id = null;
foreach ($probabilityRanges as $range) {
    if ($random >= $range['start'] && $random < $range['end']) {
        $prize_id = $range['id'];
        break;
    }
}

而商城赏(infinite_shangchengshang)则使用了与常规抽奖类似的数组展开方式:

#组合中奖商品
$all_goods_id = [];
foreach ($goodslist as $value) {
    $surplus_stock = $value['surplus_stock'];
    for ($i = 1; $i <= $surplus_stock; $i++) {
        $all_goods_id[] = $value['id'];
    }
}

// 直接取第一个
$prize_id = $all_goods_id[0];

无限赏算法与新加权随机算法对比

特性 无限赏现有算法 新加权随机算法 商城赏算法
实现原理 概率区间法 权重递减法 数组展开法
使用依据 使用real_pro字段作为概率 使用surplus_stock作为权重 使用surplus_stock作为权重
内存占用
计算效率 O(n) O(n) O(n*m)
库存控制 不减少实时库存 动态更新权重反映库存变化 直接减少库存
抽奖公平性 固定概率 动态权重更新,更准确反映剩余库存 简单随机抽取
适用场景 概率固定的无限抽奖 需要精确控制库存的抽奖 简单的库存消耗型抽奖

主要差异

  1. 概率来源不同

    • 无限赏算法使用预设的real_pro字段作为概率值
    • 新加权随机算法使用surplus_stock作为权重来计算概率
    • 商城赏则直接使用surplus_stock作为抽奖池
  2. 概率分布区别

    • 无限赏算法通过概率区间来保证中奖概率与设定概率一致
    • 新加权随机算法在连续抽奖时会动态调整权重,使概率分布更符合实际库存
    • 商城赏通过数组展开简单实现概率分布,内存占用较高
  3. 实现复杂度

    • 无限赏算法实现复杂度中等,需要构建概率区间
    • 新加权随机算法实现简单直观,边抽边调整
    • 商城赏算法实现最简单,但内存效率最低

建议优化方向

可以考虑将新的加权随机算法应用到无限赏中,同时保留使用real_pro作为权重的基础:

// 使用real_pro作为权重但采用新的加权随机算法
private function weightedRandomByProbability(array $items, string $probabilityField = 'real_pro')
{
    $weights = [];
    foreach ($items as $index => $item) {
        $weights[$index] = $item[$probabilityField];
    }
    
    $sum = array_sum($weights);
    $rand = mt_rand(1, (int)($sum * 100000)) / 100000;
    
    foreach ($weights as $index => $weight) {
        $rand -= $weight;
        if ($rand <= 0) {
            return $index;
        }
    }
    
    return array_key_first($weights);
}

这样可以统一抽奖算法的实现方式,降低代码复杂度,同时保持各类抽奖的业务特性。

主要改进

1. 随机抽取算法优化

旧算法

$ordinary_prize_all = [];
foreach ($ordinary_prize as $k => $v) {
    $surplus_prize = $v['surplus_stock'];
    if ($surplus_prize > 0) {
        for ($i = 1; $i <= $surplus_prize; $i++) {
            $ordinary_prize_all[] = $v;
        }
    }
}

shuffle($ordinary_prize_all);
shuffle($ordinary_prize_all);

for ($i = 0; $i < $prize_num; $i++) {
    $ordinary_prize_info = $ordinary_prize_all[$i];
    // 处理中奖逻辑...
}

新算法

// 过滤掉库存为0的奖品
$valid_prizes = array_filter($ordinary_prize, function($item) {
    return $item['surplus_stock'] > 0;
});

// 创建权重数组用于加权随机
$weights = [];
foreach ($valid_prizes as $index => $prize) {
    $weights[$index] = $prize['surplus_stock'];
}

// 开普通奖品
for ($i = 0; $i < $prize_num; $i++) {
    // 检查是否还有可抽奖品
    if (empty($weights) || array_sum($weights) <= 0) {
        break;
    }
    
    // 使用加权随机算法选择奖品
    $selected_index = $this->weightedRandom($weights);
    $ordinary_prize_info = $valid_prizes[$selected_index];
    
    // 减少权重以反映库存变化
    $weights[$selected_index]--;
    
    // 如果权重为0则从数组中移除
    if ($weights[$selected_index] <= 0) {
        unset($weights[$selected_index]);
        unset($valid_prizes[$selected_index]);
    }
    
    // 处理中奖逻辑...
}

2. 加权随机算法实现

/**
 * 加权随机算法
 * @param array $weights 权重数组
 * @return int 选中的索引
 */
private function weightedRandom(array $weights)
{
    $sum = array_sum($weights);
    $rand = mt_rand(1, $sum);
    
    foreach ($weights as $index => $weight) {
        $rand -= $weight;
        if ($rand <= 0) {
            return $index;
        }
    }
    
    return array_key_first($weights); // 防止浮点数精度问题导致无法选中
}

3. 特殊奖品宝箱处理统一

为特殊奖品如全局赏、LAST赏、最终赏添加了对宝箱类奖品的支持使其与普通奖品的宝箱处理逻辑保持一致

// 处理宝箱
if ($ordinary_prize_info['goods_type'] == 4) {
    // 查找宝箱奖品
    $goodslist_1 = GoodsList::where(['goods_id' => $goods_id])
        ->where('goods_list_id', '=', $ordinary_prize_info['id'])
        ->select()->toArray();
        
    if (!empty($goodslist_1)) {
        $box_res = $this->ordinary_prize_notice_box($goodslist_1, 1, $order_id, $user_id, $goods_id, $order_type, $num);
        $res = array_merge($res, $box_res);
    }
}

4. 事务处理优化

优化了事务处理逻辑,避免嵌套事务导致的问题,提高数据一致性:

try {
    // 核心抽奖逻辑...
    return $res;
} catch (\Exception $e) {
    // 记录错误日志
    trace('抽奖异常: ' . $e->getMessage(), 'error');
    // 抛出异常,让外层事务处理回滚
    throw $e;
}

各抽奖类型的处理流程

常规抽奖流程

  1. drawprize_notice - 入口方法,接收订单信息
  2. 依次调用:
    • ordinary_prize_notice - 处理普通奖品
    • special_first_notice - 处理FIRST赏
    • special_prize_notice - 处理其他特殊奖品

无限赏抽奖流程

  1. infinite_drawprize_notice - 入口方法
  2. 调用 infinite_drawprize 处理主逻辑
  3. 如果遇到宝箱类奖品,调用 infinite_drawprize_box

商城赏抽奖流程

  1. infinite_shangchengshang_notice - 入口方法
  2. 调用 infinite_shangchengshang 处理主逻辑

抽卡机抽奖流程

  1. cardextractor_drawprize_notice - 入口方法
  2. 调用 cardextractor_drawprize 处理主逻辑

优化效果对比

优化方向 旧算法 新算法 改进幅度
内存占用 随库存线性增长 与奖品种类成正比 大幅减少(>90%*
计算效率 O(n*m) O(n*k) 显著提升**
随机分布 基于数组洗牌 精确权重控制 更精确
数据一致性 可能不一致 事务保证 显著增强
错误处理 简单处理 完整异常机制 更健壮

*对于库存量大的商品如1000+),内存节省更为显著
**n为奖品种类数m为库存总量k为抽奖次数

关键优势

  1. 性能显著提升

    • 内存占用减少90%以上(对大库存商品)
    • 处理速度提升,尤其是在大量抽奖场景
  2. 随机公平性增强

    • 每次抽奖都是独立的随机事件
    • 动态调整概率分布,确保与当前库存比例一致
  3. 代码可维护性提高

    • 结构更清晰,责任划分更明确
    • 完整的错误处理机制
  4. 数据一致性保障

    • 完善的事务处理
    • 更健壮的异常处理

后续优化方向

  1. 缓存机制:考虑对奖品信息和库存数据进行缓存,减少数据库查询

  2. 分布式锁:在高并发场景下,可以考虑引入分布式锁确保数据一致性

  3. 抽奖日志:增强抽奖日志记录,便于后续数据分析和问题排查

  4. 性能监控:添加性能监控点,实时掌握抽奖系统的运行状态

  5. 算法统一:将新的加权随机算法应用到所有抽奖类型中,实现全系统的算法一致性

  6. 代码重构:考虑将抽奖逻辑抽象为独立的服务类,减少控制器代码的复杂度