简栈

拥抱AI,持续成长

简介

Lombok是一款好用顺手的工具,就像Google Guava一样,在此予以强烈推荐,每一个Java工程师都应该使用它。Lombok是一种Java™实用工具,可用来帮助开发人员消除Java的冗长代码,尤其是对于简单的Java对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现Lombok,开发人员可以节省构建诸如hashCode()和equals()这样的方法以及以往用来分类各种accessor和mutator的大量时间。

IntelliJ安装Lombok

通过IntelliJ的插件中心安装

http://static.cyblogs.com/QQ20200425-164854@2x.jpg

最后需要注意的是,在使用lombok注解的时候记得要导入lombok.jar包到工程,如果使用的是Maven Project,要在pom.xml中添加依赖。

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>

Lombok用法

Lombok注解说明
  • val:用在局部变量前面,相当于将变量声明为final

  • @NonNull:给方法参数增加这个注解会自动在方法内对该参数进行是否为空的校验,如果为空,则抛出NPE(NullPointerException)

  • @Cleanup:自动管理资源,用在局部变量之前,在当前变量范围内即将执行完毕退出之前会自动清理资源,自动生成try-finally这样的代码来关闭流

  • @Getter/@Setter:用在属性上,再也不用自己手写setter和getter方法了,还可以指定访问范围

  • @ToString:用在类上,可以自动覆写toString方法,当然还可以加其他参数,例如@ToString(exclude=”id”)排除id属性,或者@ToString(callSuper=true, includeFieldNames=true)调用父类的toString方法,包含所有属性

  • @EqualsAndHashCode:用在类上,自动生成equals方法和hashCode方法

  • @NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor:用在类上,自动生成无参构造和使用所有参数的构造函数以及把所有@NonNull属性作为参数的构造函数,如果指定staticName = “of”参数,同时还会生成一个返回类对象的静态工厂方法,比使用构造函数方便很多

  • @Data:注解在类上,相当于同时使用了@ToString@EqualsAndHashCode@Getter@Setter@RequiredArgsConstrutor这些注解,对于POJO类十分有用

  • @Value:用在类上,是@Data的不可变形式,相当于为属性添加final声明,只提供getter方法,而不提供setter方法

  • @Builder:用在类、构造器、方法上,为你提供复杂的builder APIs,让你可以像如下方式一样调用Person.builder().name("Adam Savage").city("San Francisco").job("Mythbusters").job("Unchained Reaction").build();更多说明参考Builder

  • @SneakyThrows:自动抛受检异常,而无需显式在方法上使用throws语句

  • @Synchronized:用在方法上,将方法声明为同步的,并自动加锁,而锁对象是一个私有的属性$lock$LOCK,而java中的synchronized关键字锁对象是this,锁在this或者自己的类对象上存在副作用,就是你不能阻止非受控代码去锁this或者类对象,这可能会导致竞争条件或者其它线程错误

  • @Getter(lazy=true):可以替代经典的Double Check Lock样板代码

  • @Log:根据不同的注解生成不同类型的log对象,但是实例名称都是log,有六种可选实现类

    • @CommonsLog Creates log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
    • @Log Creates log = java.util.logging.Logger.getLogger(LogExample.class.getName());
  • @Log4j Creates log = org.apache.log4j.Logger.getLogger(LogExample.class);

    • @Log4j2 Creates log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);
  • @Slf4j Creates log = org.slf4j.LoggerFactory.getLogger(LogExample.class);

    • @XSlf4j Creates log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);
阅读全文 »

最近我们自己在重构项目,系统为了符合82原则(希望是80%的业务能通过穷举的方式固定下来,只有20%的允许特殊的定义),那么在固定一些标准流程以后,比如我们放大了原子服务的能力,当放大原子服务能力的时候,你就会发现,虽然抽象上看做的事情是一个意思,但是到实际去实现的时候发现还是各不相同。

在这里为了解决一个实现不同,但流程相同的问题,以及团队协作上的问题。我们引入的SPI (Service Provider Interface)

使用案例

通常情况下,使用ServiceLoader来实现SPI机制。 SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。

SPI机制可以归纳为如下的图:

http://static.cyblogs.com/20191111203102234.png

如果大家看过源代码或者说看过一些博客文章大概都清楚,在一些开源项目中大量的使用了SPI的方式,比如:mysql-connector-javadubbo等。

我们大概看眼MySQL的一个SPI实现

http://static.cyblogs.com/WechatIMG450.png

JDBC中的接口即为:java.sql.Driver

阅读全文 »

技术架构

http://static.cyblogs.com/rocketmq_architecture_1.jpgRocketMQ架构上主要分为四部分,如上图所示:

  • Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
  • Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
  • NameServerNameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后ProducerConumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。
  • BrokerServerBroker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。
  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • Client Manager:负责管理客户端(Producer/Consumer)和维护ConsumerTopic订阅信息
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • HA Service:高可用服务,提供Master BrokerSlave Broker之间的数据同步功能。
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

RocketMQ 网络部署特点

  • NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
  • Broker部署相对复杂,Broker分为MasterSlave,一个Master可以对应多个Slave,但是一个Slave只能对应一个MasterMasterSlave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示SlaveMaster也可以部署多个。每个BrokerNameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一MasterSlave,但只有BrokerId=1的从服务器才会参与消息的读负载。
  • ProducerNameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
  • ConsumerNameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的MasterSlave建立长连接,且定时向MasterSlave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。

结合部署架构图,描述集群工作流程:

  • 启动NameServerNameServer起来后监听端口,等待BrokerProducerConsumer连上来,相当于一个路由控制中心。
  • Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有TopicBroker的映射关系。
  • 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic
  • Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
  • ConsumerProducer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。

事务消息

Apache RocketMQ在4.3.0版中已经支持分布式事务消息,这里RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示。

http://static.cyblogs.com/rocketmq_design_10.png

阅读全文 »

第一句话:时间戳

时间不分东西南北、在地球的每一个角落都是相同的。他们都有一个相同的名字,叫时间戳时间戳 指的就是Unix时间戳(Unix timestamp)。它也被称为Unix时间(Unix time)、POSIX时间(POSIX time),是一种时间表示方式,定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。

关于 时间戳, 你可以看在线时间戳 http://tool.chinaz.com/Tools/unixtime.aspx

第二句话:时区

时间戳 在地球的每一个角落都是相同的,但是在相同的时间点会有不同的表达方式,所以有了另外一个时间概念,叫时区。这里的时区地区不是同一个概念,例如我们所在的时区东八区
在设备中,可以自己手动的切换当前的系统时区:

http://static.cyblogs.com/1198135-c33bb659a21ae7d7.jpg

你会发现:当你选在不同的时区,你的当前时间是不一样的。

第三句话:时间戳与时区在Code中应用

格林威治标准时间GMT

十七世纪,格林威治皇家天文台为了海上霸权的扩张计画而进行天体观测。1675年旧皇家观测所(Old Royal Observatory) 正式成立,到了1884年决定以通过格林威治的子午线作为划分地球东西两半球的经度零度。观测所门口墙上有一个标志24小时的时钟,显示当下的时间,对全球而言,这里所设定的时间是世界时间参考点,全球都以格林威治的时间作为标准来设定时间,这就是我们耳熟能详的「格林威治标准时间(Greenwich Mean Time,简称G.M.T.)的由来,标示在手表上,则代表此表具有两地时间功能,也就是同时可以显示原居地和另一个国度的时间。

阅读全文 »

背景

2019年我们经历了一整年的各种迁移,其中包括了一项RPC框架的切换。以前我们用的HSF RPC框架,它是来自于阿里巴巴,经过了多年的双11高并发的洗礼,高性能这块儿毫无疑问没有任何的问题,而且它还同时支持TCPHTTP的方式,唯一不太好的就是它不开源,如果出现问题定位起来确实有一些问题与风险。

所以,我们为了拥抱开源,全部采用SpringCloud,系统与系统之间调用是通过FeignClient的方式来调用的,但是由于底层的部分系统由于时间、人力、历史等原因,无法在短时间内都像我们一样能积极响应。所以就出现了SpringCloud与HSF服务同时存在的情况,为了大家再编码过程中都能像本地调用(TCPFeignClient),所以就写了一个代理工具。

交互图

http://static.cyblogs.com/QQ截图20200406181706.png

如果是上面的方式,我们还是能感受到每次都是通过HttpClient等方式发起一次Http请求,写代码时候的体验不是很好。

http://static.cyblogs.com/QQ截图20200406182159.png

为了解决这个问题,那么我们的任务就是来写一个这个代理封装。

分析功能点

了解一下FeignClient
阅读全文 »

背景

这几天确实太忙了,之前是日更,说上班后来个隔日更,还是坚持不了。完成Q1季度的考评后发现群里有人问了一个问题,非常的有意思。当时我也是非常的懵逼,然后想自己尝试的去解决一下。

问题是:0xee 0xb9short得到 -4423 为什么?对啊,为什么?我TM也想知道。

http://static.cyblogs.com/QQ20200411-001905@2x.jpg

其实到这里,我先总结一下:

  • 计算机为什么能计算你这么快,原因是它只会处理0与1,二级制;
  • 其实到硬件上面表现的就是电流信号、脉冲信号;
  • 计算机虽然支持减法,乘法,除法,实际上底层只会一种算法,那就是加法;

这也就是当时大学老师跟我们说的,别以为计算机很厉害,其实它很傻逼。每一步操作都需要设定好逻辑与程序。

恶补基础知识点

我们还是梳理一下基础知识吧~

  • 1、byte:有符号(意思是有正和负),在网络传输中都是会用到byte的,它占1个字节,共8位,比如说11111111就可以用1个byte表示,转化为10进制:- (2的6次+2的5次+2的4次+2的3次+2的2次+2的1次+2的0次) = -127。其中前7位表示数字,最高位表示符号,0为正,1为负。范围是 (-2的7次 ~ 2的7次 - 1),那为什么前面最小是-127,范围最小又是-128呢?因为规定-0(10000000)为-128。

  • 2、short:有符号,占2个字节,共16位。同byte一样,它的取值范围就是 (-2的15次 ~ 2的15次 - 1)。

  • 3、int :有符号,占4个字节,共32位。它的取值范围就是(-2的31次 ~ 2的31次)。

  • 4、long:有符号,占8个字节,共64位,它的取值范围就是(-2的63次 ~ 2的63次)。

  • 5、^:表示异或位运算,两者相同则为0,两者不同则为1。比如说15^2,15用二进制表示就是1111,2用2进制表示就是0010,两者进行异或运算,结果就是1101,转换为十进制就是13。

  • 6、|:表示或运算,两者只有有一个为1就为1, 比如说13|2,13用二进制表示就是1101,2用二进制表示就是0010,两者进行或运算,那么结果就是1111,转换为十进制就是15。

  • 7、&:表示与运算,两者都为1就为1,其余都为0,比如说15&2, 13用二进制表示就是1111,2用二进制表示就是0010, 两者进行与运算,那么结果就是0010,转换为十进制就是2。

  • 8、~:取反,就是本来是0变成1,本来是1变成0。

阅读全文 »

背景

在SpringBoot环境中,我们有“使用不完的”注解。这也是SpringBoot替代了传统的Spring项目中的xml配置的原因。在使用这些annotation的时候,我们一定要了解这些注解背后的原理以及约定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.springframework.boot.context.properties;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {
......
}

支持的类型

List
1
2
3
custom.config.config1.folders[0]=/root
custom.config.config1.folders[1]=/home/user1
custom.config.config1.folders[2]=/home/user2

对应的Java实现

1
2
3
4
5
@ConfigurationProperties(prefix = "custom.config.config1")
public class Config1Properties{
private List<String> folders;
...
}
Map
1
2
3
4
5
custom.config.config1.map.key1=value1
custom.config.config1.map.key2=value2
custom.config.config1.map.key3=value3
custom.config.config1.map.key4=value4
custom.config.config1.map.key5=value5
阅读全文 »

概述

UDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响。

传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。

最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。

  • 1、添加seq/ack机制,确保数据发送到对端
  • 2、添加发送和接收缓冲区,主要是用户超时重传。
  • 3、添加超时重传机制。

详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。

目前有如下开源程序利用udp实现了可靠的数据传输。分别为 RUDP、RTP、UDT

开源程序

1、RUDP(Reliable User Datagram Protocol)

RUDP 提供一组数据服务质量增强机制,如拥塞控制的改进、重发机制及淡化服务器算法等 ,从而在包丢失和网络拥塞的情况下, RTP 客户机(实时位置)面前呈现的就是一个高质量的 RTP 流。在不干扰协议的实时特性的同时,可靠 UDP 的拥塞控制机制允许 TCP 方式下的流控制行为。

阅读全文 »

粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。

什么是粘包、拆包?

假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

**第一种情况:**接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

img

**第二种情况:**接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

img

**第三种情况:**这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

img

img

阅读全文 »

在这之前我只记住了StringBuilder不是线程安全的,StringBuffer是线程安全的这个结论,至于StringBuilder为什么不安全从来没有去想过。

分析

在分析设个问题之前我们要知道StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。

首先通过一段代码去看一下多线程操作StringBuilder对象会出现什么问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StringBuilderDemo {

public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++){
new Thread(new Runnable( {
@Override
public void run() {
for (int j = 0; j < 1000; j++){
stringBuilder.append("a");
}
}
}).start();
}

Thread.sleep(100);
System.out.println(stringBuilder.length());
}

}

我们能看到这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?

http://static.cyblogs.com/16cc76d348d29915.png

我们看到输出了“9326”,小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。

1、为什么输出值跟预期值不一样

我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)

阅读全文 »
0%