Redis高级进阶终极版本(内含高频面试问题)

缓存三兄弟

如果这个请求大量的高并发业务,访问我们的系统,此时是每次请求都会到达数据库去查询数据,而数据库可能会被高并发的请求打死。

缓存穿透:发出一个数据库与缓存都没有的数据。

1.缓存空对象:

无论对象是不是空,都给他存进去,相当于只会攻击数据库一次

2.数据白名单

3.布隆过滤器:

把要访问的数据发直接存到一个hash列表里面,当访问的hash值如果是1则可能冲突(布隆过滤器误差),如果是0则一定不存在。而且布隆过滤器只能过滤一定不存在的数据。有一定的判断误差不可直接取代redis和数据库。


先将数据库有的元素经过映射函数映射到bitmap里,将对应位置数置为1.。将用户搜寻的关键字经过映射函数,去找bitmap里对应有没有1。也有少概率事件:元素3(用户请求)匹配上了多个元素对应的1(误打误撞,碰巧),也会放过去(只要有一个位置出现0就过滤掉),但已经阻止90%的非法请求。


缓存击穿

缓存击穿是指一个key的热点非常高,这个热点非常高的数据来访问系统会发生什么? 假设我们找到这个热端key,我不设置过期时间,使他永不过期,但是,我是说但是,如果一个非常知名男歌星与一个不知名的女歌星结婚,这个新闻很劲爆,男歌星的数据存在于Redis中,女歌星信息不在Redis中,那么大量用户去微博搜索这个女歌星,微博的数据库直接就被干爆。这就是缓存击穿。

解决方案

1.分布式锁 :直接使用分布式锁性能太差
2.永不过期(把数据加入缓存且永不过期)
3.分布式锁===》teyLock+double check ====》企业级应用

缓存雪崩

大量的key在同一时间失效,哪怕不是热点key,此时也架不住人多的访问。
企业解决方法:固定时间(如2day)+随机时间(0-12h)的缓存淘汰机制。

有关异步编排的问题

(只适合后台程序,不适合客户端程序且访问量较低)
器是我们去做这个统计,现在走的是报表,但是实际是来说,有时候我们可能会发出特别多的sql,整个方法耗时会累加。
这样可以开启一个线程池来解决:

如何配置线程池

可以按照CPU线程数+1配置,或者模拟按20-40随缘配置,然后进行压力测试。(模拟100个线程向服务器发起请求,可能会出现内存泄漏等)一般在压力测试时会监控cpu(75%的使用率)的使用率与内存使用率

1.项目如何使用线程池

1.一个方法中有多个分支,最好是分支与分支之间没有太大逻辑关系,如方法中AB串行,de穿行,ab,c,de之间并行,解决方法就是异步编排问题。
2.你的任务太多,你找了很多人给你干活,但是每个人的内容不一样,且都是你任务的一部分,此时你会遇到:在分享成走完以后主线程再走的问题。

redis的rdb和aof

rdb(快照)===>二进制的文件
RDB:每隔一段时间,当他满足了一定条件时候,此时就会触发redis的持久化,就会把这些持久化生成一个dump.rdb文件,如果要恢复数据,就按照dump.rdb进行数据恢复就可以了


save 3600 1
save 300 100
save 60 10000
只要满足以上任意一个条件,就满足了触发持久化机制的条件

触发持久化===>此时会调用save和bgsave;
bgsave:进行所谓的持久化动作

Redis如果收到了bgsave指令,如何就会fork出来一条子进程,该子进程与主进程一模一样,由子进程来进行持久化,主进程接着响应用户的正常请求,只要子进程持久化完成后,如何把数据写到dumo.rdb中,再子进程就会自己销毁,持久化结束

save: 就是阻塞的,主进程会取执行

在这里插入图片描述

RDB的BGsave是西安的基本流程:
fork主进程得到一个子进程。共享内存空间。在fork子进程的时进程阻塞。子进程读取内存数据写入到新的RDB文件。
用新的RDB文件替换旧的RDB文件。

RDB缺点: 数据安全的问题,数据持久化导致数据丢失。

AOF

aof 是以日志的防止来最佳用户每一次操作,当需要恢复数据时,把用户的操作重新执行一遍即可

RDB缺点:有可能在执行写入持久化操作时,挂掉,导致数据丢失一大块。

aof的同步策略:

同步策略是多久时间,buffer区的记录往aof里面灌。
同步策略:

  1. always:写一条我就记一条
  2. every second: 每秒同步
  3. no:不由程序员觉得,由操作系统决定什么时候记录。(持久化过程不可控)

aof重写机制:

  1. aof文件必须是原来大小的100%
  2. aof至少要大于64mb。

用rdb+aof,用rdb恢复海量数据,用aof恢复无法恢复的数据。
rdb的优缺点:
rdb优点:适合恢复海量数据(快照)
rdb缺点:会丢数据
aof的优缺点:
aof缺点:不适合恢复海量数据
aof优点:丢的数据可控

有先使用aof恢复数据,而RDB是使用数据备份,RDB一般没有AOF文件大。

高阶Redis

  1. 单点redis 存在的问题:
  2. 数据丢失问题 :实现Redis数据持久化
  3. 并发能力问题 :搭建主从集群,实现读写分离
  4. 存储能力问题 :搭建分片机制,利用查抄机制实现动态扩容
  5. 故障恢复问题 :利用Redis烧饼,实现健康检测和自动恢复

Redis主从

主从模式命令:
从节点:
当前节点> SLAVEOF 192.168.150.101 7001
当前节点> REPLICAOF 192.168.150.101 7001
命令意思:让指定的结点:7001成为当前节点的从节点。
主节点写操作会直接同步从节点,上synchonazation同步锁。
在这里插入图片描述

数据同步原理:

阶段1:

如果从节点是第一次与主节点同步:
slave会向master发送同步请求。master判断是否是第一次同步。如果是第一次就会发送数据的版本详细到slave

阶段2

master在给slave同步之前会执行bgsave指令,生成的rdb文件将会发送给slave,在slave接收到rdb文件后会执行清空本地数据操作,然后重新加载rdb文件
在生成rdb文件期间,master可能会收到其他指令,会将这些指令存入到repl_baklog的缓冲区里面

阶段3

master会发送repl_baklog里面的指令给slave,然后接收了就发。

master是怎么判断slave是第一次同步?

  1. Replication ID 简称replid master第一次会直接把自己的repild给slave。
  2. offset:偏移量,由于标记数据同步的进程量,一般来说slave拷贝的rdb文件中数据量是小于master的,如果offer比master小,说明就有新的数据产生,需要进行数据同步。
    因此slave做数据同步,必须向master声明自己的replication id和offset,master才能判断是否需要进行同步。
    在这里插入图片描述

增量同步失败:

repl_baklog 大小有限制,如果写满数据后会覆盖最早的数据,就像循环队列一样,会覆盖之前的值,如果哪一天slave挂了,但是master又有许多的新数据写入,且超过了repl_baklog的原本大小,此时slave的数据相当于已经被覆盖,在repl_baklog找不到,当slave完成重启时,虽然 replication一样,但是offset不同了,就只能全量同步。


SENTINEL实现主从替换:

  1. 监控: Sentinal 会不断检查你的master和slave是否按照预期工作
  2. 自动故障恢复:如果哪天master挂了,会让slave提升为master。故障实例恢复后页以新的master为主
  3. 通知:sentinal在处理主从问题时,是直接在Redis内部进行的,不会给客户端返回信息,而通知是sentinel来充当Redis客户端服务发现的来源,如果集群内发生故障就会将最新的消息推送给Redis的客户端。

服务监控状态

  1. 主观下线:某个节点如果在规定时间为响应给sentinel,那么sentinel就会认为该节点寄了。
  2. 客观下线: 在主观的情况下,其他sentinel会向master发送信息,如果在规定时间之内未回复,或收到的回复信息超过半数sentinel节点就认为该master下线。

主从选择:断开时间过长,直接排除成为master的可能。然后判断slave-priority值,越小优先级越高,如果是0则永远不会参与选举。
slave的slave-priority一样,判断slave的offset是否一杨,该值越大越好。
最后判断slave的节点id大小,越小优先级越高。

在做上述事情之前,sentinel集群会进行投票,在该集群内部选取一台sentinel来做选master的选举

  1. sentinel选了某个slave为master的化,对该slave会发起一个 slaveof no one 指令成为master。
    之后sentinel广播slaveof 指令让其余的slave认新的主。
    最后,故障的master会被标记为slave,恢复服务功能后自动加入新master的slave节点。

分布式锁

在这里插入图片描述

a ,b 都会执行setnx操作,假设a线程进入公共资源区域,经过这个setnx方法会让线程去拿key,如果key不存在,就插入值,获得锁,其余的线程就自旋,当a线程执行完业务操作就会把key删了,然后b自旋成功,获取锁执行业务操作。

syn与lock都是单体锁。syn锁只能锁的到单体jvm中的对象。
jmeter可以模拟多人访问服务器。

如果在抢锁的时候,服务器断电,或者损坏了,就需要加上锁的过期时间,利用redis的原子性api加过期时间。但是可能会出现锁不住问题:假设a进入所区域,此时a线程进入等待或阻塞,由于过期时间的作用,a会把锁释放,之后b去拿锁,但是这时a线程被唤醒,此时ab都在所区域,进行操作的就会导致数据不同步

删锁问题:

在Redis加锁前,上锁的进程对象会给锁上一个值,之后如果要删除锁,就会比较自己本地的值和redis里面的锁的值取出来比较,如果一样就可以删除。
但是假设a线程进入锁区域,获得到传入redis的值之后,线程阻塞,等到自己的过期时间到了释放锁,然后b进程进入所区域,b把自己的值给redis,但是此时a线程被唤醒,执行删除锁的操作,在此时就会出现问题!

因为现在a本地的值与a拿到他传入redis的值一样,然后执行删除锁的操作也成立,但是此时的锁是b线程拿到的,要删除的锁就是b的,解决方法就是让该所区域的代码处于原子性状态,可以通过lua表达式解决。
在这里插入图片描述


在面试上就说就按以下流程叙述:
上单例锁===>出现死锁===>添加过期时间====>原子性添加过期时间===>出现的问题: 1.锁不住,2.删除别人的锁 ==>uuid判断自己的id是否为自己的逻辑=>最后采用lua表达式解决。


分布式锁详解:
在这里插入图片描述

Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。
在这里插入图片描述

在此只需要关注-1这个释放锁的时间变量参数。
在这里插入图片描述

 // 直接使用lock无参数方法
public void lock() {
    try {
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

// 进入该方法 其中leaseTime = -1
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return;
    }

   //...
}

// 进入 tryAcquire(-1, leaseTime, unit, threadId)
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

// 进入 tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    //当leaseTime = -1 时 启动 watch dog机制
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    //执行完lua脚本后的回调
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        if (ttlRemaining == null) {
            // watch dog 
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;

从源码中可以得知,如果不传release,默认会给个-1,如果release是-1的话,通过 if (leaseTime != -1) 判断就会开启看门狗机制,这也是为啥我说,无论你是tryLock还是Lock只要不传release,就会开启看门狗机制。

调度续期方法

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    //将线程放入缓存中
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    //第二次获得锁后 不会进行延期操作
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        
        // 第一次获得锁 延期操作
        renewExpiration();
    }
}

// 进入 renewExpiration()
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    //如果缓存不存在,那不再锁续期
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            //执行lua 进行续期
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
                    //延期成功,继续循环操作
                    renewExpiration();
                }
            });
        }
        //每隔internalLockLeaseTime/3=10秒检查一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

//lua脚本 执行包装好的lua脚本进行key续期
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getName()),
            internalLockLeaseTime, getLockName(threadId));
}

关键结论
上述源码读过来我们可以记住几个关键情报:

watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
从可2得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;

因为无论在释放锁的时候,是否出现异常,都会执行释放锁的回调函数,把看门狗停了

有没有设想过一种场景?服务器宕机了?其实这也没关系,首先获取锁和释放锁的逻辑都是在一台服务器上,那看门狗的续约也就没有了,redis中只有一个看门狗上次重置了30秒的key,时间到了key也就自然删除了,那么其他服务器,只需要等待redis自动删除这个key就好了,也就不存在死锁了

分片集群结构:

在这里插入图片描述

主从哨兵可以解决高可用,高并发读写的问题。

  1. 海量数据存储的问题
  2. 高并发写的问题

使用分片集群可以解决上诉问题,分片集群特征:

  1. 集群中由多个master,每个master保存不同的数据
  2. 每个master都可以由多个slave节点
  3. master之间通过ping检查彼此的健康状态
  4. master之间通过ping检测彼此的健康状态

散列插槽:

redis上的会把每个master节点映射到一个大小为0~8192*2大小的slot上,通过对key的有效部分(有’{}'就算{}内的部分为有效部分,没有{}就是全部)做crc16冗余码校验得到一个hash值, 在对slot的大小取余就是slot的实际物理地址值。访问不同的节点会转发到相应的slot上

场景题:如何让同一类商品存放在同一个slot下?这样做的好处是什么?
让商品名的有效部分相同,如:{GOODS}box,{GOODS}desk,{GOODS}chair,这样这三个商品就存入在同一slot下,由于没有再进行节点的重定向,访问的速度就会大大加快,性能会提高。

插槽slot是可以进行迁移的,但是为什么要进行插槽迁移?

  1. 负载均衡
    当某个节点的槽位数量过多,导致其负载过高时,可以将部分槽位转移到其他节点,实现集群的负载均衡。
    这可以提高集群的性能和吞吐量,确保服务的高可用。
  2. 容灾与高可用
    当某个节点出现故障时,其持有的槽位需要转移到其他正常节点上,以实现服务的快速 failover,保证高可用。
  3. 集群扩容
    当需要向集群添加新节点时,可以从其他节点转移部分槽位到新节点,实现集群的负载再平衡和扩容。
  4. 节点下线
    当需要停止某个节点时,需要首先将其持有的全部槽位转移到其他节点,然后可以安全下线这个节点。

插槽的slot值任然可能带来hash冲突,但是该值只表示,原值一个存放在该插槽的上

集群脑裂

  1. 由于网络传输时的抖动可能会导致master的网段产生变化,此时哨兵认为master挂了,但是master没挂,可能会导致让slave提升master。

1.1 照成的影响 :
[当原主库并没有真的发生故障(例如主库进程挂掉),而是由于某些原因无法处理请求,也没有响应哨兵的心跳,才被哨兵错误地判断为客观下线的。结果,在被判断下线之后,原主库又重新开始处理请求了,而此时,哨兵还没有完成主从切换,客户端仍然可以和原主库通信;

如果客户端还在基于原来的主库继续写入数据,那么新的主库将无法同步这些数据,当网络问题解决之后,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。]

简单概括影响为:在哨兵未完成主从替换的情况下,原master恢复上线,可以继续对处理请求。但是如果在完成主从切换之后,会将原来的master转为slave,原来的master在为切换主从关系之间收到的数据就会丢失。

解决方法:

min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量,即至少要保证N个从库能进行数据同步;

min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

设置这两个配置项主要是解决脑裂问题,如果不满足这两项,客户端传过来的数据直接拒收。不进行同步等待master的恢复。

  1. 同步的异步丢数据情况:
    如果master想要和slave同步数据,并且防止脑裂的情况发生,可以设置 min-slaves-max-lag,假设设置为10ms,那么在此期间slave与master的数据差不可用超过10ms的数据,如果超过了就不执行同步。

Redis过期策略与淘汰机制

  1. 淘汰机制:redis中的内存达到max-menmery(一般为人为设置的物理内存的60-70%), redis就会把数据进行淘汰。
    有多种策略:LRU TTL

  2. 过期机制:当你设置redis的key已经达到过期时间之后,redis会如何处理过期key对应的value

2.1 惰性删除:假设这个key真的过期了,假如再去获取这个key,会判断你是否真的过期了,他会删除这个key在返回一个null,简单说就是,要用的时候判断是否为空,空的话就给你删了。

定期删除:redis回去定期遍历他16个库,每个库遍历的时间是有限的,所有库遍历总和的时间也是一定的,而且比所有单个库之和的时间要短。
条件:

  1. 没有超过1/4的时间
  2. 超过了这个库的遍历时间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值