redis是一种常用的高性能缓存数据库,常用做缓存减少后台数据库的压力;其丰富的数据类型,也常用来实现各种业务场景。

Redis相关问题总结

redis的优势

  1. 速度快 单线程,内存操作,采用了非阻塞 I/O 多路复用机制,数据结构优化,RESP协议
  2. 键值对的数据结构
  3. 支持丰富数据类型,支持string,list,set,sorted set,hash
  4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
  5. 简单稳定
  6. 持久化
  7. 主从复制
  8. 高可用和分布式转移
  9. 客户端语言多

Redis为什么这么快

  1. 单线程,避免了频繁的上下文切换
  2. 内存操作
  3. 采用了非阻塞 I/O 多路复用机制
  4. 数据结构优化
  5. RESP协议

Redis的使用场景

Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别。

常用的几种场景如下:

  • 会话缓存(Session Cache)
  • 全页缓存(FPC)
  • 队列
  • 分布式锁
  • 排行榜/计数器
  • 发布/订阅

会话缓存(Session Cache)

最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。

全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。 再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。 此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

队列

Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。 如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。

排行榜/计数器

Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。

计数器,hyperLogLog

发布/订阅

最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。

Redis提供的所有特性中,我感觉这个是喜欢的人最少的一个,虽然它为用户提供如果此多功能。

Redis的数据类型及底层实现

五种基本数据类型

字符串String、列表List、字典Hash、集合Set、有序集合SortedSet

string

string 简单动态字符串SDS

hash

底层实现:压缩列表,哈希表

存放的是结构化的对象

我在做单点登录的时候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。

List

底层实现:压缩列表,链表

使用 List 的数据结构,可以做简单的消息队列的功能。rpush+blpop实现先进先出;另外还有一个就是,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

set

底层实现:hash表(不带value)

  • 全局去重的功能;因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能
  • 利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

zset

底层实现:跳表

  • Sorted Set多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。
  • 可以做排行榜应用,取 TOP N 操作。
  • Sorted Set 可以用来做延时任务。最后一个应用就是可以做范围查找。

其他一些数据结构

HyperLogLog、Geo、Pub/Sub

Redis Module

BloomFilter,RedisSearch,Redis-ML

Redis实现分布式锁

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

方案一

setnx + expire 两条指令

存在问题:

  • 两条指令结合使用,不满足事务性;
  • 如果expire未成功执行,锁只能主动释放,如果业务代码不成功,锁存在永远得不到释放的情况。

方案二

set key value [EX seconds] [NX|XX]

可以同时把setnx和expire合成一条指令来用,支持事务性

redis使用中的四个问题

redis使用中的四个问题

  1. 缓存和数据库双写一致性问题
  2. 缓存雪崩问题
  3. 缓存击穿问题
  4. 缓存的并发竞争问题

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

一致性问题是分布式常见问题,还可以再分为最终一致性强一致性

一致性问题不可避免,数据库和缓存双写,就必然会存在不一致的问题

答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。

另外,我们所做的方案从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存

回答:首先,采取正确更新策略,做两步操作:删缓存更新数据库

但是这两步操作也是有问题的;不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

  1. 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。(并发读写)

  2. 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。(宕机)

如何解决上面两步出现的问题

  1. 延时双删策略
  2. 设置缓存的过期时间

延时双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:

  1. 先删除缓存
  2. 再写数据库
  3. 休眠500毫秒(根据具体的业务时间来定)
  4. 再次删除缓存。

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。

设置缓存的过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存

结合延时双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

缓存穿透和缓存雪崩问题

这两个问题,说句实在话,一般中小型传统软件企业,很难碰到这个问题。如果有大并发的项目,流量有几百万左右。这两个问题一定要深刻考虑。

缓存穿透

缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

低频的缓存穿透是正常的,高频的缓存穿透才会影响数据库

缓存穿透解决方案

  1. 同样的请求ID的情况;
    • 把查询到的NULL值写入到缓存中,可以过滤掉同样的请求
  2. 每次都是不同的ID(最常见的攻击场景);
    • 利用布隆过滤器迅速判断请求所携带的 Key 是否合法有效,如果不合法,则直接返回;

缓存雪崩和缓存击穿都是缓存穿透的特殊形式

缓存雪崩, 即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。 缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。

缓存雪崩解决方案:

  • 平时开发: 随机失效时间;给缓存的失效时间,加上一个随机值,避免集体失效
  • redis的运维:
    • 发生雪崩事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
    • 发生雪崩事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
    • 发生雪崩事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

缓存击穿解决方案: 使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。

如何解决Redis的并发竞争Key问题

这个问题大致就是,同时有多个子系统去 Set 一个 Key。这个时候大家思考过要注意什么呢?

这时候分两种情况:不要求顺序和要求顺序

如果对这个 Key 操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。

如果对这个 Key 操作,要求顺序

增加时间戳或数据版本号,乐观锁的机制

假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。

期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。

Redis的集群方案

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个Redis内存不足时,使用Cluster进行分片存储。

Redis主从模式

Redis支持主从同步。

数据可以从主服务器向任意数量的从服务器上同步,同步使用的是发布/订阅机制。

配置主从同步步骤

  1. 当配置好slave后,slave与master建立连接,然后slave发送sync命令
  2. master都会启动一个后台进程,将 数据库快照保存到文件中(RDB),同时master主进程会开始收集新的写命令并缓存(AOF)
  3. 后台进程完成写文件后,master就发送文件给slave,slave将 文件保存到硬盘上,再加载到内存中,接着master就会把缓存的命令转发给slave,后续master将收到的写命令发送给slave。
  4. 可以是1 Master 多Slave,可以分层,Slave下可以再接Slave,可扩展成树状结构。

主从同步的应用

  1. 用于对 数据的热备份
  2. 用于读写分离

主从同步的延迟问题

根据CAP原理,网络延迟问题只能优化,不能从根本上解决;

基于局域网的master/slave机制在通常情况下已经可以满足’实时’备份的要求了。如果延迟比较大,就先确认以下几个因素:

  1. 网络延迟
  2. master负载
  3. slave负载

一般的做法是,使用多台slave来分摊读请求,再从这些slave中取一台专用的服务器,只作为备份用,不进行其他任何操作,就能相对最大限度地达到’实时’的要求了

Redis的哨兵机制

哨兵模式的作用是在Master宕机的情况下,从多个Slave中选出一个Master

哨兵作为独立的进程,通过发送命令,并等待服务器响应,来监控一主两从三台Redis是否正常运行

哨兵的两个作用:

  1. 监控;通过发送命令,让Redis服务器返回监控其运行状态,包括主和从
  2. 切换主从;当检测到master宕机,会自动将slave切换成master, 然后通过发布订阅模式通知其他从服务器,让其修改配置,切换master

我们可以设置多个哨兵,哨兵之间也可以互相监控状态,保证Redis集群高可用 – 多哨兵模式

故障切换(failover)的过程:主观下线->投票,failover操作->客观下线

  • 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线
  • 当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作
  • 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

Redis的Cluster集群模式

Cluster模式的具体工作机制:

  1. 在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
  2. 当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
  3. 为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
  4. 当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了

Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

Redis持久化和主从复制原理

持久化的两种方式:RDB + AOF

  • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的全局持久化。
  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。

RDB更适合做冷备,AOF更适合做热备

tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

RDB

优点:

  • 可以恢复到之前某一时间点的数据
  • 数据恢复的时候速度比AOF来的快。

缺点:

  • 默认五分钟甚至更久的时间才会生成一次,意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据
  • RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒

AOF

优点:

  • AOF是一秒一次去通过一个后台的线程fsync操作,最多丢一秒的数据
  • AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。
  • 适合做灾难性数据误删除的紧急恢复;AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

缺点:

  • 一样的数据,AOF文件比RDB还要大。

Redis的过期策略以及内存淘汰机制

过期策略

Redis 采用的是定期删除+懒汉式删除策略。

定期删除,Redis 默认每隔 100ms 检查,是否有过期的 Key,有过期 Key 则删除(类似小顶堆实现)

惰性删除,获取某个 Key 的时候,Redis 会检查一下,这个 Key 如果设置了过期时间,如果过期了此时就会删除。

内存淘汰机制

如果没有过期的Key被定期删除或者懒汉式删除,内存不断增长,怎么办?

在 redis.conf 中有一行配置:

# maxmemory-policy volatile-lru

六种内存淘汰策略:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。

推荐使用:allkeys-lru