V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
frozenway
V2EX  ›  PHP

PHP 做数据缓存时遇到一个不停写入缓存的问题,该怎么解决?

  •  
  •   frozenway · 2021-01-19 17:02:50 +08:00 · 2335 次点击
    这是一个创建于 1402 天前的主题,其中的信息可能已经有所发展或是发生改变。
    //获取缓存
            $tmp = unserialize(file_get_contents('tmp.txt'));
            //得到以下缓存数组
            $tmp = [
                'a' => 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq',
                'b' => 'wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww',
                'c' => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
                'd' => 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr',
                'e' => 'tttttttttttttttttttttttttttttttttttttttttt',
                'f' => 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
                'g' => 'uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu',
                'h' => 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii',
                ...
            ];
            //更新缓存
            $tmp['b'] = 'ooooooooooooooooooooooooooooooooooooooooo';
            file_put_contents('tmp.txt', serialize($tmp));
            //那么问题来了,多个线程在读取这个缓存的时候
            //当线程 A 在更新缓存时,调用以下方法
            $tmp['a'] = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
            file_put_contents('tmp.txt', serialize($tmp));
            //而在线程 A 更新缓存的同时,线程 B 在读取 tmp 这个缓存
            $tmp = unserialize(file_get_contents('tmp.txt'));
            //假设 A 线程执行更新缓存时,文件内容刚写入一部分,那么线程 B 读取出来的
            //缓存数据是
            $tmp = false;
            //那么线程 B 就会查数据库后得到数据后写入缓存
            $tmp['b'] = 'ssssssssssssssssssssssssssssssssssssssss';
            file_put_contents('tmp.txt', serialize($tmp));
            //然后最终的问题来了
            //如果设置这个缓存数据有效期 1 小时,
            //当几十上百个线程频繁访问这个缓存数据,如果不存在其中的 a 或 b 或其他键的值,就
            $tmp['x'] = '......................................'; //x 为 abcdefg..的之中一个键
            file_put_contents('tmp.txt', serialize($tmp));
            //如果更新其中一个键的值时,刚写入一部分到 tmp.txt,那么其他线程读取
            //缓存时得到的值是 false,那么又各自更新写入自己的缓存
            //这么一来,只要有一个进程更新写入到 tmp.txt 文件而还没全部写完时,另一线程就
            //读取,就会造成清空了所有缓存的情况,最后造成这个 tmp 缓存被不停清空又不停写入
            //这就缓存的使用目的了
            //这种情况该怎么解决
    
    13 条回复    2021-03-18 11:35:55 +08:00
    chaodada
        1
    chaodada  
       2021-01-19 17:06:51 +08:00   ❤️ 1
    加锁啊
    Makoto
        2
    Makoto  
       2021-01-19 17:07:05 +08:00   ❤️ 1
    缓存读的时候无限制,写之前检查 /加锁,每次只允许一个线程写
    setsunakute
        3
    setsunakute  
       2021-01-19 17:08:21 +08:00   ❤️ 1
    sagaxu
        4
    sagaxu  
       2021-01-19 17:11:55 +08:00 via Android   ❤️ 1
    写临时文件再 rename 过去,rename 是原子操作
    lovecy
        5
    lovecy  
       2021-01-19 17:14:12 +08:00
    多进程条件下保持唯一性累不累啊。。
    1. 用一个定时进程去刷新缓存文件
    2. 用一个常驻进程去处理所有的缓存读取操作(对,就是让你上 Redis )
    2kCS5c0b0ITXE5k2
        6
    2kCS5c0b0ITXE5k2  
       2021-01-19 17:14:27 +08:00   ❤️ 1
    缓存雪崩 加锁把.
    frozenway
        7
    frozenway  
    OP
       2021-01-19 17:29:38 +08:00
    @Makoto @setsunakute @sagaxu @lovecy 我现在用的是 thinkphp5.1 的 cache 的 file 模式去写缓存,看了源代码,也存在这种情况,好无助
    frozenway
        8
    frozenway  
    OP
       2021-01-19 17:34:05 +08:00
    TP5.1 的
    ```
    /**
    * 读取缓存
    * @access public
    * @param string $name 缓存变量名
    * @param mixed $default 默认值
    * @return mixed
    */
    public function get($name, $default = false)
    {
    $this->readTimes++;

    $filename = $this->getCacheKey($name);

    if (!is_file($filename)) {
    return $default;
    }

    $content = file_get_contents($filename);
    $this->expire = null;

    if (false !== $content) {
    $expire = (int) substr($content, 8, 12);
    if (0 != $expire && time() > filemtime($filename) + $expire) {
    //缓存过期删除缓存文件
    $this->unlink($filename);
    return $default;
    }

    $this->expire = $expire;
    $content = substr($content, 32);

    if ($this->options['data_compress'] && function_exists('gzcompress')) {
    //启用数据压缩
    $content = gzuncompress($content);
    }
    return $this->unserialize($content);
    } else {
    return $default;
    }
    }

    /**
    * 写入缓存
    * @access public
    * @param string $name 缓存变量名
    * @param mixed $value 存储数据
    * @param int|\DateTime $expire 有效时间 0 为永久
    * @return boolean
    */
    public function set($name, $value, $expire = null)
    {
    $this->writeTimes++;

    if (is_null($expire)) {
    $expire = $this->options['expire'];
    }

    $expire = $this->getExpireTime($expire);
    $filename = $this->getCacheKey($name, true);

    if ($this->tag && !is_file($filename)) {
    $first = true;
    }

    $data = $this->serialize($value);

    if ($this->options['data_compress'] && function_exists('gzcompress')) {
    //数据压缩
    $data = gzcompress($data, 3);
    }

    $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
    $result = file_put_contents($filename, $data);

    if ($result) {
    isset($first) && $this->setTagItem($filename);
    clearstatcache();
    return true;
    } else {
    return false;
    }
    }
    ```
    也没加锁,会不会也有问题?
    zzhpeng
        9
    zzhpeng  
       2021-01-19 17:51:25 +08:00
    一下线程,一下进程,搞懵了,fpm 模式不就是多进程单线程吗?
    wangritian
        10
    wangritian  
       2021-01-19 17:57:32 +08:00   ❤️ 1
    最佳解决方案:redis
    zzhpeng
        11
    zzhpeng  
       2021-01-19 18:12:42 +08:00   ❤️ 1
    还有,为什么这样设计呢?数组序列化存储,反序列化更新。string 存储不就好了,独立开来。案例来看,你的数据独立性挺强。
    sujin190
        12
    sujin190  
       2021-01-19 18:25:05 +08:00   ❤️ 1
    一个 key 一个文件不好么。。
    yavana
        13
    yavana  
       2021-03-18 11:35:55 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1054 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 19:14 · PVG 03:14 · LAX 11:14 · JFK 14:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.