有了这一篇Redis汇总,再也不用担心面试官问缓存相关问题了

文章目录


1. 非关系型数据库的产生

1.1 什么是非关系型数据库

  • 什么是关系型数据库
    关系型数据库就是需要依赖现实生活的具体模型来创建的数据库, 比如具体的人, 事物, 能表示多个表之间一对一, 一对多, 多对多的关系. 而且支持事务.
    常见的Mysql, Oracle都是关心型数据库.
  • 什么是非关系型数据库
    非关系型数据库就是不需要依赖现实生活中的具体模型就可以出数据库及表. 比较灵活便利.

1.2 为什么要用非关系型数据库

  • 关系型数据库具有数据安全且容易理解的优点, 但是随着互联网的发展,数据存储越来越多, 对于并发的要求也越来越高,项目中单纯使用关系型数据库已经无法满足这些要求, 于是应运而生了非关系型数据库.
  • 非关系型数据库具有格式灵活, 成本低, 查询快, 高性能, 高扩展的优点.
  • 实际开发中通常将关系型数据库和非关系型数据库结合使用, 不同非关系型数据库具体应用场景不同.

1.3 常用的非关系型数据库有哪些

1.3.1 键值对key-value型

  • key-value数据库的主要特点是具有极高的并发读写性能
  • Key-value数据库是一种以键值对存储数据的一种数据库, 类似Java中的map. 可以将整个数据库理解为一个大的map, 每个键都会对应一个唯一的值.
  • 主流最常用的代表就是Redis等
    在这里插入图片描述

1.3.2 文档型

文档型按照功能划分又分为基于海量数据存储的和基于搜索内容存储的搜索引擎,数据结构可以理解为Json格式的文档类型.

基于海量数据存储
  • 这类数据库的主要特点是在海量的数据中可以快速的查询数据
  • 文档存储通常使用内部表示法, 可以直接在应用程序中处理, 主要是JSON. JSON文档也可以作为纯文本存储在键值存储或关系数据库系统中.
    主流代表为MongoDB等
    在这里插入图片描述
搜索引擎
  • 搜索引擎是专门用于搜索数据内容的NoSQL数据库管理系统。
  • 主要是用于对海量数据进行近实时的分析处理,可用于机器学习和数据挖掘
  • 主流代表为Elasticsearch, Solr等.

1.3.3 列式数据库

  • 这类数据库的主要特点是具有很强的可拓展性
  • 关系型数据库都是以行为单位来存储数据的, 擅长以行为单位的读入处理, 比如特定条件数据的获取. 因此, 关系型数据库也被成为面向行的数据库。相反,面向列的数据库是以列为单位来存储数据的,擅长以列为单位读入数据。
  • 这类数据库想解决的问题就是传统数据库存在可扩展性上的缺陷,这类数据库可以适应数据量的增加以及数据结构的变化,将数据存储在记录中,能够容纳大量动态列。由于列名和记录键不是固定的,并且由于记录可能有数十亿列,因此可扩展性存储可以看作是二维键值存储。
  • 主流代表为HBase等.
    在这里插入图片描述

1.3.4 图数据库

  • 将数据库图形化,数据结构是图结构,
    在这里插入图片描述

1.4 几种非关系型数据库对比

RedisMongoDBElasticSearchHbaseNeo4J
数据结构键值对的key-value形式Json文档格式Json文档格式列簇式存储,将同一列数据存到一起图结构
典型应用场景缓存和并发数据库1. 做缓存数据库 2. 海量数据且对允许少许数据丢失, 例如用户评论数据, 点赞数据等海量数据进行搜索应用, 例如网站搜索页面的数据大数据分布式系统,海量数据且数据比较分散社交网络,推荐系统等
优点1. 数据存在内存中 2. 线程安全 3. 读写效率高1. 存储海量数据 2. 表结构可变数据量大,基于Lunence倒排索引原理,能实现海量数据分词搜索1.高扩展性 2. 海量数据利用图结构相关算法,符合人的思维
缺点1.数据无结构化 2. 存储少量数据读写效率不如Reids读写效率不如Redis优点也是缺点, 没有太多花哨的功能不好做分布式系统

真实开发也是多种数据库结合使用, 如图下图所示:
在这里插入图片描述

2. Redis是什么

Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。
它是一种 NoSQL(not-only sql,泛指非关系型数据库)的数据库。
Redis的特征:

  1. 数据间没有必然的关联关系
  2. 内部采用单线程机制进行工作
  3. 高性能。官方提供测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s。
  4. 多数据类型支持
    字符串类型 string
    列表类型 list
    散列类型 hash
    集合类型 set
    有序集合类型 sorted_set(zset)
  5. 持久化支持。可以进行数据灾难恢复

Redis 的应用

  • 为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等
  • 任务队列,如秒杀、抢购、购票排队等
  • 即时信息查询,如各位排行榜、各类网站访问统计、公交到站信息、在线人数信息(聊天室、网站)、设
    备信号等
  • 时效性信息控制,如验证码控制、投票控制等
  • 分布式数据共享,如分布式集群架构中的 session 分离
  • 消息队列
  • 分布式锁

3. Redis常用的数据类型及每种类型应用场景

刚才有提到Redis常用的5种数据类型, 在具体介绍前,我们先来了解下Redis内部内存管理是如何描述这5种数据类型的.

3.1 基本数据类型

在这里插入图片描述
首先Redis内部使用一个redisObject对象来表示所有的key和value.
redisObject最主要的信息如上图所示: type表示一个value对象具体是何种数据类型, encoding是不同数据类型在Redis内部的存储方式.
比如:type=string 表示 value 存储的是一个普通字符串,那么 encoding 可以是 raw 或者 int.
这里说下,数据类型是针对Value的, Redis是key=value格式存储的,Key都是一样的是String类型的, Value有不同数据类型, 接下来,我们简单介绍下这常用的5中数据类型~

3.1.1 String类型

String是Redis最基本的类型, 可以与内存存储一模一样的类型, 一个Key对应一个Value. Value不仅String类型的, 也可以是数字.
String 类型是二进制安全的,意思是 Redis 的 String 类型可以包含任何数据,比如 jpg 图片或者序列化的对象, String 类型的值最大能存储 512M.

  • 应用场景
  1. 缓存功能:String字符串是最常用的数据类型, 不仅仅是Redis, 各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  2. 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  3. 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。

3.1.2 Hash

Hash是一个键值(key-value)的集合。Redis 的 Hash 是一个 String 的 Key 和 Value 的映射表,Hash 特别适合存储对象。常用命令:hget,hset,hgetall 等。

  • 应用场景:可以封装对象格式的数据

3.1.3 List

List 列表是简单的字符串列表(双向列表), 按照插入顺序排序, 可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。

  • 应用场景:
  1. 链表特点查询快, 可以作为消息队列使用.
  2. 用户的粉丝列表
  3. 博客首页,博主的文章列表

3.1.4 Set

Set 是无序不可重复的, 底层是通过 hashtable 实现的, 常用命令:sdd、spop、smembers、sunion 等。

  • 应用场景:
  1. 和List一样,区别在于 Set 是自动去重. 而且 Set 提供了判断某个成员是否在一个 Set 集合中, 比如: 统计访问网站的所有Ip.
  2. 可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?

3.1.5 Zset

Zset 特点是有序且不可重复。常用命令:zadd、zrange、zrem、zcard 等。

  • 应用场景:
  1. 排行榜:有序不重复数据类型典型应用场景做排行榜. 例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  2. 用Zset来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务, 让重要的任务优先执行.

3.2 Redis的高级数据类型

3.2.1 Bitmaps

Bitmaps是应用于位运算来存储数据, 主要应用场景是应用于信息转态的统计, 即某个数据是黑或白的问题, 没有多个选项.

  • 基本操作
    1. 设置数据 setbit key [位移数 value] 注意: value只能是1或0.
    2. 获取数据getbit key 位移数
    3. 统计对应key 有多少个value为1的数据 bitcount key
  • 高级操作
    1. 对特定key按位进行交, 并, 非, 异或操作, 并将结果保存到合同后的key中
      bitop and(交)/or(并)/not(非)/xor(异或) 新key 旧key1 旧key2
  • 应用场景, 信息状态的统计
    比如电影网站,
    1. 统计每天某一部电影是否被点播, =>只要找到对应的key, 知道他的位移位置, 看看是不是1就知道是否被点播过了.
      在这里插入图片描述

    2. 统计每天有多少部电影被点播, =>每天的数据进行bitcount就可以了.

    3. 统计每周/月/年有多少部电影被点播, =>把每天的数据进行or运算, 都为1代表这个电影被点过了, 为0则代表从来没有被点过.
      在这里插入图片描述

    4. 统计年度哪部电影没有被点播. =>把每天的数据进行or运算, 都为1代表这个电影被点过了, 为0则代表从来没有被点过.

3.2.2 HyperLogLog

HyperLogLog存储的数据也是普通的字符串, 但是它有个特点是能够用来做基数统计(基数就是数据集去重后元素个数), 运用了LogLog的算法来去重. 主要应用场景用于独立信息统计.
在这里插入图片描述

  • 基本操作
    1. 添加数据:pfadd key [element1, element2 ...]  pfadd a 1 2 2
    2. 统计数据:pfcount key
    3. 合并数据:pfmerge 新key [旧key1, 旧key2...]
  • 应用场景
    1. 用于进行基数统计,不是集合,不保存数据,只记录数量而不是具体数据
    2. 核心是基数估算算法,最终数值存在一定误差
    3. 误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值
    4. 耗空间极小,每个hyperloglog key占用了12K的内存用于标记基数
    5. pfadd命令不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大
    6. pfmerge命令合并后占用的存储空间为12K,无论合并之前数据量多少

3.2.3 GEO

类似于微信啊, 陌陌等社交软件都有搜索附件的人, 那么地理位置如何计算出来的呢?这就可以应用Redis的geo数据类型了. 不过GEO计算的是水平距离, 没有办法加上垂直距离.

  • 基本操作
    1. 添加坐标点 geoadd key 横坐标 纵坐标 坐标点名称
    2. 计算坐标点经纬度 geohash key 坐标点名称
    3. 计算两个坐标点距离 geodist key 坐标点名称a 坐标点名称b 单位
  • 应用场景
    主要应用于计算坐标点及多个坐标点直接的水平距离.

4. 为什么要用Redis

Redis是一种非关系型数据库, 经常和Mysql结合使用, 在项目中作为缓存数据库使用. 如果大家想了解其他非关系型数据库详见博主上篇文章常见的非关系型数据库有哪些

4.1 高性能

  • 假如用户第一次访问数据库中的某些数据. 这个过程会比较慢, 因为是从硬盘上读取的. 将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。
  • 操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
    在这里插入图片描述

4.2 解决并发

  • 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
    在这里插入图片描述

4. Redis为何这么快

  • 官方提供的数据可以达到 100000+ 的 QPS(每秒内的查询次数),这个数据不比 Memcached(内存直接查询) 差!

4.1 那Redis这么快, 那它是什么线程模式呢

  • Redis确实是单进程单线程的模型, 因为Redis完全是基础内存操作, CPU不是Redis的瓶颈, Redis的瓶颈最有可能是机器内存的大小或网络带宽.
  • 那疑问又来了, 既然Redis是单线程的, 那么为什么还能这么快呢?
  1. Redis完全基于内存, 绝大部分请求是纯粹的内存操作, 非常速度, 数据存在内存中.
  2. 采用单线程, 避免了不必要的上下文切换和竞争条件, 不存在多线程导致的CPU切换, 不用去考虑各种锁的问题, 不存在加锁释放锁操作, 没有死锁问题导致的性能消耗.
  3. 使用多路复用IO模型, 非阻塞IO, 每一条线程有自己的IO模型.

5. Redis的持久化

Redis 为了保证效率, 数据缓存在了内存中, 但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中, 以保证数据的持久化.

Redis 的持久化策略有两种:

  • RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上。
  • AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
  • RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 Redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。

5.1 RDB

在不同时间点将Redis中的数据保存到文件中.

  • 优点: RDB快照方式比较适合用于备份,比如,你可以在最近24小时内, 每小时备份一次, 并且在每个月的每一天也备份一个RDB文件.
    这样即使遇上问题, 也可以随时将数据还原到不同时间点, RDB非常适合灾难性恢复.
  • 缺点: 需要进行大规模数据的恢复, 而且对数据的完整性不是非常敏感, RDB会存在数据丢失.

5.2 AOF

将所有Redis执行过的写操作记录下来, 下次启动Redis时候会重新执行一遍这个命令, 就可以实现数据的完整性.

每一个写命令都通过write函数追加到appendonly.aof中, 配置如下:

appendfsyncyesappendfsync always #每次有数据修改发生时都会写入AOF文件
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
  • 优点: 可以最大程度保证数据的完整性. 使Redis变得非常耐久, AOF的默认策略是每秒恢复一次, 这种设置, 就算发生故障停机, 也最多丢失一秒钟的数据.
  • 缺点: 对于相同的数据集来说, AOF 的文件体积通常要大于 RDB 文件的体积, AOF 的速度可能会慢于 RDB.

好了,我们说了这么多,也详细介绍了RDB和AOF, 那么小伙伴儿们是不是有疑惑了, 我们该用哪一种呢?

5.3 我们该选择哪一种持久化方式

  • 如果你非常关心你的数据, 但是任然能接受数分钟内的数据丢失,那么可以使用RDB持久化, 而且RDB效率要比AOF快.
  • 数据库备份和灾难恢复: 定时生成 RDB 快照非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快.
  • 但是如果你不能接收数分钟数据的丢失, 对数据恢复效率要求也不高, 那么就选择AOF持久化.
  • 当然了, Redis支持开启RDB和AOF结合使用, 系统重启后, Redis会优先使用AOF来回复数据, 这样丢失的数据会最少.

6. Redis事务简介

为了避免Redis执行指令过程中, 多条连续执行的指令被干扰, 打断, 插队这种情况, 就需要开启事务.

Redis事务就是一个命令执行的队列, 将一系列预定义命令包装成一个整体(一个队列), 当执行时, 一次性按照添加顺序依次执行, 中间不会被打断或者干扰.

或者简单的理解, 就是一个队列中, 一次性,顺序性, 排他性的执行一系列命令.

6.1 Redis事务操作

6.1.1 Redis事务的基本操作

  • 开启事务multi
    作用: 设定事务的开启位置, 次指令后, 后续的所有指定均加入到事务中.
  • 执行事务exec
    作用: 设定事务的结束位置, 同时执行事务, 与multi成对出现, 成对使用.

注意: 加入事务的命令暂时进入到任务队列中, 并没有立即执行, 只有与执行exec命令才开始执行.

  • 取消事务discard
    作用: 终止当前事务的定义, 发生在multi之后, exec之前.

6.1.2 事务的工作流程

在这里插入图片描述

  • 执行指令
  • 服务器判断指令是否是事务指令
    • 普通指令就普通执行
    • 如果是multi就创建队列, 并返回ok
      • 继续执行指令
      • 服务器继续判断指令是否是事务指令
        • 如果是exec, 则执行事务, 并返回队列中所有结果
        • 如果是普通指令, 则加入队列中, 返回queued
        • 如果是discard, 则销毁队列, 返回ok

6.1.3 使用事务的注意事项

  • 如果开启事务后, 命令中出现了语法错误, 则导致该事务队列失效(之前书写正确的也失效了), 这就是都会整体失败.
  • 如果开始事务后, 命令执行时出现了错误, 则提交事务后, 正确的被执行了, 错误的不被执行, 也就是说不会整体回滚, 那只能手动回滚数据.
  • 如何手动进行事务回滚呢?
  1. 记录操作过程中被影响的数据之前的状态
    单数据: String
    多数据:hash, list, set, zset
  2. 设置指令恢复所有的被修改的项
    单数据: 直接set回去,注意该值是否有时效, 如有也有设置回去
    多数据: 修改对应值或者整体克隆复制.

6.2 基于特定条件的事务执行-锁

实际开发过程中, 为了高可用, 会有多个客户端有操作Redis的权限. 为了保证同一个key只能被一个的客户端增删改操作, 需要对同一数据加锁.

  • 场景分析
    天猫双11热卖过程中, 对已经售罄的货物追加补货, 4个业务员都有权限补货, 只能被一个业务员连续操作, 不能被多个业务员重复补货, 如何解决呢?
    1. 多个客户端有可能同时操作同一个key, 且该key只能被修改一次.
    2. 如果修改后, 后面再被修改则要终止当前的操作.
    3. 肯定也要添加事务, 涉及增删改问题肯定是要添加事务的.
  • 解决方案
    1. 对同一个要修改的key添加监听锁
    2. 开启事务
    3. 一旦该key发生改变, 当前客户端操作的事务则失败
      watch key1 [key2, key3......] # 监听指定的key
      multi # 开启事务
      ... # 进行一些列操作
      exec # 提交事务
      
    4. 如何取消监视呢? 可以取消对所有key的监视
      unwatch
      

因此, 这也是Redis的一个应用场景, Redis应用基于状态控制的批量任务执行.

7. 分布式锁

7.1 分布式锁是什么

实际开发过程中, 也会遇到如下场景, 不但要保证同一个key只能被一个的客户端增删改操作, 还要监控该key对应的value值, 这时就需要设置分布式锁了.

  • 场景分析
    还是天猫双11热卖过程中, 怎么避免最后一件商品不被多人同时购买(超卖问题)
    1. watch监听能监听特定的key是否被修改, 但是无法监听被修改的值, 此处要监控的是具体的数据.
    2. 虽然Redis是单线程的, 但是多个客户端对同一数据同时进行操作时, 如何避免不被同时修改呢?
  • 解决方案
    1. 使用setnx设置一个公共锁 setnx lock-key value, value可以为随机任意值.
    2. setnx命令能返回value值.只有第一次执行的才会成功并返回1,其它情况返回0:
      如果返回是1, 说明没有人持有锁, 当前客户端设置锁成功,可以进行下一步的具体业务操作.
      如果返回是0, 说明有人持有了锁, 当前客户端设置锁失败, 那么需要排队或等待锁的释放.
    3. 操作完毕通过del操作释放锁.

这就是Redis分布式锁的雏形, 当然实际开发中要考虑锁时效性避免死锁问题,还要避免锁误删问题, 因此有接下来几种版本的分布式锁

7.2 分布式锁版本1

7.2.1 业务场景

  • 场景分析
    依赖分布式锁的机制, 某个用户操作Redis时对应的客户端宕机了, 且此时已经获取到锁, 导致锁一直被持有, 其他客户端拿不到锁, 这就是死锁问题, 如何解决呢?

    1. 由于锁操作由用户控制加锁解锁, 必定会存在加锁未解锁的风险
    2. 需要解锁操作不能仅依赖用户控制, 系统级别要给出对应的保底处理方案.
  • 解决方案

    1. 使用expire为锁key添加时间限定, 到时不释放锁, 则放弃锁.
      setnx lock-key 001 # 设置分布式锁
      expire lock-key second # 设置单位为秒
      pexpire lock-key milliseconds # 设置单位为毫秒
      
    2. 或者直接设置的时候添加时间限制
      set lock-key value NX PX 毫秒数 
      # 比如为key为name设置分布式锁
      set lock-name 001 NX PX 5000
      
      value可以是任意值
      NX代码只有lock-key不存在时才设置值
  • 实际开发中如何知道设置多少时间合适.
    由于操作通常都是微秒或毫秒级,因此该锁定时间不宜设置过大。具体时间需要业务测试后确认。
    例如:持有锁的操作最长执行时间127ms,最短执行时间7ms。

    1. 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
    2. 时间设定推荐:最大耗时*120%+平均网络延迟*110%
    3. 如果二者相差2个数量级,取其中单个耗时较长即可.

7.2.2 流程图

在这里插入图片描述

  • 1、通过set命令设置锁
  • 2、判断返回结果是否是OK
    • 1)Nil,获取失败,结束或重试(自旋锁)
    • 2)OK,获取锁成功
      • 执行业务
      • 释放锁,DEL 删除key即可
  • 3、异常情况,服务宕机。超时时间EX结束,会自动释放锁

7.3 分布式锁版本2

7.3.1 业务场景

  • 场景分析

    1. 三个进程:A和B和C,在执行任务,并争抢锁,此时A获取了锁,并设置自动过期时间为20ms
    2. A开始执行业务,因为某种原因,业务阻塞,耗时超过了20ms,此时锁自动释放了.
    3. B恰好此时开始尝试获取锁,因为锁已经自动释放,成功获取锁.
    4. A此时业务执行完毕,执行释放锁逻辑(删除key),于是B的锁被释放了,而B其实还在执行业务.
    5. 此时进程C尝试获取锁,也成功了,因为A把B的锁删除了。

    问题(1):使用版本1的分布式锁, 有可能B和C同时获取到锁,违反了锁只能被一个客户端持有的特性.
    如何解决这个问题呢?我们应该在删除锁之前, 判断这个锁是否是自己设置的锁, 如果不是(例如自己的锁已经超时释放), 那么就不要删除了.
    问题(2):如何得知当前获取锁的是不是自己呢

  • 解决方案

    1. 我们可以在set 锁时,存入自己的信息!删除锁前, 判断下里面的值是不是与自己相等. 如果不等,就不要删除了.
    2. 这里自己的信息通常是一个随机值+当前线程的id, 通过UUID.randomUUID().toString()+Thread.currentThread().getId()获取到.

7.3.2 流程图

在这里插入图片描述

  • 1、通过set命令设置锁
    id通常是一个随机值+当前线程的id.
  • 2、判断返回结果是否是OK
    • 1)Nil,获取失败,结束或重试(自旋锁)
    • 2)OK,获取锁成功
      • 执行业务
      • get lock判断返回的id是否一致
        • 一致则释放锁,DEL 删除key即可
        • 不一致则不释放锁.
  • 3、异常情况,服务宕机。超时时间EX结束,会自动释放锁

7.4 分布式锁版本3

7.4.1 业务场景

  • 场景分析
    就是实际开发过程中, 一段代码内部会有嵌套方法, 外层方法获取到锁后, 内层再去获取时由于锁已经存在了就无法获取了, 但内层代码不执行完外层也释放不了锁啊, 这就是方法嵌套导致的死锁问题, 怎么解决呢?
  • 解决方案
    1. 让锁成为可重入锁, 也就是外层代码获取到这把锁, 内层代码可以获取到该锁.
    2. 获取时判断是不是自己的锁, 是则继续使用, 而且要记录重入的次数
    3. 这里的锁不能使用之前的String类型作为lock-key的值了, 锁的value要使用hash结构
      hset lock-key 线程信息, 重入次数(默认1) NX PX 毫秒数
      
      key: lock-key
      value-key:线程信息
      value-value:重入次数

7.4.2 流程图

在这里插入图片描述
下面我们假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20

获取锁的步骤:

  • 1、判断lock是否存在 EXISTS lock
    • 返回1则存在,说明有人获取锁了,下面判断是不是自己的锁
      • 判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
        • 存在,说明是自己获取的锁,重入次数+1:HINCRBY lock threadId 1,去到步骤3
        • 不存在,说明锁已经有了,且不是自己获取的,锁获取失败,end
    • 2、返回0则不存在,说明可以获取锁,并存入value值HSET key threadId 1
    • 3、设置锁自动释放时间,EXPIRE lock 20

释放锁的步骤:

  • 1、判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
    • 存在,说明锁还在,重入次数减1:HINCRBY lock threadId -1,获取新的重入次数.
    • 不存在,说明锁已经失效,不用管了
  • 2、判断重入次数是否为0:
    • 为0,说明锁全部释放,删除key:DEL lock
    • 大于0,说明锁还在使用,重置有效时间:EXPIRE lock 20

7.5 集群模式的Redis分布式锁

7.5.1 RedLock算法

这个场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒
  • 轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
  • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  • 要是锁建立失败了,那么就依次删除这个锁
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

7.5.2 Redission

虽然我们已经实现了分布式锁,能够满足大多数情况下的需求,不过我们的代码并不是万无一失。
某些场景下,可能需要实现分布式的不同类型锁,比如:公平锁、互斥锁、可重入锁、读写锁等等。实现起来比较麻烦。
而开源框架Redission就帮我们实现了上述的这些 锁功能,而且还有很多其它的强大功能。
Redission流程图
在这里插入图片描述

  • 加锁机制: 如果某个客户端要加锁, 它面对的是Redis Cluster集群, 首先会根据hash节点选择一台机器.
  • 锁互斥机制: 这个时候如果客户端2来尝试家锁, 发现myLock这个锁Key已经存在了, 在Mylock这个锁key的剩余时间内, 客户端2会进入一个while循环, 不停的尝试加锁.
  • watch dog自动延期机制: 客户端1一旦加锁成功, 就会启动一个watch dog看门够, 他是一个后台线程,会每隔10秒检查一下, 如果客户端1还持有锁key, 那么就会不断的延长锁key的生存时间.
  • 可重入加锁机制: 执行可重入锁,会对客户端1的加锁次数, 累加1.
  • 锁释放机制: 执行释放锁, 就会对和护短加锁次数减1. 如果发现锁此时是0, 就从Redis中删除这个key, 另外客户端2就可以尝试加锁了.

8. Redis的key有效期设置及淘汰策略

Redis的数据结构是key-value格式的键值对, 在项目中通常作为缓存数据库使用, 当然使用过程中经常对key设置有效期.

8.1 key的过期时间和永久有效分别怎么设置

expire命令和persist命令.

8.2 Redis的过期键的删除策略

Redis是key-value数据库, 我们可以设置Redis中缓存的key的过期时间. Redis的过期策略就是指当Redis中缓存的key过期了, Redis如何处理.

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器, 到过期时间就会立即清除. 该策略可以立即清除过期的数据, 对内存很友好; 但是会占用大量的CPU资源去处理过期的数据, 从而影响缓存的响应时间和吞吐量.
  • 惰性过期: 只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
    (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期定期过期两种过期策略。

9. Redis集群模式

单机Redis的风险与问题

如果Redis设计成单节点, 会有如下风险:

  • 问题1: 机器故障
    1. 现象: 硬盘故障, 系统崩溃, 造成数据丢失, 很可能对业务造成灾难性打击
    2. 结论: 用户基本上会放弃使用Redis.
  • 问题2: 容量瓶颈
    1. 现象: 内存不足, 从16G升级到64G, 从64G升级到128G, 无限升级内存. 原因是项目资金不够, 硬件条件跟不上.
    2. 结论: 用户放弃使用Redis.

结论就是, 为了避免单点Redis服务器故障, 准备多台服务器, 互相连通. 将数据复制多个副本保存在不同的服务器上, 连接在一起, 并保证数据是同步的. 即使有一台服务器宕机, 其他服务器依然可以继续提供服务, 实现Redis的高可用, 同事实现数据冗余备份.

9.1 主从复制

9.1.1 主从复制简介

主从复制即将master中的数据及时, 有效的复制到slave中.
特征: 一个master可以拥有多个slave, 一个slave只对应一个master.
职责:

  • master:
    1. 写数据
    2. 执行写操作时, 将出现变化的数据自动同步到slave
    3. 读数据(可忽略)
  • slave:
    1. 读数据
    2. 写数据(禁止)

在这里插入图片描述

主从复制的作用:

  • 读写分离: master写, slave读, 提高服务器的读写负载能力.
  • 负载均衡: 基于主从结构, 配合读写分离, 由slave分担master负载, 并根据需求的变化, 改变slave的数量, 通过多个从节点分担数据读取负载, 大大提高了Redis服务器并发量与数据吞吐量.
  • 故障恢复: 当master出现问题时, 由slave提供服务, 实现快速的故障恢复.
  • 数据冗余: 实现数据热备份, 是持久化之外的一种数据冗余方式.
  • 高可用基石: 基于主从复制, 构建哨兵模式与集群, 实现Redis的高可用方案.

9.1.2 主从复制工作流程

主从复制过程大体可以分为3个阶段

  • 建立连接阶段(即准备阶段)
  • 数据同步阶段
  • 命令传播阶段
    在这里插入图片描述
阶段一: 建立连接阶段
(1) 建立连接阶段工作流程

建立slave到master的连接, 使master能够识别slave, 并保存slave端口号.

  • 步骤1: 设置master的地址和端口, 保存master信息
  • 步骤2: 简历socket连接
  • 步骤3: 发送ping命令(定时器任务)
  • 步骤4: 身份验证
  • 步骤5: 发送slave端口信息.
    在这里插入图片描述

连接成功后, master和slave的状态分别是:

  • slave: 保存master的地址与端口
  • master: 保存slave的端口
  • 总体: 主从之间创建了连接的socket.
(2) 常用命令
  • 主从连接(slave连接master)
    1. 方式一: 客户端发送命令 slaveof <masterip> <masterport>
    2. 方式二: 启动服务器时配置参数 redis-server -slaveof <masterip> <masterport>
  • 主从断开连接
    客户端发送命令 slaveof no one
    slave断开连接后,不会删除已有数据,只是不再接受master发送的数据
  • 授权访问
    1. master客户端发发送命令设置密码 requirepass <password>
    2. master配置文件设置密码
      config set requirepass <password>
      config get requirepass
    3. slave客户端发送命令设置密码 auth <password>
    4. slave配置文件设置密码 masterauth <password>
    5. slave启动服务器设置密码 redis-server –a <password>
阶段二: 数据同步阶段工作流程

在slave初次连接master后, 复制master中的所有数据到slave, 将slave的数据库状态更新成master当前的数据库状态.

  • 步骤1: 请求同步数据
  • 步骤2: 创建RDB同步数据
  • 步骤3: 恢复RDB同步数据
  • 步骤4: 请求部分同步数据
  • 步骤5: 恢复部分同步数据
    在这里插入图片描述
阶段三: 命令传播阶段
  • 当master数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的
    状态,同步的动作称为命令传播
  • master将接收到的数据变更命令发送给slave,slave接收命令后执行命令

9.2 哨兵模式

9.2.1 哨兵简介

哨兵(sentinel)是一个分布式系统, 用于对主从结构中的每台服务器进行监控, 当出现故障时通过投票机制选择新的master并将所有slave连接到新的master.

在这里插入图片描述
哨兵的作用

  • 监控
    1. 不断的检查master和slave是否正常运行.
    2. master存活检测, master与slave运行情况检测.
  • 通知(提醒)
    1. 当被监控的服务器出现问题时, 向其他(哨兵间, 客户端)发送通知.
  • 自动故障转移
    1. 断开master与slave连接, 选取一个slave作为master, 将其他slave连接到新的master, 并告知客户端新的服务器地址.

注意: 哨兵也是多台redis服务器, 只是不提供数据服务, 通常哨兵配置数量为单数.

9.2.2 哨兵工作原理

哨兵的作用就是监控Redis集群, 一旦Redis主节点宕机了, 需要将从节点选一个主节点, 哨兵在进行主从切换过程中经历三个阶段.

  • 监控
  • 通知
  • 故障转移
阶段一: 监控阶段

哨兵节点用于同步各个节点的状态

  • 哨兵连接主节点, 查看是否存活.
  • 获取master的转态
    • master属性
      • runid
      • role: master
  • 获取所有slave的转态(根据master中的slave信息)
    • slave属性
      • runid
      • role: slave
      • master_host, master_port
      • offset
  • 哨兵连接从节点, 查看从节点是否存活
  • 哨兵集群内部也要互相通信, 也就是哨兵集群之间也会有自己的朋友圈, 哨兵-1会把消息同步给其他哨兵, 其他哨兵也会去连接主从节点.

在这里插入图片描述

阶段二: 通知阶段

通知阶段主要的目的就是哨兵定期发送消息到Redis主从节点, 同时哨兵集群内部也要同步消息, 保证每个哨兵节点存储的redis集群消息是一致的.

  • sentinel1(哨兵节点1)发送消息到主从节点, 观察redis集群健康转态
  • 哨兵1将得到的健康状态同步到哨兵2,哨兵3
  • 哨兵2也会去重复哨兵1的动作, 发送消息到主从节点, 并同步给其他哨兵节点
    在这里插入图片描述
阶段三: 故障转移阶段
  • 发现问题
    1. 哨兵-1 发送消息到master, 如果没有得到回复, 则把master标记为主观下线.
    2. 哨兵-1把master挂了的消息同步给哨兵-2, 哨兵-3, 然后哨兵-2和哨兵-3也发送消息到master确认是否能连通. 如果3个哨兵中超过一半也就是2个哨兵认为master挂了, 那么master就被标记为客观下线了.
      在这里插入图片描述
  • 哨兵集群内部竞选负责人, 负责人负责选出新的master.
    • 哨兵集群内部会进行投票选举, 类似于zk的选举机制, 选出哨兵负责人
  • 哨兵负责人优选新的master
    • 肯定要挑选在线的服务器
    • 剔除响应慢的服务器
    • 剔除与原master断开时间久的服务器
    • 剩下的就采用优先原则了
      • offset偏移量小的
      • runid小的
  • 新master上任, 其他slave切换master, 原master作为slave故障恢复后连接新master.

9.3 Redis-Cluster集群架构

9.3.1 集群架构简介

虽然前面介绍的Redis主从模式和Redis哨兵原理能够解决Redis单机问题, 但是业务发展过程中遇到的峰值瓶颈.

  • redis提供的服务OPS可以达到10万/秒,当前业务OPS已经达到10万/秒
  • 内存单机容量达到256G,当前业务需求内存容量1T

集群就是使用网络将若干台计算机联通起来,并提供统一的管理方式,使其对外呈现单机的服务效果.

  • 分散单台服务器的访问压力,实现负载均衡
  • 分散单台服务器的存储压力,实现可扩展性
  • 降低单台服务器宕机带来的业务灾难
    在这里插入图片描述

9.3.2 Redis-Cluster集群结构设计原理

其实Redis集群就是将多个服务器横向扩展在一起, 存储原理底层也是利用碎片化管理进行的.

数据存储设计

Redis的数据结构就是key-value的模式进行存储,存储时, key经过一定运算进行设计存储.
在这里插入图片描述

  • 通过算法设计, 计算出key应该保存的位置
  • 将所有的存储空间计划切割成16384份, 每台主机保存一部分, 每份代表的是一个存储空间(注意: 不是一个key独占一份存储空间), 每一份是一个槽
  • 将key按照计算出的结果放到对应的存储空间.
  • 增加可扩展性.
    在这里插入图片描述
集群内部通讯设计
  • 各个数据库相互通信, 保存各个库中槽的编号数据
    • 比如, A节点保存编号1-10的槽, B节点保存编号11-20的槽, C节点保存编号21-30的槽, D节点保存编号31-40的槽空间, 等等…
  • 每台节点上都保存着其他节点的槽编号信息.
  • 客户端查询redis中的数据时, 会访问集群中的某一个节点, 如果key的存储位置正好在该节点的槽空间上, 则直接返回客户端结果.
  • 如果key的位置没有在该节点上, 则会告诉客户端对应的节点, 客户端再去对应节点上读取数据.
    在这里插入图片描述

10. Redis缺点

10.1 缓存和数据库双写一致性问题

  • 一致性的问题很常见, 因为数据数据加入到Redis缓存之后, 请求是先从Redis中查询. 如果Redis中有要查询的数据就不会再查询数据库了. 但是, 如果不能保证Redis和数据库的一致性, 就会导致请求获取到的数据不是最新的数据.
  • 如何解决?
  1. 编写删除Redis缓存的接口, 在更新数据库的同时,调用删除Redis缓存的接口删除缓存中的数据. 用户下次访问就是直接访问数据库了同时也把最新数据更新到Redis当中了.
  2. 使用消息队列的方式,将要更新的数据放到消息队列中.

10.2 缓存的并发竞争问题

  • 并发竞争: 指的是同时有多个子系统, 去获取同一个key的value值.
  • 如何解决?
    最简单的方式就是准备一个分布式锁, 大家去抢锁, 抢到锁就可以去获取key的值了.
    后面详细介绍Redis的分布式锁

10.3 缓存雪崩问题

  • 缓存雪崩, 即Redis缓存中的数据, 同一时间大面积的失效, 这个时候又来了一波请求, 结果请求都怼到数据库上, 从而导致数据库连接异常.

  • 举个栗子:如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。
    此时 6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA 都没反应过来直接挂了。
    此时,如果没什么特别的方案来处理,DBA 很着急,重启数据库,但是数据库立马又被新流量给打死了。这就是应用到项目场景中的缓存雪崩。

  • 如何解决?

  1. 在Redis缓存的失效时间上, 加一个随机值,避免集体同时失效。
    setRedis(key, value, time+Math.random()*10000);
  2. 使用互斥锁,但是该方案吞吐量明显下降了。
  3. 搭建Redis集群, 将热点数据均匀分布在不同的 Redis 库中也能避免全部失效.

10.4 缓存击穿

  • 缓存击穿, 这个跟缓存雪崩有点像, 但是又有一点不一样, 缓存雪崩是因为大面积的缓存失效, 数据库崩溃了,
  • 而缓存击穿不同的是缓存击穿是指存在一个热点数据Key, 有请求不断来访问这个Key,这么多请求在同一段时间内访问这个热点数据, 当这个 Key 失效时间到了的时候, 持续的这么多请求直接怂到数据库上了, 就在这个 Key 值上击穿了缓存.
  • 如何解决?
    设置热点数据永不过期或者加上互斥锁就搞定了.

10.5 缓存穿透

  • 缓存穿透, 缓存穿透是指访问缓存和数据库中都没有的数据, 用户(黑客)恶意访问发起请求.
  • 举个栗子:我们数据库的 id 都是从 1 自增的, 如果发起 id=-1的请求,这样的不断攻击导致数据库压力很, 严重会击垮数据库。
  • 如何解决呢?
  1. 缓存穿透我会在接口层增加校验, 比如用户鉴权, 参数做校验, 不合法的校验直接return, 比如 id 做基础校验, id<=0 直接拦截过滤掉.
  2. Redis 里还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的预防缓存穿透的发生。
    它的原理也很简单, 就是利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在, 不存在你 return 就好了, 存在你就去查 数据库刷新 KV 再 return.

11 实际开发中Redis如何部署

11.1 基础配置

主要针对redis.conf配置文件

11.1.1 基础配置

  • 设置服务器以守护进程的方式运行
daemonize yes | no
  • 绑定主机地址: 指的是redis所在的主机服务器地址
bind 127.0.0.1
  • 设置服务器端口号: 指的是redis所在的主机服务器地址
port 6379
  • 设置数据库数量: 能创建多少个数据库
database 16

11.1.2 日志配置

  • 设置服务器以指定日志记录级别
loglevel debug | verbose | notice | warning
  • 日志记录文件名
logfile 6379.log

注意: 日志级别开发期设置为verbose即可, 生产环境中配置notice, 简化日志输出量, 降低写日志IO的频度.

11.1.3 多服务器快捷配置

  • 导入并加载指定配置文件信息, 用于快速创建redis公共配置较多的redis实例配置文件, 便于维护
include /path/server-6379.conf

11.2 Redis实际开发部署了几台

  • 采用Redis cluster集群模式,10台机器,5台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例.
  • 5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。

11.3 机器是什么配置?

  • 32G内存+8核CPU+1T磁盘.
  • 分配给Redis进程的是10g内存,一般线上生产环境,Redis的内存尽量不要超过10g,超过10g可能会有问题。
  • 5台机器对外提供读写,一共有50g内存。
  • 因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis从实例会自动变成主实例继续提供读写服务.

11.4 往内存里写的是什么数据?每条数据的大小是多少?

  • 商品数据,每条数据是10kb。100条数据是1mb,10万条数据是1g。常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存的50%。

  • 目前高峰期每秒就是3500左右的请求量

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值