分类目录归档:开源技术

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 做数据存储的实现。

继续阅读

mybatis 批量插入 插件

2017-01-07 更新:这个插件做了重命名、梳理,新的 github 地址为 MyBatis-Batch

可直接从 Maven 中央仓库引用:
<dependency>
<groupId>net.coderbee</groupId>
<artifactId>mybatis-batch</artifactId>
<version>1.1.0</version>
</dependency>

背景

项目中有个设计不合理的表,总共 8 个字段,有 5 个索引,有几个索引字段还是 32 位的字符串。该表数据量已达 1 亿,最近每天新增 100 万。根据日志看,有次用户上传一个有 200 行记录的 excel,需要往这个表插入 3940 条记录,耗时 72 秒。这么大延迟是没法接受的。

要分析数据库方面的问题,首先是找 DBA 分析下表的情况,说跟以往没多大区别,只是跟这个表有关的插入的执行计划很多。因为这个表的批量插入是这样的:

<select id="batchSave" parameterType="java.util.List">
    INSERT INTO TABLE_NAME(ID,NAME) 
<foreach collection="list"item="itm" separator="union all">
    (SELECT #{itm.id}, #{itm.name} FROM DUAL)
</foreach></select>

这是用 MyBatis 对 Oracle 做批量插入的唯一方法。副作用是:假定 List 的最大长度是 N,那么 Oracle 服务器端就可能有 N 个插入的执行计划。这么多执行计划,DBA 也不乐意去分析呀,而且确实生成的每个执行计划都是很简单的。

优化

如果放弃 union all 的方式,则每条记录都需要各提交一次到数据库,显然也不好。

为了解决 N 个执行计划的问题,做到真正的批量插入,只能修改 MyBatis 的执行逻辑,因此就有了这个项目:mybatis-batchinsert-plugin

目前可以批量插入,但不支持返回主键等其他的功能,有空再完善。

我做这个插件主要是希望达到:

  • 1、解决 Oracle 服务器端对一个表做插入的执行计划过多的问题;用 union all 的方式还可能导致硬解析增多,比如需要插入的总记录有 123 条,以 100 条为一批拼接成一条 sql A,那么剩下的 23 条也会拼接成一条 B,而 B 的频率肯定比 A 的频率低很多,因为要做批量插入的记录总数不是固定的,所以余数也不是固定的,容易导致 B 的解析过期,被清除出去,然后又来了条余数 23 的,那么就需要硬解析了。
  • 2、避免用 union all 拼接成一条 sql,导致这条 sql 里的绑定变量过多。
  • 3、兼容 MyBatis 的用法,在使用上更简单了,因为不需要用 MyBatis 的 for 循环,由插件来做循环;用 union all 方式的话,Java 代码要控制批的大小,需要一个循环,MyBatis 里需要一个,用于拼接 union all。
  • 4、一个对于性能有益的是:每次调用 MyBatis 的 insert 语句,都需要调用一次 Connection.prepareStatement 方法,也就需要访问数据库一次,如果对语句没有缓存的话;用插件的话,不管多少记录,只需要一次。
  • 这个插件对性能提升不会很显著,在我们的开发数据库上测试时,以 1000 条为 1 批,已有数据量 160w,插入 40w 数据,用 union all 的方式平均一条需要 32ms,用插件大概是 27ms。

    Struts2 备忘

    今天帮两个同事各看了一个问题,有点意思。

    Struts2 JSON 序列化与 Action get 方法

    场景:同事跑来说他的ajax请求总是进入ajax的错误处理的回调函数,他需要用ajax提交请求到action,然后处理返回结果。

    异常现象:这个同事也没什么经验,他告诉我的是ajax请求总是进入ajax的错误处理的回调函数,我在chrome开发工具那里一看,http响应是500,肯定进入错误处理的回调函数了,所以问题应该是在服务端。

    分析与解决:在eclipse里debug跟踪发现action执行是ok的,返回的结果名也是 SUCCESS,配置也没问题,但控制返回给Struts2框架后却抛出一堆异常,异常信息跟请求完全无关,从异常栈来看,抛出异常的方法也没有在请求方法里调用,虽然它们都在同一个action里,异常栈的方法基本都是JSON序列化有关的方法的,也就是说问题应该是出在Struts2框架在把action结果序列化为JSON返回给浏览器之前。一开始也看不出具体问题,后来同事在网上搜索看到说是 setter/getter 导致的,然后他的那个被抛出异常的方法也是以get开头命名的,坑就这么形成了:Struts2在序列化action的结果时,会把action的所有公开的getter方法的结果序列化到json里,那个方法以get开头,自然也就被调用了,然后异常就抛出来了。

    这个问题自己写的时候一般都会注意避开,但不小心的人还是会踩上的。

    ajax 提交 JS 数组对象与 Struts2 参数解析

    场景:在页面输入一组用户信息,然后用jQuery的ajax提交给后台的Struts2 action,在JS里,这组信息是用数组来存储,数组的每个元素是一个JSON对象。

    异常现象:在tomcat的控制台输出的异常信息是这样的:No result defined for action package4.business.action and result input

    分析与解决:一开始我也以为是Java代码的问题,毕竟异常是在控制台里抛出的,但代码也看不出什么问题。这个请求的url在浏览器地址栏直接打开是没有问题的,所以我想,是不是跟请求方法有关,改为 GET 后还是出错。再想,地址栏打开是不带数据的,so在ajax里也不带数据,结果也没异常了,所以可以确定是请求参数的问题了。在chrome的开发工具那里看到,数组参数都被序列化为 persons[0][name]=name 的形式发出去了,这在js里是没问题的,访问对象的属性可以用句点(json.prop),也可以用方括号(json[prop]),参数被提交到Struts2时,它进行参数解析并映射到Java对象,它的规则却与这个有冲突了,对于句点,Struts2也是解析为对象的属性,但方括号却是被当作数组的,所以解析肯定失败,会抛出异常,捕获到参数解析异常后,请求会被路由到命名为 input 的结果那里,但我们这个项目都没配置 input,所以抛出的异常是找不到 result input

    Redis-RDB-Dump-File-Format 中文翻译

    今年年初翻译的,都忘得差不多了,最近想用Go写个程序解析RDB文件,重新翻出来。

    翻译自:
    https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format

    如果对本文档的后续更新有兴趣,可关注:https://github.com/wen866595/open-doc

    Redis RDB 文件格式

    Redis *.rdb 文件是一个内存内存储的二进制表示法。这个二进制文件足以完全恢复Redis的状态。

    rdb文件格式为快速读和写优化。LZF压缩可以用来减少文件大小。通常,对象前面有它们的长度,
    这样,在读取对象之前,你可以准确地分配内存大小。

    为快速读/写优化意味着磁盘上的格式应该尽可能接近于在内存里的表示法。这种方式正是rdb文件采用的。
    导致的结果是,在不了解Redis在内存里表示数据的数据结构的情况下,你没法解析rdb文件。

    解析RDB的高层算法

    在高层层面看,RDB文件有下面的格式:

    
    ----------------------------# RDB 是一个二进制文件。文件里没有新行或空格。
    52 45 44 49 53              # 魔术字符串 "REDIS"
    00 00 00 03                 # RDB 版本号,高位优先。在这种情况下,版本是 0003 = 3
    ----------------------------
    FE 00                       # FE = code 指出数据库选择器. 数据库号 = 00
    ----------------------------# 键值对开始
    FD $unsigned int            # FD 指出 "有效期限时间是秒为单位". 在这之后,读取4字节无符号整数作为有效期限时间。
    $value-type                 # 1 字节标记指出值的类型 - set,map,sorted set 等。
    $string-encoded-key         # 键,编码为一个redis字符串。
    $encoded-value              # 值,编码取决于 $value-type.
    ----------------------------
    FC $unsigned long           # FC 指出 "有效期限时间是豪秒为单位". 在这之后,读取8字节无符号长整数作为有效期限时间。
    $value-type                 # 1 字节标记指出值的类型 - set,map,sorted set 等。
    $string-encoded-key         # 键,编码为一个redis字符串。
    $encoded-value              # 值,编码取决于 $value-type.
    ----------------------------
    $value-type                 # 这个键值对没有有效期限。$value_type 保证 != to FD, FC, FE and FF
    $string-encoded-key
    $encoded-value
    ----------------------------
    FE $length-encoding         # 前一个数据库结束,下一个数据库开始。数据库号用长度编码读取。
    ----------------------------
    ...                         # 这个数据库的键值对,另外的数据库。
    FF                          ## RDB 文件结束指示器
    8 byte checksum             ## 整个文件的 CRC 32 校验和。
    

    继续阅读

    Dropbox Go SDK

    把最近用Go写的 Dropbox SDK 发布到了 gihub上,有兴趣的可以参与、反馈。

    地址: https://github.com/wen866595/godropbox

    大多数接口都已实现,除了获取delta数据的,因为这个接口返回的JSON结构数据没法映射到Go的数据结构上。

    一些示例:

     oauth2 := &oauth2.OAuth2{AccessToken: "you ouath2 access_token"}
    
     dropboxApi := &dropbox.DropboxApi{Signer: oauth2, Root: "dropbox", Locale: "CN"}
    
     accountInfo, err := dropboxApi.GetAccountInfo()
     if err != nil {
         fmt.Printf("error msg: %s\n", err)
     } else {
         fmt.Printf("account info: %v\n", accountInfo)
     }
    
     metadata, err := dropboxApi.GetFileMetadata("/")
     if err != nil {
         fmt.Printf("error msg: %s\n", err)
     } else {
         fmt.Printf("metadata: %v\n", metadata)
     }
    
     put, err := dropboxApi.PutFileByName("main.go", "dropbox", "/main.go")
     if err != nil {
         fmt.Printf("error msg: %s\n", err)
     } else {
         fmt.Printf("put: %v\n", put)
     }
    
     copyRef, err := dropboxApi.CopyRef("/main.go")
     if err != nil {
         fmt.Printf("error msg: %s\n", err)
     } else {
         fmt.Printf("copyRef : %v\n", copyRef)
     }
    
     revisions, err := dropboxApi.Revisions("/main.go")
     if err != nil {
         fmt.Printf("error msg: %s\n", err)
     } else {
         fmt.Printf("revisions : %v\n", revisions)
     }
    
     shares, err := dropboxApi.Shares("/main.go")
     if err != nil {
         fmt.Printf("error msg: %s\n", err)
     } else {
         fmt.Printf("shares : %v\n", shares)
     }
    
      media, err := dropboxApi.Media("/main.go")
      if err != nil {
          fmt.Printf("error msg: %s\n", err)
      } else {
          fmt.Printf("media : %v\n", media)
      }
    
      thumbnails, err := dropboxApi.Thumbnails("/IMG_20130613_121901.jpg")
      if err != nil {
          fmt.Printf("get thumbnails error msg: %s\n", err)
      } else {
          ioerr := ioutil.WriteFile("IMG_20130613_121901.jpg", thumbnails.DataByte, 666)
          if ioerr == nil {
              fmt.Printf("write image ok .\n")
          } else {
              fmt.Printf("write image error : %v\n", ioerr)
          }
      }
    
      copym, err := dropboxApi.Copy("/testcopy.txt", "/abctest/testcopy.txt")
      if err != nil {
          fmt.Printf("error msg: %s\n", err)
      } else {
          fmt.Printf("copym : %v\n", copym)
      }
    
      copym, err := dropboxApi.Copy("/testcopy.txt", "/abctest/testcopy.txt")
      if err != nil {
          fmt.Printf("error msg: %s\n", err)
      } else {
          fmt.Printf("copym : %v\n", copym)
      }
    
      move, err := dropboxApi.Move("/abctest/testcopy.txt", "/testcopy-moved.txt")
      if err != nil {
          fmt.Printf("error msg: %s\n", err)
      } else {
          fmt.Printf("move : %v\n", move)
      }
    
      createFolder, err := dropboxApi.CreateFolder("createFolder")
      if err != nil {
          fmt.Printf("error msg: %s\n", err)
      } else {
          fmt.Printf("createFolder: %v\n", createFolder)
      }
    
      deleted, err := dropboxApi.Delete("createFolder")
      if err != nil {
          fmt.Printf("error msg: %s\n", err)
      } else {
          fmt.Printf("deleted: %v\n", deleted)
      }
    

    Disruptor 源码阅读笔记

    一、Disruptor 是什么?

    Disruptor 是一个高性能异步处理框架,也可以认为是一个消息框架,它实现了观察者模式。

    Disruptor 比传统的基于锁的消息框架的优势在于:它是无锁的、CPU友好;它不会清除缓存中的数据,只会覆盖,降低了垃圾回收机制启动的频率。

    这个解读是在最新版 3.1.1 的源码上进行。

    关于Disruptor的更多介绍可见: http://ifeve.com/disruptor/

    二、Disruptor 为什么快

    • 不使用锁。通过内存屏障和原子性的CAS操作替换锁。
    • 缓存基于数组而不是链表,用位运算替代求模。缓存的长度总是2的n次方,这样可以用位运算 i & (length - 1) 替代 i % length

    • 去除伪共享。CPU的缓存一般是以缓存行为最小单位的,对应主存的一块相应大小的单元;当前的缓存行大小一般是64字节,每个缓存行一次只能被一个CPU核访问,如果一个缓存行被多个CPU核访问,就会造成竞争,导致某个核必须等其他核处理完了才能继续处理,响应性能。去除伪共享就是确保CPU核访问某个缓存行时不会出现争用。

    • 预分配缓存对象,通过更新缓存里对象的属性而不是删除对象来减少垃圾回收。

    继续阅读