如何给 Laravel 储存 (Storage) 加锁 flock

业务会用到Storage::put来存储本地文件,也会用到Storage::prepend来插入本地数据,还会Storage::get读取数据

请问如果是并发的情况下,如何给写入的文件加锁呢?避免并发写入和插入的时候,文件数据丢失

《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
最佳答案

我添加一个服务对象,通过Redis队列加锁:

在官方的git上面也有发过讨论:
github.com/laravel/framework/discu...

<?php
namespace App\Services\Site;

use Illuminate\Support\Facades\Storage;

/**
 * 给`Storage`本地存储通过redis队列方式加上锁,防止并发
 * @思路:
 *  - 每次请求一个服务对象时会自动注册一个token加入队列
 *  - 读写文件前会检查当前token是否是队列中第一个来决定执行还是等待执行
 * 
 * @为什么不用flock文件锁,使用fopen给资源上锁有几个问题:
 *  - 如果用模式`w+`,读写锁打开文件时之前的数据将被清空
 *  - 如果用模式`r+`,读写锁打开文件时指针指向文件头,如果写入的数据比之前少会造成文件破坏
 *  - 如果用模式`a+`,只能追加`log`,不能更新json
 *  - 如果读写锁分开执行,可能会并发造成当前读取的数据和最新写入的数据不一致,当二次写入造成回档
 * 
 * @关于锁回收:
 *  - 对象销毁后自动回收,或通过`unlock`手动回收
 *  - 如果执行一个慢请求的时候会造成后面资源堵塞,建议完成后手动回收
 *  - 锁回收后需要再次读写文件会重新加入队列
 *  - 锁还未回收手动通过`lock`后,也会重新加入队列
 *  - 通过单例请求对象方法并没用对象引用,会在方法请求完之后立即回收锁
 */
class StorageService 
{
    const NAME = 'storage:%s';    // storage:{key}

    private $_token = '';
    private $_path = '';

    private $_lock = 0;

    public function __construct(string $path, int $time = 10) 
    {
        // 前面、后面都不允许带有`.`
        $this->_path = trim($path, '.');
        $this->lock($time);
    }

    public function __destruct() 
    {
        $this->unlock();
    }

    public function lock(int $time = 10):bool 
    {
        if ('' === ($cache = $this->_cache())) 
        {
            return false;
        }

        $redis = app('Site\Cache');
        $list  = $redis::lRange($cache, 0, -1);

        if ($this->_token && in_array($this->_token, $list)) 
        {
            $this->unlock();
        }

        do 
        {
            $token = md5(uniqid(mt_rand(), true));
        }
        while(in_array($token, $list));

        $this->_token = $token;
        $this->_lock  = max(10, $time + 10);

        $redis::rPush($cache, $this->_token);
        if ($redis::ttl($cache) < $this->_lock) 
        {
            $redis::expire($cache, $this->_lock);
        }
        return true;
    }

    public function unlock():bool 
    {
        if ('' === ($cache = $this->_cache())) 
        {
            return false;
        }
        app('Site\Cache')::lRem($cache, 0, $this->_token);
        $this->_token = '';

        return true;
    }

    public function exists():bool 
    {
        return $this->_path ? Storage::disk('local')->exists($this->_path) : false;
    }

    public function getPath():?string 
    {
        return $this->_path ? storage_path('app/'.$this->_path) : null;
    }

    public function canWrite():int 
    {
        if ('' === ($cache = $this->_cache())) 
        {
            return -1;
        }

        $redis = app('Site\Cache');
        if ((!$redis::exists($cache) || !$this->_token) && !$this->lock($this->_lock)) 
        {
            // 如果缓存时间过了重新读文件需要再次队列上锁,如果上锁失败返回-1
            return -1;
        }

        return $redis::lIndex($cache, 0) === $this->_token ? 1 : 0;
    }

    /**
     * 获取本地数据
     */
    public function get():string 
    {
        return $this->_queue() ? Storage::disk('local')->get($this->_path) : '';
    }

    public function getJson():?array
    {
        return json_decode($this->get(), true);
    }

    /**
     * 写入文件
     */
    public function put(string $content):bool 
    {
        return $this->_queue() ? Storage::disk('local')->put($this->_path, $content) : false;
    }

    public function prepend(string $content):bool 
    {
        return $this->_queue() ? Storage::disk('local')->prepend($this->_path, $content) : false;
    }

    public function append(string $content):bool 
    {
        return $this->_queue() ? Storage::disk('local')->append($this->_path, $content) : false;
    }

    private function _cache():string 
    {
        if (null === ($full_path = $this->getPath())) 
        {
            return '';
        }

        return sprintf(StorageService::NAME, md5($full_path));
    }

    private function _queue():bool 
    {
        $start = time();
        do 
        {
            if (-1 === ($write = $this->canWrite())) 
            {
                return false;
            }
            $write === 0 && usleep(round(rand(0,100) * 1000));
        }
        while($write === 0 && (time() - $start <= $this->_lock));
        return $write === 1;
    }
}
3年前 评论
讨论数量: 1

我添加一个服务对象,通过Redis队列加锁:

在官方的git上面也有发过讨论:
github.com/laravel/framework/discu...

<?php
namespace App\Services\Site;

use Illuminate\Support\Facades\Storage;

/**
 * 给`Storage`本地存储通过redis队列方式加上锁,防止并发
 * @思路:
 *  - 每次请求一个服务对象时会自动注册一个token加入队列
 *  - 读写文件前会检查当前token是否是队列中第一个来决定执行还是等待执行
 * 
 * @为什么不用flock文件锁,使用fopen给资源上锁有几个问题:
 *  - 如果用模式`w+`,读写锁打开文件时之前的数据将被清空
 *  - 如果用模式`r+`,读写锁打开文件时指针指向文件头,如果写入的数据比之前少会造成文件破坏
 *  - 如果用模式`a+`,只能追加`log`,不能更新json
 *  - 如果读写锁分开执行,可能会并发造成当前读取的数据和最新写入的数据不一致,当二次写入造成回档
 * 
 * @关于锁回收:
 *  - 对象销毁后自动回收,或通过`unlock`手动回收
 *  - 如果执行一个慢请求的时候会造成后面资源堵塞,建议完成后手动回收
 *  - 锁回收后需要再次读写文件会重新加入队列
 *  - 锁还未回收手动通过`lock`后,也会重新加入队列
 *  - 通过单例请求对象方法并没用对象引用,会在方法请求完之后立即回收锁
 */
class StorageService 
{
    const NAME = 'storage:%s';    // storage:{key}

    private $_token = '';
    private $_path = '';

    private $_lock = 0;

    public function __construct(string $path, int $time = 10) 
    {
        // 前面、后面都不允许带有`.`
        $this->_path = trim($path, '.');
        $this->lock($time);
    }

    public function __destruct() 
    {
        $this->unlock();
    }

    public function lock(int $time = 10):bool 
    {
        if ('' === ($cache = $this->_cache())) 
        {
            return false;
        }

        $redis = app('Site\Cache');
        $list  = $redis::lRange($cache, 0, -1);

        if ($this->_token && in_array($this->_token, $list)) 
        {
            $this->unlock();
        }

        do 
        {
            $token = md5(uniqid(mt_rand(), true));
        }
        while(in_array($token, $list));

        $this->_token = $token;
        $this->_lock  = max(10, $time + 10);

        $redis::rPush($cache, $this->_token);
        if ($redis::ttl($cache) < $this->_lock) 
        {
            $redis::expire($cache, $this->_lock);
        }
        return true;
    }

    public function unlock():bool 
    {
        if ('' === ($cache = $this->_cache())) 
        {
            return false;
        }
        app('Site\Cache')::lRem($cache, 0, $this->_token);
        $this->_token = '';

        return true;
    }

    public function exists():bool 
    {
        return $this->_path ? Storage::disk('local')->exists($this->_path) : false;
    }

    public function getPath():?string 
    {
        return $this->_path ? storage_path('app/'.$this->_path) : null;
    }

    public function canWrite():int 
    {
        if ('' === ($cache = $this->_cache())) 
        {
            return -1;
        }

        $redis = app('Site\Cache');
        if ((!$redis::exists($cache) || !$this->_token) && !$this->lock($this->_lock)) 
        {
            // 如果缓存时间过了重新读文件需要再次队列上锁,如果上锁失败返回-1
            return -1;
        }

        return $redis::lIndex($cache, 0) === $this->_token ? 1 : 0;
    }

    /**
     * 获取本地数据
     */
    public function get():string 
    {
        return $this->_queue() ? Storage::disk('local')->get($this->_path) : '';
    }

    public function getJson():?array
    {
        return json_decode($this->get(), true);
    }

    /**
     * 写入文件
     */
    public function put(string $content):bool 
    {
        return $this->_queue() ? Storage::disk('local')->put($this->_path, $content) : false;
    }

    public function prepend(string $content):bool 
    {
        return $this->_queue() ? Storage::disk('local')->prepend($this->_path, $content) : false;
    }

    public function append(string $content):bool 
    {
        return $this->_queue() ? Storage::disk('local')->append($this->_path, $content) : false;
    }

    private function _cache():string 
    {
        if (null === ($full_path = $this->getPath())) 
        {
            return '';
        }

        return sprintf(StorageService::NAME, md5($full_path));
    }

    private function _queue():bool 
    {
        $start = time();
        do 
        {
            if (-1 === ($write = $this->canWrite())) 
            {
                return false;
            }
            $write === 0 && usleep(round(rand(0,100) * 1000));
        }
        while($write === 0 && (time() - $start <= $this->_lock));
        return $write === 1;
    }
}
3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!