天明的小窝


  • 首页

  • 归档

  • 标签

限流算法

发表于 2018-09-24 | 阅读次数:

前言

上周是收获最多的一周,先是被大佬套路如何排查问题,之后又一脚踩进Go的坑中,最后又见识了用户各种薅羊毛的路数。在系统设计中,通常要要秉承两条原则:

  • 上层保护下层,作为主调方要从参数,流量等保护下层接口,面对架构复杂即使不能保护下层接口,也要有保护的意识
  • 下层不信任上层,作为被调方要对接收的参数验证,来源验证,防止被恶意攻击。在内部系统中参数还好控制,一旦暴露在外部,什么牛鬼蛇神都能传进来。要强调一句对于基本数据类型的范围一定要有一个认识,不只是2的n次方-1什么的,而是要具体到大约多少亿范围。在工作中发生过调用方传超出范围的字符串,而调用integer.parseint抛出runtime exception但没有方法catch导致调用超时无数据返回的情况。无数据返回在RPC调用中非常致命,只能等超时返回,严重降低并发。也就是说只要调用必须控制超时时间,执行异常必须返回信息,不能等待超时断开连接返回空。

高并发系统中有三种常见方式来保护系统:缓存、降级和限流。缓存和降级在缓存使用系列讲过了,这回来讲限流算法。

常见的限流地方有:限制并发总数(数据库连接池、线程池、单ip并发数)、限制瞬时并发数、限制时间窗口内的平均速率等等。

1. 限流算法

1.1 漏桶算法(leaky bucket)

漏桶作为计量工具时,可以用于流量整形和流量控制,漏桶算法的描述如下:

  • 容量固定,按照固定速率流出水滴
  • 如果桶为空,则不流出水滴
  • 可以按照任意速率流入水滴到漏桶
  • 如果流入水滴超出了桶的容量,流入的水滴被丢弃,桶的容量不变Leaky_bucket_analogy.jpg 图来自维基百科

1.2 令牌桶算法(token bucket)

令牌桶算法是存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:

  • 每 1/r 秒往桶中添加一个令牌
  • 桶中最多存放b个令牌,当桶满时新添加的令牌被丢弃
  • 当n个字节的数据包到达时
    • 如果桶中存在多于n个令牌,删除n个令牌,将数据包发送到网络上
    • 不足n个令牌,不会删除令牌,数据包被丢弃或者放入缓冲区令牌桶.png 图来自参考1

漏桶算法和令牌桶算法的区别:

  • 令牌桶算法允许一定程度的并发,只要桶中有令牌就可以。漏桶法通过限制常量流出速率从而平滑流入速率
  • 两个算法的实现可以一样,但是方向是相反的,对于相同参数得到的限流效果是一样的

1.2 计数器法和滑动窗口法

计数器法相对其它算法是最容易实现的,当然各种场景下的计数器法都有致命问题,GC中的计数难以处理循环引用,这里的计数器面对临界值也有问题。比如我们统计1分钟内的访问次数,限制最多为100,每次有请求计数器加一,如果59秒时来了100个请求,1分钟时又来了100个请求,相当于2秒内来了200个请求,超出限制。有什么解决办法呢?想一想有什么协议也需要控制流量,TCP!TCP中的滑动窗口就是很好的实现方式。这里不再写了。可以参考链接里的第二篇

参考资料

并发系统之限流特技
接口限流算法总结

应用服务器性能优化

发表于 2018-09-18 | 阅读次数:

前言

网站的业务逻辑都部署在应用服务器上,是主要优化的地方,优化的常见方式是缓存、集群、异步等。

1.分布式缓存

缓存的使用无处不在,ORM框架中的多级缓存,MVC框架中的模板缓存,浏览器缓存,几乎任何框架都提供了缓存功能。

网站性能优化第一定律:优先考虑使用缓存优化性能

1.1 缓存的基本原理

简单而言都是KV,即Key-Value形式,在这之上有Key-Key-Value形式(Redis的Sorted Set,TODO:Redis数据结构实现原理),用的最多还是KV。KV中的Value是二进制安全的。

Java中的hashCode方法可以得到hashcode,这里要说的知识点很多,equals和hashCode任一方法重写都需要重写,准确的说是必须,不然会影响集合类的使用。null对象也是有哈希值的,是0!只有HashTable直接使用hashCode的值,其余都会再扰动计算。只有HashMap允许key或者value为空情况出现,ConcurrentHashMap和淘汰的HashTable都不允许,因为多线程环境无法区分key是put时为null还是key不存在,而且null作为key很容易受到攻击。SynchronizedMap因为可以包装HashMap而可以支持,但是原理和HashTable一样,锁粒度太大导致性能都很差。(TODO:Java并发集合)

1.2 合理使用缓存

  • 频繁修改的数据不适用缓存:读写比小于2:1
  • 热点访问数据适合缓存:尽量保证缓存高频访问的数据
  • 数据不一致与脏读:设置合理的缓存更新策略,一般为缓存设置失效时间
  • 缓存可用性:保证缓存服务的稳定,通过分布式集群等
  • 缓存预热:针对热点数据缓存,比如利用LRU算法等需要较长时间,可以提前人工加载热点数据
  • 缓存穿透、缓存雪崩、缓存击穿:使用缓存的经典问题,见缓存使用

1.3 分布式缓存

分布式缓存指缓存部署在多个服务器组成的集群中,以集群方式提供缓存服务,其架构方式有两种。一种是以JBoss Cache为代表的需要更新同步的分布式缓存,一种是以Memcached为代表的不互相通信的分布式缓存(TODO:Memcached以后填坑)。

2. 异步操作

很多情况下使用异步操作可以改善网站性能,使用消息队列的常见作用就是削峰填谷,将部分操作异步避免前面业务的等待,同时要注意不要把所有后续业务的处理都放在单一的业务逻辑中。如注册后的发邮件就可以放入消息队列,后续处理交由邮件发送消费者。使用消息队列要注意重放攻击,对于哪些操作能放入消息队列,生产者处理失败如何处理要有规定。

任何可以晚点做的事情都应该晚点再做

3. 使用集群

在高并发场景下使用负载均衡技术,通过多台服务器为应用构建集群是最简单的方式。请求太多怎么办,上机器!单一的机器受各种限制,配合微服务使用应用集群能够有效处理并发。

4. 代码优化

代码写的烂再多的机器都不一定能救。不同代码的优化方式有别,诸如Java从Web容器,ORM框架选择,JDK本身的优化,JVM调优等。

4.1 多线程

多线程是基本的措施,但要考虑线程安全问题。使用多线程的原因主要是IO阻塞与多CPU,当线程进行IO处理时会被阻塞,这时CPU会被释放可以调度其它线程。

网站的应用程序都是被Web服务器容器管理,不论时Web服务器容器管理的线程还是应用程序自己创建的线程,都不易过多,线程切换虽然比进程切换快,但是过多的线程会带来调度问题。通常启动的线程数:

启动线程数=[任务执行时间/(任务执行时间(IO等待时间)]]* CPU内核数

最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。

多线程带来线程安全问题,常见的解决手段有,我会单独写一片effective java读后感(TODO:线程安全):

  • 对象设计为无状态:无状态对象肯定时线程安全的
  • 使用局部对象:被多线程访问的对象会出现可见性问题,因为工作内存中的对象是共享内存的拷贝
  • 并发访问资源使用锁:强一致性要加锁,并合理降低锁的粒度
  • 防止对象逸出:切勿将内部对象返回给外部

4.2 资源复用

资源复用常见的是单例模式和对象池。对象池有许多种类,如数据库连接池、线程池,如何设计一个线程池也是有趣的问题(TODO:线程池设计),要考虑核心线程池大小、最大大小、阻塞队列设计(有限无限)、拒绝策略、线程存活时间(长时间未使用要回收)。

4.3 数据结构

优化存储结构,合理使用Redis的存储类型,选择合适的MySQl引擎

4.4 垃圾回收

如Java中要对JVM调优和内存泄漏,这里经验不多,改日填坑(TODO:JVM调优)

参考资料

《大型网站技术架构:核心原理与案例分析》

Web前端性能优化

发表于 2018-09-11 | 阅读次数:

前言

作为后端,也要经常和前端对需求,前端的优化也要了解,同时对于HTTP协议也要有一定的认识(TODO:HTTP协议)。

1. 浏览器访问优化

1.1 减少http请求

HTTP协议是基于TCP的无状态应用层协议,每次HTTP请求都要建立通信链路进行数据传输,服务器端则要启动独立的线程来处理(实际上Servlet容器都是使用线程池复用连接;高并发情况下我们也会使用各种手段过滤重复的请求,比如前端按钮点击失效一段时间)。
为了减少请求HTTP使用了很多手段(TODO:HTTP1.0、1.1、2区别):

  • HTTP1.1使用了长连接,传输完成后一段时间保持连接不关闭(前端和后端都要配置)
  • HTTP2可以使用服务器推送
    目前使用的减少HTTP的主要方式是合并资源文件为一个文件,如CSS、JS、图片等,前端一般会使用Webpack,实际过程很复杂,感觉类似MAVEN。图片可以多张合并,通过CSS偏移响应鼠标点击操作,构造成不同的URL(这个操作好灵性,好像在京东还是什么网站见到过使用)。

1.2 使用浏览器缓存

对于网站而已,CSS、JavaScript、logo图片等静态资源更新的频率都很低,但是每次HTTP请求都要重新请求这些资源,无疑要浪费大量的宽带。可以通过HTTP缓存机制,设定HTTP头中Cache-Control和Expires的属性(TODO:HTTP缓存机制)。
如果碰到静态资源文件变化需要及时应用到客户端浏览器,可以通过改变文件名,生成一个新的JS文件并更新HTML文件中的引用。
使用浏览器缓存策略更新静态资源时,应采用批量更新的方法(?按照后文推断,应该是避免批量更新),比如需要更新10个图标文件,不宜把10个文件一次全部更新,而是应一个文件一个文件逐步更新,并有一定的间隔时间,以免用户浏览器突然大量缓存失效,集中更新缓存,造成服务器负载骤增、网络堵塞的情况。就是避免缓存雪崩情况出现,包括App的更新推送等实际过程中都是分批推送更新通知的,集中一起更新会打满CDN(这里以前出过比较大的问题)。

1.3 启用压缩

采用压缩是一种CPU换时间的做法,压缩和解压本身也要消耗一定的时间,要权衡考虑,一般对文本采用压缩,如HTML、CSS、JS文件,压缩过后同时还有利于网络传输。JS代码本身是可以压缩的,这一部分一般通过WebPack管理。使用时要设置HTTP头的Content-Encoding字段。

1.4 CSS放在页面最上面、JavaScript放在页面最下面(先渲染,后加载逻辑)

浏览器会在下载完全部CSS之后才对整个页面进行渲染,因此最好的做法是将CSS放在页面最上面,让浏览器尽快下载CSS。JavaScript则相反,浏览器在加载JavaScript后立即执行,有可能会阻塞整个页面,造成页面显示缓慢,因此JavaScript最好放在页面最下面。但如果页面解析时就需要用到JavaScript,这时放在底部就不合适了。同时要注意,如果JS还没加载完用户就先操作了,这时可能时无效的。这类做法与抢红包中的抢拆分离类似,目的时造成加载完成的错觉,避免用户的焦急,动画等特效至少可以为后端和链路上的消耗争取几百毫秒时间。

1.5 减少Cookie传输

每次请求与回应都会完全传输整个Cookie,Cookie太大会影响传输效率,写入Cookie的数据要慎重考虑,也可以把部分数据存储在后端的Session中,这时后端要维护这个Session。对于静态资源的请求使用同一域名也会发送Cookie,静态资源可以使用独立域名,避免发送Cookie。

2. CDN加速

CDN(Content Distribute Network,内容分发网络)的本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,CDN一般缓存更新频率不高的静态资源,CDN工作流程各个代理商解释的都很详细,推荐阅读腾讯CDN产品概述和什么是阿里云CDN。在CDN过程中DNS起着很大的作用,DNS有着负载均衡的作用,怪不得面试总要考HTTP请求流程,往详细说很复杂(TODO:如何选出最近的服务器,设计接入系统)。

3. 反向代理

CDN位于用户接入点附近,而反向代理则位于网站机房的最外侧,代理接收到的HTTP请求,同时也可以实现负载均衡的作用,根据负载均衡算法分发请求。
反向代理的优点:

  • 实现负载均衡:构建应用集群,提高系统总体处理能力
  • 安全:来自互联网的请求必须经过代理服务器,在Web服务器和可能的网络攻击之间建立屏障
  • 加速Web请求:静态内容缓存在反向代理服务器,减轻Web服务器负载压力。静态内容和动态内容都可以,动态内容要通过通知机制更新
    反向代理的缺点:
  • 处于OSI参考模型的应用层,每一种协议都需要单独开发反向代理服务器,限制了使用的范围。目前一般用于Web服务器
  • 每次代理,代理服务器就必须打开两个连接,分别针对对内、对外,并发量大时代理服务器可能成为性能瓶颈。

参考资料

《大型网站技术架构:核心原理与案例分析》

缓存使用系列

发表于 2018-09-02 | 阅读次数:

实际工作中经常用到Redis作为缓存,只要用到缓存,就会碰到缓存穿透、缓存雪崩和缓存击穿。在看了很多博文之后记录一下关于缓存的问题,每个博文有各自的观点,这里做一下总结和个人的理解。

1. 缓存穿透

在大多数互联网应中,缓存是不可或缺的组件,缓存的使用方式如下:

  1. 当业务系统发起某一个查询请求时,首先判断缓存中是否有该数据;
    1.1 如果缓存中存在,则直接返回数据
  2. 缓存层不命中,查询数据库
  3. 然后返回数据,若无返回空结果,有则将结果写入缓存层同时返回结果(存储层和缓存层应有同步措施,保证缓存了最新的数据)缓存穿透.png

1.1 什么是缓存穿透

在实际的项目中,我们查询数据通常先从缓存中读取,如果缓存存在直接返回。不存在就查询数据库,然后返回结果。如果查询的数据一直不存在,就会一直查询DB,这样缓存就没有作用,当流量很大的时候,DB有可能挂掉。
简单而言就是:请求了大量根本不存在的数据,导致所有的请求直接请求到后端DB上,造成DB异常。

1.2 为什么会发生缓存穿透

发生缓存穿透的原因很多,一般为两种:

  1. 恶意攻击和爬虫造成大量空命中
  2. 业务自身代码或者数据出现问题

1.3 如何发现

通常使用日志、打点等,记录调用总量、缓存命中数、存储命中数,如果短时间内空命中非常多,可能存在缓存穿透。

1.4 缓存穿透解决方法

1.4.1 缓存空对象

在第3步中储存层不命中后,将空对象保存到缓存层中(使用特定key来表示),之后的访问将从缓存层中去查询获取到,从而保护了存储层。
这么做也会带来两个问题:

  1. 对空值缓存意味着需要额外的空间来存放键值,如果是恶意攻击会导致严重的内存占用。有效的措施是对这类数据设置一个较短的过期时间,让其自动剔除(redis对于过期的key采用定时加惰性删除的机制,定时删除不保证一定会清除过期key,除非主动访问)。
  2. 缓存层和存储层如果没有同步措施会存在一段时间的窗口不一致,如果设置了过期时间为5分钟,这时存储层添加或者修改了这个数据,此时缓存层和数据层消息就不一致了。我们可以利用消息系统或者其他方式清除掉缓存层中的空对象。

1.4.2 布隆过滤器拦截

一种可行的方案是使用布隆过滤器,将所有存在的key使用布隆过滤器提前保存起来。业务系统首先去查询布隆过滤器中是否存在该key,如果不存在直接返回,因为数据库中也不存在该数据,缓存中也不会存在。
布隆过滤器.png

其中5.向bitmap添加数据和缓存非空数据取决于业务的需求,我们一般会定时或者批量更新,而不是每次都触发更新。使用布隆过滤器并不适合强时效和高一致性的场景,布隆过滤器无法处理删除数据,重新生成bitmap将耗费一定时间,而且使用前要预热bitmap。

1.4.3 两种方式的对比

解决方式 适用场景 维护成本
缓存空对象 1.数据命中不高
2.数据频繁变化实时性高
1.代码维护简单
2.需要过多的缓存空间
3.可能存在数据不一致
布隆过滤器 1.数据命中不高
2.数据相对固定实时性低
1.代码维护复杂
2.缓存占用空间少

2. 缓存雪崩

2.1 什么是缓存雪崩

在实际应用中,通过合理的设置缓存,大部分请求都通过缓存层返回,有效的保护了存储层,但是由于缓存层出现问题(比如宕机或者缓存超时集体失效),所有或者超出存储层极限的调用都会直接到达存储层,从而导致存储层也挂掉。

2.2 如何预防缓存雪崩

2.2.1 保证缓存层服务高可用

面对这种情况我们一般引入集群,通过负载均衡算法分解请求,Redis Sentinel和Redis Cluster暂时还没有用过,TODO:负载均衡算法和Redis集群部分改日填坑。

2.2.2 依赖隔离组件为后端限流并降级

基础组件不是永远可靠的,在实际过程中经常碰到基础服务出现问题,小则过载,大则宕机,一些过载恢复机制会随机将请求发送到过载节点,通过返回结果来判断过载服务是否恢复,至于是谁来做重试要看业务的需求。
降级在高并发系统中是常见的:比如个性推荐服务中,如果个性化需求不能提供服务了,可以降级补充热点数据,不至于造成前端页面空白。
至于hystrix我目前并没有用过,现在组件都是微服务了,基础服务和业务都有自己线程池管理方式,改日再看情况填坑。

3. 缓存击穿(热点key失效)

这里我看了几篇博文,发现缓存击穿和热点key失效说的是一回事。缓存雪崩和缓存击穿的区别是,缓存雪崩针对的是大量key,击穿针对很少的几热点个key产生大量请求,可以看作是雪崩中的的特例,但请求量会非常大。

3.1 缓存击穿的危害

当以下两个问题同时出现,可能会对系统造成致命的危害:

  1. 这个key是热点key(例如重要的新闻等),短时间内的访问量可能非常大。
  2. 缓存的构建需要一定时间,比如要经过复杂运算、多次IO、数据库访问、依赖其它接口等等(比如游戏的赛季排行榜)。

于是就会碰到致命问题,缓存失效的瞬间有大量的线程来构建缓存(如下图,直接盗图),造成后端负载加大,甚至系统崩溃。
缓存击穿.png

3.2 缓存击穿的解决方案

我们尽量使用较少的线程来构建缓存(甚至是一个)

3.2.1 使用互斥锁

互斥锁的解决思路比较简单,就是同一时间只让一个线程来构建线程,其他线程等待缓存构建完,再从缓存中获取数据就可以。

互斥锁分单机和分布式环境。单机环境使用编程语言提供的锁来处理便可,分布式环境使用分布式锁就可以,Redis和zookeeper都可以实现分布式锁,如何实现改日再填坑(TODO:分布式锁)
原文都给了互斥锁的实现,但是这个锁是有问题的,而且是有发生死锁的可能性,想一想为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 设定一个时间,防止db.get()方法产生死锁,要保证这个时间大于db.get()的执行时间
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}

原因:Redis事务和传统数据库事务定义不同,setnx和expire这两个操作并不能保证原子性,如果setnx成功之后代码没有执行就会一直锁住,原博主没有详细解释产生死锁的原因。

3.2.2 永不过期

  1. Reis上不再设计过期时间,保证“物理”不过期
  2. 过期时间放在key对应的value里,如果过期了,先返回旧值,后台通过异步线程构建缓存,然后更新,也就是“逻辑”上过期(Java的CopyOnWrite非常与之类似,TODO:CopyOnWrite容器源码分析)

这种方式对于性能非常友好,几乎不会造成请求的阻塞,但是不适用强一致性的数据读取,因为其余线程可能读到的是老的数据。

3.2.3 两种方式的对比

解决方案 优点 缺点
互斥锁 1.思路简单
2.保证强一致性
1.代码复杂度增大
2.存在死锁的风险
3.存在线程池阻塞的风险
永不过期 1.异步构建,不会阻塞线程池
1.不保证一致性
2.代码复杂度增大(每个value都要维护一个timekey)
3.占用一定的内存空间(每个value都要维护一个timekey)。

参考资料

  • Redis架构之防雪崩设计:网站不宕机背后的兵法
  • 缓存穿透、缓存并发、热点缓存之最佳招式
  • 《Redis开发与运维》
  • 缓存系列文章–5.缓存穿透问题

Maven学习笔记

发表于 2018-08-23 | 阅读次数:

Maven学习笔记

1. 是什么

基于Java的跨平台项目管理及自动构建工具

2. 入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?> <!--xml版本和编码方式 -->
<!--project是pom.xml的根元素-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--modelVersion指定了POM模型的版本,Maven2和3只能为4.0.0-->
<modelVersion>4.0.0</modelVersion>
<!--groupId定义项目属于哪个组,同项目所在的组织或者公司存在关联-->
<groupId>com.demo</groupId>
<!--当前项目在组中唯一的ID-->
<artifactId>maven-demo</artifactId>
<!--项目当前的版本号-->
<version>1.0-SNAPSHOT</version>
</project>

以上是Maven项目的基本配置文件,其余的各项配置写在<project></project>标签内

3. 坐标和依赖

3.1 入门

我们先看一组坐标定义

1
2
3
4
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>

  • groupId:定义当前Maven项目隶属的实际目录,groupId不应该对应项目隶属的额组织或者公司,由于组织下会有许多实际项目,groupId只定义到组织级别,artifactId只能定义Maven项目(模块),实际项目将难以定义。
  • artifactId:定义实际项目中的一个Maven项目(模块),推荐使用实际项目名称作为artifactId的前缀
  • version:定义Maven项目所处的版本
  • packaging:定义Maven项目的打包方式,打包方式通常与所生成构件的文件扩展名对应,打包方式会影响到构建的生命周期,不定义packaging的时候,Maven会使用默认至jar。
  • classifier:定义构建输出的一些附属构建,附属构建与主构建对应。如上例中主构建是nexus-indexer-2.0.0jar,该项目可能还会通过使用一些插件生成如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-sources.jar这样一些附属构建,其中包含了Java文档和源代码。javadoc和sources就是这两个附属构建的classifier。不能直接定义项目的classifier,因为附属构件不是项目直接默认生成的,而是有附件的插件帮助生产
    上述5个元素中,groupId、artifactId、version是必须定义的,packaging是可选的(默认为jar),而classifier是不能直接定义的。
    项目构件的文件名是与坐标相对应的额,一般的规则为artifactId-version[-classifier].packaging。

3.2 详解

一个依赖可以包含如下的一些元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<project>
...
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
...
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

  • type:依赖的类型,对应于项目坐标定义的packaging。大部分情况下,该元素不必声明,默认为jar。
  • scope:依赖范围,见3.2.1
  • optional:依赖是否可选
  • exclusions:排除依赖传递性

3.2.1 scope详解

scope有以下几种以来范围:

  • compile:编译依赖范围。如果没有指定默认使用该依赖范围。此种依赖范围对于编译、测试、运行三种classpath都有效,如spring-core。
  • test:测试依赖范围。对测试classpath有效,在编译主代码或者运行项目时无法使用此依赖,如Junit。
  • provided:已提供依赖范围。对编译和测试classpath有效,但在运行时无效。如servlet-api,编译和测试时需要,运行时由容器提供,不需要重复引用。
  • runtime:运行时依赖范围。对测试和运行有效,编译时无效。如JDBC驱动实现,项目代码只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
  • system:系统依赖范围。同provided,对编译和测试classpath有效,但在运行时无效,但是必须通过systempath元素显示指定依赖文件的路径。不可移植,可引用环境变量。如:

    1
    2
    3
    4
    5
    6
    7
    <dependency>
    <groupId>javax.sql</groupId>
    <artifactId>jdbc-stdext</artifactId>
    <version>2.0</version>
    <scope>system</scope>
    <systemPath>${java.hone}/lib/rt.jar</systemPath>
    </dependency>
  • import:导入依赖范围,只在dependencyManagement元素下有效,见6.2.2

3.3 传递性依赖

3.3.1 什么是传递性依赖

Mavan会解析各个直接依赖的POM,将必要的间接以来,以传递性以来的形式引入到当前的项目中。如项目依赖spring-core,sprng-core又依赖commons-logging,那commons-logging就会成为当前项目的依赖范围。

3.3.2 传递性依赖和依赖范围

依赖范围不仅控制依赖与三种classpath的关系,还会对传递性依赖产生影响。最左边一列表示直接依赖范围,第一行表示第二直接依赖范围。

compile test provided runtime
compile compile - - runtime
test test - - test
provided provided - provided provided
runtime runtime - - runtime

3.4 依赖调解

当项目的依赖关系中对同一依赖有不同版本的描述,就会发生依赖调节,遵循一下两条原则

  • 路径最近者有限:如A->X(1.0)->C,A->B->c->X(2.0),X(1.0)长度为2,X(2.0)长度为4,Maven会选择最近的X(1.0)
  • 第一声明者优先:当依赖路径长度相等的前提下,在POM中依赖声明中顺序最考前的依赖会被解析使用

3.5 可选依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
...
<dependencies>
<!-- declare the dependency to be set as optional -->
<dependency>
<groupId>sample.ProjectA</groupId>
<artifactId>Project-A</artifactId>
<version>1.0</version>
<scope>compile</scope>
<optional>true</optional> <!-- value will be true or false only -->
</dependency>
</dependencies>
</project>

该项目中将Project-A声明为可选依赖,如果其他项目依赖该项目并要使用Project-A,就需要显示的声明Project-A的依赖。理想情况下我们不应该使用可选依赖,而是分别创建一个Maven项目。

3.6 实战

3.6.1 排除依赖

当项目中的某个类库不稳定会影响到当前项目或者要替换某个传递性依赖时就需要排除依赖。具体是在<dependency></dependency>中增加<ecxlusions></ecxlusions>标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<project>
...
<dependencies>
<dependency>
<groupId>sample.ProjectA</groupId>
<artifactId>Project-A</artifactId>
<version>1.0</version>
<scope>compile</scope>
<exclusions>
<exclusion> <!-- declare the exclusion here -->
<groupId>sample.ProjectB</groupId>
<artifactId>Project-B</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

我们将Project-B排除,注意不需要version,因为groupId和artifactId能唯一确定依赖图中的某个依赖,在以后的Maven解析中,不可能出现groupId和artifactId相同,但是version不同的两个依赖。

3.6.2 归类依赖

通过使用Maven属性在<version></version>标签中使用${springframework.version}来归类依赖

3.6.3 优化依赖

Maven会自定解析项目的直接依赖和传递性以来,依据股则判断依赖范围,对依赖冲突进行调节确保任何一个构件只有唯一的版本在依赖中存在,最后得到的那些依赖被称为已解析依赖(Resolved Dependency)。

  • mvn dependency:list:查看当前项目的已解析依赖
  • mvn dependency:tree:查看当前项目的依赖树
  • mvn dependency:analyze:分析当前项目的依赖

4. 仓库

4.1 介绍

Maven在某个位置统一存储所有Maven项目共享的构建,实际的Maven项目将不再各自存储其依赖的文件。

4.2 仓库的布局

Maven构件的存储路径为:groupId/artifactId/version/artifactId-version[-classifier].packaging。句点分割会被转化成路径分割,如log4j:log4j:1.2.15对应的仓库路径为log4j/log4j/1.2.15/log4j-1.2.15.jar。

4.3 仓库分类

仓库分为两类:本地仓库和远程仓库,优先查找本地仓库是否有构件。远程仓库有两类特殊仓库:中央仓库和私服。

  • 中央仓库:Maven核心自带的远程仓库,包含了绝大部分开源构建。默认本地没有Maven需要的构建时会从中央仓库下载。
  • 私服:在局域网内架设的私有仓库服务器,用来代理所有外部的远程仓库,内部项目可以部署到私服上供其他项目使用。

4.3.1 本地仓库

默认情况下,有一个路径名为~/.m2/repository/的仓库,如果用户想要自定义仓库的位置,可以编辑该文件。如:

1
2
3
<settings>
<localRepository>D:\java\repository</localRepository>
</settings>

默认情况下该文件不存在,用户需要从$M2_HOME/conf/settings.xml复制再编辑,不推荐直接修改全局目录的settings.xml文件,因为每次升级该文件都会被覆盖。

4.3.2 中央仓库

由于必须有至少一个可用的远程仓库才能在执行Maven命令时下载到需要的构件,可以解压Maven的jar文件,然后访问路径org/apache/maven/model/pom-4.0.0.xml查看pom中央仓库配置。所有的Maven都会继承该超级POM。

1
2
3
4
5
6
7
8
9
10
11
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

4.3.3 私服

一种特殊的远程仓库,架设在局域网内。当Maven需要下载构件的时候,先向私服请求,如果私服上不存在,则会从外部的远程仓库下载,缓存在私服上。无法从外部仓库下载到的构件也能从本地上传到私服供大家使用。
私服的用途.png

节省外网带宽 消除对外重复构件的下载
加速Maven构建 Maven的一些内部机制(如快照更新检查)会在执行构建的时候不停地检查远程仓库数据,当配置了很多外部仓库的时候,构建的速度会被大大降低。使用私服时只需要检查局域网内私服的数据
部署第三方构件 私有构件无法从外部仓库获得,部署到内部仓库,供内部的Maven项目使用
提高稳定性,增强控制 防止Internet不稳定的时候,Maven构建变得不稳定。一些私服软件(如Nexus)还提供了许多额外的功能,如权限管理、RELEASE/SNAPSHOT区分等,管理员可以对仓库进行一些更高级的控制
降低中央仓库的负荷 避免对中央仓库重复的下载

4.4 远程仓库的配置

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
<repositories>
<!-- 使用repository申明一个或者多个远程仓库 -->
<repository>
<!-- 仓库唯一标识,重复会覆盖上一个远程仓库 -->
<id>...</id>
<!-- 仓库名称 -->
<name>...</name>
<!-- 仓库地址,一般来说该地址都是基于http协议 -->
<url>...</url>
<!-- 重要!控制Maven对于发布版本构件的下载 -->
<releases>
<!-- true/false 控制发布版本构件的下载 -->
<enabled>...</enabled>
<!-- 更新策略 daily(默认,每天一次)、never(从不)、always(每次构建)、interval:X(间隔X分钟) -->
<updatePolicy>...</updatePolicy>
<!-- 检查检验和文件的策略 warn(默认,校验失败,输出警告信息)、fail(遇到校验和错误就要构建失败)、ignore(忽略校验失败) -->
<checksumPolicy>...</checksumPolicy>
</releases>
<!-- 重要!控制Maven对于快照版本构件的下载 -->
<snapshots>
<!-- true/false 控制快照版本构件的下载 -->
<enabled>...</enabled>
<!-- 更新策略 daily(默认,每天一次)、never(从不)、always(每次构建)、interval:X(间隔X分钟) -->
<updatePolicy>...</updatePolicy>
<!-- 检查检验和文件的策略 warn(默认,校验失败,输出警告信息)、fail(遇到校验和错误就要构建失败)、ignore(忽略校验失败) -->
<checksumPolicy>...</checksumPolicy>
</snapshots>
<!-- 仓库布局方式 -->
<layout>default</layout>
</repository>
...
</repositories>

4.4.1 远程仓库的认证

大部分远程仓库无需认证就可以访问,但有些时候出于安全方面的考虑,需要提供认证信息才能访问一些远程仓库,如私服等。
认证信息必须配置在settings.xml文件中,POM往往被提交到代码仓库中供所有成员访问,为了保证安全写在本机的settings.xml文件

1
2
3
4
5
6
7
8
9
10
11
<settings>
...
<servers>
<server>
<id>..</id><!-- 需要提供认证信息才能访问的远程仓库ID,与repository元素的id完全一致 -->
<username>...</username> <!-- 用户名 -->
<password>...</password> <!-- 密码 -->
</server>
</servers>
...
</settings>

4.4.2 部署至远程仓库

私服的一个作用是部署第三方构件,Maven除了能对项目进行编译、测试、打包之外,还能将项目生成的构建部署到仓库中。

  • 1.首先编辑pom文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <project>
    <!-- 写在distributionManagement元素中 -->
    <distributionManagement>
    <repository> <!-- 指定发布版本构件的仓库 -->
    <id>...</id>
    <name>...</name> <!-- name是方便人阅读 -->
    <url>...</url>
    </repository>
    <snapshotRepository> <!-- 指定快照版本构件的仓库 -->
    <id>...</id>
    <name>...</name>
    <url>...</url>
    </snapshotRepository>
    </distyibutionManagement>
    </project>
  • 2.往远程仓库部署构件时往往需要认真,要配置认证信息

  • 3.执行 mvn clean deploy,部署到远程仓库

4.5 快照版本

Maven中任何一个项目或者构件都必须有自己的版本,本分分为发布版和快照版。
发布版:稳定,版本值可能是1.0.0、1.3alpha-4、2.0
快照版:不稳定,版本值2.1-SNAPSHOT、2.1-20091214.221414-13
只需将版本号后加-SNAPSHOT,然后发布到私服中,Maven就会自动为构件打上时间戳,如2.1-20091214.221414-13 表示2009年12月14日22时14分14秒第13次快照。
默认情况下,Maven每天检查一次更新(由仓库配置的updatePolicy控制),用户也可以强制让Maven检查更新,如mvn clean install -U

4.6 从仓库解析依赖的机制

当本地仓库没有依赖构件时,Maven会自动从远程仓库下载;当依赖版本为快照版本时,Maven会自动找到最新的快照。其依赖解析机制如下

  • 1)依赖范围是system,Maven直接从本地文件系统解析构件。
  • 2)根据依赖坐标计算仓库路径,尝试直接直接从本地仓库寻找构件,如果发现相应构件,则解析成功。
  • 3)本地仓库不存在相应构件的情况下,如果依赖的版本是显式的发布版本构件(如1.0,2.1-beta-1),遍历所有远程仓库,发现后下载并解析使用
  • 4)如果依赖版本是RELEASE和LATEST,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库的对应元数据合并后,计算出RELEASE或LATEST的真实的值,然后基于这个值检查本地和远程仓库,如步骤2)和3)。
  • 5)如果依赖版本是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值检查本地仓库和远程仓库。
  • 6)如果最后解析得到的构件版本是时间戳格式的快照,如1.4.1-20091104.121450-121,则复制其时间戳格式文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的构件。
    当依赖的版本不明晰的时候,如RELEASE、LATEST和SNAPSHOT,Maven需要基于更新远程仓库的更新策略来检查更新。与<release>和<snapshots>中的子元素<enable>和<updatePolicy>有关,只有enable为true才能访问该仓库对应发布版本的构件信息。依赖声明中不推荐使用LATEST和RELEASE,因为Maven随时可能解析到不同的构件,且Maven不会明确告诉用户这样的变化。
    Maven3不再支持在插件配置中使用LATEST和RELEASE,如果不设置插件版本,其效果和RELEASE一样,Maven只会解析最新的发布版本构件

4.7 镜像

如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。使用镜像的目的是提供比中央仓库更快的服务,修改settings.xml如下。

1
2
3
4
5
6
7
8
9
10
<settings>
...
<mirror>
<id>nexus-aliyun</id>
<!-- mirrorOf代表被代理的地址,该值与前面的repository的id值保持一致,central表示代理中央接口-->
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</settings>

<mirrorOf>标签的匹配规则如下:

  • *:匹配所有远程仓库
  • external:*:匹配所有远程仓库,使用localhost和file://协议的除外。也就是说,匹配所有不在本机上的远程仓库
  • repo1,repo2:匹配仓库repo1和repo2,逗号分隔
  • ,!repo1:匹配除repo1以外的所有远程仓库,感叹号排除
    *镜像完全屏蔽了仓库镜像,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像的仓库,因而无法下载构件

4.8 仓库搜索服务

  • Sonatype Nexus
  • MVNrepository

5. 生命周期和插件

5.1 生命周期简介

Maven从大量项目和构建工具中学习和反思,总结了一套高度完善的、易扩展的生命周期。包含项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等。
Maven的生命周期是抽象的,实际的任务都交由插件来完成。

5.2 生命周期详解

5.2.1 三套生命周期

Maven拥有三套相互独立的生命周期,分别为clean、default和site,调用这三者中的一个不会触发其它两个生命周期。

  • clean:清理项目
  • default:构建项目
  • site:建立目标站点

每个生命周期包含一些有序阶段,并且后面的阶段依赖于前面的阶段。如clean生命周期包含pre-clean、clean和post-clean,调用pre-clean的时候,只有pre-clean阶段执行调用clean的时候pre-clean和clean阶段都会执行。

5.2.2 clean生命周期

clean生命周期的目的是清理项目,它包含三个阶段:

  • pre-clean:执行一些清理前需要完成的工作
  • clean:清理上一次构建生成的文件
  • post-clean:执行一些清理后需要完成的工作

5.2.3 default生命周期

default是所有生命周期中的核心,定义了真正构建时所需要执行的所有步骤

  • validate:验证这个项目是否正确,所有必需资源是否可用
  • initialize:初始化编译的状态,例如:设置一些properties属性,或者创建一些目录
  • generate-sources:生成所有在编译阶段需要的源代码
  • process-sources:处理项目主资源文件。一般来说,是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的主classpath目录中。
  • generate-resources:生成这个项目包所有需要包含的资源文件
  • process-resources:复制并处理资源文件到目标目录,为packaging 打包阶段做好准备
  • compile:编译项目的主源码,一般来说,是编译src/main/java目录下的Java文件至项目输出的主classpath目录中
  • process-classes:后置处理编译阶段生成的文件,例如:做java字节码的加强操作
  • generate-test-sources:生成编译阶段需要的test源代码
  • process-test-sources:处理项目测试资源文件。一般来说,是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的测试classpath目录中。
  • generate-test-resources:生成test测试需要的资源文件
  • process-test-resources:复制并处理资源文件到test测试目标目录
  • test-compile:编译项目的测试代码到指定test目标目录
  • process-test-classes:后置处理test编译阶段生成的文件,例如:做java字节码的加强操作
  • test:使用合适的单元测试框架,运行所有测试例子,这些测试用例不应该要求这些代码被打包或者部署才能执行
  • prepare-package:处理任何需要在正式打包之前要完成的必须的准备工作。这一步的通常结果是解压,处理包版本等
  • package:打包编译后的代码成可发包格式,例如:jar,war等
  • pre-integration-test:完成一些在集成测试之前需要做的预处理操作,这通常包括建立需要的环境。
  • integration-test:处理并部署(deploy)包到集成测试可以运行的环境中
  • post-integration-test:处理一些集成测试之后的事情,通常包括一些环境的清理工作
  • verify:做一些对包的验证操作,去检测这个包是一个合法的符合标准的包。
  • install:将包安装到本地仓库,供本地其他Maven项目使用
  • deploy:将最终的包复制到远程仓库,供其他开发人员和Maven项目使用

5.2.4 site生命周期

site生命周期的目的是建立和发布项目站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息

  • pre-site:执行一些在生成项目站点之前需要完成的工作
  • site:生成项目站点文档
  • post-site:执行一些在生成项目站点之后需要完成的工作
  • site-deploy:将生成的项目站点发布到服务器上

5.2.5 命令行与生命周期

我们前面说过各个生命周期是相互独立的,但一个生命周期的阶段是有前后依赖关系的

  • mvn clean:调用clean生命周期的clean阶段,实际执行clean生命周期的pre-clean和clean阶段
  • mvn test:调用default生命周期的test阶段,实际执行default生命周期的validate、initiaalize等直到test的所有阶段
  • mvn clean install:调用clean生命周期的clean阶段和default生命周期的install阶段,实际执行了clean生命周期的pre-clean、clean阶段以及default生命周期的从validate至install的所有阶段。结合了两个生命周期,非常适用于项目构建之前清理项目
  • mvn clean deploy site-deploy:实际执行clean生命周期的pre-clean 和clean ,default生命周期的所有阶段,以及site生命周期的所有阶段

5.3 插件目标与绑定

Maven核心仅仅定义了抽象的生命周期,实际的任务是交由插件完成,生命周期与插件相互绑定,用以完成实际的构建任务。一个插件有多个功能,每个功能就是一个插件目标。通用写法是插件前缀:插件目标。

5.3.1 自定义绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<!-- 推荐使用非快照版本 -->
<version>2.1.2</version>
<executions>
<!-- 配置一个任务 -->
<execution>
<!-- 任务id -->
<id>attach-sources</id>
<!-- 绑定的生命周期 -->
<phase>verify</phase>
<goals>
<!-- 配置插件目标 -->
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>

5.4 插件配置

5.4.1 命令行插件配置

Maven命令中使用-D参数,并伴随一个参数键=参数值的形式
例如:install–Dmaven.test.skip=true,跳过测试

5.4.2 POM中插件全局配置

我们可以在声明插件的时候,对此插件进行一个全局的配置让所有基于该插件的目标任务都使用这些配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<!-- 对插件进行全局设置,不管此插件绑定到什么阶段都使用同样的配置 -->
<configuration>
<!-- 编译1.7版本的源文件 -->
<source>1.7</source>
<!-- 生成与JVM 1.7 兼容的字节码文件 -->
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>

5.4.3 POM中插件任务配置

除了配置插件的全局参数,我们还可以为某个插件任务配置特定的参数,如输出语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
<executions>
<execution>
<id>ant-validate</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuraction>
<tasks>
<echo>I'm bound to validate phase.</echo>
</tasks>
</configuraction>
</execution>
</executions>
</plugin>
</plugins>
</build>

6. 聚合与继承

6.1 聚合

当我们将项目化作多个模块的时候,我们需要使用聚合,聚合模块与其它模块的目录结构通常有水平或者父子两种关系。
//TODO:聚合图
先来看一下父子目录结构的POM

1
2
3
4
5
6
7
8
9
10
11
12
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.litianming1024</groupId>
<artifactId>maven-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 聚合模块的打包方式必须为pom -->
<packaging>pom</packaging>
<modules>
<module>moduleA</module>
<module>moduleB</module>
</modules>
</project>

如果使用平行目录结构,聚合模块的POM需要做相应的修改

1
2
3
4
<modules>
<module>../moduleA</module>
<module>../moduleB</module>
</modules>

6.2 继承

前面说过所有POM实际都继承自超级POM,使用继承的好处就是减少部分重复配置。
要使用继承,首先要先定义父项目的POM

1
2
3
4
5
6
7
8
9
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.litianming1024</groupId>
<artifactId>maven-test-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 父模块的打包方式必须为pom -->
<packaging>pom</packaging>
...
</project>

子模块POM修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
<modelVersion>4.0.0</modelVersion>
<!-- parent声明父模块,groupId、artifactId、version是必须的 -->
<parent>
<groupId>io.github.litianming1024</groupId>
<artifactId>maven-test-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 默认值../pom.xml -->
<relativePath>../maven-test-parent/pom.xml</relativePath>
</parent>
<artifactId> doms-core </artifactId>
...
</project>

Maven会首先根据relativePath检查父POM,如果找不到,再从本地仓库查找。relativePath的默认值为../pom.xml
在实际的应用中一个聚合POM可能同时又是父POM,这样管理比较方便

6.2.1 可继承的POM元素

  • groupId :项目组 ID ,项目坐标的核心元素;
  • version :项目版本,项目坐标的核心元素;
  • description :项目的描述信息;
  • organization :项目的组织信息;
  • inceptionYear :项目的创始年份;
  • url :项目的 url 地址
  • develoers :项目的开发者信息;
  • contributors :项目的贡献者信息;
  • distributionManagerment :项目的部署信息;
  • issueManagement :缺陷跟踪系统信息;
  • ciManagement :项目的持续继承信息;
  • scm :项目的版本控制信息;
  • mailingListserv :项目的邮件列表信息;
  • properties :自定义的 Maven 属性;
  • dependencies :项目的依赖配置;
  • dependencyManagement :醒目的依赖管理配置;
  • repositories :项目的仓库配置;
  • build :包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等;
  • reporting :包括项目的报告输出目录配置、报告插件配置等。

6.2.2 依赖管理

由于dependencies也可以被继承,但有时子模块不会用到父模块的所有依赖,这时我们可以使用dependencyManagement元素来管理依赖配置。该元素的依赖声明不会引入实际的依赖,但是能够约束dependencies下的依赖使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<properties>
<!-- 将spring的版本约束在2.5.6 -->
<springframework.version>2.5.6</springframework.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

import:
如果想要导入合并某个POM中的dependencyManagement配置,我们需要用到import的范围依赖,如

1
2
3
4
5
6
7
8
9
10
11
12
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.litianming1024</groupId>
<artifactId>other</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- type必须为pom -->
<type>pom</span></type>
<scope>import</span></scope>
</dependency>
</dependencies>
</dependencyManagement>

6.2.3 插件管理

Maven也提供了pluginManagement元素帮助管理插件,该元素不会造成实际的插件调用行为,只是当POM中配置了真正的plugin元素,并且其groupId和artifactId与pluginManagement中配置的插件匹配时才会影响行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

7. 使用Maven进行测试

7.1 跳过测试

7.1.1 仅跳过单元测试

命令:mvn package -DskipTests
POM文件:

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>

7.1.2 跳过单元测试编译和测试

命令:mvn package -Dmaven.test.skip=true
POM文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<plugin>
<groupId>org.apache.maven.plugin</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>

8. 灵活构建

8.1 Maven属性

Maven有6类属性:

  • 内置属性:主要两个内置属性——\${basedir}表示项目根目录,即包含pom.xml文件的目录;\${version}标识项目版本。
  • POM属性:可以使用该类属性引用POM文件中对应元素的值,例如${project.artifactId}对应了\\元素的值
    常见的POM属性包括:
      ${project.build.sourceDirectory}:项目的主源码目录,默认为src/main/java/.
      ${project.build.testSourceDirectory}:项目的测试源码目录,默认为/src/test/java/.
      ${project.build.directory}:项目构建输出目录,默认为target/.
      ${project.build.outputDirectory}:项目主代码编译输出目录,默认为target/classes/.
      ${project.build.testOutputDirectory}:项目测试代码编译输出目录,默认为target/testclasses/.
      ${project.groupId}:项目的groupId.
      ${project.artifactId}:项目的artifactId.
      ${project.version}:项目的version,等同于${version}
      ${project.build.finalName}:项目打包输出文件的名称,默认为${project.artifactId}${project.version}.
      
  • 自定义属性:在POM的\元素下定义自定义Maven属性。如

    1
    2
    3
    4
    <properties>
    <!-- swagger.version -->
    <swagger.version>2.2.2</swagger.version>
    </properties>
  • Settings属性:跟pom属性同理,使用setting开头,引用setting.xml文件中xml元素的值

  • Java系统属性:所有的java系统属性都可以使用Maven属性引用,如\&{user.home}指向了用户目录
  • 环境变量属性:所有环境变量都可以使用以env.开头的Maven属性引用。如:\${env.JAVAHOME}指代了JAVA_HOME环境变量的值。可以使用mvn help:system来查看所有的环境变量。

8.2 构建环境的差异

在不同的环境中,项目的源码应该使用不同的方式进行构建。最常见的就是数据库配置。

8.3 资源过滤

为了应对环境的变化,我们需要将会发生变化的部分提取出来。一般会在src/main/resources目录下添加数据库的配置文件.properties文件。

1
2
3
4
database.jdbc.driverClass=${db.driver}
database.jdbc.connectionURL=${db.url}
database.jdbc.username=${db.username}
database.jdbc.password=${db.password}

我们使用profile在POM中包起来

1
2
3
4
5
6
7
8
9
10
11
<profiles>
<profile>
<id>dev</id>
<properties>
<db.driver>com.mysql.jdbc.Driver</db.driver>
<db.url>jdbc:mysql://192.168.1.100:3306/test</db.url>
<db.username>dev</db.username>
<db.password>dew-pwd</db.password>
</properties>
</profile>
</profiles>

但是这仅仅不够,Maven属性默认只有在POM中才会被解析,上面的properties属性仍然没有被填充,为了解决这个问题我们需要定制maven-resources-plugin开启资源过滤
为主资源目录开启过滤:

1
2
3
4
5
6
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resouces>

同样为测试资源开启过滤:

1
2
3
4
5
6
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResouces>

主资源目录和测试资源目录都可以超过一个
属性激活:mvn clean install -Pdev -P参数表示激活的profile

8.4 Maven Profile

在不同环境下的构建很可能是不同的,典型的情况就是数据库的配置。除此之外,有些环境可能需要配置插件执行一些特殊的操作,或者使用特殊版本的依赖,或者需要一个特殊的构件名称。为了能让构建在各个环境下方便地移植,Maven引入了profile的概念。profile能够在构建的时候修改POM的一个子集,或者添加额外的配置元素。用户可以使用很多方式激活profile,以实现构建在不同环境下的移植。

8.4.1 激活profile

  1. 命令激活
    用户可以使用mvn命令行参数-P加上profile的id来激活profile,多个id之间以逗号分隔。例如,下面的命令激活了dev-x和dev-y两个profile:
    mvn clean install -Pdev-x, dev-y
  2. settings文件显式激活
    如果用户希望某个profile默认一直处于激活状态,就可以配置settings.xml文件的activeProfiles元素,表示其配置的profile对于所有项目都处于激活状态,代码如下:

    1
    2
    3
    4
    5
    6
    <settings>
    ...
    <activeProfiles>
    <activeProfile>dev-x</activeProfile>
    </activeProfiles>
    </settings>
  3. 系统属性激活
    用户可以配置当某系统属性存在或者为特定值的时候自动激活profile,如test为x

    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
    <profiles>
    <profile>
    <activation>
    <property>
    <name>test</name>
    <value>x</value>
    </property>
    </activation>
    </profile>
    </profiles>
    ```
    不要忘了用户可以在命令行声明系统属性
    `mvn clean install -Dtest=x`
    4. 操作系统环境激活
    5. 文件存在与否激活
    6. 默认激活
    定义profile的时候指定其默认激活
    ```xml
    <profiles>
    <profile>
    <id>dev</id>
    <activation>
    <activeByDefault>true</activeByDefault>
    </activation>
    ...
    </profile>
    </profiles>

如果POM中有任何一个profile通过以上其他任意一种方式被激活,所有的默认激活配置都会失效
了解当前激活的profile:mvn help:active-profiles
列出当前所有的profile:mvn help:all-profiles

8.4.2 profile的种类

根据具体的需要,可以在以下位置声明profile:

  • pom.xml:pom.xml中声明的profile只对当前项目有效。
  • 用户settings.xml:用户目录下.m2/settings.xml中的profile对本机上该用户所有的Maven项目有效。
  • 全局settings.xml:Maven安装目录下conf/settings.xml中的profile对本机上所有的Maven项目有效,不建议使用。

参考资料

  • 《Maven实战》
  • Maven官方指南

MySQL自定义变量小结

发表于 2017-12-05 | 阅读次数:

前言

最近阅读《高性能MySQL》,有不少收获。用户自定义变量是一个可以用来存储内容的临时容器,在连接MySQL的整个过程中都存在。下面的话大部分是从书上搬下来的。

限制

有些场景我们不能使用自定义变量

  • 使用自定义变量的查询,无法使用查询缓存
  • 不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名、和LIMIT子句中
  • 用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信
  • 如果使用连接池或者持久话连接,自定义变量可能让看起来毫无关系的代码发生交互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生,前面是书上原话,此处存疑)
  • 在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题
  • 不能显示地声明自定义变量的类型。确定未定义变量的具体类型的时机在不同MySQL版本中也可能不一样。用户自定义变量的类型在赋值的时候会改变,MySQL的用户自定义变量是一个动态类型,最好定义时就赋初值
  • MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运行
  • 复制的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可能很让人困惑
  • 赋值符号:=的优先级非常低,所以需要注意,赋值表达式应该使用明确的括号
  • 使用未定义变量不会产生任何语法错误

适用场景

1.优化排名语句

用户自定义变量可以在给一个变量赋值的同时使用这个变量。用户自定义变量具有“左值”的特性。
来简单的实现一个rownumber的功能:

1
2
SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS rownum FROM actor;

书中还给了一个复杂的例子来演示排名的写法,Leetcode的练习题中也有类似例子178. Rank Scores

2.避免重复查询刚刚更新的数据

例如

1
2
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1;
SELECT lastUpdated FROM t1 FROM id = 1;

使用变量,可以改写为

1
2
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
SELECT @now;

改写后仍需要两个查询,需要两次网络来回,但是无需访问任何数据表

3.统计更新和插入的数量

1
INSERT INTO t1(c1, c2) VALUES(4, 4), (2,1), (3,1) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + (0 * (@x := @x + 1));

这个写法十分巧妙,每次由于冲突导致更新时对变量@x自增一次,然后再乘0来避免影响要更新的内容。同时MySQL的协议会返回被更改的总行数,所以不需要单独统计这个值。

4.确定取值的顺序

用户自定义变量的一个最常见问题是没有注意到在赋值和读取变量的时候可能是在查询的不同阶段。
例如:

1
2
3
4
SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 as cnt
FROM actor
WHERE @rownum <= 1

结果只返回了两条结果,似乎符合预期(实际读取变量和赋值不在同一阶段,但是没有影响到结果)。而

1
2
3
4
5
SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 as cnt
FROM actor
WHERE @rownum <= 1
ORDER BY first_name;

却返回了200条结果,而且有cnt超过1的条目,通过explain语句我们发现

using filesort是造成这个问题的原因,WHERE条件是在文件排序操作之前取值的(ORDER BY导致一次性选取了所有满足WHERE条件的语句,换句话说,WHERE处的@rownum只读取了一次,值为0,所有数据都符合条件,WHERE执行在SELECT之前,之后不停给@rownum赋新值,我是这样理解的)。如果first_name有索引则结果正常,使用了覆盖索引并且没有触发filesort。

5.编写偷懒的UNION

下面的查询会在两个地方查找一个用户——一个用户表,一个长时间不活跃的用户表,不活跃的用户表的目的是为了实现更高效的归档:

1
2
3
SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;

上面的查询即使在users表中已经找到了记录,也还是会去users_archived中再查找一次。我们可以用UNION查询来抑制,只有当第一个表中没有数据时,才在第二个表中查询。只要在第一个表中找到记录就定义一个变量@found通过在结果列中做一次赋值来实现,然后将赋值放在函数GREATEST中来避免返回额外的数据。为了明确结果来自哪一个表,新增了一个包含表名的列。最后需要在查询的末尾将变量重置为NULL,保证遍历时不干扰后面的结果。

1
2
3
4
5
6
SELECT GREATEST(@found := −1, id) AS id, 'users' AS which_tbl
FROM users WHERE id = 1
UNION ALL
SELECT id, 'users_archived'FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL
SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;

6.用户自定义变量的其他用处

  • 查询运行是计算总数和平均值
  • 模拟GROUP语句中的函数FITST()和LAST()
  • 对大量数据做一些数据计算
  • 计算一个大表的MD5散列值
  • 编写一个样本处理函数,当样本中的数值超过某个边界值的时候将其变成0
  • 模拟读/写游标
  • 在SHOW语句的WHERE子句中加入变量值

7.总结

先了解SQL语句的执行顺序才能读懂这部分,其他用处我还没有试验。

8.参考资料

《高性能MySQL》

我的校招之路才刚刚开始

发表于 2017-10-17 | 阅读次数:

经历了一些努力,拿到了好几家大公司的面试,已经到了TMD之流,复习的第一阶段目标已经基本达成。不论如何轮到我展现真正实力的时候到了。最近每一天都要面试,来不及写更多,对于面试的复盘我会在接下来进行记录。还要再复习。哈哈哈哈。

SQL语句执行顺序

发表于 2017-09-24 | 阅读次数:

引言

SQL语句执行顺序是面试中的基础考点,理解SQL对于优化排错有重大的作用,这里从两篇文章摘取部分重要的内容

SQL查询步骤

总结

1
2
3
4
5
6
7
8
9
(8)SELECT (9) DISTINCT (11) <TOP_specification> <select_list>
(1) FROM <left_table>
(3) <join_type> JOIN <right_table>
(2) ON <join_condition>
(4) WHERE <where_condition>
(5) GROUP BY <group_by_list>
(6) WITH {CUBE | ROLLUP}
(7) HAVING <having_condition>
(10) ORDER BY <order_by_list>

这是对语句执行顺序的基本总结

简介

  1. FROM:对FROM子句中的前两个表执行笛卡尔积(交叉联接),生成虚拟表VT1。
  2. ON:对VT1应用ON筛选器,只有那些使为真才被插入到VT2。
  3. OUTER (JOIN):如果指定了OUTER JOIN(相对于CROSS JOIN或INNER JOIN),保留表中未找到匹配的行将作为外部行添加到VT2,生成VT3。如果FROM子句包含两个以上的表,则对上一个联接生成的结果表和下一个表重复执行步骤1到步骤3,直到处理完所有的表位置。
  4. WHERE:对VT3应用WHERE筛选器,只有使为true的行才插入VT4。
  5. GROUP BY:按GROUP BY子句中的列列表对VT4中的行进行分组,生成VT5。
  6. CUTE|ROLLUP:把超组插入VT5,生成VT6。
  7. HAVING:对VT6应用HAVING筛选器,只有使为true的组插入到VT7。
  8. SELECT:处理SELECT列表,产生VT8。
  9. DISTINCT:将重复的行从VT8中删除,产品VT9。
  10. ORDER BY:将VT9中的行按ORDER BY子句中的列列表顺序,生成一个游标(VC10)。
  11. TOP:从VC10的开始处选择指定数量或比例的行,生成表VT11,并返回给调用者。

引用文章

sql语句执行顺序
SQL逻辑查询语句执行顺序

索引失效情况总结

发表于 2017-09-23 | 阅读次数:

引言

非常意外的收到了京东的面试,面试小姐姐问到了索引失效的情况,这是个很基础的问题,在这里做一下简单的总结,至于背后的原理会在后面解释。

索引失效的情况

1.查询条件包含or

如果查询条件包含or时,可能导致索引失效。
当or左右查询字段只有一个是索引,该索引失效,只有当or左右查询字段均为索引时,才会生效。

2.like查询以%开头

使用like模糊查询,当%在前时索引失效。

3.如果列类型是字符串,where时一定要用引号括起来,否则索引失效

4.如果mysql估计使用全表扫描比使用索引快,则索引失效

5.最左前缀匹配原则

这条是最为复杂的,这里直接引用美团的文章
1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

6.对索引列进行运算导致索引失效

对索引列进行运算包括(+,-,*,/,! 等),如 where id - 1 = 1;

7.索引不会包含有NULL值的列

只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

总结

以上只是可能的集中情况,根据数据库引擎和索引的类别情况可能有所不同。如果对是否使用到索引有疑问,一定要使用explain检查索引是否失效!
一定要使用explain检查索引是否失效!
一定要使用explain检查索引是否失效!
对于如何怎样建立合适的索引我会另做讨论

TCP拥塞控制-经典算法

发表于 2017-09-15 | 阅读次数:

引言

路由器因无法处理高速率到达的流量而被迫丢弃数据信息的现象称为拥塞。当路由器处于上述状态时,我们就说出现了拥塞。即使仅有一条通信连接,也可能造成一个甚至多个路由器阻塞。若不采取对策,网络性能将大受影响以致瘫痪。在最坏情况下,甚至形成拥塞崩溃。为了避免或在一定程度上缓解这种状况,TCP通信的每一方实行拥塞控制机制。不同的TCP版本采取的规程和行为有所差异。

上面是《TCP/IP详解卷1》对于拥塞控制的引言,相比《计算机网络-自顶向下》,《TCP/IP详解卷1》更为详细的讲述了不同种的算法原理。入门可以先从《计算机网络-自顶向下》读起,有个大致了解便可以,如果感兴趣再读《TCP/IP详解卷1》。
简单概括,拥塞控制包含3个方面:慢启动、拥塞避免、快速恢复。

准备

在开始之前有几个概念要了解
拥塞窗口(congestion window):记为cwnd,拥塞控制的关键参数,它描述源端在拥塞控制情况下一次最多能发送的数据包的数量。
慢启动阈值(slow start threshold):记为ssthres,拥塞控制中慢启动阶段和拥塞避免阶段的分界点。

慢启动

开始时,cwnd的值一般为1个MSS,发送端按照cwnd大小发送数据,在接收到一个数据段的ACK后,cwnd就增加一个MSS大小。cwnd的值会随着RTT呈指数增长。
例如,cwnd初始为1,接收到一个数据段的ACK后,cwnd增长到2,接着会发送两个数据段。如果成功接收到相应的新的ACK,cwnd会由2增长为4,以此类推。
结束:慢启动的结束有3种方式。

  1. 当cwnd的增长到达ssthresh,就进入拥塞避免模式。
  2. 如果收到了了一个丢包提示,就将ssthresh的值设置为cwnd的一半,并将cwnd设置为1,重新进入慢启动过程。
  3. 当收到3个重复的ACK,就执行一次快速重传并且进入快速恢复状态。

拥塞避免

进入拥塞避免说明cwnd的值大约是上次遇到拥塞时的一半,这时不能盲目翻倍,而是每经过1个RTT使cwnd增加1个MSS。也就是说当收到一个ACK时,cwnd = cwnd + 1/cwnd,当每过一个RTT时,cwnd = cwnd + 1。
结束:拥塞避免的结束有两种可能。

  1. 当出现超时时,将ssthresh设置为当前cwnd值的一半,cwnd设置为1,重新进入慢启动过程。
  2. 当收到3个重复的ACK时,把ssthresh设置为cwnd的一半,再把cwnd设置为ssthresh(某些实现为ssthresh + 3),之后进入快速恢复。

快速恢复

快速恢复就是指进入快速恢复前的一系列操作,即sssthresh设置为当前cwnd的一半,并将cwnd设置为ssthresh(某些版本为ssthresh + 3),之后进入拥塞避免状态。

参考资料

《计算机网络-自顶向下》

12

天明

11 日志
19 标签
GitHub
© 2017 — 2018 天明
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.2