Redis的String
假设,我们用String 类型存储一个键值对,他们的长度都是10位数,其实用两个8字节的Long类型表示就可以了。Long最大可以表示2^64次方,表示10为数字绝对没问题。
但是Redis中的String 类型表示这一对 key 和 val 却用了64字节,这是因为String类型实际上还需要额外的内存空间记录数据长度,空间使用,等信息。如果用String存储比较小的数据,额外的空间就显得比较大。
当我们保存64位有符号整数时,String类型会把它保存为一个8字节的Long类型整数,这种保存也叫 int 编码。如果保存的数据中包含字符串时,String 类型就会用 简单动态字符串(SDS)来保存。其结构如下图所示:
buf:字节数组,保存实际的数据,为了表示数据结束,会在数组后加一个”\0”,这就会额外占用1个字节的开销。
len:占4个字节,表示buf已使用的长度。
alloc:占4个字节,表示buf的实际分配长度,一般大于len。
对于String,除了SDS的额外开销,还有一个来自于RedisObject结构体的开销。
Redis 的数据类型有很多,而且,不同数据类型 ...
如何保证消息不丢失
如何保证消息不丢失一般来讲,消息队列都会有一定的机制去保证消息的不丢失,丢失消息大多数是使用问题。
检测消息是否丢失这里有一个逻辑上的处理办法,就是发送消息时,给消息一个编号,然后利用消息的有序性来判断消息是否丢失。这里需要配合消息队列客户端的拦截机制去做,在拦截器中去检测消息的连续性,这样检测消息连续性的代码就不会侵入业务层。
但是需要注意的一点是,像Kafka和RocketMQ这种消息队列,他们并不能保证消息在Topic上是连续的,只能保证在分区内是连续的,那么我们在编号时,就需要按照分区来做消息的递增,确保编号连续性的检测是在每一个分区内部进行的。
如果producer是多实例的,由于并不好协调不同producer之间发送的顺序,所以该编号最好也是按照每个Producer单独递增的。
Consumer 实例的数量最好和分区数量一致,做到 Consumer 和分区一一对应,这样会比较方便地在 Consumer 内检测消息序号的连续性。
这里还需要考虑一种情况,就是消息重复发送的问题。因为难免会出现消息确认延迟,但实际上消息发送成功了,这时候,在消息的消费端,就需要保证消息的幂等性。 ...
消息积压该如何处理
消息积压该如何处理一般来说,消息积压的直接原因,是因为系统中某个部分出了问题,来不及处理上游发送的消息,才会导致消息积压。
优化性能避免消息积压一般来说,我们并不需要考虑消息队列本身的性能,因为它的处理消息的能力要远强于我们业务处理能力。也就是说,性能瓶颈一般出现在生产者和消费者这两端。
1. 发送端性能优化发送端一般都是执行完业务逻辑后,才开始发送消息。如果发送端速度过慢,则考虑是否是因为发送端业务逻辑执行太慢。
Producer 发送消息的过程:Producer 发消息给 Broker,Broker 收到消息后返回确认响应,这是一次完整的交互,它的耗时是以下几个步骤耗时的和:
发送端准备数据、序列化消息、构造请求等逻辑的时间,也就是发送端在发送网络请求之前的耗时;
发送消息和返回响应在网络传输中的耗时;
Broker 处理消息的时延。
这里,如果想提高发送端的性能,最好的办法就是多线程发送消息。如果是一般的web项目,那么它本身就是多线程的,并不需要我们手动开多线程去发送消息(消息的产生与业务相关,手动开多线程没有太大的意义)。如果是一些特殊场景,则可以尝试手动开多线程发送数据 ...
逃逸分析
逃逸分析逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”
在Java虚拟机的即时编译环境下,逃逸分析将判断新建的对象是否逃逸。判断逃逸的依据是:
对象是否被存入堆中(静态字段或堆中对象的实例字段)
对象是否被传入未知代码中。
前者很好理解,一旦对象放入堆中,其他线程就可以获得该对象的引用。即时编译器也因此无法追踪所有引用该对象代码的位置。
对于第二点,由于Java编译器的编译是以方法为单位的,对于方法中未被内联的方法调用,编译器会将其当作未知代码,因为无法确认该方法调用是否会将调用者或传入的参数存储至堆当中。所以可以把方法调用的调用者以及参数是逃逸的。
基于逃逸分析的优化即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。
对于锁消除,如果即时编译器能够证明锁对象(指的就是要被加锁的对象)不逃逸,那么对该锁对象的加锁解锁操作是没有意义的。因为其他线程无法获得该锁对象,也不可能对其进行加锁操作,即时编译器可以消除对该对象的加锁解锁操作。
传统编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于 Java 虚拟机即时编译 ...
写倾斜
写倾斜举一个例子,现在有两个医生在值班,医院的规定是如果当前有两个医生在值班,那么就有一个医生可以暂时离开。如果这两个医生同时点击申请离开,会开启两个事务,两个事务会先判断是否有两个医生在值班,因为两个事务同时执行,暂时不考虑隔离性,那么两个事务判断都满足条件,然后进入下一阶段,医生离开,减少当前医生数量,然后医生数量就变为0。这显然不满足需求。
这既不是一种脏读,也不是更新丢失,两笔事务更新的是两个不同的对象(两个医生的值班状态)。
根据可串行化定义,多个事务并行执行,他们的结果与事务串行执行的要一致,可以发现事务并没有达到穿行的效果。
可以说,写倾斜是一种不易察觉到的更新丢失,或者将写倾斜定义为一种更广义的数据更新丢失问题。
即如果两个事务读取相同的一组对象,然后更新其中一部分:不同的事务更新不同的对象,则可能发生写倾斜。而不同的事务如果更新的是同一个对象,则可能发生脏写或更新丢失。
可以发现,首先他会查询一些数据(需要满足一定的条件),暂定为数据集A,然后根据这些数据去做下一步的操作。
我们根据数据集A去做一些判断,然后执行后续的操作。
在第二步判断成功后,有一些操作修改了数据集 ...
方法内联
方法内联方法内联指的是在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。它不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。
举个例子,如果没有方法内联,当调用getter/setter方法时,程序需要保存当前方法的执行位置,创建并压入用于getter/setter的栈帧,访问字段,弹出栈帧,最后再恢复当前方法的执行,当内联了对getter/setter的方法调用后,上述操作仅剩字段访问。
即时编译器首先解析字节码,并生成IR图,然后在该IR图上进行优化,优化是由一个个独立的优化阶段串联起来的,每个优化阶段都会对IR图进行转换,最后即时编译器根据IR图的节点以及调度顺序生成机器码。
一个相对于IR图好理解的内联形式:
1234567public int add(int a, int b , int c, int d){ return add(a, b) + add(c, d);}public int add(int a, int b){ return a ...
即时编译(上)
即时编译通常而言,代码会先被 Java 虚拟机解释执行,之后反复执行的热点代码则会被即时编译成为机器码,直接运行在底层硬件之上。
在Java7之前,需要根据程序的特性,选择对应的即时编译器。对于执行时间比较短的,或者对启动性能有要求的,采用编译较快的C1,执行时间较长,或对峰值性能有要求的,采用生成代码执行效率较快的C2。
Java7引入了分层编译。将虚拟机的执行状态分为了5个层次,分别是:
解释执行
执行不带profiling的C1生成的机器码
执行仅带方法调用次数以及循环回边执行次数profiling的C1生成的机器码
执行带所有profiling的C1生成的机器码
执行C2生成的机器码
其中,C2代码的执行效率要比C1代码的高,而对于C1代码的三种状态,效率由高到低,因为profiling 越多,其额外的性能开销越大。profiling 是指在程序执行过程中,收集能够反映程序执行状态的数据。这里所收集的数据,称之为程序profile。profiler 大多通过注入 (instrumentation)或者 JVMTI 事件来实现的。
在 5 个层次的执行状态中,1 层和 4 层 ...
Synchronized
synchronizedsynchronized可以用来对程序加锁,synchronized内的代码又叫做同步代码块,它既可以用来声明一个synchronized代码块,也可以直接标记静态方法或者实例方法。
当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter 和 monitorexit 指令。这两种指令均会消耗操作数栈上的一个引用类型的元素(也就是synchronized 关键字括号里的引用),作为要加锁解锁的对象。
当查看使用了synchronized 关键字的代码编译过后的字节码时,可以发现会存在一个monitorenter 和多个monitorexit ,这是因为虚拟机需要保证加锁之后,不论是正常执行还是异常执行,都需要能够释放锁。
当用synchronized 标记方法时,虚拟机在进入该方法后进行monitorenter ,而退出方法时,不论是正常结束还是抛出异常,都需要进行monitorexit 。这里的monitorenter 和monitorexit 操作所对应的锁对象是隐式的。
对于实例方法来说,这两个操作对应的锁对象是this ...
2^32 怎么表示4GB
2^32 怎么表示4GB一般来说,32位最大支持4GB内存,怎么计算出来的呢?
如果按照这样计算: 2^32 bit = 2^29 byte = 2^19 KB = 2^9 MB = 0.5 GB,其实并不够4GB。
但其实,计算机规定8bit=1byte 就是1字节=8位,内存的大小就根据格子的多少来进行计算的。
实际上,4GB = 2^2 GB = 2^12 MB = 2^22 KB = 2^32 byte = 2^35 bit。也就是说,4GB需要35位。
实际上内存是把8个bit排成1组, 每1组成为1个单位, 大小是1byte(字节), cpu每一次只能访问1个byte, 而不能单独去访问具体的1个小格子(bit). 1个byte字节就是内存的最小的IO单位。
即这里3^32次方,后面的单位并不是bit,而是byte。
计算机操作系统会给内存每1个字节分配1个内存地址, cpu只需要知道某个数据类型的地址, 就可以直接去到读影的内存位置去提取数据了。
Java的方法调用
方法调用Java中方法的重载,在编译过程中可以完成识别,而具体调用哪一个方法,Java编译器会根据所传入参数的声明类型(注意与实际类型区分比如声明的是一个List,实际实例化传入的可能是一个ArrayList)来选取重载方法。选取过程分以下三个阶段:
在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;(这里是只找普通方法)
如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;(加入自动装箱拆箱的方法)
如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况 下选取重载方法。(所有方法)
如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。示例如下:
12345void invoke(Object obj, Object... args) { ... }void invoke(String s, Object obj, Obj ...