Quartz

1. 简介

Quartz 是个任务调度工具,就是定时执行指定的任务。

Quartz 提供了极为广泛的特性如持久化任务,集群和分布式任务等,用 Java 写成,与 Spring 集成方便,伸缩性,负载均衡,高可用性。

本文只关注基于数据库的 Quartz 集群,基于 Quartz 2.2.3 来说明。

2. 核心类

2.1. org.quartz.Job

任务接口,任务类代表要调度执行的业务逻辑实现,必须实现该接口。

如果任务不允许并发执行,则任务类必须添加注解 @DisallowConcurrentExecution 。

该接口只定义了一个方法:
void execute(JobExecutionContext context) throws JobExecutionException;

通过 context 可以访问配置给任务的数据 context.getJobDetail().getJobDataMap(),如果这个 JobDataMap 需要在修改后持久化到数据库里,则需要给任务类加上注解 @PersistJobDataAfterExecution 。Quartz 在下次调度执行这个任务时,会把持久化的数据反序列化成 JobDataMap 供应用使用。

不建议通过 Quartz 的这个机制持久化任何与业务有关的数据。因为业务数据应该由应用来存储,Quartz 只关注于调度执行。

2.2. org.quartz.JobDetail

任务在内存的表示。表示任务详细信息的接口,任务用调度器名称、任务名称和任务所归属的组名 来做唯一标识。

2.3. org.quartz.Trigger

触发器。用调度器名称、触发器名称和触发器所归属的组名 来做唯一标识。用于定义在什么情况、什么时间点执行任务。最常用的触发器类型就是基于 cron 表达式的。

2.4. org.quartz.spi.JobStore

数据存储抽象。该接口定义了 任务、触发器的存储、检索、更新 钩子,该接口还定义了任务执行完成后的回调方法。

Quartz 内建支持把任务、触发器等数据存储在JVM内存、文件、数据库,通过这个接口,就隔离了底层存储机制的差异。可以在配置文件里通过 org.quartz.jobStore.class 属性来指定该接口的实现,比如基于 Redis 做数据存储的实现。

继续阅读

Spring 导入资源文件

Spring 有多种方式可以导入资源文件,可以把这些属性文件里的属性值注入到 bean 的属性上。

1. 通过 <context:property-placeholder> 标签导入

通过这种方式导入的所有属性在同一个命名空间下,如果多个属性文件里有相同的属性名,以先导入的属性文件的属性为准,不会出现覆盖。可以通过 @Value("${propName}") 的方式注入到 bean 的属性上。

这种方式不能对属性文件指定 id,没法引用指定属性文件的属性,如果有同名的属性,引用到的总是第一个导入的属性。

配置如下:

<!-- 导入外部的资源文件,可以通过 @Value("${propName}") 的方式注入到 bean 的属性上 。 -->
<context:property-placeholder
    location="classpath:/learn/properties/1.properties"
    ignore-unresolvable="true" />
<!-- 要使用 context:property-placeholder 导入多个资源文件,必须指定 ignore-unresolvable="true" -->
<!-- 多个属性文件中,属性名相同的,以第一个加载的为准,不会出现后面加载的覆盖前面的 -->
<context:property-placeholder location="learn/properties/2.properties"
    ignore-unresolvable="true" />

继续阅读

基于POI检查 excel 文件的行数、列数是否超出限制

用 POI 解析 xlsx 时,如果用这样的方式 Workbook workbook = WorkbookFactory.create(new File("super-many-row.xlsx")); 解析具有超过 100 万行的 excel ,很可能内存就被耗尽了。

因为这样虽然简单,但是要等整个 excel 都解析完成、转换成 POI 定义的对象后才会返回到业务代码,这时候才判断行数是否超过限制就晚了。

需要一种更高效的方式,在解析完成前就判断是否超过限制,了解了 POI 的 API 和 xlsx 的基本格式后,就有了下面这个工具类,基于 POI 3.13。

2016.02.28 更新:增加对 sheet 数量的校验才够完整。

继续阅读

2015 年终总结

一、工作

工作上做了些功能、逻辑的优化,取得了一些的效果,大概有 4 个月几乎没有出现 IO 告警;系统优化需要持续进行,当前的 top sql 消除后,以前的次 top sql 又会变成新的 top。

主要的优化措施就是对大表做分区,尽量做到分区消除;对大表有关的统计不再实时统计,采用统计汇总表加增量统计的方式来优化;在业务上进行优化。

一大感触就是,起始设计非常重要,一开始的设计不合理,后期的优化、修复成本极其高昂。比如用户积分表只维护的用户的当前可用总积分,而没有维护历史总积分,而页面展示却需要历史总积分,导致每次展示用户历史总积分都需要实时从明细日志里累加出来。随着历史明细数据越来越多、用户数量和活跃度提高,这样的设计是没法支撑的,而如果要维护其历史总积分,现有3千万的用户,也非常不好弄。这种设计上的债务是长期存在的。有时候下定决心重新设计数据结构,迁移历史数据、代码上做平滑迁移的过程真的很酸爽。。。

多花精力在初始设计上!

二、学习

学习主要集中在 Oracle 数据库上,基本上把《基于 Oracle 的 SQL 优化》看了一遍。

因为工作需要,写了个 MyBatis 的批量插入的插件http://coderbee.net/index.php/open-source/20150721/1274

在学 Scala,看《深入理解 Scala》。

四、不足

看书太少,危机!

博客也写得很少。。。

五、高兴的事

找到女朋友啦,,^^,^^,,程序员知道这件事的重要意义啦。。。

《高性能 MySQL》 — 第五章 创建高性能的索引

索引基础

索引的使用:现在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行。

对于有多列的索引,MySQL 只能高效地使用索引的最左前缀列。因此列的顺序很重要。

MySQL 的唯一限制和主键限制都是通过索引实现。

索引的类型

MySQL 中,索引是在存储引擎层而不是服务器层实现的,所以没有统一的索引标准。

B-Tree 索引

B-Tree 通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相等,适合范围查询。

btree-structure

B-Tree 索引适用于全键值、键值范围或键前缀查找,其中键前缀查找只适用于根据最左前缀的查找。

索引还可以用于查询中的 order by 操作。

B-Tree 索引的限制:

  • 如果不是安装索引的最左列开始查找,则无法使用索引。
  • 不能跳过索引中的列。
  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。

这些限制是 MySQL 优化器和存储引擎使用索引的方式导致的。

继续阅读

《高性能 MySQL》 — 第四章 Schema 与数据类型优化

选择优化的数据类型

选择数据类型的一般原则:

  • 更小的通常更好:尽量使用可以正确存储数据的最小数据类型。

  • 简单就好:简单数据类型的操作通常需要更少的 CPU 周期。例如整型比字符操作代价更低,以为字符集和校对规则(排序规则)使字符比较比整型更复杂。举例,应该使用 MySQL 内建的类型(date, time, datetime) 而不是字符串来存储日期和时间,应该使用整型来存储 IP 地址。

  • 尽量避免 NULL:如果查询中包含可为 NULL 的列,对 MySQL 来说更难优化,因为可为 NULL 的列使得索引、索引统计和值比较都更复杂。可为 NULL 的列会使用更多的存储空间,在 MySQL 里也需要特殊处理。当可为 NULL 的列被索引时,每个索引记录需要一个额外的字节,在 MyISAM 里甚至还可能导致固定大小的索引变成可变大小的索引。例外,对于稀疏数据(很多列的值为 NULL),InnoDB 使用单独的位(bit)存储 NULL 值,有很好的空间效率。

在为列选择数据类型时,第一步需要确定合适的大类型:数字、字符串、时间等。下一步是选择具体类型。

继续阅读

摘记–《富爸爸穷爸爸》

国庆的后面几天看完了《富爸爸穷爸爸》,做了些摘记:

一个人的观念对他的一生影响巨大。

贫穷和破产的区别是:破产是暂时的,而贫穷是永久的。

如果你认为是我的问题,你就会想改变我;如果你认为问题在那儿,你就会改变自己,学习一些东西让自己变得更聪明。大多数人认为世界上除了自己外,其他人都应该改变。改变自己比改变他人更容易。

真正的学习需要精力、激情和热切的愿望。愤怒是其中一个重要的组成部分,因为激情正是愤怒和热爱的结合体。

你现在才 9 岁,已经有了为钱工作的体验了。你只需要把上个月的生活重复 50 年,就会知道大多数人是如何度过一生的了。(类似“一年的工作经验重复用了 N年”)

老鼠赛跑:起床,上班,付账,再起床,再上班,再付账单—-他们的生活从此被这两种感觉所控制:恐惧和贪婪。给他们更多的钱,他们就会以更高的开支重复这种循环。

正是因为有感情,我们才成为人。感情使我们更加真实,它是我们行动的动力。忠实于你的感情,以你喜欢的方式运用你的头脑和感情,不要让它们(恐惧和贪婪)控制你。

不幸的是,对许多人来说,离开学校是学习的终点而不是起点。

继续阅读

《深入理解 Scala》第五章 — 利用隐式转换编写更有表达力

Scala 的隐式转换系统定义了一套定义良好的查找机制,让编译器能够调整代码。Scala 编译器可以推导下面两种情况:

  • 缺少参数的方法调用或构造器调用;
  • 缺少了的从一种类型到另一种类型的转换。(是指调用某个对象上不存在的方法时,隐式转换自动把对象转换成有该方法的对象)

介绍隐式转换系统

implicit 关键字可通过两种方式使用:1、方法或变量定义;2、方法参数列表。如果关键字用在变量或方法上,等于告诉编译器在隐式解析(implicit resolution)的过程中可以使用这些方法和变量。

隐式解析是指编译器发现代码里的缺少部分信息,而去查找缺少信息的过程。

implicit 关键字用在方法参数列表的开头,是告诉编译器应当通过隐式解析来确定所有的参数值。

scala> def findAnInt(implicit x: Int) = x
findAnInt: (implicit x: Int)Int

scala> findAnInt
<console>:9: error: could not find implicit value for parameter x: Int
              findAnInt
              ^

scala> implicit val test = 5
test: Int = 5

scala> findAnInt
res1: Int = 5

scala> def findTwoInt(implicit a: Int, b: Int) = a + b
findTwoInt: (implicit a: Int, implicit b: Int)Int

scala> findTwoInt  // implicit 作用于整个参数列表
res2: Int = 10

scala> implicit val test2 = 5
test2: Int = 5

scala> findTwoInt
<console>:11: error: ambiguous implicit values:
 both value test of type => Int
 and value test2 of type => Int
 match expected type Int
              findTwoInt
              ^

scala> findTwoInt(2, 5)  // 隐式的方法参数仍然能够显式给出
res4: Int = 7

隐式的方法参数仍然能够显式给出。

继续阅读

隐式类型转换导致全表扫描

最近系统改版了一部分(其实就是重做。。。),上线之后又开始出现数据库 IO 告警了。

DBA 抓出一个全表扫描的 SQL,是把范围在两个钟内的数据找出来做更新的,这个查询的时间字段是有索引的,结果却没有用上,
跟另一个表关联的时候,关联字段也是有索引的,但还是没有用上。

在两个钟内的数据量应该是很小的,相对于总量来说,时间字段的索引没理由不用啊;从主表找出来的数据量不大,再去关联表查询也应该走索引的。
感觉就是执行计划不对了,去找 DBA 做固化。

找 DBA 先用 hint 生成一个走索引的执行计划,然后做了固化。

这个 SQL 是两个钟才执行一次的,结果固化之后值班 DBA 说主表还是全表扫描。找 DBA 再做一次固化,还是主表全表扫描。最后开了 tunning ,提示说有隐式类型转换,主表没法走索引。

大致扫了一下代码,Java 里用的数据类型是 Date 的,而表的列的类型也是 Date,应该不需要做隐式类型转换啊。

问题出在 MyBatis 的参数类型指定为 timestamp 而不是 Date;由于查询需要时分秒,而 Date 会抹去这些值,所以被指定为 timestamp,
而这个类型刚好对应到 Oracle 的 timestamp 类型,所以就出现隐式类型转换。

细节吶。。。

Spring MVC 与线程

最近又被一个连环坑坑惨了,我踩的坑是与 Spring MVC 的线程模型有关的。

我们的系统现在有两个 war 包,分别部署在不同的 JBoss 上,第一个是互联网可访问的,叫 front 吧,第二个是在防火墙之后的,只能通过 front 来访问,叫 backend,是可以访问数据库的。

front 通过 Hessian 调用 backend 的服务。

现在有个需求要求 backend 记录互联网用户的 IP 和其他一些参数,接口太多,不可能每个方法再添加参数,由于 Hessian 也运行是在 HTTP 上的,所以我想在 Hessian 的 HTTP 请求头里把这些需要的参数传过去。

根据 debug,Hessian 采用的是 JDK 的 HessianURLConnectionFactory 来创建连接,处理 HTTP 请求,所以我就扩展了它的方法 public HessianConnection open(URL url) throws IOException ,在里面把参数放到请求头里。由于要取的是客户端实际 IP 等信息,所以还需要获取客户端的请求 HttpServletRequest ,刚好 Spring MVC 里面有个 RequestContextHolder 工具,持有了 HttpServletRequest 。然后就有了下面这行代码:HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); 。坑就挖成了。

点开 RequestContextHolder.getRequestAttributes() 方法来看看:

public static RequestAttributes getRequestAttributes() {
     RequestAttributes attributes = requestAttributesHolder.get();
     if (attributes == null) {
          attributes = inheritableRequestAttributesHolder.get();
     }
     return attributes;
}

requestAttributesHolder 是这样的:

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
          new NamedThreadLocal<RequestAttributes>("Request attributes");

NamedThreadLocal 是这样定义的: public class NamedThreadLocal<T> extends ThreadLocal<T> {
也就是说这个请求是跟线程绑定的,在别的线程是获取不到的。

其他的坑是这样的:
在 front 有个地方用了异步去调用 backend,其实是根本没必要用异步的(因为当前线程发出请求后是在等待响应的);而这个异步调用在测试环境时没有启用,被开关切去走另一个同步的分支,而生产环境确是必须走这个异步分支的。