redis基础

java开发面试整理。来源:https://github.com/Snailclimb/JavaGuide

整理自:https://github.com/Snailclimb/JavaGuide,Guide哥

简介redis

“在内存中的数据库”,redis可以做缓存、分布式锁、消息队列。redis支持事务、持久化、Lua脚本、多集群等。

redis和memcached的区别?

  1. redis支持的数据类型更加丰富,除了k-v键值对,还有list、set、zset、hash等数据结构的存储。memcached目前只支持简单的k-v键值对。
  2. redis支持数据的持久化,memcached目前不支持。
  3. redis有灾难恢复机制,因为其可以持久化数据。
  4. redis用完内存之后,可以将暂时不用的数据放到磁盘上,而memcached在服务器内存使用完之后,就会直接报异常。
  5. memcached没有原生的集群模式,而是依靠客户端来实现集群中的分片写入。redis支持cluster模式集群。
  6. Memcached 是多线程的非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )???
  7. redis支持发布-订阅模型、Lua脚本、事务等。而memcached不支持,并且redis支持更多的编程语言。
  8. 对于过期数据的删除,memcached使用的是惰性删除,而redis同时使用惰性删除与定期删除。

解释

  1. 惰性删除:当访问一个键时,程序会对这个键进行检查,如果这个键已经过期,则将其删除。

  2. 定期删除:程序会在后台定期扫描过期的键,并将它们删除。

  3. 同步与异步,阻塞与非阻塞:实际上同步与异步是针对应用程序与内核的交互而言的。同步过程中进程触发IO操作并等待(也就是我们说的阻塞)或者轮询的去查看IO操作(也就是我们说的非阻塞)是否完成。 异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。

    同步有阻塞和非阻塞之分,异步没有,它一定是非阻塞的。

  4. IO复用模型:正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。

为什么要用redis,为什么使用缓存?

  1. 快,高性能:

    直接操作内存中的数据。

  2. 高并发:

    QPS(Query Per Second):服务器每秒可以执行的查询次数;一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。

redis常用的数据结构

  1. string,简单的k-v键值对类型。应用场景有:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
  2. list,是一个双向链表。应用场景:发布与订阅或者说消息队列、慢查询。
  3. hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
  4. set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
  5. 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。

redis的单线程模型

待续…..

redis为什么开始不适用多线程?

待续……

redis6.0为什么又加入多线程?

待续……

redis为什么要给数据设置过期时间?

不然内存爆炸。

redis是如何判断数据是否过期的?

Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。

redis如何删除数据?

常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):

  1. 惰性删除 :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除 : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采用的是 定期删除+惰性/懒汉式删除

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。

怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。

redis内存淘汰机制?

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

redis持久化机制?

Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。

快照(snapshotting)持久化(RDB):

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

1
2
3
4
5
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

AOF(append-only file)持久化:

与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

1
appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

1
2
3
appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

补充:

redis4.0之后开始支持RDB与AOF的混合持久化。

AOF重写是产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。

redis事务?

使用 MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令。

但是,Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: 1. 原子性2. 隔离性3. 持久性4. 一致性

  1. 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  3. 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
  4. 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;

Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。

缓存穿透?

大量请求的key不在缓存中,导致请求全部到了数据库一层。缓存形同虚设。

解决办法:

最基本就是前端做好参数校验。还有其他的两种方法:

  1. 缓存无效key:

    如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

    另外,这里多说一嘴,一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值

    如果用 Java 代码展示的话,差不多是下面这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public Object getObjectInclNullById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
    if (cacheValue == null) {
    // 从数据库中获取
    Object storageValue = storage.get(key);
    // 缓存空对象
    cache.set(key, storageValue);
    // 如果存储数据为空,需要设置一个过期时间(300秒)
    if (storageValue == null) {
    // 必须设置过期时间,否则有被攻击的风险
    cache.expire(key, 60 * 5);
    }
    return storageValue;
    }
    return cacheValue;
    }
  2. 布隆过滤器:

    布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。

    具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

    但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

    为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!

    我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:

    1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
    2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

    我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

    1. 对给定元素再次进行相同的哈希计算;
    2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

    然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)

    更多关于布隆过滤器的内容可以看我的这篇原创:《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。

缓存雪崩?

缓存在同一时间,大面积停止服务,那么所有的请求都会到数据库。之前的缓存穿透是缓存没失效,注意区别。

举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。

解决:

针对redis服务不可用:使用redis集群;做服务限流。

针对热点缓存失效的情况:为热点设置不同的失效时间;缓存永不失效。

如何保存缓存与数据库的一致性?

旁路缓存模式。Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。

  1. 可以将缓存的失效时间变短。(但对先缓存,后数据库场景不一定适用)

  2. 如果修改DB之后,出现缓存失效怎么办?可以增加cache更新重试机制。隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。

待续……

HashMap与HashSet的区别?

  1. 实现的接口不同,一个是Map,一个是Set
  2. HashMap对键值(key)做了一一对应的关系,不允许键值重复。存储的是k-v键值对模型。
  3. HashSet中的数据是以哈希表的形式存放的,里面的不能包含重复数据。Set接口是一种一个不包含重复元素的 collection。存储的是对象。
  4. hashMap使用键值来计算HashCode,而HashSet使用对象来计算HashCode。但是可能出现对于两个对象来说,hashcoode值相等。
  5. hashmap比较快(因为使用唯一的键值来获取对象)。

Set集合与List集合?

  1. list集合以特定次序来持有元素,可能有重复元素。集合内部有序。
  2. set集合,hashset集合比较两个对象是否相等,首先看hashcode,再看equals方法。set集合中不允许用相等的对象(不仅仅hashcode不同,equals比较也要不同)

fork了别人的项目,怎么实现同步更新?

1
2
3
git remote add upstream https://github.com/Snailclimb/JavaGuide.git

git pull upstream master

解决fatal: unable to access 'https://github.com/.../.git': Could not resolve host: github.com:

1
2
3
git config --global --unset http.proxy 

git config --global --unset https.proxy
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信