内容

  • 实现简单的秒杀页面(显示当前秒杀活动状态)和秒杀接口,不需要考虑下订单和退货流程。
  • 秒杀接口要求

    • 时间到了才能开始秒杀
    • 不能超买:1个用户只能秒杀1次
    • 不能超卖
    • 在缓存崩溃重启的情况也不能出现超买和超卖的情况
  • 测试

    • 功能正常
    • 1个用户发起100个并发测试
    • 随机用户(userId:rand(1, 1000000000)) 请求,100个并发秒杀,最先完成秒杀1000个商品的活动
  • 数据表结构如下

    • 用户秒杀成功记录 log

      CREATE TABLE `log` (
      
      `id` int(11) NOT NULL AUTO_INCREMENT,
      
      `eventId` int(11) NOT NULL COMMENT '活动ID',
      
      `userId` int(11) NOT NULL COMMENT '用户ID',
      
      `createTime` int(11) NOT NULL COMMENT '创建时间',
      
      PRIMARY KEY (`id`),
      
      UNIQUE KEY `eventId` (`eventId`,`userId`)
      
      ) ENGINE=InnoDB AUTO_INCREMENT=4353 DEFAULT CHARSET=utf8;
  • 秒杀活动 event
CREATE TABLE `event` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `cnt` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '总量',

  `remainCnt` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '剩余数量',

  `startTime` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '开始时间',

  `createTime` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='活动';
  • 代码参考(重在理解)

    <?php
    namespace app\helper;
    
    class SecKill
    {
      protected $userId;//用户ID
      protected $eventId;//活动ID
      protected $eventLock;//活动锁
      protected $eventCntKey;//数量缓存键
      protected $userLogCacheKey;//用户日志缓存
      protected $userLock;//用户锁缓存键
      protected $cache = null;
      protected $db = null;
      protected $config = [];
      const EXPIRATION = 20;
    
      public function __construct($userId, $eventId)
      {
          $this->userId = $userId;
          $this->eventId = $eventId;
          $this->eventLock = $this->eventId . '_eventLock';
          $this->eventCntKey = $this->eventId . '_eventCnt';//缓存中数量比数据库中多1个
          $this->userLogCacheKey = $this->userId . '_log';
          $this->userLock = $this->userId . '_userLock';
          $this->config = [//可以写一个配置文件引入,在这里我就简单点写
              'cache' => [
                  'host' => '127.0.0.1',
                  'port' => '11211'
              ],
              'db' => [
                  'host' => '127.0.0.1',
                  'user' => 'root',
                  'password' => 123456,
                  'database' => 'miaosha',
              ]
          ];
          $this->kill();
      }
    
      private function kill()
      {
          try {
              $this->connectMemcached();
              //给用户加锁
              if (!$this->cache->add($this->userLock, 1, self::EXPIRATION)) {
                  return 'User failed to lock';
              }
              $this->connectMysql();
              $sqlTpl = "SELECT 
                            `id`
                            FROM  %s 
                            WHERE `userId` = '%d' 
                            AND `eventId` = '%d'";
              $sql = sprintf($sqlTpl, self::tableName(), $this->userId, $this->eventId);
              $res = $this->db->query($sql);
              //用户是否抢到过抢到过直接退出
              if ($res->fetch_assoc() > 0) {
                  $this->cache->delete($this->userLock);
                  return '不要贪心呦';
              }
              //递减1
              $cnt = $this->cache->decrement($this->eventCntKey, 1);
              if ($cnt === false) {
                  if (!$this->cache->add($this->eventLock, 1, self::EXPIRATION)) {
                      $this->cache->delete($this->userLock);
                      return '加锁失败';
                  }
                  $res = $this->db->query("SELECT * FROM `event` WHERE `id` = {$this->eventId}");
                  $event = $res->fetch_assoc();
                  if (!$event) {
                      $this->cache->delete($this->eventLock);
                      $this->cache->delete($this->userLock);
                      return '活动不存在';
                  }
                  $cnt = $event['remainCnt'];
                  //添加活动数量
                  if (!$this->cache->add($this->eventCntKey, $cnt)) {
                      $this->cache->delete($this->userLock);
                      $this->cache->delete($this->eventLock);
                      return "被人捷足先登";
                  }
                  $this->cache->delete($this->eventLock);
              }
              if ($cnt < 1) {
                  $this->cache->delete($this->userLock);
                  return "抢光了";
              }
              $time = time();
              $this->db->query("BEGIN");
              $this->db->query("UPDATE `event` SET `remainCnt` = `remainCnt` - 1 WHERE `id` = {$this->eventId}");
              $this->db->query("INSERT INTO `log` (`eventId`, `userId`, `createTime`) VALUES ({$this->eventId}, {$this->userId}, {$time})");
              $this->db->query("COMMIT");
              $this->cache->delete($this->userLock);
              return "Success";
          } catch (\Exception $ex) {
              echo "<pre>";
              var_dump($ex);
          }
      }
    
      /**
       * 连接memcached
       * @return string
       */
      protected function connectMemcached()
      {
          $this->cache = new \Memcache();
          if (!empty($this->config['cache'])) {
              $this->cache->addServer($this->config['cache']['host'], $this->config['cache']['port']);
          } else {
              return 'Configuration does not exist';
          }
      }
    
      /**
       * 连接mysql
       */
      protected function connectMysql()
      {
          $this->db = new \mysqli();
          $this->db->connect($this->config['db']['host'], $this->config['db']['user'], $this->config['db']['password'], $this->config['db']['database']);
          $this->db->set_charset('utf8');
      }
    
      protected static function tableName()
      {
          return '`log`';
      }
    }
Last modification:June 22, 2022
如果觉得我的文章对你有用,请随意赞赏