2020面试准备之Redis

关于缓存的介绍

缓存是一个数据模型对象,那么必然有它的一些特征:

命中率

命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。

最大元素(或最大空间)

缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。

清空策略

如上描述,缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提升命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提升命中率。常见的一般策略有:

  • FIFO(first in first out)

先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。

  • LFU(less frequently used)

最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。

  • LRU(least recently used)

最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。

除此之外,还有一些简单策略比如:

  • 根据过期时间判断,清理过期时间最长的元素;
  • 根据过期时间判断,清理最近要过期的元素;
  • 随机清理;
  • 根据关键字(或元素内容)长短清理等。

在目前的应用服务框架中,缓存分为local cache(本地缓存)和remote cache(分布式缓存):

  • 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是因为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
  • 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。

简单说说有哪些本地缓存解决方案?

先来聊聊本地缓存,这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。

1、 HashMapConcurrentHashMap

ConcurrentHashMap 可以看作是线程安全版本的 HashMap ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。

2、EhcacheGuava CacheSpring Cache这三者是使用的比较多的本地缓存框架。

Ehcache 是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现 ,Ehcache 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存。更多介绍推荐阅读:玩转EhCache之最简单的缓存框架

Guava CacheSpring Cache两者的话比较像。

Guava 相比于 Spring Cache 的话使用的更多一点, Guava Cache继承了ConcurrentHashMap的思路,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。

使用 Spring Cache 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。

3、Caffeine

Caffeine 是使用 Java8 对 Guava 缓存的重写版本 ,在 Spring Boot 2.0中将取代 Guava。如果出现 Caffeine,

CaffeineCacheManager 将会自动配置。推荐阅读:本地缓存解决方案-Caffeine Cache

为什么要有分布式缓存?/为什么不直接用本地缓存?

我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。

本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。

再来分析一下本地缓存的局限性:

  1. 本地缓存对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
  2. 容量跟随服务器限制明显。

分布式缓存优点:

缓存部署在一台单独的服务器上,与应用分离,多个应用共享缓存。 分布式缓存服务的性能、容量和提供的功能都要更加强大。

缺点:

分布式缓存需要引入额外的服务比如 Redis 或 Memcached,你需要单独保证 Redis 或 Memcached 服务的高可用。

为什么要用Redis缓存?

使用缓存主要是为了提升用户体验以及应对更多的用户,从程序设计而言是为了高性能和高并发。

高性能

假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。

这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。

不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4核8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到30w+(就单机redis的情况,redis 集群的话会更高)。

QPS(Query Per Second):服务器每秒可以执行的查询次数;

所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。

Redis 在多种数据库中的独特性

  • Redis 是基于内存的采用单线程模型的 KV 数据库,支持高并发。
  • 支持多种数据结构,操作简单。
  • 单线程模式,避免了不必要的上下文切换和竞争条件,不需要考虑锁的问题。
  • 使用多路 I/O 复用模型,非阻塞IO。
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

为什么 Redis 选择单线程?

官方解释:因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是 机器内存的大小 或者 网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

什么是Redis

Redis是一个开源的,基于内存的数据结构存储,可用作于数据库、缓存、消息中间件。

  • Redis基于内存,支持多种数据结构。
  • Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。

缺点:

  • 容量受到物理内存的限制;
  • 不具备自动容错和恢复功能;
  • 降低了系统的可用性;
  • Redis 较难支持在线扩容。

说一下 Redis 和 Memcached 的区别和共同点

Redis 因为功能更为强大,所以使用更为广泛,在选择分布式缓存方案时优先推荐 Redis。

共同点

  1. 都是基于内存的缓存。
  2. 都有过期策略。
  3. 两者的性能都非常高。

区别

  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
  3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  5. Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.
  6. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
  7. Redis 支持发布订阅模型、Lua脚本、事务等功能,而Memcached不支持。并且,Redis支持更多的编程语言。

Redis 常见数据结构以及使用场景分析

Redis 支持丰富的数据结构,常用的有 string、list、hash、set、sortset 这几种。 Redis 的存储是以key-value的形式的。Redis 中的 key 一定是字符串,value 可以是 string、list、hash、set、sortset 这几种常用的。

1.String

常用命令: set,get,decr,incr,mget 等。

String 数据结构是简单的 key-value 类型,value 其实不仅可以是 String,也可以是数字。 常规 key-value 缓存应用; 常规计数:微博数,粉丝数等。

2.Hash

常用命令: hget,hset,hgetall 等。

hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如

key=user
value={
  “id”: 1,
  “name”: “hresh”,
  “age”: 24,
  “location”: “中国”
}
3.List

常用命令: lpush,rpush,lpop,rpop,lrange 等

list 就是链表,Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

4.Set

常用命令: sadd,spop,smembers,sunion 等

Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。

当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:

sinterstore key1 key2 key3     将交集存在key1内
5.Sorted Set

常用命令: zadd,zrange,zrem,zcard 等

和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

扩展:

跳跃表

跳跃表(shiplist)是实现 sortset(有序集合)的底层数据结构之一!

它类似于 Java 中的 SortedSetHashMap 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 排序 的目的。

为什么使用跳跃表?

首先,因为 sortset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?

  1. 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 (下面详细说)
  2. 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;

关于跳跃表的创建与插入过程,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-457O0PkS-1603287177511)(https://camo.githubusercontent.com/c70eab4a050b19421f32b9e1a5ec982b96615f2b/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f373839363839302d316530363236633031336465303935652e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)]

推荐阅读: https://github.com/wmyskxz/MoreThanJava#part3-redis

Redis对象一些细节

  • (1:服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。

    • 比如我们的数据结构是 sortset,但你使用了 list 的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令。
  • (2:Redis 的对象系统带有引用计数实现的内存回收机制

    • 对象不再被使用的时候,对象所占用的内存会释放掉,
  • (3:Redis 会共享值为0到 9999 的字符串对象

  • (4:对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间

推荐阅读:《Redis设计与实现》

Redis中哈希表的特殊性

首先我们查看其结构如下:

img

我们可以发现,Redis 中有两个哈希表

  • ht[0]:用于存放真实key-vlaue数据
  • ht[1]:用于扩容(rehash)

Redis 中哈希算法和哈希冲突跟 Java 实现的差不多,它俩差异就是:

  • Redis 哈希冲突时:是将新节点添加在链表的表头
  • JDK1.8后,Java 在哈希冲突时:是将新的节点添加到链表的表尾

关于 Redis 是怎么 rehash 的,首先需要知道它不是一次性完成的,因为 Redis 是单线程的,所以它被称为渐进式 rehash。推荐阅读:redis 哈希表的 rehash 分析渐进式 rehash

Redis 设置过期时间

Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。

作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。

如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的 key 过期时间设置的过于集中,到过期的那个时间点,Redis 可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。

Redis 是怎么对过期的key 进行删除的?

1、定时删除

在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。 即从设置 key 的 Expire 开始,就启动一个定时器,到时间就删除该 key;这样会对内存比较友好,但浪费 CPU 资源。

2、惰性删除

放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。 即平时不处理,在使用的时候,先检查该 key 是否已过期,已过期则删除,否则不做处理;这样对 CPU 友好,但是浪费内存资源

3、定期删除

每隔一段时间,扫描 Redis 中过期 key 字典,并随机清除部分过期的 key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。

Redis 服务器实际使用的是惰性删除定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。

推荐阅读:理解Redis的内存回收机制

Redis 内存淘汰机制

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

  1. volatile-lru(least frequently 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: 当内存不足以容纳新写入数据时,新写入操作会报错。

Redis 会在每一次处理命令的时候(processCommand函数调用freeMemoryIfNeeded)判断当前 redis 是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的 key。

Redis 持久化机制

Redis 是基于内存的,如果不想办法将数据保存在硬盘上,一旦 Redis重启 (退出/故障),内存的数据将会全部丢失。 此时我们需要持久化数据,也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据,或者是为了防止系统故障而将数据备份到一个远程位置。

Redis 不同于 Memcached 的很重一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。

快照(snapshotting)持久化(RDB)

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

快照持久化是 Redis 默认采用的持久化方式,可以手动执行如下两个命令来生成 RDB 文件:

  • SAVE会**阻塞 **Redis 服务器进程,服务器不能接收任何请求,直到 RDB 文件创建完毕为止。
  • BGSAVE创建出一个子进程,由子进程来负责创建 RDB 文件,服务器进程可以继续接收请求。

也可以在 Redis.conf 配置文件中做如下配置:

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命令创建快照。

RDB 持久化主要依靠 fork 和 cow。fork是指 Redis 通过创建子进程来进行 RDB 操作,cow 指的是 copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

AOF(append-only file)持久化

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

appendonly yes

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

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

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

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

AOF 重写

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

Redis 将 AOF 重写程序放到子进程里执行(BGREWRITEAOF命令),像BGSAVE命令一样 fork 出一个子进程来完成重写 AOF 的操作,从而不会影响到主进程。 Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作 。

RDB和AOF对过期键的策略

RDB 持久化对过期键的策略:

  • 执行SAVE或者BGSAVE命令创建出的 RDB 文件,程序会对数据库中的过期键检查,已过期的键不会保存在 RDB 文件中
  • 载入 RDB 文件时,程序同样会对 RDB 文件中的键进行检查,过期的键会被忽略

AOF 持久化对过期键的策略:

  • 如果数据库的键已过期,但还没被惰性/定期删除,AOF 文件会保留过期键,当过期的键被删除了以后,会追加一条 DEL 命令来显示记录该键被删除了
  • 重写 AOF 文件时,程序会对文件中的键进行检查,过期的键会被忽略

复制模式:

  • 主服务器来控制从服务器统一删除过期键(保证主从服务器数据的一致性)
优缺点

RDB

优点:载入时恢复数据快、文件体积小。

缺点:会一定程度上丢失数据(因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。)

AOF

优点:丢失数据少(默认配置只丢失一秒的数据)。

缺点:恢复数据相对较慢,文件体积大。

如何选择?

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

Redis 事务

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

对于关系型数据库来说,事务还有回滚机制,即事务命令要么全部执行成功,只要有一条失败就回滚,回到事务执行前的状态。Redis 不支持回滚,它的事务只保证命令依次被执行,即使中间一条命令出错也会继续往下执行,所以说它只支持简单的事务。

Redis分布式锁

分布式锁一般有如下的特点:

  • 互斥性: 同一时刻只能有一个线程持有锁
  • 可重入性: 同一节点上的同一个线程如果获取了锁之后能够再次获取锁
  • 锁超时:和 JUC 中的锁一样支持锁超时,防止死锁
  • 高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
  • 具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒

实现方式:

1、利用 setnx+expire 命令,setnx 和 expire 是分开的两步操作,不具有原子性,如果执行完第一条指令应用异常或者重启了,锁将无法过期。

2、使用 Lua 脚本(包含setnx和expire两条指令)来保证原子性。

3、使用 set key value EX seconds[NX|XX] 命令 (正确做法)

4、Redlock算法 与 Redisson 实现

推荐阅读:基于Redis的分布式锁实现

Redis异步队列

一般使用 list 结构作为队列,**rpush **生产消息,**lpop **消费消息。当 lpop 没有消息的时候,要适当 sleep一会再重试。 当然也可以不使用 sleep,list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。

Redis同步机制

Redis 的主从同步机制可以确保 Redis 的 master 和 slave 之间的数据同步。按照同步内容的多少可以分为全同步和部分同步,按照同步的时机可以分为 slave 刚启动时的初始化同步和正常运行过程中的数据修改同步。

全同步

1)在slave启动时,会向master发送一条SYNC指令。
2)master收到这条指令后,会启动一个备份进程将所有数据写到rdb文件中去。
3)更新master的状态(备份是否成功、备份时间等),然后将rdb文件内容发送给等待中的slave。

注意,master并不会立即将rdb内容发送给slave。而是为每个等待中的slave注册写事件,当slave对应的socket可以发送数据时,再将rdb内容发送给slave。

部分同步

当 Redis 的 master/slave 服务启动后,首先进行全同步。之后,所有的写操作都在 master 上,而所有的读操作都在 slave 上。因此写操作需要及时同步到所有的 slave 上,这种同步就是部分同步。

1)master收到一个操作,然后判断是否需要同步到salve。
2)如果需要同步,则将操作记录到aof文件中。
3)遍历所有的salve,将操作的指令和参数写入到savle的回复缓存中。
4)一旦slave对应的socket发送缓存中有空间写入数据,即将数据通过socket发出去。

推荐阅读:Redis 数据同步机制分析

Redis集群

上图展示了 Redis Cluster 典型的架构图,集群中的每一个 Redis 节点都 互相两两相连,客户端任意直连到集群中的任意一台,就可以对其他 Redis 节点进行读写 的操作。

基本原理

常见的分区规则哈希分区和顺序分区,Redis 集群使用了哈希分区,顺序分区暂用不到,不做具体说明;

Rediscluster采用了哈希分区的**“虚拟槽分区”**方式(哈希分区分节点取余、一致性哈希分区和虚拟槽分区)。

RedisCluster采用此分区,所有的键根据哈希函数(CRC16[key]&16383)映射到0-16383槽内,共16384个槽位,每个节点维护部分槽及槽所映射的键值数据,每个节点通过 gossip 消息得知其它节点的信息。

哈希函数: Hash()=CRC16[key]&16383 按位与

redis用虚拟槽分区原因:解耦数据与节点关系,节点自身维护槽映射关系,分布式存储

redisCluster 的缺陷:

a,键的批量操作支持有限,比如mset, mget,如果多个键映射在不同的槽,就不支持了

b,键事务支持有限,当多个key分布在不同节点时无法使用事务,同一节点是支持事务

c,键是数据分区的最小粒度,不能将一个很大的键值对映射到不同的节点

d,不支持多数据库,只有0,select 0

e,复制结构只支持单层结构,不支持树型结构。

集群的主要作用
  1. 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量大大增加另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,bgsavebgrewriteaoffork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……
  2. 高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
客户端是如何访问集群的?

客户端会先访问集群中的任意一个节点, 然后向该节点发送一条get/set命令,接收的节点首先会依据该 key 计算对应槽位,然后再找到槽位所在的节点,判断找到的节点是否是自身, 如果是则在当前节点执行该命令,否则回复客户端 moved 异常,异常中包含真正执行命令的节点的信息,客户端需要使用获取到节点信息,重新连接获取到的节点并发送命令,但是该行为不是自动的,需要主动操作(如下图中第4步,该步骤需要在客户端通过专门编写逻辑代码执行);客户端依据返回的moved异常中的节点信息,进行的转移连接操作就是moved重定向。

GET x
-MOVED 3999 127.0.0.1:6381

MOVED 指令第一个参数 3999key 对应的槽位编号,后面是目标节点地址,MOVED 命令前面有一个减号,表示这是一个错误的消息。客户端在收到 MOVED 指令后,就立即纠正本地的 槽位映射表,那么下一次再访问 key 时就能够到正确的地方去获取了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8Gt3EUQ-1603287177521)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d4bcd213f1b4e8b90c66323845af6ef~tplv-k3u1fbpfcp-watermark.image)]

这里其实还有个迁移中的情况,如果访问的槽正在迁移,则返回ask命令,客户端会被引导去目标节点查找。

推荐阅读:玩转Redis集群之ClusterRedis应用学习——Redis Cluster客户端 原

新的主服务器是怎样被挑选出来的?

Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master,继续提供服务。

关于 slave 的选择,有如下依据:

  • 跟master断开连接的时长
  • slave优先级
  • 复制offset
  • run id
Redis Sentinel 哨兵

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZ9ylk5z-1603287177522)(https://camo.githubusercontent.com/eb48dfa93d023f7b8939289250d03eff8b4b7a54/68747470733a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f373839363839302d383834643562653961326464666562632e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)]

上图展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;
  • 数据节点: 主节点和从节点都是数据节点;

在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下方是官方对于哨兵功能的描述:

  • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
  • 通知(Notification): 哨兵可以将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

推荐阅读:从零单排学Redis【铂金二】Redis(9)——集群入门实践教程

Redis Cluster 着眼于扩展性,在单个 Redis 内存不足时,使用 Cluster 进行分片存储。

缓存雪崩

什么是缓存雪崩?

缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。 数据库由于承受压力过大,可能直接崩溃,导致整个服务瘫痪。

原因:

  • Redis 挂掉了,请求全部走数据库。
  • 对缓存数据(热点数据)设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。
有哪些解决办法?

针对Redis服务不可用的情况:

  1. 采用 Redis 集群 (主从架构+Sentinel 或者Redis Cluster) 实现高可用,避免单机出现问题整个缓存服务都没办法使用。
  2. 设置本地缓存(ehcache)+限流(hystrix),避免数据库崩溃。

针对热点缓存失效的情况:

  1. 设置不同的过期时间,比如随机设置缓存的失效时间。
  2. 缓存永不失效。

缓存穿透

什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

有哪些解决办法?

缓存无效key

如果缓存和数据库都查不到某个 key 的数据 ,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间

布隆过滤器

由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层!

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

我们需要了解一下布隆过滤器的背后原理!以及为何存在误判的现象。

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

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

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

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

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

扩展:缓存击穿

缓存击穿跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,导致数据库崩溃,而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

热点key的访问要如何解决?

1、可以通过 hash 分 key,把一个 key 拆分成多个 key,分布到不同的节点,防止单点过热。比如一个 key 之前就分到一个节点上,把 key 做了拆分之后,就像一致性 hash 的虚拟节点,分散访问。

2、使用本地缓存,比如 Ehcahe。

Redis中的一致性hash使用

一致性Hash算法原理:

先构造一个长度为2^32的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 2^32-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 2^32-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。

这种算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。

当然,万事不可能十全十美,一致性Hash算法比普通的余数Hash算法更具有伸缩性,但是同时其算法实现也更为复杂。

一致性Hash算法实现,可以在很大程度上解决很多分布式环境下不好的路由算法导致系统伸缩性差的问题,但是会带来另外一个问题:负载不均

比如说有Hash环上有A、B、C三个服务器节点,分别有100个请求会被路由到相应服务器上。现在在A与B之间增加了一个节点D,这导致了原来会路由到B上的部分节点被路由到了D上,这样A、C上被路由到的请求明显多于B、D上的,原来三个服务器节点上均衡的负载被打破了。某种程度上来说,这失去了负载均衡的意义,因为负载均衡的目的本身就是为了使得目标服务器均分所有的请求

解决这个问题的办法是引入虚拟节点,其工作原理是:将一个物理节点拆分为多个虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布在Hash环上(物理服务器数越大,需要的虚拟节点越多)。采取这样的方式,就可以有效地解决增加或减少节点时候的负载不均衡的问题。

解决方案之一: 给每个真实结点后面根据虚拟节点加上后缀再取Hash值,比如"192.168.0.0:111"就把它变成"192.168.0.0:111&&VN0"到"192.168.0.0:111&&VN4",VN就是Virtual Node的缩写,还原的时候只需要从头截取字符串到"&&"的位置就可以了。

推荐阅读:对一致性Hash算法,Java代码实现的深入研究

对于缓存雪崩、缓存穿透、缓存击穿的总结

一般避免以上情况发生我们从三个时间段去分析下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

有两个选择:

  1. 先删除缓存,再更新数据库
  2. 先更新数据库,再删除缓存
先删缓存,再更新数据库

该方案仍然有可能导致不一致。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
那么,如何解决呢?采用延时双删策略

先删除缓存,再写数据库(这两步和原来一样),休眠1秒,再次删除缓存 。可以将1秒内所造成的缓存脏数据,再次删除。

具体休眠多久,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

先更新数据库,再删缓存

正常的情况是这样的:

  • 先更新数据库,成功;
  • 再删除缓存,也成功;

如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有。

假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存

概率低的原因在于:只有步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。

解决思路:

1、利用消息队列进行失败重试

流程如下所示:
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功

然而,该方案有一个缺点,对业务线代码造成大量的侵入。

2、订阅 binlog 程序

流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。

上述的订阅 binlog 程序在 MySQL 中有现成的中间件叫 canal,可以完成订阅 binlog 日志的功能。至于 oracle 中,目前不知道有没有现成中间件可以使用。

推荐阅读:【原创】分布式之数据库和缓存双写一致性方案解析

参考文献

Redis 知识集锦

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值