985研一学习日记 - 2024.11.02

一个人内耗,说明他活在过去;一个人焦虑,说明他活在未来。只有当一个人平静时,他才活在现在。

日常

1、起床6:00√

2、健身1.5h

今天练了胸和背,然后跑了半小时步,明天练二头和肩部
今天又放纵了,明天少吃点!!!

3、LeetCode刷了3题

  1. 最小生成树MST:Prim、Kruskal
    1. 使用并查集和Kruskal算法来构建最小生成树先对所有的边根据权值进行从小到大排序,同时构造并查集来判断两个顶点是否在同一个集合中,刚开始初始化每个顶点均是单独集合,然后遍历所有以及排序后的边先判断两个顶点是否在同一个集合中,如果不在则将当前边加入最小生成树,并使得这两个顶点合并,然后继续遍历边,直到遍历结束,如果所有的顶点都在一个集合中,则说明存在最小生成树,必须先将所有的边根据权值从小到大排序
    2. Prim算法:从顶点开始更新,每次选择一个顶点加如最小生成树,并同时更新其余顶点到最小生成树的minDist初始化全为Max,因为刚开始所有顶点都不在MST中),关键是更新minDist,方法是当加入一个新的顶点时,遍历该顶点的所有边,并同时对minDist进行更新,如果某个边的权值小于对于结点到最小生成树的距离,则更新这个顶点的minDistminDist是一个数组,记录不在最小生成树中的顶点到最小生成树的距离,每次将距离最近的顶点加入最小生成树,并同时根据该顶点的边更新其余顶点的最小距离1、先选择距离生成树最近的顶点 2、将顶点加入生成树 3、根据加入顶点相连的边更新minDist数组
    3. Kruskal算法适用于顶点多边少的稀疏图O( ElogE )Prim算法适用于顶点少边多的稠密图O( V^2 )Kruskal算法是和并查集(判断两个顶点是否在集合中,将两个集合合并)配合使用,因为从边开始,要判断两边的顶点是否在同一个集合中,不在同一个集合时才将边加入;Prim算法加入一个新的顶点后要更新minDist,只需遍历与该顶点相连的所有边即可更新
    4. Prim算法根据顶点开始,适用于顶点少边多的稠密图,时间复杂度为O(v2)Kruskal算法根据边开始,适用于边少顶点多的稀疏图,时间复杂度为O(ElogE)
    5. 要打印最小生成树的每条边时,此时要把每条边均记录下来,然后就可以构造最小生成树,只需将每条边记录下来
  2. 拓扑排序
    1. 判断是否存在拓扑排序时要先记录所有顶点的入度,然后每次找入度为0的顶点,删除与该顶点所有相连的边,更新删除边后顶点的入度,然后删除该顶点,如果**最后所有的顶点都被删除,则说明存在拓扑排序
    2. 在删除节点时将其保存下来就可以得到拓扑排序,拓扑排序就是每次找到入度为0的顶点,然后删除顶点的所有边并修改相连顶点的入度,删除删除该顶点,如果最后所有的顶点都被删除了,则说明该图存在拓扑排序,如果最后存在顶点未被删除,则说明不存在拓扑排序,则一定存在环
    3. 如果存在拓扑排序则一定不存在环存在环一定不存在拓扑排序,故可以使用拓扑排序判断是否有环;求拓扑排序时一定要记录每个顶点的入度,每次将入度为0的顶点加入队列,当队列不为空时取出一个顶点加入拓扑排序,然后遍历所有相连的边,对顶点修改入度,并判断如果入度为0则加入队列
    4. 使用队列(使用ArrayList和head和rear实现)存放所有入度为0的顶点,当队列不为空时,取出一个顶点删除所有的边后更新入度,如果入度为0则加入队列,最后删除该节点,并使得节点个数-1,最后如果节点个数=0,则说明所有的顶点均被删除,此时说明存在拓扑排序
  3. 邻接表存储有向图
    1. 使用List<Integer>[] list = new List<>[n] 来创建一个邻接表,并对每个List进行初始化 list[i] = new ArrayList<>() ,表示每个顶点都有一个存放所有相连顶点的集合,然后遍历所有的有向边,并将边加入到邻接表中
    2. 此时就可以通过 List[i] 快速得到与定点 i 相连的所有顶点
  4. 课程表II
    1. 课程表问题可以转换为有向图是否存在拓扑序列问题,两个课程之间的依赖关系可以看做有向图的一条边,只有队头完成后才可到达队尾,即符合拓扑排序,必须存在拓扑排序才可说明有效,必须先根据边创建邻接表
    2. 先使用邻接表存储有向图(使用List<Interger>[])表示邻接表,并对每个顶点可以达到的边的集合进行初始化,并创建一个int[] entry来记录每个顶点的入度,然后遍历所有的有向边,将边添加到邻接表中并更新顶点的入度,要注意课程表的依赖关系确定有向边的方向
    3. 此时遍历所有入度为0的顶点将其加入到队列queue中(入队时要rear++),然后当队列不为空(head != rear)时依次取出(先用pos标记当前层队列)当前队列中所有入度为0的顶点加入拓扑排序中,然后删除邻接表中所有相邻有向边,并更新相连顶点的入度,如果入度为0,则加入队列,直到队列为空,此时说明不存咋入度为0的顶点,然后判断所有顶点的入度是否还存在>0,如果存在说明还存在边,即存在环,如果不存咋,则说明存在拓扑排序,输入即可
    4. 拓扑排序的关键就是遍历所有入度为0的顶点,删除与其相连的所有边,并更新相连顶点的入度

4、复盘23:00√

不复盘等于白学!!!


学习和感想

Redis学习

1. **Redlock算法(MultiLock)**和底层源码分析

  1. 自研一个分布式锁的要点:Lock规范、独占性、高可用、防死锁、不乱抢、可重入
    1. 按照JUC的Lock接口规范编写
    2. tryLock()加锁关键逻辑:加锁定期(当没有hash类型的key或者存在当前线程分布式锁时使用lua脚本实现原子操作进行HINCRBY加锁并设置过期时间)、自旋延迟重试(当加锁失败时必须延迟一段时间再重试,且不要递归),一定先判断是否有key时再加锁
    3. unlock()解锁关键逻辑:当不存在锁或者不是当前锁时直接报错,否则使用HINCRBY -1 解锁,并当=0时删除当前锁,即删除hash类型的key(uuid只是标识不同锁的field,其值是加当前锁的个数),必须解自己加的锁,且必须加几次锁就解几次锁
    4. 工厂设计模式使用工厂设计模式使得同一个线程中申请的分布式锁唯一且不同微服务的不同(使用uuid区别)
    5. 自动续期不自动续期仍会出现超卖现象,加了自动续期可能会一直死锁,但当执行结束解锁后就不会自动续期,自动续期时必须先判断锁是否存在,不存在时则直接退出
    6. SETNX无法实现可重入性,因为无法对锁进行计数,故使用 HSET lock id count 来加锁并实现可重入性,实际上只是记录了一个当前锁的个数,仍要判断是否存在key时再加锁,当不存在锁或者存在锁且是当前锁时则可以加锁
    7. 自研的分布式锁,其所有的锁都存在到Redis中,但由于Redis主从复制以及集群的异步性,就会出现不安全,如果某一个加锁后更新master但还未同步时,master宕机,此时另一个加锁,则新的master仍会同意,导致同时两个同时访问Redis,造成不安全
  2. Redlock红锁算法:Redis提供的分布式锁实现算法,实现了更安全的DLM
    1. Redis提供了一个规范的分布式锁实现算法-Redlock红锁算法,实现了更安全的DLM(分布式锁管理器)
    2. 自研的分布式锁存在Redis缓存中,是不安全的,如果master宕机,就算有从机,但因为主从复制是异步的(Redis集群是AP的,不保证一致性),所以可能导致Redis分布式锁更新错误一个获得锁后还未同步就宕机了,就会导致另一个也可以获得锁![[Pasted image 20241101202908.png]]
  3. Redlock算法设计理念
    1. Redlock算法是Redis提供的分布式锁实现算法,实现了更安全的DLM(分布式锁管理器),为了解决自研分布式锁存放于master中由于主从复制的异步性造成的不安全
    2. Redlock基于多个master实例的分布式锁锁变量由多个master实例维护类似于集群,但不是集群,因为各个master节点完全独立不存在主从复制,各个master完全独立,不存在异步
    3. 设计理念:直接用多个相互独立的master存放锁,当加锁时,对每个master上的分布式锁均获得锁(且要设置超时时间,超时时间要小于失效时间,此时当一个master实例宕机时就可以快速去写一个master加锁),且必须大多数以上加锁成功才算成功,当加锁失败时,要对所有的master上的分布式锁进行解锁(防止某些master加上锁但客户端没有收到)就算没有获得锁也要进行解锁,以此来解决由于主从复制异步性导致的不安全,只有当大多数master实例上均获得了锁,并且获得锁的时间小于有效时间时才认为加锁成功![[Pasted image 20241101203846.png]]
    4. 解决方案:容错公式 N = 2X + 1 :其中X为容错机器数即最大可出错的机器数,N为最终部署机器数,即要确保不出错的机器数始终大于出错的机器数;如果希望X台机器出错后还能用,则**必须部署 N = 2X + 1 台机器
  4. Redlock落地实现:Redisson
    1. 官网
      1. Redisson是Redis提供的基于Redlock算法实现的更安全的DLM
    2. SpringBoot引入Redisson实现分布式锁
      1. 单机版
        1. 使用redisson分布式锁也会出现错误,因为有时候删除的不是自己的锁,故先判断是不是自己的锁再删除,使用redisson先判断是不是自己的锁再删除,会自动封装为原子操作
    3. Redisson源码解析
      1. 必须实现Lock规范,有加锁、解锁、可重入、续期功能
      2. 和自己实现的可重入分布式锁的源码类似,均实现了Lock接口规范,并重写相应的函数,加锁时先判断是否存在锁,不存在则直接加锁,如果存在则判断是不是自己的锁,如果是则计数+1实现可重入,使用lua脚本来实现原子性,加锁成功后开启一个看门狗来自动续期,默认超时时间是30s,当解锁时仍要先判断是不是自己的锁,如果不是则报错,只有是自己的锁时才会解锁,且在使用redisson分布式锁时,解锁之前也要使用redisson判断是不是存在锁并去是自己的锁,然后再解锁,当是自己的锁时,计数-1,然后判断计数是否为0,如果为0则要删除锁不为0则重置过期时间后直接结束
      3. 看门狗是定时检查redis分布式锁是否还存在,并不断延长锁的过期时间
      4. 在redisson中当获得锁后会自动设置一个监控线程看门狗,会定期对锁进行监控,如果还未释放且快过期了则对锁续期,看门狗默认30s监控一次
      5. 守护进程来续命:定期检查锁是否存在,存在则重新设置过期时间,以防止过期了还未执行完业务,使用看门狗,当获得锁后会给锁加一个看门狗,会另起一个定时任务,不断查询锁是否存在并进行续期
      6. 加锁源码使用lua脚本实现原子性和自动续期看门狗![[Pasted image 20241102123637.png]]![[Pasted image 20241102123926.png]]![[Pasted image 20241102124112.png]]
      7. 解锁源码,先判断是不是自己的锁,如果不是则返回nil,是自己的锁时先解一次锁,计数-1,然后判断计数是否为0,如果为0说明没有重入了,故要删除整个锁,如果不为0,则重置过期时间后返回
      8. 但对于redisson可能高并发出现错误,故在使用时,解锁之前使用redisson先判断一下是不是存在锁且是自己的锁,然后再使用redisson解锁
    4. 小总结
      1. redisson基于Redlock算法,防止出现单个redis故障造成的整个服务停止运行以及主从复制异步性造成互斥失效的问题 ![[Pasted image 20241102125357.png]]
  5. Redisdon多机实现
    1. 介绍
      1. redisson基于RedLock算法来实现更安全的DLM,其基于多个相互独立的master实例来存放分布式锁,只有当大多数实例均加锁成功时,才算加锁成功,一次来避免单机master因为主从复制的异步性而造成错误,且加锁失败时必须所有的master均解锁,为了保证正确性,必须使得正确的master个数大于错误的master个数
      2. 去官网
    2. 实现
      1. RedLock(已弃用)
        1. 通过redis通过的redisson分布式锁来实现多机,仍是先创建多个单机实例,然后使用redis通过的方法将多个RLock对象关联为一个RedLock,此时就实现了基于多个master的分布式锁
        2. 将多个RLock实例关联为一个RedLock,此时使用RedLock对象进行加锁,是基于多个master实例的分布式锁,且加锁时可以指定过期时间,而且可以设置指定的重试时间来进行加锁
      2. MultiLock多重锁
        1. 和RedLock一样,仍然是先将多个RLock(可能不是一个master下的RLock)关联为一个MultiLock多重锁,此时加锁时必须大多数锁都申请成功才可以加锁,且可以指定锁的时间,以及限制多少时间内去申请锁,解锁时对所有的master实例上的锁均要解锁
        2. 也就是创建多个相互独立的master实例,然后在Java客户端使用redisson先创建多个单机实例的Rlock分布式锁,然后根据这多个RLock创建一个MultiLock分布式锁,其基于多个master实例以防止单个节点故障以及主从复制异步性造成的问题,然后使用MultiLock就可以实现更加安全的DLM(分布式锁管理器),为了保证容错性更高,要遵循容错公式: N = 2X + 1,即实际部署的master实例个数 = 容忍最大故障树* 2 +1,保证正确的个数始终大于出错的个数
        3. 必须要在多个不同的master实例上获得RLock此时锁的名字可以相同,因为master实例不同,不可以同一个实例上获得三个不同的锁,因为此时仍是同一个master实例上,就无法解决单点故障问题,必须在多个master上获得RLock,再关联为一个MultiLock
        4. 当创建好锁后,会自动生成一个监控锁的看门狗,超时时间默认是30s,其会定时1/3秒去查看一下锁的状态如果未释放则对锁自动续期,然后继续监控
        5. 当使用MultiLock时,只有多个master实例均加锁成功后才会加锁成功,如果加锁失败,则所有的master实例都要进行解锁
  6. 小总结
    1. RedLock算法就是为了实现更加安全的DLM,其基于多个相互独立的master实例来获得分布式锁,可以保证单点故障后仍正常时间加锁解锁
    2. 具体实现是使用redis提供的Redission来创建MultiLock锁,其根据多个独立master实例上创建的RLock来创建
    3. Redis分布式锁用HSET实现,SETNX无法实现可重入性

2. Redis缓存过期淘汰策略

  1. 面试题
    1. Redis的缓存过期淘汰策略
    2. LRU是最近最久未使用LFU最近最少使用
  2. Redis内存满了怎么办
    1. 默认内存与配置
      1. 在redis.conf配置文件中对通过MAXMEMORY配置项设置最大内存,单位是BYTE字节,当未配置时默认最大内存为0(通过config get maxmemory得到配置项的值),不设置默认为0,表示不限制最大内存,在redis.conf配置文件中对maxmemory进行配置设置redis的最大内存,默认为0表示不进行限制,以BYTE为单位,推荐设置为3/4,且默认的缓存淘汰策略是NoEviction,即不清楚缓存,故当达到maxmemory时会报错OOM
      2. 一般最大内存推荐设置为物理最大内存的四分之三,当不配置默认为0表示不限制内存默认为0表示不限制内存
      3. 可以通过修改redsi.conf配置文件来修改maxmemory配置项,此时会永久生效(要重启服务端才会生效),也可以通过命令config set maxmemory xx来设置(可以多个参数),但只作用于本次客户端重启失效,单位是BYTE字节
      4. 通过 info memory 命令来查看redis内存占用情况
    2. 如果内存超过了设置的最大内存maxmemory时会报OOM错误不允许超过设置的最大内存,因为默认的缓存淘汰策略是Noeviction不进行清除,当达到最大时就不允许添加,会报错
  3. Redis中的数据是如何删除
    Redis的键过期后不一定要立即删除,会占用大量cpu时间,可以使用惰性删除
    1. 立即删除(用时间CPU换空间内存)
      1. 当键过期时立即删除,保证了内存数据的新鲜度,使得内存立即释放,但对cpu非常不友好,因为要时时刻刻进行删除,会对CPU造成额外的压力
    2. 惰性删除(用空间内存换时间CPU)
      1. 当键过期时不做删除,当**下次访问时如果访问时过期了再删除键,如果没过期则返回数据
      2. 但可能会造成内存泄漏,因为如果一个键过期了但一直没有被访问,则会一直占用内存空间而不被删除造成内存泄漏
      3. 在配置文件中对lazyfree-lazy-eviction(清除)设置为yes来开启惰性删除
      4. 惰性删除要在配置文件中进行配置开启 lazyfree配置项
    3. 定期删除:折中
      1. 定期抽查部分键判断是否过期,只对过期的键进行删除,不会判断所有的键是否过期,而是定期抽查一部分键,只删除定期随机抽查到且过期的键,仍会出现有的键过期后一直未被删除
      2. 关键是确定删除操作执行的时长和频率,时长过长频率过高会退化为立即删除,时长过短频率过低会退化为惰性删除
      3. 每隔一段时间随机抽查部分键,然后根据过期键的占比来执行过期键删除操作
      4. 当键过期时不会立即删除,也不会等下一次访问过期后再删除,而是隔一段时间后统一进行删除,不是全部检查,而是随机抽取key,看过期键的占比,如果过高则执行删除过期键操作
      5. 周期性的随机抽查存储空间,根据过期键占比来控制删除频度
    4. 漏洞:仍会内存泄漏
      1. 在定期删除(定期随机抽查部分键,只删除过期的键)时,可能有的键从来没有被抽查过,导致永远不会被删除
      2. 在惰性删除(过期后不会删除,而是下次访问时过期则删除)时,用空间换时间,但可能有的过期键永远不会被再次访问,则此时就不会被删除,造成内存泄漏
      3. 立即删除会大量占用CPU时间
  4. Redis缓存淘汰策略
    1. 配置文件
      1. Redis.conf配置文件中的MEMORY MANAGEMENT配置项
      2. 当Redis内存超过设置的最大内存MAXMemory时,会根据设置的一种缓存淘汰策略来释放内存,默认是Noeviction不进行缓存淘汰,此时当内存满了之后就会报错OOM
      3. 当Redis的缓存达到设置的最大内存后才会执行缓存淘汰,共8种,默认使用noeviction不淘汰此时任何增加内存均会导致报错OOM,因为超过了最大内存且不进行淘汰
      4. 共8种,分别是针对所有的键allkeys和只针对过期了的键volatile,使用LRU、LFU、RANDOM、NO和TTL
    2. LRU和LFU:均用到了局部性原理
      1. LRU是最近最久未使用将最近的时间内最久未使用的键淘汰,可以用一个队列实现,当某个键访问时加入队列,最后从队尾删除即可或者为每个键设置计数器,当某个键访问时,计数器置为0其余的键的计数器全部+1最后淘汰计数器最大的键
      2. LFU是最近最少使用:将最近一段时间内使用次数最少得键淘汰,可以为每个键设置计数器,当访问某个键则计数器+1,最后淘汰计数器最少的键
    3. 缓存淘汰策略(8种)
      1. 默认的缓存淘汰策略就是 noeviction(不淘汰,即不进行内存优化),所有当内存达到设置的最大内存时,任何增加内存命令都会OOM报错,因为默认设置的不对内存进行淘汰,达到最大后就不可以再增加
      2. allkeys:表示对所有的key都生效
      3. volatile:表示只对过期的键key生效
    4. 小总结
      1. 一共8中缓存淘汰策略,当缓存达到设置的最大内存时就会生效
      2. 2个维度:allkeys(对所有键生效)volatile(只对过期的键生效)
      3. 4个方面:lru、lfu、randomNo和ttl
    5. 工作中用哪种
      1. 不确定时推荐使用allkeys-lru(对所有的键根据最近最久未使用进行淘汰) ,如果所有key访问概率都差不多,使用RANDOM,如果所有的key都有过期时间,则推荐使用volatile-ttl删除马上要过期的key
      2. 通过在redis.conf配置文件中修改memory-management配置项来永久生效,要重启服务器,或者使用 config set allkeys-lru yes来开启,只当前有效
  5. 建议:避免存储bigkey开启惰性删除lazyfree-lazy-eviction = yes
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值