Redis的常见问题

1,014 阅读15分钟

Redis最常用的数据类型有String类型、Hash、List、Set、SortedSet(分数控制的有序Set)。 mark

Redis常用数据类型

今天要介绍的是Redis的数据类型,顺便说说Redis的ping-pong: mark

String类型:最基本的数据类型,二进制安全,最大存储长度为1G的字符串,String可以保存任何对象,无论是JPG图片,还是序列化的对象都是可以保存的!

mark

如果要统计用户对网站的日访问量应该如何统计呢?其实很简单,如图只要把UserId+日期当成Key,并赋值为0,用户每访问一次就把key对应的值+1,这样就可以轻松统计了:

mark

string类型的数据结构:

//保存字符串对象的数据结构
struct sdshdr{
    //buf中已占用空间长度
    int len;
    
    //buf中剩余空间
    int free;
    
    //数据空间
    char buf[];
}

Hash:看看Hash数据类型,String元素组成的,适合用于存储对象

mark

List:列表,按照String元素插入顺序排序,大约可以存储40亿成员,List可用于最新消息的展示,消息越新,越会立马展示

mark

Set:String元素组成的无序集合,通过Hash表实现,不允许重复

mark

Redis提供了求交集、并集、差集等操作,就可以很方便的实现如共同关注、共同喜好等功能

SortedSet:通过分数来为集合中的成员进行从小到大的排序

mark

其实Redis还支持存储很多类型,用于计数的HyperLogLog,用于支持存储地理位置信息的Geo

Redis海量数据里查询某固定前缀的Key

首先看一段脚本,会向Redis插入2000万条数据:

for((i=1;i<=20000000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;

首先生成2千万条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中,去掉行尾的^M符号:

vim /tmp/redisTest.txt
:set fileformat=dos #设置文件的格式,通过这句话去掉每行结尾的^M符号
:wq #保存退出

通过redis提供的管道–pipe形式,去跑redis,传入文件的指令批量灌数据,需要花10分钟左右:

cat /tmp/redisTest.txt | redis-cli -h 主机ip -p 端口号 --pipe

如果使用keys指令效果是这样的:

img

使用keys对线上的业务的影响 KEYS pattern:查找所有符合给定模式pattern的key KEYS指令会一次性返回所有匹配的key,如果键的数量过大会使服务卡顿

那么应怎么做呢?

SCAN指令可以帮我们解决这个问题,命令用于迭代当前数据库中的数据库键, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS命令带来的问题,当KEYS命令被用于处理一个大的数据库时,它们可能会阻塞服务器达数秒之久。

基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。以0作为游标开始一次新的迭代,直到命令返回游标0时完成一次遍历。不保证每次执行都返回某个给定数量的元素,支持模糊查询。一次返回的数量不可控,只能是大概率符合count参数。

mark

虽然可能获取的Key是有重复的,但是只需要在应用中去重就好了。

Redis如何实现分布式锁

这个问题其实在我之前的博客中已经说到过:《基于Redis实现分布式锁》 ,现在来回顾一下,其实主要就是几个指令:SETNX key value,如果Key不存在,则创建Key、并赋值,时间复杂度为O(1),如果设置成功则会返回1,如果失败则返回0。

如果一个线程成功设置了Key,那么Key岂不是一直存在,别的线程根本不可能设置成功吗?是滴,所以要给Key加上一个过期时间,就要用到了EXPIRE key seconds这条指令了,当Key过期时(生存时间为0),会被自动删除

mark

于是乎我们可以得出伪代码如下:

int status = redisService.setnx(key, "1");
if(status == 1){
    redisService.expire(key, expire);
    //TODO...
}

但是这样就有一个问题就是,如果在执行了setnx后程序挂掉了,那么就并没有设置超时时间,就形成了死锁,所以这样的做法是不可取的,因为必须要保证sexnx和设置超时时间这两个操作是原子的,从Redis2.2.6以后,set命令可以把原来的set和expire命令合并在一起,成为一个原子操作:

SET key value[EX seconds][PX millisecond][NX][XX]

EX seconds:设置键的过期时间为seconds秒

PX millisecond:设置键的过期时间为millisecond毫秒

NX:只有在键不存在的时候,才对键进行设置操作

XX:只有在键已经存在的时候,才对键进行设置操作

SET:操作完成时才会返回OK,否则返回nil

mark

Redis 雪崩问题

目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者查不到之后去更新缓存的,定时任务刷新就有一个问题, 如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。 此时6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA 都没反应过来直接挂了。 此时,如果没什么特别的方案来处理,DBA 很着急,重启数据库,但是数据库立马又被新流量给打死了。这就是我理解的缓存雪崩。

简单来说就是在同一时间缓存大面积失效,瞬间 Redis 跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的。

那应该如何处理呢? 在批量往 Redis 存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。

setRedis(key, value, time+Math.random()*10000);

如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。

或者设置热点数据永不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就好了,不要设置过期时间),电商首页的数据也可以用这个操作,比较保险的做法。

缓存穿透和击穿

缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求。 例如我们数据库的 id 都是从1自增的,如果发起 id=-1 的数据或者 id 特别大不存在的数据,这样的不断攻击导致数据库压力很大,严重会击垮数据库。

缓存穿透的两种解决方式:

方法一:在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接 return,比如 id 做基础校验,id<=0 直接拦截;

方法二:Redis 里还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的预防缓存穿透的发生。它的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在,不存在你return 就好了,存在你就去查 DB 刷新 KV 再 return;

缓存击穿和缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB。 而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。

缓存击穿的解决方式

设置热点数据永不过期;或者加上互斥锁就搞定了,代码如下:

public static String getData(String key) throws InterruptedException {
	//从Redis查询数据
	
	String result = getDataByKV(key);
	//参数校验
	if (StringUtils.isBlank(result)) {
		try {
			//获得锁
			if (reenLock.tryLock()) {
				//去数据库查询
				result = getDataByDB(key);
				//校验
				if (StringUtils.isNotBlank(result)) {
					//插进缓存
					setDataToKV(key, result);
				}
			} else {
				//睡一会再拿
				Thread.sleep(100L);
				result = getData(key);
			}
		} finally {
			//释放锁
			reenLock.unlock();
		}
	}
	return result;
}

Redis如何做异步队列

使用list作为队列,RPUSH生产消息,LPOP消费消息。

mark

如图所示,RPUSH生产消息,LPOP消费消息,但是当消息被消费完毕的时候LPOP不会等待,而是立即返回,通常的做法是让线程Sleep一会儿,再去尝试LPOP。有没有更好的办法呢?有的:

BLPOP指令:阻塞直到队列有消息或者超时

BLPOP key [key...] timeout

img

但是这样做也有一个缺点就是,只能提供一个消费者进行消费,那么怎么解决这个问题呢?

pub/sub 主题订阅者模式:

  • 发送者(pub)发送消息,订阅者(sub)接收消息
  • 订阅者可以订阅定义数量的频道

mark

下面演示一下:

img

pub/sub 订阅者模式的缺点:消息的发布是无状态的,无法保证可达,如果要解决这种问题就必须要使用专门的消息队列中间件来解决了,如Kafka等。

Redis如何做持久化

Redis 为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。

Redis 的持久化策略有两种:

  • **RDB:**快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。
  • **AOF:**把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。Redis 默认是快照 RDB 的持久化方式。

当Redis 重启的时候,它会优先使用 AOF 文件来还原数据集,因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存储。

RDB 的工作方式: 默认 Redis 是会以快照”RDB”的形式将数据持久化到磁盘的一个二进制文件 dump.rdb。当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。 当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以 copy-on-write。RDB 非常适合灾难恢复。RDB 的缺点是:如果你需要尽量避免在服务器故障时丢失数据,那么RDB不合适你。而且RDB是做了内存数据的全量同步,数据量大的时候会由于IO而严重影响性能

AOF的工作方式:

appendfsync yes   
appendfsync always     #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec   #每秒钟同步一次,该策略为AOF的缺省策略。

AOF可以做到全程持久化,只需要在配置中开启 appendonly yes。这样 Redis 每执行一个修改数据的命令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,恢复到 Redis 关闭前的最后时刻。 使用 AOF 的优点是会让 Redis 变得非常耐久。可以设置不同的 Fsync 策略,AOF的默认策略是每秒钟 Fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 Fsync 策略,AOF 的速度可能会慢于 RDB。

两种持久化的比较:

1、如果你非常关心你的数据,但仍然可以承受数分钟内的数据丢失,那么可以只使用 RDB 持久。

2、AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低Redis的性能,不知道你是否可以接受

数据库备份和灾难恢复:定时生成 RDB 快照非常便于进行数据库备份,并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快。Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。

mark

上面的配置文件中save那一块主要是自动触发备份的条件,主动触发RDB持久化的命令:

SAVE指令: 阻塞Redis的服务器进程,直到RDB文件被创建完毕

BGSAVE指令:Fork出一-个子进程来创建RDB文件,不阻塞服务器进程

日志重写解决AOF文件大小不断增大的问题,原理如下

  • 调用fork() ,创建一个子进程
  • 子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件
  • 主进程持续将新的变动同时写到内存和原来的AOF里
  • 主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动
  • 使用新的AOF文件替换掉旧的AOF文件

RDB和AOF文件共存的情况下的恢复流程

mark

Redis的默认持久化方式:RDB和AOF混合持久化方式的流程

mark

BGSAVE做全量持久化,AOF做增量持久化。因为BGSAVE会耗费较长时间,不够实时,会导致大量丢失数据的问题,所以呢需要AOF来做增量持久化配合使用。

Pipeline以及主从同步

Pipeline和Linux的管道是类似的,还记得我们之前做的插入2000万条数据吗?

cat /tmp/redisTest.txt | redis-cli -h 主机ip -p 端口号 --pipe

Redis基于请求/响应模型,单个请求处理需要一一应答,所以如果需要批量操作数据的时候,每个数据操作都需要请求、应答的流程,那么IO负载将会变得非常高,为了提升效率,Pipeline会批量执行指令,即一次发送多条指令,节省多次IO往返时间(但是这样做的前提是批量指令之间没有依赖性)。

主从同步的原理:

mark

首先将BGSAVE的镜像文件做同步,在把期间的增量数据做同步。

全同步的过程:

1、Salve发送sync命令到Master

2、Master启动一个后台进程,将Redis中的数据快照保存到文件中

3、Master将保存数据快照期间接收到的写命令缓存起来

4、Master完成写文件操作后,将该文件发送给Salve

5、使用新的AOF文件替换掉旧的AOF文件

6、Master将这期间收集的增量写命令发送给Salve端

增量同步过程:

1、Master接收到用户的操作指令,判断是否需要传播到Slave

2、将操作记录追加到AOF文件

3、将操作传播到其他Slave : ①对齐主从库;②往响应缓存写入指令

4、将缓存中的数据发送给Slave

Redis Sentinel用来解决主从同步Master宕机后的主从切换问题: 1、监控:检查主从服务器是否运行正常

2、提醒:通过API向管理员或者其他应用程序发送故障通知

3、自动故障迁移:主从切换

流言协议Gossip

在杂乱无章中寻求一致

  • 每个节点都随机地与对方通信,最终所有节点的状态达成一致
  • 种子节点定期随机向其他节点发送节点列表以及需要传播的消息
  • 不保证信息一定会传递给所有节点,但是最终会趋于一致

在区块链的去中心化实现方式中便用到了这种协议。

Redis集群与一致性Hash

如何从海量数据里快速找到所需? 分片:按照某种规则去划分数据,分散存储在多个节点上。常规的按照哈希划分无法实现节点的动态增减。

比如userId对2模,即可把用户数据分散到两台不同的数据库服务器,但是很容易出现数据分布不均匀的问题, 而且很难实现节点的动态增减。

什么是一致性Hash呢?其实就是对2^32取模,将哈希值空间组织成虚拟的圆环:

mark使用数据Key相同的函数Hash计算出Hash值

mark

数据只需要进行Hash运算,然后顺时针找到最近的节点,就可以找到对应的服务:

mark

那么这样做的好处是什么呢?

现在我们假设Node C宕机了,那么如下图:

mark

即使Node C宕机了,那么也会找到最近的Node D节点,最大化的止损。

如果是增加服务器又会是怎样的情况呢?

mark

如果是新增服务器,只会使一小部分数据发送改变,因为还是只需要找到最近的节点存储即可

接下来说说一致性Hash的缺点,Hash环的数据倾斜问题: mark 此时,我们会引入虚拟节点来解决数据倾斜的问题 mark