当前位置: 首页 > news >正文

建设医院网站ppt模板下载商赢网站建设

建设医院网站ppt模板下载,商赢网站建设,flash在线制作网站,重庆最新消息今天封城了系列文章目录 第一章 C/C语言篇第二章 计算机网络篇第三章 操作系统篇第四章 数据库MySQL篇第五章 数据库Redis篇第六章 场景题/算法题第七篇 常见HR问题篇 本系列专栏#xff1a;点击进入 后端开发面经 关注走一波 秋招阶段#xff0c;面过很多大中小厂#xff0c;积攒了…系列文章目录 第一章 C/C语言篇第二章 计算机网络篇第三章 操作系统篇第四章 数据库MySQL篇第五章 数据库Redis篇第六章 场景题/算法题第七篇 常见HR问题篇 本系列专栏点击进入 后端开发面经 关注走一波 秋招阶段面过很多大中小厂积攒了很多面经都是高频问题 前言本系列文章初衷是为了整理出最全面最详细的面经非常适用于想走后端/软件开发的同学近些年越来越多的人投入互联网的浪潮由于岗位hc有限企业筛选门槛也随之提高。以往的简单八股问答也在不断升级面试官开始更喜欢问为什么会围绕八股的某一点不断深问。所以本系列文章的面经不仅仅是简单问答而是帮你深入理解和掌握知识点其中一些晦涩难懂的知识点全都用案例和代码帮你彻底掌握切记一定要理解原理拒绝死记硬背 文章目录 系列文章目录关系型数据库和非关系数据库的区别MySQL、MongoDB和Redis的区别Redis相当于MySQL的优缺点Redis常用指令redis各种数据类型的应用场景redis各种数据类型的底层结构Redis数据结构-RedisObjectRedis数据结构-StringRedis数据结构-ListRedis数据结构-Set结构Redis数据结构-ZSETRedis数据结构-Hash redis持久化机制详解快照持久化-RDB (Redis Database)增量持久化-AOF (Append Only File)RDB与AOF对比 redis单线程模型redis 为什么快Redis发布订阅介绍 redis 缓存穿透问题以及解决思路介绍 redis 缓存雪崩问题以及解决思路介绍 redis 缓存击穿问题以及解决思路redis中热key和大key怎么处理redis内存回收机制redis分布式锁的实现为什么需要Redis主从复制主从节点的同步机制是什么?全量同步增量同步 主从同步优化Redis哨兵机制redis集群怎么实现数据分布式存储redis客户端对分布式集群发起一个查询介绍整个过程同一份数据既要在MySQL存储又要在Redis存储要怎么设计实现 关系型数据库和非关系数据库的区别 关系型数据库是一种使用表格形式存储数据的数据库系统数据通过行和列组织并且表之间可以通过外键建立关系常见的有MySQL、PostgreSQL和Oracle等。非关系型数据库则不采用固定的表格结构可以支持多种数据存储模型如文档、键值、图和列族适合处理大规模和多样化的数据常见的有MongoDB、Redis和Cassandra等。 下面是它们的主要区别 对比类型关系型数据库非关系数据库数据结构表格形式文档型mongdb、键值型redis事务支持支持ACID事务保证强一致性最终一致性不是严格的事务管理灵活性数据结构固定表结构的变更昂贵复杂数据结构灵活存储形式多样查询语句SQL语句类 SQLmongdb、命令行操作redis适用场景适合需要复杂查询和事务支持的应用适合需要高可扩展性、灵活数据模型和快速读写的应用 MySQL、MongoDB和Redis的区别 数据库MySQLMongoDBRedis数据模型关系型数据库非关系型数据库非关系型数据库存储数据类型二维表结构BSON格式文档存储键值对数据结构数值、时间、字符串等常用类型除常用类型还支持数组和字典字符串、列表、集合、哈希等存储方式不同引擎有不同的存储基于内存热数据存放物理内存高速读写完全基于内存持久化可选查询方式SQL语句基于文档的查询语言基于键的简单操作多表联查一对多、多对多情况下使用外键JOIN查询嵌套或者通过ID引用其他集合不支持联查事务支持支持仅支持单文档事务支持简单事务数据一致性强一致性保证在所有副本上的数据是一致的会牺牲一致性来获得更好的性能采用最终一致性支持单线程操作强一致性 Redis相当于MySQL的优缺点 Redis 优点 高性能Redis 是内存数据库读写速度非常快适合对性能要求高的应用。数据结构丰富支持字符串、哈希、列表、集合、有序集合等多种数据类型灵活性强。支持持久化可以将内存数据持久化到磁盘支持 RDB 和 AOF 两种持久化方式。高可扩展性支持分布式部署易于扩展。适合缓存常用于缓存数据减少数据库压力提高访问速度。 Redis 缺点 内存限制数据存储在内存中受限于服务器内存大小适合存储小到中型数据集。复杂查询支持弱不支持复杂的 SQL 查询如联接和聚合操作。数据一致性问题由于是内存数据库数据持久化时可能存在数据丢失的风险尤其是在系统崩溃的情况下。 Redis常用指令 字符串操作 SET key value设置键的值。 GET key获取键的值。 DEL key删除键。 INCR key将键的值加 1。 DECR key将键的值减 1。 APPEND key value将值追加到键的值后。 哈希操作 HSET key field value设置哈希表中的字段值。 HGET key field获取哈希表中的字段值。 HDEL key field删除哈希表中的字段。 HGETALL key获取哈希表中的所有字段和值。 列表操作 LPUSH key value在列表头部添加一个值。 RPUSH key value在列表尾部添加一个值。 LPOP key移除并返回列表的第一个元素。 RPOP key移除并返回列表的最后一个元素。 LRANGE key start stop获取列表指定范围内的元素。 集合操作 SADD key member向集合添加成员。 SREM key member从集合中删除成员。 SMEMBERS key获取集合中的所有成员。 SISMEMBER key member检查成员是否在集合中。 有序集合操作 ZADD key score member向有序集合添加成员。 ZRANGE key start stop获取有序集合中指定范围内的成员。 ZREM key member从有序集合中删除成员。 ZScore key member获取成员的分数。 事务 MULTI标记事务的开始。 EXEC执行事务。 DISCARD放弃事务。 WATCH key监视一个或多个键以实现乐观锁。 服务器管理 INFO获取服务器信息和统计数据。 FLUSHDB清空当前数据库。 FLUSHALL清空所有数据库。 SAVE将数据持久化到磁盘。 BGSAVE在后台异步保存数据到磁盘。 其他 EXPIRE key seconds设置键的过期时间。 TTL key获取键的剩余生存时间。 PING测试与服务器的连接。 redis各种数据类型的应用场景 string计数、存储特定字符串变量。底层采用SDS简单动态字符串raw编码、embstr编码、int编码。list用来充当栈RPUSH、RPOP、队列LPUSH、RPOP。底层采用QuickList结构双向链表ziplist之前版本用ziplist。set可以得到交集、并集、差集用来找共同关注共同爱好推荐好友等。底层用字典结构hash数据量小的时候用intset。zset需要用到排序的时候如班级成绩表、工资表排序。底层用字典SkipList数据量小的时候用ZipList。hash适合于对象的存储如用户信息之类的。底层用字典结构hash数据量小的时候用ZipList。Geospatial用于存储地理位置和执行与地理位置相关的操作比如位置服务、区域分析、路径计算等。Hyperloglog是一种概率性的数据结构主要用于基数估算适用场景包括访客计数、去重分析、数据分析。BitmapBitmap 是一种高效的位数组用于处理大量的二进制状态常见应用场景包括用户活跃度追踪、广告点击率等。 redis各种数据类型的底层结构 Redis数据结构-RedisObject Redis中的任意数据类型的键和值都会被封装为一个RedisObject也叫做Redis对象。 什么是 redisObject 从Redis的使用者的角度来看⼀个Redis节点包含多个database非cluster模式下默认是16个cluster模式下只能是1个而一个database维护了从key space到object space的映射关系。这个映射关系的key是string类型⽽value可以是多种数据类型比如string, list, hash、set、sorted set等。我们可以看到key的类型固定是string而value可能的类型是多个。 ⽽从Redis内部实现的角度来看database内的这个映射关系是用⼀个dict来维护的。dict的key固定用⼀种数据结构来表达就够了这就是动态字符串sds。而value则比较复杂为了在同⼀个dict内能够存储不同类型的value这就需要⼀个通⽤的数据结构这个通用的数据结构就是robj全名是redisObject。 Redis的编码方式 Redis中会根据存储的数据类型不同选择不同的编码方式共包含11种不同类型 编号编码方式说明0OBJ_ENCODING_RAWraw编码动态字符串1OBJ_ENCODING_INTlong类型的整数的字符串2OBJ_ENCODING_HThash表字典dict3OBJ_ENCODING_ZIPMAP已废弃4OBJ_ENCODING_LINKEDLIST双端链表5OBJ_ENCODING_ZIPLIST压缩列表6OBJ_ENCODING_INTSET整数集合7OBJ_ENCODING_SKIPLIST跳表8OBJ_ENCODING_EMBSTRembstr的动态字符串9OBJ_ENCODING_QUICKLIST快速列表10OBJ_ENCODING_STREAMStream流 五种数据结构 Redis中会根据存储的数据类型不同选择不同的编码方式。每种数据类型的使用的编码方式如下 数据类型编码方式OBJ_STRINGint、embstr、rawOBJ_LISTLinkedList和ZipList(3.2以前)、QuickList3.2以后OBJ_SETintset、HTOBJ_ZSETZipList、HT、SkipListOBJ_HASHZipList、HT 下面将详细介绍五种数据类型的底层原理。 Redis数据结构-String 我们都知道Redis中保存的Key是字符串value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。 不过Redis没有直接使用C语言中的字符串因为C语言字符串存在很多问题 获取字符串长度的需要通过运算非二进制安全不可修改 Redis构建了一种新的字符串结构称为简单动态字符串Simple Dynamic String简称SDS。 例如我们执行命令 那么Redis将在底层创建两个SDS其中一个是包含“name”的SDS另一个是包含“虎哥”的SDS。 Redis是C语言实现的其中SDS是一个结构体源码如下 例如一个包含字符串“name”的sds结构如下 SDS之所以叫做动态字符串是因为它具备动态扩容的能力例如一个内容为“hi”的SDS 假如我们要给SDS追加一段字符串“,Amy”这里首先会申请新内存空间 如果新字符串小于1M则新空间为扩展后字符串长度的两倍1 如果新字符串大于1M则新空间为扩展后字符串长度1M1。称为内存预分配。 String是Redis中最常见的数据存储类型其基本编码方式是RAW基于简单动态字符串SDS实现存储上限为512mb。 如果存储的SDS长度小于44字节则会采用EMBSTR编码此时object head与SDS是一段连续空间。申请内存时只需要调用一次内存分配函数效率更高。 底层实现⽅式动态字符串sds 或者 long String的内部存储结构⼀般是sdsSimple Dynamic String可以动态扩展内存但是如果⼀个String类型的value的值是数字那么Redis内部会把它转成long类型来存储从⽽减少内存的使用。 如果存储的字符串是整数值并且大小在LONG_MAX范围内则会采用INT编码直接将数据保存在RedisObject的ptr指针位置刚好8字节不再需要SDS了。 确切地说String在Redis中是⽤⼀个robj来表示的。 用来表示String的robj可能编码成3种内部表⽰OBJ_ENCODING_RAWOBJ_ENCODING_EMBSTROBJ_ENCODING_INT。 其中前两种编码使⽤的是sds来存储最后⼀种OBJ_ENCODING_INT编码直接把string存成了long型。 在对string进行incr, decr等操作的时候如果它内部是 OBJ_ENCODING_INT 编码那么可以直接行加减操作如果它内部是OBJ_ENCODING_RAW或OBJ_ENCODING_EMBSTR编码那么Redis会先试图把sds存储的字符串转成long型如果能转成功再进行加减操作。对⼀个内部表示成long型的string执行append, setbit, getrange这些命令针对的仍然是string的值即⼗进制表示的字符串而不是针对内部表⽰的long型进⾏操作。比如字符串”32”如果按照字符数组来解释它包含两个字符它们的ASCII码分别是0x33和0x32。当我们执行命令setbit key 7 0的时候相当于把字符0x33变成了0x32这样字符串的值就变成了”22”。⽽如果将字符串”32”按照内部的64位long型来解释那么它是0x0000000000000020在这个基础上执⾏setbit位操作结果就完全不对了。因此在这些命令的实现中会把long型先转成字符串再进行相应的操作。 Redis数据结构-List Redis的List类型可以从首、尾操作列表中的元素 哪一个数据结构能满足上述特征redis提供了几个关于链表的数据结构如下 LinkedList 普通链表可以从双端访问内存占用较高内存碎片较多。ZipList 压缩列表可以从双端访问内存占用低存储上限低。QuickListLinkedList ZipList可以从双端访问内存占用较低包含多个ZipList存储上限高。 1LinkedList 这个就是普通的双端链表属于常用数据结构这里就不多讲这个类型了把重点放在下面两个类型中。 2ZipList ZipList 是一种特殊的“双端链表” 由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1)。 属性类型长度用途zlbytesuint32_t4 字节记录整个压缩列表占用的内存字节数zltailuint32_t4 字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节通过这个偏移量可以确定表尾节点的地址。zllenuint16_t2 字节记录了压缩列表包含的节点数量。 最大值为UINT16_MAX 65534如果超过这个值此处会记录为65535但节点的真实数量需要遍历整个压缩列表才能计算得出。entry列表节点不定压缩列表包含的各个节点节点的长度由节点保存的内容决定。zlenduint8_t1 字节特殊值 0xFF 十进制 255 用于标记压缩列表的末端。 ZipListEntry ZipList 中的Entry并不像普通链表那样记录前后节点的指针因为记录两个指针要占用16个字节浪费内存。而是采用了下面的结构 previous_entry_length前一节点的长度占1个或5个字节。 如果前一节点的长度小于254字节则采用1个字节来保存这个长度值如果前一节点的长度大于254字节则采用5个字节来保存这个长度值第一个字节为0xfe后四个字节才是真实长度数据 encoding编码属性记录content的数据类型字符串还是整数以及长度占用1个、2个或5个字节contents负责保存节点的数据可以是字符串或整数 ZipList中所有存储长度的数值均采用小端字节序即低位字节在前高位字节在后。例如数值0x1234采用小端字节序后实际存储值为0x3412 Encoding编码 ZipListEntry中的encoding编码分为字符串和整数两种 字符串如果encoding是以“00”、“01”或者“10”开头则证明content是字符串 编码编码长度字符串大小|00pppppp|1 bytes 63 bytes|01pppppp|qqqqqqqq|2 bytes 16383 bytes|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|5 bytes 4294967295 bytes 例如我们要保存字符串“ab”和 “bc” ZipListEntry中的encoding编码分为字符串和整数两种 整数如果encoding是以“11”开始则证明content是整数且encoding固定只占用1个字节 编码编码长度整数类型110000001int16_t2 bytes110100001int32_t4 bytes111000001int64_t8 bytes11110000124位有符整数(3 bytes)1111111018位有符整数(1 bytes)1111xxxx1直接在xxxx位置保存数值范围从0001~1101减1后结果为实际值 ZipList的连锁更新问题 ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小长度是1个或5个字节 如果前一节点的长度小于254字节则采用1个字节来保存这个长度值如果前一节点的长度大于等于254字节则采用5个字节来保存这个长度值第一个字节为0xfe后四个字节才是真实长度数据 现在假设我们有N个连续的、长度为250~253字节之间的entry因此entry的previous_entry_length属性用1个字节即可表示如图所示 ZipList这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新Cascade Update。新增、删除都可能导致连锁更新的发生。 ZipList特性 压缩列表的可以看做一种连续内存空间的双向链表。列表的节点之间不是通过指针连接而是记录上一节点和本节点长度来寻址内存占用较低。如果列表数据过多导致链表过长可能影响查询性能。增或删较大数据时有可能发生连续更新问题。 3QuickList 问题1ZipList虽然节省内存但申请内存必须是连续空间如果内存占用较多申请内存效率很低。怎么办 答为了缓解这个问题我们必须限制ZipList的长度和entry大小。 问题2但是我们要存储大量数据超出了ZipList最佳的上限该怎么办 答我们可以创建多个ZipList来分片存储数据。 问题3数据拆分后比较分散不方便管理和查找这多个ZipList如何建立联系 答Redis在3.2版本引入了新的数据结构QuickList它是一个双端链表只不过链表中的每个节点都是一个ZipList。 为了避免QuickList中的每个ZipList中entry过多Redis提供了一个配置项list-max-ziplist-size来限制。 如果值为正则代表ZipList的允许的entry个数的最大值 如果值为负则代表ZipList的最大内存大小其默认值为 -2分5种情况 -1每个ZipList的内存占用不能超过4kb-2每个ZipList的内存占用不能超过8kb-3每个ZipList的内存占用不能超过16kb-4每个ZipList的内存占用不能超过32kb-5每个ZipList的内存占用不能超过64kb 以下是QuickList的和QuickListNode的结构源码 我们接下来用一段流程图来描述当前的这个结构 QuickList的特点 是一个节点为ZipList的双端链表。节点采用ZipList解决了传统链表的内存占用问题。控制了ZipList大小解决连续内存空间申请效率问题。中间节点可以压缩进一步节省了内存。 综上所述List 底层原理如下 在3.2版本之前Redis采用ZipList和LinkedList来实现List当元素数量小于512并且元素大小小于64字节时采用ZipList编码超过则采用LinkedList编码。在3.2版本之后Redis统一采用QuickList来实现List。 Redis数据结构-Set结构 Set是Redis中的单列集合满足下列特点 不保证有序性保证元素唯一求交集、并集、差集 可以看出Set对查询元素的效率要求非常高思考一下什么样的数据结构可以满足 HashTable也就是Redis中的Dict不过Dict是双列集合可以存键、值对。下面来详细介绍一下 Dict 数据结构 我们知道Redis是一个键值型Key-Value Pair的数据库我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。 Dict由三部分组成分别是哈希表DictHashTable、哈希节点DictEntry、字典Dict 当我们向Dict添加键值对时Redis首先根据key计算出hash值h然后利用 h sizemask来计算元素应该存储到数组中的哪个索引位置。我们存储k1v1假设k1的哈希值h 1则13 1因此k1v1要存储到数组角标1位置。 Dict由三部分组成分别是哈希表DictHashTable、哈希节点DictEntry、字典Dict源码如下 数据结构如下 Dict的结构 类似java的HashTable底层是数组加链表来解决哈希冲突Dict包含两个哈希表ht[0]平常用ht[1]用来rehash Set是Redis中的集合不一定确保元素有序可以满足元素唯一、查询效率要求极高。 为了查询效率和唯一性set采用HT编码Dict。Dict中的key用来存储元素value统一为null。 当存储的所有数据都是整数并且元素数量不超过set-max-intset-entries时Set会采用IntSet编码以节省内存 介绍一下 Dict 的扩容即 rehash Dict中的HashTable就是数组结合单向链表的实现当集合中元素较多时必然导致哈希冲突增多链表过长则查询效率会大大降低。 Dict在每次新增键值对时都会检查负载因子LoadFactor used/size 满足以下两种情况时会触发哈希表扩容 哈希表的 LoadFactor 1并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程哈希表的 LoadFactor 5 源码如下 不管是扩容还是收缩必定会创建新的哈希表导致哈希表的size和sizemask变化而key的查询与sizemask有关。因此必须对哈希表中的每一个key重新计算索引插入新的哈希表这个过程称为rehash。过程是这样的 计算新hash表的realeSize值取决于当前要做的是扩容还是收缩 如果是扩容则新size为第一个大于等于dict.ht[0].used 1的2^n如果是收缩则新size为第一个大于等于dict.ht[0].used的2^n 不得小于4 按照新的realeSize申请内存空间创建dictht并赋值给dict.ht[1]设置dict.rehashidx 0标示开始rehash将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]在每次增删查改时进行一次迁移操作渐进式rehash将dict.ht[1]赋值给dict.ht[0]给dict.ht[1]初始化为空哈希表释放原来的dict.ht[0]的内存将rehashidx赋值为-1代表rehash结束在rehash过程中新增操作则直接写入ht[1]查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增随着rehash最终为空 Redis数据结构-ZSET ZSet也就是SortedSet其中每一个元素都需要指定一个score值和member值 可以根据score值排序后member必须唯一可以根据member查询分数 因此zset底层数据结构必须满足键值存储、键必须唯一、可排序、根据key找value这几个需求。需要下面的数据结构 SkipList可以排序并且可以同时存储score和ele值memberHTDict可以键值存储并且可以根据key找value 源码如下 当元素数量不多时HT和SkipList的优势不明显而且更耗内存。因此zset还会采用ZipList结构来节省内存不过需要同时满足两个条件 元素数量小于zset_max_ziplist_entries默认值128每个元素都小于zset_max_ziplist_value字节默认值64 ziplist本身没有排序功能而且没有键值对的概念因此需要有zset通过编码实现 ZipList是连续内存因此score和element是紧挨在一起的两个entry element在前score在后score越小越接近队首score越大越接近队尾按照score值升序排列 Redis数据结构-Hash Hash结构与Redis中的Zset非常类似 都是键值存储都需求根据键获取值键必须唯一 区别如下 zset的键是member值是scorehash的键和值都是任意值zset要根据score排序hash则无需排序 底层实现方式压缩列表ziplist 或者 字典dict 当Hash中数据项比较少的情况下Hash底层才⽤压缩列表ziplist进⾏存储数据随着数据的增加底层的ziplist就可能会转成dict具体配置如下 hash-max-ziplist-entries 512hash-max-ziplist-value 64 当满足上面两个条件其中之⼀的时候Redis就使⽤dict字典来实现hash。 Redis的hash之所以这样设计是因为当ziplist变得很大的时候它有如下几个缺点 每次插⼊或修改引发的realloc操作会有更⼤的概率造成内存拷贝从而降低性能。⼀旦发生内存拷贝内存拷贝的成本也相应增加因为要拷贝更⼤的⼀块数据。当ziplist数据项过多的时候在它上⾯查找指定的数据项就会性能变得很低因为ziplist上的查找需要进行遍历。 总之ziplist本来就设计为各个数据项挨在⼀起组成连续的内存空间这种结构并不擅长做修改操作。⼀旦数据发⽣改动就会引发内存realloc可能导致内存拷贝。 hash结构如下 zset集合如下 因此Hash底层采用的编码与Zset也基本一致只需要把排序有关的SkipList去掉即可 Hash结构默认采用ZipList编码用以节省内存。 ZipList中相邻的两个entry 分别保存field和value 当数据量较大时Hash结构会转为HT编码也就是Dict触发条件有两个 ZipList中的元素数量超过了hash-max-ziplist-entries默认512ZipList中的任意entry大小超过了hash-max-ziplist-value默认64字节 redis持久化机制详解 Redis 是内存数据库如果不将内存中的数据库状态保存到磁盘那么一旦服务器进程退出服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能。 快照持久化-RDB (Redis Database) RDB全称Redis Database Backup fileRedis数据备份文件也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后从磁盘读取快照文件恢复数据。快照文件称为RDB文件默认是保存在当前运行目录。 按照配置 隔一段时间fork一个子进程把父进程的内存数据全部copy到磁盘文件。先将数据写入到一个临时文件中等持久化过程都结束了再用这个临时文件替换上次持久化好的文件dump.rdbRedis 默认只保留最近一次持久化的 RDB 文件以保持最新的数据库状态。整个过程中主进程是不进行任何IO操作的这就确保了极高的性能。将来server重启直接把数据从磁盘加载到内存即可。 基于copy on write机制fork并不会拷贝出完整的内存而是共享父进程内存但是只限于父子进程都只读如果任意进程出现写操作一般为父进程有client写redis则还是会复制两份内容父子进程各一份父进程可继续写入子进程持久化完后回收内存。 copy on write的实现原理 fork()之后kernel把父进程中所有的内存页的权限都设为read-only然后子进程的地址空间指向父进程。当父子进程都只读内存时相安无事。当其中某个进程写内存时如父进程有client写redisCPU硬件检测到内存页是read-only的于是触发页异常中断page-faultkernel就会把触发的异常的页复制一份于是父子进程各自持有独立的一份这时就相当于复制了两份子进程的内存用完回收父进程可以在持久化过程中继续写入。 Redis 触发 RDB 持久化的方式有以下几种 Redis 配置文件 (redis.conf) 中通过 save 指令配置在一定时间内发生一定次数写操作时触发 RDB 持久化。默认配置如下 save 900 1 # 900秒15分钟内至少有1次写操作 save 300 10 # 300秒5分钟内至少有10次写操作 save 60 10000 # 60秒1分钟内至少有10000次写操作命令手动触发 RDB 持久化 BGSAVE # 后台异步保存数据库快照不会阻塞客户端请求 SAVE # 同步保存数据库快照会阻塞所有客户端请求直到保存操作完成 FLUSHALL # 清空命令执行后会立即进行一次持久化操作Redis 正常关闭时会自动触发 RDB 持久化操作 如果需要恢复不同版本的.rdb文件只需把.rdb文件放到redis启动目录下redis启动的时候会自动检查 dump.rdb 恢复其中的数据。 优点 加载速度快RDB 文件是一个紧凑的二进制文件加载 RDB 文件可以快速恢复数据磁盘I/O少RDB 持久化是通过定期的快照操作完成的较少的磁盘 I/O 操作fork开销小得益于copy on write机制fork并不会拷贝出完整的内存。所以fork本身的开销较少拷贝的数据比较精确内存有啥就copy啥不会出现冗余的磁盘占用非常适合备份 不足 数据丢失风险由于RDB是定期生成快照如果在两次快照之间Redis服务器宕机那么上次快照之后的所有数据将会丢失资源占用大在创建快照期间会消耗大量的 CPU 和内存资源。 增量持久化-AOF (Append Only File) AOF全称为Append Only File追加文件。Redis处理的每一个写命令都会记录在AOF文件可以看做是命令日志文件。 将我们的所有写命令都记录下来恢复的时候就把这个文件全部在执行一遍以日志的形式来记录每个写操作将Redis执行过得所有指令记录下来读操作不记录只许追加文件但不可以改写文件Redis启动之初会读取该文件重新构建数据即根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 AOF 默认就是文件的无限追加文件会越来越大。如果 AOF 文件大于默认 64MB会fork一个子进程来将文件进行重写。子进程根据当前内存中的数据生成新的 AOF 文件而主进程继续处理客户端请求。在子进程进行重写的同时新的写命令会被追加到一个 重写缓冲区中。重写完成后将重写缓冲区中的命令追加到新的 AOF 文件然后用新的 AOF 文件替换旧的 AOF 文件。 重写相当于丢弃之前记录的所有缓存从当前时刻内存中的数据开始重新记录。同时在重写过程中发生的写操作也要记录。 aof有三种主要的同步策略 appendfsync always # 每个命令后立即同步 appendfsync everysec # 每秒同步一次 appendfsync no # 从不同步即不会主动进行同步操作操作系统会根据自己的调度策略来决定何时将数据写入appendonly.aof优点 更高的数据安全性每次写操作后很快同步到磁盘可恢复性强AOF 文件是可读的容易进行修改或修复。Redis 在重启时会自动检测 AOF 文件的完整性并尝试修复不完整的日志记录。 不足 磁盘I/O量大每次写操作后都会记录日志这会导致更多的磁盘写入操作文件大小增长长时间的 AOF 可能会导致过大的日志文件。持久化日志将变成一个很冗余的文件加载速度慢恢复数据时需要重新执行文件中的所有写操作命令这比直接加载 RDB 文件要慢。 RDB与AOF对比 RDB和AOF各有自己的优缺点如果对数据安全性要求较高在实际开发中往往会结合两者来使用。 redis单线程模型 对于 redis 开始设计时使用单线程模型主要有如下的考虑 通过 I/O 多路复用redis 也可以很好的并发处理客户端的请求。redis 的使用瓶颈并不是 CPU影响 redis的效率主要是内存和网络请求如果 redis 提供的 1s 处理百万请求仍然无法满足要求可以同时启动多个 redis 实例使用分片的方式把请求交给不同的 redis 服务。对于内存因为 redis 的所有操作都是在内存中进行数据都在内存中操作都是纳秒级的同时它的网络协议也会简单读写解码也都很快除了持久化会数据保存到硬盘会消耗较多时间。如果 redis 使用多线程不可避免的要增加对多线程的处理线程切换因为并发所要引入的加锁解锁死锁等机制所带来的额外消耗反而是一种负优化。 所以影响 redis 性能的主要是网络 IO因此 redis6.0 中引入的多线程去优化了网络的 IO 请求时间I/O 多路复用主要是让线程可以监听连接是否可读写但是 IO 数据的读写仍然是阻塞当 socket 里有数据的时候redis 会通过系统调用将数据从内核态拷贝到用户态供 redis 解析数据量越大时间消耗也就越大在 redis6.0 中引入多线程机制主要是将主线程的 IO 读写任务拆分出来给一组独立的线程执行使得多个 socket 的读写可以并行化这样可以极大的提高效率。 而对于 redis4.0 中引入的多线程主要是为了删除超大键值对的数据当执行了 UNLINK 操作后会将键从元数据中删除释放内存空间的操作则会在后台异步执行这样避免了删除操作阻塞待处理的任务提高执行效率。 redis 为什么快 redis 的使用瓶颈并不是 CPU影响 redis的效率主要是内存和网络请求。 基于内存操作速度快不需要进行磁盘IO引入多线程和IO多路复用用于处理客户端请求。 Redis发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式发送者(pub)发送消息订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。 订阅/发布消息图第一个消息发送者 第二个频道第三个消息订阅者。 当有新消息通过 PUBLISH 命令发送给频道 channel1 时 这个消息就会被发送给订阅它的客户端 Redis是使用C实现的通过分析 Redis 源码里的 pubsub.c 文件了解发布和订阅机制的底层实现籍此加深对 Redis 的理解。 Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。 通过 SUBSCRIBE 命令订阅某频道后redis-server 里维护了一个字典字典的键就是一个个频道而字典的值则是一个链表链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键就是将客户端添加到给定 channel 的订阅链表中。 通过 PUBLISH 命令向订阅者发送消息redis-server 会使用给定的频道作为键在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表遍历这个链表将消息发布给所有订阅者。 Pub/Sub 从字面上理解就是发布Publish与订阅Subscribe在Redis中你可以设定对某一个key值进行消息发布及消息订阅当一个key值上进行了消息发布后所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统比如普通的即时聊天群聊等功能。 使用场景实时消息系统、实时聊天室、订阅系统关注系统。稍微复杂的场景我们就会使用 消息中间件 MQ。 介绍 redis 缓存穿透问题以及解决思路 缓存穿透 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在这样缓存永远不会生效这些请求都会打到数据库。 常见的解决方案有两种 缓存空对象 优点实现简单维护方便缺点 额外的内存消耗可能造成短期的不一致 布隆过滤 优点内存占用较少没有多余key缺点 实现复杂存在误判可能 缓存空对象思路分析当我们客户端访问不存在的数据时先请求redis但是此时redis中没有数据此时会访问到数据库但是数据库中也没有数据这个数据穿透了缓存直击数据库我们都知道数据库能够承载的并发不如redis这么高如果大量的请求同时过来访问这种不存在的数据这些请求就都会访问到数据库简单的解决方案就是哪怕这个数据在数据库中也不存在我们也把这个数据存入到redis中去这样下次用户过来访问这个不存在的数据那么在redis中也能找到这个数据就不会进入到缓存了 布隆过滤布隆过滤器其实采用的是哈希思想来解决这个问题通过一个庞大的二进制数组走哈希思想去判断当前这个要查询的这个数据是否存在如果布隆过滤器判断存在则放行这个请求会去访问redis哪怕此时redis中的数据过期了但是数据库中一定存在这个数据在数据库中查询出来这个数据后再将其放入到redis中。假设布隆过滤器判断这个数据不存在则直接返回。 这种方式优点在于节约内存空间但是存在误判误判原因在于布隆过滤器走的是哈希思想只要哈希思想就可能存在哈希冲突。 布隆筛选若不存在则一定是不存在若存在则不确定是否真的存在不是百分百准确。 介绍 redis 缓存雪崩问题以及解决思路 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机导致大量请求到达数据库带来巨大压力。 解决方案 给不同的Key的TTL添加随机值。利用Redis集群提高服务的可用性。给缓存业务添加降级限流策略。给业务添加多级缓存。 介绍 redis 缓存击穿问题以及解决思路 缓存击穿问题也叫热点Key问题就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了无数的请求访问会在瞬间给数据库带来巨大的冲击。 这里需要注意和缓存穿透的区别缓存击穿是指一个key非常热点在不停的扛着大并发大并发集中对这一个点进行访问当这个key在失效的瞬间持续的大并发就穿破缓存直接请求数据库就像在一个屏障上凿开了一个洞。当某个key在过期的瞬间有大量的请求并发访问这类数据一般是热点数据由于缓存过期会同时访问数据库来查询最新数据并且回写缓存会导使数据库瞬间压力过大。 逻辑分析假设线程1在查询缓存之后本来应该去查询数据库然后把这个数据重新加载到缓存的此时只要线程1走完这个逻辑其他线程就都能从缓存中加载这些数据了但是假设在线程1没有走完的时候后续的线程2线程3线程4同时过来访问当前这个方法 那么这些线程都不能从缓存中查询到数据那么他们就会同一时刻来访问查询缓存都没查到接着同一时间去访问数据库同时的去执行数据库代码对数据库访问压力过大。 常见的解决方案有两种 互斥锁逻辑过期数据永不过期 方案一使用互斥锁来解决 因为锁能实现互斥性。假设线程过来只能一个人一个人的来访问数据库从而避免对于数据库访问压力过大但这也会影响查询的性能因为此时会让查询的性能从并行变成了串行我们可以采用tryLock方法 double check来解决这样的问题。 假设现在线程1过来访问他查询缓存没有命中但是此时他获得到了锁的资源那么线程1就会一个人去执行逻辑假设现在线程2过来线程2在执行过程中并没有获得到锁那么线程2就可以进行到休眠直到线程1把锁释放后线程2获得到锁然后再来执行逻辑此时就能够从缓存中拿到数据了。 解决方案二逻辑过期方案 方案分析我们之所以会出现这个缓存击穿问题主要原因是在于我们对key设置了过期时间假设我们不设置过期时间其实就不会有缓存击穿的问题但是不设置过期时间这样数据不就一直占用我们内存了吗我们可以采用逻辑过期方案。 我们把过期时间设置在 redis的value中注意这个过期时间并不会直接作用于redis而是我们后续通过逻辑去处理。假设线程1去查询缓存然后从value中判断出来当前的数据已经过期了此时线程1去获得互斥锁那么其他线程会进行阻塞获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑直到新开的线程完成这个逻辑后才释放锁 而线程1直接进行返回假设现在线程3过来访问由于线程线程2持有着锁所以线程3无法获得锁线程3也直接返回数据只有等到新开的线程2把重建数据构建完后其他线程才能走返回正确的数据。 这种方案巧妙在于异步的构建缓存缺点在于在构建完缓存之前返回的都是脏数据。 对比 互斥锁方案由于保证了互斥性所以数据一致且实现简单因为仅仅只需要加一把锁而已也没其他的事情需要操心所以没有额外的内存消耗缺点在于有锁就有死锁问题的发生且只能串行执行性能肯定受到影响。 逻辑过期方案 线程读取过程中不需要等待性能好有一个额外的线程持有锁去进行重构数据但是在重构数据完成前其他的线程只能返回之前的数据且实现起来麻烦。 redis中热key和大key怎么处理 热key是指高请求的key某个瞬间有大量的请求去访问Redis上某个固定的key导致缓存击穿请求都打到了DB上压垮了缓存服务和DB服务 需要做一些处理来缓解redis压力 多级缓存在 Redis 之前添加一级缓存比如在应用服务器上使用本地缓存减少对 Redis 的直接访问。 将热key分散到不同的服务器中不要让固定key老是走到同一台redis节点上把这个key在多个redis节点上都备份一份即可高可读。 热key拆分把这个key给细化拆分让不同用户请求的key是不一样的 如秒杀活动场景不同用户根据人群规则命中的活动策略ID可能是不同的因此我们可以将整个活动元信息拆分成以策略为维度把活动信息的key细化这样请求过来时根据用户人群策略只会去找该策略绑定的活动信息的key全量用户的对活动信息的查询请求会分散到不同的活动策略key上从而避免固定key单点大量查询的问题 大key是指某个key-value存储的数据非常大该怎么设计 大key拆分分片存储将大key的数据拆分成多个小key分别存储在 Redis 中。可以使用哈希或其他分片算法来管理这些小key。 def set_large_data(key, data, chunk_size1024):# 将大数据拆分成多个小块并存储chunks [data[i:i chunk_size] for i in range(0, len(data), chunk_size)]for i, chunk in enumerate(chunks):redis_instance.set(f{key}:{i}, chunk)redis_instance.set(f{key}:chunks, len(chunks))def get_large_data(key):# 读取拆分的多个小块并合并chunks int(redis_instance.get(f{key}:chunks))data for i in range(chunks):data redis_instance.get(f{key}:{i}).decode(utf-8)return data# 示例使用 data A * 5000 # 示例大数据 set_large_data(large_key, data) retrieved_data get_large_data(large_key)使用合适的数据结构哈希Hash数据结构将大数据拆分为多个字段存储在哈希中可以有效利用内存并提高访问效率。 比如说存储用户信息用字符串 user:1{“name”: “Jack”, “age”: 21}使用hash改造 user:1namejackage21 压缩数据在存储大数据之前对数据进行压缩读取时在进行解压。 如果是指redis存储的数据量太大则考虑搭建集群、分片。 redis内存回收机制 设置TTL过期回收过期key的删除策略 惰性清理每次查找key时判断是否过期如果过期则删除定期清理定期抽样部分key判断是否过期如果过期则删除。定期清理的两种模式 SLOW模式执行频率默认为10每次不超过25msFAST模式执行频率不固定但两次间隔不低于2ms每次耗时不超过1ms 当Redis内存使用达到设置的上限时主动挑选部分key删除以释放更多内存的流程Redis支持8种不同淘汰策略 noeviction默认 不淘汰任何key但是内存满时不允许写入新数据。volatile-ttl 对设置了TTL的key比较key的剩余TTL值TTL越小越先被淘汰allkeys-random对全体key 随机进行淘汰。也就是直接从db-dict中随机挑选volatile-random对设置了TTL的key 随机进行淘汰。也就是从db-expires中随机挑选。allkeys-lru 对全体key基于LRU算法进行淘汰volatile-lru 对设置了TTL的key基于LRU算法进行淘汰allkeys-lfu 对全体key基于LFU算法进行淘汰volatile-lfu 对设置了TTL的key基于LFU算法进行淘汰 比较容易混淆的有两个 LRULeast Recently Used最少最近使用。用当前时间减去最后一次访问时间这个值越大则淘汰优先级越高。LFULeast Frequently Used最少频率使用。会统计每个key的访问频率值越小淘汰优先级越高。 redis分布式锁的实现 分布式锁满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁只要大家使用的是同一把锁那么我们就能锁住线程不让线程进行让程序串行执行这就是分布式锁的核心思路 实现分布式锁时需要实现的两个基本方法 获取锁 互斥确保只能有一个线程获取锁非阻塞尝试一次成功返回true失败返回false 释放锁 手动释放超时释放获取锁时添加一个超时时间 核心思路我们利用redis 的 setNx 方法当有多个线程进入时我们就利用该方法第一个线程进入时redis 中就有这个key 了返回了1如果结果是1则表示他抢到了锁那么他去执行业务然后再删除锁退出锁逻辑没有抢到锁的线程等待一定时间后重试即可。 先来实现基础版本的分布式锁如下基本接口 tryLock利用setnx方法进行加锁设置一个key同时增加过期时间防止死锁。 unlock删除该key表示释放锁。 上述是最简单的分布式锁实现肯定是存在一些问题的下面先来说说分布式锁误删的情况 逻辑说明持有锁的线程在锁的内部出现了阻塞导致他的锁自动释放这时其他线程线程2来尝试获得锁就拿到了这把锁然后线程2在持有锁执行过程中线程1反应过来继续执行而线程1执行过程中走到了删除锁逻辑此时就会把本应该属于线程2的锁进行删除这就是误删别人锁的情况说明。如下 解决方案在每个线程释放锁的时候去判断一下当前这把锁是否属于自己如果不属于自己则不进行锁的删除。假设还是上边的情况线程1卡顿锁自动释放线程2进入到锁的内部执行逻辑此时线程1反应过来然后删除锁但是线程1一看当前这把锁不是属于自己于是不进行删除锁逻辑当线程2走到删除锁逻辑时如果没有卡过自动释放锁的时间点则判断当前这把锁是属于自己的于是删除这把锁。 修改之前的分布式锁实现需要满足在获取锁时存入线程标示可以用UUID表示在释放锁时先获取锁中的线程标示判断是否与当前线程标示一致。 如果一致则释放锁如果不一致则不释放锁 核心逻辑在存入锁时放入自己线程的标识在删除锁时判断当前这把锁的标识是不是自己存入的如果是则进行删除如果不是则不进行删除。 具体代码如下加锁 private static final String ID_PREFIX UUID.randomUUID().toString(true) -; Override public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success); }释放锁 public void unlock() {// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁中的标示String id stringRedisTemplate.opsForValue().get(KEY_PREFIX name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX name);} }这样我们的分布式锁机制就完美了吗还不是哦上述修改后的代码还存在的原子性问题下面来看看。 更为极端的误删逻辑说明 线程1现在持有锁之后在执行业务逻辑过程中他正准备删除锁而且已经走到了条件判断的过程中比如他已经拿到了当前这把锁确实是属于他自己的正准备删除锁但是此时他的锁到期了那么此时线程2进来但是线程1他会接着往后执行当他卡顿结束后他直接就会执行删除锁那行代码相当于条件判断并没有起到作用这就是删锁时的原子性问题之所以有这个问题是因为线程1的拿锁、比锁、删锁这三个操作实际上并不是原子性的我们要防止刚才的情况发生。 利用Lua脚本解决多条命令原子性问题 Redis提供了Lua脚本功能在一个脚本中编写多条Redis命令确保多条命令执行时的原子性。 这里重点介绍Redis提供的调用函数语法如下 redis.call(命令名称, key, 其它参数, ...)例如我们要执行set name jack则脚本是这样 # 执行 set name jack redis.call(set, name, jack)例如我们要先执行set name Rose再执行get name则脚本如下 # 先执行 set name jack redis.call(set, name, Rose) # 再执行 get name local name redis.call(get, name) # 返回 return name写好脚本以后需要用Redis命令来调用脚本调用脚本的常见命令如下 例如我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本语法如下 如果脚本中的key、value不想写死可以作为参数传递。key类型参数会放入KEYS数组其它参数会放入ARGV数组在脚本中可以从KEYS和ARGV数组获取这些参数 接下来我们来回想一下我们释放锁的逻辑 释放锁的业务流程是这样的 获取锁中的线程标示判断是否与指定的标示当前线程标示一致如果一致则释放锁删除如果不一致则什么都不做 如果用Lua脚本来表示则是这样的 -- 这里的 KEYS[1] 就是锁的key这里的ARGV[1] 就是当前线程标示 -- 获取锁中的标示判断是否与当前线程标示一致 if (redis.call(GET, KEYS[1]) ARGV[1]) then-- 一致则删除锁return redis.call(DEL, KEYS[1]) end -- 不一致则直接返回 return 0这样我们的一个基本分布式锁就写完了小总结 基于Redis的分布式锁实现思路 利用set nx ex获取锁并设置过期时间保存线程标示释放锁时先判断线程标示是否与自己一致一致则删除锁 特性 利用set nx满足互斥性利用set ex保证故障时锁依然能释放避免死锁提高安全性利用Redis集群保证高可用和高并发特性 一路走来利用添加过期时间防止死锁问题的发生但是有了过期时间之后可能出现误删别人锁的问题这个问题我们开始是利用删之前通过拿锁、比锁、删锁这个逻辑来解决的也就是删之前判断一下当前这把锁是否是属于自己的但是现在还有原子性问题也就是我们没法保证拿锁比锁删锁是一个原子性的动作最后通过lua表达式来解决这个问题。 现在基本分布式锁已经做完了要想继续完善也是可以的参考下面几点 重入问题获得锁的线程可以再次进入到相同的锁的代码块中可重入锁的意义在于防止死锁比如HashTable这样的代码中他的方法都是使用synchronized修饰的假如他在一个方法内调用另一个方法那么此时如果是不可重入的不就死锁了吗所以可重入锁他的主要意义是防止死锁我们的synchronized和Lock锁都是可重入的。不可重试是指目前的分布式只能尝试一次我们认为合理的情况是当线程在获得锁失败后他应该能再次尝试获得锁。超时释放我们在加锁时增加了过期时间这样的我们可以防止死锁但是如果卡顿的时间超长虽然我们采用了lua表达式防止删锁的时候误删别人的锁但是毕竟没有锁住有安全隐患。主从一致性如果Redis提供了主从集群当我们向集群写数据时主机需要异步的将数据同步给从机而万一在同步过去之前主机宕机了就会出现死锁问题。 现已有完整分布式锁的实现Redisson是一个在Redis的基础上实现的Java驻内存数据网格In-Memory Data Grid。它不仅提供了一系列的分布式的Java常用对象还提供了许多分布式服务其中就包含了各种分布式锁的实现。 redission可重入锁原理 数据结构在 Redis 中Redisson 可重入锁通常使用以下结构 键锁的标识例如 “lock:myLock”。值包含锁标识和重入计数的 JSON 字符串或哈希表。 获取锁 当线程尝试获取锁时首先检查 Redis 中是否已经存在锁。如果锁不存在键未设置则设置锁并将当前线程的标识和重入计数初始为 1存储在 Redis 中。如果锁已存在检查当前线程是否为锁的拥有者通过锁标识进行比对。如果是重入计数加 1。如果不是锁的拥有者则阻塞等待直到锁被释放。 释放锁 当线程完成任务并释放锁时首先检查重入计数。如果重入计数大于 1则将计数减 1并保持锁的状态。如果计数减至 0则从 Redis 中删除锁键释放锁。 锁的过期 为了防止死锁Redisson 会为锁设置过期时间。如果线程在持有锁的情况下崩溃或未能释放锁锁会在过期后自动释放。 redission锁重试机制 允许在尝试获取锁失败后自动进行多次重试直到成功获取锁或达到最大重试次数。其工作流程如下 获取锁时重试 当一个线程请求锁时如果锁已被其他线程持有Redisson 会在配置的重试次数内等待一定的时间间隔后重新尝试获取锁。重试次数和间隔可以通过配置设置允许开发者根据具体需求调整。 redission的 WatchDog 机制 WatchDog 机制 主要用于锁的自动续期防止持锁线程由于意外情况如崩溃或长时间运行未能释放锁而导致的死锁。 工作原理 当线程成功获取锁后Redisson 会启动一个 WatchDog 线程定期检查锁的持有状态。WatchDog 会根据配置的过期时间在锁的生存期内定期更新锁的过期时间确保锁不会因超时而被意外释放。 续期操作 每次续期时WatchDog 会向 Redis 发送命令更新锁的过期时间保持锁的有效性。续期操作是在后台进行的不会影响持锁线程的执行。 锁释放 当持锁线程完成任务并释放锁时WatchDog 会停止工作锁会被正常释放。 redission锁的MutiLock原理 MultiLock 允许一个线程在同一时间对多个 Redis 键进行加锁确保在执行操作时这些资源不会被其他线程访问。这种锁的特性保证了操作的原子性即要么所有资源都被成功锁定要么没有锁定。 锁定请求 当一个线程请求 MultiLock 时Redisson 会尝试同时锁定多个指定的资源。为每个资源生成一个唯一的锁标识通常是 UUID用于跟踪每个锁的拥有者。 获取锁 Redisson 通过批量的 Redis 命令尝试锁定所有资源。如果所有资源都成功获取锁MultiLock 将保持有效状态。如果在获取某个资源的锁时失败例如某个资源已被其他线程锁定则会立即释放已成功获取的锁确保不会持有部分锁。 锁的重试机制 类似于普通锁MultiLock 也可以配置重试次数和重试间隔以处理竞争条件增加获取锁的成功率。 释放锁 一旦线程完成对多个资源的操作它会调用释放锁的方法。Redisson 将根据之前生成的锁标识释放所有锁。释放时Redisson 确保只有持有锁的线程才能释放相应的锁避免误释放。 为什么需要Redis主从复制 主从复制是指将一台Redis服务器的数据复制到其他的Redis服务器。前者称为主节点(master/leader)后者称为从节点(slave/follower)数据的复制是单向的只能由主节点到从节点。Master以写为主Slave 以读为主。默认情况下每台Redis服务器都是主节点且一个主节点可以有多个从节点(或没有从节点)但一个从节点只能有一个主节点。 主从复制的作用主要包括 数据冗余主从复制实现了数据的热备份是持久化之外的一种数据冗余方式。故障恢复当主节点出现问题时可以由从节点提供服务实现快速的故障恢复实际上是一种服务的冗余。负载均衡在主从复制的基础上配合读写分离可以由主节点提供写服务由从节点提供读服务即写Redis数据时应用连接主节点读Redis数据时应用连接从节点分担服务器负载尤其是在写少读多的场景下通过多个从节点分担读负载可以大大提高Redis服务器的并发量。高可用集群基石除了上述作用以外主从复制还是哨兵和集群能够实施的基础因此说主从复制是Redis高可用的基础。 主从节点的同步机制是什么? 全量同步 主从第一次建立连接时会执行全量同步将master节点的所有数据都拷贝给slave节点流程 这里有一个问题master如何得知salve是第一次来连接呢 有几个概念可以作为判断依据 Replication Id简称replid是数据集的标记id一致则说明是同一数据集。每一个master都有唯一的replidslave则会继承master节点的replid。offset偏移量随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset说明slave数据落后于master需要更新。 因此slave做数据同步必须向master声明自己的replication id 和offsetmaster才可以判断到底需要同步哪些数据。 因为slave原本也是一个master有自己的replid和offset当第一次变成slave与master建立连接时发送的replid和offset是自己的replid和offset。 master判断发现slave发送来的replid与自己的不一致说明这是一个全新的slave就知道要做全量同步了。 master会将自己的replid和offset都发送给这个slaveslave保存这些信息。以后slave的replid就与master一致了。 因此master判断一个节点是否是第一次同步的依据就是看replid是否一致。如下图 完整流程描述 slave节点请求增量同步master节点判断replid发现不一致拒绝增量同步master将完整内存数据生成RDB发送RDB到slaveslave清空本地数据加载master的RDBmaster将RDB期间的命令记录在repl_baklog并持续将log中的命令发送给slaveslave执行接收到的命令保持与master之间的同步 增量同步 全量同步需要先做RDB然后将RDB文件通过网络传输个slave成本太高了。因此除了第一次做全量同步其它大多数时候slave与master都是做增量同步。 什么是增量同步就是只更新slave与master存在差异的部分数据。如图 那么master怎么知道slave与自己的数据差异在哪里呢? 这就要说到全量同步时的repl_baklog文件了。 这个文件是一个固定大小的数组只不过数组是环形也就是说角标到达数组末尾后会再次从0开始读写这样数组头部的数据就会被覆盖。 repl_baklog中会记录Redis处理过的命令日志及offset包括master当前的offset和slave已经拷贝到的offset slave与master的offset之间的差异就是salve需要增量拷贝的数据了。 随着不断有数据写入master的offset逐渐变大slave也不断的拷贝追赶master的offset 直到数组被填满 此时如果有新的数据写入就会覆盖数组中的旧数据。不过旧的数据只要是绿色的说明是已经被同步到slave的数据即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。 但是如果slave出现网络阻塞导致master的offset远远超过了slave的offset 如果master继续写入新数据其offset就会覆盖旧的数据直到将slave现在的offset也覆盖 棕色框中的红色部分就是尚未同步但是却已经被覆盖的数据。此时如果slave恢复需要同步却发现自己的offset都没有了无法完成增量同步了只能做全量同步。 注意repl_baklog大小有上限写满后会覆盖最早的数据。如果slave断开时间过久导致尚未备份的数据被覆盖则无法基于log做增量同步只能再次全量同步。 总结 简述全量同步和增量同步区别 全量同步master将完整内存数据生成RDB发送RDB到slave。后续命令则记录在repl_baklog逐个发送给slave。增量同步slave提交自己的offset到mastermaster获取repl_baklog中从offset之后的命令给slave。 什么时候执行全量同步 slave节点第一次连接master节点时slave节点断开时间太久repl_baklog中的offset已经被覆盖时 什么时候执行增量同步 slave节点断开又恢复并且在repl_baklog中能找到offset时 主从同步优化 主从同步可以保证主从数据的一致性非常重要。可以从以下几个方面来优化Redis主从就集群 在master中配置repl-diskless-sync yes启用无磁盘复制避免全量同步时的磁盘IO。Redis单节点上的内存占用不要太大减少RDB导致的过多磁盘IO。适当提高repl_baklog的大小发现slave宕机时尽快实现故障恢复尽可能避免全量同步。限制一个master上的slave节点数量如果实在是太多slave则可以采用主-从-从链式结构减少master压力。 主从从架构图 Redis哨兵机制 哨兵的结构如图 哨兵的作用如下 监控Sentinel 会不断检查您的master和slave是否按预期工作。自动故障恢复如果master故障Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。通知Sentinel充当Redis客户端的服务发现来源当集群发生故障转移时会将最新信息推送给Redis的客户端。 Sentinel基于心跳机制监测服务状态每隔1秒向集群的每个实例发送ping命令 主观下线如果某sentinel节点发现某实例未在规定时间响应则认为该实例主观下线。客观下线若超过指定数量quorum的sentinel都认为该实例主观下线则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。 一旦发现master故障sentinel需要在salve中选择一个作为新的master选择依据是这样的 首先会判断slave节点与master节点断开时间长短如果超过指定值down-after-milliseconds * 10则会排除该slave节点然后判断slave节点的slave-priority值越小优先级越高如果是0则永不参与选举如果slave-prority一样则判断slave节点的offset值越大说明数据越新优先级越高最后是判断slave节点的运行id大小越小优先级越高。 当选出一个新的master后该如何实现切换呢 流程如下 sentinel给备选的slave1节点发送slaveof no one命令让该节点成为master。sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令让这些slave成为新master的从节点开始从新的master上同步数据。最后sentinel将故障节点标记为slave当故障节点恢复后会自动成为新的master的slave节点。 小结 Sentinel的三个作用是什么 监控故障转移通知 Sentinel如何判断一个redis实例是否健康 每隔1秒发送一次ping命令如果超过一定时间没有相向则认为是主观下线如果大多数sentinel都认为实例主观下线则判定服务下线 故障转移步骤有哪些 首先选定一个slave作为新的master执行slaveof no one然后让所有节点都执行slaveof 新master修改故障节点配置添加slaveof 新master redis集群怎么实现数据分布式存储 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决 海量数据存储问题高并发写的问题 使用分片集群可以解决上述问题如图 分片集群特征 集群中有多个master每个master保存不同数据。每个master都可以有多个slave节点。master之间通过ping监测彼此健康状态。客户端请求可以访问集群任意节点最终都会被转发到正确节点。 分片集群的原理是利用散列插槽Redis会把每一个master节点映射到0~16383共16384个插槽hash slot上查看集群信息时就能看到 数据key不是与节点绑定而是与插槽绑定。redis会根据key的有效部分计算插槽值分两种情况 key中包含{}且“{}”中至少包含1个字符“{}”中的部分是有效部分key中不包含“{}”整个key都是有效部分 例如key是num那么就根据num计算如果是{itcast}num则根据itcast计算。计算方式是利用CRC16算法得到一个hash值然后对16384取余得到的结果就是slot值。 如图在7001这个节点执行set a 1时对a做hash运算对16384取余得到的结果是15495因此要存储到103节点。 到了7003后执行get num时对num做hash运算对16384取余得到的结果是2765因此需要切换到7001节点。 Redis如何判断某个key应该在哪个实例 将16384个插槽分配到不同的实例根据key的有效部分计算哈希值对16384取余余数作为插槽寻找插槽所在实例即可 如何将同一类数据固定的保存在同一个Redis实例 这一类数据使用相同的有效部分例如key都以{typeId}为前缀 redis客户端对分布式集群发起一个查询介绍整个过程 连接到集群 客户端首先需要连接到集群中的任一节点。通常客户端库会处理连接的初始化和管理工作。 命令发送 计算键的哈希槽Redis Cluster 将所有的键映射到 0-16383 总共 16384 个哈希槽中。客户端使用 CRC16 哈希函数对键名进行哈希计算然后取模mod 16384来确定该键应该在哪一个哈希槽。确定目标节点每个哈希槽会被指派给一个主节点负责处理。客户端根据自己维护的集群的当前状态哪个节点负责哪个槽确定负责该槽的节点并将命令发送到该节点。 查询处理 执行命令负责该哈希槽的节点接收到命令后会在其数据集中查找或操作对应的键。返回结果操作完成后该节点会将结果返回给客户端。 错误处理与重定向 Moved 重定向如果客户端首次连接的节点不负责该键的哈希槽那么该节点会返回一个 MOVED 错误告诉客户端正确的节点地址。客户端接着会重新连接到正确的节点并重发命令。Ask 重定向在进行键迁移的过程中如果访问的键暂时处在两个节点之间原节点可能返回 ASK 错误客户端需要临时重定向到新节点并使用 ASKING 命令来发送原始请求。 客户端状态更新 在接收到 MOVED 或 ASK 重定向响应后客户端通常会更新自己维护的节点与槽的映射信息以便后续请求能直接发送到正确的节点。 同一份数据既要在MySQL存储又要在Redis存储要怎么设计实现 要实现同一份数据在 MySQL 和 Redis 中存储并确保两者之间的数据同步可以采取以下设计方案 数据架构设计 MySQL作为主数据存储负责持久化和复杂查询。Redis作为缓存层存储热数据以加速读取。 数据写入流程 写入 MySQL 当需要写入数据时首先将数据插入 MySQL 数据库。生成一条操作日志包括操作类型如新增、更新、删除、数据内容和唯一标识符将其存储到 Redis 中的日志队列。 写入 Redis 紧接着将相同的数据写入 Redis以便快速读取。使用合适的数据结构如 Hash 或 String来存储数据确保可以快速访问。 数据同步机制 使用 Redis 日志 Redis 中的日志可以用作数据同步的队列记录所有对 MySQL 的写操作。日志内容可以是 JSON 格式包括操作类型、数据主键和相关字段。 数据一致性保证 异步同步 通过一个后台服务或定时任务读取 Redis 日志并将未处理的操作应用到 MySQL 中。这种设计可以减轻 MySQL 的实时压力。 异常处理 如果在写入 MySQL 时发生错误记录该操作并采取重试机制确保最终一致性。监控 Redis 日志处理过程记录失败的操作并提供手动修复的接口。 数据读取流程 读取数据时优先从 Redis 中获取。如果 Redis 中不存在再从 MySQL 中读取并将结果缓存到 Redis。 数据更新和删除 更新 更新数据时遵循写入流程先更新 MySQL然后更新 Redis并在日志中记录操作。 删除 删除操作应在 MySQL 和 Redis 中同时进行并记录日志。 监控和告警 监控 Redis 和 MySQL 之间的同步状态设置告警机制当日志处理失败或同步出现异常时及时通知相关人员。 定期清理 定期清理 Redis 中的日志数据防止内存占用过高。 恭喜你全部读完啦古人云温故而知新。赶紧收藏关注起来面试前再翻一翻吧~ 推荐阅读 C/C后端开发面试总结点击进入 后端开发面经 关注走一波 C重点知识点击进入 C重点知识 关注走一波
http://www.tj-hxxt.cn/news/224702.html

相关文章:

  • 自己买主机可以做网站吗php儿童摄影网站源码
  • 建设电商网站报价网络广告推广
  • 如何架设网站服务器学生个人网页制作代码
  • 做空间的网站网络服务商机构域名是什么
  • 电子商务网站建设需求分析报告PC网站开发的意义
  • 公司网站主机流量30g每月够用吗深圳市网站建设公司设计
  • 黑龙江省建设集团有限公司网站首页团购网站建设
  • wordpress更改后台宁波seo怎么做推广渠道
  • flash打开网站源码家装公司哪家比较好
  • 低价网站建设哪家更好网站服务器 数据库服务器
  • 江阴哪家做网站便宜网页禁止访问怎么能打开
  • h5模板网站免费app推广拉新一手渠道
  • 外贸cms建站重庆网站建设网站建设
  • flash做ppt的模板下载网站茂名免费网站建设
  • 网站页面设计特点分销网站建立
  • 凡科建站小程序做关于家乡的网站
  • 浦东新区网站推广公司长治网站设计制作网站
  • php网站后台上传图片有没有推荐到首页的功能用自己的电脑做服务器建网站
  • 青羊区企业网站建设策划网站建设网站需要什么软件
  • 金华网站建设方案策划滑块验证wordpress
  • 菏泽市建设局网站电话号码明企科技网站建设系统
  • 网站私信界面wordpress 免插件实现
  • 最新仿5173游戏装备交易网站 游戏币交易平台源码整合支付接口北京网站建设佳v询 lotlek 能上词
  • 网站优化要做哪些工作网站建站去哪找客户
  • 拍卖网站开发多少钱杭州网络科技公司排名
  • 火炬开发区网站建设外贸平台有哪些是免费的
  • 网站建设完成后为何无法运营下去有没有专门做花鸟鱼虫的网站
  • linux 网站备份个人开小公司的流程
  • 网站开发项目报告代做网页制作网站
  • 阿里云做网站教程上海大良网站建设