├── worker.php ├── tel.txt ├── run.php ├── README.md ├── batch.sh ├── gen.sh ├── init.php ├── batch.py └── ms.php /worker.php: -------------------------------------------------------------------------------- 1 | ['host'=>'localhost', 'port'=>'6381'], 'flag'=>'BMXCHD']); 6 | 7 | $acId = 1; 8 | $data = $ms->popQueue($acId); 9 | var_dump($data); 10 | 11 | //header("content-type:text/html;charset=utf-8"); -------------------------------------------------------------------------------- /tel.txt: -------------------------------------------------------------------------------- 1 | 13248308810 2 | 13248308811 3 | 13248308812 4 | 13248308813 5 | 13248308814 6 | 13248308815 7 | 13248308816 8 | 13248308817 9 | 13248308818 10 | 13248308819 11 | 13248308820 12 | 13248308821 13 | 13248308822 14 | 13248308823 15 | 13248308824 16 | 13248308825 17 | 13248308826 18 | 13248308827 19 | 13248308828 20 | 13248308829 -------------------------------------------------------------------------------- /run.php: -------------------------------------------------------------------------------- 1 | ['host'=>'localhost', 'port'=>'6381'], 'flag'=>'BMXCHD']); 6 | 7 | $acId = 1; 8 | 9 | $tel = isset($argv[1]) ? $argv[1] : '13248308835'; 10 | $result = $ms->run($acId, $tel); 11 | 12 | var_dump($result); 13 | //header("content-type:text/html;charset=utf-8"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # miaosha(PHP+Redis) 2 | 秒杀/抢购引擎 3 | 4 | # 说明 5 | 本项目封装了秒杀/抢购的核心逻辑,提供演示程序,具体业务逻辑需要使用者自己补充。 6 | 7 | # 依赖环境 8 | php redis扩展 9 | 10 | # 使用方法 11 | ## 1、初始化数据 12 | $ php init.php 13 | $ ./gen.sh 14 | 15 | ## 2、执行秒杀/抢购 16 | ### [单次请求] 17 | > $ php run.php 18 | 19 | ### [模拟并发请求] 20 | > $ ./batch.sh 21 | 22 | ## 3、执行业务逻辑 23 | > $ php worker.php 24 | >> `(这里是从redis队列取出数据,业务逻辑需要自己补充)` 25 | -------------------------------------------------------------------------------- /batch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function robit { 3 | php run.php $1 4 | } 5 | tmp_fifofile="/tmp/$.fifo" # 脚本运行的当前进程ID号作为文件名 6 | mkfifo $tmp_fifofile # 新建一个随机fifo管道文件 7 | exec 6<>$tmp_fifofile # 定义文件描述符6指向这个fifo管道文件 8 | rm $tmp_fifofile 9 | thread=`cat tel.txt | wc -l` 10 | for ((i=0;i<$thread;i++));do # for循环 往 fifo管道文件中写入N个空行 11 | echo 12 | done >&6 13 | while read input 14 | do 15 | read -u6 # 从文件描述符6中读取行(实际指向fifo管道) 16 | { 17 | robit ${input}; # 执行pinghost函数 18 | echo >&6 # 再次往fifo管道文件中写入一个空行。 19 | }& # 放到后台执行 20 | done&- #删除文件描述符6 23 | exit 0 -------------------------------------------------------------------------------- /gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function isint () { 3 | if [ $# -lt 1 ]; then 4 | return 0 5 | fi 6 | 7 | if [[ $1 =~ ^-?[1-9][0-9]*$ ]]; then 8 | return 1 9 | fi 10 | 11 | if [[ $1 =~ ^0$ ]]; then 12 | return 1 13 | fi 14 | 15 | return 0 16 | } 17 | 18 | cnt=$1 19 | isint $cnt 20 | if [ $? = 0 ]; then 21 | cnt=0 22 | else 23 | if [ -f "tel.txt" ]; then 24 | rm tel.txt 25 | fi 26 | touch tel.txt 27 | fi 28 | 29 | prefix="186" 30 | for ((i=0;i<$cnt;i++)); 31 | do 32 | patch="" 33 | len=`expr length "$i"` 34 | counter=$(( 8-$len )) 35 | while [ $counter -gt 0 ] 36 | do 37 | patch=${patch}"0" 38 | counter=$(( $counter - 1 )) 39 | done 40 | echo ${prefix}${patch}${i}>>tel.txt 41 | done 42 | exit 0 -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | ['host'=>'localhost', 'port'=>'6381'], 'flag'=>'BMXCHD']); 6 | 7 | $acId = 1; 8 | //$ms->delActivity($acId); 9 | $ms->newActivity($acId, [ 10 | 'name' => '活动1', 11 | 'title' => '秒杀优惠券', 12 | 'start_time' => '1456308000', 13 | 'end_time' => '1456311600', 14 | 'total' => 1000, 15 | 'stock' => 10 16 | ]); 17 | $ms->setStock($acId, 10); 18 | $ms->setRewards($acId,[ 19 | ['code'=>'123456', 'money'=>'100', 'valid_days'=>30], 20 | ['code'=>'123457', 'money'=>'100', 'valid_days'=>30], 21 | ['code'=>'123458', 'money'=>'100', 'valid_days'=>30], 22 | ['code'=>'123459', 'money'=>'100', 'valid_days'=>30], 23 | ['code'=>'123450', 'money'=>'100', 'valid_days'=>30], 24 | ['code'=>'123451', 'money'=>'100', 'valid_days'=>30], 25 | ['code'=>'123452', 'money'=>'100', 'valid_days'=>30], 26 | ['code'=>'123453', 'money'=>'100', 'valid_days'=>30], 27 | ['code'=>'123454', 'money'=>'100', 'valid_days'=>30], 28 | ['code'=>'123455', 'money'=>'100', 'valid_days'=>30] 29 | ]); 30 | 31 | var_dump($ms->getActivity($acId)); 32 | var_dump($ms->getRewards($acId)); 33 | var_dump($ms->getReward($acId, 9)); 34 | 35 | //header("content-type:text/html;charset=utf-8"); -------------------------------------------------------------------------------- /batch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding=utf-8 3 | # Filename: batch.py 4 | 5 | import threading 6 | import datetime 7 | import time 8 | import os 9 | 10 | class ThreadImpl(threading.Thread): 11 | def __init__(self, cmd): 12 | threading.Thread.__init__(self) 13 | self._cmd = cmd 14 | 15 | def execCmd(self, cmd): 16 | try: 17 | print "命令%s开始运行%s\r\n" % (cmd,datetime.datetime.now()) 18 | os.system(cmd) 19 | print "命令%s结束运行%s\r\n" % (cmd,datetime.datetime.now()) 20 | except Exception, e: 21 | print '%s\t 运行失败,失败原因\r\n%s\r\n' % (cmd,e) 22 | 23 | def run(self): 24 | global total, mutex 25 | 26 | # 打印线程名 27 | print threading.currentThread().getName() 28 | self.execCmd(self._cmd) 29 | 30 | mutex.acquire() 31 | total = total + 1 32 | mutex.release() 33 | 34 | if __name__ == '__main__': 35 | #定义全局变量 36 | global total, mutex 37 | total = 0 38 | # 创建锁 39 | mutex = threading.Lock() 40 | 41 | print "程序开始运行%s\r\n" % datetime.datetime.now() 42 | 43 | #定义线程池 44 | threads = [] 45 | 46 | # 创建线程对象 47 | fh = open("tel.txt") 48 | #for x in xrange(0, num): 49 | for line in fh.readlines(): 50 | cmd = ' '.join(['php run.php', line]) 51 | threads.append(ThreadImpl(cmd)) 52 | # 启动线程 53 | for t in threads: 54 | t.start() 55 | # 等待子线程结束 56 | for t in threads: 57 | t.join() 58 | 59 | # 打印执行结果 60 | print total 61 | print "程序结束运行%s" % datetime.datetime.now() 62 | -------------------------------------------------------------------------------- /ms.php: -------------------------------------------------------------------------------- 1 | redisHost = $config['redis']['host']; 31 | } 32 | if(!empty($config['redis']['port'])) 33 | { 34 | $this->redisPort = $config['redis']['port']; 35 | } 36 | if(!empty($config['redis']['password'])) 37 | { 38 | $this->redisAuth = $config['redis']['password']; 39 | } 40 | if(!empty($config['redis']['db'])) 41 | { 42 | $this->redisDB = $config['redis']['db']; 43 | } 44 | } 45 | 46 | if(!empty($config['flag'])) 47 | { 48 | $this->hdsKey = $config['flag']; 49 | } 50 | } 51 | 52 | /** 53 | * 获取单例 54 | * @Author WirrorYin 55 | * @DateTime 2016-02-25T13:31:13+0800 56 | * @return [type] [description] 57 | */ 58 | public static function instance() 59 | { 60 | $clz = __CLASS__; 61 | if(self::$inst === null){ 62 | self::$inst = new $clz(); 63 | } 64 | return self::$inst; 65 | } 66 | 67 | /** 68 | * 获取redis实例 69 | * @Author WirrorYin 70 | * @DateTime 2016-02-25T13:31:32+0800 71 | * @return [type] [description] 72 | */ 73 | public function getRedis() 74 | { 75 | if(null === self::$redis){ 76 | $redis = new \Redis; 77 | $conn = $redis->pconnect($this->redisHost, $this->redisPort); 78 | if($conn){ 79 | $redis->auth($this->redisAuth); 80 | $redis->select($this->redisDB); 81 | self::$redis = $redis; 82 | }else{ 83 | throw new \Exception('Redis Lost'); 84 | } 85 | } 86 | 87 | return self::$redis; 88 | } 89 | 90 | /** 91 | * 新活动 92 | * @Author WirrorYin 93 | * @DateTime 2016-02-25T13:31:44+0800 94 | * @param [type] $activityId [description] 95 | * @param [type] $data [description] 96 | * @return [type] [description] 97 | */ 98 | public function newActivity($activityId, $data) 99 | { 100 | $redis = $this->getRedis(); 101 | $redis->hSet($this->hdsKey, $activityId, json_encode($data)); 102 | } 103 | 104 | /** 105 | * 删除活动 106 | * @Author WirrorYin 107 | * @DateTime 2016-02-25T15:37:01+0800 108 | * @param [type] $activityId [description] 109 | * @return [type] [description] 110 | */ 111 | public function delActivity($activityId) 112 | { 113 | $redis = $this->getRedis(); 114 | $redis->hDel($this->hdsKey, $activityId); 115 | 116 | $stockKey = sprintf(self::HD_STOCK_KEY, $this->hdsKey, $activityId); 117 | $redis->del($stockKey); 118 | 119 | $rewardKey = sprintf(self::HD_REWORD_KEY, $this->hdsKey, $activityId); 120 | $redis->del($rewardKey); 121 | 122 | $luckKey = sprintf(self::HD_LUCKY_KEY, $this->hdsKey, $activityId); 123 | $redis->del($luckKey); 124 | 125 | $queueKey = sprintf(self::HD_QUEUE_KEY, $this->hdsKey, $activityId); 126 | $redis->del($queueKey); 127 | } 128 | 129 | /** 130 | * 删除所有活动 131 | * @Author WirrorYin 132 | * @DateTime 2016-02-25T15:41:45+0800 133 | * @return [type] [description] 134 | */ 135 | public function unsetAll() 136 | { 137 | $redis = $this->getRedis(); 138 | $activities = $redis->hGetAll($this->hdsKey); 139 | if($activities) 140 | { 141 | foreach ($activities as $key => $act) { 142 | $this->delActivity($key); 143 | } 144 | 145 | $redis->del($this->hdsKey); 146 | } 147 | } 148 | 149 | /** 150 | * 获取活动信息 151 | * @Author WirrorYin 152 | * @DateTime 2016-02-25T13:47:01+0800 153 | * @param [type] $activityId [description] 154 | * @return [type] [description] 155 | */ 156 | public function getActivity($activityId) 157 | { 158 | $redis = $this->getRedis(); 159 | $data = $redis->hGet($this->hdsKey, $activityId); 160 | 161 | return $data ? json_decode($data, true) : null; 162 | } 163 | 164 | /** 165 | * 设置活动库存 166 | * @Author WirrorYin 167 | * @DateTime 2016-02-25T13:37:26+0800 168 | */ 169 | public function setStock($activityId, $stock) 170 | { 171 | $redis = $this->getRedis(); 172 | $stockKey = sprintf(self::HD_STOCK_KEY, $this->hdsKey, $activityId); 173 | 174 | $redis->set($stockKey, $stock); 175 | } 176 | 177 | /** 178 | * 获取活动库存 179 | * @Author WirrorYin 180 | * @DateTime 2016-02-25T13:44:07+0800 181 | * @param [type] $activityId [description] 182 | * @return [type] [description] 183 | */ 184 | public function getStock($activityId) 185 | { 186 | $redis = $this->getRedis(); 187 | $stockKey = sprintf(self::HD_STOCK_KEY, $this->hdsKey, $activityId); 188 | 189 | return $redis->get($stockKey); 190 | } 191 | 192 | /** 193 | * 活动库存递增 194 | * @Author WirrorYin 195 | * @DateTime 2016-02-25T13:39:29+0800 196 | * @param [type] $activityId [description] 197 | * @return [type] [description] 198 | */ 199 | public function incrStock($activityId, $val=1) 200 | { 201 | $redis = $this->getRedis(); 202 | $stockKey = sprintf(self::HD_STOCK_KEY, $this->hdsKey, $activityId); 203 | 204 | $redis->incrBy($stockKey, $val); 205 | } 206 | 207 | /** 208 | * 活动库存递减 209 | * @Author WirrorYin 210 | * @DateTime 2016-02-25T13:39:29+0800 211 | * @param [type] $activityId [description] 212 | * @return [type] [description] 213 | */ 214 | public function decrStock($activityId, $val=1) 215 | { 216 | $redis = $this->getRedis(); 217 | $stockKey = sprintf(self::HD_STOCK_KEY, $this->hdsKey, $activityId); 218 | 219 | $redis->decrBy($stockKey, $val); 220 | } 221 | 222 | /** 223 | * 设置活动奖励 224 | * @Author WirrorYin 225 | * @DateTime 2016-02-25T13:31:54+0800 226 | * @param [type] $activityId [description] 227 | * @param [type] $rewards [description] 228 | */ 229 | public function setRewards($activityId, $rewards) 230 | { 231 | $redis = $this->getRedis(); 232 | $rewardsKey = sprintf(self::HD_REWORD_KEY, $this->hdsKey, $activityId); 233 | foreach ($rewards as $k => $reward) { 234 | $redis->hSet($rewardsKey, "rwd_".$k, json_encode($reward)); 235 | } 236 | } 237 | 238 | /** 239 | * 获取活动奖励列表 240 | * @Author WirrorYin 241 | * @DateTime 2016-02-25T15:09:11+0800 242 | * @param [type] $activityId [description] 243 | * @return [type] [description] 244 | */ 245 | public function getRewards($activityId) 246 | { 247 | $redis = $this->getRedis(); 248 | $rewardsKey = sprintf(self::HD_REWORD_KEY, $this->hdsKey, $activityId); 249 | 250 | return $redis->hGetAll($rewardsKey); 251 | } 252 | 253 | /** 254 | * 获取活动奖励 255 | * @Author WirrorYin 256 | * @DateTime 2016-02-25T13:49:35+0800 257 | * @param [type] $activityId [description] 258 | * @param [type] $rwdKey [description] 259 | * @return [type] [description] 260 | */ 261 | public function getReward($activityId, $key) 262 | { 263 | $redis = $this->getRedis(); 264 | $rewardsKey = sprintf(self::HD_REWORD_KEY, $this->hdsKey, $activityId); 265 | $data = $redis->hGet($rewardsKey, "rwd_".$key); 266 | 267 | return $data ? json_decode($data, true) : null; 268 | } 269 | 270 | /** 271 | * 数据入队列 272 | * @Author WirrorYin 273 | * @DateTime 2016-02-25T14:23:47+0800 274 | * @param [type] $activityId [description] 275 | * @return [type] [description] 276 | */ 277 | public function pushQueue($activityId, $data) 278 | { 279 | $redis = $this->getRedis(); 280 | $queueKey = sprintf(self::HD_QUEUE_KEY, $this->hdsKey, $activityId); 281 | 282 | $redis->lPush($queueKey, json_encode($data)); 283 | } 284 | 285 | /** 286 | * 数据出队列 287 | * @Author WirrorYin 288 | * @DateTime 2016-02-25T14:24:08+0800 289 | * @param [type] $activityId [description] 290 | * @return [type] [description] 291 | */ 292 | public function popQueue($activityId) 293 | { 294 | $redis = $this->getRedis(); 295 | $queueKey = sprintf(self::HD_QUEUE_KEY, $this->hdsKey, $activityId); 296 | 297 | $data = $redis->rPop($queueKey); 298 | 299 | return $data ? json_decode($data, true) : null; 300 | } 301 | 302 | /** 303 | * 执行秒杀 304 | * @Author WirrorYin 305 | * @DateTime 2016-02-25T13:51:40+0800 306 | * @param [type] $activityId [description] 307 | * @return [type] [description] 308 | */ 309 | public function run($activityId, $identification) 310 | { 311 | $redis = $this->getRedis(); 312 | $stockKey = sprintf(self::HD_STOCK_KEY, $this->hdsKey, $activityId); 313 | 314 | $now = time(); 315 | $stock = $redis->get($stockKey); 316 | $acitivty = $this->getActivity($activityId); 317 | if($acitivty) 318 | { 319 | if(!empty($acitivty['start_time']) && $now < $acitivty['start_time']) 320 | { 321 | return -4;//活动未开始 322 | } 323 | 324 | if(!empty($acitivty['end_time']) && $now > $acitivty['end_time']) 325 | { 326 | return -5;//活动已结束 327 | } 328 | 329 | if($stock > 0) 330 | { 331 | $luckKey= sprintf(self::HD_LUCKY_KEY, $this->hdsKey, $activityId); 332 | if($redis->hExists($luckKey, $identification)) 333 | { 334 | return -1;//已领过 335 | } 336 | 337 | //redis事务 338 | $redis->watch($stockKey); 339 | $redis->multi(); 340 | $redis->decr($stockKey); 341 | $result = $redis->exec(); 342 | if($result) 343 | { 344 | $stock = $result[0]; 345 | $idx = $stock;//$redis->hLen($luckKey); 346 | $rewardData = ['id' => $identification, 'time' => $now]; 347 | $reward = $this->getReward($activityId, $idx); 348 | if($reward) 349 | { 350 | $rewardData['reward'] = $reward; 351 | 352 | //保存中奖信息 353 | $redis->hSet($luckKey, $identification, $rewardData); 354 | $this->pushQueue($activityId, $rewardData); 355 | return ['stock'=>$stock, 'data'=>$rewardData]; 356 | } 357 | 358 | return -6;//分配奖励失败, 理论上不应该发生 359 | } 360 | else 361 | { 362 | return -2;//领取失败 363 | } 364 | } 365 | else 366 | { 367 | return -3;//已领完 368 | } 369 | } 370 | 371 | //活动不存在 372 | return 0; 373 | } 374 | } 375 | 376 | //header("content-type:text/html;charset=utf-8"); --------------------------------------------------------------------------------