前几天看了微博上build openJDK 的一篇文章, 决定也尝试一下自己 build 出来一个 JDK.下面是 build 的过程以及遇到的一些问题及解决办法. 本文基于 Ubutu 14.04 LTS 版本.
build 过程涉及 openJDK 部分源码的修改, 修改完的代码我上传到github了, 地址: https://github.com/xiaobaoqiu/ubuntu_build_openjdk7.git
OpenJDK 官网: http://openjdk.java.net/
注意OpenJDK的版本控制使用 Mercurial 做的.
下面是 build 之前的一些准备工作, 主要包含几个:
1.安装 Mercurial
Mercurial 官网: https://www.mercurial-scm.org/
1
|
|
2.下载 openJDK 源码
1
|
|
之后, 进入源码目录, 有个 get_source.sh 的脚本, 执行就可以了.
3.安装 build 需要的工具
就是 g++ 之类的 make 需要的工具.
1
|
|
4.准备一个BOOT JDK
需要注意, 如果 build openJDK7, 需要准备 JDK6(如果用JDK7, 会碰到一个错误, 后面会讲到).
写了简单的小shell, 配置一些环境变量(如 BOOT JDK 位置).
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 |
|
和源码在同级目录(自己调整):
1 2 3 4 |
|
之后, 执行 build7.sh 脚本就行了(需要chmod +x build7.sh). 幸运的话就没问题了(基本不可能没问题), 下面是我碰到的一些问题和解决办法.
下面是 build 过程的一些问题总结.
1.This OS is not supported
错误信息大致如下:
1
|
|
这是由于内核版本太高了, 需要简单修改 Makefile, 首先看一下当前的 Linux 系统版本:
1 2 |
|
在 jdk7/hotspot/make/linux/Makefile 文件中修改 SUPPORTED_OS_VERSION, 增加自己的内核版本支持
1
|
|
2.error: “__LEAF” redefined
这是 openJDK 源码和 gcc 源码冲突, 一个bug, 参见: http://bugs.java.com/view_bug.do?bug_id=7103224
需要按照 http://hg.openjdk.java.net/lambda/lambda/hotspot/rev/a6eef545f1a2 修改代码, 涉及的代码包括以下 6 个文件, 具体的完整代码可以从文章开头提供的 github 地址下载.
1 2 3 4 5 6 |
|
3.error: converting ‘false’ to pointer type ‘methodOop’
错误信息:
1 2 3 4 |
|
修改(具体的完整代码可以从文章开头提供的 github 地址下载.): /openjdk/hotspot/src/share/vm/oops/constantPoolOop.cpp 第272行 return false改为return (methodOop)false;
4.error: converting ‘false’ to pointer type ‘Node’*
错误信息
1 2 3 4 |
|
修改(具体的完整代码可以从文章开头提供的 github 地址下载.): openjdk/hotspot/src/share/vm/opto/loopnode.cpp: 第896行 return false改为return (Node*)false;
5.Unable to load native library: /home/q/java/jdk1.7.0_80/jre/lib/amd64/libjava.so
错误信息:
1
|
|
解决: 使用 JDK6 作为BOOT JDK
6.gcc: error: unrecognized command line option ‘-mimpure-text’
这个-mimpure-text是gcc给Solaris的编译选项,所以注释掉即可
修改/jdk/make/common/shared/Compiler-gcc.gmk, 去掉 -mimpure-text (具体的完整代码可以从文章开头提供的 github 地址下载.)
7. Error: time is more than 10 years from present: 1136059200000
错误信息
1 2 3 4 5 6 |
|
修正 jdk7/jdk/src/share/classes/java/util/CurrencyData.properties 文件, 保证时间在 10 年之内.(具体的完整代码可以从文章开头提供的 github 地址下载.):
1 2 3 4 5 |
|
经过一番折腾之后, build 成功:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
默认 build 的结果在 jdk7/build/linux-amd64 下:
1 2 3 4 |
|
需要 build openJDK8 也基本一样, 下载源码地址变一下, openJDK8 源码没有需要该的, 一路通畅:
脚本稍微变更(注意需要 configure , 参考其自带的 README-builds.html):
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 |
|
build 成功:
1 2 3 4 |
|
接口限流是保证系统稳定性的三大法宝之一(缓存, 限流, 降级).
本文使用三种方式实现Api限流, 并提供了一个用Spring实现的Api限流的简单Demo, Demo的git地址: https://github.com/xiaobaoqiu/api-blocking
其中接口限流配置在文件 blocking-config.properties 中, 内容实例如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
里面包含了三种方式来实现限流, 下面将主要审核分别详细介绍三种方式:
1.Redis
2.滑动窗口
3.Guava的RateLimiter
Redis的官网的命令手册的例子就是如何使用 incr 指令实现接口限流.参见官网: https://redis.io/commands/incr/
简单说就是每个请求生成一个key(可以根据IP + 接口url生成, 也可以直接根据接口url生成), value为计数值. 设置过期时间.
需要注意 Redis 的过期策略是混合的:
1.被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key;
2.主动删除:Redis会定期(默认好像是100ms)主动淘汰一批已过期的key;当已用内存超过限定时, 也会触发主动清理策略;
大家都知道TCP中的滑动窗口有调节发送速率的作用.这里是一个类似的想法.
按照我们的配置, 我们期望 duration 时间内最多 limit 个请求, 我们可以想象有一个事件窗口, 其宽度就是 duration, 因为每个请求都有一个时间戳(可以用Long表示), 每次请求过来的时候, 我们只需要校验当前请求为尾端的时间窗口内的请求数目是否满足 limit 需求就行了.
实现很简单, 使用一个环形队列就行.具体参考 demo 代码.
直接使用Guava提供的RateLimiter实现.
RateLimiter的原理参考: http://xiaobaoqiu.github.io/blog/2015/07/02/ratelimiter/
]]>之前组内遇到的一个 ShallowEtagHeaderFilter 导致流式导出失效的问题, 具体的问题分析不是我做的, 这里记录下问题及原理.
现象就是一个数据导出接口, 用户的频繁调用导致线上内存吃满, 进而频繁 GC 导致 CPU 过高最终影响用户请求.
先交代一下导出接口的实现方式, 考虑到导出数据可能比较大导致撑爆内存, 数据导出是流式导出的.
默认情况下, 我们的select从服务器一次取出所有数据放在客户端内存中, 当一条SQL返回数据量较大时可能会出现OOM.
这里首先涉及到Mysql如何配置流式读取数据, 涉及到两个配置(第一个配置很重要很容易被忘记)
1.Mysql连接串中配置上参数useCursorFetch=true;
2.Sql中配置上FetchSize等参数
下面以MyBatis为例子, 通过设置Mapper文件中select的属性来启用这一特性
1 2 3 |
|
然后, 配合 ResultHandler 就可以实现逐行流式处理数据库中的记录.
我们就是采用这种方式, 流式获取数据写到 Response 的输出流中.
在描述问题之前, 我们先普及一下 Http 请求中 Content-Length 和 Transfer-Encoding 的知识.
Content-Length 用于描述HTTP消息实体的传输长度(the transfer-length of the message-body), 需要注意消息实体长度和消息实体传输长度是有区别, 比如说gzip压缩下, 消息实体长度是压缩前的长度, 消息实体的传输长度是gzip压缩后的长度.
通过读取 Content-Length 这个信息, 服务器/客户端可以预先知道数据量的大小, 以便预先分配空间.
Transfer-Encoding 所描述的是消息请求(request)和响应(response)所附带的实体对象(entity)的传输形式. 可选值有: chunked 和 identity, 其中chunked指把要发送传输的数据切割成一系列的块数据传输, identity指传输时不做任何处理, 自身的本质数据形式传输. 举个例子, 如果我们要传输一本小说到服务器, chunked方式就会先把这本小说分成一章一章的, 然后逐个章节上传, 而identity方式则是从小说的第一个字按顺序传输到最后一个字结束.
HTTP/1.1 规定, 所有服务器必须支持 Chunked 类型的 Transfer-Encoding, 通过这个头部, 服务器不必在返回回应报文的时候预先知道返回 Body 的大小, 而是将数据分割到一个一个的 Chunk 中(每个 Chunk 通过特定的头部和尾部区分, 最后一个 Chunk 大小为 0, 以表示报文结束), 然后持续不断地写入 TCP 流, 通过这个机制, 可以实现服务器数据到客户端的流式输出.
注意关于 Content-Length 和 Transfer-Encoding 一个准则: 有了Transfer-Encoding, 则不能有Content-Length
参考: https://tools.ietf.org/html/rfc2616#section-4.4. http://www.cnblogs.com/jcli/archive/2012/10/19/2730440.html http://blog.csdn.net/pud_zha/article/details/8809878
按照我们上面的分析, 我们的 Response 的 Transfer-Encoding 肯定是 chunked, 并且没有设置 Content-Length.但是我们发现请求的 Response 中存在 Content-Length 字段.
我们将请求的 Response 分为三个阶段: (1).从数据库读取数据; (2).服务器端整理数据发往浏览器端; (3).浏览器端接受数据;
现在已知从 Mysql 中读取数据是流式的, 而浏览器接收到的数据不是流式的, 因此问题审核出在服务器端.
最终定位到的罪魁祸首是一个 ShallowEtagHeaderFilter 的 Filter.
ShallowEtagHeaderFilter 继承了 OncePerRequestFilter, 我们可以分析一下 ShallowEtagHeaderFilter 的主要代码逻辑:
1 2 3 4 5 6 7 8 9 10 |
|
看一下包装 ShallowEtagResponseWrapper 做了什么: (其实看注释就已经知道, 将所有写到 output stream 和 writer 的内容缓冲起来放到, 并提供 oByteArray() 方法用于读取缓冲的数据):
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 |
|
最后我们看看 updateResponse 做了什么事:
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 |
|
总结 ShallowEtagHeaderFilter 作用: 1.ShallowEtagHeaderFilter 缓冲了写到 response 的所有数据; 2.如果请求的 If-None-Match 头部信息和当前根据内容生成的 ETag 相同,则请求为 304; 3.最后 ShallowEtagHeaderFilter 将缓冲的所有内容一次性写到 response 中;
需要注意的是, 使用这个 ShallowEtagHeaderFilter, 服务端的性能并没有得到提高, 因为请求逻辑还是执行了, 只是没往客户端发送, 因此节省了一定的网络带宽.
另外, 这个 ShallowEtagHeaderFilter 会让 Transfer-Encoding=Chunked 失效(我们这里就是这种情况).
根据上面的分析, 我们这里的问题就很好解释了, ShallowEtagHeaderFilter 将我们的流式写缓冲起来, 最后一次性写给客户端并且设置上了 Content-Length 字段.
关于这个问题, 其实很多人都遇到坑了, 有人给 Spring 提 Bug 了, 但是官网认为只是误用(我赞同这种观点). 参考: https://jira.spring.io/browse/SPR-10855
解决很简单, 去掉 ShallowEtagHeaderFilter 这个过滤器, 或者给 ShallowEtagHeaderFilter 配置 url pattern 的时候排除掉流式的接口.
]]>用户任务是审核流程中最重要的节点, 在流程设计中通常会遇到需要自动流转的需求, 比如请假半天的自动通过.
下面是我能想到的几种实现自动流转任务方式.
Activiti的设计者已经考虑到我们这个需求,因此在引入了 ServiceTask 这中类型的任务.参考 Activiti 的文档: https://www.activiti.org/userguide/index.html#bpmnJavaServiceTask ServiceTask的目的是调用我们自定义的一个Java类:
1
|
|
ServiceTask 的使用方式也有几种:
1. 自定义类实现 JavaDelegate 或者 ActivityBehavior 接口;
2. 设计流程的时候, 显示的告诉 Activiti 引擎需要调用的 Bean;
3. 调用一个方法表达式;
4. 调用一个求值表达式;
下面审核一个示例的自定义ServiceTask调研逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
回到我们的需求上, 要实现我们需要的自动流转任务, 我们可以使用 ServiceTask, 以上面定义的 AutoCompleteTaskService 为例子:
1 2 3 4 5 6 7 8 |
|
但是这种实现方式的一个致命的问题在于:流程设计的时候你需要知道你要调用的Java逻辑的类路径或者Bean的名称.
为什么说是致命的呢,因为这个的实现方式就没办法将流程设计交付给开发人员以外的其他人员了(他们不可能知道类路径或者bean名称这种信息).
下面我使用普通的 UserTask 来实现我们的需求.
我们将自动流转任务也设计成UserTask, 由业务自己主动触发.
比如请假半天的自动通过的例子, 业务可以在提交请假的时候判断, 让这个任务自动完成.
这种实现的问题在于: 业务逻辑实现起来太麻烦, 很容易出Bug(比如自动通过失败,可能会导致提交这个操作失败).
在上一个方法的基础上, 我们可以将自动生效的逻辑放到异步任务里面,可以避免将原始业务逻辑复杂化.
但是我们需要筛选出哪些 UserTask 可以自动完成,因此我的方案就是给这些任务制定特定的任务类型.关于任务类型, 可以参考我前面的博文: http://xiaobaoqiu.github.io/blog/2016/12/29/activitiren-wu-zeng-jia-shu-xing/
有了任务类型之后, 我们就可以将哪些任务需要自动流转配置起来.
这个方案的问题在于(绝大部分情况下不是问题): 这里实现的自动生效的不是实时的(即使我们的异步任务足够频繁).
下面是几种实现方式的对比:
成本 | ServiceTask | 主动触发UserTask | 异步触发UserTask |
---|---|---|---|
设计成本 | 需要知道具体的执行类或者Bean | ||
触发成本 | 需要业务主动触发, 逻辑复杂 | ||
配置成本 | 需要配置:任务类型 - 是否自动完成任务 |
倾向于使用 异步触发UserTask 这个方案, 成本和问题都比较小.
]]>问题:Spring上传组件的冲突 背景:同事在原有的上传模块中增加了一个新的文件接口,导致就的上传接口不可用的故障。
新增的配置就是Spring自带的上传解析器:
1 2 3 |
|
示例代码如下:
1 2 3 |
|
原始的上传使用的是 commons-fileupload 组件,简单示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
首先我们从Spring处理请求的源头开始分析,看看Spring是如何支持文件上传的。
1.DispatcherServlet中doDispatch方法中,如果请求是文件上传,会首先将 HttpServletRequest 请求做预处理,转换为 MultipartHttpServletRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这块两个问题:
1.如何判定一个请求是不是文件上传请求
2.转换为 MultipartHttpServletRequest 做了什么事
先看如何判定审核文件上传请求,就是 multipartResolver.isMultipart 的代码,跟代码会涉及到 CommonsMultipartResolver, ServletFileUpload, FileUploadBase,发现判定有两个条件
(1). 请求是 Post 请求. 参见 ServletFileUpload.isMultipartContent()
(2).请求 contentType 必须以 multipart/ 开通. 参见 FileUploadBase.isMultipartContent()
下面看第二个问题,转换 MultipartHttpServletRequest 做的什么事,这个工作由 MultipartResolver.resolveMultipart 完成,我们可以看看注释:
/**
* Parse the given HTTP request into multipart files and parameters,
* and wrap the request inside a
* {@link org.springframework.web.multipart.MultipartHttpServletRequest} object
* that provides access to file descriptors and makes contained
* parameters accessible via the standard ServletRequest methods.
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
其主要的代码实现在,主要的工作是, 首先将HttpServletRequest的inputStream最终塞入了FileItemStreamImpl的stream中,随后ServletFileUpload类逐个对FileItemStream进行处理(生成FileItem),通过Streams.copy方法对inputStream进行read操作,此时request中的inputStream被消耗(inputStream只能被读取一次), 最后将List
public List<FileItem> parseRequest(RequestContext ctx) throws FileUploadException {
List<FileItem> items = new ArrayList<FileItem>();
FileItemIterator iter = getItemIterator(ctx); //1. 构造FileItemStreamImpl
...
while (iter.hasNext()) { //2.逐个对FileItemStream进行处理
final FileItemStream item = iter.next();
...
FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName); //2.1 构造一个 FileItem
items.add(fileItem);
...
Streams.copy(item.openStream(), fileItem.getOutputStream(), true); //2.2 数据读取
}
return items; // 3. 返回
}
// 下面是更详细的代码
//1.将HttpServletRequest的inputStream最终塞入了FileItemStreamImpl的stream中
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
InputStream input = ctx.getInputStream();
...
multi = new MultipartStream(input, boundary, notifier);
...
findNextItem();
}
//2.2 数据读取,看看如何将 request 的 inputstream 数据拷贝到 FileItem 中
/**
* Copies the contents of the given {@link InputStream} to the given {@link OutputStream}.
*/
public static long copy(InputStream inputStream, OutputStream outputStream, boolean closeOutputStream)
throws IOException {
return copy(inputStream, outputStream, closeOutputStream, new byte[DEFAULT_BUFFER_SIZE]);
}
主要代码的位置:CommonsMultipartResolver.parseRequest, FileUploadBase.parseRequest, FileItemIteratorImpl
一个需要注意的点,在DispatcherServlet 中, MultipartResolver 的Bean的名称是写死的:
/** Well-known name for the MultipartResolver object in the bean factory for this namespace. */
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
private void initMultipartResolver(ApplicationContext context) {
try {
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); //bean的name固定
if (logger.isDebugEnabled()) {
logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
}
}
}
multipartResolver是一个全局的文件上传处理器,配置上 multipartResolver 这个Bean之后,全局的文件上传都会经过 multipartResolver 处理(读取并解析request的 inputstream ).而 inputstream 仅能处理一次,导致处理完的 HttpServletRequest 中的 inputStream 已经没有内容.
因此后面配置使用的 commons-fileupload 的 ServletFileUpload 无法从 request 中解析出文件上传内容.
几个解决方案:
1.全局统一使用一个解析方式(统一使用 ServletFileUpload 或者 MultipartResolver 方式);
2.继承 CommonsMultipartResolver 实现自定义的 MultipartResolver, 覆写isMultipart方法, 仅部分 url 的上传请求走我们自定义的 MultipartResolver 处理器,保证新老逻辑兼容;
]]>Activiti 内部充斥着各种各样的事件,详见:org.activiti.engine.delegate.event.ActivitiEventType的定义。
在 Activiti 5.15 以前,处理事件需要在流程设计的时候显示的加入各种Listener,比如:
1 2 3 4 5 6 7 |
|
Spring会注入 taskCreateListener 这个bean来作为 leaderVerifyTask 这个任务的创建事件响应逻辑。 这种方式的问题在于,流程设计的时候需要知道 代码中具体的类或者bean的名称,因此无法做到将流程的设计交与开发人员之外的人来做。
在 Activiti 5.15 中加入了全局的事件监听器 ActivitiEventListener,我们只需要实现一个自己的全局的事件处理器,然后自定义各种事件的处理器。
/**
* 流程服务的全局 Listener
*
* @author xiaobaoqiu Date: 16-12-19 Time: 下午7:16
*/
public class GlobalProcessEventListener implements ActivitiEventListener {
/**
* 各类 Event 的处理器
* 其中事件类型参见 ActivitiEventType 定义
*
* @see org.activiti.engine.delegate.event.ActivitiEventType
*/
private Map<ActivitiEventType, ProcessEventHandler> handlers = new HashMap<>();
@Override
public void onEvent(ActivitiEvent event) {
ProcessEventHandler handler = handlers.get(event.getType());
if (handler != null) {
handler.handle(event);
}
}
@Override
public boolean isFailOnException() {
return false;
}
public Map<ActivitiEventType, ProcessEventHandler> getHandlers() {
return handlers;
}
public void setHandlers(Map<ActivitiEventType, ProcessEventHandler> handlers) {
this.handlers = handlers;
}
}
/**
* 流程事件处理
*
* @author xiaobaoqiu Date: 16-12-21 Time: 下午1:33
*/
public interface ProcessEventHandler {
void handle(ActivitiEvent event);
}
ActivitiEventListener能做到和流程设计无关,只需要在配置 Activiti 的时候将其注入,并且可以在其中配置能处理哪些类型的事件:
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 |
|
参考: https://www.activiti.org/userguide/index.html#eventDispatcherConfiguration
需要注意的一点是 GlobalProcessEventListener 中的异常处理,我们在实现 GlobalProcessEventListener 的时候,ActivitiEventListener有一个接口需要实现:
1 2 3 4 5 |
|
即当 listener 抛出一个异常的时候,当前操作时序应该失败(其实就是是否需要吞掉 listener 的异常).
我们可以看看 Activiti 相关的源代码,在 ActivitiEventSupport 中的 dispatchEvent 函数,我们可以清晰的看到,listener.isFailOnException()为false则会讲异常吞掉,只是打一行日志.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
我的建议(也是我现在的做法)是:isFailOnException()设置为true,然后在对应的 ProcessEventHandler 自己处理需要吞掉异常的场景.
通常而言,我们的 ProcessEventHandler 需要使用到 Activiti 的Service,比如我的一个简单场景,我需要在 UserTask 获取一些业务信息,新词我需要使用 RuntimeService 来查询一些信息
1 2 3 4 5 6 7 8 9 10 11 |
|
按照第一节的写法,就会出现Bean循环依赖的问题:
1 2 3 4 5 |
|
并且 ProcessEngineFactoryBean 是 Activiti 自己实现的 FactoryBean,用来获得 ProcessEngine 实例 :
1 2 3 |
|
这个问题一个比较好的解法是, 将 GlobalProcessEventListener 的注册延后到 GlobalProcessEventListener 初始化完成之后,因此涉及到两个问题:
1.GlobalProcessEventListener 什么时机去做自己注册;
2.GlobalProcessEventListener 如何将自己注册到 SpringProcessEngineConfiguration 中;
其中第一个问题很好回答, GlobalProcessEventListener 自己初始化完成之后去自注册,这个时候依赖的各种Bean已经创建初始化完成了.因此只需要实现 ApplicationContextAware 接口,然后在 setApplicationContext 中做注册的工作.
1 2 3 4 5 6 7 8 9 |
|
第二个问题,如何自己注册上去,这个看看源代码就会找到方法, 我们在配置文件中给 SpringProcessEngineConfiguration 配置 EventListener 调用的其实是 setEventListeners 方法
1 2 3 |
|
我们看远吗发现其中使用 eventListeners 的只有一个地方,就是将其赋值给 ActivitiEventDispatcher
1 2 3 4 5 |
|
而 ActivitiEventDispatcher 才是使用 EventListener 的地方.我们再看源码发现 ActivitiEventDispatcher 提供了 addEventListener 的入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
并且 SpringProcessEngineConfiguration 有 getEventDispatcher 来获得 ActivitiEventDispatcher 实例,因此我们可以通过 getEventDispatcher 来拿到 ActivitiEventDispatcher 实例,然后将当前的 EventListener 通过 addEventListener 的方法增加到 ActivitiEventDispatcher 的 EventListener 列表中.
因此,最终的实现代码很简单:
1 2 3 4 5 6 7 8 |
|
最近逐步将组内的审核业务迁移到 Activiti 上,为了适配已有业务,需要原生的 Activiti 的用户任务上增加一些属性,比如用户任务(UserTask)增加类型属性等。 Activiti版本为:5.22.0
下面以增加任务类型为例子,记录修改点:
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 |
|
在自己的API中定义任务类型,然后我们可以修改 activiti-modeler 的相关代码,使得Activiti支持画图时候指定任务类型。
]]>Activiti 中表定义 activiti-engine 这个 module 下,具体位置 /resources/org.activiti/db/create 下,包括各种类型数据库的建表语句,这里我们以Mysql为DB。
首先简单介绍表的前缀的含义,参考官网:http://www.activiti.org/userguide/index.html#database.tables.explained
表名称包含三个部分,第一个为所有的表共有的ACT_表示Activiti,第二部分表示表的使用场景
1 2 3 4 5 6 7 |
|
下面是各个表的大致意义:
表 | 意义 | 备注 |
---|---|---|
ACT_EVT_LOG | 事件处理日志 | |
ACT_GE_BYTEARRAY | 二进制数据表 | 保存流程定义图片和xml |
ACT_GE_PROPERTY | 属性数据表存储整个流程引擎级别的数据 | |
ACT_HI_ACTINST | 历史节点表 | 只记录usertask内容 |
ACT_HI_ATTACHMENT | 历史附件表 | |
ACT_HI_COMMENT | 历史意见表 | |
ACT_HI_DETAIL | 历史详情表,提供历史变量的查询 | 流程中产生的变量详细,包括控制流程流转的变量等 |
ACT_HI_IDENTITYLINK | 历史流程人员表 | |
ACT_HI_PROCINST | 历史流程实例表 | |
ACT_HI_TASKINST | 历史任务实例表 | |
ACT_HI_VARINST | 历史变量表 | |
ACT_ID_GROUP | 用户组信息表 | |
ACT_ID_INFO | 用户扩展信息表 | |
ACT_ID_MEMBERSHIP | 用户与用户组对应信息表 | |
ACT_ID_USER | 用户信息表 | |
ACT_PROCDEF_INFO | ||
ACT_RE_DEPLOYMENT | 部署信息表 | |
ACT_RE_MODEL | 流程设计模型部署表 | 流程设计器设计流程后,保存数据到该表 |
ACT_RE_PROCDEF | 流程定义数据表 | |
ACT_RU_EVENT_SUBSCR | throwEvent,catchEvent时间监听信息表 | |
ACT_RU_EXECUTION | 运行时流程执行实例表 | |
ACT_RU_IDENTITYLINK | 运行时流程人员表,主要存储任务节点与参与者的相关信息 | |
ACT_RU_JOB | 运行时定时任务数据表 | |
ACT_RU_TASK | 运行时任务节点表 | |
ACT_RU_VARIABLE | 运行时流程变量数据表 |
参考:http://craft6.cn/detail/activiti_research_database.do http://blog.csdn.net/flygoa/article/details/51895228
]]>本文基于JDK 1.7
1 2 3 4 |
|
Java其实就是char数组的一个包装,String真实的内容就是存储在char数组中,数组的一个特性就是在逻辑和存储上都是连续的、可随机访问的。
1 2 |
|
String的操作其实都是围绕底层的char数组来的,比如trim实现,trim的其实是value的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
需要注意的是,String中的很多实现都是使用 System.arraycopy 来实现数组数据的快速拷贝,System.arraycopy是一个native方法:
1 2 3 4 5 |
|
String基本约定中最重要的一条是Immutable,Immutable的意思就是对String的每次修改都是生成一个新的String,原始对象不受影响,类似于copy-on-write的思想。
先具体看一下什么是Immutable。String不可变很简单,如下图,给一个已有字符串"abcd",第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
那String怎么做到不可变?主要通过三个手段 (1).String类声明为final(不会被继承改掉这种Immutable特性); (2).底层的value数组声明为final(value这个引用地址不可变); (3).所有String的方法里很小心的没有去动value数组里的元素,没有暴露内部成员字段;
其实String是不可变的关键都在底层的实现,而不是一个final修饰符。
不可变对象带来的最大好处就是线程安全,因为不会被改写。
String内部属性除了value数组外,还有一个 hash,用于记录String的hash值。 String能这样讲hash值cache下来的原因就是上面提到的
1 2 |
|
每次调用hashCode,先检测 hash 是否已经计算好了,如果计算了,就不要重新计算。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
String中有一个hash32()的方法,它的结果被缓存在成员变量int hash32 中。
1 2 3 4 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
关于 hash32 为什么存在,可以看下面官网的说明:
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 |
|
简单说就是为了解决 HashMap 这种hash 碰撞严重时候的性能,因为HashMap使用链地址法解决Hash碰撞,当链很长的时候,性能会受影响。 但是这个问题在Java 8 中已经解决了,因为Java 8中引入一个新的策略,当链比较长的时候(默认阈值10),会转化为采用红黑树实现后面的链表,可以讲时间从O(N)降到O(lgN).
参考: http://stackoverflow.com/questions/23716836/what-is-use-of-hash32-in-string-class http://docs.oracle.com/javase/7/docs/technotes/guides/collections/changes7.html http://stackoverflow.com/questions/23926312/difference-between-hash32-and-hash-in-java-string-object http://stackoverflow.com/questions/23926312/difference-between-hash32-and-hash-in-java-string-object/23926405#23926405
hash32这个方法最大的变化是,同一个字符串在不同的JVM上执行hash32()的结果可能不同(确切的说,多数情况下都会不同,因为其内部分别调用了一次System.currentTimeMillis()和两次System.nanoTime()用于初始化seed)。因此,某些容器在每次运行程序时迭代顺序都不同。
在String中,有一个护额略大小写的比较方法,它的实现就是我们这里要说的CaseInsensitiveComparator
1 2 3 4 5 |
|
CaseInsensitiveComparator的实现如下:
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 |
|
我们发现比较时候需要现将字符转化为大小比较,再转化为小写比较。
]]>1.使用了1年的Crate,还没有比较全面的看Crate文档,很多特性肯定还不知道;
2.Crate被越来越多的人使用,中文文档肯定ui别人有帮助;
在 github 上开了个项目(其实很早就建工程了,一直没行动,也想通过这种方式驱动自己:先把牛B吹出去…):
https://github.com/xiaobaoqiu/crate_doc_cn.git
主要分三大块:
1.Crate Server相关文档;
2.Crate Client相关文档;
3.Crate其他文档;
欢迎监督和参与。
]]>最近线上业务报了几次死锁问题,决定跟进。
有一个业务数据表business,维护了一个名为c_id的外键,一个c_id对应多个business数据。
在业务数据新增或者修改的时候,需要同步的维护 business 的数据,这时候正确的做法是diff新旧数据,得到需要删除的一部分数据,需要新增的一部分数据以及需要更新的一部分数据,这种实现有点麻烦(其实也不麻烦,使用Guava的集合操作),因此工程师们的通常做法是先根据c_id删除现有数据,再插入新数据。这个时候很容易出现死锁。
这里也解释一下外键,在业务DB中,出于性能考虑,通常禁止使用外键,通常的做法是,外键这种关系的维护都体现在表中手动维护一个外键。
在交代一下数据库相关的背景:
1 2 |
|
我们在本地重现死锁信息。
建表语句已经初始化的数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
我们模拟同时两个新数据的插入过程:
步骤 | 事务1 | 事务2 | 备注 |
---|---|---|---|
1 | begin | ||
2 | begin | ||
3 | mysql> delete from business where c_id = 6; Query OK, 0 rows affected (0.00 sec) |
事务1:Gap锁,锁住区域(5, +∞) | |
4 | mysql> delete from business where c_id = 7; Query OK, 0 rows affected (0.00 sec) |
事务2:Gap锁,锁住区域(5, +∞) | |
5 | mysql> insert into business (c_id, business_id) values (6, 1); 等待… |
事务1:插入意向锁(Insert Intention Lock),期望获取(5, 6)这个Gap锁和一个c_id=6的Recored锁。 但是因为 事务2 已经锁住了区域(5, +∞)因此这时候,事务1只能等待 事务2 释放锁. |
|
6 | mysql> insert into business (c_id, business_id) values (7, 1); ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
和上面第5步类似, 事务2:等待获取事务1的锁 出现循环等待,死锁(roll back这个事务,事务2的锁释放) |
|
7 | Query OK, 2 rows affected (2.89 sec) Records: 2 Duplicates: 0 Warnings: 0 事务2 rollback了,事务1的insert成功 |
事务1等待的锁得到,事务1成功。 |
第 5 步的锁信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
有些人可能迷惑 lock_data 的 supremum pseudo-record 是什么东西,我们先看看 lock_data 的解释,这里面解释了 supremum pseudo-record,简单说就是正无穷。
1 2 3 4 5 |
|
死锁信息:
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 |
|
从死锁信息中,我们也可以看到事务1(事务id:203797) 和 事务2(事务id:203798) 持有的锁是锁住相同的一块区域:
1
|
|
有两个点需要我们知道的信息:各种SQL语句都加什么锁,为什么这里的两个delete的锁没有冲突。
关于各种SQL语句加什么锁,参见Mysql官方文档:Locks Set by Different SQL Statements in InnoDB
我们这里来说涉及的删除和插入,先说删除:
1 2 3 4 5 |
|
再说插入,插入比较麻烦,因为涉及到插入意向锁(Insert Intention Lock),还是参考Mysql官方文档:InnoDB Locking
1 2 3 |
|
简单的是说,插入意向锁可以归结为如下:
1 2 3 4 |
|
插入的锁如下:
1 2 3 |
|
就是说插入之前会加一把插入意向锁,除此之外,会在插入的记录上加一把锁。
关于锁冲突,我们熟知的肯定是S和S兼容,X和其他所有都不兼容。事实上并没有这么简单。比如我们这里前面的例子,两个delete都加了X型的Gap锁,应该排斥才对,但事实上是兼容的。这里参考了从一个死锁看mysql innodb的锁机制这篇文章的结论(准备读源码验证):
1 2 3 4 5 6 7 8 9 10 |
|
其实仔细读Mysql官方文档,我们也能发现上面的两个delete的Gap锁是兼容的:
1 2 |
|
意思就是说Gap锁的作用是只防止其他事务在这个Gap内的插入,而不排斥其他事务在同一个Gap加上Gap锁。因此Gap X锁和Gap S锁效果相同。 (真心文档每句话都需要仔细理解哈。)
DBA的建议:先根据 c_id 查询id,根据 id 删除;
其实只要保证数据存在再区删除就没问题,我们假设我们执行两个以存在数据的先删除再插入。
一个辅助的示意图如下:
其实上面的例子中会出现一个因为 UNIQUE KEY 导致的锁等待问题,我们可以重现,现有数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
过程如下:
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | | |
2 | begin | | |
3 | mysql> delete from business where c_id = 3; Query OK, 0 rows affected (0.00 sec) |
|
4 | mysql> delete from business where c_id = 4; Query OK, 0 rows affected (0.00 sec) |
|
5 | mysql> insert into business (c_id, business_id) values (3, 1); 等待… |
INNODB_LOCKS信息及INNODB_LOCK_WAITS信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
show engine innodb status信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
这里没明白的一点好事务1(事务id:204349)的insert一个(3, 1)的数据为什么会在(4, 1)上加一个S锁?
不过实验了一下,去掉UNIQUE KEY,使用普通的key,就没有这个锁等到问题,所以这个锁是因为UNIQUE KEY引发的。 这个问题有待进一步深入查资料。
http://songuooo.com/2015/1/7/deadlock-detected-on-concurrent-insert
]]>Spring的AOP(Aspect Oriented Programming,面向切面编程)是一个强大的机制,常用场景在日志、安全、异常处理、事务等。Spring AOP的原理正是基于代理。
我想了解的问题包括:
(1).Spring AOP使用什么机制做代理;
(2).Spring 怎么使用代理达到AOP目的;
先看看Spring是如何选择代理,先看看 Spring官方AOP文档 的说明:
1 2 3 |
|
官方文档的结论是:当业务Bean实现了一个或多个接口的时候,默认使用Java动态代理,当业务Bean没有实现任何接口的时候使用CGLib。
我们来看看相关代码,Spring使用AopProxy表示AOP中的代理,它的作用只要一个:创建代理对象。它有两个实现类:JdkDynamicAopProxy 和 CglibAopProxy :
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 |
|
AopProxyFactory的默认实现类DefaultAopProxyFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
其中 AdvisedSupport 是 AOP配置管理的基类。这几个配置的说明可以参见 ProxyConfig 类。其中我注释的地方和官方文档是一致的。
得到代理对象之后的下一步就是调用代理逻辑,这部分实现直接看 JdkDynamicAopProxy.invoke() 代码:
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 |
|
先看看如何获取方法上的 MethodInterceptor,AdvisedSupport其实只是对这个做缓存,实际调用 DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice:
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 |
|
下面看看这些 Interceptor 是如何起作用的,看 ReflectiveMethodInvocation 的 proceed() 方法逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
这里其实完全参考Spring AOP官方文档的一节就行了:http://docs.spring.io/spring/docs/3.0.x/reference/aop.html#aop-understanding-aop-proxies
看懂了这一节,能很明白为什么内部调用的方法上的事务注解无效,我们日常犯的错误基本不会有了。
我们都是通过XML文件的形式使用Spring,Spring使用XmlBeanDefinitionReader类来读取解析XML文件,XmlBeanDefinitionReader使用DefaultBeanDefinitionDocumentReader,再调用BeanDefinitionParserDelegate,其中会根据配置的不同找到对应的NamespaceHandler来处理对应的标签。NamespaceHandler是一个接口,一个具体接口就是解析XML中的一个Element得到一个Spring容器中的一个Bean。
1 2 3 |
|
NamespaceHandler的具体实现类很多,其中几个我们感兴趣的AopNamespaceHandler,MvcNamespaceHandler,QScheduleNamespaceHandler,QConfigNamespaceHandler,QmqClientNamespaceHandler等,看到这里大家应该很熟悉了。我们来看两个实现吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
就是说Spring将配置根据作用分成不同的namespace,每个NamespaceHandler只解析自己独有的几个配置,比如AopNamespaceHandler负责解析scoped-proxy等几个配置,比如我们在配置文件配上:
1 2 |
|
最后就会找到AopNamespaceHandler类解析这个配置。那出了这些带namespace的配置,正常的配置谁来解析?DefaultBeanDefinitionDocumentReader。
spring在解析的过程中,会去收集spring.*.jar/META-INF下的spring.handlers,spring.schemas文件,这2个文件就是指明了解析spring中自定义标签的Namespace类。如果自己开发Spring组件,需要增加新的标签,也可以按照这个机制。我司的同学可以看看qmq-client包下的META-INF下的这两个文件。
到目前为止,大致知道了谁来解析AOP相关的标签。我们继续看看 AopNamespaceHandler 的代码,它的init()的方法就是给每个标签找一个解析器,比如 aop:config 这个配置就会找到 ConfigBeanDefinitionParser 这个解析器。,些解析器只有一个parse入口函数。
最后放一张AOP相关的图(来源:http://blog.csdn.net/moreevan/article/details/11977115)
最后我们整理一下Spring使用代理实现AOP整个流程的步骤。
1.我们在代码中使用引入bean; 2.Spring从BeanFactory总获取bean(ApplicationContext.getBean),参见AbstractBeanFactory.getBean代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
3.触发Bean的创建,参考AbstractAutowireCapableBeanFactory.createBean代码:
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 |
|
1 2 |
|
AbstractAutoProxyCreator代码:
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 |
|
ProxyFactory.java:
1 2 3 4 5 6 7 8 9 |
|
DefaultAopProxyFactory.java的createAopProxy,回到了我们 1.1 代理类型选择 中涉及的内容,整个过程串起来了。
]]>我们使用事务的目标:
1
|
|
我的理解Spring事务中包括的几个主要要素,下面会一一讲解:
1 2 3 |
|
关于事务的使用是最简单的,通常分为 编程式事务 和 注解式事务:
代码直接使用PlatformTransactionManager,因此需要使用者自己管理事务的创建,提交,回滚,挂起等逻辑。
优点:灵活度大; 缺点:使用难度大,易出错,业务代码侵入大;
我们可以使用 Spring 提供的 TransactionTemplate 类来简化编程事务:
1 2 3 4 5 6 7 8 9 10 |
|
我们可以看一下 TransactionTemplate 的实现代码,TransactionTemplate继承了DefaultTransactionDefinition(实现了TransactionDefinition),使用TransactionDefinition需要我们给定一个TransactionManager,可以修改 事务隔离级别,事务传播行为等事务属性(其实是DefaultTransactionDefinition提供的功能)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
其中 transactionManager.getTransaction(this) 的代码后面会详细分析。
使用 @Transactional 来管理事务,我们需要声明一个全局的事务管理器,以及事务对注解的支持:
1 2 3 4 5 |
|
优点:使用简单,业务代码侵入小; 缺点:理解有难度;
注意:Spring建议在具体业务实现类上使用Transactional注解,而不是在接口或则接口的方法上配置@Transactional注解:
1 2 3 4 |
|
Transactional注解上可以设置很多事务相关的参数:
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 |
|
如何定义一个事务,Spring中使用 TransactionDefinition 来定义一个事务的属性,使用接口的形式:
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 |
|
TransactionDefinition 有一个默认实现 DefaultTransactionDefinition,其中定义了事务几个属性的默认值:
1 2 3 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 34 |
|
其中 PROPAGATION_NESTED 理解有点困难,可以参考 Spring 的文档:
1 2 3 |
|
大致意思就是关于 SavePoint,JDBC定义了java.sql.SavePoint接口,提供在一个更细粒度的事务控制机制。当设置了一个保存点后,可以rollback到该保存点处的状态,而不是rollback整个事务。Connection接口的setSavepoint和releaseSavepoint方法可以设置和释放保存点。 而Spring中定义了 SavepointManager,用于 SavePoint 的创建(createSavepoint),回滚(rollbackToSavepoint),释放(releaseSavepoint)。
关于事务的隔离级别,也简单介绍一下,除了 ISOLATION_DEFAULT ,其余均使用 java.sql.Connection 中的隔离级别的定义:
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 |
|
事务管理 Spring 使用 PlatformTransactionManager 定义,其中包含一个事务的三个主要操作,在业务开始之前开始事务,执行业务逻辑之后提交事务,业务逻辑异常则回滚事务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
我们熟悉的 JDBC 支持的事务管理器 DataSourceTransactionManager 就是其一个实现,其他还包括 JtaTransactionManager 等。
同时,Spring提供了一个 TransactionStatus 的就看便于再代码中控制事务以及获取事物状态。在PlatformTransactionManager中获取事务的返回值就是一个 TransactionStatus 对象。
下面会讲解我们使用的 Transactional 注解的工作原理,即当我们在Service的某个public方法上加上 @Transactional之后,从编译到调用这个方法整个过程中会发生什么,事务再其中如何起作用。
这个好像没什么讲的,就是AOP的实现原理。简单的说就是创建Bean的时候会创建一个代理Bean,使用AOP实现事务,参考上一届的AOP实现原理。我们可以参考 TransactionInterceptor 的代码。
主要关注Spring事务的回滚,其实主要就是要搞明白Spring在什么情况下会回滚。
事务的坑 http://www.ibm.com/developerworks/library/j-ts1/
代理实现选择器: AdviceMode TransactionManagementConfigurationSelector
声明事务切面实现: TransactionInterceptor
事务实现 Connection关闭自动提交 ConnectionHolder来保持Connection
参考: http://openwares.net/java/spring_mybatis_transaction.html http://my.oschina.net/guanzhenxing/blog/214228 http://www.mybatis.org/spring/zh/transactions.html http://tracylihui.github.io/2015/07/28/spring/Spring%E5%A3%B0%E6%98%8E%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%AE%A1%E7%90%86/ http://www.cnblogs.com/davenkin/archive/2013/02/16/java-tranaction-1.html http://www.importnew.com/12300.html 详细介绍Spring事务管理: http://developer.51cto.com/art/200906/129854.htm spring事务原理1-事务的抽象: http://sqtds.github.io/2014/06/09/2014/spring-tx1/ Spring官方文档Transaction Management: http://docs.spring.io/autorepo/docs/spring/3.2.x/spring-framework-reference/html/transaction.html Spring官方文档Aspect Oriented Programming with Spring: http://docs.spring.io/spring/docs/3.0.x/reference/aop.html StackOverFlow Spring - @Transactional - What happens in background?: http://stackoverflow.com/questions/1099025/spring-transactional-what-happens-in-background Transaction strategies: Understanding transaction pitfalls: http://www.ibm.com/developerworks/library/j-ts1/ Annotation-based Transactions in Spring: http://springinpractice.com/2008/03/18/annotation-based-transactions-in-spring Design principle behind Transaction framework with Spring 3: http://stackoverflow.com/questions/11789857/design-principle-behind-transaction-framework-with-spring-3 Chapter 6. 使用Spring进行面向切面编程(AOP): http://shouce.jb51.net/spring/aop.html wiki: http://wiki.corp.qunar.com/display/~yushen.ma/Spring+3.x-JDBC+Transaction spring事务: http://wiki.corp.qunar.com/pages/viewpage.action?pageId=52084161 spring 事务: http://wiki.corp.qunar.com/pages/viewpage.action?pageId=93338632 aspectj事务: http://wiki.corp.qunar.com/pages/viewpage.action?pageId=92656954
Demo: 编程事务: http://www.tutorialspoint.com/spring/programmatic_management.htm 声明事务: http://www.tutorialspoint.com/spring/declarative_management.htm
@Transactional注解工作原理:http://blog.csdn.net/dslztx/article/details/46636079 分布式事务系列(1.2)Spring的事务体系: https://yq.aliyun.com/articles/39046 spring transaction源码分析–事务架构:http://www.cnblogs.com/davidwang456/p/4309038.html
http://blog.csdn.net/szwangdf/article/details/41516239
AopProxy DefaultAopProxyFactory
]]>最近在项目中涉及到工作流的知识,需要调研各个工作流引擎,顺道总结.
如何做流程服务,通常而言两种选择(在我的认知范围内的):
1.状态机
2.工作流
状态机通常意味着需要和业务状态绑定在一起,因此无法做到业务和流程分离,通常的后果就是业务和流程紧耦合.
因此,当我们的多个业务系统涉及到流程时候,可以考虑工作流.
先介绍几个术语:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
参考: BPMN2新规范与Activiti5 BPMN2.0规范
具体什么场景下适合使用工作流,参考前任的总结:
when-to-use-windows-workflow-foundation
1 2 3 4 5 |
|
我理解,注意这里的第三点说的流程可视化的目的并不是为了展示,而是说工作流提供了一种给非开发人员(如运营人员)在线修改流程并且流程实时生效的途径(当然,需要业务支持).
1 2 |
|
简单介绍BPMN中的基本元素,详细内容可以在BPMN 2.0规范官网下载PDF版本(章节7.2).包含5个基本元素,每个基本元素下面又包含集中类型的元素,其中Flow Objects是最重要的:
1 2 3 4 5 |
|
Flow Objects包含了工作流的三个基本执行语义
1 2 3 4 5 6 7 8 |
|
事件有三种类型:Start, Intermediate, End
1 2 3 4 |
|
连接对象用来把各个流对象或流对象与其他信息连接起来
1 2 3 4 5 6 7 8 9 10 11 |
|
使用泳道来区分不同部门或者不同参与者的功能和职责。
1 2 |
|
1 2 |
|
各种基本元素的示意图字节参考BPMN 2.0规范给出的图吧:
几个在线画图的应用如processon都支持BPMN图形.
参考: BPMN这点事-BPMN基本元素(上) BPMN这点事-BPMN基本元素(下)
Activiti官网 Activiti用户手册 Activiti 5.16 用户手册中文版
FixFlow国人开发的工作流引擎 FixFlow github地址
主要从易用性,维护成本等几个角度考虑:
1 2 3 4 |
|
目前来看,Activiti的使用是最简单方便的.
支持最新BPMN2.0规范的开源工作流引擎Activit5,实现了对规范的绝大多数图元的定义(包括一些自己的扩展定义),能够满足企业工作流的各种复杂应用. 它是一个无侵入的,支持嵌入式和独立部署的开源工作流引擎,是Tom Bayen离开jBoss加入Alfresco公司后的另立山头之作.
关于Activiti的一些关键特性:
1 2 3 4 5 6 7 8 9 |
|
几个关键的概念:
1 2 3 4 5 6 7 8 9 10 11 |
|
这里主要大致说一下怎么结合Spring使用Activiti
Maven依赖
1 2 3 4 5 6 7 8 9 10 |
|
其中activiti-spring主要是提供了SpringProcessEngineConfiguration这个配置入口,并且提供ProcessEngineFactoryBean这个流程引擎的Bean,通过这个Bean,我们就可以获取Activiti内部的各种Service:
1 2 3 4 5 6 7 |
|
activiti将其内部的表的建表语句和对应的Mybatis配置文件都打包在依赖jar包中,可以参考activiti-engine中的org.activiti.db这个包路径下的内容.
整合Acitiviti在线流程设计器 Activiti - 新一代的开源 BPM 引擎 http://www.infoq.com/cn/articles/bpmn2-activiti5
一般使用方式两种:
1 2 3 4 5 6 7 8 9 10 |
|
最近再项目中发现不少同事(包括自己)不太理解默认情况下的Spring事务的运行机制(主要是不理解AOP机制),导致随意使用事务注解.因此也在很多场景事务不生效。因此想从代理机制开始理一下整个Spring声明式事务的原理。
因为篇幅太长,分成三个部分:
(1).代理
(2).Spring的AOP
(3).Spring的事务
通常,我们代码中处理核心的业务逻辑,还包含一些枝节性的代码和功能,比如日志记录、消息发送、安全、事务保证和监控等。
我们通常期望将这些枝节性的代码功能和我们的核心业务逻辑分离,减少业务功能和枝节功能的耦合。这时候我们就要使用代理来达到我们的目的。
代理的作用是:为其它对象提供一种代理以控制对这个对象的访问。简单理解就是中间的作用。代理一般涉及到三个角色:
(1).抽象角色:声明真实对象和代理对象的共同接口;
(2).代理角色:代理对象内部包含有真实角色的引用,从而可以操作真实角色,同时代理对象与真实对象有相同的接口,能在任何时候代替真实对象,同时代理对象可以在执行真实对 象前后加入特定的逻辑以实现功能的扩展;
(3).真实角色:代理角色所代表的真实对象,是我们最终要引用的对象;
代理模式是一个经典的设计模式,介绍往上很多。
下面是一个最简单的实现:
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 |
|
UML图大致如下:
从代码上我们可以看出,代理模式要求(这也是它的限制所在):
(1).代理类需要和被代理者一样实现相同的接口;
(2).代理类包含被代理者的引用;
代理模式我们可以理解为一种静态代理,其问题是需要我们显示的为每个需要被代理的类实现一个代理类,即一个代理类只能为i一个被代理者做代理的功能。如果有上千个类需要代理,估计要骂娘了。
java动态代理正是为了解决这个问题。主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。我们只需要实现InvocationHandler接口,在实现invoke()方法的时候加上我们的代理逻辑。
一个简单的使用实例如下:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 |
|
JavaDynamicProxy.generateProxy的输出会是一个动态的代理类。debug信息如下,从debug信息我们大致知道,这个代理类的类名称为$Proxy0,内部包含一个属性名为h的属性,h指向的就是我们实现的InvocationHandler的类(这里即JavaDynamicProxy)的实例。
但是Java动态代理的限制是:
(1).被代理的类要求至少实现了一个Interface;
(2).被代理的类要求有public的构造函数(即没有显示的将其设置为private等);
(3).被代理的类要求不是final;
如下代理一个没有实现任何接口的类会报错
1 2 3 4 5 6 |
|
错误信息如下:
1 2 3 4 5 6 7 |
|
Java动态代理的机制是利用反射机制生成。具体代码可以debug,主要的代码如下:
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 |
|
我们可以自己使用ProxyGenerator来生成代理类并将其字节码记录下来:
1 2 3 |
|
获取的字节码如下:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
从生成的类我们知道,代理类继承Proxy类(这就是为什么Proxy类内的InvocationHandler实例未protected)并实现我们需要的被代理者的Interface(比如这里的bye()方法),另外它只要一个接受一个InvocationHandler为参数的构造函数。当我们调用代理类的bye()方法时候,其实是调用我们实现的InvocationHandler(即上面的JavaDynamicProxy)的invoke()方法,在invoke()方法里面,我们实现了我的代理逻辑。
我们这里这个Demo的大致UML图如下:
参考:http://www.cnblogs.com/cruze/p/3819761.html
鉴于Java动态代理的限制,我们有需要代理没有任何实现接口的类的时候,可以考虑使用CGLib。CGLib的全称是Code Generate Library。CGLib的使用使用十分广泛,我们这里要讲的Spring AOP,以及EasyMock等。
同样先上Demo代码,我们只需要实现MethodInterceptor接口:
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 55 56 |
|
同样我们debug一下,得到的信息如下,同样生成了代理类,类名称为proxy.sample.HelloImpl$$EnhancerByCGLIB$$b46b6f06,这个稀奇古怪的类名我们后面会分析:
下面还是尝试分析CGLib动态代理的原理。默认情况下生成的代理class文件只存储在内存中,我们可以在代码中设置一个环境变量:
1
|
|
之后我们在目标目录下得到很多的class,其中的proxy目录包含了生成的class文件。我们发现一大堆的class文件,类名都是稀奇古怪:
1 2 3 4 5 |
|
我们先看看class文件的生成策略。每个Class Generator(比如这里的Enhancer)都继承自AbstractClassGenerator(实现接口ClassGenerator,这个接口只有一个generateClass的方法),需要实现其generateClass()方法。generateClassName()方法用来生成Class名称:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
下面正式进入class文件的生成原理分析,还是从源代码入手,enhancer.create()最终进入AbstractClassGenerator.create()方法,我们发现最终return的Object是从内部变量obj得来,因此,我们看看ClassLoaderData的生成逻辑:
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 |
|
因此我们将目光转到generate()中:
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 |
|
所以最终的调用是Enhancer的generateClass()调用。这代码好晦涩,感觉和JVM加载字节码相关。
看一下CGLib生成的class文件,这个代理类继承了我们的被代理类并且实现了Factory类,类定义如下:
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 55 56 57 58 59 60 61 62 63 64 |
|
注意,我们生成的动态代理包含两个hello()相关的方法。我们可以使用javap获取其字节码,我们发现和我们被代理者同名的hello()方法是带代理逻辑的,而CGLIB$hello$0()这个则是原始被代理者的直接调用(不包含代理逻辑):
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 |
|
遗留问题: (1).为什么一个类会生成多个代理类,各个代理类都是什么用处;
CGLib代理同样存在限制:
(1).private方法无法代理;
(2).final方法无法代理;
private方法和final方法看代理类的字节码会发现,代理类不会这些重写函数(子类没法重写),因此会自动调用父类的。
参考: cglib源码分析(四):cglib 动态代理原理分析
简单总结对比一下两种动态代理:
代理 | 没有实现interface的类 | protected方法 | private方法 | final类 | final方法 | 构造函数private的类 |
---|---|---|---|---|---|---|
Java动态代理 | 不支持 | 不支持 | 不支持 | 支持 | 支持 | 支持 |
CGLib动态代理 | 支持 | 支持 | 不支持 | 不支持 | 不支持 | 不支持 |
关于CGLib对protected方法的支持,可以在生成的代理类中看到.
1 2 3 4 |
|
在性能方面,通常认为Java动态代理生成代理类比CGLib生成代理类快,但是CGLib的代理类运行期性能会优于Java动态代理的代理类。不过通常性能都不是问题,如果确实很关心性能,建议直接使用ASM。
参考: Why do you think CGLib proxies are faster than JDK Proxies?
Implement Your Own Proxy-Based AOP Framework Blog
BeanDefinitionParserDelegate AnnotationDrivenBeanDefinitionParser
参考: Spring加载资源分析:http://www.blogjava.net/heavensay/archive/2013/10/28/405699.html
事务的坑 http://www.ibm.com/developerworks/library/j-ts1/
代理实现选择器: AdviceMode TransactionManagementConfigurationSelector
声明事务切面实现: TransactionInterceptor
事务实现 Connection关闭自动提交 ConnectionHolder来保持Connection
参考: http://openwares.net/java/spring_mybatis_transaction.html http://my.oschina.net/guanzhenxing/blog/214228 http://www.mybatis.org/spring/zh/transactions.html http://tracylihui.github.io/2015/07/28/spring/Spring%E5%A3%B0%E6%98%8E%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%AE%A1%E7%90%86/ http://www.cnblogs.com/davenkin/archive/2013/02/16/java-tranaction-1.html http://www.importnew.com/12300.html 详细介绍Spring事务管理: http://developer.51cto.com/art/200906/129854.htm spring事务原理1-事务的抽象: http://sqtds.github.io/2014/06/09/2014/spring-tx1/ Spring官方文档Transaction Management: http://docs.spring.io/autorepo/docs/spring/3.2.x/spring-framework-reference/html/transaction.html Spring官方文档Aspect Oriented Programming with Spring: http://docs.spring.io/spring/docs/3.0.x/reference/aop.html StackOverFlow Spring - @Transactional - What happens in background?: http://stackoverflow.com/questions/1099025/spring-transactional-what-happens-in-background Transaction strategies: Understanding transaction pitfalls: http://www.ibm.com/developerworks/library/j-ts1/ Annotation-based Transactions in Spring: http://springinpractice.com/2008/03/18/annotation-based-transactions-in-spring Design principle behind Transaction framework with Spring 3: http://stackoverflow.com/questions/11789857/design-principle-behind-transaction-framework-with-spring-3 Chapter 6. 使用Spring进行面向切面编程(AOP): http://shouce.jb51.net/spring/aop.html wiki: http://wiki.corp.qunar.com/display/~yushen.ma/Spring+3.x-JDBC+Transaction spring事务: http://wiki.corp.qunar.com/pages/viewpage.action?pageId=52084161 spring 事务: http://wiki.corp.qunar.com/pages/viewpage.action?pageId=93338632 aspectj事务: http://wiki.corp.qunar.com/pages/viewpage.action?pageId=92656954
Demo: 编程事务: http://www.tutorialspoint.com/spring/programmatic_management.htm 声明事务: http://www.tutorialspoint.com/spring/declarative_management.htm
@Transactional注解工作原理:http://blog.csdn.net/dslztx/article/details/46636079 分布式事务系列(1.2)Spring的事务体系: https://yq.aliyun.com/articles/39046 spring transaction源码分析–事务架构:http://www.cnblogs.com/davidwang456/p/4309038.html
http://blog.csdn.net/szwangdf/article/details/41516239
AopProxy DefaultAopProxyFactory
]]>strace用来跟踪进程执行时的系统调用和所接收的信号.
Linux下进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备.strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间.
参数及意义如下,忽略了一些不大重要的(其实海报看一些没看懂的…):
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 |
|
使用cheat可以看到strace通常使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
通常而言,我们使用-c选项找出最耗时的系统调用,再使用-e选项跟踪这个操作.
参考: 手把手教你用Strace诊断问题
一直很好奇top命令的数据从哪里获取的.我这里的前提我知道的一点:top的数据肯定是从proc/pid/下的某个文件中读取的,有这个前提我们就可以来跟踪top命令执行过程中有那些系统调用了,打开了什么文件等.
1 2 3 4 5 6 |
|
从上面的strace中,我们知道,top的信息来源于两个文件,stat和statm.这两个文件的介绍参考上一篇博客: http://xiaobaoqiu.github.io/blog/2016/01/28/who-eat-jvms-memory/.
参考: 8 Options to Trace/Debug Programs using Linux strace Command
]]>关于JVM的一些基础知识,可以看 JVM Internals,曾经我尝试翻译过,地址:http://xiaobaoqiu.github.io/blog/2014/09/11/internal-jvm/
这里要说的是,那些参数决定了JVM内存上限,比如常用的内存配置如下:
1
|
|
说明堆(Heap)的大小1G,永久代大小256M.
另外,64位的操作系统(Linux服务器),线程的栈空间大小最大为1M:
1 2 3 4 5 6 7 |
|
JVM的占用上限由一下几个因素决定:
1. 堆内存
2. 持久代
3. 线程栈空间
4. 堆外内存
堆外内存参考: http://www.infoq.com/cn/news/2014/12/external-memory-heap-memory
线程数可以如下得到:
1 2 |
|
通常而言,我们只需要计算堆内存,持久代再加上线程栈空间.比如我们的本地一个小服务:
1
|
|
首先得知道top命令中RES数据从哪里来的.再Linux上,万物皆文件,因此RES的数据也是来源于文件.
下面是一个典型的Tomcat应用的线程下的内容,进程号为1381,简单说描述每个文件的内容或者作用:
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 |
|
参考: http://man7.org/linux/man-pages/man5/proc.5.html
和内存相关的文件主要包括(解析各个文件的时间不同,可能内容上存在对不上的情况):
1. statm:
2. stat:
3. status
4. maps
5. smaps
1. statm
进程的内存使用,注意单位是page,内容的解析参考2.3节的内容.
2. stat
1381这个进程的stat文件及各个字段的含义(含义参见proc命令手册: man proc –> 搜stat, 见/proc/[pid]/stat):
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 |
|
里面包含我们熟悉的RSS数据,注意其单位是page.
3. status
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 |
|
关于其中的TracerPid,在Linux下,我们可以使用strace来跟踪命令的运行.
参考: http://blog.csdn.net/zjl410091917/article/details/8075691
4. maps
maps内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
第一列代表内存段的虚拟地址 第二列代表执行权限虚拟内存的权限,r=读,w=写,x=,s=共享,p=私有 第三列代表在进程地址里的偏移量 第四列映射文件的主设备号和次设备号 第五列映像文件的节点号,即inode 第六列是映像文件的路径
参考: http://stackoverflow.com/questions/1401359/understanding-linux-proc-id-maps
5. smaps
smaps代表进程的各个线程的内存消耗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
关于其中的Pss,因为进程之间存在内存共享,比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB(Rss会是1000KB),那么该进程分摊其中一半的内存,即Pss=500KB。
内存页引用计数>1,则表示是shared,否则是private.
参考: stackoverflow: what does pss mean in /proc/pid/smaps
Getting information about a process' memory usage from /proc/pid/smaps
另外,当前系统的page大小可以通过getconf命令获取,比如我当前的服务器page大小为4k:
1 2 |
|
top命令RES数据来源于/proc/PID/statm中的第二个字段.还是1381这个进程的statm文件内容和对应的top信息如下:
1 2 3 4 5 6 |
|
/proc/[pid]/statm文件的几个字段的具体意义如下(注意单位是page):
1 2 3 4 5 6 7 |
|
可以验证,1381这个进程的RES大小来源, 和top命令中RES的数字是吻合的:
1
|
|
写到这发现谢步下去了,因为我一开始就写错了.
想知道谁吃了我们JVM的内存,可以jmap dump下来,用MAT分析一下.
想知道谁吃了服务器的内存,建议看看参考里面的Linux Used内存到底哪里去了这篇文章.
参考: Linux Memory Management Overview
]]>最近项目中接触到久闻大名的Protocol Buffers。简单学习Protocol Buffers是什么,能做什么,为什么需要它,怎么使用它。
Protocol Buffers官网:https://developers.google.com/protocol-buffers/
Protocol Buffers官网(中文):https://developers.google.com/protocol-buffers/?hl=zh-CN
Git地址:https://github.com/google/protobuf
Java API:https://developers.google.com/protocol-buffers/docs/reference/java/?hl=zh-CN
proro文件的编写指南:https://developers.google.com/protocol-buffers/docs/style?hl=zh-CN
Java使用指南:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN
protocol buffers是google提供的一种将结构化数据进行序列化和反序列化的方法,其优点是语言中立,平台中立,可扩展性好,目前在google内部大量用于数据存储,通讯协议等方面。语言中立说明和语言无关,可以作为跨语言的通信协议。平台中立说明Protocol Buffers可以作为不同平台之间的通信协议;可扩展性好可以理解为protocol buffers支持嵌套。
PB在功能上类似XML,但是序列化后的数据更小,解析更快,使用上更简单。用户只要按照proto语法在.proto文件中定义好数据的结构,就可以使用PB提供的工具(protoc)自动生成处理数据的代码,使用这些代码就能在程序中方便的通过各种数据流读写数据。
PB目前支持Java, C++和Python3种语言(截止目前已经额外支持C#,Ruby和Object-C)。另外,PB还提供了很好的向后兼容,即旧版本的程序可以正常处理新版本的数据,新版本的程序也能正常处理旧版本的数据。
1.下载源码 从https://github.com/google/protobuf%E4%B8%8B%E8%BD%BD%E6%BA%90%E7%A0%81
2.根据README安装 (1).使用./autogen.sh生成configure脚本; (2).配置./configure,默认在/usr/local下,可以指定路径 (3).make (4).make check (5).make install
这里安装的是最新的Protoc 3。安装之后,就会有一个根据Message文件产生对应代码文件的工具protoc:
1 2 |
|
在帮助文当中就可以看到如何生成各种类型的文件的方式:
1 2 3 4 5 6 7 |
|
参考官网的一个例子,message是定义protobuf的关键词,Person表示一个人的信息,包括id,name,email和phone四个信息,其中name和id是必须的,email是可选的,phone是数组形式并且phone本身也是一种消息(message),PhoneNumber内部包含了枚举类型PhoneType,主要枚举的定义和Java有点不同,这里是分号分割的。1234这种序号表示字段在数据中的顺序,新增字段依次增加这个序号就行。
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 |
|
string和int32是protobuf支持的基本类型,所有支持的类型及其含义如下:
如下生成message对应的Java文件如下:
1
|
|
生产数据方直接使用对应的Builder生成对象,然后往流里面写数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
消费数据方可以直接从流中读取并解析对象:
1 2 3 4 5 6 7 8 |
|
参考git上的各种序列化的测试结果: https://github.com/eishay/jvm-serializers/wiki
]]>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 |
|
场景如下:
在本地代码中重现了一下问题,并看了一下使用枚举的情况下其他可能处心问题的场景,结论如下(前提是Cache中已经存在使用RoomStatus数据):
综上,在Memcached中cache包含枚举的数据时候,建议如下:
这些序列化和反序列化的逻辑再XMemcached客户端实现的。详见SerializingTranscoder类,包含数据的encode和decode逻辑。再使用Memcached的时候也可以使用
]]>之前在项目中做数据聚合去重的逻辑的时候简单看过局部敏感Hash(Locality Sensitive Hashing,简称LSH)这个东东。今天整理一下个人的理解。
LSH可以理解为一种衡量文本相似度的算法,特点是散列前的相似点经过哈希之后,也能够在一定程度上相似,并且具有一定的概率保证。其有坚实的理论依据(98年左右理论就提出来了,99年有第一版实现)并且在高维数据空间中表现优异。简单的价格实验场景:
LSH的发展历史可以参考: http://jacoxu.com/?p=496
说到Hash,大家都很熟悉,是一种典型的Key-Value结构,最常见的算法莫过于MD5。其设计思想是使Key集合中的任意关键字能够尽可能均匀的变换到Value空间中,不同的Key对应不同的Value。通过建立Hash的方式我们能够得到O(1)的查找时间性能,其中关键在于选取一个hash function(md5就是一致hash function)。
md5这种hash函数通常情况下,Key值只有轻微变化,Value值也会发生很大地变化。比如下面实验中用到的文本,仅仅是邮箱号少了个.,其md5完全不同:
1 2 3 4 5 6 7 8 |
|
原始文本是极其相似的,但是hash之后这种相似性就丢失了。
局部敏感哈希的最大特点就在于保持数据的相似性。需要注意的是这里说的保持数据的相似度不是说保持100%的相似度,而是保持最大可能的相似度。换个角度来看,可以将LSH理解为数据降维的方法。
数据对应的维度越高,信息量也就越大,相反,如果数据进行了降维,那么毫无疑问数据所反映的信息必然会有损失。哈希函数从本质上来看就是一直在扮演数据降维的角色。
LSH的基本思想是:将原始数据空间中的两个相邻数据点通过相同的映射或投影变换后,这两个数据点在新的数据空间中仍然相邻的概率很大,而不相邻的数据点被映射到同一个桶的概率很小。
参考: https://en.wikipedia.org/wiki/Locality-sensitive_hashing http://www.cnblogs.com/maybe2030/p/4953039.html http://blog.csdn.net/weiyuweizhi/article/details/8921973
]]>