互联网面试知识技术总结(javacore、jvm、redis、kafka、zookeeper、mysql、hystrix)-第一章...

该笔记是自己在一个月时间内做出的总结,知识点排序不是很整齐,可能有错漏(欢迎指正),知识体系覆盖了javacore、jvm、gc优化、多线程开发,redis、kafka、zookeeper,mysql,hystrix,spring等核心知识点,javacore是自己基于jdk1.6、1.7、1.8做出的一些总结,redis重点是在内存管理这块(基于c++源码理解做出的总结)

1,幂等 推荐分布式锁(乐观锁)。 场景 <1>,一个订单不允许被多次支付(包括并发状态下不允许被多个人同时支付) 下单前对订单状态(status字段)校验,对订单加上乐观锁(加上一个字段lock),只有加锁成功的人才能进行支付。 或者针对每个订单生成唯一支付日志,保证一个未支付的订单只允许被一个线程支付。 <2>,库存扣减,不允许超卖。 需要考虑场景,在c端展示层,读取缓存的方式,如果库存扣减了,消息异步更新缓存。 在对库存更改的时候,使用分布式锁,锁住某个产品id的库存,只允许一个线程去更改。

2,redis集群搭建。 基于redis 3.0集群模式,多个master节点根据hash分布在16384槽上,每个master节点挂靠多个slave节点。 集群是好多个redis一起工作的,如果为了保证集群不是那么容易挂掉,所以呢,理论上就应该给集群中的每个节点至少一个slave redis节点。

3,redis集群模式: <1>standalone类型架构,单节点结构(非集群模式)。 缺点:单节点,存储空间和并发访问能力有很有限,很容易发生缓存穿透,流量直接打入db。

<2>redis主从,一个master挂着多个slave, 优点:master一般只接受写入流量,slave负责读取,提高了负载能力(主从复制是乐观复制,当客户端发送写执行给主,主执行完立即将结果返回客户端,并异步的把命令发送给从,从而不影响性能)。 缺点: A,Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。 B,主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。 C,主从机器都是全量备份的数据(浪费内存),单机需要更大内存,存储空间受限,不易扩容。

<3>哨兵模式:Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能,哨兵的作用就是监控redis主、从节点是否正常运行,主出现故障自动将从节点转换为主节点。 哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个 (1)监控主节点和从节点是否正常运行。 (2)主节点出现故障时自动将从节点转换为主节点。 备注:哨兵也是集群部署,集群初始化时配置。

哨兵工作原理: 哨兵(sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。 每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown)。若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置. 虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel).哨兵(sentinel) 的一些设计思路和zookeeper非常类似

优点:哨兵集群模式是基于主从模式的,所有主从的优点,哨兵模式同样具有,主从可以切换,故障可以转移,系统可靠性更高。 缺点: 主从机器都是全量备份的数据(浪费内存),单机需要更大内存,存储空间受限,不易扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。 哨兵模式还存在脑裂问题 Redis 哨兵模式脑裂:master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着 此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里就会有两个master,也就是所谓的脑裂

解决方案: min-slaves-to-write 1 min-slaves-max-lag 10 要求至少有1个slave,数据复制和同步的延迟不能超过10秒,如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟 那么这个时候,master就不会再接收任何请求了上面两个配置可以减少异步复制和脑裂导致的数据丢失 上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求,因此在脑裂场景下,最多就丢失10秒的数据。 链接:www.jianshu.com/p/f6ceae73c…

<4>redis 3.0 cluster:分布式存储。即每台redis存储不同的内容,共有16384个slot。每个redis分得一些slot,hash_slot = crc16(key) mod 16384 找到对应slot,键是可用键,集群至少需要3主3从,且每个实例使用不同的配置文件,主从不用配置,集群会自己选。 备注:它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。如果某个节点和所有从节点全部挂掉,我们集群就进入faill状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入发力了状态。这就是我们的redis的投票机制。

优点:具备哨兵模式的优点,数据分散存储,内存利用率更加高,支持在线扩容。 缺点:如果某个slot上面的master和slave都挂掉,就会出现集群不可用。

备注:在Redis Cluster3.0动态扩容时,新增的Master节点是没有数据的,主节点如果没有slots的话,存取数据就都不会被选中,需要手动把slot及其中数据迁移到新增的Master中(参考Redis集群官方中文教程),操作指令支持节点重新洗牌(调增slot对应的数据)。

<5>Jedis sharding集群 Redis Sharding可以说是在Redis cluster出来之前业界普遍的采用方式,其主要思想是采用hash算法将存储数据的key进行hash散列,这样特定的key会被定为到特定的节点上(采用一致性哈希算法,将key和节点name同时hashing,然后进行映射匹配) <6>中间件代理 常见中间件: Twemproxy Codis nginx

参考:blog.csdn.net/qq_35152037…

3.0 cluster那么这个集群是如何判断是否有某个节点挂掉了呢? 首先要说的是,每一个节点都存有这个集群所有主节点以及从节点的信息。 它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。如果某个节点和所有从节点全部挂掉,我们集群就进入faill状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入发力了状态。这就是我们的redis的投票机制,     (1)投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉.     (2)什么时候整个集群不可用(cluster_state:fail)?     a:如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完整时进入fail状态.     b:如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.

Redis cluster的slave选举流程:

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下: 1.slave发现自己的master变为FAIL 2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息 3.其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack 4.尝试failover的slave收集FAILOVER_AUTH_ACK 5.超过半数后变成新Master 6.广播Pong通知其他集群节点。

SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新,持有最新数据的slave将会首先发起选举(理论上)

参考:www.cnblogs.com/liyasong/p/…

3,redis持久化(rdb和aof) RDB:在指定的时间间隔能对数据进行快照存储。 优点:使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能 缺点:RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失,数据的准确性不高。 AOF:AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。 优点:可以保持更高的数据完整性,因此已成为主流的持久化方案 缺点:AOF文件比RDB文件大,且恢复速度慢。

4,redis高可用原理分析:blog.csdn.net/qq_41849945… 备注:实际还是一主多从的结构

5,redis如何实现主从复制?以及数据同步机制? Redis主从复制一般都是异步化完成(复制功能不会阻塞主服务器),Redis主从复制可以根据是否是全量分为全量同步和增量同步,

<1>Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份(master生成一份全量的rdb快照文件,发送给slave)。具体步骤如下: a,从服务器连接主服务器,发送SYNC命令; b,主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令; c,主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令; d,从服务器收到快照文件后丢弃所有旧数据,载入收到的快照; e,主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; f,从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

<2>Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

4,redis如何压缩AOF文件,具体过程如下: redis调用fork ,现在有父子两个进程 <1>,子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令 <2>,父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。 <3>,当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。 <4>,现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。 <5>,需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

简单总结:如何缩小AOF文件大小:文件重写是指定期重写AOF文件(产生新的AOF文件),减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件(为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件)

参考:www.cnblogs.com/xingzc/p/59…

5,AOF缩减自身文件大小的时候,来了新的写请求怎么办? 子进程同步完内存中数据之后,会发出指令,通知父进程把最近的写请求操作刷入新的aof文件。

6,redis multi,pipeline的区别 redis交互流程:业务应用服务器(如hotel-goods-service)--->redis client--->redis server

备注:redis属于典型的c/s架构

multi特点: <1>实际上当我们使用multi操作时,redis client是一条条发送数据到 redis server,这些请求是积压在服务端的queue里面,然后依次一次执行完毕,redis服务端一次性返回所有命令返回结果,服务端执行这段操作是开启事务机制的。 <2>由于每发送一条指令,都需要单独发给服务器,服务器再单独返回“该条指令已加入队列”这个消息。这是比Pipeline慢的原因之一。 <3>Multi执行的时候会先暂停其他命令的执行(事务机制),类似于加了个锁,直到整个Multi结束完成再继续其他客户端的请求。这是Multi能保证一致性的原因,也是比Pipeline慢的原因之二 <4>由于服务端开启了事务机制,因此multi是原子性的。 <5>由于redis client逐条发送请求到redis server中的queue,因此multi属于服务端缓冲。

pipeline特点: <1>redis client将所有命令打包一次性发送。发送成功后,服务端不用返回类似“命令已收到”这样的消息,而是一次性批量执行所有命令,成功后再一次性返回所有处理结果。 <2>服务端处理命令的时候,不需要加锁,而是与其他客户端的命令混合在一起处理,所以无法保证一致性。 <3>由于是redis client一次打包发送出去的请求,因此pipeline是客户端缓冲 <4>由于pipeline把请求打包发送给redis server,少了与redis server的多次交互,因此性能更加好。

参考:blog.walkerx.cn/2018/07/08/…

8,redis动态扩容 过程描述: HASH_SLOT = CRC16(key) mod 16384 通过key与slot的映射算法,计算出当前key应该存储在哪个slot中,从公式中可以看出,当前key与slot的映射是固定不变的。由于每个Master负责一部分slot,可知在Master节点数量调整时,slot与Master映射的关系也会调整,也就是说slot和master之间有个映射表的。

在动态扩容过程中slot的特点: 举例:MasterA节点(原集群中的旧机器)迁移部分slot到MasterB节点 MIGRATING状态是发生在MasterA节点中的一种槽的状态,预备迁移槽的时候槽的状态首先会变为MIGRATING状态,这种状态的槽会实际产生什么影响呢?当客户端请求的某个Key所属的槽处于MIGRATING状态的时候,影响有下面几条

<1>如果Key存在则成功处理 <2>如果Key不存在,则返回客户端ASK,仅当这次请求会转向另一个节点,并不会刷新客户端(redis-client)中node的映射关系,也就是说下次该客户端请求该Key的时候,还会选择MasterA节点 <3>如果Key包含多个命令,如果都存在则成功处理,如果都不存在,则返回客户端ASK,如果一部分存在,则返回客户端TRYAGAIN,通知客户端稍后重试,这样当所有的Key都迁移完毕的时候客户端重试请求的时候回得到ASK,然后经过一次重定向就可以获取这批键。

IMPORTING状态是发生在MasterB节点中的一种槽的状态,预备将槽从MasterA节点迁移到MasterB节点的时候,槽的状态会首先变为IMPORTING。IMPORTING状态的槽对客户端的行为有下面一些影响:

<1>正常命令会被MOVED重定向,如果是ASKING命令则命令会被执行,从而Key没有在老的节点已经被迁移到新的节点的情况可以被顺利处理; <2>如果Key不存在则新建; <3>没有ASKING的请求和正常请求一样被MOVED,这保证客户端node映射关系出错的情况下不会发生写错;

简述:redis在动态扩容时,需要集群里面的旧机器的部分slot迁移到新机器,由于只涉及部分slot迁移,因此这些待迁移的slot会变成迁移状态(MIGRATING),迁移过程中旧机器仍然接受读取和写入流量,如果key在旧机器不存在,请求将转发到新扩容的节点(如MasterB),等数据迁移完成再更新slot映射关系表即可。

参考:www.cnblogs.com/wxd0108/p/5…

9,单机redis如何提高并发 <1>redis性能瓶颈在io,因此单key不应该存储大值(大key分段存储)。 <2>pipeline代替multiGet操作。 <3>写入redis的数据做压缩。 <4>当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能(数据持久化时需要在持久化和延迟/性能之间做相应的权衡,实际上是在持久化的时候,数据占用了内核的页缓存,导致可用页缓存空间紧张) <5>存储对象使用hash,避免修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护。

参考:www.cnblogs.com/moonandstar…

10,redis内存优化 Redis 最为常用的数据类型主要有以下五种:String,Hash,List,Set,Sorted set Redis任何的数据类型都是由一个叫做redisObject的数据结构管理的,具体参考如下

数据类型(type)
编码方式(encoding)

数据指针(ptr)
虚拟内存(vm)

数据类型:string,list,hash,set,sorted set, 编码方式:raw,int,ht,zipmap,linkedlist,ziplist,intset lru计时时间:记录对象最后一次被访问的时间,当配置了 maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 时, 用于辅助LRU算法删除键数据。可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间

备注:首先最重要的一点是不要开启 Redis 的 VM 选项,即虚拟内存功能,这个本来是作为 Redis 存储超出物理内存数据的一种数据在内存与磁盘换入换出的一个持久化策略,但是其内存管理成本也非常的高,并且我们后续会分析此种持久化策略并不成熟,所以要关闭 VM 功能,请检查你的 redis.conf 文件中 vm-enabled 为 no。 其次最好设置下redis.conf中的 maxmemory 选项,该选项是告诉 Redis 当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护好你的 Redis 不会因为使用了过多的物理内存而导致 swap,最终严重影响性能甚至崩溃

Redis内部使用一个redisObject对象来表示所有的key和value,redisObject最主要的信息如上图所示:type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串,当使用int存储时,比使用raw存储原生的字符串更加节省内存。

Redis内存优化点: <1>存储字符串时,如果值是整数,内部转成int存储,节省空间。 <2>Redis Hash是value内部为一个 HashMap,如果该Map的成员数比较少,则会采用类似一维线性的紧凑格式来存储该Map即省去了大量指针的内存开销,具体配置参数如下: hash-max-zipmap-entries 64 hash-max-zipmap-value 512 含义是当 value 这个 Map 内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即 value 内部有64个以下的成员就是使用线性紧凑存储,超过该值自动转成真正的 HashMap, hash-max-zipmap-value 含义是当 value 这个 Map 内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。以上2个条件任意一个条件超过设置值都会转换成真正的 HashMap,也就不会再节省内存了,那么这个值是不是设置的越大越好呢,答案当然是否定的,HashMap 的优势就是查找和操作的时间复杂度都是 O(1) 的,而放弃 Hash 采用一维存储则是 O(n) 的时间复杂度,如果成员数量很少,则影响不大,否则会严重影响性能,所以要权衡好这个值的设置,总体上还是最根本的时间成本和空间成本上的权衡。 <3>共享对象池,对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

参考:www.cnblogs.com/jandison/p/…

11,页缓存技术 Page cache是通过将磁盘中的数据缓存到内存中,从而减少磁盘I/O操作,从而提高性能。此外,还要确保在page cache中的数据更改时能够被同步到磁盘上,后者被称为page回写(page writeback)。一个inode对应一个page cache对象,一个page cache对象包含多个物理page。 对磁盘的数据进行缓存从而提高性能主要是基于两个因素:第一,磁盘访问的速度比内存慢好几个数量级(毫秒和纳秒的差距)。第二是被访问过的数据,有很大概率会被再次访问。 参考:blog.csdn.net/damontive/a…

12,redis的过期数据删除策略和内存淘汰策略

一、过期数据删除策略(redis是两种策略配合一块使用) <1>惰性删除,不管过期的键,在这种策略下,当键在键空间中被取出时,首先检查取出的键是否过期,若过期删除该键,否则,返回该键。很明显,惰性删除依赖过期键的被动访问,对于内存不友好,如果一些键长期没有被访问,会造成内存泄露(垃圾数据占用内存),但是它属于cpu友好型,不需要占用太多cpu时间片。 <2>定期删除,redis创建一个定时任务随机扫描数据是否过期(CPU空闲时在定期serverCron任务中),逐出部分过期Key,具体删除过程如下

A,Redis配置项hz定义了serverCron任务的执行周期,默认为10,即CPU空闲时每秒执行10次; B,每次过期key清理的时间不超过CPU时间的25%,即若hz=1,则一次清理时间最大为250ms,若hz=10,则一次清理时间最大为25ms; C,清理时依次遍历所有的db; D,从db中随机取20个key,判断是否过期,若过期,则逐出; E,若有5个以上key过期,则重复步骤4,否则遍历下一个db; F,在清理过程中,若达到了25%CPU时间,退出清理过程;

备注:每次删除限定在25%cpu时间片范围内,并且还判断过期的key的比例,比如超过25%过期key才继续下一此删除。这是一个基于概率的简单算法,基本的假设是抽出的样本能够代表整个key空间,redis持续清理过期的数据直至将要过期的key的百分比降到了25%以下。这也意味着在长期来看任何给定的时刻已经过期但仍占据着内存空间的key的量最多为每秒的写操作量除以4。

二、内存淘汰策略 Redis内存淘汰策略被激发,是内存使用达到一定的阈值才开始运行的(redis.conf里面配置),因此内存淘汰策略和key过期删除策略是两码事。

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。 allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。 volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。 volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。 volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。 以上策略是:(非)过期、随机、lru的组合

redis过期字典: redisDb结构的expires字典保存了数据库中所有键的过期时间,为过期字典,键就是数据库键,值是long long类型,毫秒经度的unix时间戳(过期时间)。

举例简述redis 3.0对allkeys-lru的实现流程 Redis服务器每执行一个命令,都会检测内存,判断是否需要进行数据淘汰 <1>判断目前已经使用的内存大小是否比设置的maxmemory要小,如果小于maxmemory,那么无须执行进一步操作。 <2>判断淘汰策略是否为noeviction,如果是,直接return回去,不进行任何内存淘汰。 <3>根据传入的对象大小,计算需要释放多少字节的内存 <4>开始随机采样,每次随机获取10个key,选出lru时间最小的key放入一个长度为16的pool里面,后续再不断随机取样10个key,如果lru时间比pool最小lru时间还小,就加入pool,直至pool填满,然后开始淘汰pool里面lru时间最小的,直至淘汰的空间足够存储需要的值。

备注:Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。 redis的 lru算法实际上不是非常准确的,是基于快速抽样比较的实现(如果使用双向链表的指针标记,占用的空间更加大) 参考:yq.aliyun.com/articles/25…

13,redis集群访问流量倾斜 <1>hot key出现造成集群访问量倾斜 场景:Hot key,即热点 key,指的是在一段时间内,该 key 的访问量远远高于其他的 redis key, 导致大部分的访问流量在经过 proxy 分片之后,都集中访问到某一个 redis 实例上。hot key 通常在不同业务中,存储着不同的热点信息。比如:新闻应用中的热点新闻内容,活动系统中某个用户疯狂参与的活动的活动配置,商城秒杀系统中,最吸引用户眼球,性价比最高的商品信息。 解决方案: 一,对hot key数据进行本地缓存。 二,利用分片算法的特性,对key进行打散处理,比如对key加上0~9的后缀,redis key经过分片分布到不同的实例上,将访问量均摊到所有实例。 <2>big key在redis存储优化 如果big value 是个大json 通过 mset 的方式,将这个key的内容打散到各个实例中(分段存储),减小big key对数据量倾斜造成的影响。

7,mybatis延迟加载。 resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),association、collection具备延迟加载功能。   延迟加载:先从主表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。 场景举例:查询出符合要求的订单数据,再去查出这些订单的用户信息,整个过程要执行多个sql,但是在一个resultMap返回结果。

实现原理:在createResultObject的时候,会判断当前返回值是否含有延迟加载的数据,如果有,就创建动态代理对象(Javasisst或者Cglib代理),执行被代理的方法,获取数据,并封装到resultMap。 延迟加载的好处:先在单表查询、需要时再从关联表去关联查询,大大提高 数据库性能,因为查询单表要比关联查询多张表速度要快。 参考:my.oschina.net/wenjinglian…

9,mybatis中#{}和{}的区别   #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号(很大程度上防止了sql注入),将传入的数据直接显示生成在sql中(无法防止sql注入)。

10,tcp三次握手(两次不行吗?),四次挥手,为什么这么做。 三次握手是为了建立tcp的双工通信,四次挥手是为了能够保证tcp的半闭合状态。

11,网络丢包如何解决,分不同业务场景。(ack,滑动窗口)

9,读写锁源码(todo) 参考:blog.csdn.net/yanyan19880… 10,各种线程实现。

     //线程池大小固定为1
     Executors.newSingleThreadExecutor();

    //固定大小线程池由自己设定,即自己控制资源的固定分配
    Executors.newFixedThreadPool(10);

    //动态调整线程池的大小,最小为0,最大为int最大值,,newCachedThreadPool会大幅度提高大量短暂异步任务的性能,
    //如果执行业务逻辑比较慢,会导致持续创建线程,导致cpu资源消耗殆尽
    //为什么使用SynchronousQueue?最多只能持有一个任务数据,当任务数据插入队列失败,会驱动创建新线程,SynchronousQueue作为主线程池的工作队列,它是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作。这意味着,如果主线程提交任务的速度高于线程池中处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源
    Executors.newCachedThreadPool();//newCachedThreadPool不适合io密集型的网络请求,只适合计算密集型(每次请求耗时很短)

    //基于延迟队列实现的延时任务线程池,周期性的执行所提交的任务
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
    scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " run");
        }
    }, 1000,2000, TimeUnit.MILLISECONDS);
复制代码

11,各种队列实现。 <1>PriorityBlockingQueue(无界队列) 内部使用reentrantlock(基于数组实现的堆排序,数组会动态扩容),每次入队和出队都需要加锁,保证线程安全。 PriorityBlockingQueue存储的对象必须是实现Comparable接口的 因为PriorityBlockingQueue队列会根据内部存储的每一个元素的compareTo方法比较每个元素的大小 需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。PriorityBlockingQueue在take出来的时候会根据优先级 将优先级最小的最先取出

备注:优先队列扩容阶段为什么释放锁,因为只有一把锁,扩容期间不影响数据读取(提高并发效率),扩容完之后再拷贝以前的数据(拷贝阶段加锁就可以了)。 由于PriorityBlockingQueue在空间不够的时候,会自增扩容数组进行堆排序,不需要对数据的put操作进行阻塞,只对数据获取进行阻塞,因此只需要一个condition(唤醒数据查询的线程)

<2>ArrayBlockingQueue,基于数组实现,只有1个锁(数据的写入不需要构造node节点,直接存储外部传入的引用,效率已经足够高,LinkedBlockingQueue构造node节点,耗时相对高一些,因此读写锁分离为了提高并发效率),添加数据和删除数据的时候只能有1个被执行,不允许并行执行。使用Condition notEmpty,Condition notFull来实现生产者-消费者模式(通知模式)。

<3>LinkedBlockingQueue,基于链表实现,只有2个锁(由于是无界,因此不用担心队列写满,读写可以分离),放锁和读锁,两把锁分别管理head节点和last节点的操作,通过原子变量count控制队列长度状态,添加数据和删除数据是可以并行进行的,当然添加数据和删除数据的时候只能有1个线程各自执行。LinkedBlockingQueue将读和写操作分离,可以让读写操作在不干扰对方的情况下,完成各自的功能,提高并发吞吐量。使用Condition notEmpty,Condition notFull来实现生产者-消费者模式(通知模式)

备注:ArrayBlockingQueue和LinkedBlockingQueue这两个阻塞队列,队列满了,放不进去会被阻塞,队列为空,取不出结果会被阻塞,因此需要两个condition。 压测报告:一千万条的数据进行多线程插入和读取,明显看出ArrayBlockingQueue比LinkedBlockingQueue性能强30%。

<4> SynchronousQueue通过将入队出队的线程绑定到队列的节点上,并借助LockSupport的park()和unpark()实现等待和唤醒,先到达的线程A需调用LockSupport的park()方法将当前线程进入阻塞状态,知道另一个与之匹配的线程B调用LockSupport.unpark(Thread)来唤醒在该节点上等待的线程A。其内部没有任何容量,任何的入队操作都需要等待其他线程的出队操作,反之亦然。如果将SynchronousQueue用于生产者/消费者模式,那么相当于生产者和消费者手递手交易,即生产者生产出一个货物,则必须等到消费者过来取货,方可完成交易。

备注:SynchronousQueue没有使用condition(本质上基于LockSupport实现),直接使用了LockSupport的park()和unpark()实现等待和唤醒 参考:blog.csdn.net/vickyway/ar…

<5> DelayQueue的泛型参数需要实现Delayed接口,Delayed接口继承了Comparable接口,DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,最小化不必要的等待时间。DelayQueue不允许包含null元素。(借助LockSupport.parkNanos和unpark实现延时,reentrantlock实现安全操作),available.awaitNanos(delay)实现延时,available.awaitNanos内部基于LockSupport.parkNanos(this, nanosTimeout)实现挂起,指定时间范围内自动唤醒(由操作系统自己去调度);

特点:元素进入队列后,先进行排序(调用compareTo方法排序),然后,只有getDelay也就是剩余时间为0的时候, 该元素才有资格被消费者从队列中取出来,实际上只有队列头元素出队,其它才能出队,会受到头结点元素延时时间的影响。 备注:DelayQueue使用PriorityQueue(自动扩容的堆数组),因此数据的存入不需要阻塞,读取的时候才进行堵塞,因此只需要一个condition。

13,线程池核心线程如何设置。 如果计算密集型通常是cpu核数+1,io密集型是99线*qps/1000 14,Spring如何处理循环引用的 <1>,循环依赖的对象都通过构造器注入,会注入失败。(无论bean是singleton,还是prototype,或者是混合了singleton、prototype),因为生成对象必须依赖构造方法,而构造方法里面需要对方的实例对象,因此形成了死循环。 <2>,循环依赖的bean都是通过属性注入,如果注入都是singleton对象,都能创建成功。如果注入都是prototype,就会失败。如果是混合singleton、prototype,只有先创建singleton才能保证成功,否则就会失败。 Spring对于构造器注入的对象会标记为正在创建中,如果在循环依赖创建过程中发生依赖的对象正在创建中,会抛出异常。 归根结底是spring容器内部保留了singleton对象,prototype对象被丢失。 备注:singleton对象有三级缓存的概念,prototype对象没有,singleton在创建过程中会检查三级缓存,依次从里面取出数据(前提这些对象都是调用构造方法创建成功了),如果成功取出就完成注入。prototype对象在创建过程中会被标记为“正在创建中”,如果循环创建中,发现依赖的bean处于“正在创建中”,就会抛出异常。

参考:blog.csdn.net/chen2526264…

15,spring初始化对象

单例对象(基于三级缓存实现) (1)createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象 (2)populateBean:填充属性,这一步主要是多bean的依赖属性进行填充 (3)initializeBean:调用spring xml中的init 方法。 单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。

prototype对象(完成初始化之前存在一个set里面) 初始化流程,在初始化属性的时候,isPrototypeCurrentlyInCreation,会校验这个属性的bean是否在创建中,如果在创建中会抛出异常。

16,spring ioc ioc,依赖注入,在以前的软件工程编码过程中,类的属性需要硬编码生成对象数据,耦合性较高,如果使用ioc,是在容器启动过程中,在bean对象实例化过程中需要检查其依赖数据,并且进行数据注入(setter,构造器注入),完成一个对象的实例化并实现了解耦合,并且能够对这些对象进行复用。

aop,主要分为两大类:一是采用jdk动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用动态织入的方式,引入特定的语法创建“方面”,是在类加载时期织入有关“方面”的代码。它利用一种称为"横切"的技术,并将那些影响了多个类的公共行为封装到一个可重用模块,简单理解是抽象出与业务逻辑无关的公共行为逻辑。

17,java的future编程 FutureTask实现了Runnable, Future接口,并实例化Callable对象,在线程开启运行时,执行线程任务的实现类的run方法,run执行完毕将结果引用赋值给outcome属性(如果任务线程没执行完,当前主线程会进入阻塞状态,任务线程执行完毕,会设置outcome,并解除主线程阻塞)。

18,hystrix

<1> 使用场景:在soa架构中,资源隔离(线程池、或者信号量隔离),熔断(防止雪崩效应)降级,依赖的服务使用不同的commandKey(最小隔离单元,可能多个commandKey在一个线程池内)标注,实现隔离,线程池是HystrixCommandGroupKey标识。 <2> hystrix是如何通过线程池实现线程隔离的 Hystrix通过命令模式,将每个类型的业务请求封装成对应的命令请求,比如查询订单->订单Command,查询商品->商品Command,查询用户->用户Command。每个类型的Command对应一个线程池。创建好的线程池是被放入到ConcurrentHashMap中,比如查询订单。 <3> hystrix如何实现熔断的 用户请求某一服务之后,Hystrix会先经过熔断器,此时如果熔断器的状态是打开,则说明已经熔断,这时将直接进行降级处理,不会继续将请求发到线程池。如果熔断器是关闭状态,会检测最近10秒的请求错误率,当错误率超过预设的值(默认是50%)且10秒内超过20个请求,则开启熔断。熔断器默认是在5s后开始重新嗅探,会尝试放过去一部分流量进行试探,确定依赖服务是否恢复。

<4> hystrix如何统计失败率 每个熔断器默认维护10个bucket 每秒创建一个bucket 每个blucket记录成功,失败,超时,拒绝的次数 当有新的bucket被创建时,最旧的bucket会被抛弃

<5> 核心参数 HystrixCommandGroupKey,线程池分组 HystrixCommandKey,线程池标识。

Circuit Breaker(熔断器)一共包括如下6个参数。 1、circuitBreaker.enabled 是否启用熔断器,默认是TURE。 2、circuitBreaker.forceOpen 熔断器强制打开,始终保持打开状态。默认值FLASE。 3、circuitBreaker.forceClosed 熔断器强制关闭,始终保持关闭状态。默认值FLASE。 4、circuitBreaker.errorThresholdPercentage 设定错误百分比,默认值50%,例如一段时间(10s)内有100个请求,其中有55个超时或者异常返回了,那么这段时间内的错误百分比是55%,大于了默认值50%,这种情况下触发熔断器-打开。 5、circuitBreaker.requestVolumeThreshold 默认值20.意思是至少有20个请求才进行errorThresholdPercentage错误百分比计算。比如一段时间(10s)内有19个请求全部失败了。错误百分比是100%,但熔断器不会打开,因为requestVolumeThreshold的值是20. 这个参数非常重要,熔断器是否打开首先要满足这个条件。 6、circuitBreaker.sleepWindowInMilliseconds 半开试探休眠时间,默认值5000ms。当熔断器开启一段时间之后比如5000ms,会尝试放过去一部分流量进行试探,确定依赖服务是否恢复

参考:itindex.net/detail/5778…

欢迎打赏

转载于:https://juejin.im/post/5cab6f18e51d456e55623ac3

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值