Back

使用类似于奖池方式的抽奖模拟

一般来说要做一个抽奖程序会立即想到用随机数. 但是通常情况下随机数也不是那么的随机, 数量比较大的时候才能体现出比较平均, 数量较小的时候中奖概率的可控性将会很差. 一不小心多被抽中几个麻烦就大了.



<?php
class T_prize{
    var $item;
    var $table;
 
/**
 * 抽奖主程序 外部调用入口
 * @param unknown_type $db 数据库链接
 * @param unknown_type $id 抽奖类型
 */
     function prize($db, $id){
         global $prize_set;
        //
         if(!isset($prize_set[$id])){
             return false;
         }
         $this->item = $prize_set[$id];
         $table = 'my_prize_store_table_' . $id;
         //抽奖并发较大时可以进行分表, 多生成几个奖池
         //$table = 'my_prize_store_table_' . $id . '_' . rand(0, 4);
         $this->table = $table;
         //防止多个用户抽到同一个条目, 进行锁表
         $db->query("lock table " . $table . " write");
         $pid = $this->get_one($db, $id);
         //奖池中没有奖品时重新初始化奖池
         if($pid == '-1'){
             $this->create_pool($db, $id);
             $pid = $this->get_one($db, $id);
         }
         //释放锁 返回抽奖结果
         $db->query("unlock table");
         return $pid;
      }
 
/**
  * 抽奖一次
  * @param unknown_type $db
  * @param unknown_type $id
  */
     function get_one($db, $id){
         $table = $this->table;
         $sql = "SELECT * FROM ${table} WHERE enable = '1' LIMIT 1";
         $row = $db->fetch($db->query($sql));
         $success = false;
         $re = array();
         if($row){
             $id = $row['id'];
             $pid = $row['pid'];
             $update_data = array(
                 'enable' => 0,
             );
             $db->update($table, $update_data, "id='$id'");
             $result = $db->getAffectedRows();
             if($result > 0){
                 $success = true;
                 $re = $row;
             }else{
                 my_log('抽奖池条目状态修改发生错误 id:' . $id);
             }
         }else{
             return '-1';
         }
         if($success){
             return $pid;
         }else{
             return 0;
         }
     }
 
/**
  * 奖品池初始化
  * @param unknown_type $db
  * @param unknown_type $id
  */
     function create_pool($db, $id){
         $table = $this->table;
         my_log('init prize pool id:' . $id);
         $item = $this->item;
         $total_num = $item['total'];
         $list = $item['list'];
         $tprize_list = array();
         foreach ($list as $key=>$one){
             $tnum = intval($total_num * $one['probability']);
             if($tnum < 1){
                 my_log('create prize pool create alert: total_num:' . $total_num . ' item_name:' . $item['name'] . ' probability:' . $item['probability']);
                 continue;
             }
             for ($i = 0; $i < $tnum; $i ++){
                 do{
                     $id = rand(1, $total_num);
                 }while(isset($tprize_list[$id]));
                 $tprize_list[$id] = $key;
             }
         }
         my_log('start');
         $db->query('TRUNCATE ' . $table);
         $insert_str_arr = array();
         for($i=1; $i<=$total_num; $i++){
             $this_pid = 0;
             $inset_data = array('pid'=>0);
              if(isset($tprize_list[$i])){
                  $this_pid = $tprize_list[$i];
              }
              //每100条记录插入一次可以大幅减小数据库插入时间时间
              $insert_str_arr[] = "($this_pid)";
              if($i % 100 == 0){
                  $insert_str = implode(',', $insert_str_arr);
                  $insert_str = 'insert into ' . $table . '(pid) values' . $insert_str;
                  $db->query($insert_str);
                  $insert_str_arr = array();
             }
         }
         my_log('end');
     }
 
  }
 
  //自定义日志
  function my_log($msg){
    //write logs here
  }

使用奖池的概念可以将中奖概率严格限制一下,  生成一定数量的格子, 按照概率算出有奖品的格子的数量, 然后再随机填充到格子里去, 执行抽奖的时候一次去打开格子. 要么中奖,要么不中, 当所有格子都被开启过之后,  再次执行初始化重新填充所有的格子. 设置一下需要使用的奖品数据和概率


<?php
$prize_set = array();
//抽奖1
$prize_set[0] = array(
   'total' => 1000,
   'list' => array(
        1 => array('name'=> '奖品1', 'probability'=> '0.01', 'product_id' => 3),
        2 => array('name'=> '奖品2', 'probability'=> '0.05', 'product_id' => 7),
        3 => array('name'=> '奖品3', 'probability'=> '0.1', 'product_id' => 9),
    ),
 );
 
 //抽奖2
 $prize_set[1] = array(
     'total' => 1000,
     'list' => array(
        1 => array('name'=> '奖品1', 'probability'=> '0.001', 'product_id' => 3),
        2 => array('name'=> '奖品2', 'probability'=> '0.005', 'product_id' => 7),
        3 => array('name'=> '奖品3', 'probability'=> '0.01', 'product_id' => 9),
        4 => array('name'=> '奖品4', 'probability'=> '0.02', 'product_id' => 1),
        5 => array('name'=> '奖品5', 'probability'=> '0.05', 'product_id' => 2),
        6 => array('name'=> '奖品6', 'probability'=> '0.1', 'product_id' => 4),
    ),
);

我有两组抽奖, 所以定义了两组.  注意, 每组的总数 乘以 这组中最小的一个概率一定要大于1,  不然这个奖品就永远也没可能抽中了.

这种方式也有缺点:

1.概率的调整只能在下次奖池初始化时才能生效,

2.使用数据库表锁来保证同一个格子只能被一个人抽中, 会导致性能降低, (我们使用的是myisam, 只能使用表级锁)无法支持大量的并发, 有个很笨的但最简单的解决方案就是对同一个抽奖使用多个奖池, 将请求分散到各个表去.

后记:表在lock 状态中实际上是不支持truncate 操作的,  先delete from table 删除所有数据, 再 alter table xxx set auto_increment=1  就可以解决这个问题…

comments powered by Disqus