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会挂掉。
解决方案:
- 采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;
- 访问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请求量大、压力骤增。
解决方案:
- 后台设置定时任务,主动地去更新缓存数据。这种方案容易理解,但是当Key比较分散的时候,操作起来还是比较复杂的。
- 分级缓存。比如设置两层缓存保护层,1级缓存失效时间短,2级缓存失效时间长。有请求过来优先从1级缓存中去查找,如果在1级缓存中没有找到相应数据,则对该线程进行加锁,这个线程再从数据库中取到数据,更新至1级和2级缓存。其他线程则直接从2级线程中获取。
- 提供一个拦截机制,内部维护一系列合法的Key值。当请求的Key不合法时,直接返回。
- 利用加锁或者队列方式避免过多请求同时对服务器进行读写操作
热点key的重建/并发竞争key问题
概念:
当前 key 是一个热点 key( 例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次 IO、多个依赖等。
解决思路
- 减少重建缓存的次数
- 数据尽可能一致
- 较少的潜在危险
解决方案:
- 分布式锁:只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,可使用 Redis 的 setnx 命令实现该功能。该方案存在等待的问题。
- 缓存永远不过期:物理”不过期:没有设置过期时间,所以不会出现热点 key 过期后产生的问题。为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。该方案存在数据老旧问题。
Redis和数据库双写一致性问题
分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
解决方案:
1:使用队列,使读写操作串行化。比如对id为n的数据建立一个队列。对这条数据的写操作,删除缓存后,放进一个队列;然后另一个线程过来了,发现没有缓存,则把这个读操作也放进这个队列里面。
2:采用延时双删策略。
- 先淘汰缓存
- 再写数据库(这两步和原来一样)
- 休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。