Redis 相关学习笔记

2 篇文章 0 订阅
1 篇文章 0 订阅

redis基本数据结构

  • string -> 通过动态字符串sds实现,set nx
  • hash ->
    1、 当满足hash对象小于512,且value长度小于64byte时使用ziplist实现;
    2、 当不满足时使用hashTable + dict 实现,会存在两个hash表一个存储数据一个为空,为空的为扩容做准备,触发扩容条件 -> 当hash碰撞时数组下的链表长度超过扩容因子的时候,此时会进行扩容rehash
    可以实现商品购物车
  • list -> 有序的列表,元素允许重复,最大数据量存储大约40亿数量,通过quickList 双向链表实现,只有一个指向头的指针与指向尾的指针(可以理解为一个数组加链表的方式实现);
    实现消息队列、文章列表、评论列表、公告列表 , 可以当作分布式中的对列 通过blpop 阻塞出队
  • set -> 无序集合,通过hashtable实现
    用于抽奖、点赞、签到、打卡、用户记录、商品标签、用户画像、用户关注、推荐模型,sadd tags 可以取差集sdiff、交sinter、并sunion
  • zset -> 有序set集合,通过skiplist + dict 实现 ,skiplist -> skip变相的实现了二分查找法,通过随机数来定一个链表的向上层数如3,然后在节点中随机一个小于3的高度,在同一高度层建立指针链接;avl树、红黑树也可以实现二分查找 ,但是skiplist比较简洁;
    用于学生成绩、热搜排行榜等

在这里插入图片描述

高级用法

  • bitmap -> 位图,可通过getbit去实现布隆过滤器等,可用于缓存穿透时的预防
    布隆过滤器原理, 在一个很长的位数组中,可以通过hash + 位移的方式去映射成长度相同且分布均匀的实现,映射到的位置内容标志设置成1,但是hash存在hash碰撞的问题(如下图中有指向同一位置的点),可以通过加长为数组的长度或hash的次数来减少hash碰撞的概率,所以下图中通过3次hash来判断每一次是否都为1来判断是否存在(但是这种情况存在误差,因为hash碰撞),但是如果其中有一个为0(即不存在是准确的),原理大致如此。

在这里插入图片描述

BloomFilterDemo
package com.util;
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.text.NumberFormat;
import java.util.*;
import java.util.stream.IntStream;
/**
 * @description: BloomFilterDemo
 * @menu: BloomFilterDemo
 * @author: lld
 * @date: 2021/8/26 21:03
 */
public class BloomFilterDemo {
    private static final int insertions = 1000000;
    public static void main(String[] args) {
        //初始化一个存储string的布隆过滤器,初始化大小100w
        BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions);
        //存放实际key,判断keu是否存在
        Set<String> set = new HashSet<>(insertions);
        //存放实际的key,可以去除使用
        List<String> lists = new ArrayList<>(insertions);
        //向三个容器中初始化胡数据
        IntStream.rangeClosed(0,insertions).forEach(e -> {
            String uuid = UUID.randomUUID().toString();
            bf.put(uuid);
            set.add(uuid);
            lists.add(uuid);
        });
        //正确率
        int right = 0;
        //错误率
        int wrong = 0;
        for (int i = 0; i < 10000; i++) {
            //可以被100整除时,取一个存在的数值,否则随机生成
            String data = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();
            if(bf.mightContain(data)){
                if(set.contains(data)){
                    right++;
                    continue;
                }
                wrong++;
            }
        }
        NumberFormat percentFormat = NumberFormat.getPercentInstance();
        //最大小数位置
        percentFormat.setMaximumFractionDigits(2);
        float percent = (float) wrong / 9900;
        float bingo = (float) (9900 - wrong) / 9900;
        System.out.println("right -> "+right);
        System.out.println("wrong -> "+wrong);
        System.out.println("命中 -> " + percentFormat.format(bingo));
        System.out.println("误判 -> " + percentFormat.format(percent));
    }
}
  • Geospatial -> 地图 可以通过geoadd,添加经纬度,可以计算出范围内所有的地点集合
  • hyperLoglogs -> 高级不精确去重的数据结构(一般是超过一百个就开始不准确了),但是占用空间小,适用于一个页面的热点统计
  • streams -> 处理流式数据
  • pub/sub -> 发布订阅模型,缺点:并发性能不好、延迟性、不支持持久化,但是可以用于哨兵之间的互相监控
  • pipeline 批量执行一组指令,一次性返回全部结果,减少频繁的请求应当
  • lua 脚本 -> 实现真正的原子性,实现秒杀场景, 保证串行执行命令,且命令不会失效不会回滚,结合set nx + exprie(设置时间过期)实现分布式锁

redis 为何这么快

  • 最重要的是因为是纯内存KV
  • 处理请求单线程,避免了多线程的创建与销毁、线程竞争、线程上下文的切换消耗
  • 同步非阻塞IO多路复用,NIO (non-blocking I/O): 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。(serverSocketchannel一个支持IO重用,所以一个线程就可以处理多个客户端的连接),多路复用机制;具体可以参考以下java远程通信之socket、selector、select/poll/epoll/BIO/NIO/AIO的理解
  • 当然单线程并不是指 -> redis就是单线程的,而是指去处理消息时用的单线程,最新的版本中允许多线程去处理,IO多线程默认是关闭的 -> io-threads 默认为1 -> 多个时为为多线程

内存回收机制

  • 立即过期 -> 每一个key都需要一个定时器去监听,这样对CPU的资源消耗太大,代价太大,基本上不适用
  • 惰性过期 -> 只有再次过期时才会删除
  • 定性过期 -> 会有一个线程定时跑数据
  • 正常的使用时惰性与定性过期结合使用

若内存满了执行的淘汰策略,有8种

  • volatile过期key的淘汰策略 LRU(最近最少使用) 、LFU(最近使用频类最少)、ttl淘汰要过期的数据、random随机淘汰
  • allkeys全量key的淘汰策略 LRU(最近最少使用) 、LFU(最近使用频类最少)、random随机淘汰、no-enviction(禁止驱逐数据,为默认策略,内存不足时禁止写入)

LRU 最近最少使用可以通过LinkedHashMap实现,将最近使用的放在头部 最老访问的放在尾部 继承了hashMap中的removeEldestEntry 删除最近最少使用的数据,

这里放上LRUDemo
package com.util;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.IntStream;
/**
 * @description: LRUDemo
 * @menu: LRUDemo
 * @author: lld
 * @date: 2021/8/26 23:20
 */
public class LRUDemo {
    static class LRUCache<K, V> extends LinkedHashMap<K, V>{
        private final int CACHE_SIZE;
        public LRUCache(int cache_size) {
            //true -> 标识让LinkedHashMap按照访问顺序进行排序,最近的放在头部,最老访问的放在尾部
            super((int) Math.ceil(cache_size / 0.75) + 1, 0.75f, true);
            CACHE_SIZE = cache_size;
        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            //当 map中的数量大于指定的缓存数量时,自动删除最老的数据
            return size() > CACHE_SIZE;
        }
        public static void main(String[] args) {
            //初始化,设置缓存个数10个
            LRUDemo.LRUCache<Integer, Integer> lruCache = new LRUCache<>(10);
            IntStream.rangeClosed(0, 20).forEach(i -> {
                lruCache.put(i, i);
            });
            //打断点后可以发现数据0-10的都清楚了
            System.out.println(lruCache.get(0));
        }
    }
}

Redis 持久化的方式,一般时RBD与AOF结合使用

  • RDB -> 内存中的数据集以快照的形式写入磁盘,通过fork子进程执行,采用二进制压缩存储, 将数据保存在单一文件中,适合用来备灾,若快照保存玩宕机,这段时间的数据就会丢失,且有可能会导致短时间不可用
  • AOF -> 以文本日志的形式记录redis 每一个操作, 为追加写入模式,有灵活的同步策略、支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据 AOF要大于RDB ,运行效率上会低于RDB,everysec 最多丢失一秒钟 默认策略,bgrewriteaof -> 压缩文件大小 会自动触发

主从复制 + sentinel(哨兵),读写分离

  • 每次有新的节点加入时,会进行一次RDB全量复制,后续的有AOF增量复制,通过偏移量来保证是否一致
  • 通过sentinel哨兵实现高可用(路由的作用),需要奇数个哨兵来完成(监控每个主从,且相互监控),因为是通过raft算法选举领导者,且节点之间通过gossip协议完成
  • 选举策略 -> 1、每个节点可以设置priority值,值越小优先;2、相同情况下则看谁复制的数据最多;3、同等情况下,则看进程的id,最小的有限度越高
  • 不足的地方是不能水平扩展

如何实现分布式寻址,可以从三个层面上考虑客户端、代理层、服务端

  • 客户端 -> 如jedis、redission、lettuce,jedis 通过hash一致性算法完成对数据的分布;首先会构造一个hash环 0 - 2^32 - 1 ,对服务器节点的名字或ip进行hash取模就可以知道对应的位置 ,通过虚拟节点解决分布不均匀的问题,shardedJedis分片原理用红黑树构建hash环 通过红黑树计算下一个虚拟节点;

在这里插入图片描述

  • 代理层 -> twemproxy、codis 基本不使用
  • 服务层 -> CRC32算法

Redis提供的redis Cluster架构实现了水平方向的扩容,通过固定的hash slot (16384个),然后通过CRC16算法对16384取模计算出存储的位置

  • 无中心架构
  • 动态数据分布,新的节点进入则重新分片(slot)
  • 可扩展性最多可以到1000个节点
  • 高可用 自动选举 ,通过增加从节点做standby数据副本,实现故障转移自动failover,节点之间通过gossip协议交换你状态信息,用投票机制完成从到主的角色提升
  • 运维成本低

数据更新或删除时如何保证数据一致性

  • 数据更新,在缓存中肯定是先执行删除如何插入
  • 1、先更新数据库,如何删除缓存,会出现删除失败的情况( 可以通过重试机制去删除(如使用mq),但是这种情况对代码入侵比较多不太友好 )
  • 2、先删除缓存再更新数据库,会出现线程ABA的问题(1、串行处理,性能下降2、延迟双删策略(线程睡眠然后再删除))

最后缓存雪崩、缓存击穿、缓存穿透问题

  • 缓存雪崩 -> 大批量缓存数据同时过期,导致大量的请求同时到数据库,数据库中来不及反应直接挂了
      	解决方案:1、可以设置热点数据永不过期;2、可以加上互斥锁,可以考虑分布式锁实现(redission、zookeeper等);3、再设置缓存过期时间时可以加上随机时间错开;3、预更新
    
  • 缓存击穿 -> 有一个热点缓存数据突然失效,导致大量针对这个数据的请求直接到数据库中导致数据库挂了
        解决方案:1、加上互斥锁更新;2、设置热点永不过期;3、可以使用随机退避的方式,失效时随机sleep一个很短的时间,再次查询,如果失败则执行更新
    
  • 缓存穿透 -> 一般存在于恶意攻击,恶意攻击不存在的用户ID等
      解决方案:1、如为不存在的用户则在缓存中保存一个空对象标记,缺点会出现大量无用的数据 ;2、布隆过滤器实现;3、通过nginx,nginx可以过滤恶意攻击的ip,进行屏蔽;
    

最后的最后,当然上面的办法可以避免一些缓存问题,但是也是有可能出现意外,可以考虑做限流、熔断、降级等策略防止真的有意外发生,结束了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值