冰河

冰河 查看完整档案

成都编辑  |  填写毕业院校  |  填写所在公司/组织 blog.csdn.net/l1028386804 编辑
编辑

微信搜一搜【冰河技术】微信公众号,关注这个有深度的程序员,每天阅读超硬核技术干货,公众号内回复【PDF】有我准备的一线大厂面试资料和我原创的超硬核PDF技术文档,以及我为大家精心准备的多套简历模板(不断更新中),希望大家都能找到心仪的工作,学习是一条时而郁郁寡欢,时而开怀大笑的路,加油。如果你通过努力成功进入到了心仪的公司,一定不要懈怠放松,职场成长和新技术学习一样,不进则退。如果有幸我们江湖再见!

个人动态

冰河 发布了文章 · 2月26日

Java中String到底占用多大的内存空间?

写在前面

对于Java中的String类占用多大的内存空间这个问题,是最近面试中问的比较多的一个问题。很多小伙伴的回答的都不是很正确,有说不占空间的,有说1个字节的,有说2个字节的,有说3个字节的,有说不知道的,更让人哭笑不得的是竟然还有人说是2的31次方。那如果真是这样的话,服务器的内存空间还放不下一个字符串呀!作为程序员的我们,可不能闹这种笑话呀。今天,我们就一起来聊聊Java中的String到底占用多大的内存空间!

Java对象的结构

首先,我们来下Java对象在虚拟机中的结构,这里,以HotSpot虚拟机为例。

<p align="right">注:图片来源http://r6d.cn/wp7q</p>

从上面的这张图里面可以看出,对象在内存中的结构主要包含以下几个部分:

  • Mark Word(标记字段):对象的Mark Word部分占4个字节,其内容是一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。
  • Klass Pointer(Class对象指针):Class对象指针的大小也是4个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址
  • 对象实际数据:这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节
  • 对齐:最后一部分是对齐填充的字节,按8个字节填充。

换种说法就是:

  • 对象头(object header):8 个字节(保存对象的 class 信息、ID、在虚拟机中的状态)
  • Java 原始类型数据:如 int, float, char 等类型的数据
  • 引用(reference):4 个字节
  • 填充符(padding)

Java中的String类型

空String占用的空间

这里,我们以Java8为例进行说明。首先,我们来看看String类中的成员变量。

/** The value is used for character storage. */
private final char value[];
 
/** Cache the hash code for the string */
private int hash; // Default to 0
 
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;

在 Java 里数组也是对象,因此数组也有对象头。所以,一个数组所占的空间为对象头所占的空间加上数组长度加上数组的引用,即 8 + 4 + 4= 16 字节 。

所以,我们可以得出一个空String对象所占用的内存空间,如下所示。

对象头(8 字节)+ 引用 (4 字节 )  + char 数组(16 字节)+ 1个 int(4字节)+ 1个long(8字节)= 40 字节

所以,小伙伴们,你们的回答正确吗?

非空String占用的空间

如果String字符串的长度大于0的话,我们也可以得出String占用内存的计算公式,如下所示。

40 + 2 * n

其中,n为字符串的长度。

这里,可能有小伙伴会问,为什么是 40 + 2 n 呢?这是因为40是空字符串占用的内存空间,这个我们上面已经说过了,String类实际上是把数据存储到char[]这个成员变量数组中的,而char[]数组中的一个char类型的数据占用2个字节的空间,所以,只是String中的数据就会占用 2 n(n为字符串的长度)个字节的空间,再加上空字符串所占用的40个字节空间,最终得出一个字符串所占用的存储空间为: 40 + 2 * n (n为字符串长度)。

因此在代码中大量使用String对象时,应考虑内存的实际占用情况。

注:40 + 2 * n 这个公式我们可以看成是计算String对象占用多大内存空间的通用公式。

验证结论

接下来,我们就一起来验证下我们上面的结论。首先,创建一个UUIDUtils类用来生成32位的UUID,如下所示。

package io.mykit.binghe.string.test;

import java.util.UUID;

/**
 * @author binghe
 * @version 1.0.0
 * @description 生成没有-的UUID
 */
public class UUIDUtils {
    public static String getUUID(){
        String uuid = UUID.randomUUID().toString();
        return uuid.replace("-", "");
    }
}

接下来,创建一个TestString类,在main()方法中创建一个长度为4000000的数组,然后在数组中放满UUID字符串,如下所示。

package io.mykit.binghe.string.test;

import java.util.UUID;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试String占用的内存空间
 */
public class TestString{
    public static void main(String[] args){
         String[] strContainer = new String[4000000];
        for(int i = 0; i < 4000000; i++){
            strContainer[i] = UUIDUtils.getUUID();
            System.out.println(i);
        }
        //防止程序退出
        while(true){

        }
    }
}

这里,4000000个字符串,每个字符串的长度为32,所以保存字符串数据所占用的内存空间为:(40 + 32 2) 4000000 = 416000000字节,约等于416MB。

我们使用Jprofiler内存分析工具进行分析:

可以看到,使用Jprofiler内存分析工具的结果为:321MB + 96632KB,约等于417MB。之所以使用Jprofiler内存分析工具得出的结果比我们计算的大些,是因为在程序实际运行的过程中,程序内部也会生成一些字符串,这些字符串也会占用内存空间!!

所以,使用Jprofiler内存分析工具得出的结果符合我们的预期。

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月24日

字节跳动面试官这样问消息队列:高可用、不重复消费、可靠传输、顺序消费、消息堆积,我整理了下

写在前面

又到了年底跳槽高峰季,很多小伙伴出去面试时,不少面试官都会问到消息队列的问题,不少小伙伴回答的不是很完美,有些小伙伴是心里知道答案,嘴上却没有很好的表达出来,究其根本原因,还是对相关的知识点理解的不够透彻。今天,我们就一起来探讨下这个话题。注:文章有点长,你说你能一鼓作气看完,我有点不信!!

文章已收录到:

https://github.com/sunshinelyz/technology-binghe

https://gitee.com/binghe001/technology-binghe

什么是消息队列?

消息队列(Message Queue)是在消息的传输过程中保存消息的容器,是应用间的通信方式。消息发送后可以立即返回,由消息系统保证消息的可靠传输,消息发布者只管把消息写到队列里面而不用考虑谁需要消息,而消息的使用者也不需要知道谁发布的消息,只管到消息队列里面取,这样生产和消费便可以做到分离。

为什么要使用消息队列?

优点:

  • 异步处理:例如短信通知、终端状态推送、App推送、用户注册等
  • 数据同步:业务数据推送同步
  • 重试补偿:记账失败重试
  • 系统解耦:通讯上下行、终端异常监控、分布式事件中心
  • 流量消峰:秒杀场景下的下单处理
  • 发布订阅:HSF的服务状态变化通知、分布式事件中心
  • 高并发缓冲:日志服务、监控上报

使用消息队列比较核心的作用就是:解耦异步削峰

缺点:

  • 系统可用性降低 系统引入的外部依赖越多,越容易挂掉?如何保证消息队列的高可用?
  • 系统复杂度提高 怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
  • 一致性问题 A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

以下主要讨论的RabbitMQ和Kafka两种消息队列。

如何保证消息队列的高可用?

RabbitMQ的高可用

RabbitMQ的高可用是基于主从(非分布式)做高可用性。RabbitMQ 有三种模式:单机模式(Demo级别)、普通集群模式(无高可用性)、镜像集群模式(高可用性)。

  • 普通集群模式

    普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。

    这种方式确实很麻烦,也不怎么好,没做到所谓的分布式,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈

    而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让 RabbitMQ 落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。

    所以这个事儿就比较尴尬了,这就没有什么所谓的高可用性这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

  • 镜像集群模式

这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。

那么如何开启这个镜像集群模式呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。

这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就没有扩展性可言了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并没有办法线性扩展你的 queue。你想,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢?

Kafka的高可用

Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。

这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据

实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。

Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。

比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。

Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。

这么搞,就有所谓的高可用性了,因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。

写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)

消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

如何保证消息不重复消费(幂等性)?

首先,所有的消息队列都会有这样重复消费的问题,因为这是不MQ来保证,而是我们自己开发保证的,我们使用Kakfa来讨论是如何实现的。

Kakfa有个offset的概念,就是每个消息写进去都会有一个offset值,代表消费的序号,然后consumer消费了数据之后,默认每隔一段时间会把自己消费过的消息的offset值提交,表示我已经消费过了,下次要是我重启啥的,就让我从当前提交的offset处来继续消费。

但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。

其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性

举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错

所以第二个问题来了,怎么保证消息队列消费的幂等性?

其实还是得结合业务来思考,我这里给几个思路:

  • 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
  • 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。

如何保证消息的可靠传输(不丢失)?

这个是肯定的,MQ的基本原则就是数据不能多一条,也不能少一条,不能多其实就是我们前面重复消费的问题。不能少,就是数据不能丢,像计费,扣费的一些信息,是肯定不能丢失的。

数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从 RabbitMQ 和 Kafka 分别来分析一下吧。

RabbitMQ如何保证消息的可靠

生产者丢数据

生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。

此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit

// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback

// 这里再次重发这条消息
}

// 提交事务
channel.txCommit

但是问题是,RabbitMQ 事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能

所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。

所以一般在生产者这块避免数据丢失,都是用 confirm 机制的。

RabbitMQ丢数据

就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。

设置持久化有两个步骤

  • 创建 queue 的时候将其设置为持久化 这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
  • 第二个是发送消息的时候将消息的 deliveryMode 设置为 2 就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。

所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。

消费者丢数据

RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。

这个时候得用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。

Kakfa如何保证消息的可靠

  • 消费者丢数据

    唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。

    这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。

    生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。

  • Kafka丢数据

    这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。

    生产环境也遇到过,我们也是,之前 Kafka 的 leader 机器宕机了,将 follower 切换为 leader 之后,就会发现说这个数据就丢了。

    所以此时一般是要求起码设置如下 4 个参数:

    • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
    • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
    • 在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了
    • 在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

    我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。

  • 生产者丢数据

    如果按照上述的思路设置了 acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。

如何保证消息的顺序性?

我举个例子,我们以前做过一个 mysql binlog 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。

你在 mysql 里增删改一条数据,对应出来了增删改 3 条 binlog 日志,接着这三条 binlog 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。

本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。

先看看顺序会错乱的俩场景:

  • RabbitMQ:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。

  • Kafka:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。 消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。

RabbitMQ解决方案

拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

Kafka解决方案

  • 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
  • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

如何处理消息推积?

大量消息在 mq 里积压了几个小时了还没解决

一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

  • 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
  • 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
  • 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
  • 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
  • 等快速消费完积压数据之后,得恢复原先部署的架构重新用原先的 consumer 机器来消费消息。

mq 中的消息过期失效了

假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢

这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

mq 都快写满了

如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

参考资料:

  • Kafa深度解析
  • RabbitMQ源码解析

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 3 收藏 2 评论 0

冰河 发布了文章 · 2月22日

程序员心中的一道坎:主存的编址与计算和串并联系统!

写在前面

很多小伙伴认为程序员就是写写代码,不需要了解计算机底层的知识和原理。其实,这种观点是错误的。如果你想突破程序员的职业发展瓶颈,计算机硬件、操作系统原理、编译原理等是一定要掌握的知识。而【冰河技术】微信公众号的【程序员进阶系列】专题就是要系统的向大家分享程序员进阶需要掌握的各项知识技能。今天,我们来聊聊一个让程序员很头疼的话题:计算机中的主存是如何进行编址和计算的?

文章已收录到:

https://github.com/sunshinelyz/technology-binghe

https://gitee.com/binghe001/technology-binghe

主存编址与计算

这里,小伙伴们首先要区分两个概念,一个是编址,一个是寻址。

编址: 存储器是由一个个存储单元构成的,为了对存储器进行有效的管理,就需要对各个存储单元编上号,即给每个单元赋予一个地址码,这叫编址。经编址后,存储器在逻辑上便形成一个线性地址空间。

寻址: 存取数据时,必须先给出地址码,再由硬件电路译码找到数据所在地址,这叫寻址。

编址可以分为两种:按字编址和按字节编址

  • 按字编址:存储体的存储单元是字存储单元,即最小寻址单位是一个字。
  • 按字节编址:存储体的存储单元是字节存储单元,即最小寻址单位是一个字节。

对于主存编址中最常见的计算形式为:根据存储器所要求的容量和选定的存储芯片的容量,就可以计算出所需要的芯片的数量。公式如下所示。

总片数 = 总容量 / 每片的容量

这里,给小伙伴们举一个例子:若内存地址区间为4000H ~ 43FFH,每个存储单元可存储16位二进制数,该内存区域使用4片存储器芯片构成,则构成该内存所用的存储器芯片的容量是多少?

解题思路也比较简单,我们一起来看看如何解题:

(1)首先,我们来求解4000H ~ 43FFH地址空间的总容量,使用终止地址-起始地址+1即可得到总容量,也就是43FFH - 4000H + 1 = 43FFH + 1 - 4000H = 4400H - 4000H = 400H。

注意:在计算机中,以H结尾的数字是十六进制,逢16进1,而F在十六进制中表示15,所以,43FFH + 1 = 4400H。

所以,4000H ~ 43FFH地址空间的总容量为400H。

(2)接下来,我们要把400H转换成二进制,对于十六进制数转换成二进制数来说,每一位十六进制数对应着四位的二进制数,我们可以把400H拆分成4、0、0三部分,4转换成二进制数就表示0100,十六进制的0转换成二进制为0000。所以,400H转换成二进制的结果为:0100 0000 0000。

0100 0000 0000也就是2的10次方,即为2^10^。

(3)题目中说的每个存储单元可存储16位二进制数,所有总共可以存储的二进制数就是:2^10^ * 16。

(4)该区域使用4片存储器芯片构成,所以,存储芯片的容量为:2^10^ 16 / 4 = 2^10^ 4 = 2^12^,最终的结果单位为bit。

总线

一条总线同一时刻只允许一个设备发送数据,但允许多个设备接收数据。

总线的分类

总线可以分为数据总线、地址总线和控制总线。

  • 数据总线(Data Bus):在CPU和RAM之间来回传送需要处理或是需要存储的数据。
  • 地址总线(Address Bus):用来指定在RAM(Random Access Memory)之中存储的数据的地址。
  • 控制总线(Control Bus):将微处理器控制单元(Control Unit)的信号传送到周边设备,一般常见的为USB Bus和1394 Bus。

串联系统与并联系统

这里,先给小伙伴们简单介绍下什么是串联系统,什么是并联系统。

串联系统

串联系统是组成系统的所有单元中任一单元失效就会导致整个系统失效的系统。把用电器各元件逐个顺次连接起来,接入电路就组成了串联电路。我们常见的装饰用的"满天星"小彩灯,常常就是串联的。串联电路有以下一些特点:⑴电路连接特点:串联的整个电路是一个回路,各用电器依次相连,没有"分支点"。⑵用电器工作特点:各用电器相互影响,电路中一个用电器不工作,其余的用电器就无法工作。⑶开关控制特点:串联电路中的开关控制整个电路,开关位置变了,对电路的控制作用没有影响。即串联电路中开关的控制作用与其在电路中的位置无关。

注:以上对于串联系统的描述来源于网络。

接下来,我们来看一个关于串联系统的图形表示,这里我们假设串联系统中每个部分的可靠度依次为R1,R2,...Rn,如下所示。

则整个系统的可靠度为:R = R1 R2 ... * Rn。

并联系统

并联系统指的是组成系统的所有单元都失效时才失效的系统。把电路中的元件并列地接到电路中的两点间,电路中的电流分为几个分支,分别流经几个元件的连接方式叫并联。并联电路是使在构成并联的电路元件间电流有一条以上的相互独立通路,为电路组成二种基本的方式之一。例如,一个包含两个电灯泡和一个9 V电池的简单电路。若两个电灯泡分别由两组导线分开地连接到电池,则两灯泡为并联。

即若干二端电路元件共同跨接在一对节点之间的连接方式。这样连成的总体称为并联组合。其特点是:①组合中的元件具有相同的电压;②流入组合端点的电流等于流过几个元件的电流之和;③线性时不变电阻元件并联时,并联组合等效于一个电阻元件,其电导等于各并联电阻的电导之和,称为并联组合的等效电导,其倒数称为等效电阻

注:以上对于并联系统的描述来源于网络。

接下来,我们来看一个关于并联系统的图形表示,这里我们假设并联系统中每个部分的可靠度依次为R1,R2,...Rn,如下所示。

则整个并联系统的可靠度R = 1 - (1 - R1) (1 - R2) ... * (1-Rn)。

混合型系统

混合型系统就是既有串联系统,又有并联系统的系统,这里,我们也使用一个图形进行表示,如下所示。

所以,上图并联系统的可靠度为:R (1 - (1 - R) ^3^) (1 - (1 - R) ^2^)

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月20日

计算机中的层次化存储是个什么鬼?

写在前面

撸代码只是程序员的一项最基本的技能,除此之外,还有很多知识需要程序员掌握。【程序员进阶系列】专题,旨在分享程序员想要进一步提升自我,突破发展瓶颈的一系列技术。今天,我们来一起聊聊计算机中的层次化存储结构。

文章已收录到:

https://github.com/sunshinelyz/technology-binghe

https://gitee.com/binghe001/technology-binghe

层次化存储结构

首先,问小伙伴们一个问题:计算机的存储结构为什么需要进行层次化的划分呢?

说的直接一点:就是为了减少经济成本。如果说,CPU的价格非常便宜的话,根本就不需要内存了。可以把所有的内存容量全部都做到CPU里面去,就可以了。但是,事实上,CPU的内存是很精贵的,至今为止,CPU中基本上还是一级缓存和二级缓存。三级缓存比较少见。而且,CPU中的存储容量是非常小的,基本都是KB级别的存储,CPU的内存容量也就几KB,MB级别的CPU内存也是比较少见的。所以,出于经济成本的考虑,计算机中的存储结构是按照层次进行划分的。

为了能够让小伙伴们更加清晰的理解层次化存储结构,我们先来看一张图。

由上图,可以看出:

(1)层次化的存储结构可以分为:CPU、Cache(高速缓存)、主存(内存)、外存(辅存)。

(2)从上往下,速度越来越慢,容量越来越大。

局部性原理是层次化存储结构的支撑。

局部性原理

一个编写良好的计算机程序常常具有良好的局部性。也就是说。它们倾向于引用临近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。这汇总倾向性,就被称为局部性原理,这是一个持久的概念,对硬件和软件系统的设计和性能都有着极大的影响。

之所以有这个规律,很多人认为原因是:程序的指令大部分时间是顺序执行的,而且程序的集合,如数组等各种数据结构是连续存放的。

局部性原理讲的是:在一段时间内,整个程序的执行仅限于程序的某一部分,相应地,程序访问的存储空间也局限于某个内存区域。主要分为两类:

  • 时间局部性:如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
  • 空间局部性:是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。

Cache

针对Cache相关的技术,我们主要来聊聊Cache的概念和映像相关的技术。

Cache-概念

这里的Cache表示的是高速缓冲,在计算机的存储体系系统中,Cache是除寄存器外访问速度最快的层次。 使用Cache改善系统性能的依据是程序的局部性原理

如果以h代表对Cache的访问命中率,t1表示Cache的周期时间,t2表示主存储器的周期时间,以读操作为例,使用“Cache+主存储器”的系统的平均周期为t3,则可以得出如下运算公式。

t3 = h * t1 + (1 - h) * t2 

其中。(1 - h)又称为失效率,也就是未命中率。

Cache-映像

Cache的映像分为三种,分别是:直接相联映像、全相联映像、组相联映像。

  • 直接相联映像:硬件电路比较简单,但冲突率最高。
  • 全相连映像:电路难于设计和实现,只适用于小容量的Cache,冲突率比较低。
  • 组相联映像:直接相联与全相联的折中。

地址映像是将主存与Cache的存储空间划分为若干大小相同的页(或称为块)。

例如,一台计算机的主存容量为1GB,划分为2048页,每页512KB;Cache的容量为8MB,划分为16页,每页512KB。接下来,我们由此来详细图解直接相联映像、全相联映像和组相联映像。

直接相联映像

我们可以画一组图来表示Cache的直接映像。首先,我们先来简单画一个主存标记、Cache页号和页内地址的示意图。如下所示。

如上图所示,主存标记为7位,Cache页号为4位,页内地址为19位。

记录主存区号的示意图如下所示。

有了上面两张图的基础后,我们再来看直接相联映像的示意图如下所示。

这里,我们将容量为1GB的主存划分成2048页,总共127个区,每页的容量为512KB。将容量为8MB的Cache划分为16页,每页容量为512KB。

所谓直接相联映像是指Cache中的0页只能存储主存中0页的内容,这里主存中0页指的是每个区的0页,比如上图中的0区的0页,1区的16页,127区的2032页等。

在直接相联映像中,只需要记录主存标记、Cache页号和页内地址就能够快速的找到主存中的数据。

使用直接相联映像有个缺点:那就是如果Cache中的0页,存储了主存中0区0页的内容时,如果此时需要存储主存1区中的16页内容,就只能将主存0区中0页的内容从Cache的0页中清除,然后将主存1区中16页的内容存储到Cache中的0页内。冲突率比较高。细心的小伙伴会发现:这其实是违背局部性原理的。

直接相联映像访问速度最快,但冲突率最高。

全相连映像

我们先来看下全相联映像的主存页标记和页内地址的示意图,如下所示。

此时,使用11位来标识主存页标记,使用19位来标识页内地址。

使用全相连映像需要记录主存与Cache的对应关系,如下图所示。

接下来,我们来看看全相连映像的示意图,如下所示。

从图中可以看出,Cache中的任何一个也,都可以存储主存中的任何一个页。

使用全相连映像访问速度最慢,冲突率最低。

组相联映像

组相联映像本质上是直接相联映像和全相联映像的折中。同样的,我们先来看组相连映像的存储示意图。

此时,在组相连映像中,Cache组号使用3位表示,组内页号使用1位表示,页内地址使用19位表示。其中,3位的Cache组号,1位的组内页号和前面的7位构成了主存页标记;3位的Cache组号,1位的组内页号和19号的页内地址构成了Cache地址。

接下来,我们再来看看主存与Cache的对应关系,如下图所示。

组相连的映像示意图如下所示。

由上图可知,在组相连映像中,主存的组与Cache的组是组相联映像关系,而在组内则是通过直接相联映像来访问和存储数据。

好了,我已经肝不动了。今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月9日

计算机中的流水线技术到底是个啥?

写在前面

流水线技术是计算机中的一种相当重要的技术。简单的说,流水线技术的出现使得计算机能够支持并行计算,能够并行执行任务。尽管流水线技术非常重要,但是很多小伙伴对流水线技术还是一知半解,甚至很多小伙伴根本不知道什么是流水线技术,今天,我们就一起来说说什么是流水线技术。

文章已收录到:

https://github.com/sunshinelyz/technology-binghe

https://gitee.com/binghe001/technology-binghe

流水线概念

流水线是指在程序执行时,多条指令重叠进行操作的一种准并行处理的实现技术。各种部件同时处理是针对不同指令而言的,它们同时为多条指令的不同部分进行工作,以提高各部件的利用率和指令的平均执行速度。

流水线的相关参数计算包括:流水线执行时间计算、流水线吞吐率、流水线加速比、流水线效率。

在计算机中,对于指令的操作主要分为三个部分:取指、分析和执行。如下所示。

如果执行取值、分析和执行各需要1ms的话,则串行执行三条指令的时间总共需要9ms。这是因为一条执行的操作需要经过取指、分析和执行三个步骤,每个步骤需要1ms,执行一条指令的时间为3ms,则串行执行三条指令的时间为9ms。我们可以用下图来表示这个过程。

在上图的表示中,貌似执行三条指令使用9ms是没啥问题的。但是,如果我们把图形改造一下,我们就会发现相应的问题。我们使用下面的图形来表示执行三条指令的情况。

此时,我们发现,在上图执行指令操作的过程中,有很多空白的格子,而空白的格子表示在执行执行的过程中有空余的时间片资源没有利用起来。很显然,没有必要等待指令1完全执行完毕后再执行指令2,同样的,没有必要等待指令2完全执行完毕后再执行指令3。而且,我们发现按照上图执行完三条指令需要9ms时间。

此时,如果将空余的时间片利用起来,则可以使用下图来表示。

此时,在执行三条指令的过程中,取指操作对指令1执行完取指后,马上对指令2进行取指,然后又马上对指令3进行取指;分析操作同样是对指令1执行完分析后,马上对指令2进行分析,然后又马上对指令3进行分析;执行操作也是对指令1执行完毕后,马上对指令2进行执行操作,然后又马上对指令3进行执行操作。期间,将空余的时间片资源充分的利用起来了。而且,我们发现,充分利用空余的时间片后,执行三条指令的时间由原来的9ms变为现在的5ms。

从另一个角度,我们发现执行完第一条指令时,需要3ms,执行完第二条指令时,只需要在执行完第一条指令的基础上增加1ms。同样的,执行完第三条指令时,只需要在执行完第二条指令的基础上增加1ms。以后每增加一条指令,只需要增加1ms的时间便可以执行完此条指令。

这就是计算机中的流水线技术。接下来,我们就说说流水线技术的相关计算问题。

流水线计算

关于流水线计算,我们先来看一个图。

在上图中,我们可以看出,执行完第一条指令时,需要3ms时间,执行完第二条指令时,只需要在执行完第一条指令的基础上增加1ms;执行完第三条指令时,只需要在执行完第二条指令的基础上增加1ms。以此类推,执行完第n条指令时,只需要在执行第n-1条指令的基础上增加1ms。说到这里,不知道小伙伴们有没有思考这样一个问题,流水线技术的这种规律就涉及到一个非常重要的概念,叫作 流水线周期

流水线周期为执行时间最长的一段,上图中的流水线周期为1ms

流水线的计算公式为:

1条指令执行时间 + (指令条数 -1)*  流水线周期

流水线的理论公式如下所示。

(t1 + t2 + ... + tk) + (n-1) * △t

其中t1,t2...tk表示执行一条指令的每个步骤分别需要的时间,n为指令的条数,△t为流水线周期。

流水线的实践公式如下所示。

k*△t + (n-1) * △t

其中,k为执行一条指令的步骤数,n为指令的条数,△t为流水线周期。

这里,给小伙伴们举一个例子。

例如,一条执行的执行过程可以分解为取指,分析和执行三步,在取指时间t取指=3△t,分析时间分析=2△t,执行时间t执行=4△t的情况下,若按照串行方式执行,则10条指令全部执行完需要多少△t?若按照流水线方式执行,流水线周期为多少△t?使用流水线方式时,执行完10条指令需要多少△t?

(1)串行方式比较简单,就是将每条指令的执行时间进行累加。

(3△t + 2△t + 4△t) * 10 = 90△t。

(2)在执行一条指令的过程中,取指为3△t,分析为2△t,执行为4△t。根据流水线中对于流水线周期的定义:流水线周期为执行时间最长的一段,所以,流水线周期为4△t。

(3)使用流水线方式时,执行完10条指令需要的时间可以使用如下方式进行计算。

这里,我们分别计算下理论时间和实践时间。

  • 理论时间

(3△t + 2△t + 4△t) + (10-1) * 4△t = 45△t。

  • 实践时间

3 4△t + (10-1) 4△t = 48△t。

超标量流水线

关于超标量流水线,我们可以使用下图来表示。

在超标量流水线中,有一个概念叫作度。度表示在超标量流水线中,由几条流水线组成。例如上面的图中,超标量流水线由两条流水线组成,所以,度为2。此时的超标量流水线可以同时进行2个操作。也就是说,可以同时执行两个取指操作,可以同时执行两个分析操作,也可以同时执行两个执行操作。

如果此时有10条指令需要执行,使用以上超标量流水线的话,只需要10 / 2 = 5 条指令的时间。

流水线吞吐率计算

流水线的吞吐率(TP)是指在单位时间内流水线所完成的任务数量或输出的结果数量。计算流水线吞吐流程的最基本的公式如下所示。

流水线最大吞吐率计算公式如下所示。

流水线的吞吐率计算问题相对来说还是比较简单的。

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月8日

同时拿到BATJMD的Offer是怎样的一种体验?

写在前面

又到了收割Offer的季节,你准备好了吗?曾经的我,横扫各个大厂的Offer。还是那句话:进大厂临时抱佛脚是肯定不行的,一定要注重平时的总结和积累,多思考,多积累,多总结,多复盘,将工作经历真正转化为自己的工作经验。

今天,我就跟小伙伴们分享一些经典的大数据面试题,跟我一起横扫各个大厂的Offer!后续,我会给大家输出一篇我平时是如何在工作过程中总结经验的,希望能够给小伙伴们带来实质性的帮助。不多说了,开始今天的主题——面经分享。

面经分享

今天给大家分享一个面试大厂的完整面经,小伙伴们可以对照下,这些面试题自己是否都会了呢?欢迎文末留言说出你的答案!如果你想提升自己的技术,或者对自己的发展比较迷茫,都可以在文末说出你感受!

一轮技术面(90分钟)

1.hashmap和hashtable区别
2.为什么产生死锁
3.jvm类加载
4.java反射获取私有属性,改变值
5.反射用途
6.所用数据库
7.项目难点,问题
8.如何解决项目中遇到的问题
9.项目中遇到最自豪的地方
10.会什么算法

二轮技术面(120分钟)

1.讲项目
2.数据库乐观锁使用
3.状态机
4.如何解决状态机不对问题
5.如何分库分表
6.MySQL极限
7.HashMap源码
8.设计一个线程安全的HashMap
9.快排的实现,时间复杂度和空间复杂度
10.会什么算法
11.如何把项目变成SOA架构
12.Spring源码,最深刻的模块,aop用途
13.JVM内存模型
14.垃圾回收机制
15.项目中查看垃圾回收

三轮技术面(150分钟)

1.ConcurrentHashMap底层原理?
2.手写一个LRU(用LinkedHashMap)
3.HashMap底层数据结构?
4.JDK1.8中的HashMap为什么用红黑树不用普通的AVL树?
5.为什么在8的时候链表变成树?
6.为什么在6的时候从树退回链表?
7.线程池7个参数,该怎么配置最好?
8.说一下volatile
9.volatile的可见性和禁止指令重排序怎么实现的?
10.CAS是什么?PriorityQueue底层是什么,初始容量是多少,扩容方式呢?
11.若原始大小<64,则扩容为原来的2倍+2,不然就扩容为原来的1.5倍
12.HashMap的容量为什么要设置为2的次幂?
13.你知道跳表吗,什么场景会用到?
14.CopyOnWriteArrayList知道吗,迭代器支持fail-fast吗?
15.innodb的底层数据结构?
16.为什么用B+树不用B树?
17.为什么用B+树不用红黑树?
18.coding:无序数组怎么寻找第k大的数,写一个二叉树层次遍历
19.不知道大小的数据流取其中100个数,怎样的取法能最随机
20.n个物品每个物品都有一定价值,分给2个人,怎么分两个人的价值差最小

四轮技术面(120分钟)

1.项目中的权限管理
2.登录状态如何储存
3.session和cookie的区别,session如何管理
4.HashMap底层结构
5.synchronized关键字的用法
6.synchronized修饰类方法和普通方法的锁区别,获取类锁之后还能获取对象锁吗
7.类加载器的双亲委派模型的作用,能重复加载某个类吗
8.类加载器的类的缓存,key是什么
9.介绍Redis
10.如何将数据分布在不同的Redis
11.有了解过取余算法?
12.spring的apo实现
13.字节码结构
14.浏览器输入网址过程,结合springmvc

五轮技术面(120分钟)

1.HashMap在大量哈希冲突该怎么处理
2.红黑树比BST优点
3.MySQL为什么使用B+树
4.多个索引会有多份数据吗
5.数据库的隔离级别和解决的问题
6.数据库默认隔离级别,一定会产生幻读吗,怎么解决
7.输入网址到展示的整个过程,结合springmvc来讲
8.负载均衡的算法
9.哈弗曼编码,如何解决译码问题
10.实习会对工作有影响吗
11.用英文介绍一个项目
12.如何查看系统负载
13.描述一个解决问题的过程
14.如何把文件从服务器复制到本地,用什么命令

六轮技术面(90分钟)

1.讲讲你所了解的JVM垃圾收集算法
2.项目中用的哪些技术
3.项目如何设计流程流转,如果是你的话该怎么设计
4.MySQL使用的索引结构,查找效率
5.MySQL查询优化
6.MySQL慢查询开启,语句分析
7.HashMap查找效率
8.JVM内存模型
9.设计模式,策略模式的使用场景
10.如何确保单例线程安全
11.Spring的bean的默认范围
12.对Netty的了解
13.未来发展规划

七轮HR面(40分钟)

1.说一下发展方向
2.说一下经验不足导致的问题
3.说一下挑战杯的工作内容
4.说一下你给挑战杯项目的主要贡献
5.实习时间
6.拿到了哪些offer,你是怎么考虑的
7.为什么不选择腾讯
8.腾讯技术栈和阿里技术栈的区别

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月6日

即使技术再精,面试时一问这个必挂!!

写在前面

在前几年面试Java高级程序员的时候,只要是会一点JVM的基础知识,基本就都能够面试通过了。最近几年,对Java工程师的要求越来越严格,对于中级Java工程师来说,也需要掌握JVM相关的知识了。这不,一名读者出去面试Java岗位,就被问及了JVM相关的类的加载、链接和初始化的问题。结果凉凉了,今天,我们就一起来详细探讨下这个问题。

文章已收录到:

https://github.com/sunshinelyz/technology-binghe

https://gitee.com/binghe001/technology-binghe

概述

本文我们一起讨论Java类的加载、链接和初始化。 Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是 java.lang.Class类 的对象。一个Java类从字节代码到能够在JVM中被使用,需要经过加载、链接和初始化这三个步骤。这三个步骤中,对开发人员直接可见的是Java类的加 载,通过使用Java类加载器(class loader)可以在运行时刻动态的加载一个Java类;而链接和初始化则是在使用Java类之前会发生的动作。本文会详细介绍Java类的加载、链接和 初始化的过程。

Java 类的加载

Java类的加载是由类加载器来完成的。

一般来说,类加载器分成两类:启动类加载器(bootstrap)和用户自定义的类加载器(user-defined)。

两者的区别在于启动类加载器是由JVM的原生代码实现的,而用户自定义的类加载器都继承自Java中的 java.lang.ClassLoader类。在用户自定义类加载器的部分,一般JVM都会提供一些基本实现。应用程序的开发人员也可以根据需要编写自己的类加载器。 JVM中最常使用的是系统类加载器(system),它用来启动 Java应用程序的加载。通过java.lang.ClassLoader的 getSystemClassLoader()方法可以获取到该类加载器对象。

类加载器需要完成的最终功能是定义一个Java类,即把Java字节代码转换成JVM中的java.lang.Class类的对象。但是类加载的过程并不是这么简单。

Java类加载器有两个比较重要的特征:层次组织结构和代理模式。

层次组织结构指的是每个类加载器都有一个父类加载器,通过 getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。前者称为初始类加载器, 而后者称为定义类加载器。

两者的关联在于:一个Java类的定义类加载器是该类所导入的其它Java类的初始类加载器。比如类A通过import导入了类 B,那么由类A的定义类加载器负责启动类B的加载过程。一般的类加载器在尝试自己去加载某个Java类之前,会首先代理给其父类加载器。当父类加载器找不到的时候,才会尝试自己加载。这个逻辑是封装在java.lang.ClassLoader类的 loadClass()方法中的。一般来说,父类优先的策略就足够好了。在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到的时候再代理给父类加载器。这种做法在Java的Web容器中比较常见,也是 Servlet规范推荐的做法。比如,Apache Tomcat为每个Web应用都提供一个独立的类加载器,使用的就是自己优先加载的策略。 IBM WebSphere Application Server则允许Web应用选择。

类加载器使用的策略

类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅是根据该类的二进制名称 ,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类是相同的。因此,即便是同样的Java字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出 java.lang.ClassCastException。这个特性为同样名称的Java类在JVM中共存创造了条件。在实际的应用中,可能会要求同一名称的Java类的不同版本在JVM中可以同时存在。通过类加载器就可以满足这种需求。这种技术在 OSGi中得到了广泛的应用

Java 类的链接

Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。在链接之前,这个类必须被成功加载。类的链接包括验证、准备和解析等几个步骤。验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出 java.lang.VerifyError错误。

准备过程则是创建Java类中的静态域,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对其它类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的 Java类被加载。不同的 JVM 实现可能选择不同的解析策略。

一种做法是在链接的时候,就递归的把所有依赖的形式引用都进行解析。而另外的做法则可能是只在一个形式引用真正需要的时候才进行解析。也就是说如果一个 Java 类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。考虑下面的代码:

public class LinkTest {
    public static void main(String[] args) {
        ToBeLinked toBeLinked = null;
        System.out.println("Test link.");
    }
}

类LinkTest 引用了类 ToBeLinked,但是并没有真正使用它,只是声明了一个变量,并没有创建该类的实例或是访问其中的静态域。

在 Oracle 的 JDK 6 中,如果把编译好的 ToBeLinked 的 Java 字节代码删除之后,再运行 LinkTest,程序不会抛出错误。这是因为 ToBeLinked 类没有被真正用到,而 Oracle 的 JDK 6 所采用的链接策略使得ToBeLinked 类不会被加载,因此也不会发现 ToBeLinked 的 Java 字节代码实际上是不存在的。如果把代码改成 ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相同的方法运行,就会抛出异常了。因为这个时候 ToBeLinked 这个类被真正使用到了,会需要加载这个类。

Java 类的初始化

当一个 Java 类第一次被真正使用到的时候,JVM 会进行该类的初始化操作。初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。考虑下面的代码:

public class StaticTest {
    public static int X = 10;
    public static void main(String[] args) {
        System.out.println(Y); //输出60
    }
    static {
        X = 30;
    }
    public static int Y = X * 2;
}

在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到下依次执行。因此变量 X 的值首先初始化成 10,后来又被赋值成 30;而变量 Y 的值则被初始化成 60。

Java类和接口的初始化时机

Java 类和接口的初始化只有在特定的时机才会发生,这些时机包括:

  • 创建一个 Java 类的实例。如
MyClass obj = new MyClass()
  • 调用一个 Java 类中的静态方法。如
MyClass.sayHello()
  • 给 Java 类或接口中声明的静态域赋值。如
MyClass.value = 10
  • 访问 Java 类或接口中声明的静态域,并且该域不是常值变量。如
int value = MyClass.value
  • 在顶层 Java 类中执行 assert 语句。
assert true;

通过 Java 反射 API 也可能造成类和接口的初始化。需要注意的是,当访问一个 Java类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。如下面的代码所示。

package io.mykit.binghe.test;
 
class B {
    static int value = 100;
    static {
        System.out.println("Class B is initialized."); // 输出
    }
}
 
class A extends B {
    static {
        System.out.println("Class A is initialized."); // 不会输出
    }
}
 
public class InitTest {
    public static void main(String[] args) {
        System.out.println(A.value); // 输出100
    }
}

在上述代码中,类 InitTest 通过 A.value 引用了类 B 中声明的静态域 value。由于 value是在类 B 中声明的,只有类 B 会被初始化,而类 A 则不会被初始化。

创建自己的类加载器

在 Java 应用开发过程中,可能会需要创建应用自己的类加载器。典型的场景包括实现特定的 Java 字节代码查找方式、对字节代码进行加密/解密以及实现同名 Java 类的隔离等 。创建 自己的 类加载 器并不 是 一件复杂 的事情 ,只需要继承自java.lang.ClassLoader 类并覆写对应的方法即可。 java.lang.ClassLoader 中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:

  • defineClass():这个方法用来完成从Java字节代码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。
  • findLoadedClass():这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
  • findClass():这个方法用来根据名称查找并加载Java类。
  • loadClass():这个方法用来根据名称加载Java类。
  • resolveClass():这个方法用来链接一个Java类。

这里比较 容易混淆的是 findClass()方法和 loadClass()方法的作用。前面提到过,在Java 类的链接过程中,会需要对 Java 类进行解析,而解析可能会导致当前 Java 类所引用的其它 Java 类被加载。在这个时候,JVM 就是通过调用当前类的定义类加载器的 loadClass()方法来加载其它类的。 findClass()方法则是应用创建的类加载器的扩展点。应用自己的类加载器应该覆写 findClass()方法来添加自定义的类加载逻辑。loadClass()方法的默认实现会负责调用 findClass()方法。前面提到,类加载器的代理模式默认使用的是父类优先的策略。这个策略的实现是封装在 loadClass()方法中的。如果希望修改此策略,就需要覆写 loadClass()方法。

下面的代码给出了自定义的类加载的常见实现模式

public class MyClassLoader extends ClassLoader {
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = null; //查找或生成Java类的字节代码
        return defineClass(name, b, 0, b.length);
    }
}

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月3日

冰河去腾讯了?

真的去腾讯了

没错,冰河去腾讯了。不过不是去工作,而是去聆听,去交流,去学习,去分享。

应朋友的邀约,不久前去腾讯交流学习了。这次的收获还是蛮大的,今天,跟小伙伴们分享下这次去腾讯交流和学习的体会。

最近,我的一名好朋友,腾讯Tn级别的大佬,在团队内部组织了一次技术交流分享会。我有幸被邀请了。哈哈,邀请我时,他说的很直接:请你来,因为你技术不错,可以给我们分享下你的经验,互相学习嘛。听到后,我确实有点受宠若惊,一个Tn级别的大佬邀请我去他们团队交流分享经验,确实是意料之外的事情。

不过话又说回来,这次去交流,本身就是一次很好的学习机会,而且是免费交流学习的机会哦!于是我抱着学习的心态答应了。

学习交流

收到邀请后的那两天,我其实是很纠结的,纠结自己到底要分享哪些内容。几经思考,还是分享些我比较擅长的大数据吧。我分享的题目是《新一代大数据引擎Flink在企业级的大规模实践》,主要分享了我在使用Flink框架进行海量数据分析过程中的实践经验、为何选用Flink,Flink的使用场景,帮助企业解决了哪些业务问题,踩过哪些坑、解决方案是什么,Flink框架不满足业务需求时,我是如何改写Flink源码使其满足公司业务场景的。

当然,在分享的过程中,我也说了一些我个人对大数据领域的见解和体会。整体分享的氛围还是比较轻松和活跃的,在场的小伙伴也挺积极的,问了一些有深度的问题。我也跟大家一起讨论了这些问题。

其实,在现场能够看出来,大厂的小伙伴们对于技术和知识的渴望也是很强烈的。很多小伙伴都在问我:这么多的技术,你是如何学习的呢?其实,回答这个问题很简单:就是在工作中不断的积累,工作后,很多知识不可能再有时间去系统性学习了,这就要求我们在平时的工作过程中多注意积累,多总结,多思考,将做过的项目、学到的技术和技能,真正转化为自己的经验。

因为这次我主要分享的是大数据领域中的Flink框架,一些小伙伴想从Hadoop开始进军大数据领域。于是,借此机会我也向他们推荐了我个人出版的《海量数据处理与大数据技术实战》这本书(目前,天猫、京东、当当均在售),嘿嘿,推荐效果不错哦~~

会上分享的时间还是比较短的,每个人大概只有半小时左右。会后,我又跟其他小伙伴聊了各种技术话题,包含:大数据、中间件、开源项目等等。还有一些天马行空的想象。

总体来说,这次学习交流收货还是蛮大的,也能够看出,大厂的小伙伴对技术和知识的学习热情也很高。

事后总结

通过这次学习交流,给我的第一感觉就是:大厂的小伙伴们都很随和,比如我的那个Tn级别的大佬朋友,为人处世都很谦和。在大厂也是要学习的,很多小伙伴认为进了大厂,就好比进了一家很稳定的企业。其实,在大厂的竞争还是蛮大的。这里的竞争分为两种,一种是团队与团队之间的竞争,一种是个人与个人之间的竞争。如果你想在大厂中不断的取得竞争优势,那你就要有居安思危的意识,时刻提升自己的水平和认知。这样,才能让你在日益激烈的竞争环境中脱颖而出。

如果你想成为某个领域的佼佼者,那就从现在开始,为自己定个目标,朝着目标不断前进。还是那句话:再牛逼的技术,也抵不过傻逼似的坚持,专注于某个领域,你也会成为技术专家。

最后,给小伙伴们点建议,如果你想进大厂,临时抱佛脚,是肯定进步了大厂的。重在平时工作的积累,不断总结,不断复盘,不断反思,不断提升自己。很多小伙伴为了面试而面试,为了应付面试官而临时背题。其实,这些大可不必,只要工作过程中认真总结,认真积累,进大厂是很自然的事情。另外,面试官在面试过程中,能够听出来面试者的水平到底有多高。所以,不要忽悠面试官,他比你懂的多。

好了,今天就到这儿吧,关于我分享的具体内容,后面我整理下发出来,大家有啥问题直接在下面留言吧。也可以加我微信:sun_shine_lyz,我拉你进群,一起学习,一起交流。~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 2月1日

图解计算机结构与体系分类!!

写在前面

今天,我们继续更新【程序员进阶系列】专题,冰河带你从零入坑程序员。接下来,我们一起聊聊计算机的结构和体系分类。

文章已收录到:

https://github.com/sunshinelyz/technology-binghe

https://gitee.com/binghe001/technology-binghe

计算机结构

计算机结构主要由运算器、控制器、存储器、输入设备和输出设备组成。简化的结构图如下图所示。

接下来,我们再看看看其详细的结构图如下所示。

其中,主存储器又叫做内存储器,也就是内存;辅助存储器又叫做辅存,也就是外存储器,例如磁盘;CPU的核心部件为运算器和控制器。

CPU由运算器、控制器、寄存器组和内部总线组成。

运算器包含:算术逻辑单元、累加寄存器、数据缓冲寄存器、状态条件寄存器。

  • 算术逻辑单元(ALU):数据的算术运算和逻辑运算。
  • 累加寄存器(AC):通用寄存器,为ALU提供一个工作区,用于暂存数据。
  • 数据缓冲寄存器(DR):写内存时,暂存指令或数据。
  • 状态条件寄存器(PSW):存储状态标志和控制标志,有时也可以将状态条件寄存器归为控制器部分。

控制器包含:程序计数器、指令寄存器、指令译码器、时序部件。

  • 程序计数器(PC):存储下一条要执行的指令的地址。
  • 指令寄存器(IR):存储即将执行的指令。
  • 指令译码器(ID):对指令中的操作码字段进行分析解释。
  • 时序部件:提供时序控制信号。

计算机体系结构分类

首先,我们先来看一个在计算机领域中,对计算机的体系结构进行分类的一种经典方法,就是Flynn分类法,Flynn分类法将计算机分成单指令流单数据流、单指令流多数据流、多指令流单数据流、多指令流多数据流。

具体信息如下表所示。

体系结构类型结构关键特性代表
单指令流单数据流(SISD)控制部分:一个 处理器:一个 主存模块:一个单处理器系统
单指令流多数据流(SIMD)控制部分:一个 处理器:多个 主存模块:多个各处理机以异步的形式执行同一条机灵并行处理机、阵列处理机、超级向量处理机
多指令流单数据流(MISD)控制部分:多个 处理器:一个 主存模块:多个被证明是不可能的,至少是不实际的目前没有,有资料记载流水线处理机为此类
多指令流多数据流(MIMD)控制部分:多个 处理器:多个 主存模块:多个能够实现作业、任务、指令等各级全面并行多处理机系统、多计算机

指令的基本概念

一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的格式如下所示。

其中,操作码部分指出了计算机要执行什么性质的操作,例如,加法、减法、取数、存数等。地址码字段需要包含各操作数的地址及操作结果的存放地址等,从其地址结构的角度可以分为三地址指令、二地址指令、一地址指令和零地址指令。

三地址指令

例如,执行a+b=c操作时,就是使用的三地址指令。此时如下所示。

二地址指令

例如,执行a+=b操作时,执行的就是二地址指令,此时如下所示。

一地址指令

例如,执行a++操作时,执行的就是一地址指令,此时如下所示。

零地址指令

例如,宕机就是零地址指令。

寻址方式

总体来说,寻址方式可以分为:立即寻址、直接寻址、间接寻址、寄存器寻址、寄存器间接寻址。

  • 立即寻址:操作数直接在指令中,速度快,灵活性差。
  • 间接寻址:指令中存放的是操作数的地址。
  • 间接寻址:指令中存放了一个地址,这个地址对应的内容是操作数的地址。
  • 寄存器寻址:寄存器存放操作数。
  • 寄存器内存放的是操作数的地址。

CISC与RISC

CISC和RISC分别表示复杂指令集系统和精简指令集系统,具体信息如下表所示。

指令系统类型指令存执方式实现方式其他
CISC(复杂)数量多、使用频率差别大,可变长格式支持多种微程序控制技术(微码)研发周期长
SISC(精简)数量少,使用频率接近,定长格式,大部分为单周期指令,操作寄存器,只有Load/Store操作内存。支持方式少增加了通信寄存器、硬布线逻辑控制为主,适合采用流水线优化编译,有效支持高级编程语言

如何比较CISC和RISC,分哪些维度?

指令数量、指令使用频率、存执方式、寄存器、流水线支持、高级语言支持。

  • CISC:复杂、指令数量多,频率差别大、多寻址。
  • RISC:精简、指令数量少。操作寄存器,单周期,少寻址,多通用寄存器,流水线,

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

冰河 发布了文章 · 1月30日

真正牛逼的人,都是极简主义者!!

写在前面

一个真正牛逼的人,都是极简主义者;一款真正伟大的产品,都是极简主义的产品。一个人只要简单的专注于自己的领域,在思想上删繁就简,成为某个领域和某个行业的专家是很顺其自然的事情。一款产品,立足于用户刚需,解决用户痛点,在不断的发展和迭代过程中,不断挖掘用户的潜在需求,真正能够解决用户的刚需,删繁就简,那这款产品就不怕没有人用。

极简主义者

我见过太多人动不动就说自己是全栈开发,其实当我听到xxx说自己是全栈开发时,其实我内心是鄙视的。为啥?你说你工作两年或者三年,动不动就说自己全栈?那你理解了什么是全栈吗?会点前端,会点CRUD就说自己是全栈吗?别扯了好不?

真正牛逼的人,一定是极简主义者。一个人不可能啥都会,也不可能啥都不会。

如果一个人认为自己啥都会,我就是全栈,那我可能正好会反过来想:这个人应该是啥都不会。道理很简单,你把自己复杂化了,啥都会但是啥都不精,那是不是可以从侧面说明这个人啥都不会呢?

所以,小伙伴们,最好不要一开始就将自己定位为全栈开发者。专注某个领域,纵向深入发展,直到精通后,再横向扩展自己的知识领域,不要一开始就把自己定位为全栈开发者。否则,你会发现自己貌似啥都会点,但是就是掌握的不够深入,从本质上讲,你就是啥也不会。

极简的产品

不知道大家有没有思考过这样一个问题:流行的一些产品或者说当下热度很高的产品,一定是极简化的,它可能并没有太复杂的用户使用流程,也没有太过于繁琐的用户引导。但是它一定能够解决用户刚需,解决用户的痛点。

这样的一款产品一定是经过团队深思熟虑,对于行业或者用户需求有着深刻的理解。我很喜欢的一句话就是: 用户提出的需求,你要变着法的给他,用户说的也不一定全是对的。

删繁就简,解决用户刚需,解决用户痛点,一定是一个产品长期生存和发展的关键所在。

往往那些做的功能很全,包罗万象,啥都想做,堆砌了各种功能的所谓的产品,到头来反而没有人用了。

一个产品难的地方不在于对它累加各种功能,而是如何对它做减法,如何删减那些伪需求带来的不必要的功能。

总结

做人做事,都要简化。还是那句话:一个真正牛逼的人,都是极简主义者;一款真正伟大的产品,都是极简主义的产品。

好了,今天就到这儿吧,我是冰河,大家有啥问题可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你进群,一起交流技术,一起进阶,一起牛逼~~

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 10 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Mycat

    2013年阿里的Cobar在社区使用过程中发现存在一些比较严重的问题,及其使用限制,经过Mycat发起人第一次改良,第一代改良版——Mycat诞生。 Mycat开源以后,一些Cobar的用户参与了Mycat的开发,最终Mycat发展成为一个由众多软件公司的实力派架构师和资深开发人员维护的社区型开源软件。截至2015年12月,超过4000名用户加群或研究讨论或测试或使用Mycat。

注册于 2018-07-20
个人主页被 2.6k 人浏览