0%

基于Docker搭建的Centos 7平台
最新版的firefox 59, 新版的Chrome已经不再支持Linux, Opera半死不活

Java + Selenium + GeckoDriver + firefox 版本太难兼容
本文的版本供参考(验证可用)

  • Java 1.8.0 update 91
  • Selenium-Java 3.9.1
  • GeckoDriver 0.19.1
  • Firefox 59.0b8

安装 Java 8

1
yum -y install java-1.8.0-openjdk.x86_64

Selenium-Java

1
2
3
4
5
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.9.1</version>
</dependency>

GeckoDriver

Gecko是Firefox的内核,顾名思义,GeckoDriver就是驱动
启动后监听6411端口, Selenium通过IPC调用,访问GeckoDriver

1
2
3
wget https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz
tar -zxvf geckodriver-v0.19.1-linux64.tar.gz
mv geckodriver /usr/local/bin/

Firefox

1
2
3
4
https://ftp.mozilla.org/pub/firefox/releases/59.0b8/linux-x86_64/zh-CN/firefox-59.0b8.tar.bz2
tar -xvf firefox-59.0b8.tar.bz2
mv firefox /usr/local/
ln /usr/local/firefox/firefox /usr/bin/firefox

安装虚拟桌面

1
2
#安装Xvfb及其他依赖
yum install xorg-x11-server-Xvfb bzip gtk3

安装字体,支持中文

1
2
yum groupinstall "Fonts"
# 安装后 重启

上代码

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
System.setProperty("webdriver.gecko.driver", "/usr/local/bin/geckodriver");

FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setHeadless(true);
firefoxOptions.setAcceptInsecureCerts(true);
firefoxOptions.addArguments("--disable-gpu", "--window-size=1920,1200", "--ignore-certificate-errors");
WebDriver driver = new FirefoxDriver(firefoxOptions);
System.out.println("init chromeDriver");
driver.get("https://www.coinone.com");
System.out.println("open url");

try {
Thread.sleep(6000);
} catch (InterruptedException e) {
}
System.out.println("sleep over");

WebElement closeBtn = null;

try {
closeBtn = driver.findElement(By.id("close_btn"));
closeBtn.click();
} catch (NoSuchElementException e) {
}


List<WebElement> elements = driver.findElements(By.cssSelector(".intro_chart_price table td"));
for (int i = 0; i < elements.size(); i += 2) {
String value = elements.get(i + 1).getText().trim();
System.out.println("empty value , try " + tryCount);
System.out.println(elements.get(i).getText().trim() + "\t" + value);
}
driver.close();

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1518339710700	geckodriver	INFO	geckodriver 0.19.1
1518339710706 geckodriver INFO Listening on 127.0.0.1:6411
1518339711356 mozrunner::runner INFO Running command: "/usr/local/firefox/firefox" "-marionette" "-headless" "--disable-gpu" "--window-size=1920,1200" "--ignore-certificate-errors" "-profile" "/tmp/rust_mozprofile.1ZVD24PplvDp"
*** You are running in headless mode.
1518339712100 Marionette INFO Enabled via --marionette
1518339715188 Marionette INFO Listening on port 36443
1518339715212 Marionette WARN TLS certificate errors will be ignored for this session
Feb 11, 2018 9:01:55 AM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: W3C
init chromeDriver
open url
sleep over

BTC 8,918,000
BCH 1,197,500
ETH 814,200
ETC 22,720
XRP 1,056
QTUM 29,710
LTC 165,050
IOTA 1,910
BTG 370,350

[参考文献]

太多

[TOC]

实现原理

线程池的组成

1.constructs

提交Job的处理流程:

2.new_task

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
// 如果线程数小于核心工作线程数, 则创建线程并执行当前任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
// 如线程数大于等于核心工作线程数或线程创建失败, 则将当前任务放到任务队列中.
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command);
}
// 如果线程池不处于运行中或任务无法放入队列, 并且当前线程数量小于最大工作线程数,
// 则创建一个线程执行任务.
else if (!addIfUnderMaximumPoolSize(command))
// 抛出RejectedExecutionException异常
reject(command); // is shutdown or saturated
}
}
  1. 工作线程worker数量低于核心工作线程数corePoolSize时会优先创建一个工作线程worker处理job, 处理成功则返回.
  2. 工作线程worker数量高于核心工作线程数时会优先把job放入到任务队列, 放入队列成功时处理结束.
  3. 入队失败会识别工作线程数是否还小于最大工作线程数maximumPoolsize, 小于的话也会新创建一个工作线程worker处理job.
  4. 饱和策略

此外, 运行过程中, 更新核心工作线程数时, 若发现扩容, 会增加工作线程数.

备注:

  1. Java中的线程与操作系统的线程是一一对应的
  2. 添加新线程需要获得全局锁
    private final ReentrantLock mainLock = new ReentrantLock();, 因此, 当工作线程work数量大于核心工作线程数corePoolSize时, 优先放入任务队列
  3. 只要工作线程达不到corePoolSize, 不管是否线程空闲, 都会创建新线程
  4. 调用prestartAllCoreThreads会初始化所有的核心线程, 没有预热期, 响应快, 但空载浪费资源
  5. 任务队列添加任务, 不需要获取全局锁, 效率高
  6. 工作线程的复用: 执行完一个任务, 不断从任务队列取任务, 避免因创建和销毁操作系统线程带来的性能消耗
  7. 获取全局锁, 是性能性能瓶颈, corePoolSize是预热

工作线程的销毁

满足下面条件会销毁:

  1. 任务队列里没有job并且工作线程worker数量超过了核心工作线程数corePoolSize.
  2. 任务队列里没有job并且允许工作线程数量小于核心工作线程参数为true, 此场景会至少保留一个工作线程线程.

工作线程空闲后, 最长等待keepAliveTime

ThreadPoolExecutor

1
2
3
4
5
6
7
8
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

用给定的初始参数和默认的线程工厂及饱和策略创建新的 ThreadPoolExecutor.
使用 Executors 工厂方法之一比使用此通用构造方法方便得多, 但是阿里巴巴Java规约不推荐使用, 见下文

  • 参数:
    • corePoolSize - 池中所保存的线程数, 包括空闲线程.
    • maximumPoolSize - 池中允许的最大线程数.
    • keepAliveTime - 当线程数大于核心时, 此为终止前多余的空闲线程等待新任务的最长时间.
    • unit - keepAliveTime 参数的时间单位.
    • workQueue - 执行前用于保持任务的队列. 此队列仅保持由 execute 方法提交的 Runnable 任务.
  • 抛出:
    • IllegalArgumentException - 如果 corePoolSizekeepAliveTime 小于零, 或者 maximumPoolSize 小于或等于零, 或者 corePoolSize 大于 maximumPoolSize.
    • NullPointerException - 如果 workQueuenull

工作线程的存储

使用了HashSet来存储工作线程worker, 通过可重入锁ReentrantLock对其进行并发保护. 每个worker都是一个Runnable接口.

1
2
3
4
5
/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();

任务队列 runnableTaskQueue

线程池中的队列采用的是BlockingQueue

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列, 此队列按 FIFO(先进先出)原则对元素进行排序.
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列, 此队列按FIFO排序元素, 吞吐量通常要高于ArrayBlockingQueue. 静态工厂方法Executors.newFixedThreadPool()使用了这个队列.
  • SynchronousQueue:一个不存储元素的阻塞队列. 每个插入操作必须等到另一个线程调用移除操作, 否则插入操作一直处于阻塞状态, 吞吐量通常要高于Linked-BlockingQueue, 静态工厂方法Executors.newCachedThreadPool使用了这个队列.
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列.

线程工厂 ThreadFactory

设置创建线程的工厂, 通过线程工厂给每个创建出来的线程设置更有意义的名字. 使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字, 代码如下.

1
newThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

RejectedExecutionHandler(饱和策略)

当队列和线程池都满了, 说明线程池处于饱和状态, 那么必须采取一种策略处理提交的新任务. 这个策略默认情况下是 AbortPolicy , 表示无法 处理新任务时抛出异常. 在JDK 1.5中Java线程池框架提供了以下4种策略.

  • AbortPolicy :直接抛出异常.

  • CallerRunsPolicy :只用调用者所在线程来运行任务.

  • DiscardOldestPolicy :丢弃队列里最近的一个任务, 并执行当前任务.

  • DiscardPolicy :不处理, 丢弃掉.

当然, 也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略. 如记录日志或持久化存储不能处理的任务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 线程池异常处理类:
* 任务执行失败, 持久化到数据库
* @author averyzhang
*/
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {

@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
System.out.println("Begin exception handler-----------");
//执行失败任务
CachingOnFirstOpenRunnable coor = (CachingOnFirstOpenJob)task;
List<CachingEntity> list = coor.getLastData();
CachingDaoUtils.save(list);
//打印线程池的对象
System.out.println("The pool RejectedExecutionHandler = "+executor.toString());
}
}

提交任务: submit与execute

execute 没有返回值

submit 可以通过 Future 接口, 获取任务执行的结果: 线程池会返回一个future类型的对象, 通过这个 future对象可以判断任务是否执行成功, 并且可以通过future的get()方法来获取返回值, get()方法会阻塞当前线程直到任务完成, 而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回, 这时候有可能任务没有执行完.

关闭: shutdown与shutdownNow

原理是遍历线程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程, 所以无法响应中断的任务可能永远无法终止.

  • shutdownNow首先将线程池的状态设置成 STOP, 然后尝试停止所有的正在执行或暂停任务的线程, 并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程

获取线程的关闭状态:

调用两个关闭方法, isShutdown方法就会返回true. 当所有的任务都已关闭后, 才表示线程池关闭成功, 这时调用isTerminaed方法会返回true.

至于应该调用哪一种方法来关闭线程池, 应该由提交到线程池的任务特性决定, 通常调用shutdown方法来关闭线程池, 如果任务不一定要执行完, 则可以调用shutdownNow方法.

优化配置

性质不同的任务可以用不同规模的线程池分开处理:

  • CPU密集型任务应配置尽可能小的线程, 如配置Ncpu+1个线程的线程池
  • IO密集型任务线程并不是一直在执行任务, 则应配置尽可能多的线程, 如2*Ncpu
  • 混合型的任务, 如果可以拆分, 将其拆分成一个CPU密集型任务和一个IO密集型任务, 只要这两个任务执行的时间相差不是太大, 那么分解后执行的吞吐量将高于串行执行的吞吐量. 如果这两个任务执行时间相差太大, 则没必要进行分解.
  • 可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
  • 如果任务很多, 并且每个任务执行的时间比较短, 可以调大keepAliveTime时间, 提高线程的利用率.
  • 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理. 它可以让优先级高的任务先执行. 优先级低的任务可能永远不能执行.
  • 建议使用有界队列. 有界队列能增加系统的稳定性和预警能力, 可以根据需要设大一点:如果采用无解队列, 当任务无法处理引起堆积, 系统撑爆, 殃及其他业务

通常基于几个维度进行:待处理工作job数、线程池定义的最大最小工作线程数、工作线程闲置时间.

监控

通过线程池提供的参数进行监控, 在监控线程池的时候可以使用以下属性.

  • taskCount:线程池需要执行的任务数量.

  • completedTaskCount:线程池在运行过程中已完成的任务数量, 小于或等于taskCount.

  • largestPoolSize:线程池里曾经创建过的最大线程数量. 通过这个数据可以知道线程池是否曾经满过. 如该数值等于线程池的最大大小, 则表示线程池曾经满过.

  • getPoolSize:线程池的线程数量. 如果线程池不销毁的话, 线程池里的线程不会自动销 毁, 所以这个大小只增不减.

  • getActiveCount:获取活动的线程数. 通过扩展线程池进行监控.

  • 通过继承线程池来自定义线程池, 重写线程池的 beforeExecuteafterExecuteterminated方法, 可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控. 例如, 监控任务的平均执行时间、最大执行时间和最小执行时间等. 这几个方法在线程池里是空方法.

1
2
3
protected void beforeExecute(Thread t,Runnable r) {

}

sample

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
public class MyScheduledExecutorService extends ScheduledThreadPoolExecutor {
private static final Logger logger = LoggerFactory.getLogger(MyScheduledExecutorService.class);

public TableauScheduledExecutorService(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, threadFactory, handler);
}

public TableauScheduledExecutorService(int corePoolSize, ThreadFactory threadFactory) {
super(corePoolSize, threadFactory);
}

@Override
protected void beforeExecute(Thread t, Runnable r) {
logger.info("############# before execute \n" + currentStatus());
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
logger.info("############# after execute \n" + currentStatus());
}

@Override
protected void terminated() {
logger.info("############# terminated \n" + currentStatus());
}

private String currentStatus() {
// 需要执行的任务数目
final long taskCount = getTaskCount();
// 执行完成的任务数目
final long completedTaskCount = getCompletedTaskCount();
// 线程池最大线程数量
final int largestPoolSize = getLargestPoolSize();
// 线程池线程数量
final int poolSize = getPoolSize();
// 活动线程数量
final int activeCount = getActiveCount();

return "[要执行任务:" + taskCount + ",完成:" + completedTaskCount + ",线程池最大数量:" + largestPoolSize +
",当前线程数量:" + poolSize + ",活动线程数量:" + activeCount + "]";
}
}

TODO 等待关闭

Alibaba Java 规约

线程池不允许使用Executors去创建, 而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学更加明确线程池的运行规则, 规避资源耗尽的风险. 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存, 甚至OOM.
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE, 可能会创建数量非常多的线程, 甚至OOM.

Positive example 1:

1
2
//org.apache.commons.lang3.concurrent.BasicThreadFactory
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

Positive example 2:

1
2
3
4
5
6
7
8
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();

//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

pool.execute(()-> System.out.println(Thread.currentThread().getName()));
pool.shutdown();//gracefully shutdown

Positive example 3:

1
2
3
4
5
6
7
8
9
10
11
<bean id="userThreadPool"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="queueCapacity" value="2000" />

<property name="threadFactory" value= threadFactory />
<property name="rejectedExecutionHandler">
<ref local="rejectedExecutionHandler" />
</property>
</bean>
1
2
//in code
userThreadPool.execute(thread);

线程池的关闭

等待关闭超时

next…TODO

  1. Future接口
  2. 任务队列的实现
  3. ArrayBlockingQueue
  4. LinkedBlockingQueue
  5. SynchronousQueue
  6. PriorityBlockingQueue
  7. 线程池的底层实现
  8. guava与线程工厂的使用
  9. 线程关闭与中断: 强制关闭与响应中断
  10. Java获取设备参数的API
  11. 连接池

  1. Calendar
    获取小时数 [2017-10-17 16:10:36 星期二]
1
2
3
4
5
Calendar calendar = Calendar.getInstance();
// 获取12小时制的小时数
int hour12 = calendar.get(Calendar.HOUR);
// 获取24小时制的小时数
int hour24 = calendar.get(Calendar.HOUR_OF_DAY);
  1. 读锁(共享锁)与写锁(独占锁)

[2017-10-17 16:10:41 星期二]

虽然读锁可以允许多个线程读取,写锁会独占,但是当一个线程对资源加了读锁后,另一个线程要加独占锁也必须等待读锁释放。

  1. Java web 获取 classes 目录
1
String path = Thread.currentThread().getContextClassLoader().getResource("/").toURI().getPath();

Jpa

创建时间 更新时间

1.实体类加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 创建时间
*/
@CreatedDate
@Column(name = "create_time")
private Date createTime;

/**
* 修改时间
*/
@LastModifiedDate
@Column(name = "modify_time")
private Date modifyTime;

2.实体类头加注解

1
2
@EntityListeners(AuditingEntityListener.class)

3.SpringBoot启动类加注解

1
2
@EnableJpaAuditing

另外数据库添加相应控制也可以:
createTime : CURRENT_TIMESTAMP
modifyTime : CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

1. BlockingQueue 简介

在实际编程中,会经常使用到 JDK 中 Collection 集合框架中的各种容器类如实现 List,Map,Queue 接口的容器类,但是这些容器类基本上不是线程安全的,除了使用 Collections 可以将其转换为线程安全的容器,Doug Lea 大师为我们都准备了对应的线程安全的容器,如实现 List 接口的 CopyOnWriteArrayList(关于 CopyOnWriteArrayList 可以看这篇文章),实现 Map 接口的 ConcurrentHashMap(关于 ConcurrentHashMap 可以看这篇文章),实现 Queue 接口的 ConcurrentLinkedQueue(关于 ConcurrentLinkedQueue 可以看这篇文章)。

最常用的”生产者-消费者“问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。

2. 基本操作

BlockingQueue 基本操作总结如下(此图来源于 JAVA API 文档):

Throws exception Special value Blocks Times out
Insert add(e) offser(e) put(e) offser(e,time,unit)
Remove remove() poll() take() poll(time,unit)
Examine element() peek() - -

BlockingQueue 继承于 Queue 接口,因此,对数据元素的基本操作有:

插入元素

  1. add(E e) :往队列插入数据,当队列满时,插入元素时会抛出 IllegalStateException 异常;
  2. offer(E e):当往队列插入数据时,插入成功返回true,否则则返回false。当队列满时不会抛出异常;

删除元素

  1. remove(Object o):从队列中删除数据,成功则返回true,否则为false
  2. poll():删除数据,当队列为空时,返回 null;

查看元素

  1. element():获取队头元素,如果队列为空时则抛出 NoSuchElementException 异常;
  2. peek():获取队头元素,如果队列为空则抛出 NoSuchElementException 异常

BlockingQueue 具有的特殊操作:

插入数据:

  1. put(E e):当阻塞队列容量已经满时,往阻塞队列插入数据的线程会被阻塞,直至阻塞队列已经有空余的容量可供使用;
  2. offer(E e, long timeout, TimeUnit unit):若阻塞队列已经满时,同样会阻塞插入数据的线程,直至阻塞队列已经有空余的地方,与 put 方法不同的是,该方法会有一个超时时间,若超过当前给定的超时时间,插入数据的线程会退出;

删除数据

  1. take():当阻塞队列为空时,获取队头数据的线程会被阻塞;
  2. poll(long timeout, TimeUnit unit):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出

3. 常用的 BlockingQueue

实现 BlockingQueue 接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明:

1.ArrayBlockingQueue

**ArrayBlockingQueue**是由数组实现的有界阻塞队列。该队列命令元素 FIFO(先进先出)。因此,对头元素时队列中存在时间最长的数据元素,而对尾数据则是当前队列最新的数据元素。ArrayBlockingQueue 可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue 一旦创建,容量不能改变。

当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,一旦 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:

1
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);

关于 ArrayBlockingQueue 的实现原理,可以看这篇文章

2.LinkedBlockingQueue

LinkedBlockingQueue 是用链表实现的有界阻塞队列,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE

3.PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。

4.SynchronousQueue

SynchronousQueue 每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue 实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue 也可以通过构造器参数来为其指定公平性。

5.LinkedTransferQueue

LinkedTransferQueue 是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了 TransferQueue 接口,与其他阻塞队列相比主要有以下不同的方法:

transfer(E e) 如果当前有线程(消费者)正在调用 take()方法或者可延时的 poll()方法进行消费数据时,生产者线程可以调用 transfer 方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出;

tryTransfer(E e) tryTransfer 方法如果当前有消费者线程(调用 take 方法或者具有超时特性的 poll 方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回false。因此,与 transfer 方法相比,transfer 方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而 tryTransfer 方法能够立即返回结果退出。

tryTransfer(E e,long timeout,imeUnit unit)
transfer 基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回false

6.LinkedBlockingDeque

LinkedBlockingDeque 是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为 Integer.MAX_VALUE。与 LinkedBlockingQueue 相比,主要的不同点在于,LinkedBlockingDeque 具有双端队列的特性。LinkedBlockingDeque 基本操作如下图所示(来源于 java 文档)

`LinkedBlockingDeque`的基本操作.png

LinkedBlockingDeque的基本操作.png

如上图所示,LinkedBlockingDeque 的基本操作可以分为四种类型:

1.特殊情况,抛出异常;
2.特殊情况,返回特殊值如 null 或者 false;
3.当线程不满足操作条件时,线程会被阻塞直至条件满足;
4. 操作具有超时特性。

另外,LinkedBlockingDeque 实现了 BlockingDueue 接口
LinkedBlockingQueue 实现的是 BlockingQueue,

这两个接口的主要区别如下图所示(来源于 java 文档):

BlockingQueue和BlockingDeque的区别.png

BlockingQueue和BlockingDeque的区别

从上图可以看出,两个接口的功能是可以等价使用的,比如 BlockingQueue 的 add 方法和 BlockingDeque 的 addLast 方法的功能是一样的。

7.DelayQueue

DelayQueue 是一个存放实现 Delayed 接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过 poll 等方法获取数据元素则返回 null。所谓数据延时期满时,则是通过 Delayed 接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于 0 则说明该数据元素的延时期已满。

ArrayBlockingQueue 实现原理

阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程)。那么,多线程中消息通知机制最常用的是 lock 的 condition 机制,关于 condition 可以看这篇文章的详细介绍。那么 ArrayBlockingQueue 的实现是不是也会采用 Condition 的通知机制呢?下面来看看。

2.1 ArrayBlockingQueue 的主要属性

ArrayBlockingQueue 的主要属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*

Concurrency control uses the classic two-condition algorithm
found in any textbook.
*/

/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;

从源码中可以看出 ArrayBlockingQueue 内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是 Condition,当获取数据的消费者线程被阻塞时会将该线程放置到 notEmpty 等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到 notFull 等待队列中。而 notEmpty 和 notFull 等中要属性在构造方法中进行创建:

1
2
3
4
5
6
7
8
9
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

接下来,主要看看可阻塞式的 put 和 take 方法是怎样实现的。

2.2 put 方法详解

put(E e)方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果当前队列已满,将线程移入到notFull等待队列中
while (count == items.length)
notFull.await();
//满足插入数据的要求,直接进行入队操作
enqueue(e);
} finally {
lock.unlock();
}
}
复制代码

该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到 notFull 等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素。enqueue 方法源码为:

1
2
3
4
5
6
7
8
9
10
11
12
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//插入数据
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//通知消费者线程,当前队列中有数据可供消费
notEmpty.signal();
}

enqueue 方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。

2.3 take 方法详解

take 方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列为空,没有数据,将消费者线程移入等待队列中
while (count == 0)
notEmpty.await();
//获取数据
return dequeue();
} finally {
lock.unlock();
}
}

take 方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作dequeue。dequeue 方法源码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取数据
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//通知被阻塞的生产者线程
notFull.signal();
return x;
}

dequeue 方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);2. 通知 notFull 等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得 lock,并执行完成功退出。

从以上分析,可以看出 put 和 take 方法主要是通过 condition 的通知机制来完成可阻塞式的插入数据和获取数据。在理解 ArrayBlockingQueue 后再去理解 LinkedBlockingQueue 就很容易了。

3. LinkedBlockingQueue 实现原理

LinkedBlockingQueue 是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE。从它的构造方法可以看出:

1
2
3
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}

3.1 LinkedBlockingQueue 的主要属性

LinkedBlockingQueue 的主要属性有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**

Head of linked list.
Invariant: head.item == null
*/
transient Node<E> head;

/**

Tail of linked list.
Invariant: last.next == null
*/
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
复制代码

可以看出与 ArrayBlockingQueue 主要的区别是,LinkedBlockingQueue 在插入数据和删除数据时分别是由两个不同的 lock(takeLockputLock)来控制线程安全的,因此,也由这两个 lock 生成了两个对应的 condition(notEmptynotFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node 结点的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node&lt;E&gt; next;

Node(E x) { item = x; }

接下来,我们也同样来看看 put 方法和 take 方法的实现。

3.2 put 方法详解

put 方法源码为:

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
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//如果队列已满,则阻塞当前线程,将其移入等待队列
while (count.get() == capacity) {
notFull.await();
}
//入队操作,插入数据
enqueue(node);
c = count.getAndIncrement();
//若队列满足插入数据的条件,则通知被阻塞的生产者线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}

put 方法的逻辑也同样很容易理解,可见注释。基本上和 ArrayBlockingQueue 的 put 方法一样。take 方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件
while (count.get() == 0) {
notEmpty.await();
}
//移除队头元素,获取数据
x = dequeue();
c = count.getAndDecrement();
//如果当前满足移除元素的条件,则通知被阻塞的消费者线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}

take 方法的主要逻辑请见于注释,也很容易理解。

4. ArrayBlockingQueue 与 LinkedBlockingQueue 的比较

相同点:ArrayBlockingQueue 和 LinkedBlockingQueue 都是通过 condition 通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;

不同点:1. ArrayBlockingQueue 底层是采用的数组进行实现,而 LinkedBlockingQueue 则是采用链表数据结构;

  1. ArrayBlockingQueue 插入和删除数据,只采用了一个 lock,而 LinkedBlockingQueue 则是在插入和删除分别采用了putLocktakeLock,这样可以降低线程由于线程无法获取到 lock 而进入 WAITING 状态的可能性,从而提高了线程并发执行的效率。

Java Microbenchmark Harness

JMH 是 Java Microbenchmark Harness(微基准测试)

性能调优必备利器之 JMH - 武培轩 - 博客园 (cnblogs.com)

https://github.com/openjdk/jmh.git

测试项目构建

官方archetype

1
2
3
4
5
6
7
8
mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=com.tencent.demo.jmh \
-DartifactId=jmh-log \
-Dversion=1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyBenchmark {

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1)
@Threads(value = 1)
public void testMethod() {
// This is a demo/sample template for building your JMH benchmarks. Edit as
// needed.
// Put your benchmark code here.
LogBenchMarkWorker.getInstance().logBatch();
}
}

使用

1
mvn clean verify

运行

1
java -jar target/benchmark.jar

JMH运行原理

大牛推荐的文献:

  1. https://shipilev.net
  2. https://shipilev.net/jvm/anatomy-quarks/

事务简介

事务的核心是锁和并发, 采用同步控制的方式保证并发的情况下性能尽可能高, 且容易理解. 这种方式的优势是方便理解; 它的劣势是性能比较低.

计算机可以简单的理解为一个标准的打字机, 尽管看起来计算机可以并行处理很多事情, 但实际上每个CPU单位时间内只能做一件事, 要么读取数据、要么计算数据、要么写入数据, 所有的任务都可以看成这三件事的集合. 计算机的这种特性引出了一个问题:当多个人去读、算、写操作时, 如果不加访问控制, 系统势必会产生冲突. 而事务相当于在读、算、写操作之外增加了同步的模块, 进而保证只有一个线程进入事务当中, 而其他线程不会进入.

单个事务单元

事务的四大特性分别是:原子型、一致性、隔离性和持久性.
原子性指的是事务中包含的所有操作要么全做, 要么全不做;
一致性是指在事务开始以前, 数据库处于一致性的状态, 事务结束后, 数据库也必须处于一致性的状态;
隔离性要求系统必须保证事务不受其他并发执行的事务的影响;
持久性是指一个事务一旦成功完成, 它对数据库的改变必须是永久的, 即使是在系统遇到故障的情况下也不会丢失, 数据的重要性决定了事务的持久性的重要.

事务单元是通过Begin-Traction, 然后Commit(Begin-TractionCommitRollback之间所有针对数据的写入、读取的操作都应该添加同步访问), BeginCommit之间就是一个同步的事务单元. 例如, Bob给Smith 100块钱就是一个事务单元, 这个过程中有很多步操作, 具体如上图所示; 但对业务来说, 仅是一个转账的操作.

事务之间的关系

事务单元之间的happens-before关系: 《事务管理》

  • 读写
  • 写读
  • 写写
  • 读读

amdahl定律: 最快并行, 最慢串行

最快的速度并保证逻辑顺序

目标–提高系统的易用性而不损失系统的性能

数据库使用多线程的原因

事务问题的来源

慢速设备:硬盘和网路 I/O PS过低,吞吐量很高

快速设备:内存

一组事务单元

当三个账户都在进行转账操作时, 每个操作都涉及Smith账户, 所有的事务都会排队, 各自形成一组事务单元.

事务单元之间的Happen-Before关系中的四种可能性:读写写读读读写写.
所有事务之间的关系都可以抽象成这四种之一, 来对应现在所有的业务逻辑处理. 在此基础之上, 需要用最快的速度处理多个事务单元之间的关系, 同时还能保障这四种操作的逻辑顺序.

单个事务单元的其他例子

除了转账操作是事务单元外, 诸如商品要建立一个基于GMT_Modified的索引、从数据库中读取一行记录、向数据库中写入一行记录, 同时更新这行记录的所有索引、删除整张表等都是一个事务单元.

也是一个事务单元:

  1. 添加索引
  2. 从数据库读一条数据
  3. 向数据库写一条记录, 并更新索引
  4. 删除整张表

事务单元的实现方式

Two Phase Lock(2PL)是数据库中非常重要的一个概念.
数据库操作InsertUpdateDelete都是先读再写的操作, 例如Insert操作是先读取数据, 读取之后判读数据是否存在, 如果不存在, 则写入该数据, 如果数据存在, 则返回错误.
假设在该场景下没有读操作, 只是单纯写入数据, 则数据本身并没有事务操作, DeleteUpdate操作与之类似.
数据库利用这些操作的特性, 在每一次查询过程中, 只要查到数据, 就会在该数据上加锁.理论上, 所有被读取的数据都已加锁, 不会再被其他人读到, 也就是说对数据进行的中间操作状态对所有人都不可见, 当所有中间状态完成后, 提交操作时, 解开锁, 此时数据对所有系统可见 , 例如在转账过程中, 所有人只能看到两种状态:开始时, A有钱, B没钱; 结束时, B有钱, A没钱, 而中间A减掉钱, B尚未加上钱的状态被锁隐藏掉了, 这个操作就是数据库中处理事务的最标准的方式. 如上图所示:事务中的Trx2(JoeLock)与其他事务不相关, 因此可以并行执行; Trx1需要Lock两个数据Boblock和Smithlock, 而Trx3同样需要Lock这两个数据, 因此Trx3必须等待, 且等待在Boblock上; Joe事务会先结束, Trx3会等到Trx1完成后才会开始.

两阶段提交协议(基础是2pl): 事务单元内, 从读数据开始, 将所读的行锁住, 直到事务提交才释放.

处理事务的常见方法

处理事务的常见方法有排队法排他锁读写锁MVCC等方式, 下面来一一解析.

排队法

事务处理中最重要也是最简单的方案是排队法, 单线程地处理一堆数据. 在Redis中, 如果数据全部在内存中, 则单线程处理所有PutGet操作效率最高.
这是因为多线程本质是CPU模拟多个线程, 这种模拟是以上下文切换为代价, 而对于内存的数据库来说, 没有上下文切换时效率最高. 因此, 单个CPU绑定一块内存的数据, 针对这块数据做多次读写操作时都是在单个CPU上完成的, 单线程处理方式在内存的情况是效率是最优的.

那么什么时候事务需要用到多线程呢?这个问题的本质取决于下层所使用的存储, 如果是内存操作, 则可以动态地申请和销毁内存块; 而磁盘的IOPS很低, 但吞吐量很高. 如果一个场景涉及多次读写操作, 单线程可以很高的效率对于内存进行读写操作; 但是, 由于磁盘的IOPS仅为内存的几千分之一, 如果依旧用操作内存的方式操作磁盘, 那系统的整体性能将会很低, 这意味着必须将大量的读写操作聚合成一个Batch后再提交时才能达到较好的性能. 而将大量请求攒到一起的方式一是异步, 也就是请求本身和线程不绑定, 线程可以不Block(本质来说还是一种多线程的方式), 处理完一个线程后再处理其他线程. 这种做法的核心是将大量不同的请求提交到一个Buffer中, 再由该Buffer统一读取或者写入磁盘, 从而提高效率. 在慢速设备中, 多线程或异步非常常见, 在设计系统时, 面对磁盘、网络、SSD等慢速设备必须考虑使用多线程.

排他锁

有些场景不适合用单线程操作, 可以利用排他锁的方式来快速隔离并发读写事务. 数据库中有一些事务单元是共享的, 如图中的事务单元1是共享的, 事务单元2/3共享数据; 针对事务单元2/3共享数据的所有读写Block住, 事务单元1单独用一个锁来控制, 用这种方式完成系统的访问控制.

读写锁

如果是一个只读的事务, 例如只对数据进行查询操作, 在该过程中数据一定不被修改, 因此多个查询操作可以并行执行, 因此一种针对读读场景的优化自然而然产生——读写锁. 读写锁的核心是在多次读的操作中, 同时允许多个读者来访问共享资源, 提高并发性.

MVCC

在最初的数据库事务实现中是不存在MVCC的, 它是Oracle在八十年代新加的功能, 本质是Copy On Write, 也就是每次写都是以重新开始一个新的版本的方式写入数据,
因此, 数据库中也就包含了之前的所有版本. 在数据读的过程中, 先申请一个版本号, 如果该版本号小于正在写入的版本号, 则数据一定可以查询到, 无需等到新版本完全写完即可返回查询结果. 这种方式可以在读读不阻塞的前提下, 实现读写/写读不阻塞, 尽可能保证所有的读操作并行, 而写操作串行.

事务的调优原则

事务的调优的思路是在不影响业务应用的前提下:

第一. 尽可能减少锁的覆盖范围, 例如 Myisam表锁Innodb行锁就是一个减少锁覆盖范围的过程; 对于原位锁(排他锁读写锁等)可变为MVCC多版本(本质仍然是减少锁的范围).
第二. 增加锁上可并行的线程数, 例如读锁和写锁的分离, 允许并行读取数据.
第三. 选择正确锁类型, 其中悲观锁适合并发争抢比较严重的场景; 乐观锁适合并发争抢不太严重的场景.

  • 悲观锁 适合并发争抢比较严重的场景
    Collections.sychronizedList(new ArrayList());

  • 乐观锁 适合并发争抢不太严重的场景, 如自旋锁

分布式事务

目标

  • 完整的事务支持
    • 像传统单机事务一样的操作方式
    • 可按需无限扩展

容易理解的模型往往性能不好, 性能好的模型往往不容易理解—-这就是生活

分布式事务带来的问题

  • 网络带来的问题
  • 基于锁的事务实现中遇到的问题
    • 从2PL到2PC
    • 分布式事务的异常处理
    • 分布式日志记录
    • 分布式事务延迟变大的问题
  • 结合MVCC的事务实现中遇到的问题
    • 分布式顺序问题

并行与并发区别

分布式事务

分布式事务服务(Distributed Transaction Service,DTS)

由于在分布式系统中经常发生丢包、网络故障,分区容忍性是必须要满足的,同时为了兼顾高可用性,绝大部分系统都将强一致性需求转化成最终一致性的需求,并通过幂等机制保证了数据的最终一致性。

理解2PC和3PC协议

为了解决分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(2 Phase Commitment Protocol),三阶提交协议(3 Phase Commitment Protocol)。

2PC

分布式事务最常用的解决方案就是二阶段提交。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有参与者节点的操作结果并最终指示这些节点是否要把操作结果进行真正的提交。

因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。

第一阶段:投票阶段

该阶段的主要目的在于打探数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:

  1. 协调者向所有的参与者发送事务执行请求,并等待参与者反馈事务执行结果。
  2. 事务参与者收到请求之后,执行事务,但不提交,并记录事务日志。
  3. 参与者将自己事务执行情况反馈给协调者,同时阻塞等待协调者的后续指令。

第二阶段:事务提交阶段

在第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在三种可能:

  1. 所有的参与者回复能够正常执行事务。
  2. 一个或多个参与者回复事务执行失败。
  3. 协调者等待超时。

对于第一种情况,协调者将向所有的参与者发出提交事务的通知,具体步骤如下:

  1. 协调者向各个参与者发送commit通知,请求提交事务。
  2. 参与者收到事务提交通知之后,执行commit操作,然后释放占有的资源。
  3. 参与者向协调者返回事务commit结果信息。

对于第二、三种情况,协调者均认为参与者无法正常成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:

  1. 协调者向各个参与者发送事务rollback通知,请求回滚事务。
  2. 参与者收到事务回滚通知之后,执行rollback操作,然后释放占有的资源。
  3. 参与者向协调者返回事务rollback结果信息。

两阶段提交协议解决的是分布式数据库数据强一致性问题,其原理简单,易于实现,但是缺点也是显而易见的,主要缺点如下:

  • 单点问题:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,那么就会影响整个数据库集群的正常运行,比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。
  • 同步阻塞:两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率及其低下。
  • 数据不一致性:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

3PC

针对两阶段提交存在的问题,三阶段提交协议通过引入一个“预询盘”阶段,以及超时策略来减少整个集群的阻塞时间,提升系统性能。三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。

第一阶段:can_commit

该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的执行事务,这个过程是轻量的,具体步骤如下:

  1. 协调者向各个参与者发送事务询问通知,询问是否可以执行事务操作,并等待回复。
  2. 各个参与者依据自身状况回复一个预估值,如果预估自己能够正常执行事务就返回确定信息,并进入预备状态,否则返回否定信息。

第二阶段:pre_commit

本阶段协调者会根据第一阶段的询盘结果采取相应操作,询盘结果主要有三种:

  1. 所有的参与者都返回确定信息。
  2. 一个或多个参与者返回否定信息。
  3. 协调者等待超时。

针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:

  1. 协调者向所有的事务参与者发送事务执行通知。
  2. 参与者收到通知后,执行事务,但不提交。
  3. 参与者将事务执行情况返回给客户端。

在上面的步骤中,如果参与者等待超时,则会中断事务。 针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发出abort通知,请求退出预备状态,具体步骤如下:

  1. 协调者向所有事务参与者发送abort通知
  2. 参与者收到通知后,中断事务

第三阶段:do_commit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:

  1. 所有的参与者都能正常执行事务。
  2. 一个或多个参与者执行事务失败。
  3. 协调者等待超时。

针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:

  1. 协调者向所有参与者发送事务commit通知。
  2. 所有参与者在收到通知之后执行commit操作,并释放占有的资源。
  3. 参与者向协调者反馈事务提交结果。

针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发送事务回滚请求,具体步骤如下:

  1. 协调者向所有参与者发送事务rollback通知。
  2. 所有参与者在收到通知之后执行rollback操作,并释放占有的资源。
  3. 参与者向协调者反馈事务提交结果。

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续commit。相对于两阶段提交虽然降低了同步阻塞,但仍然无法避免数据的不一致性。

高吞吐 高性能 和单机事务一样易用

ACID

spandex

xa

java 协调器

分布式日志记录

隔离级别 事务的传递性

幂等


【相关文献】

apache Kylin 权威指南

各式各样的”SQL on Hadoop”技术应运而生,其中以Hive为代表,Impala、Presto、Phoenix、Drill、SparkSQL等紧随其后。它们的主要技术是”大规模并行处理”(Massive Parallel Processing,MPP)和”列式存储”(Columnar Storage)。大规模并行处理可以调动多台机器一起进行并行计算,用线性增加的资源来换取计算时间的线性下降。列式存储则将记录按列存放,这样做不仅可以在访问时只读取需要的列,还可以利用存储设备擅长连续读取的特点,大大提高读取的速率。这两项关键技术使得Hadoop上的SQL查询速度从小时提高到了分钟。

对于分析师来说,完备的、经过验证的数据模型比分析性能更加重要,直接访问纷繁复杂的原始数据并进行相关分析其实并不是很友好的体验,特别是在超大规模的数据集上,分析师将更多的精力花在了等待查询结果上,而不是在更加重要的建立领域模型上。

“预计算”就是Kylin在”大规模并行处理”和”列式存储”之外,提供给大数据分析的第三个关键技术。

Apache Kylin的工作原理本质上是MOLAP(Multidimensional Online Analytical Processing)Cube,也就是多维立方体分析。这是数据分析中相当经典的理论

Cube理论

星形模型(Star Schema)

MDX(MultiDimensional eXpressions)作为接口。虽然MDX作为OLAP查询语言

·大规模并行处理:可以通过增加机器的方式来扩容处理速度,在相同的时间里处理更多的数据。 ·列式存储:通过按列存储提高单位时间里数据的I/O吞吐率,还能跳过不需要访问的列。 ·索引:利用索引配合查询条件,可以迅速跳过不符合条件的数据块,仅扫描需要扫描的数据内容。 ·压缩:压缩数据然后存储,使得存储的密度更高,在有限的I/O速率下,在单位时间里读取更多的记录。

所有这些方法都只是提高了单位时间内处理数据的能力,当大家都一致采用这些技术时,它们之间的区别将只停留在实现层面的代码细节上。最重要的是,这些技术都不会改变一个事实,那就是处理时间与数据量之间的正比例关系。当数据量翻倍时,MPP(在不扩容的前提下)需要翻倍的时间来完成计算;列式存储需要翻倍的存储空间;索引下符合条件的记录数也会翻倍;压缩后的数据大小也还是之前的两倍。因此查询速度也会随之变成之前的两倍。当数据量成十倍百倍地增长时,这些技术的查询速度就会成十倍百倍地下降,最终变得不能接受。

Kylin对基数的计算方法采用的是HyperLogLog的近似算法

衍生维度 ??

增量构建

ETL

log4j是一个jar包,接口和实现都是在一起的,log4j2的接口和实现是分开的,这是log4j2的运行时依赖的地址,和slf4j的使用形式差不多

日志api/框架 桥接jar包(也可以称为适配器 被桥接对象 描述
log4j-api log4j-core 直接实现,不用桥接
log4j-1.2-api log4j-api/log4j-core log4j的jar中api的覆写,将项目中对log4j的调用转移到log4j2,(需要删除原有log4j的jar包)
commons-logging log4j-jcl log4j-api/log4j-core commons-logging 使用log4j2作为实现
slf4j-api log4j-slf4j-impl log4j-api/log4j-core 桥接包可以理解为log4j2作为slf4j的实现
Java Util Logging log4j-jul log4j-api/log4j-core
log4j-api log4j-to-slf4j slf4j-api及其实现 可以通过此链接到logback(logback-classic/logback-core)

log4j2

1
2
3
4
5
6
<log4j.version>2.17.2</log4j.version>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
1
mvn clean dependency:tree 
1
2
3
4
[INFO] +- org.apache.logging.log4j:log4j-slf4j-impl:jar:2.17.2:compile
[INFO] | +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] | +- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] | \- org.apache.logging.log4j:log4j-core:jar:2.17.2:runtime

Log4j patternLayout

如果您希望基于某种模式生成特定格式的日志信息,可使用 org.apache.Log4j.PatternLayout 格式化您的日志信息。

PatternLayout 继承自抽象类 org.apache.Log4j.Layout,覆盖了其 format() 方法,通过提供的模式,来格式化日志信息。

PatternLayout 是一个简单的 Layout 对象,提供了如下属性,该属性可通过配置文件

conversionPattern
设置转换模式,默认为 %r [%t] %p %c %x - %m%n

模式转换字符

下面的表格解释了上面模式中用到的字符,以及所有定制模式时能用到的字符:

转换字符 含义
c category的名称,可使用{n}限制输出的精度。例如:logger名为”a.b.c”,%c{2}将输出”b.c”。
C 产生log事件的java完全限定类名。可使用{n}限制输出的精度。例如:“org.apache.xyz.SomeClass”,%C{2}将输出“SomeClass”。
d 时间和日期的输出格式,例如:%d{yyyy MM dd HH:mm:ss,SS},可不带后面的日期格式字符。
F 产生log事件的java源文件名,带“.java”后缀及包名称。
l log发生位置的详细描述,包括方法名、文件名及行号。
L log发生在源文件中的位置。
m log事件的消息内容。
M log发生时所在的方法名称。
n 根据所运行的平台输出相应的行分隔字符。
p log事件的级别。
r 自程序运行至log事件产生所经过的时间。
t 产生log的线程名称。
x 用于与产生该日志事件的线程相关联输出的NDC(嵌套诊断上下文)
X 在X转换字符后面是键为的MDC。例如  X{clientIP} 将打印存储在MDC对键clientIP的信息
% 文字百分号 %%将打印%标志

格式修饰符

缺省情况下,信息保持原样输出。但是借助格式修饰符的帮助,就可调整最小列宽、最大列宽以及对齐。

PatternLayout 示例

下面是为 PatternLayout 编写的一个简单配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Define the root logger with appender file
log=log
log4j.rootLogger=DEBUG, FILE, stdout
# Define the file appender
log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=${log}/log.out
# Define the layout for file appender
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=%d{yyyy-MM-dd}-%t-%x-%-5p-%-10c:%m%n
# Define the file appender
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} %l %m%n

下面是生成日志信息的 Java 程序:

1
2
3
LOG.info("checkpointId: {}", savepoint.getCheckpointId());
LOG.info("version: {}", savepoint.getVersion());
LOG.info("version: {}", savepoint.getMasterStates());

编译并运行上述程序,会在目录 log 下生成一个名为 log.out 的文件,该文件包含如下日志信息:

1
2
3
[INFO ] 2021-10-12 14:12:48,232 com.tencent.flake.training.savepoint.Main.main(Main.java:23) checkpointId: 398
[INFO ] 2021-10-12 14:12:48,237 com.tencent.flake.training.savepoint.Main.main(Main.java:24) version: 2
[INFO ] 2021-10-12 14:12:48,237 com.tencent.flake.training.savepoint.Main.main(Main.java:25) version: []

更多格式样例

1
%d{yyyy-MM-dd HH\:mm\:ss,SSS} %-5p [%t] %-60c %x\:%L - %m%n
1
2
3
2021-10-12 14:19:35,851 INFO  [main] com.tencent.flake.training.savepoint.Main                    :23 - checkpointId: 398
2021-10-12 14:19:35,855 INFO [main] com.tencent.flake.training.savepoint.Main :24 - version: 2
2021-10-12 14:19:35,855 INFO [main] com.tencent.flake.training.savepoint.Main :25 - version: []

问题

StaticLoggerBinder

1
2
3
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

org.slf4j.impl.StaticLoggerBinder 是在适配器里面实现的,需要增加slf4j-log4j12、log4j-slf4j-impl、slf4j-jdk4

No appenders

1
2
3
log4j:WARN No appenders could be found for logger (com.tencent.flake.training.savepoint.Main).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

没有找到appender,需要配置properties或指定使用默认configurator.

1
BasicConfigurator.configure();

log4j 是目前最常见的日志系统之一,本文主要总结log4j的使用和配置.

log4j的组件

  • logger
  • appender
  • level
  • layout

logger-记录器 打印事件

logger 可以理解为日志记录器,它有name、appender和level等属性.
当调用

1
Logger logger = LoggerFactory.getLogger(AClass.class);

时,logger变量即为名称为AClass类的全名的logger.

如果使用

1
Logger logger = LoggerFactory.getLogger("myRedo");

logger变量即为名称为myRedo的logger

  1. 所有的logger的根都是rootLogger

  2. 一般情况下,取类名作为logger的名称. 父子关系遵从Java package的方法

  3. 子logger默认继承父logger的所有的配置. 子logger的配置会覆盖父logger的配置

  4. logger的名称可以为任意字符串

  5. log4j可以使用多种配置方式: properties文件、xml文件和Java代码

  6. 使用Java代码配置时,可以指定为基本配置: BasicConfigurator.configure();

  7. 可以指定特定包的log
    样例: 使用代码的方式配置特定包的logger

    1
    2
     Logger.getRootLogger().setLevel(Level.WARN);
    Logger.getLogger("cd.itcast.core").setLevel(Level.DEBUG);

appender 打印目的地

log4j定义了多种appender:

1
2
3
4
5
6
- org.apache.log4j.ConsoleAppender(控制台)
- org.apache.log4j.FileAppender(文件)
- org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
- org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
- org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
- org.apache.log4j.jdbc.JDBCAppender(将日志信息写入数据库)

在properties文件中配置

1
log4j.appender.<appenderName> = fully.qualified.name.of.appender.class

Appender类的属性

ConsoleAppender

Threshold=DEBUG :指定日志消息的输出最低层次.
ImmediateFlush=true :默认值是true,意谓着所有的消息都会被立即输出.
Target=System.err :默认情况下是:System.out,指定输出控制台

FileAppender

Threshold=DEBUF :指定日志消息的输出最低层次.
ImmediateFlush=true :默认值是true,意谓着所有的消息都会被立即输出.
File=mylog.txt :指定消息输出到mylog.txt文件.
Append=false :默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容.

RollingFileAppender

Threshold=DEBUG :指定日志消息的输出最低层次.
ImmediateFlush=true :默认值是true,意谓着所有的消息都会被立即输出.
File=mylog.txt :指定消息输出到mylog.txt文件.
Append=false :默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容.
MaxFileSize=100KB : 后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件.
MaxBackupIndex=2 :指定可以产生的滚动文件的最大数.
log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n

DailyRollingFileAppender

DatePattern
layout
Encoding
MaxBackupIndex

level 输出级别

ERROR、WARN、INFO、DEBUG

  • ERROR 为严重错误 主要是程序的错误
  • WARN 为一般警告,比如session丢失
  • INFO 为一般要显示的信息,比如登录登出
  • DEBUG 为程序的调试信息

layout

-X号: X信息输出时左对齐;

1
2
3
4
5
6
7
8
9
10
11
12
%p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
%d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%r: 输出自应用启动到输出该log信息耗费的毫秒数
%c: 输出日志信息所属的类目,通常就是所在类的全名
%t: 输出产生该日志事件的线程名
%l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数. 举例:Testlog4.main (TestLog4.java:10)
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中.
%%: 输出一个"%"字符
%F: 输出日志消息产生时所在的文件名称
%L: 输出代码中的行号
%m: 输出代码中指定的消息,产生的日志具体信息
%n: 输出一个回车换行符,Windows平台为"/r/n",Unix平台为"/n"输出日志信息换行

日志信息格式中几个符号所代表的含义:
可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式. 如:

  1. %20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,默认的情况下右对齐.
  2. %-20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,”-“号指定左对齐.
  3. %.30c:指定输出category的名称,最大的宽度是30,如果category的名称大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格.
  4. %20.30c:如果category的名称小于20就补空格,并且右对齐,如果其名称长于30字符,就从左边较远输出的字符截掉.

log4j性能优化

  1. 为了避免多次打开文件引起资源的浪费,log4j当打开文件或连接数据库后,会始终保持着连接,直到引用关闭.
    因此, 当引用运行过程中,删除文件. log4j不会再次创建该文件.
  2. logger文件是懒生成模式:当打印时,判断是否需要创建新文件

通过命令行执行log4j配置文件

1
2
3
java -jar -Dlog4j.configurationFile=file:/data/work/oceanus2/conf/log4j2-spring.properties \
-Dlogging.config=/data/work/oceanus2/conf/log4j2-spring.properties \
my_application.jar

典型应用

配置非类名logger及代码获取logger的信息

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
###################
# rootLogger的级别为INFO,appender为terminal01和allToFile
###################
log4j.rootLogger=INFO,terminal01,allToFile
###################
# Console Appender
###################
log4j.appender.terminal01=org.apache.log4j.ConsoleAppender
log4j.appender.terminal01.Target=System.out
log4j.appender.terminal01.layout=org.apache.log4j.PatternLayout
log4j.appender.terminal01.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.sss},%c,%-p,%m %n
########################
# Rolling File
########################
log4j.appender.allToFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.allToFile.File=/data/logs/blog-all.log
log4j.appender.allToFile.Append=true
log4j.appender.allToFile.MaxFileSize=100MB
log4j.appender.allToFile.MaxBackupIndex=7
log4j.appender.allToFile.Encoding=UTF-8
log4j.appender.allToFile.layout=org.apache.log4j.PatternLayout
log4j.appender.allToFile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.sss},%c,%-p,%m %n
########################
# 指定某个包的logger
########################
log4j.logger.com.abc.engin=INFO,engin
########################
# appender engin
########################
log4j.additivity.engin=false
## 是否追加到root appender中
log4j.appender.engin=org.apache.log4j.DailyRollingFileAppender
log4j.appender.engin.File=/data/logs/engin.log
log4j.appender.engin.Append=true
log4j.appender.engin.MaxFileSize=500MB
log4j.appender.engin.MaxBackupIndex=7
log4j.appender.engin.Encoding=UTF-8
log4j.appender.engin.layout=org.apache.log4j.PatternLayout
log4j.appender.engin.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.sss},%c,%-p,%m %n
########################
# 非类名logger
########################
log4j.logger.error=ERROR,errorAppender
########################
# appender errorAppender
########################
log4j.appender.errorAppender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.errorAppender.File=/data/logs/blog-error.log
log4j.appender.errorAppender.Append=true
log4j.appender.errorAppender.MaxFileSize=500MB
log4j.appender.errorAppender.MaxBackupIndex=7
log4j.appender.errorAppender.Encoding=UTF-8
log4j.appender.errorAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.errorAppender.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.sss},%c,%-p,%m %n

打印

1
2
3
4
5
Logger logger = LoggerFactory.getLogger(AClass.class);
logger.error("this is an error log");
// 获取名称为error的logger
Logger loggerError = LoggerFactory.getLogger("error");
loggerError.error("this is an error log");

从Java代码获取logger的配置信息

1
2
3
4
5
org.apache.log4j.Logger loggerError = org.apache.log4j.Logger.getLogger("error");
Appender appender = loggerError.getAppender("errorAppender");
DailyRollingFileAppender errorAppender = (DailyRollingFileAppender) appender;
String fileName = errorAppender.getFile();
String datePattern = errorAppender.getDatePattern();

每个小时打印一个文件

配置文件的方式

1
2
3
4
5
6
7
8
9
10
11
log4j.logger.redo=ERROR,redoLogger
# appender for redo logger
log4j.appender.redoLogger=org.apache.log4j.DailyRollingFileAppender
log4j.appender.redoLogger.File=/data/logs/my-redo.log
log4j.appender.redoLogger.Append=true
log4j.appender.redoLogger.MaxFileSize=500MB
log4j.appender.redoLogger.MaxBackupIndex=7
log4j.appender.redoLogger.Encoding=UTF-8
log4j.appender.redoLogger.layout=org.apache.log4j.PatternLayout
log4j.appender.redoLogger.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.sss},%c,%-p,%m \n
log4j.appender.redoLogger.DatePattern='.'yyyy-MM-dd-HH

Java代码的方式

1
2
3
4
5
DailyRollingFileAppender redoAppender = new DailyRollingFileAppender();
redoAppender.setDatePattern("'.'yyyy-MM-dd-HH");

Logger loggerRedo = Logger.getLogger("redo");
loggerRedo.addAppender(appender);

使用

1
2
3
public static final Logger redoLogger = LoggerFactory.getLogger("redo");
...
redoLogger.error("something is wrong");

下面的列表
The following list shows all the date patterns which have defined by log4j,

name DatePattern sample
Minutely '.'yyyy-MM-dd-HH-mm application.log.2013-02-28-13-54
Hourly '.'yyyy-MM-dd-HH application.log.2013-02-28-13
Half-daily '.'yyyy-MM-dd-a application.log.2013-02-28-AM app.log.2013-02-28-PM
Daily '.'yyyy-MM-dd application.log.2013-02-28
Weekly '.'yyyy-ww application.log.2013-07 app.log.2013-08
Monthly '.'yyyy-MM application.log.2013-01 app.log.2013-02

自定义 writerAppender

log4j将日志输出到swing控件

自定义 writer

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
public class LogWriter extends Writer {

private LogListModel model;

public LogWriter(LogListModel model) {
this.model = model;
}

@Override
public void write(int c) throws IOException {
model.addElement("" + c);
}

@Override
public void write(char[] cbuf) throws IOException {
model.addElement(new String(cbuf));
}

@Override
public void write(String str) throws IOException {
model.addElement(str);
}

@Override
public void write(String str, int off, int len) throws IOException {
model.addElement(str);
}

@Override
public void write(char[] cbuf, int off, int len) throws IOException {
model.addElement(new String(cbuf));
}

@Override
public void flush() throws IOException {
}

@Override
public void close() throws IOException {
}
}

创建logger与Swing组件

1
2
3
4
5
6
7
8
9
10
11
private Logger loggerA = LoggerFactory.getLogger("A1");
private LogListModel logListModel;

logListModel = new LogListModel();
JList ml = new JList(logListModel);

WriterAppender writeappender = new WriterAppender(new SimpleLayout(), new LogWriter(logListModel));
writeappender.setName("A1");
writeappender.setImmediateFlush(true);
Logger.getRootLogger().addAppender(writeappender);
Logger.getRootLogger().setLevel(Level.INFO);

应用

1
2
3
4
5
6
log4j.rootLogger=DEBUG,STDOUT

log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.Target=System.out
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.sss},%c,%-p,%m %n

log4j 1升级到log4j 2

删除项目中引用的 log4j jar 包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>

引入 log4j2  jar 包


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-1.2-api</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.0</version>
</dependency>

若使用了 slf4j 需要引入 

1
2
3
4
5
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.13.0</version>
</dependency>

删除 log4j.properties  新建 log4j2.xml

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
<?xml version="1.0" encoding="UTF-8" ?>
<configuration monitorInterval="5">

<appenders>
<!-- 将日志输出到控制台-->
<console >
<!-- 日志输出格式-->
<PatternLayout pattern="%d{yyyy-MM-dd hh:mm:ss}:%p %t %c %l- %mn"/>
<!-- 控制台只输出level及其以上的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</console>

<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<File >
<PatternLayout pattern="../logs/mapi/test/test.log.%d{yyyyMMdd}.log"/>
</File>
示例1:
<RollingFile
filePattern="../logs/mapi/consumer/consumer.log.%d{yyyyMMdd}.log">
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss:SSS} %p %t %c{1} - %mn</Pattern>
</PatternLayout>
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour filePattern最后时间单位为天则为一天-->
<TimeBasedTriggeringPolicy modulate="true" inyrtval="1"/>
</Policies>
</RollingFile>

示例2:
<!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile
filePattern="../logs/mapi/consumer/consumer.log.%d{yyyyMMdd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss:SSS} %p %t %c{1} - %mn</Pattern>
</PatternLayout>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<!-- 1.max参数是与filePattern中的计数器%i配合起作用的,若filePattern为filePattern="../logs/mapi/consumer/consumer.log.%d{yyyyMMdd}.log">,由于没有设置%i计数器,max参数将不起作用。-->

<!-- 2.max参数不是需要保留的文件的最大个数,日志文件date/time pattern不再符合filePattern时,计算器将被重置为1,日志总个数超过了max的指定值。-->

<!-- 可认为max参数规定了一定时间范围内归档文件的最大个数,默认为7-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
</appenders>
</configuration>

log4j2 配置参数详解

日志级别:如果一条日志信息的级别大于等于配置文件的级别,就记录。

  • trace:追踪,就是程序推进一下,可以写个 trace 输出
  • debug:调试,一般作为最低级别,trace 基本不用。
  • info:输出重要的信息,使用较多
  • warn:警告,有些信息不是错误信息,但也要给程序员一些提示。
  • error:错误信息。用的也很多。
  • fatal:致命错误。

PatternLayout 自定义日志布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
%d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间,输出到毫秒的时间
%-5level : 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0
%c : logger的名称(%logger)
%t : 输出当前线程名称
%p : 日志输出格式
%m : 日志内容,即 logger.info("message")
%n : 换行符
%C : Java类名(%F)
%L : 行号
%M : 方法名
%l : 输出语句所在的行数, 包括类名、方法名、文件名、行数
hostName : 本地机器名
hostAddress : 本地ip地址

log4j2 配置详解

根节点 Configuration
有两个属性:

  • status
  • monitorinterval

有两个子节点:

  • Appenders
  • Loggers(表明可以定义多个 Appender 和 Logger).

status 用来指定 log4j 本身的打印日志的级别.
monitorinterval 用于指定 log4j 自动重新配置的监测间隔时间,单位是 s, 最小是 5s.

Appenders 节点
常见的有三种子节点: Console、RollingFile、File

Console 节点用来定义输出到控制台的 Appender.

  • name: 指定 Appender 的名字.
  • target:SYSTEM_OUT 或 SYSTEM_ERR, 一般只设置默认: SYSTEM_OUT.
  • PatternLayout: 输出格式,不设置默认为:%m%n.

File 节点用来定义输出到指定位置的文件的 Appender.

  • name: 指定 Appender 的名字.
  • fileName: 指定输出日志的目的文件带全路径的文件名.
  • PatternLayout: 输出格式,不设置默认为:%m%n.

RollingFile 节点用来定义超过指定条件自动删除旧的创建新的 Appender.

  • name: 指定 Appender 的名字.
  • fileName: 指定输出日志的目的文件带全路径的文件名.
  • PatternLayout: 输出格式,不设置默认为:%m%n.
  • filePattern : 指定当发生 Rolling 时,文件的转移和重命名规则.
  • Policies: 指定滚动日志的策略,就是什么时候进行新建日志文件输出日志.
  • TimeBasedTriggeringPolicy:Policies 子节点,基于时间的滚动策略,interval 属性用来指定多久滚动一次,默认是 1 hour。modulate=true 用来调整时间:比如现在是早上 3am,interval 是 4,那么第一次滚动是在 4am,接着是 8am,12am… 而不是 7am.
  • SizeBasedTriggeringPolicy:Policies 子节点,基于指定文件大小的滚动策略,size 属性用来定义每个日志文件的大小.
  • DefaultRolloverStrategy: 用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的 (通过 max 属性)。

Loggers 节点,常见的有两种: Root 和 Logger.
Root 节点用来指定项目的根日志,如果没有单独指定 Logger,那么就会默认使用该 Root 日志输出

  • level: 日志输出级别,共有 8 个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < AppenderRef:Root 的子节点,用来指定该日志输出到哪个 Appender.
  • Logger 节点用来单独指定日志的形式,比如要为指定包下的 class 指定不同的日志级别等。
  • level: 日志输出级别,共有 8 个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
  • name: 用来指定该 Logger 所适用的类或者类所在的包全路径, 继承自 Root 节点.
  • AppenderRef:Logger 的子节点,用来指定该日志输出到哪个 Appender, 如果没有指定,就会默认继承自 Root. 如果指定了,那么会在指定的这个 Appender 和 Root 的 Appender 中都会输出,此时我们可以设置 Logger 的 additivity=”false” 只在自定义的 Appender 中进行输出。
  1. How to rotate log files based on time rather than size in Log4j?

  2. Log4j详细介绍(五)—-输出地Appender

  3. log4j2的入门到了解

参考: http://blog.csdn.net/fenglibing/article/details/6411999

  1. javah (C Header and Stub File Generator:用于生成native方法对应的C头文件,见JNI
  2. jps (Java Virtual Machine Process Status Tool)
  3. jstack (Java Stack Trace)
  4. jstat (Java Virtual Machine Statistics Monitoring Tool)
  5. jmap (Java Memory Map)
  6. jinfo (Java Configuration Info)
  7. jconsole (Java Monitoring and Management Console)
  8. jvisualvm (Java Virtual Machine Monitoring, Troubleshooting, and Profiling Tool)
  9. jhat (Java Heap Analyse Tool)
  10. Jdb (The Java Debugger)
  11. Jstatd (Java Statistics Monitoring Daemon)

//TODO
javap

1
2
3
4
5
6
7
8
set JAVA_OPTS=-Xms%INITIAL_HEAP_SIZE% -Xmx%MAXIMUM_HEAP_SIZE% -Xss%STACK_SIZE%
if not "%USER_LANGUAGE%"=="" set JAVA_OPTS=%JAVA_OPTS% -Duser.language=%USER_LANGUAGE%
if not "%USER_COUNTRY%"=="" set JAVA_OPTS=%JAVA_OPTS% -Duser.country=%USER_COUNTRY%

rem run Jude //注释 运行jude
start javaw %JAVA_OPTS% -jar "%JUDE_HOME%\%JUDE_JAR%" %1 %2 %3 //
IF ERRORLEVEL 2 goto noJavaw
goto end

[源码]:

  1. 源码百度云链接 密码:yfo5

[参考文献]:

  1. Think in Java