From bb898a531d35ca960d441222eaef215430b5a0de Mon Sep 17 00:00:00 2001 From: ironartisan Date: Sun, 22 Aug 2021 09:25:54 +0800 Subject: [PATCH 01/26] =?UTF-8?q?=E6=9B=B4=E6=AD=A3=E9=94=99=E5=88=AB?= =?UTF-8?q?=E5=AD=97=E5=8F=8A=E9=93=BE=E6=8E=A5=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Java\345\237\272\347\241\200\344\270\212.md" | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git "a/Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\344\270\212.md" "b/Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\344\270\212.md" index 8321ac1..901317c 100644 --- "a/Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\344\270\212.md" +++ "b/Java\345\237\272\347\241\200/Java\345\237\272\347\241\200\344\270\212.md" @@ -358,7 +358,7 @@ hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返 ### 为什么重写 equals 方法必须重写 hashcode 方法 ? -断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。 +判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。 在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。 diff --git a/README.md b/README.md index b96f4ae..5d20007 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ ### Java基础 -* [Java基础上](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80.md) +* [Java基础上](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80%E4%B8%8A.md) * [Java基础下](https://github.com/cosen1024/Java-Interview/blob/main/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80%E4%B8%8B.md) ### 集合 * [Java集合高频面试题](https://github.com/cosen1024/Java-Interview/blob/main/Java%E9%9B%86%E5%90%88/Java%E9%9B%86%E5%90%88%E9%AB%98%E9%A2%91%E9%9D%A2%E8%AF%95%E9%A2%98.md) From 08f2bc3f92c97c472c180cd3b1ff91d6a1560e82 Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 00:52:02 +0800 Subject: [PATCH 02/26] update redis --- Redis/Redis.md | 1189 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 926 insertions(+), 263 deletions(-) diff --git a/Redis/Redis.md b/Redis/Redis.md index 64945ab..9a2986a 100644 --- a/Redis/Redis.md +++ b/Redis/Redis.md @@ -1,81 +1,252 @@ -## 1. Redis是什么?**简述它的优缺点?** -Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。 +# 概述 -因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。 +## 1. Redis是什么?简述它的优缺点? -Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能。 +Redis本质上是一个Key-Value类型的内存数据库,很像Memcached,整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据flush到硬盘上进行保存。 -比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。 +因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value 数据库。 -另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。 Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 +**优点**: + +* 读写性能极高, Redis能读的速度是110000次/s,写的速度是81000次/s。 +* 支持数据持久化,支持AOF和RDB两种持久化方式。 +* 支持事务, Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。 +* 数据结构丰富,除了支持string类型的value外,还支持hash、set、zset、list等数据结构。 +* 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。 +* 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等特性。 + +**缺点**: + +* 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 +* 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。 ## 2. Redis为什么这么快? -1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1); +- 内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。 + +- 单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。 + +- 非阻塞IO:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。 + +- 优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能。 + +- 使用底层模型不同:Redis直接自己构建了 VM (虚拟内存)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。 + + > Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。 + > + > Redis提高数据库容量的办法有两种:一种是可以将数据分割到多个RedisServer上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。**需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现。** + +## 3. Redis相比Memcached有哪些优势? + +* 数据类型:Memcached所有的值均是简单的字符串,Redis支持更为丰富的数据类型,支持string(字符串),list(列表),Set(集合)、Sorted Set(有序集合)、Hash(哈希)等。 + +* 持久化:Redis支持数据落地持久化存储,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 memcache不支持数据持久存储 。 + +* 集群模式:Redis提供主从同步机制,以及 Cluster集群部署能力,能够提供高可用服务。Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据 + +* 性能对比:Redis的速度比Memcached快很多。 + +* 网络IO模型:Redis使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞IO模式。 + +* Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。 + + 这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。 + +## 4. 为什么要用 Redis 做缓存? + +**从高并发上来说:** + +- 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 + +**从高性能上来说:** + +- 用户第一次访问数据库中的某些数据。 因为是从硬盘上读取的所以这个过程会比较慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据。 + +## 5. 为什么要用 Redis 而不用 map/guava 做缓存? + +缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 + +使用Redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或memcached服务的高可用,整个程序架构上较为复杂。 + +对比: + +* Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了; +* Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了; +* Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里; +* Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象; +* Redis 缓存有过期机制,Map 本身无此功能;Redis 有丰富的 API,Map 就简单太多了; +* Redis可单独部署,多个项目之间可以空想,本地内存无法共享; +* Redis有专门的管理工具可以查看缓存数据。 + +## 6. Redis的常用场景有哪些? + +**1、缓存** + +缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。 + +**2、排行榜** + +很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。 + +**3、计数器** + +什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。 + +**4、分布式会话** + +集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 + +**5、分布式锁** + +在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。 + +**6、 社交网络** + +点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出。 + +**7、最新列表** + +Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。 + +**8、消息系统** + +消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。 + +## 8. Redis的数据类型有哪些? + +有五种常用数据类型:String、Hash、Set、List、SortedSet。以及三种特殊的数据类型:Bitmap、HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。 + +**五种常用的数据类型**: + +1、String:String是最常用的一种数据类型,普通的key- value 存储都可以归为此类。其中Value既可以是数字也可以是字符串。使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。 + +2、Hash:Hash 是一个键值(key => value)对集合。Redishash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。 + +3、Set:Set是一个无序的天然去重的集合,即Key-Set。此外还提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注什么的功能实现特别方便。 + +4、List:List是一个有序可重复的集合,其遵循FIFO的原则,底层是依赖双向链表实现的,因此支持正向、反向双重查找。通过List,我们可以很方面的获得类似于最新回复这类的功能实现。 + +5、SortedSet:类似于java中的TreeSet,是Set的可排序版。此外还支持优先级排序,维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。 + +**三种特殊的数据类型**: + +1、Bitmap:位图,Bitmap想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。使用Bitmap实现统计功能,更省空间。如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。 + +2、Hyperloglog。HyperLogLog 是一种用于统计基数的数据集合类型,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大 + +时,计算基数所需的空间总是固定 的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。场景:统计网页的UV(即Unique Visitor,不重复访客,一个人访问某个网站多次,但是还是只计算为一次)。 + +要注意,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。 + +3、Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如朋友的定位、附近的人、打车距离计算等。 + +# 持久化 + +## 6. Redis持久化机制? + +为了能够重用Redis数据,或者防止系统故障,我们需要将Redis中的数据写入到磁盘空间中,即持久化。 + +Redis提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照`RDB`,另一种叫只追加文件`AOF`。 + +**RDB** + +在指定的时间间隔内将内存中的数据集快照写入磁盘(`Snapshot`),它恢复时是将快照文件直接读到内存里。 + +**优势**:适合大规模的数据恢复;对数据完整性和一致性要求不高 + +**劣势**:在一定间隔时间做一次备份,所以如果Redis意外`down`掉的话,就会丢失最后一次快照后的所有修改。 + +**AOF** + +以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 + +AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时, Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.。 + +**优势** + +- 每修改同步:`appendfsync always` 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 +- 每秒同步:`appendfsync everysec` 异步操作,每秒记录,如果一秒内宕机,有数据丢失 +- 不同步:`appendfsync no` 从不同步 + +**劣势** + +- 相同数据集的数据而言`aof`文件要远大于`rdb`文件,恢复速度慢于`rdb` +- `aof`运行效率要慢于`rdb`,每秒同步策略效率较好,不同步效率和`rdb`相同 + +## 9. 如何选择合适的持久化方式 + +- 如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化。 +- 如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB。 +- 如果是用做内存数据库,要使用Redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDB方式更适合做数据的备份,AOF可以保证数据的不丢失。 + +**补充:Redis4.0 对于持久化机制的优化** + +Redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。 -2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的; +简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图: -3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗; +![](https://images2018.cnblogs.com/blog/1075473/201807/1075473-20180726181756270-1907770368.png) + +**优势**:混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 + +**劣势**:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。 -4、使用多路I/O复用模型,非阻塞IO; +## 10. Redis持久化数据和缓存怎么做扩容? -5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求; +* 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。 -## 3. **Redis相比memcached有哪些优势?** +* 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。 -* redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。 -* Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。 -* 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的. -* Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。 +# 过期键的删除策略、淘汰策略 -## 4. Redis 的数据类型? +## 11. Redis过期键的删除策略 -Redis 支持五种数据类型: string( 字符串),hash( 哈希), list( 列表), set( 集合) 及 zsetsorted set: 有序集合)。 +**Redis的过期删除策略就是:惰性删除和定期删除两种策略配合使用。** -- string:redis 中字符串 value 最大可为512M。可以用来做一些计数功能的缓存(也是实际工作中最常见的)。 常规计数:微博数,粉丝数等。 -- list:简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。可以实现一个简单消息队列功能,做基于redis的分页功能等。(另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。) -- set:是一个字符串类型的无序集合。可以用来进行全局去重等。(比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程) -- sorted set:是一个字符串类型的有序集合,给每一个元素一个固定的分数score来保持顺序。可以用来做排行榜应用或者进行范围查找等。 -- hash:键值对集合,是一个字符串类型的 Key和 Value 的映射表,也就是说其存储的Value是一个键值对(Key- Value)hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。 +**惰性删除**:惰性删除不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过期,如果过期则执行删除并返回 null 给客户端,如果没有过期则返回正常信息给客户端。它的优点是简单,不需要对过期的数据做额外的处理,只有在每次访问的时候才会检查键值是否过期,缺点是删除过期键不及时,造成了一定的空间浪费。 -我们实际项目中比较常用的是 string,hash。 如果你是 Redis 的高级用户,还需要加上下面几种数据结构 HyperLogLog、Geo、Pub/Sub。 +**定期删除**:Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。 -如果你说还玩过 Redis Module,像 BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。 +附:删除key常见的三种处理方式。 -## 5. Redis的常用场景? +**1、定时删除** -1、会话缓存( Session Cache) +在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。 -最常用的一种使用 Redis 的情景是会话缓存( session cache)。用 Redis 缓存会话比其他存储( 如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时, 如果用户的购物车信息全部丢失, 大部分人都会不高兴的, 现在, 他们还会这样吗? 幸运的是, 随着 Redis 这些年的改进, 很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件。 +优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。 -2、全页缓存( FPC) +缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。 -除基本的会话 token 之外, Redis 还提供很简便的 FPC 平台。回到一致性问题, 即使重启了 Redis 实例, 因为有磁盘的持久化, 用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。 此外, 对 WordPress 的用户来说, Pantheon 有一个非常好的插件 wp-redis, 这个插件能帮助你以最快速度加载你曾浏览过的页面。 +**2、惰性删除** -3、队列 +设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。 -Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作, 这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言( 如 Python)对 list 的 push/pop 操作。 如果你快速的在 Google 中搜索“ Redis queues”, 你马上就能找到大量的开源项目, 这些项目的目的就是利用 Redis 创建非常好的后端工具, 以满足各种队列需求。例如, Celery 有一个后台就是使用 Redis 作为 broker, 你可以从这里去查看。 +优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。 -4, 排行榜/计数器 +缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存泄漏。 -Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合( Set) 和有序集合( Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以, 我们要从排序集合中获取到排名最靠前的 10 个用户– 我们称之为“ user_scores”, 我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数, 你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子, 用 Ruby 实现的, 它的排行榜就是使用 Redis 来存储数据的, 你可以在这里看到。 +**3、定期删除** -5、发布/订阅 +每隔一段时间,我们就对一些key进行检查,删除里面过期的key。 -发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用, 还可作为基于发布/订阅的脚本触发器, 甚至用 Redis 的发布/订阅功能来建立聊天系统! +优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。 -## 6. redis 过期键的删除策略? +缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。 -1、定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时, 立即执行对键的删除操作。 +## 12. Redis key的过期时间和永久有效分别怎么设置? -2、惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是 否过期, 如果过期的话, 就删除该键;如果没有过期, 就返回该键。 +通过expire或pexpire命令,客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间。 -3、定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至 于要删除多少过期键, 以及要检查多少个数据库, 则由算法决定。 +与expire和pexpire命令类似,客户端可以通过expireat和pexpireat命令,以秒或毫秒精度给数据库中的某个键设置过期时间,可以理解为:让某个键在某个时间点过期。 -## 7. redis 内存淘汰机制? +## 13. Redis内存淘汰策略 -redis v4.0前提供 6种数据淘汰策略: +Redis是不断的删除一些过期数据,但是很多没有设置过期时间的数据也会越来越多,那么Redis内存不够用的时候是怎么处理的呢?答案就是淘汰策略。此类的 + +当Redis的内存超过最大允许的内存之后,Redis会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器的正常运行。 + +**Redisv4.0前提供 6种数据淘汰策略**: - volatile-lru:利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used ) - allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的) @@ -84,399 +255,891 @@ redis v4.0前提供 6种数据淘汰策略: - allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 - no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! -**redis v4.0后增加以下两种**: +**Redisv4.0后增加以下两种**: - volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(LFU(Least Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到) - allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。 -## 8. redis 持久化机制 +内存淘汰策略可以通过配置文件来修改,Redis.conf对应的配置项是maxmemory-policy 修改对应的值就行,默认是noeviction。 -为了能够重用`Redis`数据,或者防止系统故障,我们需要将`Redis`中的数据写入到磁盘空间中,即持久化。`Redis`提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照`RDB`,另一种叫只追加文件`AOF` +# 缓存异常 -### RDB +> 缓存异常有四种类型,分别是缓存和数据库的数据不一致、缓存雪崩、缓存击穿和缓存穿透。 -在指定的时间间隔内将内存中的数据集快照写入磁盘(`Snapshot`),它恢复时是将快照文件直接读到内存里。 +## 14. 如何保证缓存与数据库双写时的数据一致性? - `Redis`会单独创建(`fork`)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那`RDB`方式要比`AOF`方式更加的高效。`RDB`的缺点是最后一次持久化后的数据可能丢失。 +> 背景:使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。 -`Fork`的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。**`Rdb` 保存的是`dump.rdb`文件。** +共有四种方案: -**优势** +1. 先更新数据库,后更新缓存 +2. 先更新缓存,后更新数据库 +3. 先删除缓存,后更新数据库 +4. 先更新数据库,后删除缓存 -适合大规模的数据恢复 -对数据完整性和一致性要求不高 +第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。 -**劣势** +第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。 -在一定间隔时间做一次备份,所以如果`redis`意外`down`掉的话,就会丢失最后一次快照后的所有修改。 +目前主要用第三和第四种方案。 -### AOF(Append Only File) +## 15. 先删除缓存,后更新数据库 -以日志的形式来记录每个写操作,将`Redis`执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,`redis`启动之初会读取该文件重新构建数据,换言之,`redis`重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。**AOF保存的是appendonly.aof文件** +该方案也会出问题,此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作) -#### AOF重写 +1. 请求A进行写操作,删除缓存 +2. 请求B查询发现缓存不存在 +3. 请求B去数据库查询得到旧值 +4. 请求B将旧值写入缓存 +5. 请求A将新值写入数据库 -AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时, Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。 +上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。 -AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据, 每条记录有一条的set语句。重写aof文件的操作,并没有读取旧的aof文件, 而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。 +### 答案一:延时双删 -**优势** +最简单的解决办法延时双删 -- 每修改同步:`appendfsync always` 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 -- 每秒同步:`appendfsync everysec` 异步操作,每秒记录,如果一秒内宕机,有数据丢失 -- 不同步:`appendfsync no` 从不同步 +使用伪代码如下: -**劣势** +```java +public void write(String key,Object data){ + Redis.delKey(key); + db.updateData(data); + Thread.sleep(1000); + Redis.delKey(key); + } +``` -- 相同数据集的数据而言`aof`文件要远大于`rdb`文件,恢复速度慢于`rdb` -- `aof`运行效率要慢于`rdb`,每秒同步策略效率较好,不同步效率和`rdb`相同 +转化为中文描述就是 +(1)先淘汰缓存 +(2)再写数据库(这两步和原来一样) +(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。 -### Redis 4.0 对于持久化机制的优化 +如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。 -redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。 +![主从同步时间差](http://blog-img.coolsen.cn/img/1735bb5881bbb1d4~tplv-t2oaga2asx-watermark.awebp) -混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图: +此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作) -![](https://images2018.cnblogs.com/blog/1075473/201807/1075473-20180726181756270-1907770368.png) +1. 请求 A 更新操作,删除了 Redis +2. 请求主库进行更新操作,主库与从库进行同步数据的操作 +3. 请 B 查询操作,发现 Redis 中没有数据 +4. 去从库中拿去数据 +5. 此时同步数据还未完成,拿到的数据是旧数据 -**优点** +此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。 -混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 -**缺点** -兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。 +![从主库中拿数据](http://blog-img.coolsen.cn/img/1735bb5881a19fec~tplv-t2oaga2asx-watermark.awebp) -## 9. 缓存雪崩、缓存击穿和缓存穿透问题解决方案 +### 答案二: **更新与读取操作进行异步串行化** -### 缓存击穿 +采用**更新与读取操作进行异步串行化** -举例:redis中存储的是热点数据,当高并发请求访问redis中热点数据的时候,如果redis中的数据过期了,会造成缓存击穿的现象,请求都打到了数据库上。 +**异步串行化** -解决办法:使用互斥锁,只让一个请求去load DB,成功之后重新写缓存,其余请求没有获取到互斥锁,可以尝试重新获取缓存中的数据。。 +我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。 -### 缓存穿透 +这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。 -缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上(举例:故意的去请求缓存中不存在的数据,导致请求都打到了数据库上,导致数据库异常。) +**读操作去重** -解决办法 +多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。 -1.首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等 -2.缓存无效 key :如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 -3.布隆过滤器.把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程 +如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存) -![](http://blog-img.coolsen.cn/img/1584173117136_7.png) +## 16. 先更新数据库,后删除缓存 -### 缓存雪崩 +这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。 -缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉 +![先更新数据库,后删除缓存](http://blog-img.coolsen.cn/img/1735bb5881fb4a1b~tplv-t2oaga2asx-watermark.awebp) -- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。 -- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉 -- 事后:利用 redis 持久化机制保存的数据尽快恢复缓存 +此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下: -![](http://blog-img.coolsen.cn/img/1584172719093_6.png) +1. 请求 A 先对数据库进行更新操作 +2. 在对 Redis 进行删除操作的时候发现报错,删除失败 +3. 此时将Redis 的 key 作为消息体发送到消息队列中 +4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作 +但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。 +![利用订阅 binlog 删除缓存](http://blog-img.coolsen.cn/img/1735bb588215b298~tplv-t2oaga2asx-watermark.awebp) -## 10. 为什么 Redis 不支持回滚(roll back) +## 17. 什么是缓存击穿? -- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。 -- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 +缓存击穿跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是某个热点的key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。 -有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 [INCR key](http://redisdoc.com/string/incr.html#incr) 命令将键的值加上 `1` , 却不小心加上了 `2` , 又或者对错误类型的键执行了 [INCR key](http://redisdoc.com/string/incr.html#incr) , 回滚是没有办法处理这些情况的。 +从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。 -鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。 +解决方案: -## 11. Redis中跳跃表(skiplist)实现原理 +* 在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降 -跳表(skiplist)是一个特殊的链表,相比一般的链表,有更高的查找效率,其效率可比拟于二叉查找树。 +* 热点数据缓存永远不过期。永不过期实际包含两层意思: + * 物理不过期,针对热点key不设置过期时间 + * 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建 -### 跳跃表来源 +## 18. 什么是缓存穿透? -跳跃表在 1990 年由 William Pugh 提出,而红黑树早在 1972 年由鲁道夫·贝尔发明了。红黑树在空间和时间效率上略胜跳表一筹,但跳跃表实现相对简单得到程序猿们的青睐。Redis 和 Leveldb 中都有采用跳跃表。 +缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。 -以下是个典型的跳跃表例子 +> 缓存穿透的关键在于在Redis中查不到key值,它和缓存击穿的根本区别在于传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示。 -image-20200714130333639 +![img](http://blog-img.coolsen.cn/img/2021013117512340.png) -按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。 +解决方法: -但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。 +* 将无效的key存放进Redis中: -下面看Redis 跳跃表的实现,如何解决的这个问题。 +当出现Redis查不到数据,数据库也查不到数据的情况,我们就把这个key保存到Redis中,设置value="null",并设置其过期时间极短,后面再出现查询这个key的请求的时候,直接返回null,就不需要再查询数据库了。但这种处理方式是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。 -### Redis 跳跃表的实现 +* 使用布隆过滤器: -为了满足自身的功能需要, Redis 基于 William Pugh 论文中描述的跳跃表进行了以下修改: +如果布隆过滤器判定某个 key 不存在布隆过滤器中,那么就一定不存在,如果判定某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一个布隆过滤器,将数据库中的所有key都存储在布隆过滤器中,在查询Redis前先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,不让其访问数据库,从而避免了对底层存储系统的查询压力。 -1. 允许重复的 `score` 值:多个不同的 `member` 的 `score` 值可以相同。 -2. 进行对比操作时,不仅要检查 `score` 值,还要检查 `member` :当 `score` 值可以重复时,单靠 `score` 值无法判断一个元素的身份,所以需要连 `member` 域都一并检查才行。 -3. 每个节点都带有一个高度为 1 层的后退指针,用于从表尾方向向表头方向迭代:当执行 [ZREVRANGE](http://redis.readthedocs.org/en/latest/sorted_set/zrevrange.html#zrevrange) 或 [ZREVRANGEBYSCORE](http://redis.readthedocs.org/en/latest/sorted_set/zrevrangebyscore.html#zrevrangebyscore) 这类以逆序处理有序集的命令时,就会用到这个属性。 +> 如何选择:针对一些恶意攻击,攻击带过来的大量key是随机,那么我们采用第一种方案就会缓存大量不存在key的数据。那么这种方案就不合适了,我们可以先对使用布隆过滤器方案进行过滤掉这些key。所以,针对这种key异常多、请求重复率比较低的数据,优先使用第二种方案直接过滤掉。而对于空数据的key有限的,重复率比较高的,则可优先采用第一种方式进行缓存。 -跳跃表的结构定义: +## 19. 什么是缓存雪崩? -```c -typedef struct zskiplist { +如果缓在某一个时刻出现大规模的key失效,那么就会导致大量的请求打在了数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。 - // 头节点,尾节点 - struct zskiplistNode *header, *tail; +造成缓存雪崩的关键在于同一时间的大规模的key失效,主要有两种可能:第一种是Redis宕机,第二种可能就是采用了相同的过期时间。 - // 节点数量 - unsigned long length; +解决方案: - // 目前表内节点的最大层数 - int level; +1、事前: -} zskiplist; -``` +* 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。如把每个Key的失效时间都加个随机值,`setRedis(Key,value,time + Math.random() * 10000);`,保证数据不会在同一时间大面积失效。 -跳跃表的节点定义: +* 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。 -```c -typedef struct zskiplistNode { +* 热点数据缓存永远不过期。永不过期实际包含两层意思: + * 物理不过期,针对热点key不设置过期时间 + * 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建 - // member 对象 - robj *obj; +* 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用 主从+ 哨兵,Redis集群来避免 Redis 全盘崩溃的情况。 - // 分值 - double score; +2、事中: - // 后退指针 - struct zskiplistNode *backward; +* 互斥锁:在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降 - // 层 - struct zskiplistLevel { +* 使用熔断机制,限流降级。当流量达到一定的阈值,直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。 - // 前进指针 - struct zskiplistNode *forward; +3、事后: - // 这个层跨越的节点数量 - unsigned int span; +开启Redis持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。 - } level[]; +## 20. 什么是缓存预热? -} zskiplistNode; -``` +缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。 + +如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。 + +缓存预热解决方案: + +* 数据量不大的时候,工程启动的时候进行加载缓存动作; + +* 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新; + +* 数据量太大的时候,优先保证热点数据进行提前加载到缓存。 + +## 21. 什么是缓存降级? + +缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。 + +在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案: + +* 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; + +* 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; + +* 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; + +* 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。 + +# 线程模型 + +## 22. Redis为何选择单线程? + +在Redis 6.0以前,Redis的核心网络模型选择用单线程来实现。先来看下官方的回答: + +> It's not very frequent that CPU becomes your bottleneck with Redis, as usually Redisis either memory or network bound. For instance, using pipelining Redisrunning on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU. + +核心意思就是,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。具体到 Redis的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执行速度是非常快的,因此这部分操作通常不会是性能瓶颈,Redis真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,因此 Redis选择了单线程的 I/O 多路复用来实现它的核心网络模型。 + +实际上更加具体的选择单线程的原因如下: + +* 避免过多的上下文切换开销:如果是单线程则可以规避进程内频繁的线程切换开销,因为程序始终运行在进程中单个线程内,没有多线程切换的场景。 +* 避免同步机制的开销:如果 Redis选择多线程模型,又因为 Redis是一个数据库,那么势必涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁,而我们知道 Redis不仅仅提供了简单的 key-value 数据结构,还有 list、set 和 hash 等等其他丰富的数据结构,而不同的数据结构对同步访问的加锁粒度又不尽相同,可能会导致在操作数据过程中带来很多加锁解锁的开销,增加程序复杂度的同时还会降低性能。 +* 简单可维护:如果 Redis使用多线程模式,那么所有的底层数据结构都必须实现成线程安全的,这无疑又使得 Redis的实现变得更加复杂。 + +总而言之,Redis选择单线程可以说是多方博弈之后的一种权衡:在保证足够的性能表现之下,使用单线程保持代码的简单和可维护性。 + +## 23. Redis真的是单线程? + +讨论 这个问题前,先看下 Redis的版本中两个重要的节点: + +1. Redisv4.0(引入多线程处理异步任务) +2. Redis 6.0(在网络模型中实现多线程 I/O ) + +所以,网络上说的Redis是单线程,通常是指在Redis 6.0之前,其核心网络模型使用的是单线程。 + +且Redis6.0引入**多线程I/O**,只是用来**处理网络数据的读写和协议的解析**,而**执行命令依旧是单线程**。 + +> Redis在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环。 +> +> 在 Redisv4.0 之后增加了一些的非阻塞命令如 `UNLINK`、`FLUSHALL ASYNC`、`FLUSHDB ASYNC`。 + +## 24. Redis 6.0为何引入多线程? + +很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。 + +随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向: + +- 优化网络 I/O 模块 +- 提高机器内存读写的速度 + +后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向: + +- 零拷贝技术或者 DPDK 技术 +- 利用多核优势 + +零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。 + +总结起来,Redis支持多线程主要就是两个原因: + +* 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核 + +* 多线程任务可以分摊 Redis 同步 IO 读写负荷 + +## 25. Redis 6.0 采用多线程后,性能的提升效果如何? + +Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。 + +国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。 + +## 26. 介绍下Redis的线程模型 + +Redis的线程模型包括Redis 6.0之前和Redis 6.0。 + +下面介绍的是Redis 6.0之前。 + +Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。 + +> IO多路复用是 IO 模型的一种,有时也称为异步阻塞 IO,是基于经典的 Reactor 设计模式设计的。多路指的是多个 Socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:Select,Poll,Epoll。 +> +> Epoll 是最新的也是目前最好的多路复用技术。 + +模型如下图: + +![202105092153018231.png](http://blog-img.coolsen.cn/img/202105092153018231.png) + +文件事件处理器的结构包含了四个部分: + +- 多个 Socket。Socket 会产生 AE_READABLE 和 AE_WRITABLE 事件: + - 当 socket 变得可读时或者有新的可以应答的 socket 出现时,socket 就会产生一个 AE_READABLE 事件 + - 当 socket 变得可写时,socket 就会产生一个 AE_WRITABLE 事件。 +- IO 多路复用程序 +- 文件事件分派器 +- 事件处理器。事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器对应不同的 socket 事件: + - 如果是客户端要连接 Redis,那么会为 socket 关联连接应答处理器 + - 如果是客户端要写数据到 Redis(读、写请求命令),那么会为 socket 关联命令请求处理器 + - 如果是客户端要从 Redis 读数据,那么会为 socket 关联命令回复处理器 + +多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。 + +下图是客户端与 Redis 通信的一次完整的流程: + +![202105092153019692.png](http://blog-img.coolsen.cn/img/202105092153019692.png) + +1. Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。 +2. 如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket,同时将这个 socket 的 AE_READABLE 事件与命令请求处理器关联起来。 +3. 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联。 +4. 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。 +5. 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。 + +## 27. Redis 6.0 多线程的实现机制? + +**流程简述如下**: + +- 主线程负责接收建立连接请求,获取 Socket 放入全局等待读处理队列。 +- 主线程处理完读事件之后,通过 RR(Round Robin)将这些连接分配给这些 IO 线程。 +- 主线程阻塞等待 IO 线程读取 Socket 完毕。 +- 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行。 +- 主线程阻塞等待 IO 线程将数据回写 Socket 完毕。 + + +![image-20210828175543973](http://blog-img.coolsen.cn/img/image-20210828175543973.png) + +**该设计有如下特点**: + +- IO 线程要么同时在读 Socket,要么同时在写,不会同时读或写。 +- IO 线程只负责读写 Socket 解析命令,不负责命令处理。 -image-20200714131425529 +## 28. Redis 6.0开启多线程后,是否会存在线程并发安全问题? -上图就是跳跃列表的示意图,图中只画了3层,Redis 的跳跃表共有 64 层,容纳 2^64 个元素应该不成问题。 +从实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。 -跳表的性质: +所以我们不需要去考虑控制 Key、Lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。 -1. 由很多层结构组成 -2. 每一层都是一个有序的链表 -3. 最底层(Level 1) 的链表包含所有元素 -4. 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。 -5. 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。 +## 29. Redis 6.0 与 Memcached 多线程模型的对比 -### 随机层数的设计 +- **相同点:**都采用了 Master 线程 -Worker 线程的模型。 -Redis 使用随机层数,解决插入、删除时,时间复杂度重新蜕化成O(n)的问题 +- **不同点**:Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。 -它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。 + 而 Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。 -image-20200714140215699 +# 事务 -插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度,这让它在插入性能上明显优于平衡树。 +## 30. Redis事务的概念 -#### 随机层数的计算方式 +Redis的事务并不是我们传统意义上理解的事务,我们都知道 单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis **事务的执行并不是原子性的**。 -执行插入操作时计算随机数的过程,是一个很关键的过程,它对skiplist的统计特性有着很重要的影响。这并不是一个普通的服从均匀分布的随机数,它的计算过程如下: +事务可以理解为一个**打包的批量执行脚本**,但**批量指令并非原子化**的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。 -- 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。 -- 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。 -- 节点最大的层数不允许超过一个最大值,记为MaxLevel。 +**总结:** -这个计算随机层数的伪码如下所示: +  1. Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。**鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的**。 + + 2. Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 + + 3. 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。 + +> 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的Redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。 + +## 31. Redis事务的三个阶段 + +1. multi 开启事务 +2. 大量指令入队 +3. exec执行事务块内命令,**截止此处一个事务已经结束。** +4. discard 取消事务 +5. watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。 + +事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队. + +## 32. Redis事务相关命令 + +Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 + +* WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。 +* MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 +* EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。 + 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。 +* UNWATCH命令可以取消watch对所有key的监控。 + +## 33. Redis事务支持隔离性吗? + +Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,**Redis 的事务是总是带有隔离性的**。 + +## 34. Redis为什么不支持事务回滚? + +* Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,这些问题不能在入队时发现,这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中. +* 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 + +## 35. Redis事务其他实现 + +* 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, + 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。 +* 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐。 + +# 主从、哨兵、集群 + +## 36. Redis常见使用方式有哪些? + +Redis的几种常见使用方式包括: + +* Redis单副本; +* Redis多副本(主从); +* Redis Sentinel(哨兵); +* Redis Cluster; +* Redis自研。 + +使用场景: + +如果数据量很少,主要是承载高并发高性能的场景,比如缓存一般就几个G的话,单机足够了。 + +主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。 + +哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。 + +Redis cluster 主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有master的容量总和就是Redis cluster可缓存的数据容量。 + +## 37. 介绍下Redis单副本 + +Redis单副本,采用单个Redis节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。 + +![image-20210829103307048](http://blog-img.coolsen.cn/img/image-20210829103307048.png) + +**优点:** + +* 架构简单,部署方便; +* 高性价比:缓存使用时无需备用节点(单实例可用性可以用supervisor或crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务; +* 高性能。 + +**缺点:** + +* 不保证数据的可靠性; +* 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务; +* 高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用Memcached替代。 + +## 38. 介绍下Redis多副本(主从) + +Redis多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。 + +![image-20210829103327631](http://blog-img.coolsen.cn/img/image-20210829103327631.png) + +**优点:** + +* 高可靠性:一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题; +* 读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。 + +**缺点:** + +* 故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其它从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐; +* 主库的写能力受到单机的限制,可以考虑分片; +* 主库的存储能力受到单机的限制,可以考虑Pika; + +* 原生复制的弊端在早期的版本中也会比较突出,如:Redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿;又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IO和CPU(压缩)资源消耗;发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本。 + +## 39. 介绍下Redis Sentinel(哨兵) + +> 主从模式下,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这种方式并不推荐,实际生产中,我们优先考虑哨兵模式。这种模式下,master 宕机,哨兵会自动选举 master 并将其他的 slave 指向新的 master。 + +Redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群。 + +其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个。 + +![image-20210829103343110](http://blog-img.coolsen.cn/img/image-20210829103343110.png) + +**优点:** + +* Redis Sentinel集群部署简单; +* 能够解决Redis主从模式下的高可用切换问题; +* 很方便实现Redis数据节点的线形扩展,轻松突破Redis自身单线程瓶颈,可极大满足Redis大容量或高性能的业务需求; +* 可以实现一套Sentinel监控一组Redis数据节点或多组数据节点。 + +**缺点:** + +* 部署相对Redis主从模式要复杂一些,原理理解更繁琐; +* 资源浪费,Redis数据节点中slave节点作为备份节点不提供服务; +* Redis Sentinel主要是针对Redis数据节点中的主节点的高可用切换,对Redis的数据节点做失败判定分为主观下线和客观下线两种,对于Redis的从节点有对节点做主观下线操作,并不执行故障转移。 +* 不能解决读写分离问题,实现起来相对复杂。 + +## 40. 介绍下Redis Cluster + +> Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 Redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容。 + +Redis Cluster是社区版推出的Redis分布式集群解决方案,主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。 + +Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。 + +Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。 + +![image-20210829103444245](http://blog-img.coolsen.cn/img/image-20210829103444245.png) + +**优点:** + +* 无中心架构; +* 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布; +* 可扩展性:可线性扩展到1000多个节点,节点可动态添加或删除; +* 高可用性:部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升; +* 降低运维成本,提高系统的扩展性和可用性。 + +**缺点:** + +* Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。 +* 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。 +* 数据通过异步复制,不保证数据的强一致性。 +* 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。 +* Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。 +* Key批量操作限制,如使用mset、mget目前只支持具有相同slot值的Key执行批量操作。对于映射为不同slot值的Key由于Keys不支持跨slot查询,所以执行mset、mget、sunion等操作支持不友好。 +* Key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个Key分布于不同的节点上时无法使用事务功能。 +* Key作为数据分区的最小粒度,不能将一个很大的键值对象如hash、list等映射到不同的节点。 +* 不支持多数据库空间,单机下的Redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db 0。 +* 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。 +* 避免产生hot-key,导致主库节点成为系统的短板。 +* 避免产生big-key,导致网卡撑爆、慢查询等。 +* 重试时间应该大于cluster-node-time时间。 +* Redis Cluster不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。 + +## 41. 介绍下Redis自研 + +Redis自研的高可用解决方案,主要体现在配置中心、故障探测和failover的处理机制上,通常需要根据企业业务的实际线上环境来定制化。 + +![image-20210829103426922](http://blog-img.coolsen.cn/img/image-20210829103426922.png) + +**优点:** + +* 高可靠性、高可用性; +* 自主可控性高; +* 贴切业务实际需求,可缩性好,兼容性好。 + +**缺点:** + +* 实现复杂,开发成本高; +* 需要建立配套的周边设施,如监控,域名服务,存储元数据信息的数据库等; +* 维护成本高。 + +## 42. Redis高可用方案具体怎么实施? + +使用官方推荐的哨兵(sentinel)机制就能实现,当主节点出现故障时,由Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。它有四个主要功能: + +- 集群监控,负责监控Redis master和slave进程是否正常工作。 +- 消息通知,如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 +- 故障转移,如果master node挂掉了,会自动转移到slave node上。 +- 配置中心,如果故障转移发生了,通知client客户端新的master地址。 + +## 43. 了解主从复制的原理吗? + +**1、主从架构的核心原理** + +当启动一个slave node的时候,它会发送一个PSYNC命令给master node + +如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization + +开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。 + +slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。 + +**2、主从复制的断点续传** + +从Redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份 + +master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制 + +但是如果没有找到对应的offset,那么就会执行一次resynchronization + +**3、无磁盘化复制** + +master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了 + +repl-diskless-sync repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来 + +**4、过期key处理** + +slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。 + +## 44. 由于主从延迟导致读取到过期数据怎么处理? + +1. 通过scan命令扫库:当Redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥Redis惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。 +2. Redis加入了一个新特性来解决主从不一致导致读取到过期数据问题,增加了key是否过期以及对主从库的判断,如果key已过期,当前访问的master则返回null;当前访问的是从库,且执行的是只读命令也返回null。 + +## 45. 主从复制的过程中如果因为网络原因停止复制了会怎么样? + +如果出现网络故障断开连接了,会自动重连的,从Redis 2.8开始,就支持主从复制的断点续传,可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 + +master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。 + +master node会在内存中创建一个`backlog`,master和slave都会保存一个`replica offset`,还有一个`master id`,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制。 + +但是如果没有找到对应的offset,那么就会执行一次`resynchronization`全量复制。 + +## 46. Redis主从架构数据会丢失吗,为什么? + +有两种数据丢失的情况: + +1. 异步复制导致的数据丢失:因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。 +2. 脑裂导致的数据丢失:某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。 + +## 47. 如何解决主从架构数据丢失的问题? + +数据丢失的问题是不可避免的,但是我们可以尽量减少。 + +在Redis的配置文件里设置参数 ``` -randomLevel() - level := 1 - // random()返回一个[0...1)的随机数 - while random() < p and level < MaxLevel do - level := level + 1 - return level复制代码 +min-slaves-to-write 1 +min-slaves-max-lag 10 ``` -randomLevel()的伪码中包含两个参数,一个是p,一个是MaxLevel。在Redis的skiplist实现中,这两个参数的取值为: +`min-slaves-to-write`默认情况下是0,`min-slaves-max-lag`默认情况下是10。 + +上面的配置的意思是要求至少有1个slave,数据复制和同步的延迟不能超过10秒。如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了。 + +减小`min-slaves-max-lag`参数的值,这样就可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往master中写入数据。 + +那么对于client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入master来保证数据不丢失;也可以将数据写入kafka消息队列,隔一段时间去消费kafka中的数据。 + +## 48. Redis哨兵是怎么工作的? + +1. 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。 + +2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被当前 Sentinel 标记为主观下线。 + +3. 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。 + +4. 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。 + +5. 当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 )。 + +6. 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。 + +7. sentinel节点会与其他sentinel节点进行“沟通”,投票选举一个sentinel节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。 + +## 49. 故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么? + +如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息。 + +* 跟master断开连接的时长。 + 如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master. ``` -p = 1/4 -MaxLevel = 32 +( down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state ``` -#### skiplist的算法性能分析 +* slave优先级。 + 按照slave优先级进行排序,slave priority越低,优先级就越高 -根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。 +* 复制offset。 + 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高 -当skiplist中有n个节点的时候,它的总层数的概率均值是多少。这个问题直观上比较好理解。根据节点的层数随机算法,容易得出: +* run id + 如果上面两个条件都相同,那么选择一个run id比较小的那个slave。 -- 第1层链表固定有n个节点; -- 第2层链表平均有n*p个节点; -- 第3层链表平均有n*p2个节点; +## 50. 同步配置的时候其他哨兵根据什么更新自己的配置呢? -计算很复杂,没有看懂,总结来说:平均时间复杂度为O(log n) +执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。 -### rank排名计算 +如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch 作为新的version号。 -图中前向指针上面括号中的数字,表示对应的span的值。即当前指针跨越了多少个节点,这个计数不包括指针的起点节点,但包括指针的终点节点。 +这个version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的。 -img +## 51. 为什么Redis哨兵集群只有2个节点无法正常工作? -举例: +哨兵集群必须部署2个以上节点。 -在这个skiplist中查找score=89.0的元素(即Bob的成绩数据),在查找路径中,我们会跨域图中标红的指针,这些指针上面的span值累加起来,就得到了Bob的排名(2+2+1)-1=4(减1是因为rank值以0起始)。需要注意这里算的是从小到大的排名,而如果要算从大到小的排名,只需要用skiplist长度减去查找路径上的span累加值,即6-(2+2+1)=1。 +如果两个哨兵实例,即两个Redis实例,一主一从的模式。 -通过这种方式就能得到一条O(log n)的查找路径 +则Redis的配置quorum=1,表示一个哨兵认为master宕机即可认为master已宕机。 -## 12. Redis为什么用skiplist(跳跃表)而不用平衡树? +但是如果是机器1宕机了,那哨兵1和master都宕机了,虽然哨兵2知道master宕机了,但是这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移。 -Redis的作者 @antirez 从内存占用、对范围查找的支持和实现难易程度做了解释。[原文](https://news.ycombinator.com/item?id=1171423): +但此时哨兵1没了就只有1个哨兵了了,此时就没有majority来允许执行故障转移,所以故障转移不会执行。 -> There are a few reasons: -> -> 1) They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees. -> -> 2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees. -> -> 3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code. +## 52. Redis cluster中是如何实现数据分布的?这种方式有什么优点? -总结来说: +Redis cluster有固定的16384个hash slot(哈希槽),对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。 -* 不需要太多内存 -* 范围查询和平衡树一样好 -* 容易实现和调试 +Redis cluster中每个master都会持有部分slot(槽),比如有3个master,那么可能每个master持有5000多个hash slot。 -## 13. 假如 Redis 里面有 1 亿个key,其中有 10w 个key 是以某个固定的已知的前缀开头的,如果将它们全部找出来? +hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。每次增加或减少master节点都是对16384取模,而不是根据master数量,这样原本在老的master上的数据不会因master的新增或减少而找不到。并且增加或减少master时Redis cluster移动hash slot的成本是非常低的。 -使用 keys 指令可以扫出指定模式的 key 列表。 +## 53. Redis cluster节点间通信是什么机制? -追问: 如果这个 redis 正在给线上的业务提供服务, 那使用 keys 指令会有什么问题? +Redis cluster节点间采取gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更之后U不断地i将元数据发送给其他节点让其他节点进行数据变更。 -这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间, 线上服务会停顿, 直到指令执行完毕, 服务才能恢复。这个时候可以使用 scan 指令, scan 指令可以无阻塞的提取出指定模式的 key 列表, 但是会有一定的重复概率, 在客户端做一次去重就可以了, 但是整体所花费的时间会比直接用 keys 指令长。 +> 节点互相之间不断通信,保持整个集群所有节点的数据是完整的。 +> 主要交换故障信息、节点的增加和移除、hash slot信息等。 -## 14. **Redis6.0为什么要引入多线程呢?** +这种机制的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; -Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。 +缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后。 -但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。 +# 分布式问题 -从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向: +## 54. 什么是分布式锁?为什么用分布式锁? -• 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式 • 使用多线程充分利用多核,典型的实现比如 Memcached。 +锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized 、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。 -协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因: +分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。 -• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核 • 多线程任务可以分摊 Redis 同步 IO 读写负荷 +思路是:在整个系统提供一个**全局、唯一**的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。 -![img](https://obs-emcsapp-public.obs.cn-north-4.myhwclouds.com/wechatSpider/modb_20200720_110226.png) +一般来说,分布式锁需要满足的特性有这么几点: -**Redis6.0采用多线程后,性能的提升效果如何?** +1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁; -Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。 +2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署; -### 实现机制 +3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁; -**流程简述如下**: +4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。 -1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列 +## 55. 常见的分布式锁有哪些解决方案? -2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程 +实现分布式锁目前有三种流行方案,即基于关系型数据库、Redis、ZooKeeper 的方案 -3、主线程阻塞等待 IO 线程读取 socket 完毕 + 1、基于关系型数据库,如MySQL +基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。 -4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行 +缺点: -5、主线程阻塞等待 IO 线程将数据回写 socket 完毕 +* 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 +* 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 +* 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 +* 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 -6、解除绑定,清空等待队列 +2、基于Redis实现 -在这里插入图片描述 +优点: -该设计有如下特点: +Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。 - 1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写 +缺点: -2、IO 线程只负责读写 socket 解析命令,不负责命令处理 +* Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮; +* key 的过期时间设置多少不明确,只能根据实际情况调整; +* 需要自己不断去尝试获取锁,比较消耗性能。 -****开启多线程后,是否会存在线程并发安全问题?** +3、基于zookeeper -Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。 +优点: -## 15. **Redis集群方案应该怎么做?都有哪些方案?** +zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。 -* codis。目前用的最多的集群方案,基本和twemproxy一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新hash节点。 -* redis cluster3.0自带的集群,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。具体看官方文档介绍。 -* 在业务代码层实现,起几个毫无关联的redis实例,在代码层,对key 进行hash计算,然后去对应的redis实例操作数据。 这种方式对hash层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。 +缺点: -## 16.讲一讲 位图(Bitmap) +在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风险。 -位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。 +## 56. Redis实现分布式锁 -![img](https://user-gold-cdn.xitu.io/2018/7/2/1645926f4520d0ce?imageslim) +### 分布式锁的三个核心要素 -当我们要统计月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。这时就可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到月底遍历一次位图就可以得到月度活跃用户数。不过这个方法也是有条件的,那就是 userid 是整数连续的,并且活跃占比较高,否则可能得不偿失。 +1、加锁 -## 17. 讲一讲HyperLogLog +使用setnx来加锁。key是锁的唯一标识,按业务来决定命名,value这里设置为test。 -`HyperLogLog`,下面简称为`HLL`,它是 `LogLog` 算法的升级版,作用是能够提供不精确的去重计数。存在以下的特点: +``` +setx key test +``` -- 代码实现较难。 -- 能够使用极少的内存来统计巨量的数据,在 `Redis` 中实现的 `HyperLogLog`,只需要`12K`内存就能统计`2^64`个数据。 -- 计数存在一定的误差,误差率整体较低。标准误差为 0.81% 。 -- 误差可以被设置`辅助计算因子`进行降低。 +当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败; -### 为什么用HyperLogLog +2、解锁 -如果要实现这么一个功能: +有加锁就得有解锁。当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行del指令。 -> 统计 APP或网页 的一个页面,每天有多少用户点击进入的次数。同一个用户的反复点击进入记为 1 次。 +``` +del key +``` -用 `HashMap` 这种数据结构就可以,假设 APP 中日活用户达到`百万`或`千万以上级别`的话,我们采用 `HashMap` 的做法,就会导致程序中占用大量的内存。 +释放锁之后,其他线程就可以继续执行setnx命令来获得锁。 -估算下 `HashMap` 的在应对上述问题时候的内存占用。假设定义`HashMap` 中 `Key` 为 `string` 类型,`value` 为 `bool`。`key` 对应用户的`Id`,`value`是`是否点击进入`。明显地,当百万不同用户访问的时候。此`HashMap` 的内存占用空间为:`100万 * (string + bool)`。 +3、锁超时 -### HyperLogLog原理 +锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。 -如图,给定一系列的随机整数,我们记录下低位连续零位的最大长度 k,通过这个 k 值可以估算出随机数的数量。 +所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx不支持超时参数,所以需要额外指令, -img +``` +expire key 30 +``` -HyperLogLog与伯努利试验有关,具体可参考[HyperLogLog 算法的原理讲解](https://juejin.im/post/5c7900bf518825407c7eafd0) +### 上述分布式锁存在的问题 -## 18. `Redis`单线程如何处理那么多的并发客户端连接? -因为Redis 的线程模型:基于非阻塞的IO多路复用机制。 +**通过上述`setnx` 、`del`和`expire`实现的分布式锁还是存在着一些问题。** -Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。模型如下图: +1、SETNX 和 EXPIRE 非原子性 -[![img](http://cmsblogs.com/wp-content/resources/image.cmsblogs/sike-java/sike-redis/redis-202002171001.png)](http://cmsblogs.com/wp-content/resources/image.cmsblogs/sike-java/sike-redis/redis-202002171001.png) +假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。 -从上图可知,文件事件处理器的结构包含了四个部分: +**解决措施:** -- 多个 Socket -- IO 多路复用程序 -- 文件事件分派器 -- 事件处理器 +由于`setnx`指令本身是不支持传入超时时间的,而在Redis2.6.12版本上为`set`指令增加了可选参数, 用法如下: -多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。 +``` +SET key value [EX seconds][PX milliseconds] [NX|XX] +``` + +- EX second: 设置键的过期时间为second秒; +- PX millisecond:设置键的过期时间为millisecond毫秒; +- NX:只在键不存在时,才对键进行设置操作; +- XX:只在键已经存在时,才对键进行设置操作; +- SET操作完成时,返回OK,否则返回nil。 + +2、锁误解除 + +如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。 + +**解决办法:** + +在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁。 + +具体在加锁的时候把当前线程的id当做value,可生成一个 UUID 标识当前线程,在删除之前验证key对应的value是不是自己线程的id。 + +还可以使用 lua 脚本做验证标识和解锁操作。 + +3、超时解锁导致并发 + +如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。 + +A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题: + +- 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。 +- 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。 + +4、不可重入 + +当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。 + +5、无法等待锁释放 + +上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。 + +- 可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。 +- 另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。 + +具体实现参考:https://xiaomi-info.github.io/2019/12/17/Redis-distributed-lock/ + +## 57. 了解RedLock吗? + +Redlock是一种算法,Redlock也就是 Redis Distributed Lock,可用实现多节点Redis的分布式锁。 + +RedLock官方推荐,Redisson完成了对Redlock算法封装。 + +此种方式具有以下特性: + +* 互斥访问:即永远只有一个 client 能拿到锁 +* 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。 +* 容错性:只要大部分 Redis 节点存活(一半以上),就可以正常提供服务 + +## 58. RedLock的原理 + +假设有5个完全独立的Redis主服务器 + +1. 获取当前时间戳 + +2. client尝试按照顺序使用相同的key,value获取所有Redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的Redis服务。并且试着获取下一个Redis实例。 + + 比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁 + +3. client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个Redis实例成功获取锁,才算真正的获取锁成功 + +4. 如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移); + +5. 如果客户端由于某些原因获取锁失败,便会开始解锁所有Redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁 + +算法示意图如下: + +![image-20210829131128229](http://blog-img.coolsen.cn/img/image-20210829131128229.png) + + + +# 其他 + +## 59. Redis如何做内存优化? + +* **控制key的数量**。当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash,list,set,zset 等结构。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。 -## 参考 +* **缩减键值对象**,降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。 -https://juejin.cn/post/6844903663224225806 + - key长度:如在设计键时,在完整描述业务情况下,键值越短越好。 + - value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。 -https://juejin.cn/post/6844903716290576392 +* **编码优化**。Redis对外提供了string,list,hash,set,zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。可参考文章:https://cloud.tencent.com/developer/article/1162213 -https://www.cnblogs.com/wdliu/p/9377278.html -https://researchlab.github.io/2018/10/08/redis-11-redisio/ +## 60. 如果现在有个读超高并发的系统,用Redis来抗住大部分读请求,你会怎么设计? +如果是读高并发的话,先看读并发的数量级是多少,因为Redis单机的读QPS在万级,每秒几万没问题,使用一主多从+哨兵集群的缓存架构来承载每秒10W+的读并发,主从复制,读写分离。 +使用哨兵集群主要是提高缓存架构的可用性,解决单点故障问题。主库负责写,多个从库负责读,支持水平扩容,根据读请求的QPS来决定加多少个Redis从实例。如果读并发继续增加的话,只需要增加Redis从实例就行了。 -一份后端开发的综合笔记: +如果需要缓存1T+的数据,选择Redis cluster模式,每个主节点存一部分数据,假设一个master存32G,那只需要n*32G>=1T,n个这样的master节点就可以支持1T+的海量数据的存储了。 -> 链接:https://pan.baidu.com/s/1UNq4egIlxe3hijwjTfvW1w -> 提取码:u9go +> Redis单主的瓶颈不在于读写的并发,而在于内存容量,即使是一主多从也是不能解决该问题,因为一主多从架构下,多个slave的数据和master的完全一样。假如master是10G那slave也只能存10G数据。所以数据量受单主的影响。 +> 而这个时候又需要缓存海量数据,那就必须得有多主了,并且多个主保存的数据还不能一样。Redis官方给出的 Redis cluster 模式完美的解决了这个问题。 -![](http://blog-img.coolsen.cn/img/image-20210508113649706.png) \ No newline at end of file +## \ No newline at end of file From a06cc19f39468f575d6c91054f48b9503356de65 Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 00:56:33 +0800 Subject: [PATCH 03/26] add kafka --- ...ka\351\235\242\350\257\225\351\242\230.md" | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 "Kafka\351\235\242\350\257\225\351\242\230.md" diff --git "a/Kafka\351\235\242\350\257\225\351\242\230.md" "b/Kafka\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..92c6baa --- /dev/null +++ "b/Kafka\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,189 @@ +## 1. Apache Kafka是什么? + +Apach Kafka是一款分布式流处理平台,用于实时构建流处理应用。它有一个核心的功能广为人知,即作为企业级的消息引擎被广泛使用(通常也会称之为消息总线message bus)。 + +## 2. Kafka 的设计是什么样的? + +Kafka 将消息以 topic 为单位进行归纳 + +将向 Kafka topic 发布消息的程序成为 producers. + +将预订 topics 并消费消息的程序成为 consumer. + +Kafka 以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 broker. + +producers 通过网络将消息发送到 Kafka 集群,集群向消费者提供消息 + +## 3. Kafka 如何保证高可用? + +`Kafka` 的基本架构组成是:由多个 `broker` 组成一个集群,每个 `broker` 是一个节点;当创建一个 `topic` 时,这个 `topic` 会被划分为多个 `partition`,每个 `partition` 可以存在于不同的 `broker` 上,每个 `partition` 只存放一部分数据。 + +这就是**天然的分布式消息队列**,就是说一个 `topic` 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。 + +在 `Kafka 0.8` 版本之前,是没有 `HA` 机制的,当任何一个 `broker` 所在节点宕机了,这个 `broker` 上的 `partition` 就无法提供读写服务,所以这个版本之前,`Kafka` 没有什么高可用性可言。 + +在 `Kafka 0.8` 以后,提供了 `HA` 机制,就是 `replica` 副本机制。每个 `partition` 上的数据都会同步到其它机器,形成自己的多个 `replica` 副本。所有 `replica` 会选举一个 `leader` 出来,消息的生产者和消费者都跟这个 `leader` 打交道,其他 `replica` 作为 `follower`。写的时候,`leader` 会负责把数据同步到所有 `follower` 上去,读的时候就直接读 `leader` 上的数据即可。`Kafka` 负责均匀的将一个 `partition` 的所有 `replica` 分布在不同的机器上,这样才可以提高容错性。 + +![img](http://blog-img.coolsen.cn/img/Solve-MQ-Problem-With-Kafka-01.png) + +拥有了 `replica` 副本机制,如果某个 `broker` 宕机了,这个 `broker` 上的 `partition` 在其他机器上还存在副本。如果这个宕机的 `broker` 上面有某个 `partition` 的 `leader`,那么此时会从其 `follower` 中重新选举一个新的 `leader` 出来,这个新的 `leader` 会继续提供读写服务,这就有达到了所谓的高可用性。 + +写数据的时候,生产者只将数据写入 `leader` 节点,`leader` 会将数据写入本地磁盘,接着其他 `follower` 会主动从 `leader` 来拉取数据,`follower` 同步好数据了,就会发送 `ack` 给 `leader`,`leader` 收到所有 `follower` 的 `ack` 之后,就会返回写成功的消息给生产者。 + +消费数据的时候,消费者只会从 `leader` 节点去读取消息,但是只有当一个消息已经被所有 `follower` 都同步成功返回 `ack` 的时候,这个消息才会被消费者读到。 + +![img](https://gitee.com/dongzl/article-images/raw/master/2020/13-Solve-MQ-Problem-With-Kafka/Solve-MQ-Problem-With-Kafka-02.png) + +## 4. Kafka 消息是采用 Pull 模式,还是 Push 模式? + +生产者使用push模式将消息发布到Broker,消费者使用pull模式从Broker订阅消息。 + +push模式很难适应消费速率不同的消费者,如果push的速度太快,容易造成消费者拒绝服务或网络拥塞;如果push的速度太慢,容易造成消费者性能浪费。但是采用pull的方式也有一个缺点,就是当Broker没有消息时,消费者会陷入不断地轮询中,为了避免这点,kafka有个参数可以让消费者阻塞知道是否有新消息到达。 + +## 5. Kafka 与传统消息系统之间的区别 + +* Kafka 持久化日志,这些日志可以被重复读取和无限期保留 + +* Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性 + +* Kafka 支持实时的流式处理 + +## 6. 什么是消费者组? + +消费者组是Kafka独有的概念,即消费者组是Kafka提供的可扩展且具有容错性的消费者机制。 + +但实际上,消费者组(Consumer Group)其实包含两个概念,作为队列,消费者组允许你分割数据处理到一组进程集合上(即一个消费者组中可以包含多个消费者进程,他们共同消费该topic的数据),这有助于你的消费能力的动态调整;作为发布-订阅模型(publish-subscribe),Kafka允许你将同一份消息广播到多个消费者组里,以此来丰富多种数据使用场景。 + +需要注意的是:在消费者组中,多个实例共同订阅若干个主题,实现共同消费。同一个组下的每个实例都配置有相同的组ID,被分配不同的订阅分区。当某个实例挂掉的时候,其他实例会自动地承担起它负责消费的分区。 因此,消费者组在一定程度上也保证了消费者程序的高可用性。 + +[![1.jpg](http://dockone.io/uploads/article/20201024/7b359b7a1381541fbacf3ecf20dfb347.jpg)](http://dockone.io/uploads/article/20201024/7b359b7a1381541fbacf3ecf20dfb347.jpg) + +## 7. 在Kafka中,ZooKeeper的作用是什么? + +目前,Kafka使用ZooKeeper存放集群元数据、成员管理、Controller选举,以及其他一些管理类任务。之后,等KIP-500提案完成后,Kafka将完全不再依赖于ZooKeeper。 + +- “存放元数据”是指主题分区的所有数据都保存在 ZooKeeper 中,且以它保存的数据为权威,其他 “人” 都要与它保持对齐。 +- “成员管理” 是指 Broker 节点的注册、注销以及属性变更,等等。 +- “Controller 选举” 是指选举集群 Controller,而其他管理类任务包括但不限于主题删除、参数配置等。 + +KIP-500 思想,是使用社区自研的基于Raft的共识算法,替代ZooKeeper,实现Controller自选举。 + +## 8. 解释下Kafka中位移(offset)的作用 + +在Kafka中,每个主题分区下的每条消息都被赋予了一个唯一的ID数值,用于标识它在分区中的位置。这个ID数值,就被称为位移,或者叫偏移量。一旦消息被写入到分区日志,它的位移值将不能被修改。 + +## 9. kafka 为什么那么快? + +- Cache Filesystem Cache PageCache缓存 +- `顺序写`:由于现代的操作系统提供了预读和写技术,磁盘的顺序写大多数情况下比随机写内存还要快。 +- `Zero-copy`:零拷技术减少拷贝次数 +- `Batching of Messages`:批量量处理。合并小的请求,然后以流的方式进行交互,直顶网络上限。 +- `Pull 拉模式`:使用拉模式进行消息的获取消费,与消费端处理能力相符。 + +## 10. kafka producer发送数据,ack为0,1,-1分别是什么意思? + +- `1`(默认) 数据发送到Kafka后,经过leader成功接收消息的的确认,就算是发送成功了。在这种情况下,如果leader宕机了,则会丢失数据。 +- `0` 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。 +- `-1`producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。当ISR中所有Replica都向Leader发送ACK时,leader才commit,这时候producer才能认为一个请求中的消息都commit了。 + +## 11. Kafka如何保证消息不丢失? + +首先需要弄明白消息为什么会丢失,对于一个消息队列,会有 `生产者`、`MQ`、`消费者` 这三个角色,在这三个角色数据处理和传输过程中,都有可能会出现消息丢失。 + +![img](http://blog-img.coolsen.cn/img/Solve-MQ-Problem-With-Kafka-03.png) + +消息丢失的原因以及解决办法: + +### 消费者异常导致的消息丢失 + +消费者可能导致数据丢失的情况是:消费者获取到了这条消息后,还未处理,`Kafka` 就自动提交了 `offset`,这时 `Kafka` 就认为消费者已经处理完这条消息,其实消费者才刚准备处理这条消息,这时如果消费者宕机,那这条消息就丢失了。 + +消费者引起消息丢失的主要原因就是消息还未处理完 `Kafka` 会自动提交了 `offset`,那么只要关闭自动提交 `offset`,消费者在处理完之后手动提交 `offset`,就可以保证消息不会丢失。但是此时需要注意重复消费问题,比如消费者刚处理完,还没提交 `offset`,这时自己宕机了,此时这条消息肯定会被重复消费一次,这就需要消费者根据实际情况保证幂等性。 + +### 生产者数据传输导致的消息丢失 + +对于生产者数据传输导致的数据丢失主常见情况是生产者发送消息给 `Kafka`,由于网络等原因导致消息丢失,对于这种情况也是通过在 **producer** 端设置 **acks=all** 来处理,这个参数是要求 `leader` 接收到消息后,需要等到所有的 `follower` 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试。 + +### Kafka 导致的消息丢失 + +`Kafka` 导致的数据丢失一个常见的场景就是 `Kafka` 某个 `broker` 宕机,,而这个节点正好是某个 `partition` 的 `leader` 节点,这时需要重新重新选举该 `partition` 的 `leader`。如果该 `partition` 的 `leader` 在宕机时刚好还有些数据没有同步到 `follower`,此时 `leader` 挂了,在选举某个 `follower` 成 `leader` 之后,就会丢失一部分数据。 + +对于这个问题,`Kafka` 可以设置如下 4 个参数,来尽量避免消息丢失: + +- 给 `topic` 设置 `replication.factor` 参数:这个值必须大于 `1`,要求每个 `partition` 必须有至少 `2` 个副本; +- 在 `Kafka` 服务端设置 `min.insync.replicas` 参数:这个值必须大于 `1`,这个参数的含义是一个 `leader` 至少感知到有至少一个 `follower` 还跟自己保持联系,没掉队,这样才能确保 `leader` 挂了还有一个 `follower` 节点。 +- 在 `producer` 端设置 `acks=all`,这个是要求每条数据,必须是写入所有 `replica` 之后,才能认为是写成功了; +- 在 `producer` 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个参数的含义是一旦写入失败,就无限重试,卡在这里了。 + +## 13. Kafka 如何保证消息的顺序性 + +在某些业务场景下,我们需要保证对于有逻辑关联的多条MQ消息被按顺序处理,比如对于某一条数据,正常处理顺序是`新增-更新-删除`,最终结果是数据被删除;如果消息没有按序消费,处理顺序可能是`删除-新增-更新`,最终数据没有被删掉,可能会产生一些逻辑错误。对于如何保证消息的顺序性,主要需要考虑如下两点: + +- 如何保证消息在 `Kafka` 中顺序性; +- 如何保证消费者处理消费的顺序性。 + +### 如何保证消息在 Kafka 中顺序性 + +对于 `Kafka`,如果我们创建了一个 `topic`,默认有三个 `partition`。生产者在写数据的时候,可以指定一个 `key`,比如在订单 `topic` 中我们可以指定订单 `id` 作为 `key`,那么相同订单 `id` 的数据,一定会被分发到同一个 `partition` 中去,而且这个 `partition` 中的数据一定是有顺序的。消费者从 `partition` 中取出来数据的时候,也一定是有顺序的。通过制定 `key` 的方式首先可以保证在 `kafka` 内部消息是有序的。 + +### 如何保证消费者处理消费的顺序性 + +对于某个 `topic` 的一个 `partition`,只能被同组内部的一个 `consumer` 消费,如果这个 `consumer` 内部还是单线程处理,那么其实只要保证消息在 `MQ` 内部是有顺序的就可以保证消费也是有顺序的。但是单线程吞吐量太低,在处理大量 `MQ` 消息时,我们一般会开启多线程消费机制,那么如何保证消息在多个线程之间是被顺序处理的呢?对于多线程消费我们可以预先设置 `N` 个内存 `Queue`,具有相同 `key` 的数据都放到同一个内存 `Queue` 中;然后开启 `N` 个线程,每个线程分别消费一个内存 `Queue` 的数据即可,这样就能保证顺序性。当然,消息放到内存 `Queue` 中,有可能还未被处理,`consumer` 发生宕机,内存 `Queue` 中的数据会全部丢失,这就转变为上面提到的**如何保证消息的可靠传输**的问题了。 + +## 14. Kafka中的ISR、AR代表什么?ISR的伸缩指什么? + +- `ISR`:In-Sync Replicas 副本同步队列 +- `AR`:Assigned Replicas 所有副本 + +ISR是由leader维护,follower从leader同步数据有一些延迟(包括`延迟时间replica.lag.time.max.ms`和`延迟条数replica.lag.max.messages`两个维度,当前最新的版本0.10.x中只支持`replica.lag.time.max.ms`这个维度),任意一个超过阈值都会把follower剔除出ISR,存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。 + +> AR=ISR+OSR。 + +## 15. 描述下 Kafka 中的领导者副本(Leader Replica)和追随者副本(Follower Replica)的区别 + +Kafka副本当前分为领导者副本和追随者副本。只有Leader副本才能对外提供读写服务,响应Clients端的请求。Follower副本只是采用拉(PULL)的方式,被动地同步Leader副本中的数据,并且在Leader副本所在的Broker宕机后,随时准备应聘Leader副本。 + +加分点: + +- 强调Follower副本也能对外提供读服务。自Kafka 2.4版本开始,社区通过引入新的Broker端参数,允许Follower副本有限度地提供读服务。 +- 强调Leader和Follower的消息序列在实际场景中不一致。通常情况下,很多因素可能造成Leader和Follower之间的不同步,比如程序问题,网络问题,broker问题等,短暂的不同步我们可以关注(秒级别),但长时间的不同步可能就需要深入排查了,因为一旦Leader所在节点异常,可能直接影响可用性。 + + +注意:之前确保一致性的主要手段是高水位机制(HW),但高水位值无法保证Leader连续变更场景下的数据一致性,因此,社区引入了Leader Epoch机制,来修复高水位值的弊端。 + +## 16. 分区Leader选举策略有几种? + +分区的Leader副本选举对用户是完全透明的,它是由Controller独立完成的。你需要回答的是,在哪些场景下,需要执行分区Leader选举。每一种场景对应于一种选举策略。 + +- OfflinePartition Leader选举:每当有分区上线时,就需要执行Leader选举。所谓的分区上线,可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区Leader选举场景。 +- ReassignPartition Leader选举:当你手动运行kafka-reassign-partitions命令,或者是调用Admin的alterPartitionReassignments方法执行分区副本重分配时,可能触发此类选举。假设原来的AR是[1,2,3],Leader是1,当执行副本重分配后,副本集合AR被设置成[4,5,6],显然,Leader必须要变更,此时会发生Reassign Partition Leader选举。 +- PreferredReplicaPartition Leader选举:当你手动运行kafka-preferred-replica-election命令,或自动触发了Preferred Leader选举时,该类策略被激活。所谓的Preferred Leader,指的是AR中的第一个副本。比如AR是[3,2,1],那么,Preferred Leader就是3。 +- ControlledShutdownPartition Leader选举:当Broker正常关闭时,该Broker上的所有Leader副本都会下线,因此,需要为受影响的分区执行相应的Leader选举。 + + +这4类选举策略的大致思想是类似的,即从AR中挑选首个在ISR中的副本,作为新Leader。 + +## 17. Kafka的哪些场景中使用了零拷贝(Zero Copy)? + +在Kafka中,体现Zero Copy使用场景的地方有两处:基于mmap的索引和日志文件读写所用的TransportLayer。 + +先说第一个。索引都是基于MappedByteBuffer的,也就是让用户态和内核态共享内核态的数据缓冲区,此时,数据不需要复制到用户态空间。不过,mmap虽然避免了不必要的拷贝,但不一定就能保证很高的性能。在不同的操作系统下,mmap的创建和销毁成本可能是不一样的。很高的创建和销毁开销会抵消Zero Copy带来的性能优势。由于这种不确定性,在Kafka中,只有索引应用了mmap,最核心的日志并未使用mmap机制。 + +再说第二个。TransportLayer是Kafka传输层的接口。它的某个实现类使用了FileChannel的transferTo方法。该方法底层使用sendfile实现了Zero Copy。对Kafka而言,如果I/O通道使用普通的PLAINTEXT,那么,Kafka就可以利用Zero Copy特性,直接将页缓存中的数据发送到网卡的Buffer中,避免中间的多次拷贝。相反,如果I/O通道启用了SSL,那么,Kafka便无法利用Zero Copy特性了。 + +## 18. 为什么Kafka不支持读写分离? + +在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读的生产消费模型。 + +Kafka 并不支持主写从读,因为主写从读有 2 个很明 显的缺点: + +- **数据一致性问题**。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。 +- **延时问题**。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历`网络→主节点内存→网络→从节点内存`这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历`网络→主节点内存→主节点磁盘→网络→从节点内存→从节点磁盘`这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。 + +## 参考 + +http://dockone.io/article/10853 + +https://segmentfault.com/a/1190000023716306 + +https://dongzl.github.io/2020/03/16/13-Solve-MQ-Problem-With-Kafka/index.html \ No newline at end of file From c5a1af2db2b473b0b4125fd9503203aaddb04f94 Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 00:57:05 +0800 Subject: [PATCH 04/26] update Spring --- Spring/Spring.md | 379 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 329 insertions(+), 50 deletions(-) diff --git a/Spring/Spring.md b/Spring/Spring.md index be23f4a..46b8ea5 100644 --- a/Spring/Spring.md +++ b/Spring/Spring.md @@ -1,4 +1,20 @@ -## 1. 什么是依赖注入?可以通过多少种方式完成依赖注入? +## 1. 使用Spring框架的好处是什么? + +- **轻量:**Spring 是轻量的,基本的版本大约2MB +- **控制反转:**Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们 +- **面向切面的编程(AOP):**Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开 +- **容器:**Spring 包含并管理应用中对象的生命周期和配置 +- **MVC框架:**Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品 +- **事务管理:**Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA) +- **异常处理:**Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。 + +## 2. 什么是 Spring IOC 容器? + +Spring 框架的核心是 Spring 容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过 XML,Java 注解或 Java 代码提供。 + +![image.png](http://blog-img.coolsen.cn/img/3101171-33099411d16ca051.png) + +## 3. 什么是依赖注入?可以通过多少种方式完成依赖注入? 在依赖注入中,您不必创建对象,但必须描述如何创建它们。您不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC 容器将它们装配在一起。 @@ -10,7 +26,7 @@ 在 Spring Framework 中,仅使用构造函数和 setter 注入。 -## 2. 区分 BeanFactory 和 ApplicationContext? +## 4. 区分 BeanFactory 和 ApplicationContext? | BeanFactory | ApplicationContext | | -------------------------- | ------------------------ | @@ -31,7 +47,16 @@ ApplicationContext的优缺点: - 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题。 - 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大。 -## 3. spring 提供了哪些配置方式? +## 5. 区分构造函数注入和 setter 注入 + +| 构造函数注入 | setter 注入 | +| -------------------------- | -------------------------- | +| 没有部分注入 | 有部分注入 | +| 不会覆盖 setter 属性 | 会覆盖 setter 属性 | +| 任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 | +| 适用于设置很多属性 | 适用于设置少量属性 | + +## 6. spring 提供了哪些配置方式? - 基于 xml 配置 @@ -73,7 +98,7 @@ public class StudentConfig { } ``` -## 4. Spring 中的 bean 的作用域有哪些? +## 7. Spring 中的 bean 的作用域有哪些? - singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 - prototype : 每次请求都会创建一个新的 bean 实例。 @@ -81,7 +106,7 @@ public class StudentConfig { - session : :在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。 - global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话 -## 5. 如何理解IoC和DI? +## 8. 如何理解IoC和DI? IOC就是控制反转,通俗的说就是我们不用自己创建实例对象,这些都交给Spring的bean工厂帮我们创建管理。这也是Spring的核心思想,通过面向接口编程的方式来是实现对业务组件的动态依赖。这就意味着IOC是Spring针对解决程序耦合而存在的。在实际应用中,Spring通过配置文件(xml或者properties)指定需要实例化的java类(类名的完整字符串),包括这些java类的一组初始化值,通过加载读取配置文件,用Spring提供的方法(getBean())就可以获取到我们想要的根据指定配置进行初始化的实例对象。 @@ -89,7 +114,7 @@ IOC就是控制反转,通俗的说就是我们不用自己创建实例对象 **DI:DI—Dependency** Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 -## 6. 将一个类声明为Spring的 bean 的注解有哪些? +## 9. 将一个类声明为Spring的 bean 的注解有哪些? 我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现: @@ -98,7 +123,19 @@ IOC就是控制反转,通俗的说就是我们不用自己创建实例对象 - @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 - @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 -## 7. Spring 中的 bean 生命周期? +## 10. spring 支持几种 bean scope? + +Spring bean 支持 5 种 scope: + +- **Singleton** - 每个 Spring IoC 容器仅有一个单实例。 +- **Prototype** - 每次请求都会产生一个新的实例。 +- **Request** - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。 +- **Session** - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。 +- **Global-session** - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。 + +仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。 + +## 11. Spring 中的 bean 生命周期? Bean的生命周期是由容器来管理的。主要在创建和销毁两个时期。 @@ -133,34 +170,67 @@ Bean的生命周期是由容器来管理的。主要在创建和销毁两个时 此时,Bean初始化完成,可以使用这个Bean了。 销毁过程:如果实现了DisposableBean的destroy方法,则调用它,如果实现了自定义的销毁方法,则调用之。 -## 8. 什么是 AOP? +## 12. 什么是 spring 的内部 bean? -AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. -在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** +只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 `` 或 `` 中提供了 `` 元素的使用。内部 bean 总是匿名的,它们总是作为原型。 -## 9. AOP 有哪些实现方式? +例如,假设我们有一个 Student 类,其中引用了 Person 类。这里我们将只创建一个 Person 类实例并在 Student 中使用它。 -实现 AOP 的技术,主要分为两大类: +Student.java -- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; - - 编译时编织(特殊编译器实现) - - 类加载时编织(特殊的类加载器实现)。 -- 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 - - `JDK` 动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。 - - `CGLIB`动态代理: 如果目标类没有实现接口,那么 `Spring AOP` 会选择使用 `CGLIB` 来动态代理目标类 。`CGLIB` ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, `CGLIB` 是通过继承的方式做的动态代理,因此如果某个类被标记为 `final` ,那么它是无法使用 `CGLIB` 做动态代理的。 +```java +public class Student { + private Person person; + //Setters and Getters +} +public class Person { + private String name; + private String address; + //Setters and Getters +} +``` -## 10. Spring AOP and AspectJ AOP 有什么区别? +bean.xml -Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 -Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 +```xml + + + + + + + + + +``` + +## 13. 什么是 spring 装配? + +当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。 + +Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。 + +自动装配的不同模式: -## 11. Spring中出现同名bean怎么办? +- **no** - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。 +- **byName** - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。 +- **byType** - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。 +- **构造函数** - 它通过调用类的构造函数来注入依赖项。它有大量的参数。 +- **autodetect** - 首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。 + +## 14. 自动装配有什么局限? + +- 覆盖的可能性 - 您始终可以使用 `` 和 `` 设置指定依赖项,这将覆盖自动装配。 +- 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。 +- 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。 + +## 15. Spring中出现同名bean怎么办? - 同一个配置文件内同名的Bean,以最上面定义的为准 - 不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件 - 同文件中ComponentScan和@Bean出现同名Bean。同文件下@Bean的会生效,@ComponentScan扫描进来不会生效。通过@ComponentScan扫描进来的优先级是最低的,原因就是它扫描进来的Bean定义是最先被注册的~ -## 12. Spring 怎么解决循环依赖问题? +## 16. Spring 怎么解决循环依赖问题? spring对循环依赖的处理有三种情况: ①构造器的循环依赖:这种依赖spring是处理不了的,直 接抛出BeanCurrentlylnCreationException异常。 @@ -182,28 +252,52 @@ Spring的单例对象的初始化主要分为三步: ![](http://blog-img.coolsen.cn/img/1584758309616_10.png) -A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步(createBeanINstance实例化),并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 +举例:A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了 -## 13. SpringMVC 工作原理了解吗? +初始化的第一步(createBeanINstance实例化),并且将自己提前曝光到singletonFactories中。 -**原理如下图所示:** +此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过 -![SpringMVC运行原理](https://user-gold-cdn.xitu.io/2019/6/5/16b27eedc1634c3f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。 -上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 `DispatcherServlet` 的作用是接收请求,响应结果。 +此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 -**流程说明(重要):** +## 17. Spring 中的单例 bean 的线程安全问题? -1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 -2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 -3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 -4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 -5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 -6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 -7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 -8. 把 `View` 返回给请求者(浏览器) +当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。 +**线程安全问题都是由全局变量及静态变量引起的。** +若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全. + +**无状态bean和有状态bean** + +- 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。 +- 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。 + +在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,适合用Prototype原型模式。 +Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。 -## 14. Spring 框架中用到了哪些设计模式? +## 18. 什么是 AOP? + +AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. +在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** + +## 19. AOP 有哪些实现方式? + +实现 AOP 的技术,主要分为两大类: + +- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; + - 编译时编织(特殊编译器实现) + - 类加载时编织(特殊的类加载器实现)。 +- 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 + - `JDK` 动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。 + - `CGLIB`动态代理: 如果目标类没有实现接口,那么 `Spring AOP` 会选择使用 `CGLIB` 来动态代理目标类 。`CGLIB` ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, `CGLIB` 是通过继承的方式做的动态代理,因此如果某个类被标记为 `final` ,那么它是无法使用 `CGLIB` 做动态代理的。 + +## 20. Spring AOP and AspectJ AOP 有什么区别? + +Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 +Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 + +## 21. Spring 框架中用到了哪些设计模式? **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 @@ -219,12 +313,22 @@ A的某个field或者setter依赖了B的实例对象,同时B的某个field或 **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 -## 15. Spring 事务实现方式有哪些? +## 22. Spring 事务实现方式有哪些? - 编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。 - 声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者XML配置管理事务。 -## 16. spring事务定义的传播规则 +## 23. Spring框架的事务管理有哪些优点? + +- 它提供了跨不同事务api(如JTA、JDBC、Hibernate、JPA和JDO)的一致编程模型。 + +- 它为编程事务管理提供了比JTA等许多复杂事务API更简单的API。 + +- 它支持声明式事务管理。 + +- 它很好地集成了Spring的各种数据访问抽象。 + +## 24. spring事务定义的传播规则 - PROPAGATION_REQUIRED: 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 - PROPAGATION_SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式执行。 @@ -234,23 +338,198 @@ A的某个field或者setter依赖了B的实例对象,同时B的某个field或 - PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。 - PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。 -## 17. .Spring 中的单例 bean 的线程安全问题? +## 25. SpringMVC 工作原理了解吗? -当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。 -**线程安全问题都是由全局变量及静态变量引起的。** -若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全. +**原理如下图所示:** -**无状态bean和有状态bean** +![img](http://blog-img.coolsen.cn/img/SpingMVC-Process.jpg) -- 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。 -- 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。 +上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 `DispatcherServlet` 的作用是接收请求,响应结果。 -在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,适合用Prototype原型模式。 -Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。 +**流程说明(重要):** + +1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 +3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 +4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 +6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 +7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 +8. 把 `View` 返回给请求者(浏览器) + +## 26. 简单介绍 Spring MVC 的核心组件 + +那么接下来就简单介绍一下 `DispatcherServlet` 和九大组件(按使用顺序排序的): + +| 组件 | 说明 | +| :-------------------------- | :----------------------------------------------------------- | +| DispatcherServlet | Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作 | +| MultipartResolver | 内容类型( `Content-Type` )为 `multipart/*` 的请求的解析器,例如解析处理文件上传的请求,便于获取参数信息以及上传的文件 | +| HandlerMapping | 请求的处理器匹配器,负责为请求找到合适的 `HandlerExecutionChain` 处理器执行链,包含处理器(`handler`)和拦截器们(`interceptors`) | +| HandlerAdapter | 处理器的适配器。因为处理器 `handler` 的类型是 Object 类型,需要有一个调用者来实现 `handler` 是怎么被执行。Spring 中的处理器的实现多变,比如用户处理器可以实现 Controller 接口、HttpRequestHandler 接口,也可以用 `@RequestMapping` 注解将方法作为一个处理器等,这就导致 Spring MVC 无法直接执行这个处理器。所以这里需要一个处理器适配器,由它去执行处理器 | +| HandlerExceptionResolver | 处理器异常解析器,将处理器( `handler` )执行时发生的异常,解析( 转换 )成对应的 ModelAndView 结果 | +| RequestToViewNameTranslator | 视图名称转换器,用于解析出请求的默认视图名 | +| LocaleResolver | 本地化(国际化)解析器,提供国际化支持 | +| ThemeResolver | 主题解析器,提供可设置应用整体样式风格的支持 | +| ViewResolver | 视图解析器,根据视图名和国际化,获得最终的视图 View 对象 | +| FlashMapManager | FlashMap 管理器,负责重定向时,保存参数至临时存储(默认 Session) | + +Spring MVC 对各个组件的职责划分的比较清晰。`DispatcherServlet` 负责协调,其他组件则各自做分内之事,互不干扰。 + +## 27. @Controller 注解有什么用? + +`@Controller` 注解标记一个类为 Spring Web MVC **控制器** Controller。Spring MVC 会将扫描到该注解的类,然后扫描这个类下面带有 `@RequestMapping` 注解的方法,根据注解信息,为这个方法生成一个对应的**处理器**对象,在上面的 HandlerMapping 和 HandlerAdapter组件中讲到过。 + +当然,除了添加 `@Controller` 注解这种方式以外,你还可以实现 Spring MVC 提供的 `Controller` 或者 `HttpRequestHandler` 接口,对应的实现类也会被作为一个**处理器**对象 + +## 28. @RequestMapping 注解有什么用? + +`@RequestMapping` 注解,在上面已经讲过了,配置**处理器**的 HTTP 请求方法,URI等信息,这样才能将请求和方法进行映射。这个注解可以作用于类上面,也可以作用于方法上面,在类上面一般是配置这个**控制器**的 URI 前缀 + +## 29. @RestController 和 @Controller 有什么区别? + +`@RestController` 注解,在 `@Controller` 基础上,增加了 `@ResponseBody` 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。当然,返回什么样的数据格式,根据客户端的 `ACCEPT` 请求头来决定。 + +## 30. @RequestMapping 和 @GetMapping 注解的不同之处在哪里? + +1. `@RequestMapping`:可注解在类和方法上;`@GetMapping` 仅可注册在方法上 +2. `@RequestMapping`:可进行 GET、POST、PUT、DELETE 等请求方法;`@GetMapping` 是 `@RequestMapping` 的 GET 请求方法的特例,目的是为了提高清晰度。 + +## 31. @RequestParam 和 @PathVariable 两个注解的区别 + +两个注解都用于方法参数,获取参数值的方式不同,`@RequestParam` 注解的参数从请求携带的参数中获取,而 `@PathVariable` 注解从请求的 URI 中获取 + +## 32. 返回 JSON 格式使用什么注解? + +可以使用 **`@ResponseBody`** 注解,或者使用包含 `@ResponseBody` 注解的 **`@RestController`** 注解。 + +当然,还是需要配合相应的支持 JSON 格式化的 HttpMessageConverter 实现类。例如,Spring MVC 默认使用 MappingJackson2HttpMessageConverter。 + +## 33. 什么是springmvc拦截器以及如何使用它? + +Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法: + +- preHandle:在执行实际处理程序之前调用。 +- postHandle:在执行完实际程序之后调用。 +- afterCompletion:在完成请求后调用。 + +## 34. Spring MVC 和 Struts2 的异同? + +**入口**不同 + +- Spring MVC 的入门是一个 Servlet **控制器**。 +- Struts2 入门是一个 Filter **过滤器**。 + +**配置映射**不同, + +- Spring MVC 是基于**方法**开发,传递参数是通过**方法形参**,一般设置为**单例**。 +- Struts2 是基于**类**开发,传递参数是通过**类的属性**,只能设计为**多例**。 + +**视图**不同 + +- Spring MVC 通过参数解析器是将 Request 对象内容进行解析成方法形参,将响应数据和页面封装成 **ModelAndView** 对象,最后又将模型数据通过 **Request** 对象传输到页面。其中,如果视图使用 JSP 时,默认使用 **JSTL** 。 +- Struts2 采用**值栈**存储请求和响应的数据,通过 **OGNL** 存取数据。 + +## 35. REST 代表着什么? + +REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端 + +可以看看 [REST API design and development](http://bit.ly/2zIGzWK) ,知乎上的 [《怎样用通俗的语言解释 REST,以及 RESTful?》](https://www.zhihu.com/question/28557115)了解。 + +## 36. 什么是安全的 REST 操作? + +REST 接口是通过 HTTP 方法完成操作 + +- 一些 HTTP 操作是安全的,如 GET 和 HEAD ,它不能在服务端修改资源 +- 换句话说,PUT、POST 和 DELETE 是不安全的,因为他们能修改服务端的资源 + +所以,是否安全的界限,在于**是否修改**服务端的资源 + +## 37. REST API 是无状态的吗? + +**是的**,REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的 + +REST API 中的请求应该包含处理它所需的所有细节。它**不应该**依赖于以前或下一个请求或服务器端维护的一些数据,例如会话 + +**REST 规范为使其无状态设置了一个约束,在设计 REST API 时,你应该记住这一点** + +## 38. REST安全吗? 你能做什么来保护它? + +安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的 + +REST 通常不是安全的,需要开发人员自己实现安全机制 + +## 39. 为什么要用SpringBoot? + +在使用Spring框架进行开发的过程中,需要配置很多Spring框架包的依赖,如spring-core、spring-bean、spring-context等,而这些配置通常都是重复添加的,而且需要做很多框架使用及环境参数的重复配置,如开启注解、配置日志等。Spring Boot致力于弱化这些不必要的操作,提供默认配置,当然这些默认配置是可以按需修改的,快速搭建、开发和运行Spring应用。 + +以下是使用SpringBoot的一些好处: + +- 自动配置,使用基于类路径和应用程序上下文的智能默认值,当然也可以根据需要重写它们以满足开发人员的需求。 +- 创建Spring Boot Starter 项目时,可以选择选择需要的功能,Spring Boot将为你管理依赖关系。 +- SpringBoot项目可以打包成jar文件。可以使用Java-jar命令从命令行将应用程序作为独立的Java应用程序运行。 +- 在开发web应用程序时,springboot会配置一个嵌入式Tomcat服务器,以便它可以作为独立的应用程序运行。(Tomcat是默认的,当然你也可以配置Jetty或Undertow) +- SpringBoot包括许多有用的非功能特性(例如安全和健康检查)。 + +## 40. Spring Boot中如何实现对不同环境的属性配置文件的支持? + +Spring Boot支持不同环境的属性配置文件切换,通过创建application-{profile}.properties文件,其中{profile}是具体的环境标识名称,例如:application-dev.properties用于开发环境,application-test.properties用于测试环境,application-uat.properties用于uat环境。如果要想使用application-dev.properties文件,则在application.properties文件中添加spring.profiles.active=dev。 + +如果要想使用application-test.properties文件,则在application.properties文件中添加spring.profiles.active=test。 + +## 41. Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? + +启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: + +@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 + +@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 + +@ComponentScan:Spring组件扫描。 + +## 42. 你如何理解 Spring Boot 中的 Starters? + +Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。 + +Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。 + +## 43. Spring Boot Starter 的工作原理是什么? + +Spring Boot 在启动的时候会干这几件事情: + +- Spring Boot 在启动时会去依赖的 Starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包。 +- 根据 spring.factories 配置加载 AutoConfigure 类 +- 根据 @Conditional 注解的条件,进行自动配置并将 Bean 注入 Spring Context + +总结一下,其实就是 Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应 Bean 资源即可 + +## 44. 保护 Spring Boot 应用有哪些方法? + +- 在生产中使用HTTPS +- 使用Snyk检查你的依赖关系 +- 升级到最新版本 +- 启用CSRF保护 +- 使用内容安全策略防止XSS攻击 + +## 45. Spring 、Spring Boot 和 Spring Cloud 的关系? + +Spring 最初最核心的两大核心功能 Spring Ioc 和 Spring Aop 成就了 Spring,Spring 在这两大核心的功能上不断的发展,才有了 Spring 事务、Spring Mvc 等一系列伟大的产品,最终成就了 Spring 帝国,到了后期 Spring 几乎可以解决企业开发中的所有问题。 + +Spring Boot 是在强大的 Spring 帝国生态基础上面发展而来,发明 Spring Boot 不是为了取代 Spring ,是为了让人们更容易的使用 Spring 。 + +Spring Cloud 是一系列框架的有序集合。它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。 + +Spring Cloud 是为了解决微服务架构中服务治理而提供的一系列功能的开发框架,并且 Spring Cloud 是完全基于 Spring Boot 而开发,Spring Cloud 利用 Spring Boot 特性整合了开源行业中优秀的组件,整体对外提供了一套在微服务架构中服务治理的解决方案。 + +用一组不太合理的包含关系来表达它们之间的关系。 + +Spring ioc/aop > Spring > Spring Boot > Spring Cloud ## 参考 https://juejin.cn/post/6844903860658503693 -https://www.cnblogs.com/jingmoxukong/p/9408037.html \ No newline at end of file +https://www.cnblogs.com/jingmoxukong/p/9408037.html + +http://www.ityouknow.com/springboot/2019/07/24/springboot-interview.html \ No newline at end of file From ffb99e2cab5de46789752d1f85f005b8c2161a37 Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 00:58:03 +0800 Subject: [PATCH 05/26] update Mybatis --- Mybatis/Mybatis.md | 142 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 23 deletions(-) diff --git a/Mybatis/Mybatis.md b/Mybatis/Mybatis.md index 763b26d..3b0b40d 100644 --- a/Mybatis/Mybatis.md +++ b/Mybatis/Mybatis.md @@ -1,27 +1,95 @@ -## 1. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的? -1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。 解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。 +## 1. MyBatis是什么? -2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 +* Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。 +* 作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 +* 通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 +* 由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。 -3、 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 解决: Mybatis自动将java对象映射至sql语句。 +## 2. Mybaits的优缺点 -4、 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 解决:Mybatis自动将sql执行结果映射至java对象。 +优点: + +* 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 +* 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接; +* 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。 +* 能够与Spring很好的集成; +* 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。 + +缺点: + +* SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。 +* SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 + +## 3. 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 + +而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +## 4. Hibernate 和 MyBatis 的区别 + +**相同点**:都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。 + +**不同点** + +1、映射关系 + +MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单。 + +Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂。 + +2、 SQL优化和移植性 + +Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。 +MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。 + +3、开发难易程度和学习成本 + +Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统 + +MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统 + +**总结** + +MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架, + +Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。 + +## 5. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的? + +1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。 + +解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。 + +2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。 + +解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 + +3、 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 + +解决: Mybatis自动将java对象映射至sql语句。 + +4、 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 + +解决:Mybatis自动将sql执行结果映射至java对象。 + +## 6. MyBatis编程步骤是什么样的? -## 2. MyBatis编程步骤是什么样的? 1、创建SqlSessionFactory 2、通过SqlSessionFactory创建SqlSession 3、 通过sqlsession执行数据库操作 4、 调用session.commit()提交事务 5、 调用session.close()关闭会话 -## 3. MyBatis与Hibernate有哪些不同? +## 7. MyBatis与Hibernate有哪些不同? + 1、Mybatis 和 hibernate 不同,它不完全是一个 ORM 框架,因为 MyBatis 需要 程序员自己编写 Sql 语句。  2、Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常 适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需 求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性, 如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大。  3、Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的 软件,如果用 hibernate 开发可以节省很多代码,提高效率 -## 4. Mybaits 的优点: +## 8. Mybaits 的优点: 1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任 何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。 @@ -31,59 +99,87 @@ 4、能够与 Spring 很好的集成; 5、提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射 标签,支持对象关系组件维护 -## 5. MyBatis 框架的缺点:  +## 9. MyBatis 框架的缺点:  1、SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。 2、SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 -## 6. #{}和${}的区别? -{}是预编译处理,${}是字符串替换。 -Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; +## 10. #{}和${}的区别? -Mybatis在处理${}时,就是把${}替换成变量的值。 +* #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。 +* Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。 +* Mybatis在处理时 , 是 原 值 传 入 , 就 是 把 {}时,是原值传入,就是把时,是原值传入,就是把{}替换成变量的值,相当于JDBC中的Statement编译 +* 变量替换后,#{} 对应的变量自动加上单引号 ‘’;变量替换后,${} 对应的变量不会加上单引号 ‘’ +* #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入 +* #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外 -使用#{}可以有效的防止SQL注入,提高系统安全性。 +## 11. 通常一个Xml映射文件,都会写一个Dao接口与之对应,那么这个Dao接口的工作原理是什么?Dao接口里的方法、参数不同时,方法能重载吗? -## 7. 通常一个Xml映射文件,都会写一个Dao接口与之对应,那么这个Dao接口的工作原理是什么?Dao接口里的方法、参数不同时,方法能重载吗? Dao接口即Mapper接口。接口的全限名就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名的拼接字符串作为key值,可唯一定位一个MapperStatement。 Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 -## 8. 在Mapper中如何传递多个参数? + +## 12. 在Mapper中如何传递多个参数? + 1、若Dao层函数有多个参数,那么其对应的xml中,#{0}代表接收的是Dao层中的第一个参数,#{1}代表Dao中的第二个参数,以此类推。 2、使用@Param注解:在Dao层的参数中前加@Param注解,注解内的参数名为传递到Mapper中的参数名。 3、多个参数封装成Map,以HashMap的形式传递到Mapper中。 -## 9. Mybatis动态sql有什么用?执行原理是什么?有哪些动态sql? + +## 13. Mybatis动态sql有什么用?执行原理是什么?有哪些动态sql? + Mybatis动态sql可以在xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值完成逻辑判断,并动态拼接sql的功能。 Mybatis提供了9种动态sql标签:trim、where、set、foreach、if、choose、when、otherwise、bind -## 10. xml映射文件中,不同的xml映射文件id是否可以重复? + +## 14. xml映射文件中,不同的xml映射文件id是否可以重复? + 不同的xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复; 原因是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也不同。 -## 11. Mybatis实现一对一有几种方式?具体是怎么操作的? + +## 15. Mybatis实现一对一有几种方式?具体是怎么操作的? + 有联合查询和嵌套查询两种方式。 联合查询是几个表联合查询,通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过association配置,但另外一个表的查询是通过select配置的。 -## 12. Mybatis实现一对多有几种方式?具体是怎么操作的? + +## 16. Mybatis实现一对多有几种方式?具体是怎么操作的? + 有联合查询和嵌套查询两种方式。 联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过collection,但另外一个表的查询是通过select配置的。 -## 13. Mybatis的一级、二级缓存 + +## 17. Mybatis的一级、二级缓存 + 1、 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。 2、 二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(namespace),并且可自定义存储源,如Ehcache。默认打不开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespace)进行了增/删/改操作后,默认该作用域下所有select中的缓存将被clear。 -## 14. 使用MyBatis的Mapper接口调用时有哪些要求? + +## 18. 使用MyBatis的Mapper接口调用时有哪些要求? + 1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同; 2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType类型相同; 3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同; -4、Mapper.xml文件中的namespace即是mapper接口的类路径。 \ No newline at end of file +4、Mapper.xml文件中的namespace即是mapper接口的类路径。 + +## 19. Mybatis动态sql是做什么的?都有哪些动态sql? + +Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。 + +其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。 + +## 20. Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。 + +原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 \ No newline at end of file From 4b6ce09515ecf84a167af08db8a62f152c9cfbd7 Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 00:58:43 +0800 Subject: [PATCH 06/26] update MQ --- ...ka\351\235\242\350\257\225\351\242\230.md" | 0 ...MQ\351\235\242\350\257\225\351\242\230.md" | 328 ++++++++++++++++++ 2 files changed, 328 insertions(+) rename "Kafka\351\235\242\350\257\225\351\242\230.md" => "MQ/Kafka\351\235\242\350\257\225\351\242\230.md" (100%) create mode 100644 "MQ/MQ\351\235\242\350\257\225\351\242\230.md" diff --git "a/Kafka\351\235\242\350\257\225\351\242\230.md" "b/MQ/Kafka\351\235\242\350\257\225\351\242\230.md" similarity index 100% rename from "Kafka\351\235\242\350\257\225\351\242\230.md" rename to "MQ/Kafka\351\235\242\350\257\225\351\242\230.md" diff --git "a/MQ/MQ\351\235\242\350\257\225\351\242\230.md" "b/MQ/MQ\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..ba97f6f --- /dev/null +++ "b/MQ/MQ\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,328 @@ +## 为什么使用MQ? + +使用MQ的场景很多,主要有三个:解耦、异步、削峰。 + +- 解耦:假设现在,日志不光要插入到数据库里,还要在硬盘中增加文件类型的日志,同时,一些关键日志还要通过邮件的方式发送给指定的人。那么,如果按照原来的逻辑,A可能就需要在原来的代码上做扩展,除了B服务,还要加上日志文件的存储和日志邮件的发送。但是,如果你使用了MQ,那么,A服务是不需要做更改的,它还是将消息放到MQ中即可,其它的服务,无论是原来的B服务还是新增的日志文件存储服务或日志邮件发送服务,都直接从MQ中获取消息并处理即可。这就是解耦,它的好处是提高系统灵活性,扩展性。 +- 异步:可以将一些非核心流程,如日志,短信,邮件等,通过MQ的方式异步去处理。这样做的好处是缩短主流程的响应时间,提升用户体验。 +- 削峰:MQ的本质就是业务的排队。所以,面对突然到来的高并发,MQ也可以不用慌忙,先排好队,不要着急,一个一个来。削峰的好处就是避免高并发压垮系统的关键组件,如某个核心服务或数据库等。 + +下面附场景解释: + +### 解耦 + +场景:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃...... + +![img](http://blog-img.coolsen.cn/img/727602-20200108091205317-949408193.png) + +在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊! + +如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091329888-1880681145.png) + +总结:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。 + +### 异步 + +场景:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091632167-740723329.png) + +一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。 + +如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091722601-747710174.png) + +### 削峰 + +场景:每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。 + +使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。 + +![img](http://blog-img.coolsen.cn/img/727602-20200108091915241-1598228624.png) + +这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。 + +## 消息队列的缺点 + +1、 系统可用性降低 + +系统引入的外部依赖越多,越容易挂掉。 + +2、 系统复杂度提高 + +加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。 + +3、 一致性问题 + +A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,这就数据不一致了。 + +## Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点? + +| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | +| ------------------------ | ------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 开发语言 | java | erlang | java | scala | +| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | +| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | +| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | +| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | +| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | +| 社区活跃度 | 低 | 很高 | 一般 | 很高 | + +- 中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择; +- 大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。 +- 大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,几乎是全世界这个领域的事实性规范。 + +# RabbitMQ + +## 1. RabbitMQ是什么? + +RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。 + +## 2. RabbitMQ特点? + +可靠性: RabbitMQ使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。 + +灵活的路由 : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个 交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。 + +扩展性: 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。 + +高可用性 : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。 + +多种协议: RabbitMQ除了原生支持AMQP协议,还支持STOMP, MQTT等多种消息 中间件协议。 + +多语言客户端 :RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。 + +管理界面 : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。 + +令插件机制: RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自 己的插件。 + +## 3. AMQP是什么? + +RabbitMQ就是 AMQP 协议的 `Erlang` 的实现(当然 RabbitMQ 还支持 `STOMP2`、 `MQTT3` 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。 + +RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。 + +## 4. AMQP的3层协议? + +Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。 + +Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。 + +TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。 + +## 5. 说说Broker服务节点、Queue队列、Exchange交换器? + +- Broker可以看做RabbitMQ的服务节点。一般请下一个Broker可以看做一个RabbitMQ服务器。 +- Queue:RabbitMQ的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。 +- Exchange:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。 + +## 6. 如何保证消息的可靠性? + +分三点: + +* 生产者到RabbitMQ:事务机制和Confirm机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。 +* RabbitMQ自身:持久化、集群、普通模式、镜像模式。 +* RabbitMQ到消费者:basicAck机制、死信队列、消息补偿机制。 + +## 7. 生产者消息运转的流程? + +1. `Producer`先连接到Broker,建立连接Connection,开启一个信道(Channel)。 + +2. `Producer`声明一个交换器并设置好相关属性。 + +3. `Producer`声明一个队列并设置好相关属性。 + +4. `Producer`通过路由键将交换器和队列绑定起来。 + +5. `Producer`发送消息到`Broker`,其中包含路由键、交换器等信息。 + +6. 相应的交换器根据接收到的路由键查找匹配的队列。 + +7. 如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。 + +8. 关闭信道。 + +9. 管理连接。 + +## 8.消费者接收消息过程? + +1. `Producer`先连接到`Broker`,建立连接`Connection`,开启一个信道(`Channel`)。 + +2. 向`Broker`请求消费响应的队列中消息,可能会设置响应的回调函数。 + +3. 等待`Broker`回应并投递相应队列中的消息,接收消息。 + +4. 消费者确认收到的消息,`ack`。 + +5. `RabbitMq`从队列中删除已经确定的消息。 + +6. 关闭信道。 + +7. 关闭连接。 + +## 9. 生产者如何将消息可靠投递到RabbitMQ? + +1. Client发送消息给MQ + +2. MQ将消息持久化后,发送Ack消息给Client,此处有可能因为网络问题导致Ack消息无法发送到Client,那么Client在等待超时后,会重传消息; + +3. Client收到Ack消息后,认为消息已经投递成功。 + +## 10. RabbitMQ如何将消息可靠投递到消费者? + +1. MQ将消息push给Client(或Client来pull消息) + +2. Client得到消息并做完业务逻辑 + +3. Client发送Ack消息给MQ,通知MQ删除该消息,此处有可能因为网络问题导致Ack失败,那么Client会重复消息,这里就引出消费幂等的问题; + +4. MQ将已消费的消息删除。 + +## 11. 如何保证RabbitMQ消息队列的高可用? + +RabbitMQ 有三种模式:`单机模式`,`普通集群模式`,`镜像集群模式`。 + +**单机模式**:就是demo级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式 + +**普通集群模式**:意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。 + +**镜像集群模式**:这种模式,才是所谓的RabbitMQ的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据(元数据指RabbitMQ的配置数据)还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。 + +# RocketMQ + +## 1. RocketMQ是什么? + +RocketMQ 是阿里巴巴开源的分布式消息中间件。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。它里面有几个区别于标准消息中件间的概念,如Group、Topic、Queue等。系统组成则由Producer、Consumer、Broker、NameServer等。 + +**RocketMQ 特点** + +- 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式等特点 +- Producer、Consumer、队列都可以分布式 +- Producer 向一些队列轮流发送消息,队列集合称为 Topic,Consumer 如果做广播消费,则一个 Consumer 实例消费这个 Topic 对应的所有队列,如果做集群消费,则多个 Consumer 实例平均消费这个 Topic 对应的队列集合 +- 能够保证严格的消息顺序 +- 支持拉(pull)和推(push)两种消息模式 +- 高效的订阅者水平扩展能力 +- 实时的消息订阅机制 +- 亿级消息堆积能力 +- 支持多种消息协议,如 JMS、OpenMessaging 等 +- 较少的依赖 + +## 2. RocketMQ由哪些角色组成,每个角色作用和特点是什么? + +| 角色 | 作用 | +| ---------- | ------------------------------------------------------------ | +| Nameserver | 无状态,动态列表;这也是和zookeeper的重要区别之一。zookeeper是有状态的。 | +| Producer | 消息生产者,负责发消息到Broker。 | +| Broker | 就是MQ本身,负责收发消息、持久化消息等。 | +| Consumer | 消息消费者,负责从Broker上拉取消息进行消费,消费完进行ack。 | + +## 3. RocketMQ消费模式有几种? + +消费模型由Consumer决定,消费维度为Topic。 + +1、集群消费 + +* 一条消息只会被同Group中的一个Consumer消费 + +* 多个Group同时消费一个Topic时,每个Group都会有一个Consumer消费到数据 + +2、广播消费 + +消息将对一 个Consumer Group 下的各个 Consumer 实例都消费一遍。即即使这些 Consumer 属于同一个Consumer Group ,消息也会被 Consumer Group 中的每个 Consumer 都消费一次。 + +## 4. RocketMQ消费消息是push还是pull? + +RocketMQ没有真正意义的push,都是pull,虽然有push类,但实际底层实现采用的是**长轮询机制**,即拉取方式 + +> broker端属性 longPollingEnable 标记是否开启长轮询。默认开启 + +### 追问:为什么要主动拉取消息而不使用事件监听方式? + +事件驱动方式是建立好长连接,由事件(发送数据)的方式来实时推送。 + +如果broker主动推送消息的话有可能push速度快,消费速度慢的情况,那么就会造成消息在consumer端堆积过多,同时又不能被其他consumer消费的情况。而pull的方式可以根据当前自身情况来pull,不会造成过多的压力而造成瓶颈。所以采取了pull的方式。 + +## 5. broker如何处理拉取请求的? + +Consumer首次请求Broker + +- Broker中是否有符合条件的消息 + +- 有 + +- - 响应Consumer + - 等待下次Consumer的请求 + +- 没有 + +- - DefaultMessageStore#ReputMessageService#run方法 + - PullRequestHoldService 来Hold连接,每个5s执行一次检查pullRequestTable有没有消息,有的话立即推送 + - 每隔1ms检查commitLog中是否有新消息,有的话写入到pullRequestTable + - 当有新消息的时候返回请求 + - 挂起consumer的请求,即不断开连接,也不返回数据 + - 使用consumer的offset, + +## 6. 如何让RocketMQ保证消息的顺序消费? + +首先多个queue只能保证单个queue里的顺序,queue是典型的FIFO,天然顺序。多个queue同时消费是无法绝对保证消息的有序性的。所以总结如下: + +同一topic,同一个QUEUE,发消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个queue里的消息。 + +## 7. RocketMQ如何保证消息不丢失? + +首先在如下三个部分都可能会出现丢失消息的情况: + +- Producer端 +- Broker端 +- Consumer端 + +1 、Producer端如何保证消息不丢失 + +- 采取send()同步发消息,发送结果是同步感知的。 +- 发送失败后可以重试,设置重试次数。默认3次。 + +- 集群部署,比如发送失败了的原因可能是当前Broker宕机了,重试的时候会发送到其他Broker上。 + +2、Broker端如何保证消息不丢失 + +- 修改刷盘策略为同步刷盘。默认情况下是异步刷盘的。 + +- 集群部署,主从模式,高可用。 + +3、Consumer端如何保证消息不丢失 + +- 完全消费正常后在进行手动ack确认。 + +## 7. rocketMQ的消息堆积如何处理? + +首先要找到是什么原因导致的消息堆积,是Producer太多了,Consumer太少了导致的还是说其他情况,总之先定位问题。 + +然后看下消息消费速度是否正常,正常的话,可以通过上线更多consumer临时解决消息堆积问题 + +### 追问:如果Consumer和Queue不对等,上线了多台也在短时间内无法消费完堆积的消息怎么办? + +- 准备一个临时的topic +- queue的数量是堆积的几倍 +- queue分布到多Broker中 +- 上线一台Consumer做消息的搬运工,把原来Topic中的消息挪到新的Topic里,不做业务逻辑处理,只是挪过去 +- 上线N台Consumer同时消费临时Topic中的数据 +- 改bug +- 恢复原来的Consumer,继续消费之前的Topic + +### 追问:堆积时间过长消息超时了? + +RocketMQ中的消息只会在commitLog被删除的时候才会消失,不会超时。也就是说未被消费的消息不会存在超时删除这情况。 + +### 追问:堆积的消息会不会进死信队列? + +不会,消息在消费失败后会进入重试队列(%RETRY%+ConsumerGroup),18次(默认18次,网上所有文章都说是16次,无一例外。但是我没搞懂为啥是16次,这不是18个时间吗 ?)才会进入死信队列(%DLQ%+ConsumerGroup)。 + +## 8. RocketMQ为什么自研nameserver而不用zk? + +1. RocketMQ只需要一个轻量级的维护元数据信息的组件,为此引入zk增加维护成本还强依赖另一个中间件了。 +2. RocketMQ追求的是AP,而不是CP,也就是需要高可用。 + * zk是CP,因为zk节点间通过zap协议有数据共享,每个节点数据会一致,但是zk集群当挂了一半以上的节点就没法使用了。 + * nameserver是AP,节点间不通信,这样会导致节点间数据信息会发生短暂的不一致,但每个broker都会定时向所有nameserver上报路由信息和心跳。当某个broker下线了,nameserver也会延时30s才知道,而且不会通知客户端(生产和消费者),只能靠客户端自己来拉,rocketMQ是靠消息重试机制解决这个问题的,所以是最终一致性。但nameserver集群只要有一个节点就可用。https://juejin.cn/post/6844904068771479559 \ No newline at end of file From ba0c06fffd5f9f299cecc94d973e8496c0af346c Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 00:59:16 +0800 Subject: [PATCH 07/26] update Dubbo --- ...bo\351\235\242\350\257\225\351\242\230.md" | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 "Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" diff --git "a/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" "b/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..88f3748 --- /dev/null +++ "b/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,182 @@ +## 1.Dubbo是什么? + +Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC 分布式服务框架,现已成为 Apache 基金会孵化项目。 + +其核心部分包含: + +* 集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 +* 远程通讯:提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。 +* 自动发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。 + +## 2. Dubbo和 Spring Cloud 有什么区别? + +最大的区别: + +- Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC通信; +- 而SpringCloud是基于Http协议+rest接口调用远程过程的通信,相对来说,Http请求会有更大的报文,占的带宽也会更多。但是REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更为合适,至于注重通信速度还是方便灵活性,具体情况具体考虑。 + +模块区别: + +* Dubbo主要分为服务注册中心,服务提供者,服务消费者,还有管控中心; + +* 相比起Dubbo简单的四个模块,SpringCloud则是一个完整的分布式一站式框架,他有着一样的服务注册中心,服务提供者,服务消费者,管控台,断路器,分布式配置服务,消息总线,以及服务追踪等; + +## 3. Dubbo核心组件有哪些? + + + +![image-20210829190835070](http://blog-img.coolsen.cn/img/image-20210829190835070.png) + + + +- Provider:暴露服务的服务提供方 +- Consumer:调用远程服务消费方 +- Registry:服务注册与发现注册中心 +- Monitor:监控中心和访问调用统计 +- Container:服务运行容器 + +## 4. Dubbo都支持什么协议,推荐用哪种? + +1、 Dubbo协议:Dubbo默认使用Dubbo协议。 + +* 适合大并发小数据量的服务调用,以及服务消费者远大于提供者的情况 +* Hessian二进制序列化。 +* 缺点是不适合传送大数据包的服务。 + +2、rmi协议:采用JDK标准的rmi协议实现,传输参数和返回参数对象需要实现Serializable接口。使用java标准序列化机制,使用阻塞式短连接,传输数据包不限,消费者和提供者个数相当。 + +* 多个短连接,TCP协议传输,同步传输,适用常规的远程服务调用和rmi互操作 +* 缺点:在依赖低版本的Common-Collections包,java反序列化存在安全漏洞,需升级commons-collections3 到3.2.2版本或commons-collections4到4.1版本。 + +3、 webservice协议:基于WebService的远程调用协议(Apache CXF的frontend-simple和transports-http)实现,提供和原生WebService的互操作多个短连接,基于HTTP传输,同步传输,适用系统集成和跨语言调用。 + +4、http协议:基于Http表单提交的远程调用协议,使用Spring的HttpInvoke实现。对传输数据包不限,传入参数大小混合,提供者个数多于消费者 + +* 缺点是不支持传文件,只适用于同时给应用程序和浏览器JS调用 + +5、hessian:集成Hessian服务,基于底层Http通讯,采用Servlet暴露服务,Dubbo内嵌Jetty作为服务器实现,可与Hession服务互操作 +通讯效率高于WebService和Java自带的序列化 + +* 适用于传输大数据包(可传文件),提供者比消费者个数多,提供者压力较大 + +* 缺点是参数及返回值需实现Serializable接口,自定义实现List、Map、Number、Date、Calendar等接口 + +6、thrift协议:对thrift原生协议的扩展添加了额外的头信息。使用较少,不支持传null值 + +7、memcache:基于memcached实现的RPC协议 + +8、redis:基于redis实现的RPC协议 + +## 5. Dubbo服务器注册与发现的流程? + +- 服务容器Container负责启动,加载,运行服务提供者。 +- 服务提供者Provider在启动时,向注册中心注册自己提供的服务。 +- 服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。 +- 注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +- 服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +- 服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。 + +## 6. Dubbo内置了哪几种服务容器? + +三种服务容器: + +* Spring Container +* Jetty Container +* Log4j Container + +Dubbo的服务容器只是一个简单的 Main 方法,并加载一个简单的 Spring 容器,用于暴露服务。 + +## 7. Dubbo负载均衡的作用? + + 将负载均衡功能实现在rpc客户端侧,以便能够随时适应外部的环境变化,更好地发挥硬件作用。而且客户端的负载均衡天然地就避免了单点问题。定制化的自有定制化的优势和劣势。 + +它可以从配置文件中指定,也可以在管理后台进行配置修改。 + +事实上,它支持 服务端服务/方法级别、客户端服务/方法级别 的负载均衡配置。 + +## 8. Dubbo有哪几种负载均衡策略,默认是哪种? + +Dubbo提供了4种负载均衡实现: + +1. RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。 +2. RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。 +3. LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。 +4. ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。 + +## 9. Dubbo服务之间的调用是阻塞的吗? + +默认是同步等待结果阻塞的,支持异步调用。 + +Dubbo是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。 + +## 10. DubboMonitor 实现原理? + +Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。 + +1. MonitorFilter 向 DubboMonitor 发送数据 +2. DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference +3. SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000) +4. SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写) +5. SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表 + +## 11. Dubbo有哪些注册中心? + +- Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。 +- Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。 +- Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。 +- Simple 注册中心。 +- 推荐使用 Zookeeper 作为注册中心 + +## 12. Dubbo的集群容错方案有哪些? + +- Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。 +- Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +- Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +- Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +- Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。 +- Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +## 13. Dubbo超时设置有哪些方式? + +Dubbo超时设置有两种方式: + +- 服务提供者端设置超时时间,在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。 +- 服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。 + +## 14. Dubbo用到哪些设计模式? + +1、**工厂模式** + +Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +``` +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 +``` + +Dubbo里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。 + +2、**装饰器模式** + +Dubbo在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是: + +``` +EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> +ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter -> +ExceptionFilter +复制代码 +``` + +更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter 则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。 + +3、**观察者模式** + +Dubbo的 Provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息,provider 接受到 notify 消息后,运行 NotifyListener 的 notify 方法,执行监听器方法。 + +4、**动态代理模式** + +Dubbo扩展 JDK SPI 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。Dubbo需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的 createAdaptiveExtensionClassCode 方法。代理类主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。 + + From b2cbe4b05fb367a6fae6f1a4ca429146eeac5168 Mon Sep 17 00:00:00 2001 From: coolsenma Date: Mon, 30 Aug 2021 01:11:32 +0800 Subject: [PATCH 08/26] update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index be16ca4..31f9d9b 100644 --- a/README.md +++ b/README.md @@ -83,5 +83,11 @@ ## 操作系统 * [操作系统](https://github.com/cosen1024/Java-Interview/blob/main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md) +## MQ +[Kafka](https://github.com/cosen1024/Java-Interview/blob/main/MQ/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98.md) +[RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) +## Dubbo +[Dubbo]https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md + ## 关于我 [库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) \ No newline at end of file From 38b9559fd84fa0a435a71c903d13cda776061895 Mon Sep 17 00:00:00 2001 From: fenghaojiang <457420285@qq.com> Date: Tue, 31 Aug 2021 23:42:47 +0800 Subject: [PATCH 09/26] Update Redis.md --- Redis/Redis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Redis/Redis.md b/Redis/Redis.md index 9a2986a..3ced78a 100644 --- a/Redis/Redis.md +++ b/Redis/Redis.md @@ -76,7 +76,7 @@ * Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里; * Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象; * Redis 缓存有过期机制,Map 本身无此功能;Redis 有丰富的 API,Map 就简单太多了; -* Redis可单独部署,多个项目之间可以空想,本地内存无法共享; +* Redis可单独部署,多个项目之间可以共享,本地内存无法共享; * Redis有专门的管理工具可以查看缓存数据。 ## 6. Redis的常用场景有哪些? From b1d739e715ec719a8a897fbad5c0362c8474e445 Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Wed, 1 Sep 2021 08:45:50 +0800 Subject: [PATCH 10/26] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31f9d9b..d6fe520 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,9 @@ ## MQ [Kafka](https://github.com/cosen1024/Java-Interview/blob/main/MQ/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98.md) [RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) + ## Dubbo -[Dubbo]https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md +[Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## 关于我 -[库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) \ No newline at end of file +[库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) From 5d3d67d69f49a3df79af7f43926ae86881d0a9d9 Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Wed, 1 Sep 2021 08:46:41 +0800 Subject: [PATCH 11/26] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d6fe520..befa762 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,12 @@ * [操作系统](https://github.com/cosen1024/Java-Interview/blob/main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md) ## MQ -[Kafka](https://github.com/cosen1024/Java-Interview/blob/main/MQ/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98.md) -[RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) +* [Kafka](https://github.com/cosen1024/Java-Interview/blob/main/MQ/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98.md) + +* [RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## Dubbo -[Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) +* [Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## 关于我 [库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) From 0c47e28a87d9eba44e0661e7da1f6fa57945c510 Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Wed, 1 Sep 2021 08:47:34 +0800 Subject: [PATCH 12/26] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index befa762..9a5beb6 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ * [RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## Dubbo -* [Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) +* Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) + ## 关于我 [库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) From 628fa3f29407404458efc904e25312b1e28f8328 Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Sat, 4 Sep 2021 15:21:55 +0800 Subject: [PATCH 13/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c83df71..31f7fe7 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ * [RabbitMQ、RocketMQ](https://github.com/cosen1024/Java-Interview/blob/main/MQ/MQ%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## Dubbo -* Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) +* [Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## 关于我 From c69536508e15878ae5eae3bf1ff63f39ff70cbe1 Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:57:58 +0800 Subject: [PATCH 14/26] Create Netty.md add netty --- Netty.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Netty.md diff --git a/Netty.md b/Netty.md new file mode 100644 index 0000000..7995495 --- /dev/null +++ b/Netty.md @@ -0,0 +1,31 @@ +1.Netty 是什么? +Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的,它封装了jdk的nio,让我们使用起来更加方法灵活。 + +2.Netty 的特点是什么? +高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。 +传输快:Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。 +封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。 +3.Netty 的优势有哪些? +使用简单:封装了 NIO 的很多细节,使用更简单。 +功能强大:预置了多种编解码功能,支持多种主流协议。 +定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。 +性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。 +稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身。 +社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。 + +4. Netty 的高性能表现在哪些方面? +心跳,对服务端:会定时清除闲置会话 inactive(netty5),对客户端:用来检测会话是否断开,是否重来,检测网络延迟,其中 idleStateHandler 类 用来检测会话状态 + +串行无锁化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。 + +可靠性,链路有效性检测:链路空闲检测机制,读/写空闲超时机制;内存保护机制:通过内存池重用 ByteBuf;ByteBuf 的解码保护;优雅停机:不再接收新消息、退出前的预处理操作、资源的释放操作。 + +Netty 安全性:支持的安全协议:SSL V2 和 V3,TLS,SSL 单向认证、双向认证和第三方 CA证。 + +高效并发编程的体现:volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。IO 通信性能三原则:传输(AIO)、协议(Http)、线程(主从多线程) + +流量整型的作用(变压器):防止由于上下游网元性能不均衡导致下游网元被压垮,业务流中断;防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。 + +TCP 参数配置:SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K; + +SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法; From 1b3417ee74f6c1731599eabc3803488090faa8fd Mon Sep 17 00:00:00 2001 From: cosen Date: Wed, 2 Feb 2022 19:37:47 +0800 Subject: [PATCH 15/26] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 31f7fe7..0754c66 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Java-Interview -「Java面试小抄」一份通向理想互联网公司的面试指南,包括 Java基础、集合、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统、计算机网络、系统设计、分布式、Java 项目实战等 +「Java面试小抄」一份通向理想互联网公司的面试指南,包括 Java基础、集合、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统、计算机网络、系统设计、分布式、Java 项目实战等。 From 319875307076f28683fb279c5f95004faa3e133a Mon Sep 17 00:00:00 2001 From: cosen Date: Mon, 7 Feb 2022 14:21:23 +0800 Subject: [PATCH 16/26] update redis --- Redis/Redis.md | 4 ++-- ...3\215\344\275\234\347\263\273\347\273\237.md" | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Redis/Redis.md b/Redis/Redis.md index 3ced78a..7ada9b1 100644 --- a/Redis/Redis.md +++ b/Redis/Redis.md @@ -113,7 +113,7 @@ Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键 消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。 -## 8. Redis的数据类型有哪些? +## 7. Redis的数据类型有哪些? 有五种常用数据类型:String、Hash、Set、List、SortedSet。以及三种特殊的数据类型:Bitmap、HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。 @@ -143,7 +143,7 @@ Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键 # 持久化 -## 6. Redis持久化机制? +## 8. Redis持久化机制? 为了能够重用Redis数据,或者防止系统故障,我们需要将Redis中的数据写入到磁盘空间中,即持久化。 diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237.md" index d3395bf..40a79d7 100644 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237.md" +++ "b/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237.md" @@ -1,11 +1,23 @@ -* 一次调用时的状态。 +## 1. 进程和线程的区别? + +* 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。 +* 切换:线程上下文切换比进程上下文切换要快得多。 +* 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。 +* 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。 + +## 2. 协程与线程的区别? + +* 线程和进程都是同步机制,而协程是异步机制。 +* 线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。 +* 一个线程可以有多个协程,一个进程也可以有多个协程。 +* 协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。 +* 协程能保留上一次调用时的状态。 ## 3. 并发和并行有什么区别? 并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器可以做到并发。比如有两个进程`A`和`B`,`A`运行一个时间片之后,切换到`B`,`B`运行一个时间片之后又切换到`A`。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。 并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。 - ## 4. 进程与线程的切换流程? 进程切换分两步: From 0e23366cfcd373065e1cda7d3a5d108701b6cb8d Mon Sep 17 00:00:00 2001 From: cosen Date: Mon, 7 Feb 2022 14:23:12 +0800 Subject: [PATCH 17/26] update jvm --- JVM/JVM.md | 407 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 398 insertions(+), 9 deletions(-) diff --git a/JVM/JVM.md b/JVM/JVM.md index ddccaf8..e6ef355 100644 --- a/JVM/JVM.md +++ b/JVM/JVM.md @@ -1,3 +1,4 @@ +# JVM 常考面试题 ## 1. 什么是JVM内存结构? ![](http://blog-img.coolsen.cn/img/image-20210220111553294.png) @@ -367,7 +368,7 @@ Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8 - JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。 -## 21.说一下 JVM 调优的命令? +## 21. 说一下 JVM 调优的命令? * jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 * jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 @@ -375,21 +376,409 @@ Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。 * jhat:jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。 * jstack:jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。 -## Java对象创建过程 +## 22. Java对象创建过程 1. JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲) 2. 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)” 3. 将除对象头外的对象内存空间初始化为0 4. 对对象头进行必要设置 -## 巨人的肩膀 +## 23. JDK新特性 -https://jishuin.proginn.com/p/763bfbd35094 +**JDK8** -https://www.javanav.com/val/93550f179edb4a77bbf4d35faa6d560c.html +支持 Lamda 表达式、集合的 stream 操作、提升HashMap性能 -https://juejin.cn/post/6844903941805703181 +**JDK9** -https://www.cnblogs.com/chiangchou/p/jvm-2.html +```java +//Stream API中iterate方法的新重载方法,可以指定什么时候结束迭代 +IntStream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println); +``` -https://juejin.cn/post/6844903887866953735 +默认G1垃圾回收器 + +**JDK10** + +其重点在于通过完全GC并行来改善G1最坏情况的等待时间。 + +**JDK11** + +ZGC (并发回收的策略) 4TB + +用于 Lambda 参数的局部变量语法 + +**JDK12** + +Shenandoah GC (GC 算法)停顿时间和堆的大小没有任何关系,并行关注停顿响应时间。 + +**JDK13** + +增加ZGC以将未使用的堆内存返回给操作系统,16TB + +**JDK14** + +删除cms垃圾回收器、弃用ParallelScavenge+SerialOldGC垃圾回收算法组合 + +将ZGC垃圾回收器应用到macOS和windows平台 + + + + + +# 线上故障排查 + +## 1、硬件故障排查 + +如果一个实例发生了问题,根据情况选择,要不要着急去重启。如果出现的CPU、内存飙高或者日志里出现了OOM异常 + +**第一步是隔离**,第二步是**保留现场**,第三步才是**问题排查**。 + +**隔离** + +就是把你的这台机器从请求列表里摘除,比如把 nginx 相关的权重设成零。 + +**现场保留** + +**瞬时态和历史态** + +![img](https://tva1.sinaimg.cn/large/008eGmZEly1gobnwy22d2j30l10cpt9d.jpg) + +查看比如 CPU、系统内存等,通过历史状态可以体现一个趋势性问题,而这些信息的获取一般依靠监控系统的协作。 + +**保留信息** + +(1)**系统当前网络连接** + +``` +ss -antp > $DUMP_DIR/ss.dump 2>&1 +``` + + +使用 ss 命令而不是 netstat 的原因,是因为 netstat 在网络连接非常多的情况下,执行非常缓慢。 + +后续的处理,可通过查看各种网络连接状态的梳理,来排查 TIME_WAIT 或者 CLOSE_WAIT,或者其他连接过高的问题,非常有用。 + +(2)**网络状态统计** + +```java +netstat -s > $DUMP_DIR/netstat-s.dump 2>&1 +``` + + +它能够按照各个协议进行统计输出,对把握当时整个网络状态,有非常大的作用。 + +```java +sar -n DEV 1 2 > $DUMP_DIR/sar-traffic.dump 2>&1 +``` + + +在一些速度非常高的模块上,比如 Redis、Kafka,就经常发生跑满网卡的情况。表现形式就是网络通信非常缓慢。 + +(3)**进程资源** + +```java +lsof -p $PID > $DUMP_DIR/lsof-$PID.dump +``` + + +通过查看进程,能看到打开了哪些文件,可以以进程的维度来查看整个资源的使用情况,包括每条网络连接、每个打开的文件句柄。同时,也可以很容易的看到连接到了哪些服务器、使用了哪些资源。这个命令在资源非常多的情况下,输出稍慢,请耐心等待。 + +(4)**CPU 资源** + +``` +mpstat > $DUMP_DIR/mpstat.dump 2>&1 +vmstat 1 3 > $DUMP_DIR/vmstat.dump 2>&1 +sar -p ALL > $DUMP_DIR/sar-cpu.dump 2>&1 +uptime > $DUMP_DIR/uptime.dump 2>&1 +``` + +主要用于输出当前系统的 CPU 和负载,便于事后排查。 + +(5)**I/O 资源** + +```java +iostat -x > $DUMP_DIR/iostat.dump 2>&1 +``` + + +一般,以计算为主的服务节点,I/O 资源会比较正常,但有时也会发生问题,比如**日志输出过多,或者磁盘问题**等。此命令可以输出每块磁盘的基本性能信息,用来排查 I/O 问题。在第 8 课时介绍的 GC 日志分磁盘问题,就可以使用这个命令去发现。 + +(6)**内存问题** + +```java +free -h > $DUMP_DIR/free.dump 2>&1 +``` + + +free 命令能够大体展现操作系统的内存概况,这是故障排查中一个非常重要的点,比如 SWAP 影响了 GC,SLAB 区挤占了 JVM 的内存。 + +(7)**其他全局** + +```java +ps -ef > $DUMP_DIR/ps.dump 2>&1 +dmesg > $DUMP_DIR/dmesg.dump 2>&1 +sysctl -a > $DUMP_DIR/sysctl.dump 2>&1 +``` + + +dmesg 是许多静悄悄死掉的服务留下的最后一点线索。当然,ps 作为执行频率最高的一个命令,由于内核的配置参数,会对系统和 JVM 产生影响,所以我们也输出了一份。 + +(8)**进程快照**,最后的遗言(jinfo) + +```java +${JDK_BIN}jinfo $PID > $DUMP_DIR/jinfo.dump 2>&1 +``` + + +此命令将输出 Java 的基本进程信息,包括**环境变量和参数配置**,可以查看是否因为一些错误的配置造成了 JVM 问题。 + +**(9)dump 堆信息** + +```java +${JDK_BIN}jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil.dump 2>&1 +${JDK_BIN}jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity.dump 2>&1 +``` + + +jstat 将输出当前的 gc 信息。一般,基本能大体看出一个端倪,如果不能,可将借助 jmap 来进行分析。 + +**(10)堆信息** + +```java +${JDK_BIN}jmap $PID > $DUMP_DIR/jmap.dump 2>&1 +${JDK_BIN}jmap -heap $PID > $DUMP_DIR/jmap-heap.dump 2>&1 +${JDK_BIN}jmap -histo $PID > $DUMP_DIR/jmap-histo.dump 2>&1 +${JDK_BIN}jmap -dump:format=b,file=$DUMP_DIR/heap.bin $PID > /dev/null 2>&1 +``` + + +jmap 将会得到当前 Java 进程的 dump 信息。如上所示,其实最有用的就是第 4 个命令,但是前面三个能够让你初步对系统概况进行大体判断。因为,第 4 个命令产生的文件,一般都非常的大。而且,需要下载下来,导入 MAT 这样的工具进行深入分析,才能获取结果。这是分析内存泄漏一个必经的过程。 + +**(11)JVM 执行栈** + +```java +${JDK_BIN}jstack $PID > $DUMP_DIR/jstack.dump 2>&1 +``` + + +jstack 将会获取当时的执行栈。一般会多次取值,我们这里取一次即可。这些信息非常有用,能够还原 Java 进程中的线程情况。 + +```java +top -Hp $PID -b -n 1 -c > $DUMP_DIR/top-$PID.dump 2>&1 +``` + + +为了能够得到更加精细的信息,我们使用 top 命令,来获取进程中所有线程的 CPU 信息,这样,就可以看到资源到底耗费在什么地方了。 + +**(12)高级替补** + +```java +kill -3 $PID +``` + + +有时候,jstack 并不能够运行,有很多原因,比如 Java 进程几乎不响应了等之类的情况。我们会尝试向进程发送 kill -3 信号,这个信号将会打印 jstack 的 trace 信息到日志文件中,是 jstack 的一个替补方案。 + +```java +gcore -o $DUMP_DIR/core $PID +``` + + +对于 jmap 无法执行的问题,也有替补,那就是 GDB 组件中的 gcore,将会生成一个 core 文件。我们可以使用如下的命令去生成 dump: + +```java +${JDK_BIN}jhsdb jmap --exe ${JDK}java --core $DUMP_DIR/core --binaryheap +``` + +3. **内存泄漏的现象** + +稍微提一下 jmap 命令,它在 9 版本里被干掉了,取而代之的是 jhsdb,你可以像下面的命令一样使用。 + +```java +jhsdb jmap --heap --pid 37340 +jhsdb jmap --pid 37288 +jhsdb jmap --histo --pid 37340 +jhsdb jmap --binaryheap --pid 37340 +``` + +一般内存溢出,表现形式就是 Old 区的占用持续上升,即使经过了多轮 GC 也没有明显改善。比如ThreadLocal里面的GC Roots,内存泄漏的根本就是,这些对象并没有切断和 GC Roots 的关系,可通过一些工具,能够看到它们的联系。 + + + +## 2、报表异常 | JVM调优 + +有一个报表系统,频繁发生内存溢出,在高峰期间使用时,还会频繁的发生拒绝服务,由于大多数使用者是管理员角色,所以很快就反馈到研发这里。 + +业务场景是由于有些结果集的字段不是太全,因此需要对结果集合进行循环,并通过 HttpClient 调用其他服务的接口进行数据填充。使用 Guava 做了 JVM 内缓存,但是响应时间依然很长。 + +初步排查,JVM 的资源太少。接口 A 每次进行报表计算时,都要涉及几百兆的内存,而且在内存里驻留很长时间,有些计算又非常耗 CPU,特别的“吃”资源。而我们分配给 JVM 的内存只有 3 GB,在多人访问这些接口的时候,内存就不够用了,进而发生了 OOM。在这种情况下,没办法,只有升级机器。把机器配置升级到 4C8G,给 JVM 分配 6GB 的内存,这样 OOM 问题就消失了。但随之而来的是频繁的 GC 问题和超长的 GC 时间,平均 GC 时间竟然有 5 秒多。 + +进一步,由于报表系统和高并发系统不太一样,它的对象,存活时长大得多,并不能仅仅通过增加年轻代来解决;而且,如果增加了年轻代,那么必然减少了老年代的大小,由于 CMS 的碎片和浮动垃圾问题,我们可用的空间就更少了。虽然服务能够满足目前的需求,但还有一些不太确定的风险。 + +第一,了解到程序中有很多缓存数据和静态统计数据,为了减少 MinorGC 的次数,通过分析 GC 日志打印的对象年龄分布,把 MaxTenuringThreshold 参数调整到了 3(特殊场景特殊的配置)。这个参数是让年轻代的这些对象,赶紧回到老年代去,不要老呆在年轻代里。 + +第二,我们的 GC 时间比较长,就一块开了参数 CMSScavengeBeforeRemark,使得在 CMS remark 前,先执行一次 Minor GC 将新生代清掉。同时配合上个参数,其效果还是比较好的,一方面,对象很快晋升到了老年代,另一方面,年轻代的对象在这种情况下是有限的,在整个 MajorGC 中占的时间也有限。 + +第三,由于缓存的使用,有大量的弱引用,拿一次长达 10 秒的 GC 来说。我们发现在 GC 日志里,处理 weak refs 的时间较长,达到了 4.5 秒。这里可以加入参数 ParallelRefProcEnabled 来并行处理Reference,以加快处理速度,缩短耗时。 + +优化之后,效果不错,但并不是特别明显。经过评估,针对高峰时期的情况进行调研,我们决定再次提升机器性能,改用 8core16g 的机器。但是,这带来另外一个问题。 + +**高性能的机器带来了非常大的服务吞吐量**,通过 jstat 进行监控,能够看到年轻代的分配速率明显提高,但随之而来的 MinorGC 时长却变的不可控,有时候会超过 1 秒。累积的请求造成了更加严重的后果。 + +这是由于堆空间明显加大造成的回收时间加长。为了获取较小的停顿时间,我们在堆上**改用了 G1 垃圾回收器**,把它的目标设定在 200ms。G1 是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的 GC 暂停目标,就能得到不错的性能。修改之后,虽然 GC 更加频繁了一些,但是停顿时间都比较小,应用的运行较为平滑。 + +到目前为止,也只是勉强顶住了已有的业务,但是,这时候领导层面又发力,**要求报表系统可以支持未来两年业务10到100倍的增长**,并保持其可用性,但是这个“千疮百孔”的报表系统,稍微一压测,就宕机,那如何应对十倍百倍的压力呢 ? 硬件即使可以做到动态扩容,但是毕竟也有极限。 + +使用 MAT 分析堆快照,发现很多地方可以通过代码优化,那些占用内存特别多的对象: + +1、select * 全量排查,只允许获取必须的数据 + +2、报表系统中cache实际的命中率并不高,将Guava 的 Cache 引用级别改成弱引用(WeakKeys) + +3、限制报表导入文件大小,同时拆分用户超大范围查询导出请求。 + +每一步操作都使得JVM使用变得更加可用,一系列优化以后,机器相同压测数据性能提升了数倍。 + + + +## 3、大屏异常 | JUC调优 + +有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长,也有可能会造成服务整体的阻塞。 + +![img](https://tva1.sinaimg.cn/large/008eGmZEly1gobr4whjzwj30l1058dfx.jpg) + +接口 A 通过 HttpClient 访问服务 2,响应 100ms 后返回;接口 B 访问服务 3,耗时 2 秒。HttpClient 本身是有一个最大连接数限制的,如果服务 3 迟迟不返回,就会造成 HttpClient 的连接数达到上限,**概括来讲,就是同一服务,由于一个耗时非常长的接口,进而引起了整体的服务不可用** + +这个时候,通过 jstack 打印栈信息,会发现大多数竟然阻塞在了接口 A 上,而不是耗时更长的接口 B,这个现象起初十分具有迷惑性,不过经过分析后,我们猜想其实是因为接口 A 的速度比较快,在问题发生点进入了更多的请求,它们全部都阻塞住的同时被打印出来了。 + +为了验证这个问题,我搭建了一个demo 工程,模拟了两个使用同一个 HttpClient 的接口。fast 接口用来访问百度,很快就能返回;slow 接口访问谷歌,由于众所周知的原因,会阻塞直到超时,大约 10 s。 利用ab对两个接口进行压测,同时使用 jstack 工具 dump 堆栈。首先使用 jps 命令找到进程号,然后把结果重定向到文件(可以参考 10271.jstack 文件)。 + +过滤一下 nio 关键字,可以查看 tomcat 相关的线程,足足有 200 个,这和 Spring Boot 默认的 maxThreads 个数不谋而合。更要命的是,有大多数线程,都处于 BLOCKED 状态,说明线程等待资源超时。通过grep fast | wc -l 分析,确实200个中有150个都是blocked的fast的进程。 + +问题找到了,解决方式就顺利成章了。 + +1、fast和slow争抢连接资源,通过线程池限流或者熔断处理 + +2、有时候slow的线程也不是一直slow,所以就得加入监控 + +3、使用带countdownLaunch对线程的执行顺序逻辑进行控制 + + + + + + + + + +## **4、接口延迟 | SWAP调优** + +有一个关于服务的某个实例,经常发生服务卡顿。由于服务的并发量是比较高的,每多停顿 1 秒钟,几万用户的请求就会感到延迟。 + +我们统计、类比了此服务其他实例的 CPU、内存、网络、I/O 资源,区别并不是很大,所以一度怀疑是机器硬件的问题。 + +接下来我们对比了节点的 GC 日志,发现无论是 Minor GC,还是 Major GC,这个节点所花费的时间,都比其他实例长得多。 + +通过仔细观察,我们发现在 GC 发生的时候,vmstat 的 si、so 飙升的非常严重,这和其他实例有着明显的不同。 + +使用 free 命令再次确认,发现 SWAP 分区,使用的比例非常高,引起的具体原因是什么呢? + +更详细的操作系统内存分布,从 /proc/meminfo 文件中可以看到具体的逻辑内存块大小,有多达 40 项的内存信息,这些信息都可以通过遍历 /proc 目录的一些文件获取。我们注意到 slabtop 命令显示的有一些异常,dentry(目录高速缓冲)占用非常高。 + +问题最终定位到是由于某个运维工程师删除日志时,定时执行了一句命令: + +find / | grep "xxx.log" + + +他是想找一个叫做 要被删除 的日志文件,看看在哪台服务器上,结果,这些老服务器由于文件太多,扫描后这些文件信息都缓存到了 slab 区上。而服务器开了 swap,操作系统发现物理内存占满后,并没有立即释放 cache,导致每次 GC 都要和硬盘打一次交道。 + + + +**解决方式就是关闭 SWAP 分区。** + + + +swap 是很多性能场景的万恶之源,建议禁用。在高并发 SWAP 绝对能让你体验到它魔鬼性的一面:进程倒是死不了了,但 GC 时间长的却让人无法忍受。 + +## 5、**内存溢出 | Cache调优** + +> 有一次线上遇到故障,重新启动后,使用 jstat 命令,发现 Old 区一直在增长。我使用 jmap 命令,导出了一份线上堆栈,然后使用 MAT 进行分析,通过对 GC Roots 的分析,发现了一个非常大的 HashMap 对象,这个原本是其他同事做缓存用的,但是做了一个无界缓存,没有设置超时时间或者 LRU 策略,在使用上又没有重写key类对象的hashcode和equals方法,对象无法取出也直接造成了堆内存占用一直上升,后来,将这个缓存改成 guava 的 Cache,并设置了弱引用,故障就消失了。 +> +> 关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,**close 方法又没有放在 finally** 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。 + +内存溢出是一个结果,而**内存泄漏**是一个原因。内存溢出的原因有**内存空间不足、配置错误**等因素。一些错误的编程方式,不再被使用的对象、没有被回收、没有及时切断与 GC Roots 的联系,这就是内存泄漏。 + +举个例子,有团队使用了 HashMap 做缓存,但是并没有设置超时时间或者 LRU 策略,造成了放入 Map 对象的数据越来越多,而产生了内存泄漏。 + +再来看一个经常发生的内存泄漏的例子,也是由于 HashMap 产生的。代码如下,由于没有重写 Key 类的 hashCode 和 equals 方法,造成了放入 HashMap 的所有对象都无法被取出来,它们和外界失联了。所以下面的代码结果是 null。 + +```java +//leak example +import java.util.HashMap; +import java.util.Map; +public class HashMapLeakDemo { + public static class Key { + String title; + public Key(String title) { + this.title = title; + } +} + +public static void main(String[] args) { + Map map = new HashMap<>(); + map.put(new Key("1"), 1); + map.put(new Key("2"), 2); + map.put(new Key("3"), 2); + Integer integer = map.get(new Key("2")); + System.out.println(integer); + } +} +``` + + +即使提供了 equals 方法和 hashCode 方法,也要非常小心,尽量避免使用自定义的对象作为 Key。 + +再看一个例子,关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,**close 方法又没有放在 finally** 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。 + +## 6、CPU飙高 | 死循环 + +我们有个线上应用,单节点在运行一段时间后,CPU 的使用会飙升,一旦飙升,一般怀疑某个业务逻辑的计算量太大,或者是触发了死循环(比如著名的 HashMap 高并发引起的死循环),但排查到最后其实是 GC 的问题。 + +(1)使用 top 命令,查找到使用 CPU 最多的某个进程,记录它的 pid。使用 Shift + P 快捷键可以按 CPU 的使用率进行排序。 + +```java +top +``` + + +(2)再次使用 top 命令,加 -H 参数,查看某个进程中使用 CPU 最多的某个线程,记录线程的 ID。 + +```java +top -Hp $pid +``` + + +(3)使用 printf 函数,将十进制的 tid 转化成十六进制。 + +```java +printf %x $tid +``` + + +(4)使用 jstack 命令,查看 Java 进程的线程栈。 + +```java +jstack $pid >$pid.log +``` + + +(5)使用 less 命令查看生成的文件,并查找刚才转化的十六进制 tid,找到发生问题的线程上下文。 + +```java +less $pid.log +``` + + +我们在 jstack 日志搜关键字DEAD,以及中找到了 CPU 使用最多的几个线程id。 + +可以看到问题发生的根源,是我们的堆已经满了,但是又没有发生 OOM,于是 GC 进程就一直在那里回收,回收的效果又非常一般,造成 CPU 升高应用假死。接下来的具体问题排查,就需要把内存 dump 一份下来,使用 MAT 等工具分析具体原因了。 -https://segmentfault.com/a/1190000023182342 \ No newline at end of file From 721398e5b2e0b46988197f78320f2070a3006c98 Mon Sep 17 00:00:00 2001 From: cosen Date: Mon, 7 Feb 2022 16:46:43 +0800 Subject: [PATCH 18/26] =?UTF-8?q?update=20=E5=88=86=E5=B8=83=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../\351\235\242\350\257\225\351\242\230.md" | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 "\345\210\206\345\270\203\345\274\217/\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index 0754c66..871025e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ ## Dubbo * [Dubbo](https://github.com/cosen1024/Java-Interview/blob/main/Dubbo/Dubbo%E9%9D%A2%E8%AF%95%E9%A2%98.md) +## 分布式 +* [分布式](/分布式/面试题.md) ## 关于我 -[库森的校招经历](http://mp.weixin.qq.com/s?__biz=Mzg4MjUxMTI4NA==&mid=2247483796&idx=1&sn=bdc95819d442ac946b6e49dee6ee36de&chksm=cf54dd4ff82354594280041ce65f639a8ed3361df03da95bed743474a6075dfa973f35acc029&token=2080698617&lang=zh_CN#rd) +[库森的校招经历](https://mp.weixin.qq.com/s/Q1uNpj_6mGscy5JbMqMY-Q) diff --git "a/\345\210\206\345\270\203\345\274\217/\351\235\242\350\257\225\351\242\230.md" "b/\345\210\206\345\270\203\345\274\217/\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..fb4e9cc --- /dev/null +++ "b/\345\210\206\345\270\203\345\274\217/\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,53 @@ +## 1.解释一下什么是CAP? +* Consistency:一致性就是在客户端任何时候看到各节点的数据都是一致的。 +* Availability:可用性就是在任何时刻都可以提供读写。 +* Partition Tolerance:分区容错性是在网络故障、某些节点不能通信的时候系统仍能继续工作。 +具体地讲在分布式系统中,在任何数据库设计中,一个Web应用最多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。 +![img](http://blog-img.coolsen.cn/img/801753-20151107213219867-1667011131.png) + + +AP(高可用&&分区容错): + +允许至少一个节点更新状态会导致数据不一致,即丧失了C性质(一致性)。会导致全局的数据不一致。 + +CP(一致&&分区容错): + +为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质(可用性)。分区同步会导致同步时间无限延长(也就是等数据同步完成之后才能正常访问) + +CA(一致&&高可用): + +两个节点可以互相通信,才能既保证C(一致性)又保证A(可用性),这又会导致丧失P性质(分区容错性)。这样的话就分布式节点受阻,无法部署子节点,放弃了分布式系统的可扩展性。因为分布式系统与单机系统不同,它涉及到多节点间的通讯和交互,节点间的分区故障是必然发生的,所以在分布式系统中分区容错性是必须要考虑的。 +## 2.什么分布式事务? + +分布式事务服务(Distributed Transaction Service,DTS)是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。 + +CAP理论告诉我们在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的,所以我们只能在一致性和可用性之间进行权衡。 + +为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。 + +## 3.了解BASE理论吗? +BASE理论指的是: + +* Basically Available(基本可用) +* Soft state(软状态) +* Eventually consistent(最终一致性) +BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,是对互联网大规模分布式系统的实践总结,强调可用性。 + +理论的核心思想就是:基本可用(Basically Available)和最终一致性(Eventually consistent)。虽然无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。 + +## 4.实现分布式事务一致性(Consistency)的方法有哪些? +最著名的就是二阶段提交协议、三阶段提交协议和Paxos算法。 + +**两阶段提交协议** + +* prepare(准备阶段) + +当开始事务调用的时候,事务处理器向事务执行者(有可能是数据库本身支持)发出命令,事务执行者进行prepare操作。 +当所有事务执行者都完成了prepare操作,就进行下一步行为。 +如果有一个事务执行者在执行prepare的时候失败了,那么通知事务处理器,事务处理器再通知所有的事务执行者执行回滚操作。 +* commit(提交阶段) + +当所有事务执行者都prepare成功以后,事务处理器会再次发送commit请求给事务执行者,所有事务执行者进行commit处理。 +当所有commit处理都成功了,那么事务执行结束。 +如果有一个事务执行者的commit处理不成功,这个时候就要通知事务处理器,事务处理器通知所有的事务执行者执行回滚(abort)操作。 +但是两阶段提交的诟病就是在于性能问题。比如由于执行链比较长,锁定资源的时间也变长了。所以在高性能的系统中都会避免使用二阶段提交。 \ No newline at end of file From 65822202a50586561d892b8ae5a1a1ecf2924456 Mon Sep 17 00:00:00 2001 From: cosen Date: Mon, 7 Feb 2022 16:48:05 +0800 Subject: [PATCH 19/26] update Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 871025e..ac105ce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Java-Interview 「Java面试小抄」一份通向理想互联网公司的面试指南,包括 Java基础、集合、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统、计算机网络、系统设计、分布式、Java 项目实战等。 - +> 在线阅读:https://www.javalearn.cn/


From de6d7e753496d5328f4d8e1b23863763df7fa760 Mon Sep 17 00:00:00 2001 From: cosen Date: Tue, 21 Mar 2023 22:06:02 +0800 Subject: [PATCH 20/26] update readme: --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac105ce..e05e690 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,10 @@

+
+
+ + ## 更多 @@ -26,7 +29,7 @@ 请微信扫描或搜索下方个人公众号『**程序员库森**』后,回复关键字『**pdf**』,即可下载该面试小抄的**最新 PDF 版本**。 -
+

个人公众号

From c87b1aa7aa57abfdccf612198792fd8350338ad1 Mon Sep 17 00:00:00 2001 From: cosen Date: Tue, 21 Mar 2023 22:10:18 +0800 Subject: [PATCH 21/26] update readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e05e690..8f318c5 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ @@ -29,7 +30,7 @@ 请微信扫描或搜索下方个人公众号『**程序员库森**』后,回复关键字『**pdf**』,即可下载该面试小抄的**最新 PDF 版本**。 -
+

个人公众号

From 0a11bc2f294adf17aabca77fad0a3e986ed92d8d Mon Sep 17 00:00:00 2001 From: cosen Date: Tue, 21 Mar 2023 22:11:20 +0800 Subject: [PATCH 22/26] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f318c5..0e35a88 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@

From f59a43c0a989f60f59db6eb65a26a96a8a64d535 Mon Sep 17 00:00:00 2001 From: cosen Date: Tue, 21 Mar 2023 22:12:09 +0800 Subject: [PATCH 23/26] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e35a88..10dbdc3 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ From f3aa7084ebb8cb8de749d402e0e00522bba0b5c0 Mon Sep 17 00:00:00 2001 From: cosen Date: Tue, 21 Mar 2023 22:12:57 +0800 Subject: [PATCH 24/26] update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10dbdc3..c243c78 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ > 在线阅读:https://www.javalearn.cn/ +
+
+


微信交流群 @@ -15,8 +18,7 @@

From f9c7defd3682eca2d660adaf977b19140d52250d Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Thu, 27 Apr 2023 20:02:10 +0800 Subject: [PATCH 25/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c243c78..382b345 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,4 @@ * [分布式](/分布式/面试题.md) ## 关于我 -[库森的校招经历](https://mp.weixin.qq.com/s/Q1uNpj_6mGscy5JbMqMY-Q) +[库森的校招经历](https://mp.weixin.qq.com/s?__biz=MzkyMTI3Mjc2MQ==&tempkey=MTIxNV81aG91ZWFjc0E3SWQzUCtmY2ZRQ3I1QXJ0MjZPcndKU3FVdDhqWVI2dGh0bDBCTE9ZTGxWamhUcmFuODZmRFhubXBQMGtCVHd3UUhPTkRHYVh0cTZJaHhSeUx2aTJkUXJocUtSVVpVUU5qaGZoYUdFVFNPOG15X2tGbWFVM1g5bFVQVlo2SGZmbGVtdjVSU2RVZTlhSW9zT1NtcjFHeG1nNzhQbUlRfn4%3D&chksm=c184814ef6f308589c207f852920ac287d1d5eb7b05846483b6e7699448a8bf2578b7a03bc33&token=483239364&lang=zh_CN#rd) From e7902d7cf74845006ddd1422aecd2e995cc4fec3 Mon Sep 17 00:00:00 2001 From: cosen1024 <43594200+cosen1024@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:26:38 +0800 Subject: [PATCH 26/26] Update README with additional resources Added links for discounts and online reading resources. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 382b345..2fa4455 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Java-Interview 「Java面试小抄」一份通向理想互联网公司的面试指南,包括 Java基础、集合、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统、计算机网络、系统设计、分布式、Java 项目实战等。 +> 优惠价GPT claude会员充值,正版订阅 售后无忧,https://doloffer.com 9折优惠码:AI8888 > 在线阅读:https://www.javalearn.cn/