高并发情况下缓存相关问题

123 阅读2分钟

应用缓存可很大程度上减小查询对数据库层的冲击。如果缓存使用不当,在高并发情况下也很有可能会出现数据库挂掉的情况。

  • 缓存穿透
    查询一个数据库一定不存在的数据,导致每次都去查询数据库,大流量时候会导致数据库挂掉 。

    缓存穿透可能会成为一种攻击手段

  • 缓存雪崩
    缓存设置了相同过期时间导致某一刻查询全部转到数据库导致数据库查询压力过大导致数据库挂掉。

  • 缓存击穿
    对于一个过期的缓存key,如果某一时刻并发量特别高时候,那么查询压力会瞬间压垮数据库。

对于以上三个缓存问题可采用如下方法解决

1、对于缓存穿透有两种方式解决:

  • 将空结果缓存(缓存时间不宜过长3,5分钟即可);
  • 采用布隆过滤器,所有可能存在的数据哈希到一个足够大的bitmap中参考

2、缓存雪崩问题在设置缓存过期时间时给一个随机的时间,避免出现相同时间的缓存。

<?php
$redis->set('key', 'hello world!', 800 + mt_rand(100, 999));

3、缓存击穿可以使用redis的set-nx锁处理

使用set nx,当给已存在的key设置值时会返回false,设置成功返回true。从2.6.12版本开始,redis为SET命令增加了一系列选项。其中包括 NX(Only set the key if it does not already exist.)。网上有一些基于SETNX 和 SETEX共同实现锁的方法是有问题的。并未考虑到SETEX 和 SETNX 设置时中间的时间差,无法并没有保证两步操作的原子性,因此我自己实现了一个可靠的redis锁操作类(单机)。代码如下:

<?php

/**
 * +----------------------------------------------------------------------
 * |Created by PhpStorm.
 * +----------------------------------------------------------------------
 * |User: gongxulei <email:790707988@qq.com>
 * +----------------------------------------------------------------------
 * |Date: 2019/11/23
 * +----------------------------------------------------------------------
 * |Time: 5:05 下午
 * +----------------------------------------------------------------------
 * */

namespace app\common\tool;

class AdvancedCacheForRedis
{
    private $handler;
    //单机redis缓存配置
    protected $options = [
        'host' => '127.0.0.1',
        'port' => 6379,
        'password' => '123456',
        'select' => 2,
        'timeout' => 10, //10s超时
        'expire' => 100000,
        'persistent' => false,
        'prefix' => '',
        'lock_timeout_times' => 10,
        'lock_expire' => 15000, //锁过期时间均为15s
    ];
   
    public function __construct()
    {
        if (!extension_loaded('redis')) {
            throw new \Exception('not support: redis');
        }
        if (!empty($options)) {
            $this->options = array_merge($this->options, $options);
        }
        $this->handler = new \Redis;
        if ($this->options['persistent']) {
            $this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']);
        } else {
            $this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']);
        }

        if ('' != $this->options['password']) {
            $this->handler->auth($this->options['password']);
        }

        if (0 != $this->options['select']) {
            $this->handler->select($this->options['select']);
        }
    }

    /**
     * 高级操作1:管道技术
     * @access public
     * @param Closure $callback 回调函数
     * @return mixed
     **/
    public function pipelining(\Closure $callback)
    {
        //开启管道模式
        $pipe = $this->handler->multi(\Redis::PIPELINE);
        $callback($pipe);
        $result = $pipe->exec();
        return $result;
    }

    /**
     * redis事务
     * @access public
     * @param Closure $callback 事务中需要执行的操作
     * @return mixed
     **/
    public function transaction(\Closure $callback)
    {
        $multi = $this->handler->multi();
        $callback($multi);
        $exeStatus = $multi->exec();
        if (empty($exeStatus)) {
            return false;
        }
        return true;
    }

    /**
     * 描述
     * @access public
     * @param mixed $field 字段描述
     * @return mixed
     **/
    public function get($name)
    {
        $value = $this->handler->get($this->getCacheKey($name));
        if (is_null($value) || false === $value) {
            return false;
        }

        try {
            $result = 0 === strpos($value, 'think_serialize:') ? unserialize(substr($value, 16)) : $value;
        } catch (\Exception $e) {
            $result = false;
        }

        return $result;
    }

    /**
     * 写入缓存
     * @access public
     * @param string $name 缓存变量名
     * @param mixed $value 存储数据
     * @param integer|\DateTime $expire 有效时间(秒)
     * @return boolean
     */
    public function set($name, $value, $expire = null)
    {
        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }
        $key = $this->getCacheKey($name);
        $value = is_scalar($value) ? $value : 'think_serialize:' . serialize($value);
        if ($expire == 0) {
            $result = $this->handler->set($key, $value);
        } else {
            $result = $this->handler->set($key, $value, ['px' => $expire]);
        }
        return $result;
    }

    /**
     * 通过加锁的方式获取
     * @access public
     * @param string $key 字段key
     * @param string $request_id 锁唯一标识:要具有唯一性
     * @param string $expire 设置的数据过期时间
     * @param \Closurere $callback 加锁成功执行的回调
     * @return mixed
     **/
    public function getWithlock($key, $request_id, $expire, \Closure $callback)
    {
        $lock_key = 'lock:' . $key; //设置锁KEY
        $lock_expire = $this->options['lock_expire'];           //设置锁的有效期为10秒
        $status = true;
        $times = 0;
        while ($status) {
            $times++;
            $result = $this->get($key);
            if ($result !== false) {
                return $result;
            }
            //创建锁
            $lock = $this->tryGetDistributedLock($lock_key, $request_id, $lock_expire);
            if ($lock === false) {
                if ($times > $this->options['lock_timeout_times']) {
                    //var_dump('加锁超时10s');
                    return [];
                }
                usleep(1000000);
                continue;
            }
            //var_dump('加锁成功');
            //执行查库操作
            $result = $callback();
            if ($result !== false) { //查到数据
                $this->set($key, $result, $expire);
                //删除锁
                $this->releaseDistributedLock($lock_key, $request_id);
            }
            return $result;
        }
    }


    /**
     * 获取分布式锁
     * @param string $lock_key 锁
     * @param string $request_id 请求标识
     * @param int $expire 过期时间
     * @return bool
     */
    private function tryGetDistributedLock($lock_key, $request_id, $expire)
    {
        $result = $this->handler->set($lock_key, $request_id, ['NX', 'PX' => $expire]);
        if (empty($result)) {
            return false;
        }
        return true;
    }

    /**
     * 释放分布式锁:使用lua脚本执行,redis中lua脚本执行具有原子性
     * @param $lock_key 锁
     * @param $request_id 请求标识
     * @return 是否释放成功
     */
    private function releaseDistributedLock($lock_key, $request_id)
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        $delStatus = $this->handler->eval($script, [$lock_key, $request_id], 1);
        if ($delStatus) {
            return true;
        }
        return false;
    }

    /**
     * 获取实际的缓存标识
     * @access public
     * @param string $name 缓存名
     * @return string
     */
    protected function getCacheKey($name)
    {
        return $this->options['prefix'] . $name;
    }

}

以上是我实现的redis单机分布式锁。这是分布式锁的基础。