大厂面试--redis

8大类型

  1. String(字符类型)
  2. Hash(散列类型)
  3. List(列表类型)
  4. Set(集合类型)
  5. SortedSet(有序集合类型,简称zset)
  6. Bitmap(位图)
  7. HyperLoglog(统计)
  8. GEO(地理)

命令不区分大小写,而key是区分大小写的
help @类型名词

string

命令:设置单个键值 set 获取单个get
同时设置多个键值mset 同时获取多个mget
在这里插入图片描述

数值增减

递增数字 ->  INCR key   
增加指定的整数  -> INCRBY key increment
递减数值 -> DECR key
减少指定的整数 -> DECRBY key decrement

获取字符串长度 STRLEN key

分布式锁

setnx key value

set key value[ EX seconds][ PX milliseconds][ NX XX]

在这里插入图片描述
设置键值k1 v1在10秒后过期

TTL key:返回key剩余的过期时间
在这里插入图片描述
应用场景

商品编号、订单号采用INCR命令生成
是否喜欢的文章不
底层实现:
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

hash

Map<String, Map<0bject , Object>>
一次设置一个字段值 HSET key field value
一次获取一个字段值 HGET key field
一次设置多个字段值 HMSET key field value [field value …]
一次获取多个字段值 HMGET key field [field …
获取所有字段值 hgetall key
获取某个key内的全部数量 hlen
删除一个key hdel
在这里插入图片描述
应用场景:购物车早期,当前小中厂可用
底层实现:
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

list:

向列表左边添加元素 -> LPUSH key value [value …]
向列表右边添加元素 -> RPUSH key value [value …]
查看列表 -> LRANGE key start stop
获取列表中元素的个数 -> LLEN key
应用场景 -> 微信文章订阅公众号

在这里插入图片描述
在这里插入图片描述
底层实现:
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

set:

添加元素 -> SADD key member [member …]
删除元素 -> SREM key member [member …]
获取集合中的所有元素 -> SMEMBERS key
判断元素是否在集合中 -> SISMEMBER key member
获取集合中的元素个数 -> SCARD key
从集合中随机弹出一个元素,元素不删除 -> SRANDMEMBER key [数字]
从集合中随机弹出一个元素,出一个删一个SPOP key[数字]

集合的差集运算A-B:属于A但不属于B的元素构成的集合 -> SDIFF key [key …]
集合的交集运算AnB:属于A同时也属于B的共同拥有的元素构成的集合 -> SINTER key [key …]
集合的并集运算A∪B:属于A或者属于B的元素合并后的集合 -> SUNION key [key…]

应用场景

微信抽奖小程序
微信朋友圈点赞
微博好友关注社交关系
QQ内推可能认识的人

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
底层实现:
Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

zset

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
底层实现:
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

redis过期策略

定时删除
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。

立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。。。。。。。

这会产生大量的性能消耗,同时也会影响数据的读取操作。

总结:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)

惰性删除

数据到达过期时间,不做处理。等下次访问该数据时,
如果未过期,返回数据;
发现已过期,删除,返回不存在。

惰性删除策略的缺点是,它对内存是最不友好的。
如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏-无用的垃圾数据占用了大量的内存,而服务器却不会
自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。

总结:对memory不友好, 用存储空间换取处理器性能(拿空间换时间)

定期删除

定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行-次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度。

特点1: CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

总结:周期性抽查存储空间( 随机抽查,重点抽查)

举例:

redis默认每个100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis 直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。

1定期删除时,从来没有被抽查到

2惰性删除时,也从来没有被点中使用过

.上述2步骤======>大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽

内存淘汰策略:八种

noeviction:不会删除任何key,直接报错
allkeys-lru:对所有key使用LRU算法(移除最近最少使用的)进行删除
volatile-lru:对所有设置了过期时间的key使用LRU算法(移除最近最少使用的)进行删除
allkeys-random:对所有key随机删除
volatle-random:对所有设置了过期时间的key随机删除
volatile-ttl:删除马上要过期的key
allkeys-lfu:对所有key使用LFU算法(最近最不常用)进行删除
volatile-lfu:对所有设置了过期时间的key使用LFU算法(最近最不常用)进行删除
(LRU是最近最少使用页面置换算法(Least Recently Used),也就是首先淘汰最长时间未被使用的页面!LFU是最近最不常用页面置换算法(Least Frequently Used),也就是淘汰一定时期内被访问次数最少的页!)

LRU

是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。

运用你所掌握的数据结构,设计和实现一个LRU (最近最少使用)缓存机制。它应该支持以下操作:获取数据get和写入数据put.

获取数据get (key) - 如果关键字(key)存在于缓存中,则获取关键字的值(总是正数)。否则返
回-1.

写入数据put (key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

LRU的算法核心是哈希链表

本质就是HashMap+DoubleLinkedList,时间复杂度是0(1),哈希表+双尚链表的结合体.

package com.company;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
//LRU算法


public class LRUCacheDemo<K,V> extends LinkedHashMap<K,V> {
    /**
     * true访问顺序
     * [1, 2, 3]
     * [2, 3, 4]
     *
     * [2, 4, 3]
     * [2, 4, 3]
     *
     * [4, 3, 5]
     *
     * false插入顺序
     * [1, 2, 3]
     * [2, 3, 4]
     *
     * [2, 3, 4]
     * [2, 3, 4]
     *
     * [3, 4, 5]
     *
     * @param <K>
     * @param <V>
     */
   /* private int capacity;//缓存坑位
    
    accessOrder – the ordering mode -
     true for access-order,
     false for insertion-order
   */
     
/*
    public LRUCacheDemo(int capacity) {
        super(capacity,0.75F,true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size() > capacity;
    }

    public static void main(String[] args) {
        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
        lruCacheDemo.put(1,"a");
        lruCacheDemo.put(2,"b");
        lruCacheDemo.put(3,"c");
        System.out.println(lruCacheDemo.keySet());

        lruCacheDemo.put(4,"v");

        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(3,"v");

        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(3,"v");

        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(5,"v");

        System.out.println(lruCacheDemo.keySet());
    }*/

	//手写LRU算法
	
    //map负责查找,构建又给虚拟的双向链表,它里面安装的就是一个个Node节点,作为数据载体。

    //1.构造一个Node节点,作为数据载体
    class Node<K,V>{
        K key;
        V value;
        Node<K,V> prev;
        Node<K,V> next;

        public Node(){
            this.prev = this.next = null;
        }

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.prev = this.next =null;
        }

    }

    //2.构建一个虚拟的双向链表,里面放的是我们的Node。
    class DoubleLinkedList<K,V>{
        Node<K,V> head;
        Node<K,V> tail;

        //2.1构造方法
        public DoubleLinkedList() {
            head = new Node<>();
            tail = new Node<>();
            head.next = tail;
            tail.prev = head;
        }

        //2.2添加到头
        public void addHead(Node<K, V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        //2.3删除节点
        public void removeNode(Node<K, V> node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }

        //2.4获得最后一个节点
        public Node getLast(){
            return tail.prev;
        }
    }

    private int cacheSize;
    Map<Integer,Node<Integer,Integer>> map;
    DoubleLinkedList<Integer,Integer> doubleLinkedList;

    public LRUCacheDemo(int cacheSize){
        this.cacheSize = cacheSize;//坑位
        map = new HashMap<>();//查找
        doubleLinkedList = new DoubleLinkedList<>();
    }

    public int get(int key){
        if (!map.containsKey(key)){
            return -1;
        }

        Node<Integer,Integer> node = map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);

        return node.value;
    }

    //saveOrUpdate method
    public void put(int key,int value){
        if (map.containsKey(key)){
            Node<Integer,Integer> node = map.get(key);
            node.value = value;
            map.put(key,node);

            doubleLinkedList.removeNode(node);
            doubleLinkedList.addHead(node);
        }else {
            if (map.size() == cacheSize){//坑位满了
                Node<Integer,Integer> lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.removeNode(lastNode);
            }
            //才是新增
            Node<Integer,Integer> newNode = new Node<>(key,value);
            map.put(key,newNode);
            doubleLinkedList.addHead(newNode);
        }
    }

    public static void main(String[] args) {

        /**
         * 结果与linkedlist的false结果一样
         */
        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
        lruCacheDemo.put(1,1);
        lruCacheDemo.put(2,2);
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(4,1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(3,1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(3,1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(3,1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(5,1);
        System.out.println(lruCacheDemo.map.keySet());
        
    }
}

Redis事务
是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
为什么要做成事务
想想一个场景:有很多人有你的账户,同时去参加双十一抢购

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

Redis事务三特性
单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

Redis 提供了2个不同形式的持久化方式。
RDB(Redis DataBase)
AOF(Append Of File)
持久化
Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。

RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。

AOF 对日志文件的写入操作使用的追加模式,有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。

Redis是怎么持久化的?服务主从数据怎么交互的?
RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

缓存穿透
缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。

解决的办法如下。

对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。
缓存击穿
缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。

解决这个问题有如下办法。

可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。
缓存雪崩
缓存雪崩,产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。

解决方法:

使用快速失败的熔断策略,减少 DB 瞬间压力;
使用主从模式和集群模式来尽量保证缓存服务的高可用。
实际场景中,这两种方法会结合使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值