高级面试题(new)

HashMap

32.1 几种形式和比较 keyset,values,entrySet,iterator

1.keyset

2.values

3.entrySet

4.iterator

  • keyset性能最差(查询map两遍),其他相差不多(依据业务和数据量时间进行测试)
  • values只能获取values

32.2 HashMap原理

1.hashMap实现原理:
HashMap 基于 Hash 算法实现的,当往hashmap中插入值时,会将Key.hashcode进行hash运算,结果(取摸,取址)放入对应的槽位(数组链表O(1)),

  • 如存在key.hashCode相同,即出现hash冲突,为解决冲突,将以链表形式存在,此时时间复杂度变成O(n)
  • 当达到数组阈值(0.75)的时候,为减少冲突,就进行扩容,同时将原有数据迁移到新的数组上
  • 当数组长度达到64,链表高度达到8,为解决时间复杂度问题,此时会将底层结构转变成红黑树,此时时间复杂度变成log(n)


2.hash算法:


key的hashCode与右移16位的hashCode进行异或,右移16位,正好32位一半,高半区和低半区做异或操作,就是了混合原hash的高位和地位,增加hash值(低位)随机性,让hashmap中元素位置尽量分布均匀,减少hash冲突

3.扩容长度为2^n的原因:

得出的结果为0,1,4,5,不可能为2,3

  • 扩容长度为2的n次幂有点:
  • 1.充分利用空间,避免浪费(因为与0取与肯定为0,部分空间无法利用)
  • 2.扩容数据迁移时不需要重新hash,在原来位置原来位置+扩容长度(参考上图)

32.3 ConcurrentHashMap原理:

jdk 1.7 -> jdk 1.8

总结:CAS

  • JDK1.7 ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。将整个哈希表分成了多个小的哈希表,每个小哈希表Segment上都有一把锁。

  • JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized(锁头结点替换ReentrantLock)来保证并发更新的安全。

  • 数据结构采用:数组(cas)+链表(Synchronized,链表挂在数组上,即数组长度大多,锁粒度就多大,相对分段锁大大降低)+红黑树,采用红黑树之后可以保证查询效率O(logn))。

  • 扩容,1.7直接读写等待扩容完成才可操作,1.8协助扩容

  • 分段计数

  • 总长度大于64(扩容两次),某个链表碰撞大于8(深度),就会从jdk1.7 数组+链表=》jdk1.8 数组+链表+红黑树

  • 速度更快:底层数据结构hashmap/hashset,hash算法,0.75*长度,扩容

  • ConcurrentHashMap,jdk1.7 concurrentLevel=16,段太大可能导致浪费,太小可能每个段使用过多效率低,jdk1.8采用链表+红黑树 ,并且CAS算法,效率大大提高

  • jdk1.8拿掉永久区,取代为MetaSpace 元空间,使用物理内存,空间大大增大,oom发送概率大大降低,同时垃圾回收也大大降低,效率提高

  • jdk 1.7 hashmap插入数据是插入链表的头部(头插法),rehash之后链表逆序了(另一个多线程又还是按照顺序执行),在多线程map扩容情况下有可能出现死循环情况,而jdk 1.8则插入尾部,避免了死循环情况

ConcurrentHashMap查询:

  • get方法不会加锁

  • 在数组上基于key的hashcode定位位置找到value(hashcode相同位置)

    • 数组上,如果key相等O(1),就拿取
    • 数组上如果key不相等,那么看双向链表O(n),遍历找到相同key,就拿取
    • 链表上没找到,那么查看是否在扩容,如果是,nexTable那么新数组也是按照上面流程找
    • 如果都没找到,那么就看红黑树上,如果有写操作或等待,那么查转换红黑树时保留的双向链表(红黑树没完成,查询不准确,又不想让查询等待),否则查红黑树O(logn),二分查找法找到key
  • 总结:1.存储结构优化(红黑树);2.写数据加锁优化(抛弃分段锁);3.扩容优化(协助扩容);4.计算优化;
    ConcurrentHashMap虽然保持线程安全,提升读写速度,但是不保证复合操作时候线程安全,即保持弱一致性,不保证强一致性,已有读又有写,读的是最新写的内容

RabbitMQ

img

MQ防重复消费

幂等性(同样的请求来很多次,确保对应的数据是不会改变的,只消费一次,如何避免消息的重复消费)
解决方案:

  • 基于数据库的唯一键来保证重复数据不会重复插入多条,消费一次插入表中,唯一性保证无法再次插入(唯一ID + 指纹码机制)
  • 乐观锁,每次插入或更新判断版本是否符合预期,符合才操作
  • redis分布式锁保持数据,消费的时候判断是否存在,如果存在就重复消费

MQ防消息丢失

生产者 -》MQ服务器-》消费者

  • 生产者-》MQ服务器异步confirm模式,MQ服务器成功接收到消息返回confirm确认给生产者,并持久化到磁盘,否则生产者会将消息保留,后续会重发
  • MQ服务器-》消费者:一般设置手工确认ack机制,确保消费成功才ok,否则会重发给消费端
  • Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失(配置死信队列解决)!
RabbitMQ三种机制
  • 保证消息的成功投递(confirm)(mq返回生产者投递成功消息),
  • 成功消费(ack)(消费者手工ack返回mq消费成功消息)
  • 消息丢失(return)(生产者投递失败情况,死信队列保证消息不丢失)的处理
1、RabbitMQ的Confirm机制

confirm机制机制是保证消息成功投递到了服务端,通过回调通知生产者是否收到了消息,重点在生产者这里,要设置消息确认,然后监听回调。

public static void main(String args[]) throws Exception {
      ···
      channel.basicPublish(EXCHANG_NAME, ROUTING_KEY, null, msg.getBytes());
      // 6、添加消息确认监听
      channel.addConfirmListener(new ConfirmListener() {
          // 消息确认
          @Override
          public void handleAck(long l, boolean b) throws IOException {
              System.out.println("--------handle Ack---------");
          }

          // 消息未确认
          @Override
          public void handleNack(long l, boolean b) throws IOException {
              System.out.println("--------handle No Ack---------");
          }
      });
  }
2、RabbitMQ的ACK机制

ACK机制是rabbitmq保证消息成功消费的机制,默认应该是自动签收的,也就是消息被队列取出即视为已消费,

// 成功签收
channel.basicAck(envelope.getDeliveryTag(), false);
// 未成功签收,第三个参数标识:是否重回队列,设置为true,则会重回到队列,如果设置为false则需要自己处理,写日志或其他方式
channel.basicNack(envelope.getDeliveryTag(), false, true);
  public static void main(String args[]) throws Exception {
        ···
       channel.basicConsume(QUEUE, false, consumer);
        
        //autoAck 是否自动确认消息,true自动确认,false 不自动要手动调用,建立设置为false
        //channel.basicAck(envelope.getDeliveryTag(), false);
        // 第三参数:是否重回队列,设置为true,则会重回到队列
        //channel.basicNack(envelope.getDeliveryTag(), false, true);
    }
3、RabbitMQ的Return机制

return机制是保证消息不丢失,有些时候我们生产的消息没有投递到任何队列,或者队列名称、交换机、路由错了,导致没有投递到队列里面,这个时候return监听机制就能获取到未被成功投递的消息,然后做业务处理,生产消息的时候要设置消息确认模式,然后添加return监听器,设置mandatory为true,如果为false,那么broker端自动删除该消息。

public static void main(String args[]) throws Exception {
        ···
        // 添加return监听
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
            	System.err.println("消息投递失败!!!");
            }
        });
        // 4、发送消息
        String msg = "this is return msg !";
        //  mandatory, 设置为true,则监听器会接收到路由不可达的消息, 然后进行处理,如果设置为false,那么broker端自动删除该消息。
        channel.basicPublish(EXCHANG_NAME, ROUTING_KEY, true,null, msg.getBytes());

    }

RabbitMQ 怎么实现延迟消息队列?

延时队列:借助普通队列 + 死信队列实现

  • 1.设置TTL(消息存活时间)属性的普通队列,不被监听消费,让消息超时进入死信队列(防消息丢失),只监听死信队列里的消费即可实现延时队列

  • 2.rabbitmq-delayed-message-exchange 插件实现延时队列,它可以让消息在指定的时间后才被消费者消费(header上加一个延时时间参数而已)

场景:定时关单

  • 订单创建同时发送消息给延时队列
  • 延时队列在30分钟未付款就经死信路由到死信队列
  • 订单系统监控死信队列收到消息就关单并库存释放
    img

顺序消息

一般场景:一个Queue对应一下Consumer,每次只消费一条信息,是可以保证顺序消费的

问题:一个消息队列多个消费者监听,consumer从MQ里面读取数据是有序的,但是每个consumer的开始执行时间或执行完成时间是不固定的,无法保证先读到消息的consumer一定先完成操作

  • 1、给发送的消息加上序号,消费者接收到消息直接入库,等所有消息接收完,再按照顺序进行消费
  • 2、拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。

kafka

设计原则

  • 高吞吐、低延迟:非常快,几十万/s,最低延迟几毫秒;
  • 高伸缩性:每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中;
  • 持久性、可靠性:Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储;
  • 容错性:允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作;
  • 高并发:支持数千个客户端同时读写。

数据保留的策略?

  • 按照过期时间保留
  • 按照存储的消息大小保留。

时间和大小不论那个满足条件,都会清空数据

kafka读取性能影响因素:

  • cpu 性能瓶颈
  • 磁盘读写瓶颈
  • 网络瓶颈

集群注意点:

  • 最好不要超过 7 个,因为节点越多,消息复制需要的时间就越长,吞吐量就越低。
  • 集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高。

为何如此之快

  • 顺序读写(避免了随机磁盘寻址的浪费);
  • 零拷贝(避免了内核与用户空间之间的切换);
  • 消息压缩(批处理数据压缩减少 I/O 延迟);
  • 分批发送(将数据记录分批发送)。

Linux中的零拷贝

应用程序和内核空间 共享缓冲区,不需要互相拷贝(省略在用户空间拷贝)

应用程序数据-》复制到内核缓冲区-》复制到socket缓存区-》复制到网卡
preview
img

基本概念和架构

  • Producer:发布的消息在分区partition上,随机或哈希
  • Topic:主题,Kafka根据topic对消息进⾏归类,发布的消息都需要指定⼀个topic,消费消息也需要指定topic,分多个区可以并发读取和写入,提升效率
  • Consumer Group (CG):消费者组,一个topic可以有多个CG,topic的消息会复制到所有的CG,但每个partion只会把消息发给该CG中的一个consumer(防止重复消费)。
  • Broker:⼀个Kafka节点就是⼀个broker,⼀个或者多个Broker可以组成⼀个Kafka集群,数据保存在zookeeper上,保证broker之间无状态。

(1)Topic与broker
首先,一个broker可以容纳多个topic,同一个kafka集群可以共同拥有一个topic,而同一个topic又拥有不同的分区,分布在不同的borker上,也就是不同的机器上,所以,分区是分布式的,则数据也是分布式的,kafka就是分布式模式。
img
img

  • 同一个topic可以在同一集群下的多个Broker中分布。
  • 发布的消息在分区partition上
  • topic的消息会复制到所有的CG,但每个partion只会把消息发给该CG中的一个consumer(防止重复消费)
  • 消费者和分区建立连接,这个分区的内容只能是由这个消费者消费(防重复消费)

每个partition在存储层面是一个append log文件,发布到此partition的消息会追加到log文件的尾部,为顺序写入磁盘每条消息在log文件中的位置成为offset(偏移量),offset为一个long型数字,唯一标记一条消息

生产者往后追加消息,消费者默认按照顺序消费消息,也可以指定从头消费消息


消费消息

单播消息:一个消费者组中只有一个消费者可以消费消息

多(广)播消息:多个消费组中的多个消费者可以消费消息



新消费组的消费offset规则

新消费组中的消费者在启动以后,默认会从当前分区的最后⼀条消息的offset+1开始消费(消费新消息)。可以通过以下的设置,让新的消费者第⼀次从头开始消费。之后开始消费新消息(最后消费的位置的偏移量+1)

  • Latest:默认的,消费新消息

  • earliest:第⼀次从头开始消费。之后开始消费新消息(最后消费的位置的偏移量+1)

⽅式⼀:从当前主题中的最后⼀条消息的offset(偏移量位置)+1开始消费

./kafka-console-consumer.sh --bootstrap-server 172.16.253.38:9092 --topic test

⽅式⼆:从当前主题中的第⼀条消息开始消费

./kafka-console-consumer.sh --bootstrap-server 172.16.253.38:9092 --from-beginning --topic test

通过以下命令可以查看到消费组的相关信息:

./kafka-consumer-groups.sh --bootstrap-server 172.16.253.38:9092 --describe --group testGroup

  • current-offset: 最后被消费的消息的偏移量(当前消费到的位置)
  • Log-end-offset: 消息总量(最后⼀条消息的偏移量)
  • Lag:积压了多少条消息

分区

kafka通过topic将消息进⾏分类。不同的topic会被订阅该topic的消费者消费。但是有⼀个问题,如果说这个topic中的消息⾮常⾮常多,多到需要⼏T来存,因为消息是会被保存到log⽇志⽂件中的。为了解决这个⽂件过⼤的问题,kafka提出了Partition分区的概念
  • 分区存储,可以解决一个topic存储⽂件过⼤的问题
  • 分批读写,提供了读写的吞吐量:读和写可以同时在多个分区中进⾏

./kafka-topics.sh --create --zookeeper 172.16.253.35:2181 --replication-factor 1 --partitions 2 --topic test1

kafka中消息⽇志⽂件中保存的内容: 00000.log

kafka内部⾃⼰创建了主题包含了50个分区:__consumer_offsets-49:

这个主题⽤来存放消费者消费某个主题的偏移量。因为每个消费者都会⾃⼰维护着消息的主题的偏移量,也就是说每个消费者会把消费的主题的偏移量⾃主上报给kafka中的默认主题。
因此kafka为了提升这个主题的
并发性,默认设置了50个分区。

⽂件中保存的消息,默认保存7天


kafka集群

副本是为了为主题中的分区创建多个备份,多个副本在kafka集群的多个broker中,会有⼀个副本作为leader,其他是follower

  • leader:kafka的写和读的操作,都发⽣在leader上。leader负责把数据同步给follower。当leader挂了,经过主从选举,从多个follower中选举产⽣⼀个新的leader
  • follower:接收leader的同步的数据
  • isr:拥有最新数据主、从节点被存⼊到isr集合中。但如果节点性能较差,会被提出isr集合

念梳理清楚:集群中有多个broker,创建主题时可以指明主题有多个分区(把消息拆分到不同的分区中存储),可以为分区创建多个副本,不同的副本存放在不同的broker⾥。

  • ⼀个partition只能被⼀个消费组中的⼀个消费者消费,⽬的是为了保证消费的顺序性,但是多个partion的多个消费者消费的总的顺序性是得不到保证的,那怎么做到消费的总顺序性呢
  • partition的数量决定了消费组中消费者的数量,建议同⼀个消费组中消费者的数量不要超过partition的数量,否则多的消费者消费不到消息
  • 如果消费者挂了,那么会触发rebalance机制,会让其他消费者来消费该分区

生产者ack机制

  • ack = 0不需要收到任何的broker收到消息,就⽴即返回ack给⽣产者,最容易丢消息的,效率是最⾼的(没ack确认)
  • ack=1(默认): 多副本之间的leader已经收到消息,并把消息写⼊到本地的log中,才会返回ack给⽣产者,性能和安全性是最均衡的
  • ack=-1/all:⾥⾯有默认的配置min.insync.replicas=2(默认为1,推荐配置⼤于等于2),此时就需要leader和⼀个follower同步完后,才会返回ack给⽣产者(此时集群中有2个broker已完成数据的接收),这种⽅式最安全,但性能最差

消息发送缓冲区

  • kafka默认会创建⼀个消息缓冲区,缓冲消息,缓冲区是32m
  • kafka本地线程会去缓冲区中⼀次拉16k的数据,发送到broker
  • 如果线程拉不到16k的数据,间隔10ms也会将已拉到的数据发到broker

⾃动提交和⼿动提交offset,以及poll消息

⾃动提交:可能会导致消息丢失

手动提交

⼿动同步提交:在消费完消息后调⽤同步提交的⽅法,当集群返回ack前⼀直阻塞,返回ack后表示提交成功,执⾏之后的逻辑

⼿动异步提交:在消息消费完后提交,不需要等到集群ack,直接执⾏之后的逻辑,可以设置⼀个回调⽅法,供集群调⽤


⻓轮询poll消息

默认情况下,消费者⼀次会poll 500条消息。

//⼀次poll最⼤拉取消息的条数,可以根据消费速度的快慢来设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

代码中设置了⻓轮询的时间是1000毫秒

while (true) {
 /*
 * poll() API 是拉取消息的⻓轮询
 */
 ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
 for (ConsumerRecord<String, String> record : records) {
 System.out.printf("收到消息:partition = %d,offset = %d, key = %s,value = %s%n", record.partition(),record.offset(), record.key(), record.value());
 }

意味着:

  • 1秒内,如果这⼀次没有poll到500条,那么⻓轮询继续poll,要么到500条,要么到1s,那么直接执⾏for循环

  • 如果两次poll的间隔超过30s(for循环执行太久),触发rebalance机制,该消费者被踢出消费组。可以通过设置这个参数,让⼀次poll的消息条数少⼀点**

    //⼀次poll最⼤拉取消息的条数,可以根据消费速度的快慢来设置
     props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
     //如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能⼒过弱,将其踢出消费组。将分区分配给其他消费者。rebalance
     props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
    

消费者的健康状态检查

  • 消费者每隔1s向kafka集群发送⼼跳,集群发现如果有超过10s没有续约的消费者,将被踢出消费组,触发该消费组的rebalance机制,将该分区交给消费组⾥的其他消费者进⾏消费。
//consumer给broker发送⼼跳的间隔时间
 props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
 //kafka如果超过10秒没有收到消费者的⼼跳,则会把消费者踢出消费组,进⾏
rebalance,把分区分配给其他消费者。
 props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);

kafka集群中的controller、rebalance、HW

1.controller

作用:每个broker启动时会向zk创建⼀个临时序号节点,获得的序号最⼩的那个broker将会作为集群中的controller

  • 选举: 当集群中有⼀个副本的leader挂掉,需要在集群中选举出⼀个新的leader,选举的规则是从isr集合中最左边获得。当集群中有broker新增或减少,controller会同步信息给其他broker

  • 通知:当集群中有分区新增或减少,controller会同步信息给其他broker

2.rebalance机制

前提:消费组中的消费者没有指明分区来消费

触发的条件:当消费组中的消费者和分区的关系发⽣变化的时候

分区分配的策略:在rebalance之前,分区怎么分配会有这么三种策略

  • range:根据公示计算得到每个消费消费哪⼏个分区:前⾯的消费者是分区总数/消费者数量+1,之后的消费者是分区总数/消费者数量

    7个分区,a,b,c三个消费者,1,2,3分区a消费在;4,5分区 b消费者;6,7分区 c消费者 
    
  • 轮询:⼤家轮着来

  • sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之前的分配情况(节点3挂了,会将消息均摊加到1,2上)。如果这个策略没有开,那么就要进⾏全部的重新分配。建议开启。

3.HW(高水位)和LEO(日志末位位移)

每个broker都拿到这个消息,hw才会更新,消费者才能消费到这条消息,这样的⽬的是防⽌消息的丢失和重复消费

break-0break-1都有最新消息了,当break-0挂了,break-1有消息备份,防丢失(都没有会重复消息)
还未同步到break-1就消费了,如果此时break-0挂了,break-1变成主节点,会再次消费,导致重复消费


Kafka中的优化问题

消息丢失

⽣产者(confirm):把ack设成1或者all,并且设置同步的分区数>=2(主节点挂了,子节点还有备份数据)

消费者(ack):把⾃动提交改成手动提交

重复消费

幂等性原则,分布式锁,和主键id,乐观锁

顺序消费

⽣产者:保证消息按顺序消费,且消息不丢失——使⽤同步的发送(异步发送,由于网络等原因,消息是有可能乱序的),ack设置成⾮0的值

消费者:主题只能设置⼀个分区,同一个Partition中的消息是有序的,默认只有一个消费者组中的一个消费者可以消费(相当于1对1)(参考rabbitmq,在db内按顺序消费)

kafka的顺序消费使⽤场景不多,因为牺牲掉了性能,但是⽐如rocketmq在这⼀块有专⻔的功能已设计好。

消息积压

  • 使⽤多线程,充分利⽤机器的性能进⾏消费消息。

  • 优化业务代码,提升业务层⾯消费的性能。

  • 创建多个消费组,多个消费者,部署到其他机器上,⼀起消费,提⾼消费者的消费速度


Redis

img

redis基本命令

String数据结构

命令和场景:
img

redis中 key 是string类型,SDS simple dynamic string

String类型:redis的value指向一个redisObject对象,底层结构依据存储数据类型进行变化
- 整数 int,
- 字符则 embstr,长度超过44则为raw

  • embstr原理:cpu一次读64byte ,redisobject本身占用16byte,type占用4byte,只要数据不大于44byte,只要读取一次

list数据结

img

底层:quicklist双向链表

quicklist双向链表,每个节点对应一个node,里面存储压缩列表,每个节点对应一个压缩列表,将整个list切分成小份,同时quicklist的head指向头节点,tail指向尾节点,以实现快速的插入、删除和遍历操作

hash 数据结构

命令和场景:
img

  • 底层是哈希表(hashtable),k,v结构,key:数组+链表
  • 当冲突时,通过数组转链表(链表指针法,注意头插法)和扩容解决hash冲突

set 数据结构

底层是哈希表(hashtable)

命令和场景:
img

SRANDMEMBER:抽奖,共同好友

spop表示执行完之后从List中剔除,例如抽了1等奖用户不能再抽2等奖

img

求交集,并集,差集

img

添加,检查,获取

img


zset 数据结构

命令和场景:img
img
img

跳表(哈希表)

跳表数据结构:zskiplist节点:索引 + 数据(list是一个双向链表),优点:大数据情况下,查找效率提升明细

img

  • 跳表时间复杂度为logN,类似于二分查找,没加一层索引层节点个数减半

redis 常见问题

img


如何保证Redis和数据库的一致

1.先删缓存,再写数据库,高并发环境,删除缓存还没有写数据库,此时没缓存了,读取的脏数据到缓存中

-- 写操作不频繁场景适合

  • 延时双删(业界常用),先删缓存,然后写数据库,休眠一会,再次删除缓存(删脏缓存),写操作频繁,还是会有脏数

-- 始终只能保证一定时间内的最终一致性

2.先写数据库,再删缓存,如果数据库写完,缓存删除失败或还没来得及删,数据不一致

  • 给缓存设置一个过期时间。问题:过期时间内,缓存不会更新
  • 引入MQ重试机制,保证原子操作,数据库写入,MQ保证一定删除缓存,某个时间内不一致

PV、UV、IP分别是什么意思?

  • PV(Page View)访问量, 即页面浏览量或点击量
  • UV(Unique Visitor)独立访客,统计1天内访问某站点的用户数
  • IP(Internet Protocol)独立IP数,是指1天内多少个独立的IP浏览了页面,即统计不同的IP浏览用户数量

官方命令网址:http://redis.cn/commands.html


Scan命令

Redis Scan 命令用于迭代数据库中的数据库键,SCAN 基于游标的迭代器

每次调用完,返回下一次需要查询的游标参数, 如果新游标返回 0 表示迭代已结束。

SCAN cursor [MATCH pattern] [COUNT count]
  • cursor - 游标。
  • pattern - 匹配的模式。
  • count - 指定从数据集里返回多少元素,默认值为 10 。
    img

找出10w个key是以某个固定的已知的前缀开头的所有key

1)使用keys指令可以扫出指定模式的key列表。

如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表(指定取出条数),但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

redis 四种特殊的数据结构

  • help @命令

  • bloomFilter 布隆过滤器 位数组 + hash函数,缓存穿透判断key,底层bitmap 位图

    bf.add key element 添加
    bf.exists key element 判断是否存在
    bf.madd key element1 element2 ... 批量添加
    bf.mexists key element1 element2 ... 批量判断
    
  • HyperLogLog(超级日志) 计算页面的UV替代set实现去重和计数(内存消耗),不精确的去重计数方案(误差值在 0.81% 左右),HyperLogLog 只占用 12KB 的内存

    1)Redis对HyperLoglog的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐新超过了阈值时オ会一次性转变成稠空矩阵

    2)HyperLoglog是一种算法,并非redis独有,目的是做基数统计,故不是集合,不会保存元数据,只记录数量而不是数值,耗空间极小,支持输入非常体积的数据量,核心是基数估算算法,主要表现为计算时内的使用和数据合并的处理,最终数值存在一定误差

     奇数级:{1,3,5,5,5,7} 得到的结果是 {1,3,5,7} 基数为4,计数估计就是在误差可接受范围内,快速计算
     pfadd key 1 3 5 5 5 7  -- 新增
     pfcount key -- 基数为4
     pfmerge key1 key2 -- 将多个HyperLogLog合并为一个 HyperLogLog
    

    3)当数据集不多的时候,可以使用set集合处理,进行去重,交集啥,如果数据太大进行去重等,就需要使用Hyperloglog了,比如统计每天访问人数,注册ip数,实时uv,在线用户数,总访问ip数,但比如统计总访问量(不需要去重),不需要去重,那么使用string的incr即可

  • bitmap 统计千万级别用户每日登录情况,以及连续登录用户,或至少一天登录

    1)使用 bitmap 进行统计,set bit-key,它的数据结构是一个2^32-1bit的数组(512 M)字符串最大值,存放1/0数字

    2)如果登录设置为1即可,可以存放4亿多用户数,连续登录只需要将两天用按位与 “&” 运行即可如果结果为1则表示连续登录,月活则保持30个bitmap,然后进行与运算

  • GEO/GeoHash(地理索引) 计算附近的人,附近的商店,具体原理是将地球看成一个平面,并把二维坐标映射成一维(精度损失的原因)

    geoadd key longitude latitude element(后面可配置多个三元组) 添加元素
    geodist key element1 element2 unit 计算两个元素的距离
    geopos key element [element] 获取元素的位置
    geohash key element 获取元素hash
    georadiusbymember key element distanceValue unit count countValue ASC/DESC [withdist] [withhash] [withcoord] 获取元素附近的元素 可附加后面选项[距离][hash][坐标]
    georadius key longitude latitude distanceValue unit count countValue ASC/DESC [withdist] [withhash] [withcoord] 和上面一样只是元素改成了指定坐标值 
    

    img

  • Pipeline:

    可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。

  • Stream 消息队列

    消息ID的序列化生成
    消息遍历
    消息的阻塞和非阻塞读取
    消息的分组消费
    未完成消息的处理
    消息队列监控
    

redis中实现消息队列的几种方案

  • 基于List的 LPUSH+BRPOP 的实现

    1.使用rpush和lpush操作入队列,blpop和brpop操作出队列。
    队列为空时,lpop和rpop会一直空轮训,消耗资源;所以引入阻塞读blpop和brpop(b代表blocking),阻塞读在队列没有数据的时候进入休眠状态,一旦数据到来则立刻醒过来,消息延迟几乎为零。
    
    2.空闲连接的问题。如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop和brpop或抛出异常,所以在编写客户端消费者的时候要小心,如果捕获到异常,还有重试。
    
  • PUB/SUB,订阅/发布模式

    SUBSCRIBE,用于订阅信道
    PUBLISH,向信道发送消息
    UNSUBSCRIBE,取消订阅
    
    1.此模式允许生产者只生产一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由对应的消费组消费。
    
    2.典型的广播模式,一个消息可以发布到多个消费者
    
    3.生产者只生产一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由对应的消费组消费。若客户端不在线,则消息丢失,不能寻回,不适合做消息存储,擅长处理广播,即时通讯,即时反馈的业务
    
  • 基于Sorted-Set的实现

    Sortes Set(有序列表),zset 去重加有序(score)。内部实现是“跳跃表”。
    
    有序集合的方案是在自己确定消息顺ID时比较常用,使用集合成员的Score来作为消息ID,保证顺序,还可以保证消息ID的单调递增。制作一个有序的消息队列了。
    
    优点
    就是可以自定义消息ID,在消息ID有意义时,比较重要。
    
    缺点
    缺点也明显,不允许重复消息(因为是集合),同时消息ID确定有错误会导致消息的顺序出错。
    
  • Sorted-Set实现延时队列

使用sortedset,消息作为key,当前时间戳now做为score, 调用zadd来生产消息,消费者使用zrangbyscore获取5秒之前(now)的数据做轮询处理。

按照一定的时间间隔(5秒钟,延时5秒)轮询Sorted-Set,获取score值为当前时间5秒之前的元素,即代表这些任务已经到了执行时间

msg  time
key  score
a1   now  --> 此处获取5s前的消息
a2   now-1
a3   now-2
a4   now-3
a5   now-4
a6   now-5
  • 基于Stream类型的实现
img
Stream为redis 5.0后新增的数据结构。支持多播的可持久化消息队列,实现借鉴了Kafka设计。

1.它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的。每个Stream都有唯一的名称即Redis的key,首次使用xadd指令追加消息时自动创建。

2.每个Stream都可以挂多个消费组,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。

3.每个消费组(Consumer Group)的状态都是独立的,相互不受影响。同一份Stream内部的消息会被每个消费组都消费到。

4.同一个消费组(Consumer Group)可以挂接多个消费者(Consumer),这些消费者之间是竞争关系**,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。

5.消费者(Consumer)内部会有个状态变量pending_ids(PEL),消息ID列表,保存未被ack的消息,只有ack了,才去掉,防止消息丢失(如果一值没ack且消费者组很大会导致内存放大)

redis客户端

img

  • redisTemplate,redis的key序列化不要用java的,性能差,使用redis自己的StringRedisSerializer,value用JsonRedisSerializer
@Configuration
public class RedisConfig {
    @Resource
    private RedisConnectionFactory factory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JsonRedisSerializer<>(Object.class));
        return redisTemplate;
    }
}

Redis事务

ACID

  • 原子性(事务):单线程执行命令和事务机制,一次执行一个命令,lua是提交到服务器内部执行的,它也算是“一个命令”,所以是原子性的
  • 一致性:保证数据一致性,失败也会恢复执行
  • 隔离性:单线程执行,不存在事务冲突
  • 持久性:AOF,RDB

redis不支持事务回滚,但会检查命令语法错误,不支持检查逻辑错误(将多个命令打包成一个事务一次性执行,本质就是将一些命令拼接在一起执行而已)

  • watch,乐观锁,cas监控key被修改或删除,后续事务不会被执行,直到exec
  • multi,开启事务,总是返回ok,此事务没执行完毕,其他命令都不会执行,会放到一个队列中,直到exec命令调用才会执行
  • exec,按照命令执行顺序执行所有事务命令块并返回值,当被打断,返回空值nil
  • discard,清空事务队列,放弃执行事务
  • unwatch,取消watch对所有key的监控

redis保证原子性和一致性

思路一:单命令操作

Redis 提供了 INCR/DECR/SETNX 命令,Redis是单线程,可以保证原子性

思路二:加分布式锁

思路三:Lua脚本

多个操作写到一个 Lua 脚本中(Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性)


redis主从复制

执行slaveof命令或设置slaveof选项,服务器异步复制数据:首次全量同步(rdb),后续都是增量同步(aof),但如果偏移量超过缓存区从节点存主节点id不匹配也会是全量同步


redis集群

主从模式

img

读写分离,主写,读从,减少master节点压力

哨兵模式

img

M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。

哨兵组件的主要功能:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
Cluster(高可用)
  • 多主多从,去中心化:1份数据,分给多个主节点存储,从节点作为备用,复制主节点,不做读写操作,不提供服务,主节点挂了,才会就选举晋升。

    redis的哨兵集群方式,每个节点都保存相同的同步数据,可能会存在冗余的数据;其次只能允许有一个主的节点;属于中心化集群;
    
  • 支持动态扩容节点:这是我认为算是Rerdis Cluster最大的优点之一;

    核心原理:采用hash槽,预先分配16384个卡槽,并且将卡槽分配到具体Redis的节点,通过key进行crc16(key)%16384=卡槽,可以根据卡槽存到具体Redis节点,注意一个卡槽可以存放多个不同的key,只有主的节点才会分配卡槽,从节点没有卡槽
    
    卡槽作用: 决定key存放具体的服务器位置,从而实现均摊存放数据.类似我们的数据库中具体的分表, 优点:动态实现扩容和缩容;
    
     节点分配
     现在我们是三个主节点分别是:A, B, C 三个节点,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
    
    节点A覆盖05460;
    节点B覆盖546110922;
    节点C覆盖1092316383.
    
    获取数据
    如果存入一个值,按照redis cluster哈希槽的[算法](http://lib.csdn.net/base/datastructure): CRC16('key')384 = 6782。 那么就会把这个key 的存储分配到 B 上了。同样,当我连接(A,B,C)任何一个节点想获取'key'这个key时,也会这样的算法,然后内部跳转到B节点上获取数据
    
    新增一个主节点
     新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上,我会在接下来的实践中实验。大致就会变成这样:
    
    节点A覆盖1365-5460
    节点B覆盖6827-10922
    节点C覆盖12288-16383
    节点D覆盖0-1364,5461-6826,10923-12287
    同样删除一个节点也是类似,移动完成后就可以删除这个节点了。
    
  • 节点之间相互通信,相互选举,具有自我故障检测,故障转移的特点,不再依赖sentinel:准确来说是主节点之间相互“监督”,保证及时故障转移(使用二进制协议优化传输速度和带宽)

    故障检测

    1) 集群中每个节点都会定期地向集群中的其他节点发送PING消息,以此检测对方是否在线;
    
    2) 如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将PING消息节点标记为疑似下线(possible fail,PFAIL)。
    
    3) 如果在集群中,超过半数以上负责处理槽的主节点都将某个节点X标记为PFAIL,则某个主节点就会将这个主节点X就会被标记为已下线(FAIL),并且广播到这条消息,这样其他所有的节点都会立即将主节点X标记为FAIL。
    

     

    故障转移:当一个从节点发现自己正在复制的主节点下线时,从节点将开始对下线主节点进行

    1) 在该下线主节点的所有从节点中,选择一个做主节点
    2) 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点;
    3) 新的主节点会撤销对所有对已下线主节点的槽指派,并将这些槽全部派给自己。
    4) 新的主节点向集群广播一条PONG消息,让其他节点知道“我已经变成主节点了,并且我会接管已下线节点负责的处理的槽”;
    5) 新主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
    

      

    基于Raft算法的leader选举机制

    1) 当从节点发现自己正在复制的主节点进入已下线状态时,
    2) 从节点会向集群广播消息:要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
    3) 每个主节点只有一次投票机会,所有有N个主节点的话,那么具有大于N/2+1张支持票的从节点只有一个。
    

Redis集群最大节点个数是多少?

16384个


为什么要做Redis分区?

分区可以让Redis管理更大的内存(集群),Redis将可以使用多台机器的内存。否则你最多只能使用一台机器的内存

你知道有哪些Redis分区实现方案?

客户端分区:就是在客户端就决定数据会被存储到哪个redis节点或者从哪个redis节点读取

代理分区: 客户端将请求发送给代理,代理决定去哪个节点写数据或者读数据


查看和设置内存大小?

redis.conf文件或info命令查看内存大小maxmemory(byte),64位不限大小(32位 3G),一般设置物理内存3/4
img

  • 设置内存可以修改配置文件或命令设置
    img
  • info memory命令:used_memory_human:26.55M //数据占用了多少内存

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?

redis内存数据集大小上升到一定大小的时候,就会施行数据过期和淘汰策略

key 失效机制

Redis 的 key 可以设置过期时间,过期后 Redis 采用主动和被动结合的失效机制,一个是在访问时触发被动删除(惰性删除),另一种是定期的主动删除(定期删除)。

定期+惰性+内存淘汰

key过期策略

  • 定时删除(redis的每个值设置过期时间),过期立即删除会使内存干净,但导致一直需要消耗cpu资源(内存友好,cpu不友好)
  • 惰性删除,过期但需要被访问才进行删除(或手动flushdb命令),可能导致大量过期但未被删除的key(cpu友好,内存不友好)
  • 总结:执行删除太多太久会影响退化定时,太少太短则像惰性一样浪费内存,所以要合理设置执行时长和执行频率

内存打满淘汰策略?

img

  • redis内存淘汰策略,默认使用noeviction,不进行删除,内存打满会直接oom,命令执行失败

  • 1、volatile-lru:设置过期时间最近最少使用

  • 2、volatile-ttl:设置过期时间将要过期

  • 3、volatile-random:设置过期时间任意淘汰

  • 4、allkeys-lru:最近最少使用的数据淘汰

  • 5、allkeys-random:任意淘汰

  • 6、no-enviction(驱逐):不删除

  • 配置 maxmemory-policy allkeys-lru //key最近最久未使用

    命令:config set maxmemory-policy allkeys-lru
    

linkHashMap本身能满足lru算法,自带排序和过期淘汰最久未使用数据

Redis 和 memcache 有什么区别?

  • 持久化:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部份存在硬盘上,这样能保证数据的持久性
  • 数据支持类型:memcache 对数据类型支持相对简单;Redis 有复杂的数据类型
  • value 值大小:Redis最大可以达到 512mb;memcache 只有 1mb

Redis 为什么是单线程的?

因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。

Redis基于Reactor模式多路复用技术,

Redis线程模型

  • 基于Reactor(响应式)模式,文件事件处理器(file event handler),它是单线程,所以Redis是单线程模型
  • 基于非阻塞(selector)IO多路复用,避免多线程频繁切换上下文带来消耗

redis 6.0 支持多线程

业务简单,适合用单线程,而很复杂的业务比如需要IO等待的,使用多线程

线程安全,Redis 的多线程部分只是用来处理网络数据的读写和协议解析(网络通信过程捕获网络中的数据信息,将数据包解码),执行命令仍然是单线程顺序执行。

未默认开启,在conf文件进行配置

io-threads-do-reads yes
io-threads 线程数

官方建议:4 核的机器建议设置为 2 或 3 个线程,线程数一定要小于机器核数

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案
  • 直接写个缓存刷新页面,上线时手工操作一下;
  • 数据量不大,可以在项目启动的时候自动进行加载;
  • 定时刷新缓存;

缓存降级

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

Redis如何做大量数据插入?

Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作,即将支持Redis协议的文本文件直接通过pipe导入到服务端。

  1. 新建一个文本文件,包含redis命令

缓存热点key

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

星络充电下单的时候有用到分布式锁拿字段数据,防止缓存击穿

解决方案:分布式锁

缓存穿透,缓存雪崩,缓存击穿

缓存穿透是指恶意查询一个不存在的数据,由于缓存无法命中,直接访问DB, 解决:采用布隆过滤器白名单,将可能存在数据hash到一个足够大的bitmap中一定不存在数据肯定被拦截掉(虽然存在误判,可以控制误判hash值相同,本身不相同)

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。解决:原有的失效时间基础上增加一个随机值,每一个缓存的过期时间的重复率就会降低。

缓存击穿是指key还没有缓存或刚好失效,突然大量请求访问key,会直接击穿打到数据库上,我们称为缓存击穿,使用分布式锁解决(只有一个请求能获得锁,执行完毕将数据加载到缓存才释放锁)

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
新建一张表id,lock,createdate,lock唯一标志,1000个请求过来,只有一个请求插入成功,插入成功的这笔才能获取锁执行业务,其他请求等这个请求处理完成释放锁,反常重试获取才能执行
  1. 基于缓存(Redis等)
 setnx执行成功,获取锁,才能执行业务逻辑,成功释放锁,其他请求抢锁资源
  1. 基于Zookeeper
    每一种分布式锁解决方案都有各自的优缺点:
  2. 性能:redis最高
  3. 可靠性:zookeeper最高,自动释放锁(临时节点,端口连接自动释放)

这里,我们就基于redis实现分布式锁。
img

实现原理:借助于redis中的命令setnx(key, value),同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。

问题1:setnx刚好获取到锁,业务逻辑出现异常(或宕机),导致锁无法释放

解决:设置过期时间,自动释放锁。 --》在set时指定过期时间

问题2:多线程情况可能会释放其他服务器的锁
解决:setnx获取锁时,设置一个指定的唯一值(例如:用户id);只能释放自己的锁

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  • 加锁和解锁必须具有原子性=》lua脚本/或者redis的原子操作(单线程)

img

//
1.设置过期时间;2.设置必须解锁;3.设置只能解除自己的锁;4.设置还未执行完程序给将要过期锁自动续命
try{
Rlock redissonLock = redissonLock(lockKey);
redis.locksonLock.lock();=》//setIfAbsent(lockKey,id,30)
//默认续期30s
...
}finally{
  redisonLock.unlock();
}

实际的核心代码,其实就是一段lua脚本

  • 判断key是否存在,不存在设置,并设置过期时间(默认 30s)

  • 问题:如果设置锁和设置超时时间没有同时成功,会导致死锁

    解决:redis是c语言编写的,c语言执行保证此段lua脚本具有原子性(相当于这里是一句代码),这段lua脚本要么都执行成功,要么都不成功

看门狗机制(锁续命机制源码):
img

  • 如果存在key再设置过期时间
  • 每(1/3 * 30s )10s执行一次

从cap角度解析Redlock和zookeeper锁异同:

zookeeper主从结构:cp数据一致性,半数节点同步成功,才会告知leader成功,主从节点切换,拥有最新数据的节点才能成为leader节点

redis主从切换导致锁丢失:主节点获得锁(获取成功)还未同步到从节点就宕机,此时锁丢失了,而且从节点还能再次获得这个锁

解决方案:Redlock:半数节点加锁成功才算成功,很少用可能有bug,还不如zookeeper稳定(借鉴zookeeper)
img

分布式锁导致的性能下降问题(同步串行化):

  • 分段锁,或者说降低锁粒度(最低一个key一段)
  • 高并发场景,无锁读写redis,lua脚本实现原子操作或单命令原子操作
    单纯redis命令是单线程,能保证原子性,但是如果想将业务代码结合也是原子性,那么就要用分布式锁(底层也是lua脚本)或lua脚本

redis的bigkey问题:

即某一个key的value过于大,因为redis执行命令是单线程的,拿取这个大key时间过长,阻塞后续请求,影响性能

一个字符串类型的值能存储最大容量是多少?512M

解决方案:分段存储,将大key拆成小key


Redis 支持的 Java 客户端都有哪些?

支持的 Java 客户端有 Redisson、jedis、lettuce 等,官方推荐使用Redisson。

jedis 和 Redisson 有哪些区别?

  • jedis:提供了比较全面的 Redis 命令的支持
  • Redisson:实现了分布式和可扩展的 Java 数据结构,与 jedis 相比 Redisson 的功能相对简单,不支持排序、事务、管道、分区等 Redis 特性。

Redis 持久化有几种方式?

Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。

1.配置参数,指定时间内更改次数超过阈值会执行快照

save 900 1
save 300 10
save 60 10000

2.手动执行bgsave/save 显示触发生成快照(save执行期间会导致redis不可用)

3.持久化过程,是通过fork子进程来完成的,不影响redis使用

img

优点:恢复数据,直接解析临时文件(rdb二进制),生产数据存储内存中,效率高,恢复快

缺点:RDB(Redis Database)定时快照存储,最后一次数据可能丢失,如果文件太大,执行会比较影响性能

  • AOF(Append Only File):每一个收到的写命令都通过write函数追加到文件中
    img

    优点:不存在最后一次丢失问题

    缺点:每收到一个写命令就执行一次,占用更多磁盘io

混合持久化
img

实际场景中:启动的时候使用RDB方式加载整个快照,之后就用AOF方式进行追加
img

Redis 如何做内存优化?

1.缩减键值对,key满足业务,越短越好,value用高效序列化工具:protostuff,kryo,字符串也序列化
2.对象共享池,Redis内存维护一个[0-9999]的整数对象池,不需要另外创建和内存开销,尽量使用整数对象以节省内存
3.利用好Redis的哈希类型,hash存储数据比单独每个字符串每个key存储更节约内存

Redis 常见的性能问题有哪些?该如何解决?

  • 主节点不要进行持久化,因为会阻塞主线程的工作(从节点进行rdb持久化)
  • Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性主从库最好在同一个局域网内
  • 内存不足:增加Redis实例的内存,优化Redi 的数据结构。
  • 数据淘汰策略选择不当:应该根据业务需求选择合适的淘汰策略,如 LRU、LFU 等。
  • 键命令设计不合理:尽量避免设置较长的key

Redis缓存怎么做扩容?

如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容(redis集群是依据哈希槽均分节点,动态扩缩容)。


一致性哈希

问题:普通哈希key确定访问节点,当出现增加机器或减少机器的时候,需要从新运算分配节点

img

一致性Hash算法将整个Hash空间组织成一个虚拟的圆环,Hash函数的值空间为0 ~ 2^32 - 1(一个32位无符号整型),整个哈希环如下:

img

节点宕机,比如c节点,只会影响环上此服务器到前一台服务器之间的数据(objectc,不会影响a,b)

img

节点新增,c定位到x服务器上,ab不影响

img

数据倾斜问题

为了解决数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务器节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点
所以加入虚拟节点之后,即使在服务节点很少的情况下,也能做到数据的均匀分布。

img

Redis 6有哪些方面的提升?

  • 基于内存而且使用IO多路复用技术,单线程速度很快
  • 引入多线程用以优化某些io操作
  • 读写网络占用大量cpu时间,多线程模式会使得性能很大提升,执行命令仍然是单线程,不需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。

解决缓存穿透问题?

布隆过滤器

Redis当中有一种数据结构就是位图,布隆过滤器其中重要的实现就是位图的实现

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

img

  • 查询和插入都是经过多个hash函数,得到的值放到多个bit位
  • 设计多个hash函数,bit位置二进制都为1才表示“你好”存在

img

  • 多个数据经过hash值可能都为1,我们无法确定bit=2位置1是表示哪个数据,所以不能进行删除数据,因为比如想删除你好,可能hello的bit也在2,也会被删除
  • 优点,计算哈希值,插入二进制数组下标,占用空间非常小(bitmap),查询和插入数据非常快,时间复杂度是O(K),K表示多个hash函数,如果一个hash函数就是O(1)
  • 保密性非常好,存储都是二进制数据
  • 缺点,无法进行删除操作,存在误判情况,比如你好和hello的hash结果下标都是2,此时只有数组中只有“你好”,但是hello哈希下标也是2,所以会存在误判情况,认为数组中存在“hello”
    img

布隆过滤器的如何删除
本身不支持删除,如果需要删除,有一种取巧的方式
将删除位标记为已删除,但不真正删除(后续通过其他业务来恢复误判的key),重新建立一个布隆过滤器,将原过滤器中没有被删除的插入新的过滤器中,对于共享位确实存在误删,所以布隆过滤器只适用于不需要精确删除的场景,例如对大量数据进行快速去重和查询

扩展

google Bloom 封装了布隆过滤器,可以设置误判,比如原有1000000个数,在插入100000个数,设置误判率为0.01,查询得出误判率为1031接近0.01,但是如果设置误判率无限小,那么此时运算时间会非常久,性能更差

img

如果发生过多的哈希碰撞,就会影响到判断的准确性,所以为了减少哈希碰撞,我们一般会综合考虑以下2个因素:

1、增大位图数组的大小(数组越大,占用内存越大)。

2、增加哈希函数的次数(多次hash运算,cpu消耗更多,时间更长)。

误判率原理:

  • 误差率越小,占用空间越大,hash函数越多,每个hash函数算法不一样,算出来的hash位置也是不同的,hash函数越多,算出来位置越多,数据不同,但hash值相同的概率就越低,误差率减小,但hash值也增多,那么占用二进制下标也增多,所以内存也会增多

防止缓存穿透:

img

  • 判断key如果存在布隆过滤器中就拦截,否则没有就设置为null

白名单

img

  • (1) key在布隆过滤器白名单才允许通过,所有的参数都需要放入布隆过滤器和redis中
  • 缺点:(4) 表示db中也无key,属于布隆过滤器误判,会导致穿透缓存和DB,但是误判概率是非常小的
  • 使用场景
    img

黑名单

img

  • 与白名单很相似,差别就是布隆过滤器中存在,直接拦截返回,还有如果redis和db中都不存在,直接加入到布隆过滤器黑名单中
  • 缺点:黑名单最开始是没有数据的,只有断定为非法请求key才会存入黑名单中,所以最开始请求key会缓存穿透到DB
  • 使用场景
    img

问题:40亿数据中找一个数据,限制在1G内存中寻找

背景:一个数字,64位机器上,需要8个byte存储,64个bit(进制位),而bitmap上只需要一个bit即可存放
hashmap中运算,一个数字(long,double)类型,对应8个byte,需要32G内存
如果放bitmap中,一bit存放一个数字,那么40亿数字,bitmap 则只需要 0.5G,然后通过位运算找出这个数:比如999999 >>> 2,相当于每次除以2,通过一步步移位找出来


分布式事务

场景:1.多服务;2.多数据库;3.分库分表

  • 2PC预提交锁定所有资源,直到提交完成,性能很差
  • TCC加了中间状态try,cancel,不需要锁定所有资源,性能提高许多,但是需要自己开发一些代码
  • Seata 的 TA直接执行完毕并记录undolog日志,方便回滚还原,只锁定涉及的资源

2PC 两阶段提交(atomikos框架),协议(XA/JTA)

预提交 -》提交

  • 预提交,提交阶段,sql执行完毕并锁定所有服务资源,等待协调器通知提交/回滚(通知失败(概率很小),重试预提交是否成功,否就预提交失败)

  • 极端,一直执行失败:log日志记录,定时任务补偿,人工处理

2PC 两阶段补偿型方案TCC(Try-Confirm-Cancel)

try(预操作,比如冻结库存) -> commit -> cancel(不管try失败还是commit失败,马上cancel)


  • 对比2PC,TCC虽然开发api更多(每个api需要开发 try api,confirm api,cancel api),但是锁粒度减小了(锁定执行服务资源),并且执行完可以释放锁资源(库存服务更新到冻结状态,不锁定资源),对于整个系统,高并发场景,大大提高了并发能力

分布式事务框架-seata
Seata有3个基本组件:

  • Transaction Coordinator(TC):事务协调器,管理请求调用中整个微服务链路的事务,通过全局唯一的 XID(事务链路id),决定全局事务最终统一提交或回滚。
  • Transaction Manager(TM):事务管理器,管理单个服务中事务,要注册到TC上。
  • Resource Manager(RM):资源管理器,管理单个服务中的分支事务

全局事务与分支事务:

Seata管理分布式事务的典型生命周期:
seata:事务协调器,分支事务都上传协调器,统一决断,AT模式,失败回滚undolog日志,AP一致性要求非常高,牺牲性能,蚂蚁金额这样金额系统

  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  • XID 在微服务调用链路的上下文中传播
  • RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖
  • TM 向 TC 发起针对 XID 的全局提交或回滚决议
  • TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

回滚策略:每次分支事务提交都插入操作日志到undolog中,也不锁定数据库资源,如果最后结果时回滚的话,每个微服务依据每个undolog日志回滚自己数据库数据即可

seata支持多种分布式事务解决模式,包括AT(默认)、TCC、SAGA、XA等。(XA模式开发中)

AT:两阶段演变,用户无需关心分布式事务的提交与回滚,事务问题交由seata进行管理,实现了无侵入的分布式事务解决方案(基于关系型数据库,通过jdbc访问数据库,只需要一个主键即可)
一阶段
seata会解析业务SQL,解析找到要更新的业务数据,并保存为前置快照,然后执行业务SQL,并把更新后的数据保存为后置快照(插入日志)。最后生成行锁。以上操作均在同一个数据库事务中保证了原子性。
二阶段
提交:全局事务成功提交则只需要删除快照数据和行锁,完成数据清理
回滚:数据校验,避免出现脏写,然后通过前置快照进行数据还原,最后删除快照信息和行锁,完成数据清理(通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录)

TCC模式:两阶段补偿方案,对应方法是Try、Confirm及Cancel,需要自己实现接口,对代码的侵入性比较强,同时需要考虑并发控制和异常控制

XA模式: 支持已实现XA接口的数据库的XA模式(mysql,oracle都需要)

SAGA模式: 为长事务提供有效的解决方案

Seata中全局事务xid怎么通过Feign进行,通过feign的header进行传递(类SeataFeignClient中将request的xid转移到header上)

线程池

线程池创建有四种方式,最核心的是最后一种:

  • newSingleThreadExecutor:单线程池,工作线程数目被限制为1,无界队列LinkedBlockingQueue(先进先出),任务被顺序执行没有多线程场景。)

  • newFixedThreadPool:固定线程池,可复用的固定线程数,无界队列LinkedBlockingQueue,任何时候最多有 nThreads 个工作线程是活动的。最多只有n个线程执行任务(负载比较重的服务器

  • newCachedThreadPool:可伸缩线程池,用来处理大量短时间工作任务的线程池,没有容量的有界队列SynchronousQueue,注意会无限创建线程执行任务,极端情况会出现oom

  • newScheduledThreadPool:它主要用来在给定的延迟之后运行任务,或者定期执行任务。使用有界队列DelayQueue

  • ThreadPoolExecutor:是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

  • 无界队列LinkedBlockingQueue、DelayQueue队列无限大,有oom风险

  • 无容量队列SynchronousQueue,线程无限被创建,也有oom风险

  • 所以推荐通过ThreadPoolExecutor自定义线程池,使用有界固定容量队列安全,当队列放满,就触发拒绝策略

CPU密集型 和 IO密集型

  • 计算密集型
    计算密集型任务的特点是要进行大量的计算,消耗CPU资源,。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,核心线程数等于CPU的核心数。

  • IO密集型
    网络、磁盘 IO (与DB、缓存),一旦IO,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,核心线程数应该远大于核心数(10~20倍)。

阻塞队列与非阻塞队列

  • 阻塞队列 ArrayBlockingQueue 底层通过lock实现线程安全
  • 非阻塞队列 concurrentLinkedQueue 底层使用cas乐观锁实现线程安全

线程增长策略

ThreadPoolExecutor 最全参数的构造方法:

  • corePoolSize:核心线程数;
  • maximumPoolSize:线程池的最大线程数(核心+非核心线程);
  • keepAliveTime:核心线程数之外的线程,最大空闲存活的时长;
  • unit:keepAliveTime 的时间单位;
  • workQueue:线程池的任务等待队列(jdk自带的线程池都是无界或无容量的,自定义可以设置成有界的线程池,避免oom);
  • threadFractory:线程工厂,用来为线程池创建线程(自己指定线程怎么构建,比如名字,是否用户线程,还是守护线程,优先级,自定义捕捉异常,更好控制线程对象);
  • handler:拒绝策略,当线程池无法处理任务时的拒绝方式;

线程池中线程的增长策略相关参数

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数;
  • workQueue:等待任务队列;

img

  • 注意只有队列满的时候才会创建非核心线程,但是一个能够满的队列,它的前提是必须是一个有界队列
  • 如果没有配置拒绝策略默认就会抛出RejectedExecutionException 异常

线程回收策略

线程池中线程的收缩策略,和以下几个参数相关:

  • corePoolSize:核心线程数;
  • maximumPoolSize:线程池的最大线程数;
  • keepAliveTime:核心线程数之外的线程,空闲存活的时长;
  • unit:keepAliveTime 的时间单位;

当线程数超过核心线程数且处于空闲状态,且空闲时间超过 keepAliveTime&unit 配置的时长,非核心线程会被回收,核心线程不会回收(线程池无法区分哪些线程是核心或非核心线程,他只控制数量达到 corePoolSize数)

核心线程数中的线程,通过 allowCoreThreadTimeOut(true) 方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime&unit 配置的时间,也将其回收掉( keepAliveTime 不能为 0。)。

面试官:"聊聊线程池中的线程的增长/回收策略?"

总结

提交任务优先级:先核心线程,然后阻塞队列,满了,然后才创建最大线程;

执行任务优先级:先核心线程任务,然后非核心线程任务,之后才是阻塞队列中的任务

四大拒绝策略

  • AbortPolicy : 抛出异常
  • CallerRunsPolicy : 丢回调用者(不会丢弃任务,性能可能会急剧下)
  • DiscardOldestPolicy : 丢给最先执行线程(丢弃原来的任务,然后尝试执行试试)
  • DiscardPolicy : 丢弃

springboot中线程池类

img

img

  • ThreadPoolTaskExecutor springboot中线程池类
  • queueCapacity:阻塞(缓存)队列 ,设置大小
  • 动态调整线程数量:线程池中线程数量超过核心线程数终止空闲时间超时的线程

常用的几种队列

  • ArrayBlockingQueue(有界队列):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的,方便查询和更新。

  • LinkedBlockingQueue(无界队列):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的,方便删除和新增。

  • PriorityBlockingQueue(优先队列):一个具有优先级的无限阻塞队列。,类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。

  • SynchronizedQueue(同步队列):是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue

  • DelayQueue 无界延时队列

  • ArrayBlockingQueue跟LinkedBlockingQueue的区别

  • 1.队列中的锁的实现不同

    ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;
    
    LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock
    

    2.在生产或消费时操作不同

    ArrayBlockingQueue基于数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例;

    LinkedBlockingQueue基于链表,在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。

    3.队列大小初始化方式不同

    ArrayBlockingQueue是有界的,必须指定队列的大小;

    LinkedBlockingQueue是无界的,默认是Integer.MAX_VALUE。可以指定队列大小,从而成为有界的。

    注意:

    • 在使用LinkedBlockingQueue时,若用默认大小且当生产速度大于消费速度时候,有可能会内存溢出
    • 入队操作,LinkedBlockingQueue的消耗更大

扩展

  • 等待队列还会影响拒绝策略,如果是无界队列,非核心线程永远用不到,拒绝策略永远不会执行

  • 工作线程数量达到最大线程数,等待队列也满了,拒绝策略才能生效。

核心线程数可以被「预热」

线程池中的线程默认是根据任务来增长的,但可以提前准备好线程池的核心线程,来应对突然的高并发任务,例如在抢购系统中就经常有这样的需要。

prestartCoreThread() 或者 prestartAllCoreThreads() 来提前创建核心线程,这种方式被我们称为「预热」。

对于需要无界队列的场景,怎么办?

maximumPoolSize 就是无效的。可以参考 Executors 中 newFixedThreadPool() 创建线程池的过程,将 corePoolSize 和 maximumPoolSize 保持一致即可

面试官:"聊聊线程池中的线程的增长/回收策略?"

核心线程数=最大线程数,只有增长到这个数量才会将任务放入等待队列,来保证我们配置的线程数量都得到了使用。

线程池是公平的吗?

不公平的。

不提线程池中线程执行任务是通过系统去调度的,这一点就决定了任务的执行顺序是无法保证的,这就是是非公平的。另外只从线程池本身的角度来看,我们只看提交的任务顺序来看,它也是非公平的。

首先前面到的任务,如果线程池的核心线程已经分配出去了,此时这个任务就会进入任务队列,那么如果任务队列满了之后,新到的任务会直接由线程池新创建线程去处理,直到线程数达到最大线程数。

那么此时,任务队列中的任务,虽然先添加进线程池等待处理,但执行任务是,先核心线程任务,然后非核心线程任务,之后才是等待队列中的任务

jvm面试题

jvm基础

JVM从指定的位置加载class文件(类加载器:加载,验证、解析、初始化),加载到jvm内存中(运行时数据库),再通过执行引擎执行(转换成底层系统指令(01),调用本地库接口)

img

  • 类加载器:Java代码转换成字节码
  • 运行时数据区:字节码加载到内存中
  • 执行引擎:将字节码翻译成底层系统指令,再交由 CPU 去执行
  • 本地库接口:这个过程中需要调用其他语言的本地库接口

运行时数据区

  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存

  • 方法区(Methed Area):用于存储类信息、常量、静态变量、即时编译后的代码等数据。

  • Java 虚拟机栈:储局部变量表、操作数栈、动态链接、方法出口等信息,没有GC,但有OOM;

    • 栈(线程),存放线程中的局部变量,每个线程都会分配一个独立的栈空间,一个方法一个栈帧(FILO)
    • 一个方法对应一个栈帧,方法进入出去相当于进栈出栈,先进后出
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的

  • 程序计数器(Program Counter Register):记录当前线程之前执行到的位置,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复,等基础功能,都需要依赖这个计数器来完成,既没有GC,也没有OOM(内存溢出);

img

共享和私有

  • 堆和方法区(永久代,元数据区)线程共享的
  • 程序寄程器,栈,本地栈线程私有的

堆空间安全问题,涉及线程共享部分和私有部分
img
img

1.新对象存放eden,当eden满了,就进行ygc,未回收的放入s1

2.当eden再次满了,继续进行ygc,此时eden中和上一次s1中残留本次也未能回收的,就会复制到s2,年龄晋升1,当到达到15,进入老年代

3.如果最开始进来就是大对象(大于eden一半),会直接进入老年代

4.当老年代满了,就进行fullgc,会出现stw,大大影响性能,当老年代频繁fullgc,系统就会越来越卡顿,用户体验很差
img


方法区

img
img

堆栈的区别

  • 功能方面:堆是用来存放对象的,栈是用来执行程序的。
  • 共享性:堆是线程共享的,栈是线程私有的。
  • 空间大小:堆大小远远大于栈。

怎么判断对象是否可以被回收?

一般有两种方法来判断:

  • 引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
  • 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。

Java 中都有哪些引用类型?

  • 强引用:发生 gc 的时候不会被回收
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收
  • 虚引用:无法通过虚引用获得对象(随时可能被回收),用 PhantomReference 实现虚引用,gc时候随时会回收掉,虚引用的用途是跟踪gc过程并在gc时收到一个系统通知

JVM 有哪些垃圾回收算法?

  • 标记-清除算法:标记无用对象,清除回收,效率低,清除后的空间不连续,会产生内存碎片。
    img

  • 标记-压缩算法(老年代):标记无用对象,将还需保留的对象压缩到一块,然后清楚掉其他,效率低,解决内存不连续问题。
    img

  • 复制算法(新生代):将内存空间均分两份,将有引用对象复制到另一块,这一块直接清理,效率最高,但利用率只有50%。

    • s0 和s1区交换是用就是复制算法,a区存活对象按顺序到b区,a区其他全部回收清除(指针碰撞)
      img
  • 分代算法:分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 可以控制一次回收多少个小区间,合理地回收若干个小区间, 从而减少一次GC所产生的停顿。

    img


新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

  • 新生代回收器:Serial(单线程)、ParNew(多线程)、Parallel Scavenge多线程
  • 老年代回收器:Serial Old(单线程)、Parallel Old(多线程)、CMS(牺牲吞吐量,但短停顿时间)
  • 整堆回收器:G1(兼顾吞吐量和停顿时间,但耗内存)

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低老年代回收器一般采用的是标记-整理的算法进行垃圾回收

img


详细介绍一下 CMS 垃圾回收器?

牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。参数加上“-XX:+UseConcMarkSweepGC”指定使用 CMS 垃圾回收器。
img

CMS 使用的是标记-清除的算法(无压缩)实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低

  • 初始标记,标记能直接关联到我的对象(直接关联对象非常小,速度快,比如main方法中test()方法,不需要深入),stw
  • 并发标记工作线程和垃圾回收线程同时执行
  • 重新标记并发期间工作线程在执行,需要重新标记才能确定不会产生新的垃圾或垃圾又变成非垃圾,stw
  • 并发清除 清除掉标记阶段已经死亡的对象,释放内存空间(产生新的浮动垃圾)

JVM调优实战

gc实例图:
首先对象到Eden,如果放不下,就进行minor gc,此时eden中还被引用数据不能gc,会移动到s1,下一次如果再进行gc,如果s1不能回收,就会换到s2(来回交换,直到回收或晋升),同时年龄加特殊情况:如果对象大于Eden一半直接到老年区;如果minor gc之后Eden或s1还是放不下直接到老年区;

img


调优参数

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。
  • -Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

目的,较少full gc,减少用户线程暂停,full gc会导致stw停止(专心gc,否则在gc的时候还会不停的制造垃圾)


调优命令

1.1、jvm堆最大值和最小值

-Xms6m -Xmx6m

1.2、jstat命令

jstat -gcutil ID 3000
ps -eo pid,tty,user,comm,lstart,etime | grep java进程ID

jstat是一个可以用于观察Java应用程序运行时信息的工具,它的功能非常强大,可以通过它查看堆信息的详细情况,它的基本使用方法为:

1.3、jinfo命令

jinfo可以用来查看正在运行的Java应用程序的扩展参数,甚至在运行时修改部分参数,它的基本语法为:

jinfo <option> <pid>

jinfo可以查看运行时参数:

jinfo -flag MaxTenuringThreshold ``31518``-XX:MaxTenuringThreshold=``15

1.4、jmap命令

jmap命令主要用于生成堆快照文件,它的使用方法如下:

 jmap -dump:format=b,file=/apps/provider/ecommerce/cdiscount/dump1.hprof 22864

获得堆快照文件之后,我们可以使用多种工具对文件进行分析,例如jhat,visual vm等。

1.5、jhat命令

使用jhat工具可以分析Java应用程序的堆快照文件,使用命令如下:

> jhat heap.hprof``Reading from heap.hprof...``Dump file created Tue Nov ``11` `06``:``02``:``05` `CST

1.6、jstack命令

jstack可用于导出Java应用程序的线程堆栈信息,语法为:

jstack -l <pid>
jstack可以**检测死锁**

img

cpu标高,jstack/arthas 记录哪个线程最高的

  • gc线程,频发gc,查看日志,导致垃圾回收不够
  • 业务线程

代码规范

JVM中十种内存溢出的解决方法:

https://baijiahao.baidu.com/s?id=1618742113810768761&wfr=spider&for=pc

1.变量不合理的作用域

int msg ="";
public void test(){
  msg ="123";
} //test方法结束,msg也没能回收

2.静态集合类

HashMap等,长生命周期的对象持有生命周期对象(key,value)的引用

img

  • 如果真的想用Map做缓存的时候,使用weakMap

    Map map = new WeakMap();  //弱引用继承WeakReference,下一次gc时候会进行回收它
    ``
    
  • 3.数据库,线程池,网络,IO连接需要关闭,或者使用默认自动收缩线程池,非核心线程最大

    try{
         ExecutorService executorService = Executors.newCachedThreadPool();
         executorService.execute(()->{
            System.out.println("aaaa");
         });
    } catch finally { //关闭连接 }
    
  • 4.改变hash值
    对象存入集合之后,如果对象属性改变即hash值变了,将不能被移除(依据hash直进行移除)img

  • 5.引用无法回收

   /**
     * 堆内存溢出 java.lang.OutOfMemoryError: Java heap space
     * gc 回收速度 赶不上生产垃圾速度,或者存在引用导致无法回收,比如static map(作临时缓存)被其他生命周期长对象引用,导致无法回收
     * WeakHashMap弱引用,弱引用,下一次gc时候会进行回收它
     */
    @Test
    public void test() {
        List<PersonEntity> personEntities = new ArrayList<>();
        for (int i = 0; ; i++) {
            PersonEntity personEntity = new PersonEntity();
            personEntity.setName("hello panda");
            personEntity.setAge("18");
            personEntities.add(personEntity);
            log.info("值=" + i);
        }
    }
  • 6.无限递归或栈太深
    /**
     * 栈内存溢出 java.lang.StackOverflowError
     * 无限递归,死循环,方法嵌套太多,栈深度太深
     */
    @Test
    public void test01() {
//        System.out.println(count++);
        test01();
    }

 //实现f(n):求n步台阶,一共有几种走法
   public int f(int n){
      if(n<1){
         throw new IllegalArgumentException(n + "不能小于1");
      }
      if(n==1 || n==2){
         return n;
      }
      return f(n-2) + f(n-1);
   }

  public int loop(int n){
      if(n<1){
         throw new IllegalArgumentException(n + "不能小于1");
      }
      if(n==1 || n==2){
         return n;
      }
      
      int f1 = 1;//初始化为走到第二级台阶的走法
      int f2 = 2;//初始化为走到第一级台阶的走法
      int sum = 0;
      
      for(int i=3; i<=n; i++){
         sum = f1 + f2;
         f1 = f2;
         f2 = sum;
      }
      return sum;
   }
  • 7.未重写hashcode方法
/**
     * 内存泄漏场景3:map用来当缓存,默认equals比较hashcode地址,new的对象hashcode肯定不一样,重写equals,比较的是字段属性name,重复都会一样
     * 导致:java.lang.OutOfMemoryError: GC overhead limit exceeded
     * @Data 默认是重写了hashcode方法
     */
    @Test
    public void test02_2() {
        //Person对象被HashMap引用,无法被回收,要减少对象生命周期,尽量能快速的进行垃圾回收,或者使用WeakHashMap
        HashMap<Object, Object> hashMap = new HashMap<>();
        while (true){
            for (int i = 0; i < 10000; i++) {
                boolean b = hashMap.containsKey(new Person("" + i));
                if(!b){
                    hashMap.put(new Person(""+i),"number"+i);
                }
            }
            log.warn("map大小:{}",hashMap.size());
        }
    }

实际场景

CPU飙升

java程序中,频繁GC、线程数量过多导致频繁的进行切换、线程被阻塞、死锁等原因是造成cpu飙升的主要原因。

通过arthas的dashboard命令可以看到GC的回收次数和时间

场景:ThreadLocal用完未remove

方法响应速度慢

分布式链路追踪或trace命令是统计方法的内部调用路径,并为每个调用路径统计耗时。后面需要加上类名和参数名,支持模糊匹配

trace *MethodDemo slow 
线程阻塞

检测线程死锁,找到堆栈信息

arthas:Thread -b pid

jstack:jstack -l pid
调优经验

1.监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。

内存方面

主要包括 OOM、GC 问题和堆外内存

free -h

img

堆内内存

内存问题大多还都是堆内内存问题。表象上主要分为 OOM 和 Stack Overflo

OOM

JVM 中的内存不足,OOM 大致可以分为以下几种:

1)Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

这个意思是没有足够的内存空间给线程分配 Java 栈,基本上还是线程池代码写的有问题,比如说忘记 shutdown,所以说应该首先从代码层面来寻找问题,使用 jstack 或者 jmap。如果一切都正常,JVM 方面可以通过指定Xss来减少单个 thread stack 的大小

2)Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

这个意思是堆的内存占用已经达到-Xmx 设置的最大值,应该是最常见的 OOM 错误了。解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过 jstack 和 jmap 去定位问题。如果说一切都正常,才需要通过调整Xmx的值来扩大内存

3)Caused by: java.lang.OutOfMemoryError: Meta space

元数据区的直接内存占用已经达到XX:MaxMetaspaceSize设置的最大值,比如大量加载第三方的jar包;大量动态的生成反射类,类结构放在方法区;tomcat部署工程过多。

栈内存溢出

1)Exception in thread "main" java.lang.StackOverflowError

表示线程栈需要的内存大于 Xss 值,同样也是先进行排查,例如:diao'yongdiaoyong链太深,递归。

2)通过 mat(Eclipse Memory Analysis Tools)导入 dump 文件进行分析,

内存泄漏问题一般我们直接选 Leak Suspects 即可,mat 给出了内存泄漏的建议。另外也可以选择 Top Consumers 来查看最大对象报告。和线程相关的问题可以选择 thread overview 进行分析。除此之外就是选择 Histogram 类概览来自己慢慢分析,大家可以搜搜 mat 的相关教程。

img

虚拟机栈溢出
java.lang.StackOverflowError
  • 原因:调用链过长,栈很深(死循环,嵌套方法太多,无限递归)
  • jdk默认栈大小 1M![image-20210509181707018]
void test(){
    test();
}
堆内内存与堆外内存
  • 堆内内存= 年轻代 + 老年代 + 持久代

  • 堆外内存(物理机直接内存):内存对象分配在Java虚拟机堆以外的内存;要用nio的DirectByteBuffer对象进行堆外内存的管理和使用, 它主要用于存储大量的数据,如图像、视频等,以减少堆内存的压力。堆外内存可以提高程序的性能,但是也会增加程序的复杂性,因为它需要程序员手动管理内存的分配和释放。

  • JDK1.8 取消了永久代, 由MetaSpace(元空间)代替

  • 对堆外内存的申请主要是通过成员变量unsafe来操作

    1571709464016

  • 堆外内存优点:减少了垃圾回收机制(GC 会暂停其他的工作)

  • 缺点:内存难以控制: 使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

调优工具 MAT

MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。

可以利用visualvm或者是 jmap命令生产堆文件在进行内存分析

  • 每个请求,Tomcat都会为每个线程创建两个4m的缓冲区,高并发情况下很容易导致内存溢出
使用步骤
1. 用jmap生成堆信息
heapdump /apps/provider/ecommerce/cdiscount/dump.hprof
jmap -dump:format=b,file=/apps/provider/ecommerce/cdiscount/dump1.hprof 22864

idea中:-Xms6m -Xmx6m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\Users\Administrator\Desktop\tmp\hprof\taobao

img

这样在E盘的jmap文件夹里会有一个map.bin的堆信息文件

2. 将堆信息导入到mat中分析

img

3. 生成分析报告

可以利用visualvm或者是 jmap命令生产堆文件,导入eclipse mat中生成分析报告:

img

Histogram(直方图)视图

img

  • Class Name : 类名称,java类名
  • Objects : 类的对象的数量,这个对象被创建了多少个
  • Shallow Heap :java对象占用的内存
  • Retained Heap :java对象及对象引用的类占用的内存 ,jvm gc回收时释放的内存,Retained Heap深堆大于等于Shallow Heap浅堆

通过直方图视图可以很容易找到占用内存最多的几个类(通过Retained Heap排序),还可以通过其他方式进行分组(见下图)。

img

图标进行对比,通过多次对比不同时间点下的直方图对比就很容易把溢出的类找出来。

img

在这里插入图片描述

img

快速找出某个实例没被释放的原因,可以右健 Path to GC Roots-->exclue all phantom/weak/soft etc. reference :

img

得到的结果是:

img

从表中可以看出 PreferenceManager -> … ->HomePage这条线路就引用着这个 HomePage实例。用这个方法可以快速找到某个对象的 GC Root,一个存在 GC Root的对象是不会被 GC回收掉的.


Dominator Tree(支配树)

MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系,在此视图中列出了每个对象(Object Instance)与其引用关系的树状结构,同时包含了占用内存的大小和百分比。

img

通过Dominator Tree视图可以很容易的找出占用内存最多的几个对象(根据Retained Heap或Percentage排序),和Histogram类似,可以通过不同的方式进行分组显示:

img

Histogram视图和Dominator Tree视图的角度不同,前者是基于类的角度,后者是基于对象实例的角度,并且可以更方便的看出其引用关系。

以上只是一个初步的介绍,mat还有更强大的使用,比如对比堆内存,在生产环境中往往为了定位问题,每隔几分钟dump出一下内存快照,随后在对比不同时间的堆内存的变化来发现问题。


Histogram 对比

为查找内存泄漏,通常需要两个 Dump结果作对比,定时将dump文件保存下来,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去 :

img

添加好后,打开 Compare Basket面板,得到结果:

img

点击右上角的 ! 按钮,将得到比对结果:

img

注意,上面这个对比结果不利于查找差异,可以调整对比选项:

img

再把对比的结果排序,就可得到直观的对比结果:

img

也可以对比两个对象集合,方法与此类似,都是将两个 Dump结果中的对象集合添加到Compare Basket中去对比。找出差异后用 Histogram查询的方法找出 GC Root,定位到具体的某个对象上。


Leak Suspects(内存泄漏报告)

img

  • 内存泄漏报告中看线程栈追踪stacktrace和Keywords都可以快速定位可能出现内存溢出的代码
  • 也可以直接看线程栈定位代码

img

netty 面试题

BIO、NIO、AIO 有什么区别?

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低

  • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用

    • 单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
  • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制

  • BIO是面向流的:NIO是面向缓冲区的,能从Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel,基于buffer操作不像传统IO的顺序操作, NIO 中可以随意地读取任意位置的数据;BIO的各种流是阻塞的。而NIO是非阻塞多路复用的;BIO的Stream是单向的,而NIO的channel是双向的。NIO可以通过 DirectByteBuf使用直接内存,以及零拷贝大大提升性能,而AIO呢

    // 默认文件 AIO 使用的线程都是守护线程,所以最后要执行 `System.in.read()` 以避免守护线程意外结束
            
    AsynchronousFileChannel s = AsynchronousFileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
                    ByteBuffer buffer = ByteBuffer.allocate(2);
                    log.debug("begin...");
                    // 重点 基于回调
                    s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer attachment) {
                            log.debug("read completed...{}", result);
                            buffer.flip();
                            debug(buffer);
                        }
    
                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            log.debug("read failed...");
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
                log.debug("do other things...");
                System.in.read();
    

stream vs channel

  • stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)
  • stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用
  • 二者均为全双工,即读写可以同时进行

Netty vs NIO

Netty vs NIO,工作量大,bug 多

* 需要自己构建协议
* 解决TCP传输问题,如粘包、半包
* epoll 空轮询导致 CPU 100%
* 对 API 进行增强,使之更易用,如 FastThreadLocal => ThreadLocal,ByteBuf => ByteBuffer

什么是 TCP 粘包/拆包?

粘包

  • 现象,发送 abc def,接收 abcdef
  • 原因
    • 套接字缓冲区:接收方(ByteBuf)大于数据包(存满才开始消费),且接收处理不及时
    • Nagle 算法:会造成粘包(提高网络利用率,tcp 希望尽可能发送足够大的数据)

半包

  • 现象,发送 abcdef,接收 abc def
  • 原因
    • 套接字缓冲区:接收方缓冲区(ByteBuf)小于实数据包(套接字缓冲区大小)
  • 最大报文段限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
    • MSS限制是指最大报文段大小,MSS限制可以防止TCP报文段过大而导致网络拥塞,从而提高网络的性能。

TCP的粘包拆包指的是,基于TCP发送数据时,多条消息会在一个包中粘在一起,或者一条消息直接被分为两个包发送过来,我们需要正确的处理,把每条消息都正确的拆分和合并起来。

本质是因为TCP是流式协议,消息无边界。而UDP就像快递,虽然一次运输多个,但每个包都有边界

Netty中自带的解决粘包拆包的解码器类型吧?

Netty自带四种解决粘包拆包的解码器。

  • LineBasedFrameDecoder :基于换行符的解码器,如果内容本身包含了分隔符),那么就会解析错误
  • DelimiterBasedFrameDecoder:基于分隔符的解码器,如果内容本身包含了分隔符),那么就会解析错误
  • FixedLengthFrameDecoder:基于固定长度的解码器,缺点浪费空间
  • LengthFieldBasedFrameDecoder:基于消息中的消息长度字段进行解码的解码器,消费分header和body,header中记录了起始位置和长度

如何实现长连接?

通过定时发送带序号的心跳包来实现应用层保活,并且为了防止误判,需要多发送几次心跳包来检测,在我们具体的业务实现中,使用的是Netty的IdeStateHandle中的回调方法来实现心跳机制(一定时间内如果没有收到客户端的channel数据,会触发一个IdleState#READER_IDLE 事件)


空闲状态检测器

服务端

客户端

  • 用来判断是否读空闲时间过长,或写空闲时间过长,或者读写空闲时间太长
// 5s内如果没有收到客户端的channel数据,会触发一个IdleState#READER_IDLE 事件
- new IdleStateHandler(5, 0, 0)  //参数:读,写,读写超时时间 
- ChannelDuplexHandler 可以同时作为入站和出站(读写事件捕捉)处理器
- userEventTriggered //触发IdleState事件方法

Netty 支持哪些心跳类型设置

readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。
writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。
allIdleTime:所有类型的超时时间


Netty 的零拷贝主要包含三个方面:

1.Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝

java 可以使用 DirectByteBuf 将堆外内存映射到 jvm 内存中来直接访问使用,减少了一次数据拷贝

2.Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的Buffer

slice,duplicate,CompositeByteBuf,Unpooled)

3.Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题

1)slice

【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针(逻辑切分,同一个物理内存)

例,原始 ByteBuf 进行一些初始操作

        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
        buf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i','j'});
        log(buf);

        // 在切片过程中,没有发生数据复制
        // a,b,c,d,e
        ByteBuf f1 = buf.slice(0, 5);
// f1.set("x"); //会报错,容量已经限制了
//        f1.retain();
        // f,g,h,i,j
        ByteBuf f2 = buf.slice(5, 5);
//        f2.retain();
        log(f1);
        log(f2);

//        System.out.println("释放原有 byteBuf 内存");
//        buf.release(); //原始内存释放了,slice其他buf也没有了,此时可以用retain();引用计数加1,变成2,release之后2-1=1,避免原始buf释放,导致引用slice无法使用
//        log(f1);
//
//
//        f1.release();
//        f2.release();
        System.out.println("========================");
        // 手段设置index=0为b,发现buf第一个值也为b了,即证明还是指向同一块内存
        f1.setByte(0, 'b');
        log(f1);
        log(buf);

2)duplicate

【零拷贝】的体现之一,就好比截取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的

3)CompositeByteBuf

【零拷贝】的体现之一,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝有两个 ByteBuf 如下

public class TestCompositeByteBuf {
    public static void main(String[] args) {
        ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer();
        buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});

        ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
        buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});

        /* 虽然也能实现,但是以底层复制实现的
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        buffer.writeBytes(buf1).writeBytes(buf2);
        log(buffer);*/

        // 零拷贝实现
        CompositeByteBuf buffer = ByteBufAllocator.DEFAULT.compositeBuffer();
        buffer.addComponents(true, buf1, buf2);
        log(buffer);
    }
}

4)Unpooled

Unpooled 是一个工具类,类如其名,提供了非池化的 ByteBuf 创建、组合、复制等操作

这里仅介绍其跟【零拷贝】相关的 wrappedBuffer 方法,可以用来包装 ByteBuf

ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});
// 当包装 ByteBuf 个数超过一个时, 底层使用了 
CompositeByteBufByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));

哪几种序列化协议?

1.Fastjson:阿里出品,java常用序列化工具包

Netty 自带解码器:StringDecoder 字符串解码,ObjectDecoder 对象解码器

2.Avro,Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现

3.Protobuf 序列化后码流小,兼容更多的传输层协议,跨防火墙,轻便,高效可靠结构化数据储存

http + json => tcp + Protobuf 
将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。优点:

protobuf的数据类型有多种:booldoublefloat、int32、int64、string、bytes、enum、message。protobuf的限定符:required: 必须赋值,不能为空、optional:字段可以赋值,也可s以不赋值、repeated: 该字段可以重复任意次数(包括0次)、枚举;只能用指定的常量集中的一个值作为其值;

Netty中的使用:ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类

Protocol Buffers

img
img
img

5.Netty 高性能表现在哪些方面?

  • IO 线程模型同步非阻塞,高效利用资源处理任务

  • 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。

  • 内存池设计:申请的内存可以重用,主要指直接内存

    内部实现是用一颗二叉查找树管理内存分配情况。
    
  • 串形化处理读写:避免使用锁带来的性能开销,readactive方法处异步任务也是同一个线程执行(volatile,synchronized,线程池,cas在netty底层网络编程中大量使用)。

  • 高性能序列化协议:支持 protobuf 等高性能序列化协议


Netty 发送消息有几种方式?

  • 直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动
  • 写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动
  • ctx.channel().write(msg) 从尾部开始查找出站处理器
  • ctx.write(msg) 是从当前节点找上一个出站处理器

Future & Promise

在异步处理时,经常用到这两个接口

首先要说明 netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口,netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展

  • jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果

  • netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束

  • netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器

    await 死锁检查
    
功能/名称 jdk Future netty Future Promise
cancel 取消任务 - -
isCanceled 任务是否取消 - -
isDone 任务是否完成,不能区分成功失败 - -
get 获取任务结果,阻塞等待 - -
getNow - 获取任务结果,非阻塞,还未产生结果时返回 null -
await - 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 -
sync - 等待任务结束,如果任务失败,抛出异常 -
isSuccess - 判断任务是否成功 -
cause - 获取失败信息,非阻塞,如果没有失败,返回null -
addLinstener - 添加回调,异步接收结果 -
setSuccess - - 设置成功结果
setFailure - - 设置失败结果

默认情况 Netty 起多少线程?何时启动?

Netty 默认是 CPU 处理器数的两倍,bind 完之后启动。


tcp 三次握手

accept queuesyns queueserverclientaccept queuesyns queueserverclientSYN_SENDSYN_RCVDESTABLISHEDESTABLISHEDbind()listen()connect()1. SYNput2. SYN + ACK3. ACKputaccept()
  1. 第一次握手,client 发送 SYN 到 server,状态为 SYN_SEND,server 收到,状态改变为 SYN_REVD,并将该请求放入 sync queue 队列
  2. 第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态改变为 ESTABLISHED,并发送 ACK 给 server
  3. 第三次握手,server 收到 ACK,状态改变为 ESTABLISHED,将该请求从 sync queue 放入 accept queue(目的是为了防止突然大量请求需要连接,accept处理来不及时,会放入全连接队列进行缓冲,处理完前面的,再从队列中拿来进行accept(),即三次握手是发生在accept之前的)

其中

  • 在 linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制

  • sync queue - 半连接队列

    • 大小通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在 syncookies 启用的情况下,逻辑上没有最大值限制,这个设置便被忽略
  • accept queue - 全连接队列

    • 其大小通过 /proc/sys/net/core/somaxconn 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值,所以系统参数和代码都需要改大才行
    • 如果 accpet queue 队列满了,server 将发送一个拒绝连接的错误信息到 client

netty 中可以通过 option(ChannelOption.SO_BACKLOG, 2) 来设置大小,客户端连接超过3个放入全连接队列时就会报拒绝连接错误(netty的accept是非常快的,为了模拟有3个未处理放入全连接队列,在处理时候打debug断点)


线程模型

1.传统阻塞IO模型

  • 一个客户端对应一个独立线程完成任务(读,处理,返回),并且保证长连接,高并发情况,创建大量线程,占用大量资源

2.Reactor模型

  • 基于线程池复用线程资源非阻塞操作,一个Reactor单线程监听多个客户端连接,不必为每个连接创建线程(只需一个handler等待)-》将任务分发给后续程序处理即可

3.单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。

既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。

4.多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。

5.主从多线程模型:主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor

  • MainReactor负责客户端的连接请求,并将请求转交给SubReactor
  • SubReactor负责相应通道的IO读写请求
  • 非IO请求(具体逻辑处理)的任务则会直接写入队列等待worker threads进行处理

6.netty线程模型:Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,work线程池处理任务

当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的readwrite事件,由对应的Handler处理。

netty模仿Dubbo实现RPC

img

调用流程

1.客户端构建代理类,发送请求消息(await等待结果)
2.服务端接收消息,然后channelRead0读取消息内容,反射出请求需要调用的接口和参数,调用hello方法返回结果
3.客户端收到response,await获得结果,结束请求

异步调用和通信

 1.netty:客户端和服务端通信;使用promise + await 实现网络通信,主线程异步调用同步等待获取结果;
 2.如果需要异步接收结果(开启其他线程):promise.addListener(()->{})
 3.动态代理:动态代理生成接口和调用
 4.netty + 动态代理:封装成rpc
   
 // 接口调用
 HelloService service = getProxyService(HelloService.class);
 System.out.println(service.sayHello("zhangsan"));


 Object o = Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
     // 1. 将方法调用转换为 消息对象
     int sequenceId = SequenceIdGenerator.nextId();
     RpcRequestMessage msg = new RpcRequestMessage(
             sequenceId,
             serviceClass.getName(),
             method.getName(),
             method.getReturnType(),
             method.getParameterTypes(),
             args
     );
     // 2. 将消息对象发送出去
     Channel channel = getChannel();
     channel.writeAndFlush(msg);

     // 使用promise + await 实现网络通信,异步调用和同步等待获取结果
     // 3. 准备一个空 Promise 对象,来接收结果指定 promise 对象异步接收结果线程
     DefaultPromise<Object> promise = new DefaultPromise<>(channel.eventLoop());
     RpcResponseMessageHandler.PROMISES.put(sequenceId, promise);

     // 使用新的线程异步获得结果
       promise.addListener(future -> {
           // 线程
       });

     // 4. 异步消息,同步等待 promise 结果,相对于sync,失败不会抛出异常
     promise.await();
     if(promise.isSuccess()) {
         // 调用正常
         return promise.getNow();
     } else {
         // 调用失败
         throw new RuntimeException(promise.cause());
     }
 });

共享对象:
@ChannelHandler.Sharable // 共享对象,如果类中有状态保存,需要自己考虑线程安全问题,ConcurrentHashMap 通过cas自己保证了线程安全

map.remove(msg) // remove拿到之后会从map中移除

枚举类实接口:

enum Algorithm implements Serializer {
     Java {},
     Json {}
}



Netty 架构,组件,工作原理

img

重要组件

  • Bootstrap、ServerBootstrap 引导类,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类

  • Channel:网络通道,操作抽象类, I/O 操作,如 bind、connect、read、write 等。

    * close() 可以用来关闭 channel
    * closeFuture() 用来处理 channel 的关闭
      * sync 方法作用是同步等待 channel 关闭
      * 而 addListener 方法是异步等待 channel 关闭
    * pipeline() 方法添加处理器
    * write() 方法将数据写入
    * writeAndFlush() 方法将数据写入并刷出
    
  • EventLoop:工人,主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。

  • NioEventLoop:维护了一个线程和任务队列,selector,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:

    • I/O任务selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
    • 非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。

    两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。

  • EventLoopGroup:线程池 + Selector,相当于事件循环组,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。

  • ChannelFuture:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。

  • ChannelHandler:处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。

    * 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果,按照 addLast 的顺序执行的
    * 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工,按照 addLast 的逆序执行的
    * 用于处理入站和出站事件 ChannelDuplexHandler
    
    * **ctx.channel().write(msg) 从尾部开始查找出站处理器**
    * **ctx.write(msg) 是从当前节点找上一个出站处理器**
    
  • ChannelPipeline:数据的处理多道工序流水线,为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。

  • Future、ChannelFuture

    Netty中所有的IO操作都是异步的,通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

  • Selector:Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件

    每个Boss NioEventLoop循环执行的任务包含3步:
    
    - 1 轮询accept事件
    - 2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上,runAllTasks。
    
    每个Worker NioEventLoop循环执行的任务包含3步:
        
    - 1 轮询read、write事件;
    - 2 处I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
    - 3 处理任务队列中的任务,runAllTasks。
    
        
    其中任务队列中的task有2种典型使用场景
    
    - 1 用户程序自定义的普通任务
    
    ctx.channel().eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            //...
        }
    });
    
    - 2 用户自定义定时任务
    ```text
    ctx.channel().eventLoop().schedule(new Runnable() {
        @Override
        public void run() {
    
        }
    }, 60, TimeUnit.SECONDS);
    ```
    
    ## 
    

服务器端

final ServerBootstrap serverBootstrap =new ServerBootstrap()
    
     // 1.创建 NioEventLoopGroup,可以简单理解为 `线程池 + Selector` 后面会详细展开
     // 创建mainReactor
     NioEventLoopGroup boosGroup = new NioEventLoopGroup();
     // 创建工作线程组
     NioEventLoopGroup workerGroup = new NioEventLoopGroup();

    // 主线程 和 工作线程,组装NioEventLoopGroup 
    .group(boosGroup, workerGroup)
    // 2.选择服务Socket实现类,设置channel类型为NIO类型
    .channel(NioServerSocketChannel.class)
                        // 设置连接配置参数
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
    // 3.待客户端 SocketChannel 建立连接后,执行 initChannel 以便添加更多的处理器
    // 好比:数据的处理多道工序流水线
    .childHandler(new ChannelInitializer<NioSocketChannel>() {
        protected void initChannel(NioSocketChannel ch) {
            // 5.SocketChannel 的处理器,解码 ByteBuf => String
            ch.pipeline().addLast(new StringDecoder());             
            // 6.SocketChannel 的业务处理器,使用上一个处理器的处理结果(配置入站、出站事件channel)
            ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { 
                
                @Override
                protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                    System.out.println(msg);
                }
            });
        }
    })
    // 4.ServerSocketChannel 绑定的监听端口
    .bind(8080); 
     
     serverBootstrap.addListener(future -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
            } else {
                System.err.println("端口[" + port + "]绑定失败!");
            }
     });
  • 初始化创建2个NioEventLoopGroup,其中boosGroup用于Accetpt连接建立事件并分发请求workerGroup用于处理I/O读写事件和业务逻辑
  • 基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler
  • 绑定端口,开始工作

客户端

new Bootstrap()
    // 1.创建 NioEventLoopGroup,同 Server
    .group(new NioEventLoopGroup()) 
    // 2.选择客户 Socket 实现类,NioSocketChannel 表示基于 NIO 的客户端实现,其它实现还有
    .channel(NioSocketChannel.class) 
    // 3.添加 SocketChannel 的处理器,ChannelInitializer 处理器(仅执行一次),它的作用是待客户端 SocketChannel 建立连接后,执行 initChannel 以便添加更多的处理器
    .handler(new ChannelInitializer<Channel>() { 
        @Override
        protected void initChannel(Channel ch) {
            // 8.消息会经过通道 handler 处理,这里是将 String => ByteBuf 发出
            ch.pipeline().addLast(new StringEncoder()); 
        }
    })
    // 4.指定要连接的服务器和端口
    .connect("127.0.0.1", 8080) 
    // 5.Netty 中很多方法都是异步的,如 connect,这时需要使用 sync 方法等待 connect 建立连接完毕
    // 因此 channelFuture 对象中不能【立刻】获得到正确的 Channel 对象
    .sync() 
    // 6.获取 channel 对象,它即为通道抽象,可以进行数据读写操作
    .channel() 
    // 7.写入消息并清空缓冲区
    .writeAndFlush(new Date() + ": hello world!"); 
   
    // 相对于同步sync,异步起新线程接收连接
    .addListener((ChannelFutureListener) future -> {
            System.out.println(future.channel());
            // 2 在连接建立时被调用(其中 operationComplete 方法),因此执行到 2 时,连接肯定建立了
       });

  • 数据经过网络传输,到达服务器端,服务器端 5 和 6 处的 handler 先后被触发,走完一个流程

  • CONNECT_TIMEOUT_MILLIS 超时机制

    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 300)
    
    // 原理:设置超时时间>0,设置内部有个定时任务 eventLoop().schedule,检测超时就触发报错,否则取消定时任务
    // 主线程 和nio线程通过同个promise进行通信,主线程等待nio线程,当超时时候,nio的connectPromise.tryFailure(cause)唤醒主线程
    // 此时future.sync()等待获取异常对象,无法channel()了,直接抛出异常
    

CloseFuture

 			// 获取 CloseFuture 对象, 1) 同步处理关闭, 2) 异步处理关闭
            ChannelFuture closeFuture = channel.closeFuture();
            /*log.debug("waiting close...");        closeFuture.sync();        			             log.debug("处理关闭之后的操作");*/
            closeFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    log.debug("处理关闭之后的操作");
                    group.shutdownGracefully();
                }
            });

ByteBuf 对字节数据的封装类

  • 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能

  • 可以直接内存

    * 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
    * 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
    
  • 读写指针分离,不需要像 ByteBuffer 调用flip()切换读写模式

  • 可以自动扩容

  • 支持链式调用,使用更流畅

  • 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf

扩容规则是

  • 如何写入后数据大小未超过 512则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
  • 如果写入后数据大小超过 512则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 210=1024(29=512 已经不够了)
  • 扩容不能超过 max capacity 会报错

retain & release

由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可

    每次都会新建一个缓冲区对象
    
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存

  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

    * 采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。
    

回收内存的源码实现,请关注下面方法的不同实现

protected abstract void deallocate()

引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口,基于AtomicIntegerFieldUpdater用于内存回收

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

nio空轮询bug

描述:特殊情况没有任务时候,for循环空转, selector.select方法不会阻塞,导致cpu达到最高

netty解决方案:通过加一个计数,配置一个阈值,for循环计数加1,当达到这个阈值的时候,默认触发epoll的bug,重新创建一个selector替换旧的selector,旧的信息复制上去

NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化EventExecutor时NioEventLoopGroup重载newChild方法,所以children元素的实际类型为NioEventLoop。

线程启动时调用SingleThreadEventExecutor的构造方法,执行NioEventLoop类的run方法,首先会调用hasTasks()方法判断当前taskQueue是否有元素。如果taskQueue中有元素,执行 selectNow() 方法,最终执行selector.selectNow(),该方法会立即返回。如果taskQueue没有元素,执行 select(oldWakenUp) 方法

select ( oldWakenUp) 方法解决了 Nio 中的 bug,如上...

每个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。

select和poll vs epoll IO多路复用模型

在linux 没有实现事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在大数据、高并发、集群等一些名词唱得火热之年代,select和poll的用武之地越来越有限,风头已经被epoll占尽。

select的局限性

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
  2. 内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

案例:

假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

epoll IO多路复用模型实现机制

设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

epoll通过在Linux内核中申请一个简易的文件系统(B+树)。把原先的select/poll调用分成了3个部分:

1)调用epoll_create()建立一个epoll对象。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

3)调用epoll_wait收集已经发生的事件的连接。

如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

Linux内核具体的epoll机制实现思路。

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

     struct eventpoll{
     
     /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
     struct rb_root  rbr;
  
     /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
     struct list_head rdlist;
    
     };

红黑树结构:每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

双向链表结构:所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中

epoll_wait:当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

epoll.jpg

从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

flink底层原理和设计

  • 流式计算模型:将数据看作一系列事件,事件流经过不同的算子进行处理和转换,最终输出结果

  • 基于状态的计算:算子可以维护状态,即算子的输出结果可以依赖于之前的输入数据和状态。Flink将状态存储在内存或外部存储系统中,以支持更复杂的计算。

  • 基于时间的窗口:支持滚动窗口和滑动窗口,并支持多种窗口类型,如时间窗口和计数窗口等

  • 基于异步快照的容错机制:定期将状态快照写入外部存储系统,以支持故障恢复和任务重启

  • 基于可插拔的数据源和数据接收器:支持多种数据源和数据接收器,如Kafka、Hadoop、HDFS等,同时也支持自定义数据源和数据接收器。

  • 基于多级流水线的执行引擎:将计算过程划分为多个阶段,每个阶段都可以并行执行,以提高计算效率。

  • 基于可扩展的分布式架构:通过增加节点来扩展计算能力,同时也支持高可用性和负载均衡等特性。

负载均衡(非重点)

37.1 nginx服务端负载均衡:传统负载均衡,手动配置ip,通过nginx轮询ip进行负载均衡,但是添加新的服务机需要手动新增ip配置,不太理想

nginx 部署应用,反向代理(一个域名配多个ip),负载均衡,限流,分发算法

37.2 分发算法有:

  • 轮询(默认)
  • weight(权重) 1:2
  • ip_hash,请求者ip的hash值,实现同一ip在固定的机器上,解决session问题
  • url_hash(第三方),根据请求的url的hash值将请求分到不同的机器中,缓存高
  • fair(第三方),响应时间短的分发的请求多

轮询和权重适合静态页面,不适合动态页面 ip_hash 适合动态页面

37.3 基于请求头分发

  • 1.基于host分发 多集群,多个反向代理,多个upstream
  • 基于开发语言分发
  • 基于浏览器分发
  • 基于源ip分发

注册中心客户端负载均衡:自动监听服务器和自动推送信息

Nginx转发不同服务器实现负载均衡,网关集群部署需要有nginx反向代理转发

cdn(content delivery network) 内容分发网络:静态文件,图片,视频,js代码...
cdn,选择离你最近网络cdn获取缓存资源,除了第一个用户需要从总部拿取资源缓存到最近cdn节点(时间比较慢),以后的用户获取资源都是用缓存获取,自己推送到cdn,用户第一次也很快,但是会有冗余,有些用户并不需要的资源

37.4.算法思想

随机

  • 随机算法,Random随机获取ip
  • 权重随机,权重值为8,那么重复保存8条此ip值,随机几率就提高8倍(如果权重值很大,那么集合就越大,循环性能就越差)
  • 范围算法,依据机器权重划区域,如果随机值小于5则选择A机器,6-8选择B机器

37.5轮询

同理随机算法,比较得出性能高效的方法:


  • linux每次请求都会有一个requestId,自增的,有可能非常大,此处模拟requestId,使用hash取余让它的值落在范围区间之内

    如果出现AAAAABC,这样对A服务器性能是不好的,理想情况是AABAACA
    
  • 平滑加权轮询算法:
    动态权重:currentweight

  • AABACAA 获得的结果

37.6.哈希

共享session问题,集群情况下用户如果第二次访问,nginx转发到第三台服务机,由于没session,需要强制登录




  • 一个客户端ip将转化成hashcode存到指定服务器,同一个客户肯定只会访问某一台机
  • 需要利用一个排序的存储,treeMap 红黑树
  • 哈希环 + 红黑树 实现 哈希负载均衡算法
  • 如果hash值大于最大值,那么返回最小值,环形结构

负载均衡算法或者hash环等等,都是将具体节点转换成范围节点

Dubbo

RPC(远程过程调用)是一种通信协议,它允许程序在不同的计算机之间进行远程调用,从而实现分布式系统中的信息共享和通信

http,socket,tcp都属于rpc协议

模拟http协议源码调用

还有dubbo协议等

zookeeper

分布式协调器,分布式系统,存储数据,注册中心,管理数据在内存中
文件系统的数据结构:
    数据结构:树形节点存储;
    持久化节点;
    临时节点,session超时时间,临时节点和sessionid绑定,如果挂掉或关闭,ping不通,或超时,会删除临时节点
    持久化顺序节点;
    临时顺序节点;
    命令:
     create (-e) /zp 创建一个节点(默认持久化节点,-e 临时节点)
     create -s /zp/xx- :创建持久化顺序节点
     get -s /zp
     set /zp "aa"
     create /zp/sub 持久化创建子节点,临时节点不能拥有子节点
     get -w /zp : 数据修改,进行监听(仅一次),配置中心修改config,统一监听动态刷新配置文件(类似:nacos)
     ls -w /zp :对某个目录进行监听
     
 客户端监听机制:对子节点进行监听,节点数据修改,自动监听到事件

注册中心:服务端新加服务器会在注册中心创建节点并同步信息,客户端对服务目录进行监听,会动态感知,加入到本地缓存,实现客户端负载均衡

多个线程竞争获取锁(创建节点,一个节点唯一性),如果成功完成业务处理,并释放锁(删除节点),如果失败,监听等待释放锁,监听到释放锁,然后又一起竞争锁

问题:羊群效率?

1000个请求,只有一个成功,其他请求都需要监听等待,时间复杂度高

第一次 1个成功 999监听,第二次 1个成功 998监听 ...

解决方案:使用临时顺序节点

每次每个节点只监听它上一个节点的就可以了(/lock 容器节点),第一个节点index=0直接获得锁

问题:中间节点断了怎么办?

中间节点销毁,改监听此节点前一个节点

Nginx 反向代理:

# 向80端口发送请求,自动反向代理到http://myapp服务器,此时baidu1,baidu2,baidu3都可能收到请求(轮询)
http{
    upstream myapp {
      server www.baidu1.com
      server www.baidu2.com
      server www.baidu3.com
    }

    server{
      listen:80:
      locaiton / {
       proxy_pass http://myapp
      }
    }
}

利用curator实现分布式锁,将扣减库存并发请求串行化

Apache ShardingSphere

https://hucheng.blog.csdn.net/article/details/107528635

ShardingSphere介绍

  • 垂直分库和分表,库按业务划分,表按照大字段或非必须字段划分,结构是不一样的
  • 水平分库和分表,它们都是按照数据来进行划分,比如1-10000,10001-20000,表结构是一样的,数据不一样
  • 集群或读写分离,库,表,数据源都一样

实际应用:

  • 数据库设计的时候就考虑垂直分库和垂直分表
  • 随着数量增多,先考虑缓存处理,读写分离,索引,如果还不能解决才考虑用水平分库和水平分表

问题:

  • 跨节点连接查询问题(分页,查询)
  • 多数据源管理问题

Sharding-JDBC

4.0.0版本

  • 轻量级java框架,增强的jdbc驱动
  • 简化分库分表之后数据相关的操作

水平分表场景

配置Sharding-JDBC 表分片策略

  • 数据按照要求进行分片

一个实体类Course对应两张表course_1,course_2,会导致报错,需要设置下

新增和查询(奇数id)都是按照奇偶策略进行的

# 配置真实数据源
spring.shardingsphere.datasource.names=ds0,ds1

# 配置第 1 个数据源
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://localhost:3306/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=

# 配置第 2 个数据源
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource ...

# 配置 t_order 表规则
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes
=ds$->{0..1}.t_order$->{0..1}

//t_order表名,ds库名 ds0,ds1 ,t_order0,t_order1


# 配置分库策略
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-algorithm-name=database_inline

# 配置分表策略
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=table_inline

# 省略配置 t_order_item 表规则...
# ...

# 配置 分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.props.algorithm-expression=ds_${user_id % 2}
spring.shardingsphere.rules.sharding.sharding-algorithms.table_inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.table_inline.props.algorithm-expression=t_order_${order_id % 2}

水平分库场景

  • 可以设置库中所有表设置满足表分片策略,或指定表分片策略

垂直分库

专库专表

  • 解决多数据源问题
  • 操作User实体类只访问user库DB

公共表

读写分离

  • ShardingJdbc依据sql中insert,update拦截跳转执行主数据库
  • 然后主从数据库通过binlog日志实现数据同步

  • 配置binlog日志,主从配置等

Sharding-Proxy

  • 透明化的数据库代理端(类似mycat),默认3307端口

  • 修改文件server.yaml(公共源),config-shading.yaml 分库分表,一库多表

mysql -P3307 -uroot -p  //连接到Sharding proxy
  • 操作Sharding proxy表,默认数据库中所有表(比如两张表)也修改,方便的实现Sharding proxy分表分库,读写分离等操作
读写分离配置
  • 修改conf中config-master-yaml文件修改

总结

Sharding-Sphere 与mycat区别?

底层实现原理:将任务拆分给所有的片段(每个子表)去执行,然后将所有真正执行sql的结果进行汇总合并,返回
水平分表,查询100000之后10条数据

SELECT * FROM t_order ORDER BY id LIMIT 10 //每个片段都执行这段,然后汇总结果,在进行取前10条数据
  • Sharding proxy相当于一个数据库代理层,它实现了将多个库表整合
  • Sharding proxy 水平分表之后数据分片(1-10000,10001-20000...),进行分页查询时,由于设置主键id是自增,

  • Sharding-Sphere 就是一个jar包,类似nacos,集成到每个应用
  • mycat 是一个组件,类似redis,需要专门维护
posted @   pandazou  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示