├── README.md ├── RedisLock.php ├── hongbao.php ├── prize1.php ├── prize2.php ├── prize3.php └── prize4.php /README.md: -------------------------------------------------------------------------------- 1 | # prizeDraw 2 | 抽奖算法整理 3 | -- 4 | - prize1.php 抽奖算法1 5 | - prize2.php 抽奖算法2 6 | - prize3.php 抽奖算法3 7 | - prize4.php 抽奖算法4 思路,比较好 8 | - hongbao.php 生成随机红包算法 9 | - redis.php php+redis实现电商秒杀功能 任务队列 10 | 11 | -------------------------------------------------------------------------------- /RedisLock.php: -------------------------------------------------------------------------------- 1 | redisString = RedisFactory::get($param)->string; 12 | } 13 | 14 | /** 15 | * 加锁 16 | * @param [type] $name 锁的标识名 17 | * @param integer $timeout 循环获取锁的等待超时时间,在此时间内会一直尝试获取锁直到超时,为0表示失败后直接返回不等待 18 | * @param integer $expire 当前锁的最大生存时间(秒),必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放 19 | * @param integer $waitIntervalUs 获取锁失败后挂起再试的时间间隔(微秒) 20 | * @return [type] [description] 21 | */ 22 | public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) { 23 | if ($name == null) return false; 24 | 25 | //取得当前时间 26 | $now = time(); 27 | //获取锁失败时的等待超时时刻 28 | $timeoutAt = $now + $timeout; 29 | //锁的最大生存时刻 30 | $expireAt = $now + $expire; 31 | 32 | $redisKey = "Lock:{$name}"; 33 | while (true) { 34 | //将rediskey的最大生存时刻存到redis里,过了这个时刻该锁会被自动释放 35 | $result = $this->redisString->setnx($redisKey, $expireAt); 36 | 37 | if ($result != false) { 38 | //设置key的失效时间 39 | $this->redisString->expire($redisKey, $expireAt); 40 | //将锁标志放到lockedNames数组里 41 | $this->lockedNames[$name] = $expireAt; 42 | return true; 43 | } 44 | 45 | //以秒为单位,返回给定key的剩余生存时间 46 | $ttl = $this->redisString->ttl($redisKey); 47 | 48 | //ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建) 49 | //如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用 50 | //这时可以直接设置expire并把锁纳为己用 51 | if ($ttl < 0) { 52 | $this->redisString->set($redisKey, $expireAt); 53 | $this->lockedNames[$name] = $expireAt; 54 | return true; 55 | } 56 | 57 | /*****循环请求锁部分*****/ 58 | //如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出 59 | if ($timeout <= 0 || $timeoutAt < microtime(true)) break; 60 | 61 | //隔 $waitIntervalUs 后继续 请求 62 | usleep($waitIntervalUs); 63 | 64 | } 65 | 66 | return false; 67 | } 68 | /** 69 | * 解锁 70 | * @param [type] $name [description] 71 | * @return [type] [description] 72 | */ 73 | public function unlock($name) { 74 | //先判断是否存在此锁 75 | if ($this->isLocking($name)) { 76 | //删除锁 77 | if ($this->redisString->deleteKey("Lock:$name")) { 78 | //清掉lockedNames里的锁标志 79 | unset($this->lockedNames[$name]); 80 | return true; 81 | } 82 | } 83 | return false; 84 | } 85 | /** 86 | * 释放当前所有获得的锁 87 | * @return [type] [description] 88 | */ 89 | public function unlockAll() { 90 | //此标志是用来标志是否释放所有锁成功 91 | $allSuccess = true; 92 | foreach ($this->lockedNames as $name => $expireAt) { 93 | if (false === $this->unlock($name)) { 94 | $allSuccess = false; 95 | } 96 | } 97 | return $allSuccess; 98 | } 99 | /** 100 | * 给当前所增加指定生存时间,必须大于0 101 | * @param [type] $name [description] 102 | * @return [type] [description] 103 | */ 104 | public function expire($name, $expire) { 105 | //先判断是否存在该锁 106 | if ($this->isLocking($name)) { 107 | //所指定的生存时间必须大于0 108 | $expire = max($expire, 1); 109 | //增加锁生存时间 110 | if ($this->redisString->expire("Lock:$name", $expire)) { 111 | return true; 112 | } 113 | } 114 | return false; 115 | } 116 | /** 117 | * 判断当前是否拥有指定名字的所 118 | * @param [type] $name [description] 119 | * @return boolean [description] 120 | */ 121 | public function isLocking($name) { 122 | //先看lonkedName[$name]是否存在该锁标志名 123 | if (isset($this->lockedNames[$name])) { 124 | //从redis返回该锁的生存时间 125 | return (string)$this->lockedNames[$name] = (string)$this->redisString->get("Lock:$name"); 126 | } 127 | 128 | return false; 129 | } 130 | } 131 | 132 | 133 | 134 | 135 | 136 | 137 | /** 138 | * 任务队列 139 | */ 140 | class RedisQueue { 141 | 142 | private $_redis; 143 | 144 | public function __construct($param = null) { 145 | $this->_redis = RedisFactory::get($param); 146 | } 147 | 148 | /** 149 | * 入队一个 Task 150 | * @param [type] $name 队列名称 151 | * @param [type] $id 任务id(或者其数组) 152 | * @param integer $timeout 入队超时时间(秒) 153 | * @param integer $afterInterval [description] 154 | * @return [type] [description] 155 | */ 156 | public function enqueue($name, $id, $timeout = 10, $afterInterval = 0) { 157 | //合法性检测 158 | if (empty($name) || empty($id) || $timeout <= 0) 159 | return false; 160 | //加锁 161 | if (!$this->_redis->lock->lock("Queue:{$name}", $timeout)) { 162 | Logger::get('queue')->error("enqueue faild becouse of lock failure: name = $name, id = $id"); 163 | return false; 164 | } 165 | //入队时以当前时间戳作为 score 166 | $score = microtime(true) + $afterInterval; 167 | //入队 168 | foreach ((array) $id as $item) { 169 | //先判断下是否已经存在该id了 170 | if (false === $this->_redis->zset->getScore("Queue:$name", $item)) { 171 | $this->_redis->zset->add("Queue:$name", $score, $item); 172 | } 173 | } 174 | //解锁 175 | $this->_redis->lock->unlock("Queue:$name"); 176 | return true; 177 | } 178 | 179 | /** 180 | * 出队一个Task,需要指定$id 和 $score 181 | * 如果$score 与队列中的匹配则出队,否则认为该Task已被重新入队过,当前操作按失败处理 182 | * 183 | * @param [type] $name 队列名称 184 | * @param [type] $id 任务标识 185 | * @param [type] $score 任务对应score,从队列中获取任务时会返回一个score,只有$score和队列中的值匹配时Task才会被出队 186 | * @param integer $timeout 超时时间(秒) 187 | * @return [type] Task是否成功,返回false可能是redis操作失败,也有可能是$score与队列中的值不匹配(这表示该Task自从获取到本地之后被其他线程入队过) 188 | */ 189 | public function dequeue($name, $id, $score, $timeout = 10) { 190 | //合法性检测 191 | if (empty($name) || empty($id) || empty($score)) 192 | return false; 193 | //加锁 194 | if (!$this->_redis->lock->lock("Queue:$name", $timeout)) { 195 | Logger:get('queue')->error("dequeue faild becouse of lock lailure:name=$name, id = $id"); 196 | return false; 197 | } 198 | //出队 199 | //先取出redis的score 200 | $serverScore = $this->_redis->zset->getScore("Queue:$name", $id); 201 | $result = false; 202 | //先判断传进来的score和redis的score是否是一样 203 | if ($serverScore == $score) { 204 | //删掉该$id 205 | $result = (float) $this->_redis->zset->delete("Queue:$name", $id); 206 | if ($result == false) { 207 | Logger::get('queue')->error("dequeue faild because of redis delete failure: name =$name, id = $id"); 208 | } 209 | } 210 | //解锁 211 | $this->_redis->lock->unlock("Queue:$name"); 212 | return $result; 213 | } 214 | 215 | /** 216 | * 获取队列顶部若干个Task 并将其出队 217 | * @param [type] $name 队列名称 218 | * @param integer $count 数量 219 | * @param integer $timeout 超时时间 220 | * @return [type] 返回数组[0=>['id'=> , 'score'=> ], 1=>['id'=> , 'score'=> ], 2=>['id'=> , 'score'=> ]] 221 | */ 222 | public function pop($name, $count = 1, $timeout = 10) { 223 | //合法性检测 224 | if (empty($name) || $count <= 0) 225 | return []; 226 | //加锁 227 | if (!$this->_redis->lock->lock("Queue:$name")) { 228 | Logger::get('queue')->error("pop faild because of pop failure: name = $name, count = $count"); 229 | return false; 230 | } 231 | //取出若干的Task 232 | $result = []; 233 | $array = $this->_redis->zset->getByScore("Queue:$name", false, microtime(true), true, false, [0, $count]); 234 | //将其放在$result数组里 并 删除掉redis对应的id 235 | foreach ($array as $id => $score) { 236 | $result[] = ['id' => $id, 'score' => $score]; 237 | $this->_redis->zset->delete("Queue:$name", $id); 238 | } 239 | //解锁 240 | $this->_redis->lock->unlock("Queue:$name"); 241 | return $count == 1 ? (empty($result) ? false : $result[0]) : $result; 242 | } 243 | 244 | /** 245 | * 获取队列顶部的若干个Task 246 | * @param [type] $name 队列名称 247 | * @param integer $count 数量 248 | * @return [type] 返回数组[0=>['id'=> , 'score'=> ], 1=>['id'=> , 'score'=> ], 2=>['id'=> , 'score'=> ]] 249 | */ 250 | public function top($name, $count = 1) { 251 | //合法性检测 252 | if (empty($name) || $count < 1) 253 | return []; 254 | //取错若干个Task 255 | $result = []; 256 | $array = $this->_redis->zset->getByScore("Queue:$name", false, microtime(true), true, false, [0, $count]); 257 | //将Task存放在数组里 258 | foreach ($array as $id => $score) { 259 | $result[] = ['id' => $id, 'score' => $score]; 260 | } 261 | //返回数组 262 | return $count == 1 ? (empty($result) ? false : $result[0]) : $result; 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /hongbao.php: -------------------------------------------------------------------------------- 1 | 平均值,则产生小红包 49 | //当随机数<平均值,则产生大红包 50 | if (rand($bonus_min, $bonus_max) > $average) { 51 | // 在平均线上减钱 52 | $temp = $bonus_min + xRandom($bonus_min, $average); 53 | $result[$i] = $temp; 54 | $bonus_total -= $temp; 55 | } else { 56 | // 在平均线上加钱 57 | $temp = $bonus_max - xRandom($average, $bonus_max); 58 | $result[$i] = $temp; 59 | $bonus_total -= $temp; 60 | } 61 | } 62 | // 如果还有余钱,则尝试加到小红包里,如果加不进去,则尝试下一个。 63 | while ($bonus_total > 0) { 64 | for ($i = 0; $i < $bonus_count; $i++) { 65 | if ($bonus_total > 0 && $result[$i] < $bonus_max) { 66 | $result[$i]++; 67 | $bonus_total--; 68 | } 69 | } 70 | } 71 | // 如果钱是负数了,还得从已生成的小红包中抽取回来 72 | while ($bonus_total < 0) { 73 | for ($i = 0; $i < $bonus_count; $i++) { 74 | if ($bonus_total < 0 && $result[$i] > $bonus_min) { 75 | $result[$i]--; 76 | $bonus_total++; 77 | } 78 | } 79 | } 80 | return $result; 81 | } 82 | $bonus_total = 200; 83 | $bonus_count = 100; 84 | $bonus_max = 10;//此算法要求设置的最大值要大于平均值 85 | $bonus_min = 1; 86 | $result_bonus = getBonus($bonus_total, $bonus_count, $bonus_max, $bonus_min); 87 | $total_money = 0; 88 | $arr = array(); 89 | foreach ($result_bonus as $key => $value) { 90 | $total_money += $value; 91 | if(isset($arr[$value])){ 92 | $arr[$value] += 1; 93 | }else{ 94 | $arr[$value] = 1; 95 | } 96 | 97 | } 98 | //输出总钱数,查看是否与设置的总数相同 99 | echo $total_money; 100 | //输出所有随机红包值 101 | var_dump($result_bonus); 102 | //统计每个钱数的红包数量,检查是否接近正态分布 103 | ksort($arr); 104 | var_dump($arr); -------------------------------------------------------------------------------- /prize1.php: -------------------------------------------------------------------------------- 1 | $proCur) { 25 | $randNum = mt_rand(1, $proSum); 26 | if ($randNum <= $proCur) { 27 | $result = $key; 28 | break; 29 | } else { 30 | $proSum -= $proCur; 31 | } 32 | } 33 | unset ($proArr); 34 | return $result; 35 | } 36 | 37 | 38 | /* 39 | * 奖项数组 40 | * 是一个二维数组,记录了所有本次抽奖的奖项信息, 41 | * 其中id表示中奖等级,prize表示奖品,v表示中奖概率。 42 | * 注意其中的v必须为整数,你可以将对应的 奖项的v设置成0,即意味着该奖项抽中的几率是0, 43 | * 数组中v的总和(基数),基数越大越能体现概率的准确性。 44 | * 本例中v的总和为100,那么平板电脑对应的 中奖概率就是1%, 45 | * 如果v的总和是10000,那中奖概率就是万分之一了。 46 | * 47 | */ 48 | $prize_arr = array( 49 | '0' => array('id'=>1,'prize'=>'平板电脑','v'=>1), 50 | '1' => array('id'=>2,'prize'=>'数码相机','v'=>5), 51 | '2' => array('id'=>3,'prize'=>'音箱设备','v'=>10), 52 | '3' => array('id'=>4,'prize'=>'4G优盘','v'=>12), 53 | '4' => array('id'=>5,'prize'=>'10Q币','v'=>22), 54 | '5' => array('id'=>6,'prize'=>'下次没准就能中哦','v'=>50), 55 | ); 56 | 57 | /* 58 | * 每次前端页面的请求,PHP循环奖项设置数组, 59 | * 通过概率计算函数get_rand获取抽中的奖项id。 60 | * 将中奖奖品保存在数组$res['yes']中, 61 | * 而剩下的未中奖的信息保存在$res['no']中, 62 | * 最后输出json个数数据给前端页面。 63 | */ 64 | foreach ($prize_arr as $key => $val) { 65 | $arr[$val['id']] = $val['v']; 66 | } 67 | $rid = get_rand($arr); //根据概率获取奖项id 68 | 69 | $res['yes'] = $prize_arr[$rid-1]['prize']; //中奖项 70 | unset($prize_arr[$rid-1]); //将中奖项从数组中剔除,剩下未中奖项 71 | shuffle($prize_arr); //打乱数组顺序 72 | for($i=0;$i0.5,'b'=>0.2,'c'=>0.4) 17 | * @return string 返回上面数组的key 18 | */ 19 | function random($ps){ 20 | static $arr = array(); 21 | $key = md5(serialize($ps)); 22 | 23 | if (!isset($arr[$key])) { 24 | $max = array_sum($ps); 25 | foreach ($ps as $k=>$v) { 26 | $v = $v / $max * 10000; 27 | for ($i=0; $i<$v; $i++) $arr[$key][] = $k; 28 | } 29 | } 30 | return $arr[$key][mt_rand(0,count($arr[$key])-1)]; 31 | } -------------------------------------------------------------------------------- /prize3.php: -------------------------------------------------------------------------------- 1 | = self::FULL_CHANCE) { 29 | $full_chance_prize[] = $item; 30 | }else{ 31 | $nofull_chance_prize[$item['prizeID']] = $item; 32 | } 33 | } 34 | 35 | //存在满概率奖品,则随机取出一个奖品并返回 36 | $len = count($full_chance_prize); 37 | if($len > 0){ 38 | $r = mt_rand(0,$len-1); 39 | return $full_chance_prize[$r]; 40 | } 41 | 42 | //计算总概率空间O 43 | $O = count($prize) * self::FULL_CHANCE; 44 | 45 | //计算总中奖空间H并生成概率数组 46 | foreach($nofull_chance_prize as $k => $v){ 47 | $H += $v['prizeChance']; 48 | $arr_chance[$k] = $v["prizeChance"]; 49 | } 50 | 51 | $R = mt_rand(1,$O); 52 | if($R > $H){ //R不在中奖空间 53 | return false; 54 | }else{//R落在中奖空间 55 | asort($arr_chance); 56 | for($i = 0; $i < count($arr_chance) ; $i++){ 57 | $arr_delimiter[key($arr_chance)] = array_isum($arr_chance,0,$i+1); 58 | next($arr_chance); 59 | } 60 | foreach($arr_delimiter as $key => $val){ 61 | if($R <= $val) { 62 | return $nofull_chance_prize[$key]; 63 | } 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * 辅助函数array_isum,计算数组中i起n个数的和 70 | * @params $input array,要计算的数组 71 | * @params $start,int,起始位置 72 | * @params $num,int,个数 73 | * @return int 74 | */ 75 | function array_isum($input,$start,$num){ 76 | $temp = array_slice($input, $start,$num); 77 | return array_sum($temp); 78 | } 79 | -------------------------------------------------------------------------------- /prize4.php: -------------------------------------------------------------------------------- 1 | 0 limit 1 ; 38 | 同步到mc 或者 redis 39 | 40 | 查询到后对其进行更新,如果被他人抢走,则未中奖 41 | update award_pool set balance = balance – 1 where id = #{id} balance > 0 ; 42 | 43 | 同时留下抽奖情况到t_record中。 44 | 45 | 思路二 46 | 在思路一中,为了方便抽奖时判断当前是否有可中奖品,进行了初始化每件奖品的释放时间, 47 | 当奖品数量比较小的时候,情况还好,对于奖品数非常多的时候,抽奖的查询耗时会增加, 48 | 初始化奖池也是耗时的动作,是否可以不依赖这个表之间通过实时计算判断当前是否有奖品释放。 49 | 在t_award_batch表中添加两个字段,奖品总剩余量balance和上一次中奖时间last_update_time。 50 | 51 | 52 | ID 名称(name) 奖品总量(amount) 奖品余量(balance) 更新时间(last_update_time) 53 | 54 | */ 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | --------------------------------------------------------------------------------