PHP利用Redis的CAS乐观锁解决高并发请求

来源

https://killuachen.com/view/44

使用方法

require 'iLock.php';

$source_id = '这里传入资源唯一编号,比如兑换奖品编号';
$iLock = new iLock($source_id);
if ($iLock->lock()) {
    //iLock处于锁定状态
    header('http/1.1 403');
    exit;
}

$redis = new Redis();
if (!$redis->connect( '127.0.0.1', 6379)) {
    throw new Exception('Redis数据库连接失败');
}

$quantity = $redis->get('quantity');
if ($quantity > 0) {
    //按照我们的代码逻辑,只要挤进来都可以抢到库存
    $quantity = $redis->decr('quantity');
    if ($quantity < 0) {
        //如果小于了0,表示我们代码存在问题
        header('http/1.1 500');
    }
    //正常情况,返回200
    echo "剩余库存:$quantity";
} else {
    //挤进来发现库存不足
    header('http/1.1 404');
}

预览图

测试结果

1万个请求,2千并发下,只有10个请求成功拿到库存,其余用户均争夺失败

并且500错误的请求不存在,最后数据库的库存也正常的为0

所以我们的代码并无问题,可放心使用

iLock源代码

/**
 * 添加锁定
 *
 * @author KilluaChen
 *
 * 用法:
 * $key = array(x=>x);
 * $key = '123123123';
 * $key mixed
 *
 * $objLock = new iLock($key);
 * $objLock->lock(); //true表示被锁定 false表示未被锁定
 *
 * //设定过期时间30秒,意味着对于这个key锁定30秒,除非手动解锁,否则将持续锁定30秒
 * $objLock = new iLock($key);
 * $objLock->setExpire(30);
 * $objLock->lock();
 *
 *
 */
class iLock
{

    private $_cache = null;

    private $_key = '';

    private $_expire = 30;

    private $_isInstance = false;

    private $_isLocked = false;

    private $_autoUnLock = true;

    public function __construct($key)
    {
        if (empty($key)) {
            throw new Exception('new iLock($key) $key is not empty');
        }
        // 由于乐观锁只能用于单实例操作,所以从ilock自身去连接redis单实例数据库
        $redis = new Redis();
        $host = '127.0.0.1';
        $port = 6379;

        if (!$redis->connect($host, $port)) {
            throw new Exception('Redis数据库连接失败');
        }

        if (defined('REDIS_PASSWORD') && REDIS_PASSWORD != '') {
            if (!$redis->auth(REDIS_PASSWORD)) {
                throw new Exception('redis connect auth error,please check password');
            }
        }
        $redis->select(2); // 指定库
        $this->_cache = $redis;
        $this->_key = cacheKey(__FILE__, $key);
        $this->_expire = ini_get('max_execution_time') > 0 ? ini_get('max_execution_time') : 30;
    }

    /**
     * 设定缓存的强制锁定时间,必须锁定足够时间才能解锁
     * @param int|number $sec
     * @throws Exception
     */
    public function setExpire($sec = 5)
    {
        $this->_autoUnLock = false;
        $this->_expire = $sec;
        if ($sec > 86400)
            throw new Exception('$sec is too large,more than 1 day');
    }

    /**
     * 采用CAS乐观锁
     *
     * @throws Exception
     * @return boolean
     */
    public function lock()
    {
        if ($this->_isInstance) {
            throw new Exception('每个iLock实例只能lock一次,当一个请求中需要多次锁定时,请分别实例化iLock类');
        }

        try {
            $this->_isLocked = true;
            $this->_isInstance = true;

            if ($this->_cache->exists($this->_key)) {
                return $this->_isLocked;
            }

            $this->_cache->watch($this->_key);
            $val = $this->_cache->get($this->_key);
            if ($val !== false) {
                return $this->_isLocked;
            }

            $rst = $this->_cache->multi()
                ->incr($this->_key)
                ->expireAt($this->_key, time() + $this->_expire)
                ->exec();

            if ($rst !== false) {
                $this->_isLocked = false;
            }
            return $this->_isLocked;
        } catch (Exception $e) {
            return true;
        }
    }

    /**
     * 返回ttl信息
     *
     */
    public function ttl()
    {
        return $this->_cache->ttl($this->_key);
    }

    /**
     * 释放锁,在特定情况下,手动释放
     * @return bool
     * @throws Exception
     */
    public function release()
    {
        if (!$this->_cache instanceof redis) {
            throw new Exception('$this->_cache is not a redis instance');
        }
        $this->_cache->delete($this->_key);
        return true;
    }

    /**
     * 自动释放锁,注意点:只有加锁的人自己才可以解锁
     */
    public function __destruct()
    {
        if (!$this->_isLocked && $this->_autoUnLock) {
            $this->_cache->delete($this->_key);
            $this->_cache->close();
        }
    }
}

if (!function_exists("cacheKey")) {

    function cacheKey()
    {
        $args = func_get_args();
        return md5(__FILE__ . serialize($args));
    }
}
评论数量: 0

1
点赞
65
浏览
0
评论

贡献 7