<?php
namespace common\libs\lock;

use Yii;
use yii\base\BaseObject;
use yii\base\StaticInstanceTrait;

/**
 * Redis分布锁
 * 使用锁:
 * $key = 'lock';
 * if (!$lock->lock($key)) {
 * 	throw new Exception('error');
 * }
 */
class RedisLock extends BaseObject {
    use StaticInstanceTrait;
    /**
     * 前缀
     */
    const KEY_PREFIX = 'BonusRedisLock:';
    /**
     * 重试次数,0:无限重试
     * @var int
     */
    public $retryCount = 0;
    /**
     * 获取锁失败重试等待的时间,微妙 ,1ms=1000us
     * @var int
     */
    public $retryInterval = 100000;
    /**
     * 获取锁失败,是否重试
     * @var bool
     */
    public $retry = true;
    /**
     * 锁的超时时间,防止死锁发生,应该是业务的最大处理时间
     * @var int
     */
    public $expire = 5;
    /**
     * Redis 对象
     * @var null
     */
    public $redis = null;
    /**
     * @inheritdoc
     */
    public function init() {
        parent::init();
    }

    /**
     * 获取redis对象
     * @return mixed|null
     */
    public function getRedis(){
        if(!is_null($this->redis)){
            return $this->redis;
        }
        $this->redis = Yii::$app->redis;
        return $this->redis;
    }

    /**
     * @return float
     */
    public static function microTime() {
        // 获取当前毫秒时间戳
        list ($s1, $s2) = explode(' ', microtime());
        $currentTime = (float) sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000);
        return $currentTime;
    }

    /**
     * @param $key
     * @return string
     */
    public static function getKey($key){
        return self::KEY_PREFIX . $key;
    }

    /**
     * 加锁
     * @param $key
     * @return bool
     */
    public function lock($key) {
        $count = 0;
        $key = self::getKey($key);
        while (true) {
            $nowTime = self::microtime();
            $lockValue = self::microtime() + $this->expire;
            $lock = $this->getRedis()->setnx($key, $lockValue);
            if (!empty($lock) || ($this->getRedis()->get($key) < $nowTime && $this->getRedis()->getset($key, $lockValue) < $nowTime )) {
                $this->getRedis()->expire($key, $this->expire);
                return true;
            } elseif ($this->retry && (($this->retryCount > 0 && ++$count < $this->retryCount) || ($this->retryCount == 0))) {
                usleep($this->retryInterval);
            } else {
                break;
            }
        }
        return false;
    }

    /**
     * 解锁
     * @param $key
     * @return bool
     */
    public function unlock($key) {
        $key = self::getKey($key);
        if ($this->getRedis()->ttl($key)) {
            return $this->getRedis()->del($key);
        }
        return true;
    }
}