Redis常见问题总结

[toc]mysql

本文发表在 www.weypage.com/2019/09/18/…c++

一、为何使用 Redis

在项目中使用 Redis,主要考虑两个角度:性能和并发。若是只是为了分布式锁这些其余功能,还有其余中间件 Zookpeer 等代替,并不是必定要使用 Redis。程序员

性能:

以下图所示,咱们在碰到须要执行耗时特别久,且结果不频繁变更的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求可以迅速响应。redis

特别是在秒杀系统,在同一时间,几乎全部人都在点,都在下单。。。执行的是同一操做———向数据库查数据。算法

根据交互效果的不一样,响应时间没有固定标准。在理想状态下,咱们的页面跳转须要在瞬间解决,对于页内操做则须要在刹那间解决。sql

并发:

以下图所示,在大并发的状况下,全部的请求直接访问数据库,数据库会出现链接异常。这个时候,就须要使用 Redis 作一个缓冲操做,让请求先访问到 Redis,而不是直接访问数据库。数据库

使用 Redis 的常见问题

  • 缓存和数据库双写一致性问题编程

  • 缓存雪崩问题后端

  • 缓存击穿问题缓存

  • 缓存的并发竞争问题

二、单线程的 Redis 为何这么快

这个问题是对 Redis 内部机制的一个考察。不少人都不知道 Redis 是单线程工做模型。

缘由主要是如下三点:

纯内存操做

单线程操做,避免了频繁的上下文切换

采用了非阻塞 I/O 多路复用机制

仔细说一说 I/O 多路复用机制,打一个比方:小名在 A 城开了一家快餐店店,负责同城快餐服务。小明由于资金限制,雇佣了一批配送员,而后小曲发现资金不够了,只够买一辆车送快递。

经营方式一

客户每下一份订单,小明就让一个配送员盯着,而后让人开车去送。慢慢的小曲就发现了这种经营方式存在下述问题:

时间都花在了抢车上了,大部分配送员都处在闲置状态,抢到车才能去送。

随着下单的增多,配送员也愈来愈多,小明发现快递店里愈来愈挤,没办法雇佣新的配送员了。

配送员之间的协调很花时间。

综合上述缺点,小明痛定思痛,提出了经营方式二。

经营方式二

小明只雇佣一个配送员。当客户下单,小明按送达地点标注好,依次放在一个地方。最后,让配送员依次开着车去送,送好了就回来拿下一个。上述两种经营方式对比,很明显第二种效率更高。

在上述比喻中:

每一个配送员→每一个线程

每一个订单→每一个 Socket(I/O 流)

订单的送达地点→Socket 的不一样状态

客户送餐请求→来自客户端的请求

明曲的经营方式→服务端运行的代码

一辆车→CPU 的核数

因而有了以下结论:

经营方式一就是传统的并发模型,每一个 I/O 流(订单)都有一个新的线程(配送员)管理。

经营方式二就是 I/O 多路复用。只有单个线程(一个配送员),经过跟踪每一个 I/O 流的状态(每一个配送员的送达地点),来管理多个 I/O 流。

下面类比到真实的 Redis 线程模型,如图所示:

Redis-client 在操做的时候,会产生具备不一样事件类型的 Socket。在服务端,有一段 I/O 多路复用程序,将其置入队列之中。而后,文件事件分派器,依次去队列中取,转发到不一样的事件处理器中。

三、Redis 的数据类型及使用场景

一个合格的程序员,这五种类型都会用到。

String

最常规的 set/get 操做,Value 能够是 String 也能够是数字。通常作一些复杂的计数功能的缓存。

Hash

这里 Value 存放的是结构化的对象,比较方便的就是操做其中的某个字段。我在作单点登陆的时候,就是用这种数据结构存储用户信息,以 CookieId 做为 Key,设置 30 分钟为缓存过时时间,能很好的模拟出相似 Session 的效果。

List

使用 List 的数据结构,能够作简单的消息队列的功能。另外,能够利用 lrange 命令,作基于 Redis 的分页功能,性能极佳,用户体验好。

Set

由于 Set 堆放的是一堆不重复值的集合。因此能够作全局去重的功能。咱们的系统通常都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操做,能够计算共同喜爱,所有的喜爱,本身独有的喜爱等功能。

Sorted Set

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

四、Redis 的过时策略和内存淘汰机制

Redis 是否用到家,从这就能看出来。好比你 Redis 只能存 5G 数据,但是你写了 10G,那会删 5G 的数据。怎么删的,这个问题思考过么?

正解:Redis 采用的是按期删除+惰性删除策略。

为何不用定时删除策略

定时删除,用一个定时器来负责监视 Key,过时则自动删除。虽然内存及时释放,可是十分消耗 CPU 资源。在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,所以没有采用这一策略。

按期删除+惰性删除如何工做

按期删除,Redis 默认每一个 100ms 检查,有过时 Key 则删除。须要说明的是,Redis 不是每一个 100ms 将全部的 Key 检查一次,而是随机抽取进行检查。若是只采用按期删除策略,会致使不少 Key 到时间没有删除。因而,惰性删除派上用场。

采用按期删除+惰性删除就没其余问题了么

不是的,若是按期删除没删除掉 Key。而且你也没及时去请求 Key,也就是说惰性删除也没生效。这样,Redis 的内存会愈来愈高。那么就应该采用内存淘汰机制。

在 redis.conf 中有一行配置:

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的:

  • noeviction:当内存不足以容纳新写入数据时,新写入操做会报错。

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(推荐使用,目前项目在用这种)(最近最久使用算法)

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。(应该也没人用吧,你不删最少使用 Key,去随机删)

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,移除最近最少使用的 Key。这种状况通常是把 Redis 既当缓存,又作持久化存储的时候才用。(不推荐)

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,随机移除某个 Key。(依然不推荐)

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,有更早过时时间的 Key 优先移除。(不推荐)

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

一致性问题还能够再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。前提是若是对数据有强一致性要求,不能放缓存。咱们所作的一切,只能保证最终一致性。

另外,咱们所作的方案从根本上来讲,只能下降不一致发生的几率。所以,有强一致性要求的数据,不能放缓存。首先,采起正确更新策略,先更新数据库,再删缓存。其次,由于可能存在删除缓存失败的问题,提供一个补偿措施便可,例如利用消息队列。

  • 一致性要求高场景,实时同步方案,即查询redis,若查询不到再从DB查询,保存到redis;

  • 更新redis时,先更新数据库,再将redis内容设置为过时(建议不要去更新缓存内容,直接设置缓存过时),再用ZINCRBY增量修正redis数据

  • 并发程度高的,采用异步队列的方式,采用kafka等消息中间件处理消息生产和消费

  • 阿里的同步工具canal,实现方式是模拟mysql slave和master的同步机制,监控DB bitlog的日志更新来触发redis的更新,解放程序员双手,减小工做量

  • 利用mysql触发器的API进行编程,c/c++语言实现,学习成本高。

六、如何应对缓存穿透和缓存雪崩问题

这两个问题,通常中小型传统软件企业很难碰到。若是有大并发的项目,流量有几百万左右,这两个问题必定要深入考虑。缓存穿透,即黑客故意去请求缓存中不存在的数据,致使全部的请求都怼到数据库上,从而数据库链接异常。

缓存穿透解决方案:

  • 利用互斥锁,缓存失效的时候,先去得到锁,获得锁了,再去请求数据库。没获得锁,则休眠一段时间重试。

  • 采用异步更新策略,不管 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存若是过时,异步起一个线程去读数据库,更新缓存。须要作缓存预热(项目启动前,先加载缓存)操做。

  • 提供一个能迅速判断请求是否有效的拦截机制,好比,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。若是不合法,则直接返回。

  • 缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而致使数据库链接异常。

缓存雪崩解决方案:

  • 给缓存的失效时间,加上一个随机值,避免集体失效。

  • 使用互斥锁,可是该方案吞吐量明显降低了。

  • 双缓存。咱们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。本身作缓存预热操做。

  • 而后细分如下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,而且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。

缓存击穿解决方案

  • 对于一些设置了过时时间的key,若是这些key可能会在某些时间点被超高并发地访问,是一种很是“热点”的数据。这个时候,须要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是不少key。 缓存在某个时间点过时的时候,刚好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过时通常都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 咱们的目标是:尽可能少的线程构建缓存(甚至是一个) + 数据一致性 + 较少的潜在危险

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

这个问题大体就是,同时有多个子系统去 Set 一个 Key。这个时候要注意什么呢?你们基本都是推荐用 Redis 事务机制。

可是我并不推荐使用 Redis 的事务机制。由于咱们的生产环境,基本都是 Redis 集群环境,作了数据分片操做。你一个事务中有涉及到多个 Key 操做的时候,这多个 Key 不必定都存储在同一个 redis-server 上。所以,Redis 的事务机制,十分鸡肋。

若是对这个 Key 操做,不要求顺序

这种状况下,准备一个分布式锁,你们去抢锁,抢到锁就作 set 操做便可,比较简单。

若是对这个 Key 操做,要求顺序

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

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

假设时间戳以下:

系统 A key 1 {valueA 3:00} 系统 B key 1 {valueB 3:05} 系统 C key 1 {valueC 3:10}

那么,假设系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现本身的 valueA 的时间戳早于缓存中的时间戳,那就不作 set 操做了,以此类推。其余方法,好比利用队列,将 set 方法变成串行访问也能够。

相关文章
相关标签/搜索