Earyant的技术博客

欢迎来到Earyant的技术博客,在这里我将与你分享新技术。

本文章记录自己通过docker搭建elk的过程

docker下载镜像

1
docker pull sebp/elk :latest

docker 至少分配3g内存

elasticsearch 至少需要2g,logstash至少需要1g

设置最大运行线程数

1
最大线程数要超过262144
  • 使用docker toolbox方式 先进入虚拟机

    docker-machine ssh default

  • 永久生效配置

    sudo vi /etc/sysctl.conf

    vm.max_map_count=262144

    sudo sysctl -p //生效

  • 临时生效配置

    sysctl -w vm.max_map_count=262144

启动

启动,并配置端口映射

docker run -p 5601:5601 -p 9200:9200 -p 5044:5044 -it —name elk sebp/elk

查看kibana

打开网址 http:\192.168.99.100:5601 若打开成功说明部署成功

配置使用

  1. 进入容器

    docker exec -it elk /bin/bash

  2. 配置logstash

    /opt/logstash/bin/logstash -e ‘input { stdin { } } output { elasticsearch { hosts => [“localhost”] } }’

    注意:如果看到这样的报错信息 Logstash could not be started because there is already another instance using the configured data directory. If you wish to run multiple instances, you must change the “path.data” setting. 请执行命令:service logstash stop 然后在执行就可以了。

    当命令成功被执行后,看到:Successfully started Logstash API endpoint {:port=>9600} 信息后,输入:test 然后回车,模拟一条日志进行测试。

    1. 打开浏览器,输入:http://192.168.99.100:9200/_search?pretty ,就会看到我们刚刚输入的日志内容
    2. 创建kibana与logstash关联
    3. index name or pattern 输入 logstash-*
    4. time-field name 输入 @timestamp
    5. create
    6. 打开kibana首页即可看到刚刚输入的内容

java8新特性

lambda表达式

java8接受一个函数作为另一个函数的参数传入进去

Arrays.asList( “a”, “b”, “d” ).forEach( e -> System.out.println( e ) ); 参数的类型是编译器推断出来的

Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。

默认方法和静态方法

在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。默认方法允许继续使用现有的Java接口,而同时能够保障正常的编译过程。这方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……

重复注解

Java编译器的新特性

1
2
3
4
5
6
7
8
public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod( "main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}

Parameter: args

对于有经验的Maven用户,通过maven-compiler-plugin的配置可以将-parameters参数添加到编译器中去。

1
2
3
4
5
6
7
8
9
10
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

Java 类库的新特性

Optional

平时用的判空:

1
2
3
4
5
6
7
public void get(String name){
if (name == null){
return "";
}else{
return name;
}
}

改成Optional:

1
2
3
4
5
6
7
8
public void get(String name){
Optional<String > names = Opntional.of(name);
if (names.isPresent()){
return names.get();
}else{
return "";
}
}

如果单纯改成这样,还不如直接写成 ==null,看起来更加繁琐。 应该改成这样:

1
2
3
4
public void get(String name){
return Opntional.of(name).get().orElse("");

}

Stream

1
2
3
4
5
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();

filter

对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素;

map

映射转换

mapTo*

映射转换成 的类型, 包括Integer、Double、Long

flatMap

合并集合,和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

1
2
3
{{1,2},{3,4},{5,6}} - > flatMap - > {1,2,3,4,5,6}

{'a','b'},{'c','d'},{'e','f'}} - > flatMap - > {'a','b','c' D", 'E', 'F'}

flatMapTo*

distinct

对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;

sorted

peek

监控器

提供一个Consumer消费函数,返回一个新的Stream,如果只写 Stream.of(“1”,”2”,”3”).peek(e-> System.out.Println(e)) 是不会打印结果的 新Stream每个元素被消费的时候都会执行给定的消费函数; Stream.of(“1”,”2”,”3”).peek(e-> System.out.Println(e)).collect(Collectors.toList());

limit

对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;

skip

返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

forEach

遍历

forEachOrdered

与forEach一样,只不过每次遍历结果都是相同的,forEach不是固定的序列

generate

给定一个生成函数,生成一个无限的队列 Stream.generate(Math::random);

iterate

也是生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环

Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);

collect

汇聚函数

方法参数 返回类型 作用 示例
toList List<T > 把流中的参数汇聚成List集合 示例:List<Menu> menus=Menu.getMenus.stream().collect(Collectors.toList())
toSet Set<T> 把流中所有元素收集到Set中,删除重复项 Set<Menu> menus=Menu.getMenus.stream().collect(Collectors.toSet())
toCollection Collection<T> 把流中所有元素收集到给定的供应源创建的集合中 ArrayList<Menu> menus=Menu.getMenus.stream().collect(Collectors.toCollection(ArrayList::new))
Counting Long 计算流中元素个数 Long count=Menu.getMenus.stream().collect(Collectors.counting());
SummingInt Integer 对流中元素的一个整数属性求和 Integer count=Menu.getMenus.stream().collect(Collectors.summingInt(Menu::getCalories))
averagingInt Double 计算流中元素integer属性的平均值 Double averaging=Menu.getMenus.stream().collect((Collectors.averagingInt(Menu::getCalories))
Joining String 连接流中每个元素的toString方法生成的字符串 String name=Menu.getMenus.stream().map(Menu::getName).collect(Collectors.joining(“, “))
maxBy Optional<T> 一个包裹了流中按照给定比较器选出的最大元素的optional 如果为空返回的是Optional.empty() Optional<Menu> fattest=Menu.getMenus.stream().collectCollectors.maxBy(Menu::getCalories))
minBy Optional<T> 一个包裹了流中按照给定比较器选出的最大元素的optional如果为空返回的是Optional.empty() Optional<Menu> lessest=Menu.getMenus.stream().collect(minBy(Menu::getCalories))
Reducing 归约操作产生的类型 从一个作为累加器的初始值开始,利用binaryOperator与流中的元素逐个结合,从而将流归约为单个值 int count=Menu.getMenus.stream().collect(reducing(0,Menu::getCalories,Integer::sum));
collectingAndThen 转换函数返回的类型 包裹另一个转换器,对其结果应用转换函数 Int count=Menu.getMenus.stream().collect(collectingAndThen(toList(),List::size))
groupingBy Map<K,List<T>> 根据流中元素的某个值对流中的元素进行分组,并将属性值做为结果map的键 Map<Type,List<Menu>> menuType=Menu.getMenus.stream().collect(groupingby(Menu::getType))
partitioningBy Map<Boolean,List<T>> 根据流中每个元素应用谓语的结果来对项目进行分区 Map<Boolean,List<Menu>> menuType=Menu.getMenus.stream().collect(partitioningBy(Menu::isType));

reduce

  • Optional<t> reduce(BinaryOperator<t> accumulator);

List <integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10); System.out.println(“ints sum is:” + ints.stream().reduce((sum, item) -> sum + item).get());

可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional,这是Java8防止出现NPE的一种可行方法,后面的文章会详细介绍,这里就简单的认为是一个容器,其中可能会包含0个或者1个对象。

  • T reduce(T identity, BinaryOperator<t> accumulator);
    这个定义上上面已经介绍过的基本一致,不同的是:它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值。

count

个数

allMatch

是不是所有都满足给定的条件

anyMatch

是不是存在某一个元素满足给定的条件

findFirst

返回第一个,返回的是Optional值

noneMatch

是否没有一个元素满足给定的条件

max和min

给定的比较条件,返回最大最小值

参考
好文收藏

  • 注册中心不自动上线下线
  • 异步返回值
  • bean参数传值
    *

eureka 不自动下线

Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。

详见:https://github.com/Netflix/eureka/wiki/Understanding-Eureka-Peer-to-Peer-Communication

解决方法:设置enableSelfPreservation:false

配置心跳检测时长,下线leaseRenewalIntervalInSeconds: 2

默认没有开启:

1
2
3
4
5
6
7
8
9
10
spring.cloud.loadbalancer.retry.enabled=true

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000

hello-service.ribbon.ConnectTimeout=250
hello-service.ribbon.ReadTimeout=1000
hello-service.ribbon.OkToRetryOnAllOperations=true
hello-service.ribbon.MaxAutoRetriesNextServer=2
hello-service.ribbon.MaxAutoRetries=1

如何处理服务挂掉后或者手动关闭服务后,Ribbon负载均衡还是一直调用这个服务,然后调用@HystrixCommand断路器注解的方法:利用Hystrix,在error callback方法中可以shutdown指定的server

1
2
3
4
5
ZoneAwareLoadBalancer<Server> lb = (ZoneAwareLoadBalancer<Server>) springClientFactory.getLoadBalancer("CLOUD-SERVICE");
Server server = lb.chooseServer();
System.out.println("error->" + server.getHostPort());
lb.markServerDown(server);

1. 概述

本文在上文 Spring cloud系列四 Eureka 之概述和服务注册中心集群的基础上,继续介绍Eureka新的内容:

集群重要类:PeerAwareInstanceRegistryImpl
新的Eureka Server节点加入集群后的影响
新服务注册(Register)注册时的影响
服务心跳(renew)
服务下线和剔除
自我保护模式

2. Eureka Server的集群同步操作

2.1. Eureka官网的架构图

下方的操作需要结合下图理解:
这里写图片描述

2.2. PeerAwareInstanceRegistryImpl

集群相关重要的类com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl: 为了保证集群里所有Eureka Server节点的状态同步,所有以下操作都会同步到集群的所有服务上:服务注册(Registers)、服务更新(Renewals)、服务取消(Cancels),服务超时(Expirations)和服务状态变更(Status Changes)。以下是一些部分方法:

syncUp:在Eureka Server重启或新的Eureka Server节点加进来的,会执行初始化,从集群其他节点中获取所有的实例注册信息,从而能够正常提供服务。当Eureka Server启动时,它会从其它节点获取所有的注册信息,如果获取同步失败,它在一定时间(此值由决定)内拒绝服务。
replicateToPeers: 同步以下操作到所有的集群节点:服务注册(Registers)、服务更新(Renewals)、服务取消(Cancels),服务超时(Expirations)和服务状态变更(Status Changes)
register: 注册实例,并且复印此实例的信息到所有的eureka server的节点。如果其它Eureka Server调用此节点,只在本节点更新实例信息,避免通知其他节点执行更新
renew:心跳,同步集群
cancel
其他

Eureka Server集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。

2.3. 新的Eureka Server节点加入集群后的影响

当有新的节点加入到集群中,会对现在Eureka Server和Eureka Client有什么影响以及他们如何发现新增的Eureka Server节点:

新增的Eureka Server:在Eureka Server重启或新的Eureka Server节点加进来的,它会从集群里其它节点获取所有的实例注册信息。如果获取同步失败,它会在一定时间(此值由决定eureka.server.peer-eureka-nodes-update-interval-ms决定)内拒绝服务。

已有Eureka Server和Service Consumer如何发现新的Eureka Server
    已有的Eureka Server:在运行过程中,Eureka Server之间会定时同步实例的注册信息。这样即使新的Application Service只向集群中一台注册服务,则经过一段时间会集群中所有的Eureka Server都会有这个实例的信息。那么Eureka Server节点之间如何相互发现,各个节点之间定时(时间由eureka.server.peer-eureka-nodes-update-interval-ms决定)更新节点信息,进行相互发现。
    Service Consumer:Service Consumer刚启动时,它会从配置文件读取Eureka Server的地址信息。当集群中新增一个Eureka Server中时,那么Service Provider如何发现这个Eureka Server?Service Consumer会定时(此值由eureka.client.eureka-service-url-poll-interval-seconds决定)调用Eureka Server集群接口,获取所有的Eureka Server信息的并更新本地配置。

2.4. 新服务注册(Register)注册时的影响

Service Provider要对外提供服务,把自己注册到Eureka Server上。如果配置参数eureka.client.registerWithEureka=true(默认值true)时,会向Eureka Server注册进行注册,Eureka Server会保存注册信息到内存中。

Service Consumer为了避免每次调用服务请求都需要向Eureka Server获取服务实例的注册信息,此时需要设置eureka.client.fetchRegistry=true,它会在本地缓存所有实例注册信息。为了保证缓存数据的有效性,它会定时(值由eureka.client.registry-fetch-interval-seconds定义,默认值为30s)向注册中心更新实例。

2.5. 服务心跳(renew)

服务实例会通过心跳(eureka.instance.lease-renewal-interval-in-seconds定义心跳的频率,默认值为30s)续约的方式向Eureka Server定时更新自己的状态。Eureka Server收到心跳后,会通知集群里的其它Eureka Server更新此实例的状态。Service Provider/Service Consumer也会定时更新缓存的实例信息。

2.6. 服务下线和剔除

服务的下线有两种情况:

在Service Provider服务shutdown的时候,主动通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务。
Eureka Server会定时(间隔值是eureka.server.eviction-interval-timer-in-ms,默认值为0,默认情况不删除实例)进行检查,如果发现实例在在一定时间(此值由eureka.instance.lease-expiration-duration-in-seconds定义,默认值为90s)内没有收到心跳,则会注销此实例。

这种情况下,Eureka Client的最多需要[eureka.instance.lease-renewal-interval-in-seconds + eureka.client.registry-fetch-interval-seconds]时间才发现服务已经下线。同理,一个新的服务上线后,Eureka Client的服务消费方最多需要相同的时间才发现服务已经上线

服务下线,同时会更新到Eureka Server其他节点和Eureka client的缓存,流程类似同以上的register过程

2.7. 自我保护模式

如果Eureka Server最近1分钟收到renew的次数小于阈值(即预期的最小值),则会触发自我保护模式,此时Eureka Server此时会认为这是网络问题,它不会注销任何过期的实例。等到最近收到renew的次数大于阈值后,则Eureka Server退出自我保护模式。

自我保护模式阈值计算:

每个instance的预期心跳数目 = 60/每个instance的心跳间隔秒数
阈值 = 所有注册到服务的instance的数量的预期心跳之和 *自我保护系数

以上的参数都可配置的:

instance的心跳间隔秒数:eureka.instance.lease-renewal-interval-in-seconds
自我保护系数:eureka.server.renewal-percent-threshold

如果我们的实例比较少且是内部网络时,推荐关掉此选项。我们也可以通过eureka.server.enable-self-preservation = false来禁用自我保护系数

2.8 配置demo

以下配置的demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
server:
port: 10761
spring:
application:
name: cloud-registration-center
## eureka : 主要配置属性在EurekaInstanceConfigBean和EurekaClientConfigBean中
eureka:
instance:
# hostname: 127.0.0.1
# 使用IP注册
preferIpAddress: true
# 心跳间隔
lease-renewal-interval-in-seconds: 3
# 服务失效时间: 如果多久没有收到请求,则可以删除服务
lease-expiration-duration-in-seconds: 7
client:
# 关闭eureka client
# enabled: false
# 注册自身到eureka服务器
registerWithEureka: true
# 表示是否从eureka服务器获取注册信息
fetchRegistry: false
# 客户端从Eureka Server集群里更新Eureka Server信息的频率
eureka-service-url-poll-interval-seconds: 60
# 定义从注册中心获取注册服务的信息
registry-fetch-interval-seconds: 5
# 设置eureka服务器所在的地址,查询服务和注册服务都需要依赖这个地址
serviceUrl:
defaultZone: http://127.0.0.1:10761/eureka/
# 设置eureka服务器所在的地址,可以同时向多个服务注册服务
# defaultZone: http://192.168.21.3:10761/eureka/,http://192.168.21.4:10761/eureka/
server:
# renewal-percent-threshold: 0.1
# 关闭自我保护模式
enable-self-preservation: false
# Eureka Server 自我保护系数,当enable-self-preservation=true时,启作用
# renewal-percent-threshold:
# 设置清理间隔,单位为毫秒,默认为0
eviction-interval-timer-in-ms: 3000
# 设置如果Eureka Server启动时无法从临近Eureka Server节点获取注册信息,它多久不对外提供注册服务
wait-time-in-ms-when-sync-empty: 6000000
# 集群之间相互更新节点信息的时间频率
peer-eureka-nodes-update-interval-ms: 60000

索引介绍

索引对查询的速度有至关重要的影响。考虑如下情况,假设数据库中一个表有10^6^条记录,DBMS的页面大小为4K,并存储100条记录。如果没有索引,查询将对整个表进行扫描,最坏的情况下,如果所有数据页都不在内存,需要读取10^4^个页面,如果这10^4^个页面在磁盘上随机分布,需要进行10^4^次I/O,假设磁盘每次I/O时间为10ms(忽略数据传输时间),则总共需要100s(但实际上要好很多很多)。如果对之建立B-Tree索引,则只需要进行log100(10^6^)=3次页面读取,最坏情况下耗时30ms。这就是索引带来的效果,很多时候,当你的应用程序进行SQL查询速度很慢时,应该想想是否可以建索引

索引与优化

索引的数据类型

选择索引类型原则:
  1. 数据整形: 越小的整形在磁盘、内存、和CPU之间都需要更少的空间,处理起来更快。
    1. 简单的数据类型更好: 整形数据比起字符型,处理开销更小,因为字符串的比较更复杂,在Mysql中,应该用内置的日期和时间数据类型,而不是字符串来存储时间,以及用整形数据类型存储ip地址。
    2. 尽量避免NULL: 应该指定列为NOT NULL ,除非想存储NULL。在mysql中,含有空值的列很难进行查询优化,以为他使得索引、索引的统计信息以及比较运算符更加复杂。应该用0、一个特殊值、或者一个空串代替空值。

选择标识符

选择合适的标识符是非常重要的,选择是不仅应该考虑存储类型,而且应该考虑mysql是怎么进行运算和比较的,一旦选定数据类型,应该保证所有的表都使用相同的数据类型。

1. 整形: 通常是作为标识符最好的选择,因为更苦阿爹处理,因为可以更快的处理,而且可以设置为AUTO_INCREMENT。
2. 字符串:尽量避免使用字符串作为标识符,它们消耗更好的空间,处理起来也较慢。而且,通常来说,字符串都是随机的,所以它们在索引中的位置也是随机的,这会导致页面分裂、随机访问磁盘,聚簇索引分裂(对于使用聚簇索引的存储引擎)。

索引入门

对于任何DBMS,索引都是进行优化的最主要的因素。对于少量的数据,没有合适的索引影响不是很大,但是,当随着数据量的增加,性能会急剧下降。
如果对多列进行索引(组合索引),**列的顺序**非常重要,MySQL仅能对索引最左边的前缀进行有效的查找。例如:

假设存在组合索引 表t1 建立 c1c2(c1,c2)联合索引,查询语句select from t1 where c1=1 and c2=2能够使用该索引。查询语句select from t1 where c1=1也能够使用该索引。但是,查询语句select * from t1 where c2=2不能够使用该索引,因为没有组合索引的引导列,即,要想使用c2列进行查找,必需出现c1等于某值。

索引的类型

索引是在存储引擎中实现的,而不是在服务器层中实现的。所以,每种存储引擎的索引都不一定完全相同,并不是所有的存储引擎都支持所有的索引类型。

B-Tree索引
1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE People (

last_name varchar(50) not null,

first_name varchar(50) not null,

dob date not null,

gender enum('m', 'f') not null,

key(last_name, first_name, dob)

);

其索引包含表中每一行的last_name、first_name和dob列。其结构大致如下:

索引存储的值按索引列中的顺序排列。可以利用B-Tree索引进行全关键字、关键字范围和关键字前缀查询,当然,如果想使用索引,你必须保证按索引的最左边前缀(leftmost prefix of the index)来进行查询。

  1. 匹配全值(Match the full value):对索引中的所有列都指定具体的值。例如,上图中索引可以帮助你查找出生于1960-01-01的Cuba Allen。
  2. 匹配最左前缀(Match a leftmost prefix):你可以利用索引查找last name为Allen的人,仅仅使用索引中的第1列。
  3. 匹配列前缀(Match a column prefix):例如,你可以利用索引查找last name以J开始的人,这仅仅使用索引中的第1列。
  4. 匹配值的范围查询(Match a range of values):可以利用索引查找last name在Allen和Barrymore之间的人,仅仅使用索引中第1列。
  5. 匹配部分精确而其它部分进行范围匹配(Match one part exactly and match a range on another part):可以利用索引查找last name为Allen,而first name以字母K开始的人。
  6. 仅对索引进行查询(Index-only queries):如果查询的列都位于索引中,则不需要读取元组的值。

由于B-树中的节点都是顺序存储的,所以可以利用索引进行查找(找某些值),也可以对查询结果进行ORDER BY。当然,使用B-tree索引有以下一些限制:

  1. 查询必须从索引的最左边的列开始。关于这点已经提了很多遍了。例如你不能利用索引查找在某一天出生的人。
  2. 不能跳过某一索引列。例如,你不能利用索引查找last name为Smith且出生于某一天的人。
  3. 存储引擎不能使用索引中范围条件右边的列。例如,如果你的查询语句为WHERE last_name=”Smith” AND first_name LIKE ‘J%’ AND dob=’1976-12-23’,则该查询只会使用索引中的前两列,因为LIKE是范围查询。

    Hash索引

    MySQL中,只有Memory存储引擎显示支持hash索引,是Memory表的默认索引类型,尽管Memory表也可以使用B-Tree索引。Memory存储引擎支持非唯一hash索引,这在数据库领域是罕见的,如果多个值有相同的hash code,索引把它们的行指针用链表保存到同一个hash表项中。

    假设创建如下一个表:

    1
    2
    3
    4
    5
    CREATE TABLE testhash (
    fname VARCHAR(50) NOT NULL,
    lname VARCHAR(50) NOT NULL,
    KEY USING HASH(fname)
    ) ENGINE=MEMORY;

    假设索引使用hash函数f( ),如下:

    f(‘Arjen’) = 2323
    f(‘Baron’) = 7437
    f(‘Peter’) = 8784
    f(‘Vadim’) = 2458
    此时,索引的结构大概如下:

Slots是有序的,但是记录不是有序的。当你执行
mysql> SELECT lname FROM testhash WHERE fname=’Peter’;
MySQL会计算’Peter’的hash值,然后通过它来查询索引的行指针。因为f(‘Peter’) = 8784,MySQL会在索引中查找8784,得到指向记录3的指针。
因为索引自己仅仅存储很短的值,所以,索引非常紧凑。Hash值不取决于列的数据类型,一个TINYINT列的索引与一个长字符串列的索引一样大。
Hash索引有以下一些限制

  1. 由于索引仅包含hash code和记录指针,所以,MySQL不能通过使用索引避免读取记录。但是访问内存中的记录是非常迅速的,不会对性造成太大的影响。
  2. 不能使用hash索引排序。
  3. Hash索引不支持键的部分匹配,因为是通过整个索引值来计算hash值的。
  4. Hash索引只支持等值比较,例如使用=,IN( )和<=>。对于WHERE price>100并不能加速查询。
空间(R-Tree)索引
MyISAM支持空间索引,主要用于地理空间数据类型,例如GEOMETRY。
全文(Full-text)索引
全文索引是MyISAM的一个特殊索引类型,主要用于全文检索。

高性能的索引策略

聚簇索引(Clustered Indexes)

mysql聚簇索引保证关键字的值相近的元组存储的物理位置也相同(所以字符串类型不宜建立聚簇索引,特别是随机字符串,会使得系统进行大量的移动操作),且一个表只能有一个聚簇索引。因为由存储引擎实现索引,所以,并不是所有的引擎都支持聚簇索引。目前,只有solidDB和InnoDB支持。
结构如下:

注:叶子页面包含完整的元组,而内节点页面仅包含索引的列(索引的列为整型)。一些DBMS允许用户指定聚簇索引,但是MySQL的存储引擎到目前为止都不支持。InnoDB对主键建立聚簇索引。如果你不指定主键,InnoDB会用一个具有唯一且非空值的索引来代替。如果不存在这样的索引,InnoDB会定义一个隐藏的主键,然后对其建立聚簇索引。一般来说,DBMS都会以聚簇索引的形式来存储实际的数据,它是其它二级索引的基础。

InnoDB和MyISAM的数据布局的比较

为了更加理解聚簇索引和非聚簇索引,或者primary索引和second索引(MyISAM不支持聚簇索引),来比较一下InnoDB和MyISAM的数据布局,对于如下表:

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE layout_test (

col1 int NOT NULL,

col2 int NOT NULL,

PRIMARY KEY(col1),

KEY(col2)

);

假设主键的值位于1—-10,000之间,且按随机顺序插入,然后用OPTIMIZE TABLE进行优化。col2随机赋予1—-100之间的值,所以会存在许多重复的值。

  1. MyISAM的数据布局
    其布局十分简单,MyISAM按照插入的顺序在磁盘上存储数据,如下:

参考

EffectiveJava 读书笔记

本文于2017年10月26日再读一遍,对书中的内容进行整理提取,方便以后查阅

[TOC]

第一章:创建和销毁对象

创建对象的方法

  • 构造器方法new一个;
  • 公共静态方法返回实例;
  • Build工程模式。

    一般来说创建对象最简单的方法就是使用的时候new一个,如Person person = new Person();既通过构造器创建对象。这种使用方法不适用于所有情况,例如一个单例工具类,不需要每次使用都new,而是通过一个公共静态方法返回类的实例。

构造器方式

缺点

每一个构造器都与类名相同,无法知其寓意。

构造器方法时最常用的方法;

公共静态方法

优点1

每一个公共静态方法都有名字,顾名思义。

优点2

不必为每次调用都产生一个新的实例,可以重复利用。

优点3

可以返回原返回类型的任何子类型对象。

优点3

创建参数化类型实例,使代码变得更简洁。

缺点1

类如果不含有共有的或者受保护的构造器,就不能被子类化。

缺点2

与其他静态方法没有任何区别。

使用情景1 通过方法名知其方法作用

如BigInteger(int,int,Random)返回的BitInteger可能为素数,但如果用BigInteger.probablePrime静态方法来表示,更加清楚方法寓意。

如果两个构造器方法中只是参数类型顺序上不同,很容易造成寓意混乱,需要进一步查询文档才能确定用哪一个,容错率很低。

使用场景2 使用预先定义好的实例。

公共静态方法返回的实例可以控制是否是单例,或者控制是否可以实例化。

  • getInstance和newInstance区别:

    getInstance方法重点是返回已经创建好的实例。
    newInstance方法重点是返回一个新的实例。

使用场景3 Collections的32个便利实现、通过适配器提供更丰富的服务接口

分别提供了不可修改的集合、同步集合等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Service interface
public interface Service{

}

//Service provider interface
public interface Provider{
Service newService();
}

public class Services{
private Services(){};

private static final Map<String,Provider> providers = new ConcurrentHashMap<String,Provider>();

public static final String DEFAULT_PROVIDER_NAME = "<def>";

public static void registerDefaultProvider(Provider p){
registerProvider(DEFAULT_PROVIDER_NAME,p););
}

public static void registerProvider(String name,Provider p){
providers.put(name,p);
}

public static Service newInstance(){
return newInstance(DEFAULT_PROVIDER_NAME);
}

public static Service newInstance(String name){
Provider p = provider.get(name);
if(p == null){
throw new IllegalArgumentException("No provider registered with name:"+ name);
}
return p.newService();
}
}

使用场景4 代替繁琐的参数声明。

使用场景6

valueOf — 返回的实例与参数具有相同的值,实际上是类型转换方法。
of — valueOf的简洁替代。
getInstance — 返回的实例是通过方法的参数来描述的,但是不能说与参数有相同的值。对于Singleton来说,该方法没有参数。
newInstance — 跟getInstance一样,但是该方法确保返回的实例与其他实例不同。
getType — 跟getInstance一样,但是在工厂方法中处于不同的类中的时候使用,Type表示工厂方法锁返回的对象类型。
newType — 像newInstance一样,但是在工厂方法中处于不同的类中的时候使用。

构建器

静态工厂和构造器都有个局限性,都不能很好的扩展到大量的可选参数。
参数量大的情况下通常有两种方法:

  • 重叠构造器

    可行,但很难编写,难以阅读。

  • 工厂模式

    build构造参数很简单明了,链式调用很少出错。
    但是在构造Bean的时候,可能出现不一致的状态,类无法仅仅通过构造器参数的有效性来保证一致性。

用私有构造器或者枚举类型强化Singleton属性

构造器私有,公开一个公共静态方法返回实例。

Java对象头

一般占用两个机器码,在32位虚拟机中,一个机器码占用4个字节,就是32位,但是如果是数组,需要占用3个机器码,需要1位确认数组大小。

  • 标记字段
    • 哈希码
    • GC分代年龄
    • 锁状态标志
    • 线程持有的锁
    • 偏向线程ID
    • 偏向时间戳
  • 类型指针

    | 25bit | 4bit | 1bit | 2bit |
    | —- | —- | —- | —- |
    | 对象的hashCode | 分代年龄 | 是否是偏向锁 | 锁标记位 |

初步设想

最简单的方法,就是for循环一下。

1
2
3
4
double result = 1L;
for (int i = 0; i < n; i++) {
result *= m;
}

这种方法结果肯定正确,但是时间复杂度上,需要O(n)。很是不理想。于是我想着改进一下。

探求乘法原理

乘法原理就是n个m相乘,例如m=10,n=5, 那么表达式为 1010101010,有人说这不是废话嘛,当然不是,之所以展开,是因为可以改写法,如:(1010) (1010) 10,又有人说了,这不是还是废话嘛,别急往下看,你会发现(10*10)重复了。

什么意思?

如果单纯用for循环10的4次幂,需要 循环4次运算,现在我先用1010,第1次运算,再用得出的结果乘以得出的结果,也就是(1010) (1010),第2次运算。就这么简单,此次运算节省了2次,节省了百分之50啊,重大发现啊。

如果是10的8次幂呢?单纯for循环需要8次,新的方法需要几次?3次。怎么算的?(1010) (1010) (1010) (1010)。发现就是算1010的4次方,算出1010需要 第一次*,4次方刚刚算了是2次,一共是3次,这么神奇啊,节省了5次运算啊。这是节省了百分之。。。ummmm,管他百分之多少呢,反正很多。

照这么下去,16次方需要 4 次运算,32次方需要 5 次运算,64次方需要 6 次运算,重大发现,也就是说,如果n是2的i次幂,那么就需要i次运算就可以算出m的n次方了。

天晴了, 世界和平了,我又拯救了世界了,可以回家睡懒觉了。

咦,等等,如果n不是2的多少次幂怎么办?比如10的5次幂?

ummmm,我想了想,把5拆成4+1呢?也就是10的4次幂再乘以10,这个方法可以。

那10的6次幂就可以拆成4+2.

7次方就可以拆成4+2+1.嗯,不错。

于是,我写了个代码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

public class Pow {
public static void main(String[] args) {
int m = 5;
int n = 134;
long start = System.currentTimeMillis();
System.out.println("for循环版,结果为 : " + Double.toString(pow(m, n)));
long end = System.currentTimeMillis();
System.out.println("for循环版,总耗时: " + (end - start));


start = System.currentTimeMillis();
System.out.println("叠乘法,结果为 : " + Double.toString(pow2(m, n)));
end = System.currentTimeMillis();
System.out.println("叠乘法,总耗时: " + (end - start));
}

/**
* m的n次方,for循环版
*
* @param m 底数
* @param n 幂数
* @return
*/
static double pow(int m, int n) {
boolean negative = false;
if (m == 0) {
return 0;
}
if (n == 0) {
return 1;
}
if (n < 0) {
n = -n;
negative = true;
}
/**
* 循环乘法
*/
double result = 1L;
for (int i = 0; i < n; i++) {
result *= m;
}
if (negative) {
return 1 / result;
}
return result;
}

/**
* m的n次方,折中叠乘法
*
* @param m 底数
* @param n 幂数
* @return
*/
static double pow2(int m, int n) {
boolean negative = false;
if (m == 0) {
return 0;
}
if (n == 0) {
return 1;
}
if (n < 0) {
n = -n;
negative = true;
}
// 当m是2的时候,直接位移就可以。
if (m == 2) {
return 2 << n;
}
/**
* 叠乘法
*/
// 最后结果。
double result = 1L;
// 定义一个指针,从右向左按位与。
for (int i = 1; i < n; i <<= 1) {
// 按位与,如果大于0,说明此位为1,需要m的i次方。
if ((i & n) > 0) {
double resultTemp = 1L;
// m的i次方不能写成循环i次的m相乘,而是m叠乘,次数为ln(i)。 例如m的8次方,就是((m*m)*(m*m))*((m*m)*(m*m)),也就是 ((m*m)2)2
for (int j = 1; j <= i; j <<= 1) {
if (j == 1) {
resultTemp = m;
} else {
if (greatThanMax(resultTemp, resultTemp)) {
return Double.MAX_VALUE;
}
resultTemp *= resultTemp;
}
}
if (greatThanMax(resultTemp, result)) {
return Double.MAX_VALUE;
}
result *= resultTemp;
}
}

if (negative) {
return 1 / result;
}
return result;
}


/**
* 检查两个数相乘是否大于double的最大值。
*
* @param m
* @param n
* @return
*/
public static boolean greatThanMax(double m, double n) {
return (m > (Double.MAX_VALUE / n));
}
}

测试结果为

for循环版,结果为 : 4.591774807899563E93
for循环版,总耗时: 1
叠乘法,结果为 : 4.591774807899562E93
叠乘法,总耗时: 0

彩蛋

如果m是2的话,只需要返回2<<n就行了。自行判断是否越界。

本文作为收藏用

#

作者:匿名用户
链接:https://www.zhihu.com/question/39890405/answer/83676977
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 第一个是基础。

    比如对集合类,并发包,IO/NIO,JVM,内存模型,泛型,异常,反射,等有深入了解,最好是看过源码了解底层的设计。比如一般面试都会问ConcurrentHashMap,CopyOnWrite,线程池,CAS,AQS,虚拟机优化等知识点,因为这些对互联网的企业是绝对重要的。而且一般人这关都过不了,还发闹骚说这些没什么用,为什么要面试。举一例子,在使用线程池时,因为使用了无界队列,在远程服务异常情况下导致内层飙升,怎么去解决?你要是连线程池都不清楚,你怎么去玩?再举一例,由于对ThreadLocal理解出错,使用它做线程安全的控制,导致没能实现真的线程安全。所以作为一个拿两万的JAVA程序员这点基础是要有的。

  • 第二你需要有全面的互联网技术相关知识。

    从底层说起,你起码得深入了解mysql,redis,mongodb,nginx,tomcat,rpc,jms等方面的知识。你要问需要了解到什么程度,我可以给你说个大慨。首先对于MySQL,你要知道常见的参数设置,存储引擎怎么去选择,还需要了解常见的索引引擎,知道怎么去选择。知道怎么去设计表,怎么优化sql,怎么根据执行计划去调优。高级的你需要去做分库分表的设计和优化,一般互联网企业的数据库都是读写分离,还会垂直与水平拆分,所以这个也有经验的成分在里面。然后redis,mongodb都是需要了解原理,需要会调整参数的,而nginx和tomcat几乎都是JAVA互联网方面必配,其实很阿里的技术栈选择有点关系。至于rpc相关的就多的去,必须各种网络协议,序列化技术,SOA等等,你要有一个深入的理解。现在应用比较广的rpc框架,在国内就是dubbo了,可以自行搜索。至于jms相关的起码得了解原理吧,一般情况下不是专门开发中间件系统和支撑系统的不需要了解太多细节,国内企业常用的主要是activeMQ和kafka。你能对我说的都研究的比较深入,阿里p7都不是太大问题的,当然这个还需要看你的架构能力方面的面试表现了。

  • 第三就是编程能力

    编程思想,算法能力,架构能力。首先2W程序员对算法的要求我觉得还是比较低,再高级也最多红黑树吧,但是排序和查询的基本算法得会。编程思想是必须的,问你个AOP和IOC你起码的清清楚楚,设计模式不说每种都用过,但也能了解个几种吧。编程能力这个我觉得不好去评价,但是拿一个2000W用户根据姓名年龄排序这种题目也能信手拈来。最后就是架构能力,这种不是说要你设计个多牛逼多高并发的系统,起码让你做一个秒杀系统,防重请求的设计能快速搞定而没有坑吧。

感谢

在北京做Java开发如何月薪达到两万,需要技术水平达到什么程度?
零基础应该选择学习 java、php、前端 还是 python?