Redis使用过程中遇到的问题

Redis使用过程中的问题

  • 数据库和redis的数据一致性问题(最终一致性)

  1. 先更新数据库,再删除缓存(相当于被动更新)

  2. 先删除缓存,再更新数据库

  • 缓存雪崩

大量热点数据同时失效,或者redis出现故障

由redis的key过期导致

  1. 过期时间设置一个随机值

  2. 对于热点数据,没必要设置过期时间

  3. 增加二级缓存

  • 缓存穿透

redis和mysql都不存在的情况,有可能是恶意攻击。

Redis使用起来很简单,但是在实际应用过程中,一定会碰到一些比较麻烦的问题,常见的问题有

  • redis和数据库数据的一致性

  • 缓存雪崩

  • 缓存穿透

  • 热点数据发现

下面逐一来分析这些问题的原理及解决方案。

数据一致性

针对读多写少的高并发场景,我们可以使用缓存来提升查询速度。当我们使用Redis作为缓存的时候,一般流程如图3-4所示。

  • 如果数据在Redis存在,应用就可以直接从Redis拿到数据,不用访问数据库。

  • 如果Redis里面没有,先到数据库查询,然后写入到Redis,再返回给应用。

     

因为这些数据是很少修改的,所以在绝大部分的情况下可以命中缓存。但是,一旦被缓存的数据发生变化的时候,我们既要操作数据库的数据,也要操作Redis的数据,所以问题来了。现在我们有两种选择:

  • 先操作Redis的数据再操作数据库的数据

  • 先操作数据库的数据再操作Redis的数据

到底选哪一种?

首先需要明确的是,不管选择哪一种方案, 我们肯定是希望两个操作要么都成功,要么都一个都不成功。不然就会发生Redis跟数据库的数据不一致的问题。但是,Redis的数据和数据库的数据是不可能通过事务达到统一的,我们只能根据相应的场景和所需要付出的代价来采取一些措施降低数据不一致的问题出现的概率,在数据一致性和性能之间取得一个权衡。

对于数据库的实时性一致性要求不是特别高的场合,比如T+1的报表,可以采用定时任务查询数据库数据同步到Redis的方案。由于我们是以数据库的数据为准的,所以给缓存设置一个过期时间,是保证最终一致性的解决方案。

Redis:删除还是更新?

这里我们先要补充一点,当存储的数据发生变化,Redis的数据也要更新的时候,我们有两种方案,一种就是直接更新,调用set;还有一种是直接删除缓存,让应用在下次查询的时候重新写入。

这两种方案怎么选择呢?这里我们主要考虑更新缓存的代价。

更新缓存之前,判断是不是要经过其他表的查询、接口调用、计算才能得到最新的数据,而不是直接从数据库拿到的值,如果是的话,建议直接删除缓存,这种方案更加简单,一般情况下也推荐删除缓存方案。

这一点明确之后,现在我们就剩一个问题:

  • 到底是先更新数据库,再删除缓存

  • 还是先删除缓存,再更新数据库

先更新数据库,再删除缓存

正常情况:更新数据库,成功。删除缓存,成功。

异常情况

  1. 更新数据库失败,程序捕获异常,不会走到下一步,所以数据不会出现不一致。

  2. 更新数据库成功,删除缓存失败。数据库是新数据,缓存是旧数据,发生了不一致的情况。

这种问题怎么解决呢?我们可以提供一个重试的机制。

比如:如果删除缓存失败,我们捕获这个异常,把需要删除的key发送到消息队列。然后自己创建一个消费者消费,尝试再次删除这个key,如图3-5所示。

 

另外一种方案,异步更新缓存

因为更新数据库时会往binlog写入日志,所以我们可以通过一个服务来监听binlog的变化(比如阿里的canal),然后在客户端完成删除key的操作。如果删除失败的话,再发送到消息队列。

总之,对于后删除缓存失败的情况,我们的做法是不断地重试删除,直到成功。无论是重试还是异步删除,都是最终一致性的思想,如图3-6所示。

基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了mysql。

 

先删除缓存,再更新数据库

正常情况:删除缓存,成功。更新数据库,成功。

异常情况:

  • 删除缓存,程序捕获异常,不会走到下一步,所以数据不会出现不一致。

  • 删除缓存成功,更新数据库失败。 因为以数据库的数据为准,所以不存在数据不一致的情况。

看起来好像没问题,但是如果有程序并发操作的情况下:

  • 线程A需要更新数据,首先删除了Redis缓存

  • 线程B查询数据,发现缓存不存在,到数据库查询旧值,写入Redis,返回

  • 线程A更新了数据库

这个时候,Redis是旧的值,数据库是新的值,发生了数据不一致的情况,如图3-7所示,这种情况就比较难处理了,只有针对同一条数据进行串行化访问,才能解决这个问题,但是这种实现起来对性能影响较大,因此一般情况下不会采用这种做法。

 

缓存雪崩

缓存雪崩就是Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候Redis请求的并发量又很大,就会导致所有的请求落到数据库。

关于缓存过期

在实际开发中,我们经常会,比如限时优惠、缓存、验证码有效期等。一旦过了指定的有效时间就需要自动删除这些数据,否则这些无效数据会一直占用内存但是缺没有任何价值,因此在Redis中提供了Expire命令设置一个键的过期时间,到期以后Redis会自动删除它。这个在我们实际使用过程中用得非常多。

expire key seconds # 设置键在给定秒后过期 
pexpire key milliseconds # 设置键在给定毫秒后过期 
expireat key timestamp # 到达指定秒数时间戳之后键过期 
pexpireat key timestamp # 到达指定毫秒数时间戳之后键过期

EXPIRE 返回值为1表示设置成功,0表示设置失败或者键不存在,如果向知道一个键还有多久时间被删除,可以使用TTL命令

ttl key # 返回键多少秒后过期 
pttl key # 返回键多少毫秒后过期

当键不存在时,TTL命令会返回-2,而对于没有给指定键设置过期时间的,通过TTL命令会返回-1。除此之外,针对String类型的key的过期时间,我们还可以通过下面这个方法来设置,其中可选参数 ex表示设置过期时间。

set key value [ex seconds]

如果向取消键的过期时间设置(使该键恢复成为永久的),可以使用PERSIST命令,如果该命令执行成功或者成功清除了过期时间,则返回1 。 否则返回0(键不存在或者本身就是永久的)

SET expire.demo 1 ex 20 
TTL expire.demo 
PERSIST expire.demo 
TTL expire

除了PERSIST命令,使用set命令为键赋值的操作也会导致过期时间失效。

关于key过期的实现原理

Redis使用一个过期字典(Redis字典使用哈希表实现,可以将字典看作哈希表)存储键的过期时间,字

典的键是指向数据库键的指针(使用指针可以避免浪费内存空间),字典的值是一个毫秒时间戳,所以

在当前时间戳大于字典值的时候这个键就过期了,就可以对这个键进行删除(删除一个键不仅要删除数

据库中的键,也要删除过期字典中的键)。

设置过期时间的命令都是使用 pexpireat 命令实现的,其他命令也会转换成 pexpireat 。给一个键设

置过期时间,就是将这个键的指针以及给定的到期时间戳添加到过期字典中。比如,执行命令

pexpireat key 1608290696843 ,那么过期字典结构将如图3-8所示。

 

过期键的删除

过期键的删除有两种方法。

  • 被动方式删除

被动方式的核心原理是,当客户端尝试访问某个key时,发现当前key已经过期了,就直接删除这个key。

当然,有可能会存在一些key,一直没有客户端访问,就会导致这部分key一直占用内存,因此加了一个主动删除方式。

  • 主动方式删除

主动删除就是Redis定期扫描过期间中的key进行删除,它的删除策略是:

从过期键中随机获取20个key,删除这20个key中已经过期的key。

如果在这20个key中有超过25%的key过期,则重新执行当前步骤。实际上这是利用了一种概率算法。

Redis结合这两种设计很好的解决了过期key的处理问题。

如何解决缓存雪崩

了解了过期key的删除后,再来分析缓存雪崩问题。缓存雪崩有几个方面的原因导致。

  • Redis的大量热点数据同时过期(失效)

  • Redis服务器出现故障, 这种情况,我们需要考虑到redis的高可用集群,这块后面再说。

我们来分析第一种情况,这种情况无非就是程序再去查一次数据库,再把数据库中的数据保存到缓存中就行,问题也不大。可是一旦涉及大数据量的需求,比如一些商品抢购的情景,或者是主页访问量瞬间较大的时候,单一使用数据库来保存数据的系统会因为面向磁盘,磁盘读/写速度比较慢的问题而存在严重的性能弊端,一瞬间成千上万的请求到来,需要系统在极短的时间内完成成千上万次的读/写操作,这个时候往往不是数据库能够承受的,极其容易造成数据库系统瘫痪,最终导致服务宕机的严重生产问题。

解决这类问题的方法有几个。

  • 对过期时间增加一个随机值,避免同一时刻大量key失效。

  • 对于热点数据,不设置过期时间。

  • 当从redis中获取数据为空时,去数据库查询数据的地方互斥锁,这种方式会造成性能下降。

  • 增加二级缓存,以及缓存和二级缓存的过期时间不同,当一级缓存失效后,可以再通过二级缓存获取。

缓存穿透

缓存穿透,一般是指当前访问的数据在redis和mysql中都不存在的情况,有可能是一次错误的查询,也可能是恶意攻击。

在这种情况下,因为数据库值不存在,所以肯定不会写入Redis,那么下一次查询相同的key的时候,肯定还是会再到数据库查一次。试想一下,如果有人恶意设置大量请求去访问一些不存在的key,这些请求同样最终会访问到数据库中,有可能导致数据库的压力过大而宕机。

这种情况一般有两种处理方法。

缓存空值

我们可以在数据库缓存一个空字符串,或者缓存一个特殊的字符串,那么在应用里面拿到这个特殊字符串的时候,就知道数据库没有值了,也没有必要再到数据库查询了。

但是这里需要设置一个过期时间,不然的会数据库已经新增了这一条记录,应用也还是拿不到值。

这个是应用重复查询同一个不存在的值的情况,如果应用每一次查询的不存在的值是不一样的呢?即使你每次都缓存特殊字符串也没用,因为它的值不一样,比如我们的用户系统登录的场景,如果是恶意的请求,它每次都生成了一个符合ID规则的账号,但是这个账号在我们的数据库是不存在的,那Redis就完全失去了作用,因此我们有另外一种方法,布隆过滤器。

布隆过滤器解决缓存穿透

先来了解一下布隆过滤器的原理,

  • 首先,项目在启动的时候,把所有的数据加载到布隆过滤器中。

  • 然后,当客户端有请求过来时,先到布隆过滤器中查询一下当前访问的key是否存在,如果布隆过滤器中没有该key,则不需要去数据库查询直接反馈即可

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

powerfuler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值