Redis系列问题那些事记录,有没有你不知道的点

还没被捞走,先写写博客吧,最近整的都有点上火了,本来就不太擅长面试,谁慧眼识珠快快把我带走哈哈

分布式锁

大家应该都知道有两种琐吧,一个是普通setnx,一个是看门狗,问题是这两种有什么区别,什么场景用,怎么用?来个表

特性Redisson 看门狗锁普通 Redis 锁
适用场景长时间任务、业务耗时不确定短期任务(<10秒)、简单场景明确知道执行时间
锁粒度支持可重入、公平锁等高级特性仅基础互斥锁
性能损耗略高(维护后台线程)较低
推荐使用场景订单支付、库存扣减等关键业务缓存更新、简单同步控制

看门狗

自动续期机制

  • 默认每 10 秒检查锁状态
  • 若业务未完成,自动将锁过期时间重置为30 秒
  • 无需手动计算业务耗时

防死锁设计

  • 客户端宕机时,锁最终会因超时自动释放
  • 默认锁超时时间 30 秒(可通过配置修改)
@Autowired
private RedissonClient redissonClient;

public void processWithWatchdogLock(String resourceId) {
    RLock lock = redissonClient.getLock("myLock:" + resourceId);
    try {
        // 获取锁(自动启用看门狗)
        boolean isLocked = lock.tryLock(10, 5 * 60, TimeUnit.SECONDS); 
        if (isLocked) {
            // 业务逻辑(自动续期)
            doBusinessLogic(resourceId);
        } else {
            log.warn("获取锁失败,资源ID: {}", resourceId);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

private void doBusinessLogic(String resourceId) {
    // 模拟耗时操作(无需担心锁过期)
    try {
        Thread.sleep(120_000); // 2分钟
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    log.info("处理完成,资源ID: {}", resourceId);
}

有个问题:如果两个时间都设置为-1,业务不结束,获取琐会一直等待,获取到琐后看门狗会一直续期,默认每 10 秒检查锁状态,若业务未完成,将锁过期时间‌重置为 30 秒,这样会形成逻辑上的死锁,为避免这种情况怎木办?

客户端崩溃或宕机会停止续期

无限续期问题

解决方案直接放上来

  • 强制设置琐最大持有时间:boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)设置成lock.tryLock(10, 5 * 60, TimeUnit.SECONDS);最多持有5分钟主动释放(评估业务)
  • 业务层超时控制,在业务逻辑中增加超时检测,超时后主动中断任务并释放锁‌,写法如下
Future<?> task = executor.submit(() -> doBusiness());
try {
    task.get(5, TimeUnit.MINUTES); // 业务最多执行5分钟
} catch (TimeoutException e) {
    task.cancel(true);
    lock.unlock();
}

主从锁失效问题

场景:当 redis 是一个主从架构时,当 A 用户从 master 节点获取到了锁,这时候出现这么一个情况,这个锁没有及时同步到 slave 节点时,master 节点挂了,由 redis 哨兵做了主从切换,以前的 slave 成了主,而这时 slave 是没有锁的这个 key 的,那么就会导致其他用户也可以成功竞争到锁进行业务操作,从而导致脏数据。

server端高可用方案

通过配置保证server尽量可能少的丢失数据

min-slaves-to-write 1 min-slaves-max-lag 10

  • min-slaves-to-write:设置主库最少得有 N 个健康的从库存活才能执行写命令。这个配置虽然不能保证 N 个从库都一定能接收到主库的写操作,但是能避免当没有足够健康的从库时,主库无法正常写入,以此来避免数据的丢失。
  • min-slaves-max-lag:配置从库和主库进行数据复制时的 ACK 消息延迟的最大时间,可以确保从库在指定的时间内,如果 ACK 时间没在规定时间内,则拒绝写入

Client 端 的高可用方案

适用红锁,只有在获得了大多数 Redis 实例的锁(即 N/2 + 1 个节点,N 为节点总数)之后,才认为成功获取了分布式锁

红锁的问题

  1. 性能问题
  2. 并发安全问题:客户端在持有锁的过程中发生长时间停顿(例如 JVM 的 STW),导致锁实际上已经失效,但客户端由于停顿结束后仍然认为持有锁
  3. Redisson官方已废弃红锁,所以老铁们别考虑这个了

系列问题

长话短说了哈,有问题,就有解决方案

缓存雪崩

解释下,大量的key设置相同的过期时间,同时失效,造成瞬间DB请求量大,压力剧增引起雪崩

解决方案

  1. 给缓存过期时间设置随机时间不同时失效
  2. 热点数据不过期
  3. 提高redis可用性
  4. 服务限流&降级
  5. 缓存预热

缓存穿透

访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时会挂掉

解决方案:

  1. 采用布隆过滤器,使用大的bitmap,存储可能访问的key,不存在的key直接过滤
  2. 访问key未查到值,也将空值写进缓存,设置较短时间(使其不查数据库),问题是如果大数据量查询会导致缓存大量的空值的key占用空间
  3. 校验完善:好的校验会防止不合法的参数请求,减少穿透的概率

缓存击穿

一个存在的key,过期的瞬间有大量请求,击穿db,可能是个hotkey

解决方案

  1. 互斥锁,可用分布式锁控制的强,如果服务器不多也可用本地锁,就算击穿也不会将db打死
  2. 缓存空值,设置一个合理时间
  3. 热点数据不过期
  4. 限流,都可以

Redis6.0后为什么引入了多线程?

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题

Redis6.0 的多线程默认是禁用的(暂时不建议开),只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 redis.conf:

rio-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

Redis命令执行最初为什么设计成单线程的?而不是多线程?多线程不是提高效率吗?

因为没必要

多线程是通过并发的方式来提升I/O的利用率和CPU的利用率

Redis不需要提升CPU利用率,因为操作都是内存,CPU不是瓶颈

多线程可以提升IO的利用率,但是多线程带来的并发问题也会带来更多的复杂性,切换也会带来一定的开销,所以选择了IO多路复用的方式

Redis能否保证100%不丢失?

不能,这个我们在持久化那说过了,flushAppendOnlyFile为一个wihile循环,叫事件循环

  • 第N+1轮循环的第一阶段,调用flushAppendOnlyFile 的,会将aof buffer写到磁盘上
  • 第N轮循环的第二阶段,将读取到的命令,写入aof buffer,而不是直接落盘

即使在配制appendfsync=always的策略下,还是会可能丢失一个事件循环的aof_buf数据

无底洞问题,如何解决?

热点key我们一般采取对key求hash加到尾部将key放到多个节点的方式来存取,取数据时需要跨多个节点执行,导致网络请求次数随节点数量线性增长,性能下降问题(正常水平扩展应该性能变好),叫无底洞问题

  1. 数据模型优化:减少跨节点操作

  2. 使用Hash Tag,强制相关 Key 分配到同一分片,如果键中包含 {…},只有花括号内的部分参与哈希计算,其他部分被忽略

    1. Hash Tag使用场景:跨节点访问-无底洞问题;事务操作必须在同一节点;Lua脚本必须在同一节点;数据关联性
      1. 单机没有问题,操作符{}只会被看做普通操作符
      2. 使用注意事项:合理设计tag,保证唯一性,避免过度集中
# 使用 {} 定义 Hash Tag,确保 user:123 的所有数据在同一哈希槽同一节点 
SET user:123:{1000} "xxx" 
SET user:123:{1000} "yyy" 
# 批量操作时只需访问一个分片 
MGET user:123:{profile} user:123:{orders}
  1. 客户端优化:并行+异步,多线程获取,阻塞合并
  2. 本地缓存:减少访问Redis的次数

Redis内存不足如何处理

  1. 设置maxmemory增加内存防内存溢出
  2. 修改内存淘汰策略
  3. 横向扩容
  4. 优化key,设置过期时间

rehash机制|字典是怎么扩容的

hash扩容会非常耗时导致服务阻塞,如果在主仓库里进行会被卡主

为了不阻塞,Redis设计了两个仓库ht[0]和ht[1]

ht[0]:主哈希表,用于存储当前所有数据,并处理客户端的读写请求

ht[1]:备用哈希表,用于扩容操作

  1. 避免阻塞
  2. 高效响应
  3. 无缝切换

双表渐进式rehash扩容过程

  1. 在ht[1]中创建一个新的,更大的哈希表
  2. 将ht[0]中的数据逐步迁移到ht[1]中
  3. 每次最多迁移一个哈希桶的数据
  4. 每次迁移操作都在客户端的读写请求中完成,利用空闲时间进行
  5. 如果连续遇到10个空的桶,则本次rehash终止,等待下一次机会继续

新数据处理

在rehash过程中,所有新添加的数据直接存储到 ht[1] 中,而不是 ht[0]

rehash 完成后将 ht[1] 的指针赋值给 ht[0],使 ht[1] 成为新的主哈希表

重置ht[1],为下一次扩容做准备

Redis为什么用跳表而不用B+树?

同理MySQL为什么用B+树而不用跳表?

  1. 跳表结构简单,B+树需处理节点分裂、合并、平衡等复杂逻辑
  2. 跳表对内存的访问呢是顺序或近邻的,缓存局部性较好;B+树可能导致更多的缓存未命中
  3. 动态操作性能:跳表的插入删除仅调整相邻节点;B+树开销更大
  4. 查询效率高
  5. 跳表无锁机制,适合redis的单线程

跳表与B+树比较

  • 相同点:都是最下面一层包含所有数据,都有序,都是有序链表+多级索引

  • 不同点

    • B+树为维持平衡,不断页分裂来保证B+树的子树们高度层级尽量一致,适合读多写少
    • skiplist新增删除数据靠随机函数,向上添加索引
  • IO操作单位不同:B+树是多叉平衡搜索树,每个结点都是一个16k的数据页,一个page 能存放较多索引信息 ,所以树的层数比较低只需要3层左右就能存放2kw左右的数据,最多需要查询三次磁盘IO

    • 同样情况下跳表是链表结构,最底层存放2KW数据,每次查询都要达到二分查找的效果,2kw数据高度大概需要24层左右,如果要一个节点要进行一次磁盘IO,大概要进行 24次磁盘IO,假设层高对应磁盘IO,那么B+树的读性能会比跳表要好,因此mysql选了B+树做索引
  • B+树是 "磁盘友好型"的数据结构,适合于DB,比如mysql就使用B+树

  • 跳表 是 "内存友好型"的数据结构, 适合于Cache,比如redis就使用跳表

启动时WARNING优化

请添加图片描述

第一个warning

高并发环境下你需要一个高backlog值来避免慢客户端连接问题

第二个warning

redis在备份数据的时候,会fork出一个子进程,理论上child进程所占用的内存和parent是一样的,比如parent占用的内存为8G,这个时候也要同样分配8G的内存给child,如果内存无法负担,往往会造成redis服务器的down机或者IO负载过高,效率下降

所以内存分配策略应该设置为 1(表示内核允许分配所有的物理内存,而不管当前的内存状态如何)。

内存分配策略有三种

可选值:0、1、2。

0, 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。

1, 不管需要多少内存,都允许申请。

2, 只允许分配物理内存和交换内存的大小(交换内存一般是物理内存的一半)

第三个warning

THP会造成内存锁影响redis性能,对持久化不友好,建议关闭,THP先自己查一下,它对于linux本身合并内存页是好的,但是对于Redis大块内存会造成阻塞,后面会从linux角度介绍下

这篇主要记录各种问题以及解决方案,后面再补充吧,就酱

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

站长大人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值