一、简单聊下redis的瓶颈

  1. redis很快。原因是redis的数据是存储在机器内存上的,那么redis在拿数据的时候不会从硬盘上面读取,也就大大减少了IO次数。
  2. redis是单线程。在处理网络请求时只有一个线程来处理,也就避免了多线程情况下由加锁之类带来的的cpu处理机消耗。
  3. 使用多路I/O复用模型。多路指的是多个请求,复用指的是复用同一个线程,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求。
    以上三条可以得到redis在操作数据时候cpu并不是其瓶颈。它的瓶颈是机器内存大小网络带宽(客户端和redis服务之间的网络传输带来消耗)。

通过以上,可以知道,客户端会先去redis的缓存数据中取数据,没的话就去MySQL中取。接下来聊下redis缓存会出现什么问题。

二、 redis缓存会出现的问题(缓存雪崩、缓存击穿、缓存穿透)

  1. 缓存雪崩,分析如下:
    客户端访问数据访问流程见图如下:
    为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里。
    那么,当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩
    由上面分析可得导致缓存雪崩的原因为:大量缓存数据同一时间过期;Redis故障宕机。
    从这两个原因逐个分析出解法方法,先从大量缓存数据同一时间过期这个原因出发,解决如下:
    (1)均匀设置过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
    (2)互斥锁。如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存,当缓存构建完成后,再释放锁。这样新的请求由于未获取互斥锁,那么它只能等待锁释放后重新读取缓存,或者返回空值或者默认值。
    实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
    (3)双 key 策略。我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。
    (4)后台更新缓存。业务线程(它的主要任务就是处理客户端的请求并对其响应)不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。但是呢,当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值。看来后台更新缓存还要分情况的:
  • 第一种方法,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,若检测到缓存无效,就从数据库读取数据,并更新到缓存。
  • 第二种方法,在业务线程发现缓存数据被淘汰后,通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。
    所以呢,在业务刚上线的时候,我们最好提前把数据缓存起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热

再从redis故障宕机这个原因出发,解决如下:
(1)服务熔断或请求限流机制
因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。

服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作
为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。
(2)构建 Redis 缓存高可靠集群
通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务。

  1. 缓存击穿
    简介:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿
    解决方法同缓存雪崩(因为一个是大部分数据,一个是热点数据,后者是前者的子集)。
  2. 缓存穿透
    简介:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透
    解决方法如下:
    (1)非法请求的限制。当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在入口处要判断求请求参数等是否合理。
    (2)缓存空值或者默认值。当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
    (3)使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在在写入数据库数据时,使用布隆过滤器做个标记。在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在而不是查数据库,若布隆过滤器中不存在此数据,也就不用查数据库了。这样以来,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库

三、缓存中的数据要和数据库中的数据一致

  1. 先想一个问题,修改数据之后,是先修改缓存中的还是MySQL中的数据?
    先更新数据库再更新缓存 还是 先更新缓存再更新数据库结果都会因为并发问题导致出现bug。二者情况见图如下:
    第一种,先更新数据库,再更新缓存。
  2. redis的瓶颈在哪里 redis 瓶颈_缓存

  3. 此时,缓存中数据为1,数据库中数据为2。
    第二种,先更新缓存,再更新数据库。
  4. redis的瓶颈在哪里 redis 瓶颈_redis_02

  5. 此时,缓存中数据为2, 数据库中数据为1。
  6. 既然二者都是更新操作时(更新数据库和更新缓存),会因为并发问题,造成bug,那么就换种思路 —— 更新数据库,删除缓存。
    可知情况有二,如下:
    第一种,先删除缓存,再更新数据库。
  7. redis的瓶颈在哪里 redis 瓶颈_redis_03

  8. 此时,缓存中数据为20,数据库中数据为21。
    第二种,先更新数据库,再删除缓存。
  9. redis的瓶颈在哪里 redis 瓶颈_缓存_04

  10. 此时,缓存中值为20,数据库中值为21。

至此,那这删除缓存策略也不行啊。其是 先更新数据库,再删除缓存这种策略之下,发生的并发问题概率很小,因为缓存的IO速度远远大于数据库的IO速度(前者是读写内存中的数据,后者是读写磁盘中的数据)。因此,先删除缓存,再更新数据库。这个方法是可以保证缓存中和数据库数据的一致性的。

问题引出:先更新数据库,再删除缓存这个操作并不像mysql中的事务那样具有原子性,所以,这个操作可能不会都执行成功。那么就会造成一个问题 —— 通常情况下,缓存中的数据都有过期时间的设定,那么当用户更新数据时,假如 删除缓存 这个操作失败了,就会导致缓存中的数据是旧的,用户看到的就是数据更新没有成功。因为该旧的数据在缓存中设置了过期时间,过期之后的话,用户在进行访问该数据之后,会从数据库中查,再重新构建缓存,也就是造成了,更新数据之后,过一会儿才有效

所以接下来,是怎么解决 更新数据库 且 删除缓存 操作都能成功。
解决如下:
(1)重试机制
(2)订阅 MySQL binlog,再操作缓存

挨个来看。

  1. 重试机制。引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。如果删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。直至删除缓存成功了,此数据才从消息队列中移除。当然,若重试次数过多,就需要向业务层发送报错信息了
  2. 订阅 MySQL binlog,再操作缓存。更新数据库成功之后,会在 MySQL binlog 中记录一条日志,于是就可通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。
    MySQL binglog 订阅举例:阿里巴巴Canal中间件的作用。Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

至此,可知若想保证 先更新数据库,再删缓存 二个操作都能执行成功,我们可以通过 重试机制 ,或者 订阅MySQL binlog再操做缓存 完成保证,这两种方法有一个共同的特点,都是采用异步操作缓存。