常见缓存问题及解决方案

Redis线上不能使用危险的命令

1、严禁使用 Keys

2、严禁使用 Flush

3、严禁作为消息队列使用

如没有非常特殊的需求,严禁将 Redis 当作消息队列使用。Redis 当作消息队列使用,会有容量、网络、效率、功能方面的多种问题。如需要消息队列,可使用高吞吐的 Kafka 或者高可靠的 RocketMQ。

4、严禁不设置范围的批量操作

redis 那么快,慢查询除了网络延迟,就属于这些批量操作函数。大多数线上问题都是由于这些函数引起。

频繁报异常——TimeOut/无可用连接

这类错误还是挺常见的,一般来说都属于下面几种情况之一:

一、没有单例

当并发达到一定量级时候,如果没有使用单例模式实例化,会引发此类错误致服务不可用。因为在高并发情况下没有使用单例进行实例化,会导致连接池被大量创建,在业务不停止的情况下很容易发生来不及释放的情况,最终导致阻塞。

private IDatabase DB;
private static ConnectionMultiplexer conn = null;
public static object locker = new object();
public Redis(int I)
{
    if (conn == null)
    {
        lock (locker)
        {
            if (conn == null)
            {
                conn = ConnectionMultiplexer.Connect("1.1.1.1:6379");
            }
        }
    }
    DB = conn.GetDatabase(I);
}

二、耗时过长的指令(StackExchange客户端连接的只读属性TimeoutMilliseconds=1000),可以通过修改配置搞定:

var config = new ConfigurationOptions
{
    ConnectTimeout = 15000,//连接超时改成15秒
    ResponseTimeout = 15000//返回超时改成15秒
};

缓存无底洞

概念:

增加节点机器,性能没有提升反而下降了。以用户为例:user-133-age,user-133-name,user-133-height ... N个ke,当服务器增多的时候,133号用户的信息,也被更散落在更多的节点上,所以,同样是访问个人主页,得到相同的个人信息,节点越多,要连接的节点也越多,对于memecached的连接数,并没有随着节点的增多,而降低。于是出现了无底洞现象。Nosql与传统的RDBMS并不是水火不容,两者在某些设计上,是可以相互参考的,对于memcacede、redis这种kv存储,key的设计,可以参考Mysql中表/列的设计

解决方案:

  • 减少网络通信次数,redis pipeline实现1次网络请求
  • hash_tag实现。在保存用户信息的时候key键使用共同的前缀进行保存。在用分布式算法求其节点时,应该以"user-133"来计算。这样,两个关于个人信息的key(user:133:age = 23, user:133:name='lisi'),就落到同一个节点上

缓存穿透

概念:

访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。

解决方案:

  1. 采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;
  2. 访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。
<?php
/**
 * Author: sai
 * Date: 2019/5/17
 * Time: 14:10
 * 代码来自网络,有改动
 */
class BloomFilterHash
{
    /**
     * 由Justin Sobel编写的按位散列函数
     */
    public function JSHash($string, $len = null)
    {
        $hash = 1315423911;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash ^= (($hash << 5) + ord($string[$i]) + ($hash >> 2));
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 该哈希算法基于AT&T贝尔实验室的Peter J. Weinberger的工作。
     * Aho Sethi和Ulman编写的“编译器(原理,技术和工具)”一书建议使用采用此特定算法中的散列方法的散列函数。
     */
    public function PJWHash($string, $len = null)
    {
        $bitsInUnsignedInt = 4 * 8; //(unsigned int)(sizeof(unsigned int)* 8);
        $threeQuarters = ($bitsInUnsignedInt * 3) / 4;
        $oneEighth = $bitsInUnsignedInt / 8;
        $highBits = 0xFFFFFFFF << (int) ($bitsInUnsignedInt - $oneEighth);
        $hash = 0;
        $test = 0;
        $len || $len = strlen($string);
        for($i=0; $i<$len; $i++) {
            $hash = ($hash << (int) ($oneEighth)) + ord($string[$i]); } $test = $hash & $highBits; if ($test != 0) { $hash = (($hash ^ ($test >> (int)($threeQuarters))) & (~$highBits));
    }
//    var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 类似于PJW Hash功能,但针对32位处理器进行了调整。它是基于UNIX的系统上的widley使用哈希函数。
     */
    public function ELFHash($string, $len = null)
    {
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = ($hash << 4) + ord($string[$i]); $x = $hash & 0xF0000000; if ($x != 0) { $hash ^= ($x >> 24);
            }
            $hash &= ~$x;
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 这个哈希函数来自Brian Kernighan和Dennis Ritchie的书“The C Programming Language”。
     * 它是一个简单的哈希函数,使用一组奇怪的可能种子,它们都构成了31 .... 31 ... 31等模式,它似乎与DJB哈希函数非常相似。
     */
    public function BKDRHash($string, $len = null)
    {
        $seed = 131;  # 31 131 1313 13131 131313 etc..
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash * $seed) + ord($string[$i]));
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 这是在开源SDBM项目中使用的首选算法。
     * 哈希函数似乎对许多不同的数据集具有良好的总体分布。它似乎适用于数据集中元素的MSB存在高差异的情况。
     */
    public function SDBMHash($string, $len = null)
    {
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (ord($string[$i]) + ($hash << 6) + ($hash << 16) - $hash);
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 由Daniel J. Bernstein教授制作的算法,首先在usenet新闻组comp.lang.c上向世界展示。
     * 它是有史以来发布的最有效的哈希函数之一。
     */
    public function DJBHash($string, $len = null)
    {
        $hash = 5381;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash << 5) + $hash) + ord($string[$i]);
        }
        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * Donald E. Knuth在“计算机编程艺术第3卷”中提出的算法,主题是排序和搜索第6.4章。
     */
    public function DEKHash($string, $len = null)
    {
        $len || $len = strlen($string);
        $hash = $len;
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash << 5) ^ ($hash >> 27)) ^ ord($string[$i]);
        }
//        var_dump((int)($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 参考 http://www.isthe.com/chongo/tech/comp/fnv/
     */
    public function FNVHash($string, $len = null)
    {
        $prime = 16777619; //32位的prime 2^24 + 2^8 + 0x93 = 16777619
        $hash = 2166136261; //32位的offset
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) ($hash * $prime) % 0xFFFFFFFF;
            $hash ^= ord($string[$i]);
        }
//        var_dump(($hash % 0xFFFFFFFF) & 0xFFFFFFFF);die;
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }
}

/**
 * 使用redis实现的布隆过滤器
 */
abstract class BloomFilterRedis
{
    /**
     * 需要使用一个方法来定义bucket的名字
     */
    protected $bucket;

    protected $hashFunction;

    public function __construct()
    {
        if (!$this->bucket || !$this->hashFunction) {
            throw new Exception("需要定义bucket和hashFunction", 1);
        }
        $this->Hash = new BloomFilterHash;
        $this->Redis = self::getRedis(); //假设这里你已经连接好了
    }

    public static function getRedis()
    {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        $redis->auth('0rb!t');
//        var_dump($redis->info('SERVER'));die;
        $redis->select(7);
        return $redis;
    }

    /**
     * 添加到集合中
     */
    public function add($string)
    {
        foreach ($this->hashFunction as $key=>$function) {
            $hash = $this->Hash->$function($string);
            $this->Redis->setBit($this->bucket.$key, $hash, 1);
        }
        return true;
    }

    /**
     * 查询是否存在, 不存在的一定不存在, 存在有一定几率会误判(hash冲突)
     */
    public function exists($string)
    {
        $pipe = $this->Redis->multi();
        $len = strlen($string);
        foreach ($this->hashFunction as $key=>$function) {
            $hash = $this->Hash->$function($string, $len);
            $pipe = $pipe->getBit($this->bucket.$key, $hash);
        }

        $res = $pipe->exec();
//        var_dump($res);
        foreach ($res as $bit) {
            if ($bit == 0) {
                return false;
            }
        }
        return true;
    }
}

/**
 * 重复内容过滤器
 * 该布隆过滤器总位数为2^32位, 判断条数为2^30条. hash函数最优为3个.(能够容忍最多的hash函数个数)
 *
 * 注意, 在存储的数据量到2^30条时候, 误判率会急剧增加, 因此需要定时判断过滤器中的位为1的的数量是否超过50%, 超过则需要清空.
 */
class FilteRepeatedComments extends BloomFilterRedis
{
    /**
     * 表示判断重复内容的过滤器
     * @var string
     */
    protected $bucket = 'bulong';

    protected $hashFunction = ['FNVHash', 'JSHash', 'ELFHash'];
}

var_dump((new FilteRepeatedComments())->add('abc')); //true
var_dump((new FilteRepeatedComments())->add('bcd'));//true
var_dump((new FilteRepeatedComments())->add('dfg'));//true
var_dump((new FilteRepeatedComments())->exists('dfg'));//true
var_dump((new FilteRepeatedComments())->exists('dgg'));//false

缓存雪崩

概念:

大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

解决方案:

  • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  • 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
  • 给缓存加上一定区间内的随机生效时间,不同的Key设置不同的失效时间,避免同一时间集体失效,让缓存失效的时间点尽量均匀
  • 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

缓存击穿

概念:

一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

解决方案:

  1. 后台设置定时任务,主动地去更新缓存数据。这种方案容易理解,但是当Key比较分散的时候,操作起来还是比较复杂的。
  2. 分级缓存。比如设置两层缓存保护层,1级缓存失效时间短,2级缓存失效时间长。有请求过来优先从1级缓存中去查找,如果在1级缓存中没有找到相应数据,则对该线程进行加锁,这个线程再从数据库中取到数据,更新至1级和2级缓存。其他线程则直接从2级线程中获取。
  3. 提供一个拦截机制,内部维护一系列合法的Key值。当请求的Key不合法时,直接返回。
  4. 利用加锁或者队列方式避免过多请求同时对服务器进行读写操作

热点key的重建/并发竞争key问题

概念:

当前 key 是一个热点 key( 例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次 IO、多个依赖等。
解决思路

  •     减少重建缓存的次数
  •     数据尽可能一致
  •     较少的潜在危险

解决方案:

  • 分布式锁:只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,可使用 Redis 的 setnx 命令实现该功能。该方案存在等待的问题。
  • 缓存永远不过期:物理”不过期:没有设置过期时间,所以不会出现热点 key 过期后产生的问题。为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。该方案存在数据老旧问题。

Redis和数据库双写一致性问题

分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存

解决方案:

1:使用队列,使读写操作串行化。比如对id为n的数据建立一个队列。对这条数据的写操作,删除缓存后,放进一个队列;然后另一个线程过来了,发现没有缓存,则把这个读操作也放进这个队列里面。

2:采用延时双删策略。

  • 先淘汰缓存
  • 再写数据库(这两步和原来一样)
  • 休眠1秒,再次淘汰缓存

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值