Java 任务处理

最近梳理其他同事以前写的 job 后有点想法,记录下。

一、业务场景

在大多数的系统都有类似这样的逻辑,比如下单了给用户赠送积分,用户在论坛上发表了帖子,给用户增加积分等等。

下单赠送积分,那么一个订单肯定不能重复赠送积分,所以需要一些状态来比较来哪些是已赠送的,哪些是没有赠送的。或许可以在订单表里加个字段来标记是否赠送了积分。

有时候,业务人员出于营销的需要,可能要搞个某某时间段内下单返券的活动。难道又在订单表里加个字段?肯定不能,谁知道还要搞多少活动呢。

二、实现

为了使核心的业务流程尽可能简单高效,赠送积分、返券(后面简称为task)之类的逻辑应该通过异步的job来处理。

因为 task 的处理状态不能放在核心的业务表里,所以,可以另外建一个表示异步任务的 async_task 表,结构如下:

-- 业务job处理 任务
create table async_task (
  id number(11) primary key,
  key_work  varchar2(32),  --  不同业务逻辑的task用不同的keyword
  biz_id char(32),         --  业务数据 ID,比如订单号
  biz_data varchar2(256),  --  核心的业务数据,用于避免关联业务表;具体结构取决于keyword
  status number,           --  任务的处理状态; -2:未处理, -1:处理中, 0:已处理, 大于 0 的数字表示失败次数
  create_tm date,          --  任务的创建时间
  modify_tm date           --  任务的修改时间
);

处于性能考虑,可以在 key_work 字段上建立分区,在 biz_id 上建立索引。

当业务表有需要处理的数据时,就往 async_task 插入一条相应的记录(可以异步插入),异步 job 再从 async_task 表里取数据来处理。

注意:处理 task 时,要保证数据的一致性。所在的项目组曾出现过,下单返券的活动里,送券与更新状态的操作没有放在同一个事务里,出现券送了,状态没更新,重复送券的问题。一定要注意事务的正确处理。

继续阅读

踩坑之 Java 可变长参数列表

Java 可变长参数列表

这是 Java 5 引入的一个特性,如果一个方法要接收的参数数量是不确定的,那么这个特性就可以派上用场了。

比如,在涉及IO操作的地方,基本上至少需要关闭两个流:输入、输出,我喜欢把流关闭的操作封装成下面的方法,这样只需一次调用就可以关闭多个流。

public static void closeSilent(Closeable... closeables) {
     for (Closeable closeable : closeables) {
          if (closeable != null) {
               try {
                    closeable.close();
               } catch (IOException ignored) {
               }
          }
     }
}

这是我觉得这个特性唯一适合使用的地方,具备下面的特点:

  • 这些参数具有相同的类型;
  • 参数数量不确定,每一个都是可选的;
  • 这些参数的用途都是一样的,比如上面都是执行关闭。

Java 可变长参数列表只能放在方法参数列表的最后。

继续阅读

《松本行弘的程序世界》– 面向对象

编程语言不是从安全的角度考虑减少程序员犯错误,而是在程序员自己负责的前提下为他提供最大限度发挥能力的灵活性。

一、结构化编程

结构化编程的基本思想是有序地控制流程,即把程序的执行顺序限制为顺序、分支和循环这 3 种,把共通的处理称为例程。

把基本上相同的处理抽象成例程,其中不同的部分由外部传递进来的参数来对应。

面向对象编程是结构化编程的延伸。

二、面向对象编程

多态性(也称动态绑定)、数据抽象(也称信息隐藏或封装)和继承被称为面向对象编程的三原则。

多态

多态的基本内容:根据对象的不同类型而进行适当处理;自动选择最合适的方法,而程序内部则不发生冲突。

各种数据可以统一处理。多态性让程序员只关注要处理什么(What),而不是怎么去处理(How)。

数据抽象

数据抽象是数据和处理方法的结合。对数据内容的处理和操作,必须通过事先定义好的方法来进行。数据和处理方法结合起来成为了黑盒子。

利用现有的类派生新类的方法称为“差分编程法”(difference programming)。

类以数据为核心,把与之相关的处理也都集中到一起。

黑盒化是模块化的基本原则,面向对象编程语言将每一类数据都当作黑盒处理。

继续阅读

Java 反射 抽取类的方法信息

目前参与的项目是用 Spring MVC + MyBatis 实现的,项目部署就是一个war包。公司从外面请了个顾问,建议将公司网络分为A、B两个区,B区的安全级别高些,可以访问数据库,A区的安全级别低些,不能访问数据库,直接面向互联网,应用需要访问外部互联网服务时 或 外部用户请求应用时都必须在 A 区完成,A区通过定制的网关访问 B 区的应用。这个建议是强制执行,所以就需要拆分项目了。

考虑到开发的方便性,A区与B区之间就必须工作在类似Hessian之类的远程调用上,而不能直接在http层上,要不然装包拆包都累死人了。

项目目前的代码层次是 Rest 风格的 Controller + Service + MyBatis 的 Mapper。Controller 里大量使用servlet的API,所以不能把controller层抽取出来作为远程调用的接口。Mapper本身只是一个接口,service层与mapper层之间没法再拆,只能在controller与service之间拆。项目里没有专门为每个 service 组件定义一个相应的接口,需要根据已有的service组件抽取出对应的接口。

由于组件太多,只能写工具类抽取。

工具类的目标:

  1. 抽取所有组件的公开方法作为接口的方法,保留方法定义的类型信息和参数名等信息。
  2. 生成接口所依赖的导入并拷贝所有依赖的导入类。
  3. 生成对应的Hessian配置。

要保留方法的参数名信息需要 Java 8 的特性。Java 8 的 javac 增加了一个选项 -parameters,表示在生成的字节码文件里保留方法的参数名。

继续阅读

Session Fix 与 Jboss 4.2.2

Session Fix 会话固定

Session Fix,会话固定,是一个安全漏洞,以 servlet 容器为例。

  • 无状态的HTTP与会话:由于 HTTP 是无状态的服务,容器为了在同一个用户的不同请求之间保持状态,为每个状态维持一个会话,Servlet 容器里一般就是 HttpSession 对象。HTTP 客户端每次请求时,都需要把表示这个 HttpSession 对象的 ID (一般命名为 JSESSIONID)传递过来,JSESSIONID 的传递机制常用的是作为URL 的一部分或放在 Cookie,由于每个 HTTP 请求都会传输 Cookie,所以这是最常见的机制。在第一次请求时,由于没有 JSESSIONID,容器会创建一个 HttpSession 对象,把它的的ID 作为 JSESSIONID 的值设置到 Cookie,HTTP 客户端第二次请求时,把这个 JSESSIONID 的值也传递给了容器,容器就可以找到对应的 HttpSession 对象。

  • 会话固定
    登陆与注销前后,如果这个 JSESSIONID 不会改变,则表示存在会话固定的漏洞。
    登陆前,用户第一次请求容器,容器生成一个 HttpSession 对象,设置 JSESSIONID,但这个JSESSIONID 是未验证的,如果这个 JSESSIONID 被恶意用户获取到,那么用户登录后,这个 JSESSIONID 变为有效的后,恶意用户就可以把用 JSESSIONID 来伪冒合法用户。
    注销后,也需要把合法的 JSESSIONID 作废,以免被恶意用户获取后假冒。

  • 修复方法
    登陆成功后作废登陆前的会话;注销成功后作为之前的合法会话。
    注销后需要调用 HttpSession.invalidate() 方法来作废合法的 HttpSession 对象,要不然还会存在内存泄漏,因为未注销的 HttpSession 对象只有在超时后才会被回收。

Jboss 4.2.2 默认配置导致会话固定

Jboss 4.2.2 的配置 .../deploy/jboss-web.deployer/server.xml 里的默认配置 emptySessionPath="true",这会导致 HttpSession.invalidate() 方法无效,需要修改为 false


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

JVM CPU 使用率高 问题两则

最近有两个系统先后被恶意扫描,出现 CPU 使用率高的现象。很好,把问题暴露出来解决掉。

CPU 使用率很高,首先是要找出 CPU 在执行什么样的代码,然后在分析这些代码有什么问题。

一、问题定位

1、 用命令 “ps aux | grep APP” 找出应用的进程 id:

801       84703  5.6 28.8 4627436 1132100 pts/2 Sl   11:08  11:19 /usr/jdk1.6.0_38/bin/java APP

2、 找出耗CPU的线程,在Linux系统下用命令:“ top –H –p pid ”, pid 就是前面找出来的应用进程 ID 。这个命令会显示出当前占用CPU高的线程。

Tasks: 426 total,   0 running, 426 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.5%us,  0.6%sy,  0.0%ni, 72.3%id, 26.6%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   3924912k total,  3308088k used,   616824k free,      768k buffers
Swap:  8388600k total,  3236720k used,  5151880k free,    12304k cached

   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
84784 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:00.85 java
84903 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:34.66 java
84983 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:00.99 java
85091 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:02.69 java
85164 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:04.92 java
84703 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:00.00 java
84704 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:00.42 java
84705 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.52 java
84706 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.64 java
84707 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.46 java
84708 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.39 java
84709 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:33.99 java

这里的 PID 比如 84784 是十进制的,需要转换为十六进制,用windows的计算器就可以转换了,转换为十六进制是:14B30

继续阅读

MySQL 事务隔离级别与MVCC

一、隔离级别

参考了维基百科:事務隔離

隔离级别规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。

下面对隔离级别的说明都是基于锁机制并发控制的数据库管理系统而言。

1. 可序列化(Serializable)

实现可序列化要求在选定对象上的读锁和写锁保持直到事务结束后才能释放。在 SELECT 的查询中使用一个 WHERE 子句来描述一个范围时应该获得一个“范围锁(range-locks)”。这种机制可以避免“幻影读(phantom reads)”现象。

可序列化是最高级别的隔离。

2. 可重复读(Repeatable read)

该级别保证了同一个事务中多次读取同样的记录的结果是一致的。

对选定对象的读锁(read locks)和写锁(write locks)一直保持到事务结束,但不要求“范围锁(range-locks)”,因此可能会发生“幻影读(phantom reads)”。

幻影读:是因为没有保持范围锁,该事务执行了一个 where 子句的范围查询后,其他事务可能新增了一条处于该事务 where 查询范围内的记录,那么该事务再次执行范围查询时就会看到这些新增的记录行(幻行,Phantom row)。

可重复读是 MySQL 的默认事务隔离级别。

3.提交读(Read committed)

该级别也叫不可重复读(nonrepeatable read)。

DBMS需要对选定对象的写锁(write locks)一直保持到事务结束,但是读锁(read locks)在SELECT操作完成后马上释放(因此“不可重复读”现象可能会发生,见下面描述)。和前一种隔离级别一样,也不要求“范围锁(range-locks)”。

不可重复读是因为,事务只维持了选定对象的写锁,如果一些选定对象只涉及读锁,那么在读锁释放之后,其它事务可以对这些对象进行修改,该事务再次读取时就不一致了。

大多数数据库的默认事务隔离级别都是这个。

4. 未提交读(Read uncommitted)

也称为脏读(dirty read)。

一个事务可以读取到其它事务未提交的更改。

不可重复读的重点是修改:同样的条件,读取过的数据,再次读取出来发现值不一样了。

幻读的重点在于新增或者删除:同样的条件,第 1 次和第 2 次读出来的记录数不一样。

继续阅读

不恰当使用线程池处理 MQ 消息引起的故障

现状

业务部门反应网站访问特别慢,负责运维监控的同事说MQ消息队列积压了,中间件的说应用服务器内存占用很高,GC 一直回收不了内存,GC 线程占了近 100% 的 CPU,其他的基本上都在等待,数据库很正常,完全没压力。没啥办法,线程、堆 dump 出来后,重启吧,然后应用又正常了。

分析

这种故障之前其实也碰到过了,分析了当时 dump 出来的堆后发现,处理 MQ 消息的线程池的队列长度达百万级别,占用了超过 1.3G 内存,这些内存都是没法回收的。

程序的实现目前是这样的:关联系统把消息推送到 MQ 上,我们再从 MQ 上拉消息下来处理;每种类型的消息都有一个线程负责从 MQ 上拉消息,拉下来后封装成线程池的任务提交给相应的线程池去执行。代码可以简化为:

package net.coderbee.mq.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MQListener {
     public ExecutorService executor = Executors.newFixedThreadPool(8);

     public void onMessage(final Object message) {
          executor.execute(new Runnable() {
               @Override
               public void run() {
                    // 耗时且复杂的消息处理逻辑
                    complicateHanlde(message);
               }
          });
     }

     private void complicateHanlde(Object message) {
     }
}

继续阅读

踩坑之 双机热备切换

这个问题还是去年在上家公司时碰到的,当时领导要我优化一个后台系统,它的核心逻辑就是接收客户端上传文件,把解析出的信息入库,再把文件上传到一个 FTP 服务器。

搞完之后总得部署到生产环境,因为这是个接手系统,只有源码,也不知道部署结构,只告诉我有两个结点,部署上去之后验证下没问题就行了。

我当时是准备一个一个结点部署的,在 serverA 上更新完成,验证后发现没有问题,然后准备更新 serverB 时,却发现突然又不行了,当时好像是请求没收到还是咋,具体不记得了,只有一个人在那里,折腾很久,最后联系运维人员,人家把双机热备的切换机制给停了才行。

事后分析认为是双机热备切换导致的,主机更新后一开始正常是因为还没有切换到备机上,而主机上的服务很快就起来了,请求仍然是到了主机上,所以一开始验证是可以的,但后来被切换到备机上去了,备机上跑的是旧的程序,请求不会到主机上,在主机上看是怎么也不会知道原因的,还以为程序有问题,当时也不会抓包分析,人都慌了,哈哈。

现在的 web 系统为了达到高可用,都会进行双机热备,就是两台服务器 serverA 和 serverB 上的部署是完全一样的,它们都配置了同一个虚拟 IP,但只有一台机器对外提供服务。如果这台提供服务的机器出故障了,双机热备程序就会自动把请求切换到另一台服务器上,从而保证服务是可用的。但这个切换是有延迟的,取决于双机热备的实现机制和配置。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

故障之 CDN 使用问题

故障问题

最近公司的一个系统用上了 CDN 服务,用户反应快多了,那个系统主要是静态资源展示和一些可公开的数据查询服务。另一个项目组的系统A看了也要上,不过这个A的后台代码是放在我所在项目组M里,是同一个应用,都在同一个域名 main.com 下。网络组把 main.com 的 DNS 解析到 CDN 服务器上去之后,访问也是快了,但是用户反应数据乱了,而且有些服务没法用了,后台统计特定地区用户的访问也不准了,因为所有请求基本上都是从几个 IP 里过来的。

问题原因

稍微对 CDN 有所了解的人看完问题描述之后都会发现问题所在:由于只有一个域名 main.com ,且被解析到 CDN 服务器上,CDN 不仅分发静态资源,还变成代理服务器了,所有动态请求、数据都要经过 CDN 服务器。

很明显,这是错误使用 CDN 带来的问题。这带来的问题不仅是上面描述的问题,更大的业务安全问题:当所有业务数据都经过 CDN 服务器时,CDN 厂商就有了那些业务数据,这相当于把银行卡和密码交给一个中间人,要存钱、取钱的时候就让这个中间人去 ATM/银行 处理,再给自己反馈结果。有这么值得信赖的中间人吗??

继续阅读