首页>软件资讯>常见问题

常见问题

Redis缓存一致性问题

发布时间:2025-11-17 08:51:32人气:1


我们的项目中有遇到过使用redis做缓存的场景,主要是使用redis存储用户权限信息以及缓存热点数据。那么不可避免地,会被问到:缓存一致性问题和雪崩,击穿以及穿透问题。

首先,为什么使用Redis?

有很多公司的招聘要求上还标明了Memcache,但是目前更流行的还是Redis。

数据结构:Memcache支持的数据结构很少,Key-value,hashmap这几个,如果要保存List,还得序列化为JSON字符串,取出来时还要反序列化。

持久化:Redis提供了持久化机制,而Memcache是纯内存的。

集群:Memcache的集群很简单,客户端直接根据hash值判断存取Memcache的节点,但是redis就有各种模式,主从,哨兵以及集群模式。

Redis.png

实际场景:

项目中需要调用另一个service去付钱还贷款,service返回成功的结果后会相应地去db的记录里扣除这笔贷款已经还掉的金额。而贷款未还部分的金额,在页面很多地方会需要显示,所以需要缓存来加快页面加载速度。

数据结构是key-value,贷款的id作为key,贷款的金额以及一些其他的要展示的数据作为value。这是一个读高并发,写低并发的情况,因为一个人在多个渠道同时还贷款的情况还是蛮少的。

1. 预热:项目启动时加载loan还款记录到缓存,使用的String存储Json字符串,如:Loan:20250821I001: {'outstandingAmt': 2000, 'userId': 'aaa', 'status': 'open'}。

2. 读:cache-aside。先从缓存中读取数据,如果缓存中有数据,则取到数据;如果缓存未命中,就从数据库里拿,然后写入缓存。如果写入缓存失败:会重试三次,还是失败的话就需要程序员上来看是什么原因(有设置alert),但是也没有太大影响,最多是从数据库拿数据,不会影响数据准确性。

缓存击穿问题

如果某个key过期,读的时候大量的线程访问这个key,会导致数据库压力过大。我们采取措施去避免这个问题,就是给key加互斥锁,就是当读取到为空值时,给“从数据库读取以及写入缓存”的操作加锁,确保只有一个线程去数据库读取数据,后面的线程都从缓存读。锁的过期时间用的看门狗机制,大概是3秒,但是每隔一秒去查看是否可以释放锁。


缓存雪崩问题

大量的key失效,导致数据库压力过大。预防的办法是过期时间设置为10小时+-10分钟内的偏移量。

缓存穿透问题

某个key既不存在于缓存又不存在于数据库,但是源源不断地有请求过来。解决方法是缓存空值,5s过期,并且对loanId进行格式验证。

3. 写:采用的先更新DB后异步删除缓存。只有在这样的情况下才会有不一致的问题:A线程发现缓存里没有数据,去db拿数据。同时,B线程在更新数据库,还没有删除的时,A线程已经把旧数据更新到缓存中了,这个时候C线程过来刚好读到旧数据——这种情况下,需要三个线程才会有不一致的问题,概率比较小;另一种情况是,A线程拿到数据后更新缓存缓慢,在B线程删除了以后才将旧数据更新进去——这种情况也很少,一般A线程的操作会更快。而为了避免删缓存失败,会用MQ异步删除,失败重试。

数据库更新成功,缓存删除失败怎么办:缓存删除时也会try catch,重试三次,并且过期时间设置为一小时左右,避免长时间不一致。

如果发生了缓存不一致的prod issue怎么办?

1. 临时解决办法:

针对比较独立的数据,找到出现不一致的key,然后手动删除缓存。

2. 再分析log,看是什么导致的:

我们之前的缓存更新的结构是先更新数据库再删除缓存,如果缓存操作失败了,会有异步任务去重试。如果重试三次失败了,会发alert,并且log里面记录。当时我们是有收到alert邮件的,就是删除缓存时有失败的情况。不过还是去redis确认了一下,看了下log就是那段很短的时间里,可能是因为网络抖动之类的问题,导致一个结点断连了,然后又重连。在这个时候redis从结点会发送自己的offset到主结点,主结点在自己的缓冲区里看这个offset是否在这个范围内,如果在就将后面的数据复制到从结点。在复制的这个过程中这些数据是不能被更新或者删除的,所以我们的删除是失败的。并且我们之前的删除操作的重试都是没有时间间隔的,所以在这种情况下重试其实没有用。后来就是加上了时间间隔,200ms,400ms, 800ms,并且都有一个随机的浮动值避免多个同时重试。

但我的一个建议是采用oracle 的golden gate这个cdc工具,是监听修改的操作,然后发消息给kafka topic。mysql的话是监听binlog,应该也是有很多成熟的工具的。

补充:Redis主从复制过程

全量复制:新的从结点加入或者某个结点断连了很长时间。

1. 从结点发消息到主结点,主结点知道了“要全量复制”,会返回主结点的id和当前的offset。然后从结点会记录这两个值。

2. 主结点调用bgsave命令,会fork一个子进程,生成当前数据的rdb快照。这个时候主结点还是正常接收客户端的请求。这些写指令会被放到缓冲区。

3. RDB文件生成后主结点会将它发送给从结点,从结点会清除自己所有的数据然后加载RDB文件。并且主结点也会将缓存区的指令发送给从结点,从结点在加载完RDB之后,再执行这些指令。

增量复制:某些结点临时中断后又恢复。

1. 从结点会发送主结点的运行id和自己当前的offset给主结点

2. 主结点会判断,如果运行id不是自己,那么会全量复制;如果是,就会看offset是否在缓冲区的范围内,不是的话还是全量复制;是的话,就将offset之后的部分发送给从结点。那么从结点会执行这些命令。



上一条:Redis原理

下一条:Redis不同版本的线程模型