Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

基于Spark自动扩展scikit-learn (spark-sklearn) - CSDN博客

$
0
0

1、基于Spark自动扩展scikit-learn(spark-sklearn)

1.1 导论

Spark MLlib 将传统的单机机器学习算法改造成分布式机器学习算法,比如在梯度下降算法中,单机做法是计算所有样本的梯度值,单机算法是以全体样本为计算单位;而分布式算法的逻辑是以每个样本为单位,在集群上分布式的计算每个样本的梯度值,然后再对每个样本的梯度进行聚合操作等。在Spark Mllib中分布式的计算单位可以是:一个样本数据、一个分区的样本数据,一个矩阵等等,分布式的计算单位根据算法的需求而不同,前提条件是每个单位的计算应该是可独立,不依赖于其它单位的计算结果,所以一般在分布式算法设计时,需要把每个单位计算时所需要的数据放在一个单位里,例如在ALS的分布式设计中,将U和V的数据进行重新分区,并建立新的数据集。

Spark Mllib实现了在大数据训练样本下的分布式计算,适应于工程化的实践项目中,如果当计算模型中需要涉及到各种模型参数的调优时,Spark Mllib就会显得有些不足,那我们能否设想下:在小样本训练集下,我在Spark上随机生成1千万个计算模型,把这1千万个计算模型分布式的运行在Spark集群上对训练集进行模型测试计算,是不是可以得到一个结果最优的模型,该模型对应的参数就是最优参数,然后我们根据最优化参数应用在工程化的实践中。

我们可以对Spark Mllib 进行扩展,把我们的带有参数的机器学习模型当作分布的计算单位,每个单位的元素包括:(带参数的模型,训练样本,测试样本),每个单位的计算过程就是将对训练样本训练带参数的模型,得到模型,然后计算测试样本的精度,在集群中对各个单位进行分布式的计算,最终取得最优结果的那个模型。

这就是我下面要介绍的:Auto-scaling scikit-learn with Spark。

1.2 spark-sklearn背景

数据科学家经常花几个小时或几天来调优模型使得计算的精度最高。这种调优通常是在Python或R中运行大量的单机机器学习(ML)任务。

目前Spark集成了Scikit-learn包,这样可以极大的简化了Python数据科学家们的工作,这个包可以在Spark集群上自动分配模型参数优化计算任务,而且不影响现有的工作流程:

如果在单个机器上使用时, Spark可以使用scikit-learn(Joblib)替代默认的多线程框架。

如果需要工作在多台机器上,也不需要修改代码,可以在单机和集群中运行。

1.3 轻松应对大规模模型计算

对于数据分析处理,Python是一种最流行的编程语言,这在很大程度上是由于高质量的计算库,比如数据分析的Pandas 和机器学习orscikit-learn等。Scikit-learn提供快速、健壮的标准ML算法如集群、分类和回归等。

Scikit-learn的优势通常是在单个节点上进行机器学习的计算,。对于一些常见的场景,如参数调优,大量小任务可以并行地运行。这些场景可以完美使用Spark来解决。

1.4 随机森林的分布优化

采用图像识别数字的一个经典例子。数据包括:数字图像的数据集与对应的标签:


我们通过训练随机森林分类器来识别数字。这个分类器有许多参数需要调整,但是没有简单的方法来知道哪个参数效果的好与坏,除了尝试大量的不同组合。Scikit-learn提供了GridSearchCV接口,一个搜索算法,自动搜索最优参数设置。如下图示例,GridSearchCV采用交叉验证的方式进行参数选择,每个参数设置产生一个模型,最终选择表现最好的模型。


使用scikit-learn的原代码如下:

from sklearn import grid_search, datasets
from sklearn.ensemble import RandomForestClassifier
from sklearn.grid_search import GridSearchCV
digits = datasets.load_digits()
X, y = digits.data, digits.target
param_grid = {"max_depth": [3, None],"max_features": [1, 3, 10],"min_samples_split": [1, 3, 10],"min_samples_leaf": [1, 3, 10],"bootstrap": [True, False],"criterion": ["gini", "entropy"],"n_estimators": [10, 20, 40, 80]}
gs = grid_search.GridSearchCV(RandomForestClassifier(), param_grid=param_grid)
gs.fit(X, y)

训练数据集很小(数百kb),但探索所有的组合大约需要5分钟。Spark的scikit-learn包提供了一种在Spark集群上进行分布式的交叉验证算法计算工作。每个节点运行训练算法使用的本地的scikit-learn库,并且向集群的master报告最佳模型:

 

他之前的代码是一样的,除了一行变化:

from sklearn import grid_search, datasets
from sklearn.ensemble import RandomForestClassifier
# Use spark_sklearn’s grid search instead:
from spark_sklearn import GridSearchCV
digits = datasets.load_digits()
X, y = digits.data, digits.target
param_grid = {"max_depth": [3, None],"max_features": [1, 3, 10],"min_samples_split": [1, 3, 10],"min_samples_leaf": [1, 3, 10],"bootstrap": [True, False],"criterion": ["gini", "entropy"],"n_estimators": [10, 20, 40, 80]}
gs = grid_search.GridSearchCV(RandomForestClassifier(), param_grid=param_grid)
gs.fit(X, y)

这个例子在4个节点(16 cpu)的集群上运行时间小于30秒。对于大数据集和更多的参数设置,效率的提升则更大。

 

如果你想试试这个包,需要:

https://pypi.python.org/pypi/spark-sklearn

http://spark-packages.org/package/databricks/spark-sklearn

实例地址: http://go.databricks.com/hubfs/notebooks/Samples/Miscellaneous/blog_post_cv.html

详细见API:

http://pythonhosted.org/spark-sklearn/

 

转载请注明出处:

http://blog.csdn.net/sunbow0

 

 


java操作pdf制作电子签章 - CSDN博客

$
0
0

java操作pdf制作电子签章

电子签章简介

电子签章,与我们所使用的数字证书一样,是用来做为身份验证的一种手段,泛指所有以电子形式存在,依附在电子文件并与其逻辑关联,可用以辨识电子文件签署者身份,保证文件的完整性,并表示签署者同意电子文件所陈述事实的内容。一般来说,对电子签章的认定,都是从技术角度而言的。主要是指通过特定的技术方案来鉴别当事人的身份及确保交易资料内容不被篡改的安全保障措施。从广义上讲,电子签章不仅包括我们通常意义上讲的”非对称性密钥加密”,也包括计算机口令、生物笔迹辨别、指纹识别,以及新近出现的眼虹膜透视辨别法、面纹识别等。而电子签章技术作为目前最成熟的”数字签章”,是以公钥及密钥的”非对称型”密码技术制作的。电子签章是电子签名的一种表现形式,利用图像处理技术将电子签名操作转化为与纸质文件盖章操作相同的可视效果,同时利用电子签名技术保障电子信息的真实性和完整性以及签名人的不可否认性 。
如果对数字证书,签名验签,摘要,数据签名不太理解的同学,可以参考我之前的文章
安全之加密算法(-)
openssl 自建ca,颁发客户端证书
具体了解下

java代码实现

java 操作pdf的开源类库我大概了解了两种pdfbox,itextpdf,两个库各有优势,目前据我使用可知,pdfbox功能较为强大,但是定制性较小,itextpdf 可定制性较高
准备
p12 证书
测试电子签章图片

这里写图片描述
测试pdf

这里写图片描述

使用jar包
这里写图片描述

itextpdf实现电子签章

itextpdf 提供了 MakeSignature 这个入口类,操作相对较为简单

封装下即可

public static void sign(InputStream src  //需要签章的pdf文件路径
            , OutputStream dest  // 签完章的pdf文件路径
            , InputStream p12Stream, //p12 路径
            char[] password
            , String reason  //签名的原因,显示在pdf签名属性中,随便填
            , String location,String chapterPath) //签名的地点,显示在pdf签名属性中,随便填
                    throws GeneralSecurityException, IOException, DocumentException {
         //读取keystore ,获得私钥和证书链
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(p12Stream, password);
        String alias = (String)ks.aliases().nextElement();
        PrivateKey pk = (PrivateKey) ks.getKey(alias, password);
        Certificate[] chain = ks.getCertificateChain(alias);

        //下边的步骤都是固定的,照着写就行了,没啥要解释的
        // Creating the reader and the stamper,开始pdfreader
        PdfReader reader = new PdfReader(src);
        //目标文件输出流
        //创建签章工具PdfStamper ,最后一个boolean参数 
        //false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
        //true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
        PdfStamper stamper = PdfStamper.createSignature(reader, dest, '\0', null, false);
        // 获取数字签章属性对象,设定数字签章的属性
        PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
        appearance.setReason(reason);
        appearance.setLocation(location);
        //设置签名的位置,页码,签名域名称,多次追加签名的时候,签名预名称不能一样
        //签名的位置,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
        //四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
        appearance.setVisibleSignature(new Rectangle(0, 800, 100, 700), 1, "sig1");
        //读取图章图片,这个image是itext包的image
        Image image = Image.getInstance(chapterPath);
        appearance.setSignatureGraphic(image); 
        appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
        //设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
        appearance.setRenderingMode(RenderingMode.GRAPHIC);

        // 这里的itext提供了2个用于签名的接口,可以自己实现,后边着重说这个实现
        // 摘要算法
        ExternalDigest digest = new BouncyCastleDigest();
        // 签名算法
        ExternalSignature signature = new PrivateKeySignature(pk, DigestAlgorithms.SHA1, null);
        // 调用itext签名方法完成pdf签章CryptoStandard.CMS 签名方式,建议采用这种
        MakeSignature.signDetached(appearance, digest, signature, chain, null, null, null, 0, CryptoStandard.CMS);
    }   

pdfbox 实现电子签章

pdfbox 实现签名可以从pdfbox 官网示例中找到
svn地址
https://svn.apache.org/repos/asf/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/
简单封装示例

  public static void sign(char[] password,InputStream p12Input,FileInputStream imageStream,File srcPdf,File signed,String signerName,String reason,String location) throws Exception{
        boolean externalSig=false;
        KeyStore keystore = KeyStore.getInstance("PKCS12");
        keystore.load(p12Input, password);
        PdfSignBox signing = new PdfSignBox(keystore, password);
        File signedDocumentFile;
        int page=1;
        signing.setVisibleSignDesigner(srcPdf.toString(), 50, 100, -90, imageStream, page);
        signing.setVisibleSignatureProperties(signerName, location, reason, 0, page, true);
        signing.setExternalSigning(externalSig);
        signing.signPDF(srcPdf, signed, null);
    }   

签名测试代码

package com.taoyuan.pdf.sign.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

import com.taoyuan.pdf.sign.itext.PdfSignBox;
import com.taoyuan.pdf.sign.itext.PdfSignItext;

public class Test {
    public static void main(String[] args) throws  Exception {
             String KEYSTORE="d://test.p12";
               char[] PASSWORD = "123".toCharArray();//keystory密码
               String SRC="d://demo.pdf" ;//原始pdf
               String DEST="d://demo_signed_box.pdf" ;//签名完成的pdf
               String DEST2="d://demo_signed_itext.pdf" ;//签名完成的pdf
              String chapterPath="d://chapter.png";//签章图片
              String signername="測試";
              String reason="数据不可更改";
              String location="桃源乡";

    PdfSignBox.sign(PASSWORD, new FileInputStream(KEYSTORE), 
            new FileInputStream(chapterPath), 
            new File(SRC),new File(DEST),signername, reason, location); 


    PdfSignItext.sign(new FileInputStream(SRC), new FileOutputStream(DEST2), 
            new FileInputStream(KEYSTORE), PASSWORD, 
         reason, location, chapterPath);
    }
}   

签章效果,使用adboe reader打开pdf,查看
这里写图片描述

这里写图片描述

当然自己生成的证书去给pdf签章,是不被认可的,这种证书需要花钱的,大概1000多元一年吧

完整代码下载地址
http://download.csdn.net/download/do_bset_yourself/10010394

docker - 从安装到部署一个web应用(go、java) - CSDN博客

$
0
0

说明:
1.权限是root,不是则先提升权限


一:安装docker

1. https://docs.docker.com/engine/installation/binaries/
下载docker最新版二进制tar.gz

linux下:
wget https://get.docker.com/builds/Darwin/x86_64/docker-1.11.0.tgz

2.丢到 $path中

mv docker /usr/local/sbin

3.启动

docker daemon &

二.在容器上运行tomcat

docker官方镜像仓库由于有墙,所以下载的很慢。目前我用的是时速云的镜像。

第一步:拉取镜像到本地
docker pull index.tenxcloud.com/tenxcloud/tomcat

第二步:为镜像添加一个别名
docker tag index.tenxcloud.com/tenxcloud/tomcat tomcat-1

第二步:启动tomcat
docker run -p 5000:8080 --name container1 tomcat-1
如此一来,tomcat就启动了,-p 5000:8080的意思是把容器tomcat的8080端口隐射到宿主机的端口上,这样外网访问5000就能访问到我们的container1的8080 tomcat上面了.

如此一来,一个简单的tomcat就跑起来了.

此处容器container1 和 镜像tomcat-1,我的理解是镜像就是一个模板,container1就是根据这个模板创造的一个真正的盆子,这个盆子里面就跑着我们的tomcat. 所以我们可以用同一个镜像创建许多container。

三.在tomcat上面部署我们的应用

接下来我们要部署我们的应用上去,思路是进入到container1里面去,此时可以把container1想象为一个新的机器,我们只需要到tomcat的webapp丢war,然后重启就行了.

1.进入容器内部

docker exec -it container2 /bin/bash

2.查看tomcat webapp路径

/tomcat/webapps

3.传war

把war丢到宿主机 在丢到container里面丢到tomcat/webapps

docker cp DemoOne.war container2:tomcat/webapps

太TM惊喜了,docker本身就支持啊!!!666666.

4.重启容器

不需要了。。。docker自动帮你部署了
这里写图片描述

5.访问应用

这里写图片描述


至此,一个完整的docker部署tomcat及上线一个java web应用流程就走通了.
说实话,走通后才发现是这么的简单。之前概念上面不懂的地方这下也基本通了。
不得不说很Nice,和预想中的完全一样,就把dokcer给你创建的container当成一个新的linux用就行啦!


使用docker部署一套应用系统

接下来部署一套完整的系统,包括如下组件:
负载均衡:Haproxy
JAVA工:tomcat
缓存:Redis Master、Slave

流程是Java开一个restful接口,为redis写入一个数据,
再开一个restful接口,从redis读取一个数据。

系统结构如图:
这里写图片描述

步骤:
1.准备java工程,并打包成war
2.拉取haproxy镜像,并运行

//注意 --name不能放在最后,6555:80 80不可更改,是haproxy本身的端口docker run-d-p6555:80--linkcontainer2:container2--name haproxy-1haproxy

这里写图片描述
可以看到,haproxy已经成功实现了代理的功能.
目前的镜像不知道为什么不能通过修改haproxy.cfg的方式来支持,后续研究


之后再补上golang镜像及应用部署的流程

使用shell通过微信公众号发送模板消息 - CSDN博客

$
0
0

如下通过shell脚本实现,通过微信公众号发送模板消息到个人微信号。

1.配置微信公众号

由于没有认证的公众号,只能通过自己申请的个人订阅号(可以自行申请),并到开发者工具中开通公众平台测试帐号实现该功能。
1.获取测试公众号appID和appsecret
这里写图片描述
*2.关注测试号二维码获取用户openid
这里写图片描述
3.新增模板获取模板ID
这里写图片描述
这里写图片描述
得到模板id: OA0PX8pqc2X7t_y05y5GxZ8LutBpu341FIYSeQOkno

2.通过脚本实现消息发送

这里就不啰嗦了,直接上shell脚本代码,具体看注释

#!/bin/sh# 微信消息发送脚本 zhutw#全局配置--#微信公众号appIDappID=wx*******0ebde756#微信公众号appsecretappsecret=138********0446e9ae04f2#微信公众号发送消息模板tpl_id=OA0PX8pqc2X7t_-y05y5GxZ8LutBpu341FIYSeQOkno#消息模板:#   {{first.DATA}}#   项目名称:{{name.DATA}}#   报警时间:{{date.DATA}}##   {{remark.DATA}}#获取微信公众号AccessToken,并缓存到本地 函数getAccessToken(){if[-f"$HOME/.wechat_accesstoken"];thenaccess_token=`cat$HOME/.wechat_accesstoken | awk -F":"'{print $1}'`
        expires_in=`cat$HOME/.wechat_accesstoken | awk -F":"'{print $2}'`
        time=`cat$HOME/.wechat_accesstoken | awk -F":"'{print $3}'`if[ -z$access_token] || [ -z$expires_in] || [ -z$time];thenrm-f$HOME/.wechat_accesstoken
            getAccessTokenfielsecontent=$(curl"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appID&secret=$appsecret")echo"get content:$content"access_token=`echo$content| awk -F"\""'{print $4}'`
        expires_in=`echo$content| awk -F"\""'{print $7}'| cut-d"}"-f1|cut -c2-`echo"access_token =$access_token"echo"expires_in =$expires_in"time=$(date +%s)echo"$access_token:$expires_in:$time">$HOME/.wechat_accesstokenif[ -z$access_token] || [ -z$expires_in] || [ -z$time];thenecho"not get access_token"exit0fifiremain=$[$(date +%s) -$time]
    limit=$[$expires_in-60]if[$remain-gt$limit];thenrm-f$HOME/.wechat_accesstoken
        getAccessTokenfi}#发送消息函数sendMessage(){#消息json体message=`cat << EOF
    {"touser":"$openid","template_id":"$tpl_id","url":"$url","data":{"first": {"value":"$first","color":"#FF0000"},"name":{"value":"$name","color":"#173177"},"date": {"value":"$date","color":"#173177"},"remark":{"value":"$remark","color":"#FF0000"}
    }
     }
EOF
`echo"send message :$message"curl -X POST -H"Content-Type: application/json"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=$access_token-d"$message"}#帮助信息函数usage(){
    cat <<EOF
usage:$0[-u openids-ssummary -n name -t time-ddetail-llink] [-h]
    u   wechat user openid , multiple comma separated
    s   message summary
    n   project name
    t   alarm time
    d   message detail
    l   link address
    h   output thishelpandexitEOF
}#获取脚本执行参数whilegetopts":u:s:n:t:d:h:l:"op;docase$opinu)
        openids="$OPTARG";;
        s)
        first="$OPTARG";;
            n)
               name="$OPTARG";;
            t)
        date="$OPTARG";;
            d)
        remark="$OPTARG";;
        l)
        url="$OPTARG";;
        *)
        usageexit0;;esacdone#判断条件满足发送消息if[[ -n$openids&& -n$first&& -n$name&& -n$date]];thengetAccessToken
    OLD_IFS="$IFS"IFS=","arr=($openids)
    IFS="$OLD_IFS"foropenidin${arr[@]}dosendMessagedoneexit$?elseecho"params error."usageexit1fi

关于微信公众号接口说明查看如下接口wiki:
https://mp.weixin.qq.com/wiki
1.开始开发->获取access_token
2.消息管理->模板消息接口

3.接收到消息

执行命令脚本即上述shell脚本内容。记得设置脚本执行权限 chmod +x sendMessageForWechat
shell
./sendMessageForWechat -u o4bHbvjL9aWoRCa29vdOQ9aJMq0w -s "192.168.1.90磁盘空间不足" -n 测试系统 -t "2017-01-15 13:00:10" -d "磁盘已使用超过80%,剩余5G,请及时处理" -l "http://m.baidu.com"

这里写图片描述
点击消息,打开百度链接。。。

redis实现高并发下的抢购/秒杀功能 - 周伯通的麦田 - 博客园

$
0
0

之前写过一篇文章, 高并发的解决思路(点此进入查看),今天再次抽空整理下实际场景中的具体代码逻辑实现吧:
抢购/秒杀是如今很常见的一个应用场景,那么高并发竞争下如何解决超抢(或超卖库存不足为负数的问题)呢?

常规写法:

查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数

这里我就只谈redis的解决方案吧...
我们先来看以下代码(这里我以laravel为例吧)是否能正确解决超抢/卖的问题:

<?php$num= 10;//系统库存量$user_id=  \Session::get('user_id');//当前抢购用户id$len= \Redis::llen('order:1');//检查库存,order:1 定义为健名if($len>=$num)  return'已经抢光了哦';$result= \Redis::lpush('order:1',$user_id);//把抢到的用户存入到列表中if($result)
  return'恭喜您!抢到了哦';

如果代码正常运行,按照预期理解的是列表order:1中最多只能存储10个用户的id,因为库存只有10个。
然而,但是,在使用jmeter工具模拟多用户并发请求时,最后发现order:1中总是超过5个用户,也就是出现了“超抢/超卖”。
分析问题就出在这一段代码:

$len= \Redis::llen('order:1');//检查库存,order:1 定义为健名if($len>=$num)  return'已经抢光了哦';

在抢购进行到一定程度,假如现在已经有9个人抢购成功,又来了3个用户同时抢购,这时if条件将会被绕过(条件同时被满足了),这三个用户都能抢购成功。而实际上只剩下一件库存可以抢了。
在高并发下,很多看似不大可能是问题的,都成了实际产生的问题了。要解决“超抢/超卖”的问题,核心在于保证检查库存时的操作是依次执行的,再形象的说就是把“多线程”转成“单线程”。即使有很多用户同时到达,也是一个个检查并给与抢购资格,一旦库存抢尽,后面的用户就无法继续了。
我们需要使用redis的原子操作来实现这个“单线程”。首先我们把库存存在goods_store:1这个列表中,假设有10件库存,就往列表中push10个数,这个数没有实际意义,仅仅只是代表一件库存。抢购开始后,每到来一个用户,就从goods_store:1中pop一个数,表示用户抢购成功。当列表为空时,表示已经被抢光了。因为列表的pop操作是原子的,即使有很多用户同时到达,也是依次执行的。抢购的示例代码如下:
比如这里我先把库存(可用库存,这里我强调下哈,一般都是商品详情页抢购,后来者进来看到的库存可能不再是后台系统配置的10个库存数了)放入redis队列:

$num=10;//库存$len=\Redis::llen('goods_store:1');//检查库存,goods_store:1 定义为健名$count = $num-$len; //实际库存-被抢购的库存 = 剩余可用库存for($i=0;$i<$count;$i++)  \Redis::lpush('goods_store:1',1);//往goods_store列表中,未抢购之前这里应该是默认滴push10个库存数了

 //echo \Redis::llen('goods_store:1');//未抢购之前这里就是10了

好吧,抢购时间到了:

/*模拟抢购操作,抢购前判断redis队列库存量*/$count=\Redis::lpop('goods_store:1');//lpop是移除并返回列表的第一个元素。if(!$count)return'已经抢光了哦';
/* 下面处理抢购成功流程 */
\DB::table('goods')->decrement('num', 1);//减少num库存字段

用户抢购成功后,上面的我们也可以稍微优化下,比如我们可用将用户ID存入了order:1列表中。接下来我们可以引导这些用户去完成订单的其他步骤,到这里才涉及到与数据库的交互。最终只有很少的人走到这一步吧,也就解决的数据库的压力问题。
我们再改下上面的代码:

$user_id=  \Session::get('user_id');//当前抢购用户id/*模拟抢购操作,抢购前判断redis队列库存量*/$count=\Redis::lpop('goods_store:1');if(!$count)
  return'已经抢光了哦';$result= \Redis::lpush('order:1',$user_id);if($result)
  return'恭喜您!抢到了哦';        

为了检测实际效果,我使用jmeter工具模拟100、200、1000个用户并发进行抢购,经过大量的测试,最终抢购成功的用户始终为10,没有出现“超抢/超卖”。

上面只是简单模拟高并发下的抢购思路,真实场景要比这复杂很多,比如双11活动远远比这更复杂多啦,很多注意的地方如抢购活动页面做成静态的,通过ajax调用接口
再如上面的会导致一个用户抢多个,思路:
需要一个排队队列(比如:queue:1,以user_id为值的列表)和抢购结果队列(比如:order:1,以user_id为值的列表)及库存队列(比如上面的goods_store:1)。高并发情况,先将用户进入排队队列,用一个线程循环处理从排队队列取出一个用户,判断用户是否已在抢购结果队列,如果在则已抢购,否则未抢购,接着执行库存减1,写入数据库,将此user_id用户同时也进入结果队列。



【原创】纯干货,Spring-data-jpa详解,全方位介绍。 - 神一样的存在 - 博客园

$
0
0

本篇进行Spring-data-jpa的介绍,几乎涵盖该框架的所有方面,在日常的开发当中,基本上能满足所有需求。这里不讲解JPA和Spring-data-jpa单独使用,所有的内容都是在和Spring整合的环境中实现。如果需要了解该框架的入门,百度一下,很多入门的介绍。在这篇文章的接下来一篇,会有一个系列来讲解mybatis,这个系列从mybatis的入门开始,到基本使用,和spring整合,和第三方插件整合,缓存,插件,最后会持续到mybatis的架构,源码解释,重点会介绍几个重要的设计模式,这样一个体系。基本上讲完之后,mybatis在你面前就没有了秘密,你能解决mybatis的几乎所有问题,并且在开发过程中相当的方便,驾轻就熟。

这篇文章由于介绍的类容很全,因此很长,如果你需要,那么可以耐心的看完,本人经历了很长时间的学识,使用,研究的心血浓缩成为这么短短的一篇博客。

大致整理一个提纲:

  1、Spring-data-jpa的基本介绍;

  2、和Spring整合;

  3、基本的使用方式;

  4、复杂查询,包括多表关联,分页,排序等;

现在开始:

  1、Spring-data-jpa的基本介绍:JPA诞生的缘由是为了整合第三方ORM框架,建立一种标准的方式,百度百科说是JDK为了实现ORM的天下归一,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate是一支很大的部队,使用很广泛,也很方便,能力也很强,同时Hibernate也是和JPA整合的比较良好,我们可以认为JPA是标准,事实上也是,JPA几乎都是接口,实现都是Hibernate在做,宏观上面看,在JPA的统一之下Hibernate很良好的运行。

  上面阐述了JPA和Hibernate的关系,那么Spring-data-jpa又是个什么东西呢?这地方需要稍微解释一下,我们做Java开发的都知道Spring的强大,到目前为止,企业级应用Spring几乎是无所不能,无所不在,已经是事实上的标准了,企业级应用不使用Spring的几乎没有,这样说没错吧。而Spring整合第三方框架的能力又很强,他要做的不仅仅是个最早的IOC容器这么简单一回事,现在Spring涉及的方面太广,主要是体现在和第三方工具的整合上。而在与第三方整合这方面,Spring做了持久化这一块的工作,我个人的感觉是Spring希望把持久化这块内容也拿下。于是就有了Spring-data-**这一系列包。包括,Spring-data-jpa,Spring-data-template,Spring-data-mongodb,Spring-data-redis,还有个民间产品,mybatis-spring,和前面类似,这是和mybatis整合的第三方包,这些都是干的持久化工具干的事儿。

  这里介绍Spring-data-jpa,表示与jpa的整合。

  2、我们都知道,在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。我们一般按照三层结构来看的话,Service层做业务逻辑处理,Dao层和数据库打交道,在Dao中,就存在着上面的对象。那么ORM框架本身提供的功能有什么呢?答案是基本的CRUD,所有的基础CRUD框架都提供,我们使用起来感觉很方便,很给力,业务逻辑层面的处理ORM是没有提供的,如果使用原生的框架,业务逻辑代码我们一般会自定义,会自己去写SQL语句,然后执行。在这个时候,Spring-data-jpa的威力就体现出来了,ORM提供的能力他都提供,ORM框架没有提供的业务逻辑功能Spring-data-jpa也提供,全方位的解决用户的需求。使用Spring-data-jpa进行开发的过程中,常用的功能,我们几乎不需要写一条sql语句,至少在我看来,企业级应用基本上可以不用写任何一条sql,当然spring-data-jpa也提供自己写sql的方式,这个就看个人怎么选择,都可以。我觉得都行。

  2.1与Spring整合我们从spring配置文件开始,为了节省篇幅,这里我只写出配置文件的结构。

复制代码
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:aop="http://www.springframework.org/schema/aop" 
    xmlns:tx="http://www.springframework.org/schema/tx" 
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:mongo="http://www.springframework.org/schema/data/mongo"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/aop     
           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd   
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
           http://www.springframework.org/schema/context     
           http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/data/mongo
           http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd
           http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"><!-- 数据库连接 --><context:property-placeholder location="classpath:your-config.properties" ignore-unresolvable="true" /><!-- service包 --><context:component-scan base-package="your service package" /><!-- 使用cglib进行动态代理 --><aop:aspectj-autoproxy proxy-target-class="true" /><!-- 支持注解方式声明式事务 --><tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" /><!-- dao --><jpa:repositories base-package="your dao package" repository-impl-postfix="Impl" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager" /><!-- 实体管理器 --><bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"><property name="dataSource" ref="dataSource" /><property name="packagesToScan" value="your entity package" /><property name="persistenceProvider"><bean class="org.hibernate.ejb.HibernatePersistence" /></property><property name="jpaVendorAdapter"><bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"><property name="generateDdl" value="false" /><property name="database" value="MYSQL" /><property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" /><!-- <property name="showSql" value="true" /> --></bean></property><property name="jpaDialect"><bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" /></property><property name="jpaPropertyMap"><map><entry key="hibernate.query.substitutions" value="true 1, false 0" /><entry key="hibernate.default_batch_fetch_size" value="16" /><entry key="hibernate.max_fetch_depth" value="2" /><entry key="hibernate.generate_statistics" value="true" /><entry key="hibernate.bytecode.use_reflection_optimizer" value="true" /><entry key="hibernate.cache.use_second_level_cache" value="false" /><entry key="hibernate.cache.use_query_cache" value="false" /></map></property></bean><!-- 事务管理器 --><bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"><property name="entityManagerFactory" ref="entityManagerFactory"/></bean><!-- 数据源 --><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"><property name="driverClassName" value="${driver}" /><property name="url" value="${url}" /><property name="username" value="${userName}" /><property name="password" value="${password}" /><property name="initialSize" value="${druid.initialSize}" /><property name="maxActive" value="${druid.maxActive}" /><property name="maxIdle" value="${druid.maxIdle}" /><property name="minIdle" value="${druid.minIdle}" /><property name="maxWait" value="${druid.maxWait}" /><property name="removeAbandoned" value="${druid.removeAbandoned}" /><property name="removeAbandonedTimeout" value="${druid.removeAbandonedTimeout}" /><property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" /><property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" /><property name="validationQuery" value="${druid.validationQuery}" /><property name="testWhileIdle" value="${druid.testWhileIdle}" /><property name="testOnBorrow" value="${druid.testOnBorrow}" /><property name="testOnReturn" value="${druid.testOnReturn}" /><property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" /><property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" /><property name="filters" value="${druid.filters}" /></bean><!-- 事务 --><tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><tx:method name="*" /><tx:method name="get*" read-only="true" /><tx:method name="find*" read-only="true" /><tx:method name="select*" read-only="true" /><tx:method name="delete*" propagation="REQUIRED" /><tx:method name="update*" propagation="REQUIRED" /><tx:method name="add*" propagation="REQUIRED" /><tx:method name="insert*" propagation="REQUIRED" /></tx:attributes></tx:advice><!-- 事务入口 --><aop:config><aop:pointcut id="allServiceMethod" expression="execution(* your service implements package.*.*(..))" /><aop:advisor pointcut-ref="allServiceMethod" advice-ref="txAdvice" /></aop:config></beans>
复制代码

  2.2对上面的配置文件进行简单的解释,只对“实体管理器”和“dao”进行解释,其他的配置在任何地方都差不太多。

    1.对“实体管理器”解释:我们知道原生的jpa的配置信息是必须放在META-INF目录下面的,并且名字必须叫做persistence.xml,这个叫做persistence-unit,就叫做持久化单元,放在这下面我们感觉不方便,不好,于是Spring提供了

org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean

这样一个类,可以让你的随心所欲的起这个配置文件的名字,也可以随心所欲的修改这个文件的位置,只需要在这里指向这个位置就行。然而更加方便的做法是,直接把配置信息就写在这里更好,于是就有了这实体管理器这个bean。使用

<property name="packagesToScan" value="your entity package" />

这个属性来加载我们的entity。

  2.3 解释“dao”这个bean。这里衍生一下,进行一下名词解释,我们知道dao这个层叫做Data Access Object,数据库访问对象,这是一个广泛的词语,在jpa当中,我们还有一个词语叫做Repository,这里我们一般就用Repository结尾来表示这个dao,比如UserDao,这里我们使用UserRepository,当然名字无所谓,随意取,你可以意会一下我的意思,感受一下这里的含义和区别,同理,在mybatis中我们一般也不叫dao,mybatis由于使用xml映射文件(当然也提供注解,但是官方文档上面表示在有些地方,比如多表的复杂查询方面,注解还是无解,只能xml),我们一般使用mapper结尾,比如我们也不叫UserDao,而叫UserMapper。

  上面拓展了一下关于dao的解释,那么这里的这个配置信息是什么意思呢?首先base-package属性,代表你的Repository接口的位置,repository-impl-postfix属性代表接口的实现类的后缀结尾字符,比如我们的UserRepository,那么他的实现类就叫做UserRepositoryImpl,和我们平时的使用习惯完全一致,于此同时,spring-data-jpa的习惯是接口和实现类都需要放在同一个包里面(不知道有没有其他方式能分开放,这不是重点,放在一起也无所谓,影响不大),再次的,这里我们的UserRepositoryImpl这个类的定义的时候我们不需要去指定实现UserRepository接口,根据spring-data-jpa自动就能判断二者的关系。

  比如:我们的UserRepository和UserRepositoryImpl这两个类就像下面这样来写。

public interface UserRepository extends JpaRepository<User, Integer>{}
public class UserRepositoryImpl {}

  那么这里为什么要这么做呢?原因是:spring-data-jpa提供基础的CRUD工作,同时也提供业务逻辑的功能(前面说了,这是该框架的威力所在),所以我们的Repository接口要做两项工作,继承spring-data-jpa提供的基础CRUD功能的接口,比如JpaRepository接口,同时自己还需要在UserRepository这个接口中定义自己的方法,那么导致的结局就是UserRepository这个接口中有很多的方法,那么如果我们的UserRepositoryImpl实现了UserRepository接口,导致的后果就是我们势必需要重写里面的所有方法,这是Java语法的规定,如此一来,悲剧就产生了,UserRepositoryImpl里面我们有很多的@Override方法,这显然是不行的,结论就是,这里我们不用去写implements部分。

  spring-data-jpa实现了上面的能力,那他是怎么实现的呢?这里我们通过源代码的方式来呈现他的来龙去脉,这个过程中cglib发挥了杰出的作用。

  在spring-data-jpa内部,有一个类,叫做

public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>,
        JpaSpecificationExecutor<T>

我们可以看到这个类是实现了JpaRepository接口的,事实上如果我们按照上面的配置,在同一个包下面有UserRepository,但是没有UserRepositoryImpl这个类的话,在运行时期UserRepository这个接口的实现就是上面的SimpleJpaRepository这个接口。而如果有UserRepositoryImpl这个文件的话,那么UserRepository的实现类就是UserRepositoryImpl,而UserRepositoryImpl这个类又是SimpleJpaRepository的子类,如此一来就很好的解决了上面的这个不用写implements的问题。我们通过阅读这个类的源代码可以发现,里面包装了entityManager,底层的调用关系还是entityManager在进行CRUD。

  3. 下面我们通过一个完整的项目来基本使用spring-data-jpa,然后我们在介绍他的高级用法。

  a.数据库建表:user,主键自增

  

  b.对应实体:User

复制代码
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String password;
    private String birthday;
    // getter,setter
}
复制代码

  c.简历UserRepository接口

public interface UserRepository extends JpaRepository<User, Integer>{}

  通过上面3步,所有的工作就做完了,User的基础CRUD都能做了,简约而不简单。

  d.我们的测试类UserRepositoryTest

复制代码
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void baseTest() throws Exception {
        User user = new User();
        user.setName("Jay");
        user.setPassword("123456");
        user.setBirthday("2008-08-08");
        userRepository.save(user);
//        userRepository.delete(user);
//        userRepository.findOne(1);
    }
}
复制代码

  测试通过。

  说到这里,和spring已经完成。接下来第三点,基本使用。

4.前面把基础的东西说清楚了,接下来就是spring-data-jpa的正餐了,真正威力的地方。

  4.1 我们的系统中一般都会有用户登录这个接口,在不使用spring-data-jpa的时候我们怎么做,首先在service层定义一个登录方法。如:

User login(String name, String password);

然后在serviceImpl中写该方法的实现,大致这样:

    @Override
    public User login(String name, String password) {
        return userDao.login(name, password);
    }

接下来,UserDao大概是这么个样子:

User getUserByNameAndPassword(String name, String password);

然后在UserDaoImpl中大概是这么个样子:

    public User getUserByNameAndPassword(String name, String password) {
        Query query = em.createQuery("select * from User t where t.name = ?1 and t.password = ?2");
        query.setParameter(1, name);
        query.setParameter(2, password);
        return (User) query.getSingleResult();
    }

ok,这个代码运行良好,那么这样子大概有十来行代码,我们感觉这个功能实现了,很不错。然而这样子真正简捷么?如果这样子就满足了,那么spring-data-jpa就没有必要存在了,前面提到spring-data-jpa能够帮助你完成业务逻辑代码的处理,那他是怎么处理的呢?这里我们根本不需要UserDaoImpl这个类,只需要在UserRepository接口中定义一个方法

User findByNameAndPassword(String name, String password);

然后在service中调用这个方法就完事了,所有的逻辑只需要这么一行代码,一个没有实现的接口方法。通过debug信息,我们看到输出的sql语句是

select * from user where name = ? and password = ?

跟上面的传统方式一模一样的结果。这简单到令人发指的程度,那么这一能力是如何实现的呢?原理是:spring-data-jpa会根据方法的名字来自动生成sql语句,我们只需要按照方法定义的规则即可,上面的方法findByNameAndPassword,spring-data-jpa规定,方法都以findBy开头,sql的where部分就是NameAndPassword,被spring-data-jpa翻译之后就编程了下面这种形态:

where name = ? and password = ?

在举个例,如果是其他的操作符呢,比如like,前端模糊查询很多都是以like的方式来查询。比如根据名字查询用户,sql就是

select * from user where name like = ?

这里spring-data-jpa规定,在属性后面接关键字,比如根据名字查询用户就成了

User findByNameLike(String name);

被翻译之后的sql就是

select * from user where name like = ?

这也是简单到令人发指,spring-data-jpa所有的语法规定如下图:

通过上面,基本CRUD和基本的业务逻辑操作都得到了解决,我们要做的工作少到仅仅需要在UserRepository接口中定义几个方法,其他所有的工作都由spring-data-jpa来完成。

 接下来:就是比较复杂的操作了,比如动态查询,分页,下面详细介绍spring-data-jpa的第二大杀手锏,强大的动态查询能力。

在上面的介绍中,对于我们传统的企业级应用的基本操作已经能够基本上全部实现,企业级应用一般都会有一个模糊查询的功能,并且是多条的查询,在有查询条件的时候我们需要在where后面接上一个 xxx = yyy 或者 xxx like '% + yyy + %'类似这样的sql。那么我们传统的JDBC的做法是使用很多的if语句根据传过来的查询条件来拼sql,mybatis的做法也类似,由于mybatis有强大的动态xml文件的标签,在处理这种问题的时候显得非常的好,但是二者的原理都一致,那spring-data-jpa的原理也同样很类似,这个道理也就说明了解决多表关联动态查询根儿上也就是这么回事。

  那么spring-data-jpa的做法是怎么的呢?有两种方式。可以选择其中一种,也可以结合使用,在一般的查询中使用其中一种就够了,就是第二种,但是有一类查询比较棘手,比如报表相关的,报表查询由于涉及的表很多,这些表不一定就是两两之间有关系,比如字典表,就很独立,在这种情况之下,使用拼接sql的方式要容易一些。下面分别介绍这两种方式。

  a.使用JPQL,和Hibernate的HQL很类似。

   前面说道了在UserRepository接口的同一个包下面建立一个普通类UserRepositoryImpl来表示该类的实现类,同时前面也介绍了完全不需要这个类的存在,但是如果使用JPQL的方式就必须要有这个类。如下:

复制代码
public class StudentRepositoryImpl {
    @PersistenceContext
    private EntityManager em;
    @SuppressWarnings("unchecked")
    public Page<Student> search(User user) {
        String dataSql = "select t from User t where 1 = 1";
        String countSql = "select count(t) from User t where 1 = 1";
        if(null != user && !StringUtils.isEmpty(user.getName())) {
            dataSql += " and t.name = ?1";
            countSql += " and t.name = ?1";
        }
        Query dataQuery = em.createQuery(dataSql);
        Query countQuery = em.createQuery(countSql);
        if(null != user && !StringUtils.isEmpty(user.getName())) {
            dataQuery.setParameter(1, user.getName());
            countQuery.setParameter(1, user.getName());
        }long totalSize = (long) countQuery.getSingleResult();
        Page<User> page = new Page();
        page.setTotalSize(totalSize);
        List<User> data = dataQuery.getResultList();
        page.setData(data);
        return page;
    }
}
复制代码

通过上面的方法,我们查询并且封装了一个User对象的分页信息。代码能够良好的运行。这种做法也是我们传统的经典做法。那么spring-data-jpa还有另外一种更好的方式,那就是所谓的类型检查的方式,上面我们的sql是字符串,没有进行类型检查,而下面的方式就使用了类型检查的方式。这个道理在mybatis中也有体现,mybatis可以使用字符串sql的方式,也可以使用接口的方式,而mybatis的官方推荐使用接口方式,因为有类型检查,会更安全。

  b.使用JPA的动态接口,下面的接口我把注释删了,为了节省篇幅,注释也没什么用,看方法名字大概都能猜到是什么意思。

复制代码
public interface JpaSpecificationExecutor<T> {

    T findOne(Specification<T> spec);

    List<T> findAll(Specification<T> spec);

    Page<T> findAll(Specification<T> spec, Pageable pageable);

    List<T> findAll(Specification<T> spec, Sort sort);

    long count(Specification<T> spec);
}
复制代码

 上面说了,使用这种方式我们压根儿就不需要UserRepositoryImpl这个类,说到这里,仿佛我们就发现了spring-data-jpa为什么把Repository和RepositoryImpl文件放在同一个包下面,因为我们的应用很可能根本就一个Impl文件都不存在,那么在那个包下面就只有一堆接口,即使把Repository和RepositoryImpl都放在同一个包下面,也不会造成这个包下面有正常情况下2倍那么多的文件,根本原因:只有接口而没有实现类。

上面我们的UserRepository类继承了JpaRepository和JpaSpecificationExecutor类,而我们的UserRepository这个对象都会注入到UserService里面,于是如果使用这种方式,我们的逻辑直接就写在service里面了,下面的代码:一个学生Student类,一个班级Clazz类,Student里面有一个对象Clazz,在数据库中是clazz_id,这是典型的多对一的关系。我们在配置好entity里面的关系之后。就可以在StudentServiceImpl类中做Student的模糊查询,典型的前端grid的模糊查询。代码是这样子的:

复制代码
@Service
public class StudentServiceImpl extends BaseServiceImpl<Student> implements StudentService {
    @Autowired
    private StudentRepository studentRepository;
    @Override
    public Student login(Student student) {
        return studentRepository.findByNameAndPassword(student.getName(), student.getPassword());
    }

    @Override
    public Page<Student> search(final Student student, PageInfo page) {
        return studentRepository.findAll(new Specification<Student>() {
            @Override
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate stuNameLike = null;
                if(null != student && !StringUtils.isEmpty(student.getName())) {    
           // 这里也可以root.get("name").as(String.class)这种方式来强转泛型类型 stuNameLike = cb.like(root.<String> get("name"), "%" + student.getName() + "%"); } Predicate clazzNameLike = null; if(null != student && null != student.getClazz() && !StringUtils.isEmpty(student.getClazz().getName())) { clazzNameLike = cb.like(root.<String> get("clazz").<String> get("name"), "%" + student.getClazz().getName() + "%"); } if(null != stuNameLike) query.where(stuNameLike); if(null != clazzNameLike) query.where(clazzNameLike); return null; } }, new PageRequest(page.getPage() - 1, page.getLimit(), new Sort(Direction.DESC, page.getSortName()))); } }
复制代码

先解释下这里的意思,然后我们在结合框架的源码来深入分析。

这里我们是2个表关联查询,查询条件包括Student表和Clazz表,类似的2个以上的表方式差不多,但是正如上面所说,这种做法适合所有的表都是两两能够关联上的,涉及的表太多,或者是有一些字典表,那就使用sql拼接的方式,简单一些。

先简单解释一下代码的含义,然后结合框架源码来详细分析。两个Predicate对象,Predicate按照中文意思是判断,断言的意思,那么放在我们的sql中就是where后面的东西,比如

name like '% + jay + %';

下面的PageRequest代表分页信息,PageRequest里面的Sort对象是排序信息。上面的代码事实上是在动态的组合最终的sql语句,这里使用了一个策略模式,或者callback,就是

studentRepository.findAll(一个接口)

studentRepository接口方法调用的参数是一个接口,而接口的实现类调用这个方法的时候,在内部,参数对象的实现类调用自己的toPredicate这个方法的实现内容,可以体会一下这里的思路,就是传一个接口,然后接口的实现自己来定义,这个思路在nettyJavaScript中体现的特别明显,特别是JavaScript的框架中大量的这种方式,JS框架很多的做法都是上来先闭包,和浏览器的命名空间分开,然后入口方法就是一个回调,比如ExtJS:

Ext.onReady(function() {
    // xxx
});

参数是一个function,其实在框架内部就调用了这个参数,于是这个这个方法执行了。这种模式还有一个JDK的排序集合上面也有体现,我们的netty框架也采用这种方式来实现异步IO的能力。

接下来结合框架源码来详细介绍这种机制,以及这种机制提供给我们的好处。

 这里首先从JPA的动态查询开始说起,在JPA提供的API中,动态查询大概有这么一些方法,

从名字大概可以看出这些方法的意义,跟Hibernate或者一些其他的工具也都差不多,这里我们介绍参数为CriteriaQuery类型的这个方法,如果我们熟悉多种ORM框架的话,不难发现都有一个Criteria类似的东西,中文意思是“条件”的意思,这就是各个框架构建动态查询的主体,Hibernate甚至有两种,在线和离线两种Criteria,mybatis也能从Example中创建Criteria,并且添加查询条件。

那么第一步就需要构建出这个参数CriteriaQuery类型的参数,这里使用建造者模式,

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Student> query = builder.createQuery(Student.class);

接下来:

Root<Student> root = query.from(Student.class);

在这里,我们看方法名from,意思是获取Student的Root,其实也就是个Student的包装对象,就代表这条sql语句里面的主体。接下来:

        Predicate p1 = builder.like(root.<String> get("name"), "%" + student.getName() + "%");
        Predicate p2 = builder.equal(root.<String> get("password"), student.getPassword());

Predicate是判断的意思,放在sql语句中就是where后面 xxx = yyy, xxx like yyy这种,也就是查询条件,这里构造了2个查询条件,分别是根据student的name属性进行like查询和根据student的password进行“=”查询,在sql中就是

name like = ? and password = ?

这种形式,接下来

query.where(p1, p2);

这样子一个完整的动态查询就构建完成了,接下来调用getSingleResult或者getResultList返回结果,这里jpa的单个查询如果为空的话会报异常,这点感觉框架设计的不好,如果查询为空直接返回一个null或者一个空的List更好一点。

这是jpa原生的动态查询方式,过程大致就是,创建builder => 创建Query => 构造条件 => 查询。这么4个步骤,这里代码运行良好,如果不使用spring-data-jpa,我们就需要这么来做,但是spring-data-jpa帮我们做得更为彻底,从上面的4个步骤中,我们发现:所有的查询除了第三步不一样,其他几步都是一模一样的,不使用spring-data-jpa的情况下,我们要么4步骤写完,要么自己写个工具类,封装一下,这里spring-data-jpa就是帮我们完成的这样一个动作,那就是在JpaSpecification<T>这个接口中的

Page<T> findAll(Specification<T> spec, Pageable pageable);

这个方法,前面说了,这是个策略模式,参数spec是个接口,前面也说了框架内部对于这个接口有默认的实现类

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>,
        JpaSpecificationExecutor<T> {
}

,我们的Repository接口就是继承这个接口,而通过cglib的RepositoryImpl的代理类也是这个类的子类,默认也就实现了该方法。这个方法的方法体是这样的:

复制代码
    /*
     * (non-Javadoc)
     * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findOne(org.springframework.data.jpa.domain.Specification)
     */
    public T findOne(Specification<T> spec) {

        try {
            return getQuery(spec, (Sort) null).getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }
复制代码

这里的

getQuery(spec, (Sort) null)

返回类型是

TypedQuery<T>

进入这个getQuery方法:

复制代码
    /**
     * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
     * 
     * @param spec can be {@literal null}.
     * @param sort can be {@literal null}.
     * @return
     */
    protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {

        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<T> query = builder.createQuery(getDomainClass());

        Root<T> root = applySpecificationToCriteria(spec, query);
        query.select(root);

        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }

        return applyRepositoryMethodMetadata(em.createQuery(query));
    }
复制代码

一切玄机尽收眼底,这个方法的内容和我们前面使用原生jpa的api的过程是一样的,而再进入

Root<T> root = applySpecificationToCriteria(spec, query);

这个方法:

复制代码
    /**
     * Applies the given {@link Specification} to the given {@link CriteriaQuery}.
     * 
     * @param spec can be {@literal null}.
     * @param query must not be {@literal null}.
     * @return
     */
    private <S> Root<T> applySpecificationToCriteria(Specification<T> spec, CriteriaQuery<S> query) {

        Assert.notNull(query);
        Root<T> root = query.from(getDomainClass());

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }
复制代码

我们可以发现spec参数调用了toPredicate方法,也就是我们前面service里面匿名内部类的实现。

到这里spring-data-jpa的默认实现已经完全明了。总结一下使用动态查询:前面说的原生api需要4步,而使用spring-data-jpa只需要一步,那就是重写匿名内部类的toPredicate方法。在重复一下上面的Student和Clazz的查询代码,

复制代码
 1     @Override
 2     public Page<Student> search(final Student student, PageInfo page) {
 4         return studentRepository.findAll(new Specification<Student>() {
 5             @Override
 6             public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
 7                 
 8                 Predicate stuNameLike = null;
 9                 if(null != student && !StringUtils.isEmpty(student.getName())) {
10                     stuNameLike = cb.like(root.<String> get("name"), "%" + student.getName() + "%");
11                 }
12                 
13                 Predicate clazzNameLike = null;
14                 if(null != student && null != student.getClazz() && !StringUtils.isEmpty(student.getClazz().getName())) {
15                     clazzNameLike = cb.like(root.<String> get("clazz").<String> get("name"), "%" + student.getClazz().getName() + "%");
16                 }
17                 
18                 if(null != stuNameLike) query.where(stuNameLike);
19                 if(null != clazzNameLike) query.where(clazzNameLike);
20                 return null;
21             }
22         }, new PageRequest(page.getPage() - 1, page.getLimit(), new Sort(Direction.DESC, page.getSortName())));
23     }
复制代码

到这里位置,spring-data-jpa的介绍基本上就完成了,涵盖了该框架使用的方方面面。接下来还有一块比较实用的东西,我们看到上面第15行位置的条件查询,这里使用了一个多级的get,这个是spring-data-jpa支持的,就是嵌套对象的属性,这种做法一般我们叫方法的级联调用,就是调用的时候返回自己本身,这个在处理xml的工具中比较常见,主要是为了代码的美观作用,没什么其他的用途。

最后还有一个小问题,我们上面说了使用动态查询和JPQL两种方式都可以,在我们使用JPQL的时候,他的语法和常规的sql有点不太一样,以Student、Clazz关系为例,比如:

select * from student t left join clazz tt on t.clazz_id = tt.id

这是一个很常规的sql,但是JPQL是这么写:

select t from Student t left join t.clazz tt

left join右边直接就是t的属性,并且也没有了on t.clazz_id == tt.id,然而并不会出现笛卡尔积,这里解释一下为什么没有这个条件,在我们的实体中配置了属性的映射关系,并且ORM框架的最核心的目的就是要让我们以面向对象的方式来操作数据库,显然我们在使用这些框架的时候就不需要关心数据库了,只需要关系对象,而t.clazz_id = tt.id这个是数据库的字段,由于配置了字段映射,框架内部自己就会去处理,所以不需要on t.clazz_id = tt.id就是合理的。

  前面介绍了spring-data-jpa的使用,还有一点忘了,悲观所和乐观锁问题,这里的乐观锁比较简单,jpa有提供注解@Version,加上该注解,自动实现乐观锁,byId修改的时候sql自动变成:update ... set ... where id = ? and version = ?,比较方便。

 in操作的查询:

  在日常手动写sql的时候有in这种查询是比较多的,比如select * from user t where t.id in (1, 2, 3);有人说in的效率不高,要少用,但是其实只要in是主键,或者说是带有索引的,效率是很高的,mysql中如果in是子查询貌似不会走索引,不过我个人经验,在我遇到的实际应用中,in(ids)这种是比较多的,所以一般来说是没有性能问题的。

  那么,sql里面比较好写,但是如果使用spring-data-jpa的动态查询方式呢,就和前面的稍微有点区别。大致上是这么一个思路:

复制代码
if(!CollectionUtils.isEmpty(ids)) {
    In<Long> in = cb.in(root.<Long> get("id"));
    for (Long id : parentIds) {
        in.value(id);
    }
    query.where(in);
}
复制代码

  cb创建一个in的Predicate,然后给这个in赋值,最后把in加到where条件中。

手动配置锁:

  spring-data-jpa支持注解方式的sql,比如:@Query(xxx),另外,关于锁的问题,在实体中的某个字段配置@Version是乐观锁,有时候为了使用一个悲观锁,或者手动配置一个乐观锁(如果实体中没有version字段),那么可以使用@Lock这个注解,它能够被解析成为相关的锁。

一对多、多对多查询(查询条件在关联对象中时):

  1、在JPA中,一个实体中如果存在多个关联对象,那么不能同时eager获取,只能有一个是eager获取,其他只能lazy;在Hibernate当中有几种独有的解决方法,在JPA当中有2中方法,i.就是前面的改成延时加载;ii.把关联对象的List改成Set(List允许重复,在多层抓去的时候无法完成映射,Hibernate默认抓去4层,在第三层的时候如果是List就无法完成映射)。

  2、在多对多的查询中,我们可以使用JPQL,也可以使用原生SQL,同时还可以使用动态查询,这里介绍多对多的动态查询,这里有一个条件比较苛刻,那就是查询参数是关联对象的属性,一对多类似,多对一可以利用上面介绍的级联获取属性的方式。这里介绍这种方式的目的是为了更好的利用以面向对象的方式进行动态查询。

  举例:2张表,分别是Employee(id, name)和Company(id, name),二者是多对多的关系,那么当查询Employee的时候,条件是更具公司名称。那么做法如下:

复制代码
    @Override
    public List<Employee> findByCompanyName(final String companyName) {
        List<Employee> employeeList = employeeRepository.findAll(new Specification<Employee>() {
            public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//                ListJoin<Employee, Company> companyJoin = root.join(root.getModel().getList("companyList", Company.class), JoinType.LEFT);
                Join<Employee, Company> companyJoin = root.join("companyList", JoinType.LEFT);
                return cb.equal(companyJoin.get("name"), companyName);
            }
        });
        return employeeList;
    }
复制代码

     我们可以使用上面注释掉的方式,也可以使用下面这种比较简单的方式。因为我个人的习惯是尽量不去写DAO的实现类,除非查询特别复杂,万不得已的情况下采用,否则我个人比较偏向于这种方式。

  上面的情况如果更为极端的话,关联多个对象,可以按照下面的方式:

复制代码
    @Override
    public List<Employee> findByCompanyName(final String companyName, final String wage) {
        List<Employee> employeeList = employeeRepository.findAll(new Specification<Employee>() {
            public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//                ListJoin<Employee, Company> companyJoin = root.join(root.getModel().getList("companyList", Company.class), JoinType.LEFT);
                Join<Employee, Company> companyJoin = root.join("companySet", JoinType.LEFT);
                Join<Employee, Wage> wageJoin = root.join("wageSet", JoinType.LEFT);
                Predicate p1 = cb.equal(companyJoin.get("name"), companyName);
                Predicate p2 = cb.equal(wageJoin.get("name"), wage);
//              return cb.and(p1, p2);根据spring-data-jpa的源码,可以返回一个Predicate,框架内部会自动做query.where(p)的操作,也可以直接在这里处理,然后返回null,///              也就是下面一段源码中的实现
                query.where(p1, p2);
                return null;
            }
        });
        
        return employeeList;
    }
复制代码

  

复制代码
    /**
     * Applies the given {@link Specification} to the given {@link CriteriaQuery}.
     * 
     * @param spec can be {@literal null}.
     * @param query must not be {@literal null}.
     * @return
     */
    private <S> Root<T> applySpecificationToCriteria(Specification<T> spec, CriteriaQuery<S> query) {

        Assert.notNull(query);
        Root<T> root = query.from(getDomainClass());

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);
    // 这里如果我们重写的toPredicate方法的返回值predicate不为空,那么调用query.where(predicate)
        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }
复制代码

 

说明:虽然说JPA中这种方式查询会存在着多次级联查询的问题,对性能有所影响,但是在一般的企业级应用当中,为了开发的便捷,这种性能牺牲一般来说是可以接受的。

  特别的:在一对多中或者多对一中,即便是fetch为eager,也会先查询主对象,再查询关联对象,但是在eager的情况下虽然是有多次查询问题,但是没有n+1问题,关联对象不会像n+1那样多查询n次,而仅仅是把关联对象一次性查询出来,因此,在企业级应用当中,访问量不大的情况下一般来说没什么问题。

  补充一段题外话,关于Hibernate/JPA/Spring-Data-Jpa与MyBatis的区别联系,这种话题很多讨论,对于Hibernate/JPA/Spring-Data-Jpa,我个人而言基本上能够熟练使用,谈不上精通,对于mybatis,由于深入阅读过几次它的源码,对mybatis的设计思想以及细化到具体的方法,属性,参数算是比较熟悉,也开发过一些mybatis的相关插件。对于这两个持久化框架,总体来说的区别是,Hibernate系列的门槛相对较高,配置比较多,相对来说难度要大一些,主要体现在各种关系的问题上,据我所知,很多人的理解其实并不深刻,很多时候甚至配置得有一定的问题,但是优势也很明显,SQL自动生成,改数据库表结构仅仅需要调整几个注解就行了,在熟练使用的基础上相对来说要便捷一点。对于mybatis来说,门槛很低,真的很低,低到分分钟就能入门的程度,我个人最喜欢也是mybatis最吸引人的地方就是灵活,特别的灵活,但是修改数据库表结构之后需要调整的地方比较多,但是利用目前比较优秀的插件,对于单表操作也基本上能够达到和Hibernate差不多的境界(会稍微牺牲一点点性能),多表的情况下就要麻烦一点。性能方面的比较,由于我没做过测试,不太好比较,不过应该mybatis要稍微高一些,毕竟他的查询SQL可控一些(当然Hibernate也支持原生sql,但是对结果集的处理不够友好)。

  之后更新:Root对象还有一批fetch方法,这个目前我很少用,后面有时间再来更新。

  补充:单表分页可以传入分页对象,比如findByName(String name, new pageRequest(0, 10));

Hi3515移动侦测技术的设计与实现 - CSDN博客

$
0
0

一、MD(移动侦测):
    移动侦测是检测正在视频编码的图像是 否发生亮度变化以及相应的运动向量。移动侦测通道就是视频编码通道,最大支持运动侦测路数与编码路数相同。
    Hi3520/Hi3515 提供的移动侦测功能以宏块为最小单位,计算指定图像的宏块在指定图像间隔内的亮度变化和运动向量。如需要获取移动侦测的结果,则启用某一视频编码通道的移动侦测功能。移动侦测的结果包括 宏块 SAD、宏块运动向量 MV、宏块报警信息、宏块报警像素的个数
    MD(Move Detect)模块支持 H.264编码和 MJPEG编码时进行移动侦测,针对同一 VI通道的主次码流,二者只能有一个作 MD。

    实际工作中,主要获取 宏块报警信息,宏块报警信息为侦测某个宏块(16*16以像素为单位)的结果:要么1,要么0,如果1的累加值超过阈值,就报警,当然这是相对简单的,如果想复杂点,比如同时可以通过相对静像的测试,判断出距离内报警信息等,是需要相对比较复杂的算法的,待续……

    另一种方式:使用视频芯片CX25828实现移动侦测: http://blog.csdn.net/huangminqiang201209/article/details/8292060 

    也可以有其他方法,比如网上比较多的H.264帧差法,还有就是转化为YUV,然后灰度值的原始像素点比较,从而得到帧之间的差别,从而进行判定。

 

二、侦测方法:  

    时间差分(又称相邻帧差)方法(Temporal Difference )是在连续的图像序列中两个或三个相邻帧间采用基于像素的时间差分并且阈值化来提取出图像中的运动区域。时间差分运动检测方法对于动态环境具有较强的自适应性,但一般不能完全提取出所有相关的特征。

    同时也还有背景减除法,即以一张图像为参考,是属于静态的,如果与参考不同,并符合报警阈值,则报警。还有光流法等   

 

三、侦测的实现:
    1.信号输入处理模块:标准模拟视频信号(CVBS彩色或黑白)是亮度信号和色度信号通过频普间置叠加在一起,需经过A/D芯片(如科胜讯CX25828)的解码,将模拟信号转成数字信号,产生标准的ITU 656 YUV格式的数字信号以帧为单位送到编码卡上的DSP和内存中。

    2.CP(Image Coprocessor 图象协处理器)处理模块:YUV数据在DSP中加上OSD(字符时间叠加)和LOGO(位图)等,复合后通过PCI总线送到显存中,供视频实时预览用,还将复合后的数据送到编码卡的内存中,供编码使用。

    3.ENCODER(编码)模块:将编码卡内存中的YUV数据送到MPEG4/H264编码器中,产生压缩好的码流,送到主机内存中,供录像或网络传输使用。

    4.MOTIONDETECT处理模块:对编码卡内存中的以帧为单位YUV数据进行处理。(移动侦测)。

 

三、重要概念

MD图像与MD参考图像

    比较 MD图像与 MD参考图像,来进行移动侦测。两者均可以随时间而不断更新。

MD间隔

    MD图像与MD参考图像之间的间隔,单位为帧。通俗地讲,就是几帧做一次MD。

MD数据

    也称 MD结果。

    Hi3520/Hi3515 可以输出四种MD数据:宏块 SAD,宏块 MV,宏块报警信息,宏块报警像素个数。

    每一种 MD数据是否输出,均可以通过开关进行控制。

    SDK 仅提供 MD数据,而不进行 MD判决。客户可根据不同的应用选择合适的MD数据进行 MD判决, 推荐使用宏块SAD作为MD判决依据。

宏块

    将图像划分为16*16(以像素为单位)大小的块,每一块称为一个宏块。

宏块SAD

    当前帧与参考帧相应宏块之间的亮度绝对差之和。

    SAD(Sum of Absolute Difference):绝对差之和。

    宏块 SAD越大,说明两帧对应宏块间的差别越大。

宏块MV

    参考帧至当前帧的各帧中相应宏块的运动向量的累加值,表示该 宏块运动的方向。不推荐 MV作为移动侦测的判断依据。

    MV(Motion Vector)运动向量。

宏块报警信息

    宏块是否报警。

    在去光照效应不打开的情况下,MD模块通过比较宏块 SAD与用户设置的 SAD报警阈值决定是否报警。

    在去光照效应打开的情况下,MD模块通过宏块 SAD,用户设置的 SAD报警阈值,结合用户设置的像素报警阈值、像素报警个数阈值,通过一定去光照效应运算后得到的结果。

Hi3520/Hi3515 芯片 暂不支持去光照效应。

    只有设定宏块报警 SAD 阈值 启用宏块报警信息输出时,才会输出该信息。//故与stMdAttr.u16MBALSADTh = 1000;//宏块SAD的阈值为1000相关

宏块报警像素个数

    宏块中报警的像素总数。

    MD模块通过比较像素亮度差(当前帧和参考帧之间)与用户设置的像素报警阈值决定像素是否报警。

    只有设定宏块报警像素阈值 启用宏块报警像素个数输出时,才会输出该数据。//故与stMdAttr.u8MBPelALTh = 20;//像素报警阈值为20相关

 

四、相关结构

1.定义运动侦测属性结构体:

typedef structhiMD_CHN_ATTR_S

{

 MD_MB_MODE_S stMBMode;  //宏块模式

 MD_SADBITS_E enSADBits;    //SAD输出精度

 MD_DLIGHT_S stDlight;         //去光照效应属性

 HI_U8  u8MBPelALTh;            //像素报警阈值。 取值范围:(0, 255)。

 HI_U8  u8MBPerALNumTh;     //像素报警个数阈值。 取值范围:(0, 255)。

 HI_U16  u16MBALSADTh;       //宏块报警阈值。 取值范围: 去光照效应打开时:(0, 255),去光照效应不打开时:(0, 65535)

 HI_U32  u32MDInternal;         //MD 侦测间隔。取值范围:[0, 256],以帧为单位。

 HI_U32  u32MDBufNum;         //MD缓存大小。 取值范围:[1, 16]。

} MD_CHN_ATTR_S;

 

2.定义参考图像属性结构体:

typedef structhiMD_REF_ATTR_S

{

 MD_REF_MODE_E enRefFrameMode;    //参考图像模式

 MD_REF_STATUS_E  enRefFrameStat; //参考图像更新状态

 VIDEO_FRAME_S stUserRefFrame;      //用户输入模式,参考图像的信息结构体

} MD_REF_ATTR_S;

【注意事项】

   参考图像是否更新均是在参考图像模式为自动模式的情况下。参考图像为用户设置模式时,此参考图像更新状态无效,在此模式下用户还需配置参考图像。

 

3.定义运动侦测结果结构体:

typedef struct hiMD_DATA_S

{

 HI_U32*  pu32Addr;   //MD结果地址信息,释放结果时需用到。不允许修改。

 HI_U16  u16MBWidth; //图像的宽度。以宏块为单位。

 HI_U16  u16MBHeight; //图像的高度。以宏块为单位。

 HI_U64  u64Pts;         //时间戳。

 

 MD_MB_MODE_S stMBMode;           //宏块模式

 MD_MB_DATA_S stMBSAD;              //宏块 SAD。

 MD_MB_DATA_S stMBMV;                //宏块 MV。

 MD_MB_DATA_S stMBAlarm;            //宏块报警信息。

 MD_MB_DATA_S stMBPelAlarmNum; //宏块报警像素个数。

} MD_DATA_S;

 

4.定义宏块结果结构体:

typedef structhiMD_MB_DATA_S

{

 HI_U32* pu32Addr; //宏块数据指针

 HI_U32 u32Stride;  //宏块数据以行为单位的内存宽度

} MD_MB_DATA_S;

 

5.定义宏块模式结构体:

typedef structhiMD_MB_MODE_S

{

 HI_BOOL bMBSADMode;     //宏块 SAD模式开关。取值范围:{HI_TRUE,HI_FALSE}

 HI_BOOL bMBMVMode;      //宏块 MV模式开关。

 HI_BOOL bMBALARMMode; //宏块报警模式开关。

 HI_BOOL bMBPelNumMode; //宏块报警像素个数模式开关。

} MD_MB_MODE_S;

 

6.定义去光照效应属性结构体:

typedef structhiMD_DLIGHT_S

{

 HI_BOOL bEnable; //去光照效应开关。取值范围:{HI_TRUE, HI_FALSE}。

 HI_U8 u8DlBeta;    //去光照效应 Beta 值。 取值范围:[0, 7]。

 HI_U8 u8DlAlpha;  //去光照效应 Alpha 值。取值范围:[0, 7]。

 HI_U16 Reserved; //保留。

} MD_DLIGHT_S;

 

7.定义运动侦测的 SAD的精度:

typedef enumhiMD_SADBITS_E

{

  MD_SAD_8BIT = 0, //SAD精度为 8bit。

 MD_SAD_16BIT,      //SAD精度为 16bit。

 MD_SAD_BUTT

} MD_SADBITS_E;

【注意事项】

    不管精度值如何,在运动侦测结果中,每个宏块 SAD都要占用 2byte 内存。

 

8.定义参考图像模式:

typedef enumhiMD_REF_MODE_E

{

  MD_REF_AUTO = 0, //自动设置参考图像模式。

  MD_REF_USER,       //用户输入参考图像模式。

  MD_REF_MODE_BUTT

} MD_REF_MODE_E;

 

9.定义参考图像更新状态:

typedef enumhiMD_REF_STATUS_E

{

  MD_REF_STATIC = 0, //参考图像不更新。

 MD_REF_DYNAMIC,     //参考图像更新。

 MD_REF_STATUS_BUTT

} MD_REF_STATUS_E;

【注意事项】

    参考图像是否更新均是在参考图像模式为自动模式的情况下。参考图像为用户设置模式时,此参考图像状态无效。

 

五、API参考

1.启用/禁用某一路视频编码通道的运动侦测功能

     HI_S32 HI_MPI_MD_EnableChn(VENC_CHNVeChn);

     HI_S32 HI_MPI_MD_DisableChn(VENC_CHNVeChn);

     在启用该编码通道的运动侦测前,相对应的编码通道必须已经创建,且注册到编码通道组 GROUP 中,否则启用失败。运动侦测与编码同时实现,若对应的编码通道没有启动编码,无论运动侦测是否启用,都不会进行运动侦测。

     在启用前,必须设置该视频编码通道的运动侦测属性。如果需要获取宏块 SAD、宏块报警信息、宏块报警像素个数,还必须设置参考图像属性。

     多次启用某一视频编码通道的运动侦测功能,和启用一次效果相同,都会返回成功。

     目前支持 H.264 编码和 MJPEG编码时,进行运动侦测。对于大小码流编码通道,只支持其中一个码流编码通道进行运动侦测

 

2.设置/获取运动侦测的属性

     HI_S32 HI_MPI_MD_SetChnAttr(VENC_CHNVeChn, const MD_CHN_ATTR_S *pstAttr);

     HI_S32 HI_MPI_MD_GetChnAttr(VENC_CHNVeChn, MD_CHN_ATTR_S *pstAttr);

     在启用运动侦测功能前,需要设置某一路视频编码通道的运动侦测属性。

     在设置运动侦测的属性前,必须保证运动侦测处于禁用状态。

     如果要获取宏块报警信息,在去光照效应不打开时,需设置宏块报警 SAD阈值;在去光照效应打开时,需要设置三个阈值:宏块的 SAD阈值、宏块内像素报警阈值和宏块内像素报警个数阈值  ( Hi3520/Hi3515芯片暂不支持去光照效应) 。当发现宏块阈值的变化超过设定的阈值,就认为该宏块是运动的,然后给出宏块的报警信息。

     如果要获取宏块报警信息,需设置宏块报警 SAD阈值。

     如果要获取宏块像素报警个数时,需设置宏块报警的像素阈值。

 

3.设置/获取运动侦测的参考图像属性

     HI_S32 HI_MPI_MD_SetRefFrame(VENC_CHNVeChn, const MD_REF_ATTR_S *pstAttr);

     HI_S32 HI_MPI_MD_GetRefFrame(VENC_CHNVeChn, MD_REF_ATTR_S *pstAttr);

     如果配置的某一视频编码通道运动侦测属性的运动侦测模式中包含宏块SAD、宏块报警信息、宏块像素报警个数模式中的其中一项或几项,

那么必须设置该视频编码通道运动侦测参考图像属性信息。

     自动设置参考图像时,用户可以根据需要,设置参考图像是否更新。

       −  如果不更新,就按照启用运动侦测后第一帧的编码图像作为参考。

       −  如果更新,就按照运动侦测属性中的运动侦测间隔进行更新。

     在设置运动侦测的参考图像属性之前必须保证运动侦测处于禁用状态,如果运动侦测为启用状态,则必须首先禁用运动侦测。

     Hi3520/Hi3515 均不支持用户输入参考图象模式。

 

4.获取/释放运动侦测结果

     HI_S32 HI_MPI_MD_GetData(VENC_CHN VeChn,MD_DATA_S *pstMdData, HI_U32 u32BlockFlag);

    u32BlockFlag:阻塞标志。  HI_IO_BLOCK:阻塞, HI_IO_NOBLOCK:非阻塞。

     如果运动侦测已经禁用,返回失败。

    支持阻塞和非阻塞接口。

     如果在获取过程中禁用 MD通道,则立即返回失败。

     虽然 MD可以提供四种 MD数据,但只有用户打开该数据输出的开关,才会输出该数据。

 

     HI_S32 HI_MPI_MD_ReleaseData(VENC_CHNVeChn, const MD_DATA_S *pstMdData);

     此接口需与 HI_MPI_MD_GetData配对使用。

     运动侦测结果需要在使用完之后立即释放,如果不及时释放会导致无运动侦测结果缓存而不能进行运动侦测。

     释放的运动侦测结果必须是从该通道获取的运动侦测结果,不得对运动侦测结果结构体进行任何修改,也不允许释放从别的通道获取的运动侦测结果,否则会导

致运动侦测结果不能释放,使此运动侦测结果缓存丢失,甚至导致程序异常。

     如果在释放运动侦测结果缓存过程中销毁通道,则立刻返回失败。

 

5.获取运动侦测通道对应的设备文件句柄

     HI_S32 HI_MPI_MD_GetFd(VENC_CHN VeChn);


六、部分实现代码(软件实现,直接调用上面的函数)

/**************************************************************************************************************
 **文件:MD.c
 **编写者:huangminqiang
 **编写日期:2012年4月27号
 **简要描述:移动侦测
 **修改者:
 **修改日期:
 **注:MD模块
 **************************************************************************************************************/
 
 /***** MD的初始化 ********************************************************************************************/
HI_S32 EnableMd(void)
{
	HI_S32 s32Ret;
	VENC_CHN VeChn = VENCCHNID;
  	MD_CHN_ATTR_S stMdAttr;
  	MD_REF_ATTR_S  stRefAttr;

	/*set MD attribute*/
	//使能宏块SAD 模式,不使能 宏块MV 模式、宏块报警模式、宏块报警像素个数模式。
	stMdAttr.stMBMode.bMBSADMode =HI_TRUE;
	stMdAttr.stMBMode.bMBMVMode = HI_FALSE;
	stMdAttr.stMBMode.bMBPelNumMode = HI_FALSE;
	stMdAttr.stMBMode.bMBALARMMode = HI_FALSE;

	stMdAttr.u16MBALSADTh = 1000;//宏块SAD 的阈值为1000
	stMdAttr.u8MBPelALTh = 20;//像素报警阈值为20
	stMdAttr.u8MBPerALNumTh = 20;//像素报警个数阈值为20
	stMdAttr.enSADBits = MD_SAD_8BIT;//SAD 输出精度为8bit
	stMdAttr.stDlight.bEnable = HI_FALSE;//去光照效应不使能 (不支持使能)
	stMdAttr.u32MDInternal = 0;//MD监测间隔为0,即每帧都做MD
	stMdAttr.u32MDBufNum = 16;//MD 缓存大小为16

	/*set MD frame*/
   	 stRefAttr.enRefFrameMode = MD_REF_AUTO;//参考图像模式为:自动设置
	stRefAttr.enRefFrameStat = MD_REF_DYNAMIC;//参考图像更新状态为:更新

	//设置移动侦测属性
    s32Ret =  HI_MPI_MD_SetChnAttr(VeChn, &stMdAttr);
    if(s32Ret != HI_SUCCESS)
    {
        printf("HI_MPI_MD_SetChnAttr Err 0x%x\n", s32Ret);
        return HI_FAILURE;
    }

	//设置移动侦测的参考图像属性
    s32Ret = HI_MPI_MD_SetRefFrame(VeChn, &stRefAttr);
    if(s32Ret != HI_SUCCESS)
    {
        printf("HI_MPI_MD_SetRefFrame Err 0x%x\n", s32Ret);
        return HI_FAILURE;
    }

	//移动侦测通道使能
    s32Ret = HI_MPI_MD_EnableChn(VeChn);
    if(s32Ret != HI_SUCCESS)
    {
        printf("HI_MPI_MD_EnableChn Err 0x%x\n", s32Ret);
        return HI_FAILURE;
    }

	return HI_SUCCESS;
}

/***** 线程:获取MD数据 ***************************************************************************************/
HI_VOID *GetMdData(HI_VOID *p)
{
	HI_S32 s32Ret;
	HI_S32 s32MdFd;
	MD_DATA_S stMdData;
	VENC_CHN VeChn = VENCCHNID;
	fd_set read_fds;
	struct timeval TimeoutVal; 
    FILE *pfd;

	//创建MD结果文件
    pfd = fopen("md_result.data", "wb");
    if (!pfd)
    {
        return NULL;
    }

	//获取移动侦测通道对应的设备文件句柄
	s32MdFd = HI_MPI_MD_GetFd(VeChn);

	//get MD data
	do{
		FD_ZERO(&read_fds);
		FD_SET(s32MdFd,&read_fds);

		TimeoutVal.tv_sec = 2;
		TimeoutVal.tv_usec = 0;
		//监测设备是否有数据可读,超时是2s
		s32Ret = select(s32MdFd+1, &read_fds, NULL, NULL, &TimeoutVal);

		if (s32Ret < 0) //错误
		{
			printf("select err\n");
			return NULL;
		}
		else if (0 == s32Ret) //超时
		{
			printf("time out\n");
			return NULL;
		}
		else
		{
			//数据清零
			memset(&stMdData, 0, sizeof(MD_DATA_S));
			//有数据可读
			if (FD_ISSET(s32MdFd, &read_fds))
			{
				//获取移动侦测数据
				s32Ret = HI_MPI_MD_GetData(VeChn, &stMdData, HI_IO_NOBLOCK);
				if(s32Ret != HI_SUCCESS)
				{
					printf("HI_MPI_MD_GetData err 0x%x\n",s32Ret);
					return NULL;
				}
			}

			//打印结果(略)
			//对MD四种模式进行分析,读取数据。
			
			//释放移动侦测数据
			s32Ret = HI_MPI_MD_ReleaseData(VeChn, &stMdData);
			if(s32Ret != HI_SUCCESS)
			{
		    	printf("Md Release Data Err 0x%x\n",s32Ret);
				return NULL;
			}

		}
	}while (HI_FALSE == g_bIsQuit);

    if (pfd)
    {
        fclose(pfd);
    }
	return NULL;
}



SpringMVC 限流 - CSDN博客

$
0
0

在使用 SpringBoot做接口访问如何做接口的限流,这里我们可以使用google的Guava包来实现,当然我们也可以自己实现限流,Guava中的限流是久经考验的我们没必需重新再去写一个,如果想了解限流原理的同学可以自己查阅一下相关的资料,本文不作过来说明噢。

使用说明

在项目中引入 Guava相关包

http://mvnrepository.com/artifact/com.google.guava/guava/21.0
  • 1

maven项目

<!-- https://mvnrepository.com/artifact/com.google.guava/guava --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>21.0</version></dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

gradle项目

// https://mvnrepository.com/artifact/com.google.guava/guava
compile group: 'com.google.guava', name: 'guava', version: '21.0'
  • 1
  • 2

写一个 SpringMVC的拦截器 
SmoothBurstyInterceptor.java

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class SmoothBurstyInterceptor extends HandlerInterceptorAdapter {

    public enum LimitType {
        DROP,//丢弃
        WAIT //等待
    }

    /**
     * 限流器
     */
    private RateLimiter limiter;
    /**
     * 限流方式
     */
    private LimitType limitType = LimitType.DROP;

    public SmoothBurstyInterceptor() {
        this.limiter = RateLimiter.create(10);
    }

    /**
     * @param tps       限流量 (每秒处理量)
     * @param limitType 限流类型:等待/丢弃(达到限流量)
     */
    public SmoothBurstyInterceptor(int tps, SmoothBurstyInterceptor.LimitType limitType) {
        this.limiter = RateLimiter.create(tps);
        this.limitType = limitType;
    }
    /**
     * @param permitsPerSecond  每秒新增的令牌数
     * @param limitType 限流类型:等待/丢弃(达到限流量)
     */
    public SmoothBurstyInterceptor(double permitsPerSecond, SmoothBurstyInterceptor.LimitType limitType) {
        this.limiter = RateLimiter.create(permitsPerSecond, 1000, TimeUnit.MILLISECONDS);
        this.limitType = limitType;
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (limitType.equals(LimitType.DROP)) {
            if (limiter.tryAcquire()) {
                return super.preHandle(request, response, handler);
            }
        } else {
            limiter.acquire();
            return super.preHandle(request, response, handler);
        }
        throw new Exception("网络异常!");//达到限流后,往页面提示的错误信息。
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }

    public RateLimiter getLimiter() {
        return limiter;
    }

    public void setLimiter(RateLimiter limiter) {
        this.limiter = limiter;
    }
}
  • 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
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

SpringMVC拦截配置 
WebConfig.java

@Component
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 多个拦截器组成一个拦截器链
        registry.addInterceptor(new SmoothBurstyInterceptor(100, SmoothBurstyInterceptor.LimitType.DROP)).addPathPatterns("/**");
        //限流可配置为SmoothBurstyInterceptor.LimitType.DROP丢弃请求或者SmoothBurstyInterceptor.LimitType.WAIT等待,100为每秒的速率
        super.addInterceptors(registry);
    }

}

YARN动态资源池配置案例_Alex_新浪博客

$
0
0
CDH作为统一的企业级数据中心,往往是一个多租户的应用环境。在该环境中,不同用户会同时使用集群资源。如何保证用户数据不被任意篡改?如何保证任务的权限控制 (例如用户A不能任性地取消用户B的任务)?如何确保用户资源使用不超过他们的配额?

1. 开启HDFS权限检查 (默认是开启的)
YARN动态资源池配置案例

"Check HDFS Permissions"选中

2. 在集群中创建新用户,以cloudera-dev为例
 # 增加用户组
 [root]$ groupadd cloudera-dev
 # 增加用户
 [root]$ useradd -g cloudera-dev cloudera-dev
 # 查看用户 cloudera-dev 所属的所有组
 [root]$ groups cloudera-dev

 # Hadoop 创建相应的用户目录
 [root]$ sudo -u hdfs hdfs dfs -mkdir /user/cloudera-dev
 [root]$ sudo -u hdfs hdfs dfs -chown cloudera-dev:cloudera-dev /user/cloudera-dev 
一般而言,创建新用户会在集群中每台机器上都创建同样的用户。不过理论上,为了运行该案例,只需要在安装了资源管理器 (Resource Manager) 的机器上创建相应用户即可。CDH默认情况下使用 whoami获取用户的信息, bash -c groups xxx 获取用户与组之间的映射关系,并使用这两份信息进行ACL验证

3. 运行第一个MapReduce示例程序 "word count"
 [root]$ su cloudera-dev
 [cloudera-dev]$ echo "Hello World Bye World" > file0
 [cloudera-dev]$ echo "Hello Hadoop Goodbye Hadoop" > file1
 [cloudera-dev]$ hdfs dfs -mkdir -p /user/cloudera-dev/wordcount/input
 [cloudera-dev]$ hdfs dfs -put file* /user/cloudera-dev/wordcount/input
 [cloudera-dev]$ hadoop jar /opt/cloudera/parcels/CDH/jars/hadoop-examples.jar wordcout wordcount/input wordcount/output
 [cloudera-dev]$ hdfs dfs -getmerge wordcount/output output.txt
 [cloudera-dev]$ cat output.txt
 Bye 1
 Hadoop 2
 Hello 2
 Goodbye 1
 World 2

4. 开启资源管理器ACL并设置相应的管理ACL (Admin ACL)
YARN动态资源池配置案例

其中yarn.acl.enable默认值为true。而对于yarn.admin.acl默认值为*,意味着所有人都可以管理Resource Manager (比如运行yarn rmadmin)、管理已提交 (比如取消 kill) 的任务。格式为 "以逗号分隔的用户列表+空格+以逗号分隔的用户组列表",例如 "user1,user2 group1,group2"。如果只有组信息,需要在最前端加入一个空格,例如" group1,group2"。另外特别需要注意的是必须至少将" yarn"加入到用户列表中,默认安装CDH后,有关YARN服务的命令会以yarn用户的身份进行运行,若yarn不设置于yarn.admin.acl中,会出现权限相关的错误 (例如刷新动态资源池)。

在该示例中,yarn.admin.acl列表中包含一个用户yarn以及一个组cloudera-dev。博主注:配置在yarn.admin.acl中的用户、用户组并不能任意在资源池 (或者叫资源队列) 中提交任务。

5. 关闭未声明资源池的自动生成
YARN动态资源池配置案例

默认情况下,"Allow Undeclared Pools"可选项是选中的,需要关闭。否则如果用户指定一个尚未声明的资源池时,比如prod,YARN将会自动生成一个prod资源池。配置文件修改后需要重新启动YARN服务,重新部署客户端配置。

6. 配置“若用户提交任务不指定特定的queue,就使用default queue
YARN动态资源池配置案例

默认情况下,“Fair Scheduler User As Default Queue”可选项是选中的,意味着如果用户提交任务时如果不指定特定的queue,就使用以用户命名的queue。

7. 进入动态资源池 (Dynamic Resource Pools) 配置页面
YARN动态资源池配置案例

访问动态资源池管理页面
YARN动态资源池配置案例

选择配置选项 (Configuration,右上角)
YARN动态资源池配置案例

Resource Pools: 展示了当前资源池的分配情况,默认情况下,只有一个资源池 root.default。在该示例中,dev 是自定义的资源池。权重 (weight) 定义了资源池之间分配资源的比例。示例中,dev设置权重为4而 default 的权重为1,那么集群资源的 80% 会被分配给 dev。注意,这里提到的资源分配不是一个静态的概念,例如当前资源池 dev 中没有任务正在运行,那么资源池 default 是允许使用超过20%的集群资源,比如50%。博主注:资源池的配置信息会保存在fair-scheduler.xml文件中,Resource Manager会周期性读取该配置文件,因此资源池配置允许在线修改,即修改后不需要重新启动Yarn。

8. 为资源池 root 配置资源池 ACL
点击资源池 root 对应的 "Edit" 按钮
YARN动态资源池配置案例

在通用 (General) 栏,设置资源池 root 的调度算法,一般使用默认的DRF (根据CPU、Memory资源进行调度)。博主注:队列的"Submission Access Control"与"Administrator Access Control"的配置会自动继承子队列中,比如在root的"Submission Access Control"中配置用户alex,那么即使root.test的"Submission Access Control"中配置为空,用户alex也可以向队列root.test提交Yarn应用程序。
YARN动态资源池配置案例

在YARN栏,设置资源池权重,资源约束等
YARN动态资源池配置案例

在提交访问控制 (Submission Access Control) 栏设置哪些用户或用户组可以向该资源池提交任务
YARN动态资源池配置案例

在管理访问控制 (Administration Access Control) 栏设置哪些用户或用户组可以对资源池中的任务进行管理
YARN动态资源池配置案例

9. 为资源池 dev 配置资源池 ACL
可以使用与 root 相同的ACL配置,也可以使用不同的配置,该示例假设使用相同的设置。

10. 测试
测试一:用户 root 向资源池 dev 提交 word count 任务
 [root]$ hadoop jar wordcount-0.9.0.jar com.cloudera.example.WordCount -Dmapreduce.job.queuename=dev wordcount/input wordcount/output
 ...
 Exception in thread "main" java.io.IOException: Failed to run job: User root cannot submit applications to queue root.dev
 ...
注意:这里的word count是自定义的,与CDH自带的word count示例的唯一区别在于,自定义word count的Mapper程序在运行时首先使用Thread.sleep (300 * 1000) 休眠5分钟。这主要是为了后续对资源池管理的测试

测试二:用户 root 取消用户 cloudera-dev 提交的、运行于资源池 dev 中的 word count 任务
用户cloudera-dev向资源池dev提交word count任务
 [cloudera-dev]$ hadoop jar wordcount-0.9.0.jar com.cloudera.example.WordCount -Dmapreduce.job.queuename=dev wordcount/input wordcount/output
 ...

用户root查询相应任务的id,假设获得任务id为job_1421512955131_0006
 [root]$ hadoop job -list
 ...

用户root取消 (kill) 该任务
 [root]$ hadoop job -kill job_1421512955131_0006
 ...
 Exception in thread "main" java.io.IOException: org.apache.hadoop.yarn.exceptions.YarnException: Java.security.AccessControlException: User root cannot perform operation MODIFY_APP on application_1421512955131_0006
 ...

测试三:用户 alex (属于组 cloudera-dev) 取消用户 cloudera-dev 提交的、运行于资源池 dev 中的 word count 任务
增加用户alex,设置所属组cloudera-dev
 [root]$ useradd -g cloudera-dev alex

用户cloudera-dev向资源池dev提交word count任务
 [cloudera-dev]$ hadoop jar wordcount-0.9.0.jar com.cloudera.example.WordCount -Dmapreduce.job.queuename=dev wordcount/input wordcount/output
 ... 

用户alex查询相应任务的id,假设获得任务id为job_1421512955131_0006
 [alex]$ hadoop job -list
 ...

用户alex取消 (kill) 该任务
 [alex]$ hadoop job -kill job_1421512955131_0006
 ...
 INFO impl.YarnClientImpl: Killed application application_1421512955131_0006
 Killed job job_1421512955131_0006
 ...

11. Placement Rules
通过参数mapreduce.job.queuename可以显示地指定一个Yarn应用程序运行所在的资源池 (每个应用程序的运行都必须在资源池)。如果没有显示指定资源池,Yarn会根据一系列的规则 (Placement Rule) 自动生成资源池的名字。这些规则可以通过Cloudera Manager进行配置,如下所示:

YARN动态资源池配置案例
根据该规则:
Yarn首先检查用户alex (用户组dev) 是否显示指定了资源池,并且资源池已经预先定义好了:
是 -> 运行
否 -> 检查root.alex资源池是否已经预先定义好了:
    是 -> 运行
    否 -> 检查root.dev资源池是否已经预先定义好了:
        是 -> 运行
        否 -> 使用default资源池进行运行

另外,目前在Cloudera Manager上虽然可以定义"嵌套式用户队列" (Nested User Queue),但是尚未允许就嵌套式用户队列设置Placement Rule。因此如果需要使用该功能,只能通过直接编写XML的方式实现。

YARN动态资源池使用配置 - CSDN博客

$
0
0

动态资源池是用来做资源配置和调度策略管理,动态资源池中可以运行YARN应用和Impala查询任务。动态资源池允许用户在运行YARN应用或Impala查询任务的时候指定特定的池并调度池中可用的资源。一个典型的应用场景就是当我们通过Oozie调度Pig任务的时候,如果我们同时提交大量的Oozie任务,Oozie任务在启动launch的时候是需要占用资源的,可能就会导致资源占满而使后续的Pig任务无法执行。在CDH3U5中,我们会在FairScheduler中配置2个queue,一个放置Oozie的Launch任务,并设置其可用的slot数,另外一个放置Pig任务来解决。在YARN中资源已经不是用slot来表示,而是用VCores和Memory来表示。我们可以通过YARN动态资源管理来解决上述问题。

使用步骤:

1)关闭未声明资源池的自动生成。

进入YARN面板,选择配置->服务范围->资源管理->yarn.scheduler.fair.allow-undeclared-pools,默认选项是开启的,需要关闭,否则如果用户指定一个尚未声明的资源池时,YARN将为自动生成一个相对于的资源池。我们需要关闭该选项,修改之后点击保存更改,重启YARN服务生效。

 

2)关闭"使用默认队列时的Fair Scheduler用户"选项

进入YARN面板,选择配置->ResourceManager Default Group->yarn.scheduler.fair.user-as-default-queue,该选项默认是开启,表示用户提交任务时,如果未指定池名称,就使用用户名作为默认的池名称,我用需要关闭该选项,让未指定此名称时,任务运行在default池中。点击保存更改,重启YARN服务生效。


3)进入动态资源池配置界面

通过集群->资源管理->动态资源池进入。

 默认情况下只有一个资源池root.default,我们可以手动添加资源池并分配使用权重、VCores和内存。

4)添加动态资源池。

选择动态资源池->配置->添加资源池。

 

将弹出一个引导界面:

 

在常规面板输入资源池名称:譬如oozie,计划策略一般默认采用DRF策略。

在YARN面板我们可以配置权重,虚拟内核,内存大小,以及正在运行的应用程序最大数量,当配置虚拟内核和内存之后着2个参数将优先于权重配置。其中权重定义了资源池之间分配资源的比例,譬如上图中oozie池全权重为1而default池的权重为2,那么集群资源的33.3%会分配给oozie。注意,这里提到的资源分配不是一个静态的概念,如果当前资源池default中没有任务执行,那么资源池oozie是可以运行使用超过33.3%的集群资源的,譬如50%。虚拟内核表示资源池能够调度的虚拟内核数,可以不做配置。内存大小表示资源池能够调度的内存大小,可以不做配置。正在运行的应用程序最大数量表示资源池中能够同时运行的application数量,也就是MapReduceV1中所说的job数量。

 

点击确认即可。

5)根据需要配置计划模式

YARN动态资源池可以根据需要配置在不同时间段选择不同的资源调度规则。

操作流程:

a)选择动态资源池->配置->计划模式->添加计划模式。

 

点击之后将弹出一个引导界面。

 

我们添加一个工作日的计划模式,在配置集中输入配置集的名称,选择重复的模式,以及重复的天数和时间。

最终我们添加2个计划模式,一个在工作日全天运行,一个在周末全天运行。

 

b)根据不同的计划模式配置不同的调度规则。

添加2个计划模式后,我们在编辑资源池的编辑界面就可以看到新添加的YARN配置级了,我们可以根据需要配置相应的权重、虚拟内核、内存和运行应用程序的最大数量值。

譬如我们可以配置在weekday模式下选择default池权重为66.7%,oozie池权重为33.3%。

 

在weekend模式下选择default池权重为50%,oozie池权重为50%。


在default模式下选择default池权重为75%,oozie池权重为25%。

 

6)在Oozie中使用动态资源池方法:

在workflow.xml文件中分别加入oozie launch的启动配置池和mapreduce的job运行池即可。参数如下:

<property>

<name>oozie.launcher.mapred.job.queue.name</name>

<value>root.oozie</value>

</property>

<property>

<name>mapred.job.queue.name</name>

<value>default</value>

</property>

加入以上配置之后运行对于的oozie任务可以在动态资源池面板中看到资源池的使用情况。下图表明配置成功。

 

Spring中实现多数据源事务管理 - CSDN博客

$
0
0

前言

由于项目中引入了多个数据源,并且需要对多个数据源进行写操作,那么多数据源的事务管理自然成了不可避免的问题,这也让我对 @Transactional注解有了进一步的理解(但实际上也并不是非常深入)

然而这是一个演进的过程,刚开始项目中并没有使用 @Transactional指定具体的 TransactionManager,所以新增一个数据源后,对原有的事务产生了影响了,这也是偶尔在一次测试报错而结果没有回滚之后才发现的,遂对于 @Transactional注解的一些参数项进行了了解。

研究

由于容器中存在两个 TransactionManager,那么被 @Transactional注解的方法到底使用了哪个 TransactionManager来进行事务管理,抑或是同时使用了两个 TransactionManager来进行事务管理都是我们需要搞清楚的问题。 
首先我们先看看 @Transactional注解上有没有提供配置项来指定 TransactionManager,果不其然,发现 value属性就是用来指定具体 TransactionManager的,通过 id或者 name来指定唯一一个 TransactionManager,那么对于只需要一个事务管理的方法,问题就简单多了:

    @Transactional(value = "database2TransactionManager")
    public void test(String a) {
        // business operation
    }
  • 1
  • 2
  • 3
  • 4

关于不指定TransactionManager时会使用哪一个TransactionManager,有兴趣的童鞋可以参考另一篇文章,讲的比较清晰: http://blog.sina.com.cn/s/blog_8f61307b0100ynfb.html

好了,回到我们研究的问题,那么对于需要写入多个数据源的业务方法该怎么办呢?

进一步研究

看来 @Transactional是没有提供这种功能了,那么就自己写了一个吧。我记得 Spring中的事务管理分编程式事务和声明式事务。我们平时使用的 @Transactional就是声明式事务,它的好处其实也就是灵活度更高、代码的耦合性更低,最终的事务管理实现还是一样的,只不过将具体逻辑都剥离到了切面中。所以我们可以手写一个切面来写一次“编程式事务”,当然在具体应用时,还是声明式的。

Java中一般编程式事务的写法:

public class UserServiceImpl implements UserService {
    @Resource
    private TransactionManager txManager;
    @Resource
    private UserDao userDao;
    @Resource
    private AddressDao addressDao;

    public boolean saveUser(User user) {
        TransactionDefinition txDefinition = new TransactionDefinition();
        TransactionStatus txStatus = txManager.getTransaction(txDefinition);
        boolean result = false;
        try {
            result = userDao.save(user);
            if(!result){
                return false;
            }
            result = addressDao.save(user.getId(), user.getAddress());
            txManager.commit(txStatus);
        } catch (Exception e) {
            result = false;
            txManager.rollback(txStatus);
        }
        return result;
    }
}
  • 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

我们借用这个逻辑将事务管理相关提取到切面中,并在进入目标方法之前,让多个 TransactionManager都开启事务,并在成功执行后一并提交或失败后一并回滚,具体代码:

/**
 * @author Zhu
 * @date 2015-7-15
 * @version 0.0.1
 * @description
 */
public class MultiTransactionalAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    public Object around(ProceedingJoinPoint pjp,
            MultiTransactional multiTransactional) throws Throwable {

        Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<DataSourceTransactionManager>();
        Stack<TransactionStatus> transactionStatuStack = new Stack<TransactionStatus>();

        try {

            if (!openTransaction(dataSourceTransactionManagerStack,
                    transactionStatuStack, multiTransactional)) {
                return null;
            }

            Object ret = pjp.proceed();

            commit(dataSourceTransactionManagerStack, transactionStatuStack);

            return ret;
        } catch (Throwable e) {

            rollback(dataSourceTransactionManagerStack, transactionStatuStack);

            logger.error(String.format(
                    "MultiTransactionalAspect, method:%s-%s occors error:", pjp
                            .getTarget().getClass().getSimpleName(), pjp
                            .getSignature().getName()), e);
            throw e;
        }
    }

    /**
     * @author Zhu
     * @date 2015-7-25下午7:55:46
     * @description
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     * @param values
     */
    private boolean openTransaction(
            Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
            Stack<TransactionStatus> transactionStatuStack,
            MultiTransactional multiTransactional) {

        String[] transactionMangerNames = multiTransactional.values();
        if (ArrayUtils.isEmpty(multiTransactional.values())) {
            return false;
        }

        for (String beanName : transactionMangerNames) {
            DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) ContextHolder
                    .getBean(beanName);
            TransactionStatus transactionStatus = dataSourceTransactionManager
                    .getTransaction(new DefaultTransactionDefinition());
            transactionStatuStack.push(transactionStatus);
            dataSourceTransactionManagerStack
                    .push(dataSourceTransactionManager);
        }
        return true;
    }

    /**
     * @author Zhu
     * @date 2015-7-25下午7:56:39
     * @description
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     */
    private void commit(
            Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
            Stack<TransactionStatus> transactionStatuStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().commit(
                    transactionStatuStack.pop());
        }
    }

    /**
     * @author Zhu
     * @date 2015-7-25下午7:56:42
     * @description
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     */
    private void rollback(
            Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
            Stack<TransactionStatus> transactionStatuStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().rollback(
                    transactionStatuStack.pop());
        }
    }
  • 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
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

整体结构很清晰: 
1. 首先根据指定的多个 TransactionManager依次开启事务,这个次序不影响,因为其实大家都是平等的。 
2. 其次就是调用目标方法执行具体的业务逻辑 
3. 若是成功返回则提交每个事务,若中途报错,那么就回滚每个事务

其中为什么要用 Stack来保存 TransactionManagerTransactionStatus呢?那是因为 Spring的事务处理是按照 LIFO/stack behavior的方式进行的。如若顺序有误,则会报错:

java.lang.IllegalStateException: Cannot deactivate transaction synchronization - not active
        at org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization(TransactionSynchronizationManager.java:313)
        at org.springframework.transaction.support.TransactionSynchronizationManager.clear(TransactionSynchronizationManager.java:451)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.cleanupAfterCompletion(AbstractPlatformTransactionManager.java:986)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:782)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactio
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

题外话

刚开始碰到这个问题的时候,先想到的是分布式事务管理,也去看了JTA相关的文章,但是好像比较麻烦,而且都是一些老文章,于是想试试自己实现,最后也实现了。所以想知道 JTA TransactionManager究竟有什么用呢?

集成基于CAS协议的单点登陆 - loveis715 - 博客园

$
0
0

  相信大家对单点登陆(SSO,Single Sign On)这个名词并不感到陌生吧?简单地说,单点登陆允许多个应用使用同一个登陆服务。一旦一个用户登陆了一个支持单点登陆的应用,那么在进入其它使用同一单点登陆服务的应用时就不再需要重新登陆了。而CAS协议则正是各单点登陆产品所需要实现的协议,其全称为Central Authentication Service。

  那为什么要写这篇博客呢?这是因为在为公司的产品集成SSO的时候,我发现如果软件开发人员不了解CAS协议,那么他在集成出现错误的时候将完全没有办法对出错的原因进行分析。

 

单点登陆简介

  如果您不知道单点登陆是什么,那么先来体会一次单点登陆。首先,请在一个全新浏览器(或者清除了登录信息缓存的浏览器)的地址栏中键入 www.hotmail.com,并进入您的hotmail邮箱。接下来,我们再访问 www.msn.com。请看网页的右上角,您会发现您已经成功地通过刚刚的hotmail邮箱登入了 www.msn.com

  这就是单点登录:即使hotmail和msn的域名表明它们完完全全就是两个不同的网站,但是由于它们使用了同一个单点登录服务,因此在登陆hotmail之后再登陆msn,您刚刚输入的用户身份凭证依然有效。

  这种在一处登录就能直接访问其它应用的功能在企业级应用中是非常有用的。试想这样一个情景:在每天的工作中,我们总需要通过浏览器访问几十个系统。如在浏览器中阅览公司邮件,在需要撰写Wiki的时候需要登录到公司的Wiki系统。而报销,请假,公司共享文件服务器等都需要用户登陆。而且如果一个公司的IT部门对安全比较重视,那么他们还会要求员工在一定时间内更换一次密码。试想一下,如果公司内部有几十个系统,那么每个员工都需要维护几十个密码,并且每隔几天就需要针对一个系统更改一个密码。这种密码管理的开销无论对于员工本身还是对于IT开销都是巨大的。

  反过来,如果这些系统都使用同一个用户登录服务,那么我们就可以免除在每次登入一个系统时都要输入密码的开销,而且每个员工只需要管理一个密码。

  因此,如果希望自己的企业级应用能够成功地进入各个大型企业,那么这个产品就必须支持标准的单点登录协议,即CAS协议。而只有在正确地理解了CAS协议的基础之上,我们才能在我们的产品中正确地添加对单点登录的支持。而这也便是我写这篇文章的真正原因。

 

CAS协议的工作流程

  现在我们知道了单点登录给我们所带来的好处,但是我们并不知道它是如何工作的。由于CAS协议的执行流程较为复杂,因此,让我们首先来看一个生活中的与单点登录相似的事例,以辅助我们对单点登录运行流程的理解。

  假设公司马上就要来一位新同事,公司的行政人员需要在该同事到来前为他置办好日常工作所需要的各种办公用品。这些办公用品包括工作用的电脑以及一套全新的办公桌椅。在置办完这些办公用品后,该行政人员还要拿着购买办公用品时所开具的发票去公司的财务部报销。为了节省时间,该行政人员将会使用公司车辆在公司和卖场之间运送货物。

  由于公司的司机常常在外奔波,因此他可能并不知道当天需要载着这位行政人员去购买办公用品。在该行政人员找到他之后,他无法确认行政人员的用车安排是否与公司的其它安排相冲突。因此司机会让行政人员打电话给公司的领导确认他是否可以用车。领导接到了该行政人员的电话并陈述用车的缘由后,行政人员会将电话转交给司机,让司机本人与领导沟通。在得到了领导的肯定答复后,司机就可以放心地载着该行政人员去电脑城购买电脑了。

  当该行政人员在当天再次找到该司机说需要购买办公桌椅的时候,该司机就不再需要打电话询问领导。因为他已经打电话和领导询问过是否该行政人员需要用车的事情。

  在所有的办公用品置办完毕以后,该行政人员就会去公司的财务处报销。在见到财务人员之后,该行政人员会直接说和领导之前已经沟通过并得到过批准。在该财务人员直接打电话给该领导并简单提起购买办公用品的事情后,领导就会很快地确认需要为一位新来的员工购买办公用品的事情。在得到了肯定的答复后,该财务人员将直接接受报销申请并迅速进入结算程序。

  在整个过程中,行政人员都是资源的访问者及使用者。在每次尝试使用这些资源时,资源的管理者都需要领导的批准来给与该行政人员使用资源的权利。只是在不同时刻,由于领导及资源管理者所得到的信息量并不相同,因此产生了三种不同的使用资源的流程。

  在第一次用车的时候,由于司机和领导都不清楚当天该行政人员需要用车,因此该行政人员需要从领导那里得到可以用车的许可,并将领导的许可转交给司机,进而真正获得使用公司车辆的权利。在该过程中,行政人员既需要与司机沟通,又需要与领导沟通,而司机也需要与领导沟通。

  在第二次用车的时候,由于司机已经知道领导对于车辆使用的授权,因此他不再需要额外的沟通而直接允许该行政人员使用公司车辆。在该过程中,行政人员只需要和司机沟通,而不再需要与领导打招呼。

  而在报销的过程中,由于财务人员并不知道购买办公用品是否已经获得了领导的同意,因此需要询问领导。而此时领导已经知道财务人员是为了新入职的同事准备的办公用品,因此将直接同意对该笔款项进行报销,也不再需要行政人员再次向领导解释公司将来一名新员工的事宜。在该过程中,财务人员需要与行政人员以及领导沟通。

  CAS协议的运行流程实际上与上面所举示例的运行基本一致:第一次访问一个应用时,系统将要求用户转到SSO系统,输入表示自己身份凭证的用户名和密码,才能得到访问该应用的权限。而在第二次访问同一应用的时候,由于应用已经知道该用户曾经获得了访问权限,因此将直接允许用户对该应用进行访问。如果用户访问另一个应用,那么该应用将会根据用户当前所得到的身份凭证去SSO系统认证,从而得知用户已经拥有了对该应用的访问权限。

  下面就是通过CAS协议第一次访问应用并成功登录的流程图:

  上面的流程图列出了用户在第一次登录应用时所需要经历的步骤。首先,用户通过在浏览器的地址栏中键入 https://app.ambergarden.com来尝试访问应用。由于此时应用会话并没有被创建,因此应用将拒绝用户的登录请求,并通过302响应将用户重定向到 https://sso.ambergarden.com以要求用户首先通过SSO进行登录。注意在该重定向所标明的地址中包含了一个额外的URL参数service。其标示了用户原本想要访问的应用所在的位置。在浏览器接收到了302响应后,其将自动跳转到SSO。由于SSO中也没有与浏览器建立相应的会话,因此其将返回给用户一个登录界面,要求用户通过输入用户名和密码完成登录。在用户输入了用户名/密码并点击登录按钮后,页面逻辑将发送一个POST请求到SSO中以建立会话。如果用户登录成功,那么SSO将返回一个302重定向响应,并且该重定向响应中由Location所标明的地址还带了一个额外的参数ticket。在浏览器接收到该重定向响应之后,其将向重定向地址发送一个GET请求,并且该请求中还包含了刚刚从SSO所返回的ticket参数。在应用接受到该请求后,其将使用ticket参数所标明的凭据向SSO发送请求以验证该凭据的合法性。如果验证成功,那么应用将会认为当前对应用的访问是一个已经得到SSO认证的合法用户发起的,进而为该用户创建会话。

  在浏览器再次访问该应用的时候,由于应用已经为当前浏览器创建了相应的会话,因此应用将能识别出它是一个合法的经过SSO验证的用户:

  相较于对应用的第一次访问而言,第二次访问的流程图实际上就非常容易理解了。实际上,这就是在已经建立了会话之后再次访问应用时的流程图。

  而在访问其它应用时,CAS协议运行的流程图将如下所示:

  如果您已经理解了首次登录流程,那么该流程就非常容易理解了。首先,用户尝试访问处于 https://app2.ambergarden.com下的应用。由于之前用户并没有登录过该应用,因此该应用发送一个302请求来将用户重定向到SSO服务。而这里的URL参数service与首次登录时候的意义一样,用来在这一系列通信中记录登录成功后所需要返回到的服务地址。

  浏览器一旦接收到了302响应,那么其将立刻执行重定向并访问SSO服务。由于用户在之前访问第一个应用的时候已经建立了SSO会话,因此SSO服务将立即返回一个302响应,并在响应的Location中使用URL参数ticket标示用户从SSO所得到的凭据。在接到302响应后,浏览器再次重定向,并访问应用。应用同样使用ticket所标示的凭据向SSO请求验证。在验证成功后,应用将会认为当前访问用户是合法的,因此将为该用户创建会话。在该过程中,用户没有输入任何信息,而只经过了几次浏览器跳转就验证了用户的合法性。

 

CAS协议集成

  好了,在了解了基于CAS协议的SSO是如何工作的之后,我们就可以开始考虑如何在应用中集成对CAS协议的支持了。

  我们所要做的第一步就是分析上面所讲的各个流程中的每一步对CAS协议所定义的各接口,进行使用的方式。在分析的过程中,我们需要时刻提醒自己是在为我们的应用添加CAS协议支持,而不是实现一个基于CAS协议的SSO。这会导致我们在查看CAS协议时的角度与实现一个基于CAS协议的SSO有以下几点不同:

  1. 不必要理解CAS协议中的所有接口以及各接口中的所有参数。CAS协议中列出的很多接口实际上都是用来在浏览器和SSO服务之间进行交互的。因此我们不必要非常仔细地察看这些接口,而只需要知道这些接口所执行的功能以及产生的数据即可。而且即使是我们会用到的接口中也会有一部分参数并不会被用到。我们只需要关注在我们的应用使用SSO流程中所可能涉及到的各个参数即可。
  2. 需要注意在流程中浏览器与应用之间的交互。在前面所展示的各个流程图中已经能够看到,应用是通过一系列302重定向响应等HTTP标准方法来完成将用户重定向到SSO服务这一动作的。

  那么我们回过头来回忆一下用户首次登陆成功的流程图:

  在该流程图里面,我们需要关心的就是到底谁与应用产生了交互,以及在交互过程中使用了什么样的接口及参数。可以看到,在用户通过浏览器第一次访问应用的时候,应用需要返回给用户一个302响应,而响应中所指定的地址就是SSO所提供的登陆接口。因此我们要做得第一步就是要理解Login这个URL所做的工作。

  从API文档中可以看到,Login这个URL可以提供两种服务:请求用户输入身份凭证(Credential Requester)以及验证用户的身份凭证(Credential Acceptor)。在这张流程图里面,我们两次向该URL发送请求。第一次请求所使用的是该URL的第一种服务,该服务将请求用户输入用户的身份凭证,如显示一个登陆页面。而在用户输入了用户名和密码后,向该URL发送的请求就将使用第二种服务。在这两次请求中,URL中的service参数用来标明用户成功登陆后从SSO重定向出来时所需要跳转到的地址。由于我们是在为自己的应用添加SSO支持,那么该服务自然需要指向我们的应用。

  OK,现在我们就可以开始第一步集成了。该步骤的工作就是:当一个用户访问应用的时候,如果应用中没有该用户所对应的会话,那么返回302响应,并在响应中标示跳转的地址为SSO所在的地址,并在service参数中标明我们的应用所在的地址。如果这一切功能都正确地实现,那么对应用的访问会自动跳转到SSO服务,并在成功登陆后再次请求访问我们自己的应用。此时的访问会传入一个URL参数ticket以表示从SSO处得到的凭证。

  如果正确地完成了第一步的集成,那么我们就可以开始集成的第二个步骤了,那就是向SSO服务验证用户。在第一步中,SSO最终验证了用户的身份并向浏览器中返回了一个302响应。浏览器接收到该302响应后,其便向302响应所标示的地址进行跳转。在应用接受到了包含ticket参数的请求后,它需要使用该ticket参数所记录的信息来访问serviceValidate接口,以验证所传入的ticket参数是否是一个真正的从SSO所返回的凭据。一旦验证成功,那么该用户就是一个合法用户。此时应用就需要为该用户创建一个会话并将他重定向到程序的初始页面,如应用的Dashboard等。

  而在用户再次访问应用的时候,由于应用中已经包含了该用户所对应的会话,因此其不会再与SSO产生交互,因此也不再需要为SSO集成做任何工作。同样的,由于第三个流程也使用了同样的接口,因此我们也不再需要做任何额外的工作。

  就这样,仅仅通过这两步,我们就可以完成对SSO登陆功能的集成了。是不是很简单?

  除此之外,我们还可以选择支持Single Logout(有些讨论里面叫Single Sign Off)。但是反对该功能的人也不少。其中的原因就因为用户常常认为点击Logout按钮是从当前应用中安全地退出,但是这实际上导致用户所登陆的所有使用该SSO服务的应用都将退出。因此在决定对该功能进行支持之前一定要考虑清楚该功能是否会真的为用户带来好处。

 

需要注意的问题

  就像大家在上面所看到的一样,在应用中集成对SSO的支持实际上并不需要多少工作。甚至可以说,一个有相关经验的人可以在几个小时内就能完成。但是反过来说,如果在集成中没有注意到一些问题,那么这些问题可能会耽误您几天的时间。因此在本节中,我会简单地说一下我在SSO集成中的一些经验。

  首先就是CAS协议的API文档的阅读方法。我一直认为,对于这种侧重于流程的协议,协议的运行流程以及在该流程中的数据流是关键。因此一上来就冲到API文档中并尝试理解每个接口的含义,输入及输出并不是一个好的办法。可能对于外国人而言,先了解接口再弄懂运行流程是一种比较简单的方式,但是似乎这种方式并不太适合中国人的思维。这也就是我为什么一上来就讲解SSO的运行流程的原因。

  而在CAS协议的API文档中,对ticket进行验证的接口有好几套,如第一版中的/validate接口在第二版中演进为/servicevalidate和/proxyvalidate两个接口等。请根据需要选择相应的一套接口即可。

  一个需要注意的地方就是,很多SSO里面都有一个Filter,用来标示允许使用该SSO服务的各个应用。该Filter会阻止非法应用从SSO服务登陆。否则该非法应用将可以尝试对SSO展开攻击。举一个最简单的DDOS攻击的例子。在我的 一篇博客里已经介绍过对用户名/密码对的验证实际上是一个较为耗时的行为,因此该步是SSO服务中最为脆弱的地方。如果一个恶意人员盯上了该SSO服务,并在一个用户群基数较大的网站上找到了漏洞,更改了它的SSO服务配置,那么该网站的用户就会在登陆该网站的时候跳转到该SSO服务。接下来这些用户就会尝试用他们的用户名/密码对在该SSO服务上登陆,从而在这些用户的帮助下展开了DDOS攻击。因此在通常情况下,SSO服务都会配置一个白名单。而在进行SSO集成之前,我们则首先需要向该白名单中添加我们的应用。

  还有一个比较容易被忽略的问题就是会话的时效性问题。在整个SSO的使用中存在着两种会话:用户在应用程序中的会话以及用户在SSO服务上的会话。在一般情况下,SSO服务上的会话持续时间都会比应用程序中的会话时间略长。这样在应用程序的会话过期时,用户可以直接通过刷新页面等方式从SSO服务重新建立会话。但是在进行集成时,为了调试方便,我们常常会将应用会话时间调整得很长,甚至长于SSO服务上的会话时间,从而没有测试应用程序会话过期进而通过SSO会话来刷新应用程序会话的情况。

 

Reference

CAS协议: http://jasig.github.io/cas/development/protocol/CAS-Protocol-Specification.html

 

转载请注明原文地址并标明转载: http://www.cnblogs.com/loveis715/p/4491417.html


网络嗅探工具的原理 sniffer&wireshark - CSDN博客

$
0
0

今天突然想到这个问题:wireshark之所以能抓到其它主机的包,是因为共享式以太网;那么现在的交换式以太网怎么使用wireshark?

在网上看了一些资料,整理了下面这篇文章


Sniffer(嗅探器)是一种常用的收集有用数据方法,这些数据可以是用户的帐号和密码,可以是一些商用机密数据等等。Snifffer可以作为能够捕获网络报文的设备,ISS为Sniffer这样定义:Sniffer是利用计算机的网络接口截获目的地为其他计算机的数据报文的一种工具。

Sniffer的正当用处主要是分析网络的流量,以便找出所关心的网络中潜在的问题。例如,假设网络的某一段运行得不是很好,报文的发送比较慢,而我们又不知道问题出在什么地方,此时就可以用嗅探器来作出精确的问题判断。在合理的网络中,Sniffer的存在对系统管理员是致关重要的,系统管理员通过Sniffer可以诊断出大量的不可见模糊问题,这些问题涉及两台乃至多台计算机之间的异常通讯有些甚至牵涉到各种的协议,借助于Sniffer%2C系统管理员可以方便的确定出多少的通讯量属于哪个网络协议、占主要通讯协议的主机是哪一台、大多数通讯目的地是哪台主机、报文发送占用多少时间、或者相互主机的报文传送间隔时间等等,这些信息为管理员判断网络问题、管理网络区域提供了非常宝贵的信息。

嗅探器与一般的键盘捕获程序不同。键盘捕获程序捕获在终端上输入的键值,而嗅探器则捕获真实的网络报文。

为了对Sniffer的工作原理有一个深入的了解,我们先简单介绍一下HUB与网卡的原理。

1、预备知识 HUB(集线器)与交换机工作原理

由于以太网等很多网络(常见HUB连接的内部网)是基于总线方式,物理上是广播的,就是当一个机器发给另一个机器的数据,HUB先收到然后把它接收到的数据再发给其他的(来的那个口不发了)每一个口,所以在HUB下面同一网段的所有机器的网卡都能接收到数据。

交换机的内部单片程序能记住每个口的MAC地址,以后就该哪个机器接收就发往哪个口,而不是像共享HUB那样发给所有的口,所以交换机下只有该接收数据的机器的网卡能接收到数据,当然广播包还是发往所有口。显然集线器的工作模式使得两个机器传输数据的时候其他机器的端口也占用了,所以集线器决定了 同一网段同一时间只能有两个机器进行数据通信;而交换机上连接的两个机器传输数据的时候其它机器的端口没有占用,所以别的口之间也可以同时传输。这就是交换机与HUB不同的两个地方,HUB是同一时间只能一个机器发数据并且所有机器都可以接收,只要不是广播数据交换机同一时间可以有对机器进行数据传输并且数据是私有的。

2、网卡工作原理

再讲讲网卡的工作原理。网卡收到传输来的数据,网卡内的单片程序先接收数据头的目的MAC地址,根据计算机上的网卡驱动程序设置的接收模式判断该不该接收,认为该接收就在接收后产生中断信号通知CPU,认为不该接收就丢弃不管,所以不该接收的数据网卡就截断了,计算机根本就不知道。CPU得到中断信号产生中断,操作系统就根据网卡驱动程序中设置的网卡中断程序地址调用驱动程序接收数据,驱动程序接收数据后放入信号堆栈让操作系统处理。

3、局域网如何工作

数据在网络上是以很小的称为帧(Frame)的单位传输的帧由好几部分组成,不同的部分执行不同的功能。(例如,以太网的前12个字节存放的是源和目的的地址,这些会告诉网络:数据的来源和去处。以太网帧的其他部分存放实际的用户数据、TCP/IP的报文头或IPX报文头等)。

帧通过特定的网络驱动程序进行成型,然后通过网卡发送到网线上。通过网线到达它们的目的机器,在目的机器的一端执行相反的过程。接收端机器的以太网卡捕获到这些帧,并告诉操作系统帧的到达,然后对其进行存储。就是在这个传输和接收的过程中,嗅探器会造成安全方面的问题。

通常在局域网(LAN)中同一个网段的所有网络接口都有访问在物理媒体上传输的所有数据的能力,而每个网络接口都还应该有一个硬件地址,该硬件地址不同于网络中存在的其他网络接口的硬件地址,同时,每个网络至少还要一个广播地址。(代表所有的接口地址),在正常情况下,一个合法的网络接口应该只响应这样的两种数据帧:

1、帧的目标区域具有和本地网络接口相匹配的硬件地址。

2、帧的目标区域具有“广播地址”。

在接受到上面两种情况的数据包时,网卡通过CPU产生一个硬件中断,该中断能引起操作系统注意,然后将帧中所包含的数据传送给系统进一步处理。

当采用HUB,用户发送一个报文时,这些报文就会发送到LAN上所有可用的机器。在一般情况下,网络上所有的机器都可以“听”到通过的流量,但对不属于自己的报文则不予响应(换句话说,机器A不会捕获属于机器B的数据,而是简单的忽略这些数据)。

如果局域网中某台机器的网络接口处于混杂(promiscuous)模式(即网卡可以接收其收到的所有数据包),那么它就可以捕获网络上所有的报文和帧,如果一台机器被配置成这样的方式,它(包括其软件)就是一个嗅探器。

当采用了交换机,那么正常情况下,其它主机的数据包就不会出现在本地的网络接口,那么也就无法嗅探其它主机的数据包。当然可以采用一些特殊的方法进行嗅探。

4、Sniffer原理

在介绍完前面的内容后就可以来说明Sniffer的原理了。首先,要知道SNIFFER要捕获的东西必须是要物理信号能收到的报文信息。显然只要通知网卡接收其收到的所有包(一般叫做混杂promiscuous模式:指网络上的所有设备都对总线上传送的数据进行侦听,并不仅仅是它们自己的数据。), 在HUB下就能接收到这个网段的所有包,但是交换机下就只能是自己的包加上广播包

要想在交换机下接收别人的包,那就要让其发往你的机器所在口。交换机记住一个口的MAC是通过接收来自这个口的数据后并记住其源MAC,就像一个机器的IP与MAC对应的ARP列表,交换机维护一个物理口与MAC的表,所以可以欺骗交换机的。可以发一个包设置源MAC是你想接收的机器的MAC,那么交换机就把你机器的网线插的物理口与那个MAC对应起来了,以后发给那个MAC的包就发往你的网线插口了,也就是你的网卡可以Sniffer到了。注意这物理口与MAC的表与机器的ARP表一样是动态刷新的,那机器发包后交换HUB就又记住他的口了,所以实际上是两个在争,这只能应用在只要收听少量包就可以的场合。

内部网基于IP的通信可以用ARP欺骗别人机器让其发送给你的机器,如果要想不影响原来两方的通信,可以欺骗两方,让其都发给你的机器再由你的机器转发,相当于做中间人,这用ARP加上编程很容易实现。并且现在很多设备支持远程管理,有很多交换机可以设置一个口监听别的口,不过这就要管理权限了。

利用这一点,可以将一台计算机的网络连接设置为接受所有以太网总线上的数据,从而实现Sniffer。Sniffer就是一种能将本地网卡状态设成‘混杂’状态的软件,当网卡处于这种“混杂”方式时,该网卡具备“广播地址”,它对遇到的每一个帧都产生一个硬件中断以便提醒操作系统处理流经该物理媒体上的每一个报文包。(绝大多数的网卡具备置成混杂模式的能力)

可见,Sniffer工作在网络环境中的底层,它会拦截所有的正在网络上传送的数据,并且通过相应的软件处理,可以实时分析这些数据的内容,进而分析所处的网络状态和整体布局。值得注意的是:Sniffer是极其安静的,它是一种消极的安全攻击。

嗅探器在功能和设计方面有很多不同。有些只能分析一种协议,而另一些可能能够分析几百种协议。一般情况下,大多数的嗅探器至少能够分析下面的协议:标准以太网、TCP/IP、IPX。

嗅探器造成的危害

sniffing是作用在网络基础结构的底层。通常情况下, 用户并不直接和该层打交道,有些甚至不知道有这一层存在。所以,应该说Sniffer的危害是相当之大的,通常,使用Sniffer是在网络中进行欺骗的开始。它可能造成的危害:

嗅探器能够捕获口令。这大概是绝大多数非法使用Sniffer的理由,Sniffer可以记录到明文传送的用户名和口令。能够捕获专用的或者机密的信息。比如金融帐号,许多用户很放心在网上使用自己的信用卡或现金帐号,然而Sniffer可以很轻松截获在网上传送的用户姓名、口令、信用卡号码、截止日期、帐号和pin。比如偷窥机密或敏感的信息数据,通过拦截数据包,入侵者可以很方便记录别人之间敏感的信息传送,或者干脆拦截整个的email会话过程。可以用来危害网络邻居的安全,或者用来获取更高级别的访问权限窥探低级的协议信息。

这是很可怕的事,通过对底层的信息协议记录,比如记录两台主机之间的网络接口地址、远程网络接口IP地址、IP路由信息和TCP连接的字节顺序号码等。这些信息由非法入侵的人掌握后将对网络安全构成极大的危害,通常有人用Sniffer收集这些信息只有一个原因:他正要进行一次欺骗(通常的ip地址欺骗就要求你准确插入TCP连接的字节顺序号),如果某人很关心这个问题,那么Sniffer对他来说只是前奏,今后的问题要大得多。(对于高级的黑客而言,这是使用Sniffer的唯一理由吧)

事实上,如果在网络上存在非授权的嗅探器就意味着你的系统已经暴露在别人面前了。

一般Sniffer只嗅探每个报文的前200到300个字节。用户名和口令都包含在这一部分中,这是我们关心的真正部分。

简单的放置一个嗅探器并将其放到随便什么地方将不会起到什么作用。将嗅探器放置于被攻击机器或网络附近,这样将捕获到很多口令,还有一个比较好的方法就是放在网关上。Sniffer通常运行在路由器,或有路由器功能的主机上。这样就能对大量的数据进行监控。Sniffer属第二层次的攻击。通常是攻击者已经进入了目标系统,然后使用Sniffer这种攻击手段,以便得到更多的信息。如果这样的话就能捕获网络和其他网络进行身份鉴别的过程。

Hive集成HBase详解 - MOBIN - 博客园

$
0
0
摘要
Hive提供了与HBase的集成,使得能够在HBase表上使用HQL语句进行查询 插入操作以及进行Join和Union等复杂查询
 
应用场景
1. 将ETL操作的数据存入HBase
2. HBase作为Hive的数据源
3. 构建低延时的数据仓库
 
使用
1. 从Hive中创建HBase表
  • 使用HQL语句创建一个指向HBase的Hive表
CREATE TABLE hbase_table_1(key int, value string) //Hive中的表名hbase_table_1
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'  //指定存储处理器
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf1:val") //声明列族,列名
TBLPROPERTIES ("hbase.table.name" = "xyz", "hbase.mapred.output.outputtable" = "xyz");  
//hbase.table.name声明HBase表名,为可选属性默认与Hive的表名相同,
//hbase.mapred.output.outputtable指定插入数据时写入的表,如果以后需要往该表插入数据就需要指定该值
  • 通过HBase shell可以查看刚刚创建的HBase表的属性
复制代码
$ hbase shell
HBase Shell; enter 'help<RETURN>' for list of supported commands.
Version: 0.20.3, r902334, Mon Jan 25 13:13:08 PST 2010
hbase(main):001:0> list
xyz                                                                                                           
1 row(s) in 0.0530 seconds
hbase(main):002:0> describe "xyz"
DESCRIPTION                                                           ENABLED                               
  {NAME => 'xyz', FAMILIES => [{NAME => 'cf1', COMPRESSION => 'NONE', VE true                                  
  RSIONS => '3', TTL => '2147483647', BLOCKSIZE => '65536', IN_MEMORY =>                                       'false', BLOCKCACHE => 'true'}]}                                                                            
1 row(s) in 0.0220 seconds      
hbase(main):003:0> scan "xyz" ROW COLUMN+CELL 0 row(s) in 0.0060 seconds
复制代码
  • 使用HQL向HBase表中插入数据
INSERT OVERWRITE TABLE hbase_table_1 SELECT * FROM pokes WHERE foo=98;
  • 在HBase端查看插入的数据
hbase(main):009:0> scan "xyz"
ROW                          COLUMN+CELL                                                                      
 98                          column=cf1:val, timestamp=1267737987733, value=val_98                            
1 row(s) in 0.0110 seconds
 
2.从Hive中映射HBase
  • 创建一个指向已经存在的HBase表的Hive表
CREATE EXTERNAL TABLE hbase_table_2(key int, value string) 
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = "cf1:val")
TBLPROPERTIES("hbase.table.name" = "some_existing_table", "hbase.mapred.output.outputtable" = "some_existing_table");
该Hive表一个外部表,所以删除该表并不会删除HBase表中的数据
注意
  1. 建表或映射表的时候如果没有指定:key则第一个列默认就是行键
  2. HBase对应的Hive表中没有时间戳概念,默认返回的就是最新版本的值
  3. 由于HBase中没有数据类型信息,所以在存储数据的时候都转化为String类型
3.多列及多列族的映射
如下表:value1和value2来自列族a对应的b c列,value3来自列族d对应的列
复制代码
CREATE TABLE hbase_table_1(key int, value1 string, value2 int, value3 int) 
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,a:b,a:c,d:e"
);
INSERT OVERWRITE TABLE hbase_table_1 SELECT foo, bar, foo+1, foo+2 
FROM pokes WHERE foo=98 OR foo=100;
复制代码
 
4.Hive Map类型在HBase中的映射规则
如下表:通过Hive的Map数据类型映射HBase表,这样每行都可以有不同的列组合,列名与map中的key对应,列值与map中的value对应
复制代码
CREATE TABLE hbase_table_1(value map<string,int>, row_key int) 
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = "cf:,:key"
);
INSERT OVERWRITE TABLE hbase_table_1 SELECT map(bar, foo), foo FROM pokes 
WHERE foo=98 OR foo=100;
复制代码
cf为列族,其列名对应map中的bar,列值对应map中的foo
  • 在HBase下查看数据
hbase(main):012:0> scan "hbase_table_1"
ROW                          COLUMN+CELL                                                                      
 100                         column=cf:val_100, timestamp=1267739509194, value=100                            
 98                          column=cf:val_98, timestamp=1267739509194, value=98                              
2 row(s) in 0.0080 seconds
  • 在Hive下查看数据
复制代码
hive> select * from hbase_table_1;
Total MapReduce jobs = 1
Launching Job 1 out of 1
...
OK
{"val_100":100}    100
{"val_98":98}    98
Time taken: 3.808 seconds
复制代码
注意:由于map中的key是作为HBase的列名使用的,所以map中的key类型必须为String类型
 
以下映射语句都会报错
1.
CREATE TABLE hbase_table_1(key int, value map<int,int>) 
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf:"
);
原因:map中的key必须是String
 
2.
CREATE TABLE hbase_table_1(key int, value string) 
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf:"
);
原因:当hbase.columns.mapping中的列族后面为空时(形如cf:),说明在Hive中其对应的数据类型为map,而这条语句中对应的是String所以报错
 
5.Hive还支持简单的复合行键
如下:创建一张指向HBase的Hive表,行键有两个字段,字段之间使用~分隔
CREATE EXTERNAL TABLE delimited_example(key struct<f1:string, f2:string>, value string) 
ROW FORMAT DELIMITED 
COLLECTION ITEMS TERMINATED BY '~' 
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler' 
WITH SERDEPROPERTIES ('hbase.columns.mapping'=':key,f:c1');
6.使用Hive集成HBase表的需注意
  1. 对HBase表进行预分区,增大其MapReduce作业的并行度
  2. 合理的设计rowkey使其尽可能的分布在预先分区好的Region上
  3. 通过set hbase.client.scanner.caching设置合理的扫描缓存

 

参考资料:

Hive HBase Integration

eBay Elasticsearch性能优化实践 - CSDN博客

$
0
0

原文: eBay网Elasticsearch性能优化实践
作者: Pei Wang
翻译:无阻我飞扬

摘要:Elasticsearch是基于Apache Lucene的开源搜索和分析引擎,允许用户以近乎实时的方式存储,搜索和分析数据。虽然Elasticsearch专为快速查询而设计,但其性能在很大程度上取决于用于应用程序的场景,索引的数据量以及应用程序和用户查询数据的速率。这篇文章概述了挑战和调优过程,以及Pronto团队以战略方式构建应对挑战的工具。它还以各种图形配置展示了进行基准测试的一些结果。以下是译文。

Elasticsearch是基于Apache Lucene的开源搜索和分析引擎,允许用户以近乎实时的方式存储,搜索和分析数据。Pronto是在eBay网站上托管Elasticsearch集群的平台,该平台使得eBay网内部客户易于部署,操作用于全文搜索,实时分析和日志/事件监控的大规模的Elasticsearch。当前Pronto平台管理着60多个Elasticsearch集群和2000多个节点,日采集量达到180亿份文档,日均搜索请求达到35亿份。平台提供从条款,补救以及安全到监控,报警和诊断的全方位的信息。

虽然Elasticsearch专为快速查询而设计,但其性能在很大程度上取决于用于应用程序的场景,索引的数据量以及应用程序和用户查询数据的速率。这篇文章概述了挑战和调优过程,以及Pronto团队以战略方式构建应对挑战的工具。它还以各种图形配置展示了进行基准测试的一些结果。

挑战

迄今为止所观察到的Pronto / Elasticsearch使用案例面临的挑战包括:

  1. 高吞吐量:一些集群每天摄取高达5TB的数据,一些集群每天的搜索请求超过4亿。如果Elasticsearch无法及时处理这些请求,那么这些请求将在上游累积。
  2. 搜索延迟低:对于性能关键的集群,尤其是面向站点的系统,低搜索延迟的特性是必须具有的,否则用户体验将会受到影响。
  3. 由于数据或查询是可变的,所以最佳设置总是在变化。所有情况都没有最佳设置。例如,将索引拆分成更多的分片(代表索引分片,Elasticsearch可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。)对于耗时的查询是很有好处的,但是这可能会损害其它查询性能。

解决方案

为了帮助客户应对这些挑战,Pronto团队从用户案例开始入手并持续整个集群生命周期,构建性能测试、调优和监控的战略方法。

  1. 评估集群大小:在一个新的用户案例部署之前,收集客户提供的信息,诸如吞吐量,文档大小,文档数量和搜索类型,以评估Elasticsearch集群的初始大小。
  2. 优化索引设计:与客户一起评审索引设计。
  3. 调优索引性能:根据用户场景调优索引性能和搜索性能。
  4. 调优搜索性能:使用用户真实数据/查询运行性能测试,用Elasticsearch配置参数的组合比较和分析测试结果。
  5. 运行性能测试:在案例启动以后,集群将受到监控,每当数据发生变化,查询更改或者流量增加时,用户都可以自由地重新运行性能测试。

评估集群大小

Pronto团队为每种类型的机器和每个支持的Elasticsearch版本运行基准测试,以收集性能数据,然后将其与客户提供的信息一起用于评估集群的初始大小,这些信息包括:

  • 索引吞吐量
  • 文档大小
  • 搜索吞吐量
  • 查询类型
  • 热索引文档计数
  • 保留策略
  • 响应时间要求
  • SLA级别

优化索引设计

在开始摄取数据并运行查询之前,请三思而后行。索引代表着什么?Elastic的官方回答是“具有相似特征的文档集合”。那么下一个问题是“应该使用哪些特征来对数据进行分组?应该把所有文件放入一个索引还是多个索引呢?”答案是,这取决于所使用的查询。下面是关于如何根据最常用的查询分组索引的一些建议。

  • 如果查询有一个过滤字段并且它的值是可枚举的,那么把数据分成多个索引。例如,有大量的全球产品信息被摄取到Elasticsearch中,大多数查询都有一个过滤子句“region”(区域),并且很少有机会运行跨区域查询。查询主体可以优化为:
{"query":{"bool":{"must":{"match":{"title":"${title}"}},"filter":{"term":{"region":"US"}}}}}

在这种情况下,如果索引按照美国,欧洲等地区分成几个较小的索引,就可以获得更好的性能。然后可以从查询中删除过滤子句。如果需要运行一个跨区域查询,可以将多个索引或通配符传递给Elasticsearch。

  • 如果查询具有过滤字段并且其值不可枚举,请使用路由。可以通过使用过滤字段值作为路由键来将索引拆分成多个分片,然后删除过滤条件。关于ElasticSearch里的路由功能请参见这篇 文章

    例如,Elasticsearch有数以百万计的订单,大多数查询需要通过买家ID查询订单。为每个买家创建索引是不可能的,所以不能通过买家ID将数据拆分成多个索引。一个合适的解决方案是使用路由将具有相同买家ID的所有订单放入同一个分片中,然后几乎所有的查询都可以在匹配路由键的分片内完成。

  • 如果查询具有日期范围过滤条件,则按日期分组数据。这适用于大多数日志记录或监控场景。可以以每天,每周或每月分组索引,然后可以在指定的日期范围内获得索引列表。Elasticsearch只需要查询一个较小的数据集而不是整个数据集。此外,当数据过期时,很容易缩小/删除旧的索引。

  • 明确地设置映射。Elasticsearch可以动态地创建映射,但可能并不适用于所有场景。例如,Elasticsearch 5.x中默认的字符串字段映射是“关键字”和“文本”类型,这在很多场景下是没有必要的。

  • 如果文档使用用户定义的ID或路由索引,请避免不平衡分片。Elasticsearch采用随机ID生成器和哈希算法来确保文档均匀地分配给分片。当使用用户定义的ID或路由时,ID或路由键可能不够随机,并且一些分片可能明显比其它分片更大。在这种情况下,在这个分片上的读/写操作将比在其它分片上慢得多。可以优化ID /路由键或使用 index.routing_partition_size(在5.3和更高版本中可用)。

  • 使分片均匀分布在节点上。如果一个节点比其它节点有更多的分片,则会比其它节点承担更多的负载,并很有可能成为整个系统的瓶颈。

调优索引性能

用于索引诸如日志和监控之类的重场景,索引性能是关键指标。这里有一些建议:

  • 使用批量请求
  • 使用多个线程/工作来发送请求
  • 增加刷新间隔。每次刷新事件发生时,Elasticsearch都会创建一个新的Lucene段,并在稍后进行合并。增加刷新间隔将降低创建/合并的成本。请注意,只有在刷新事件发生后才能进行文件搜索。
    这里写图片描述

    性能和刷新间隔之间的关系

从上图可以看出,随着刷新间隔的增大,吞吐量增加,响应时间变快。可以使用下面的请求来检查有多少段以及刷新和合并花费了多少时间。

Index/_stats?filter_path= indices.**.refresh,indices.**.segments,indices.**.merges
  • 减少副本数量。Elasticsearch需要为每个索引请求将文档写入主要和所有副本分片。显然,一个大的副本数会减慢索引速度,但另一方面,增加副本数量将提高搜索性能。这个话题将在本文后面讨论。副本的作用一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复;二是提高Elasticsearch的查询效率,Elasticsearch会自动对搜索请求进行负载均衡

    这里写图片描述

    性能和副本数量之间的关系

从上面的图中,可以看到随着副本数量的增加,吞吐量下降,响应时间也变慢。

  • 如果可能,使用自动生成的ID。 Elasticsearch自动生成的ID可以确保是唯一的,以避免版本查询。如果客户真的需要使用自定义的ID,建议选择一个对Lucene友好的ID,比如零填充顺序ID,UUID-1或者Nano time。这些ID具有一致的顺序模式,压缩良好。相比之下,像UUID-4这样的ID本质上仍旧是随机的,它提供了较差的压缩比,并降低了Lucene的速度。

调优搜索性能

使用Elasticsearch的主要原因是其支持通过数据进行搜索。用户应该能够快速地找到所需要查找的信息。搜索性能取决于很多因素:

  • 如果可能的话,使用过滤语境而不是查询语境。一个查询子句用于回答“这个文档如何与查询子句匹配?” ,过滤子句用于回答“这个文档是否匹配这个过滤子句?”。Elasticsearch只需要回答“是”或“否”。它不需要计算过滤子句的相关性得分,并且可以高速缓存过滤结果。有关详细信息,请参阅 查询和过滤语境
    这里写图片描述

比较查询和过滤

  • 增加刷新间隔。正如在 调优索引性能部分所提到的,Elasticsearch每次刷新时都会创建一个新的段。增加刷新间隔将有助于减少段数并降低搜索的IO成本。而且一旦发生刷新并且数据改变,缓存将无效。增加刷新间隔可以使Elasticsearch更高效地利用缓存。

  • 增加副本数量。Elasticsearch可以在主分片或副本分片上执行搜索。拥有的副本越多,搜索中涉及的节点就越多。

这里写图片描述

性能和副本数量之间的关系

从上图可以看出,搜索吞吐量几乎与副本数量成线性关系。注意在这个测试中,测试集群有足够的数据节点来确保每个分片都有一个独占节点,如果这个条件不能满足,搜索吞吐量就不会那么好。

  • 尝试不同的分片数量。“应该为索引设置多少分片?” 这可能是最常见的问题。不幸的是,所有场景都没有标准的数字,这完全取决于当时的实际情况。

太小的分片数量会使搜索无法扩展。例如,如果分片数量设置为1,则索引中的所有文档都将存储在一个分片中。对于每个搜索,只能涉及一个节点。如果有很多文件,那是很耗费时间的。另一方面,创建索引的分片太多也会对性能造成危害,因为Elasticsearch需要在所有分片上运行查询,除非在请求中指定了路由键,然后将所有返回的结果一起取出并合并。

根据经验来说,如果索引小于1G,可以将分片数设置为1。对于大多数情况,可以将分片数保留为默认值5,但是如果分片大小超过30GB,应该增加分片数量将索引分成更多的分片。创建索引后,分片数量不能更改,但是可以创建新的索引并使用reindex API转移数据。

在这里测试了一个拥有1亿个文档,大约150GB的索引,使用了100个线程发送搜索请求。

这里写图片描述

性能和分片数量之间的关系

从上图中可以看出,优化后的分片数量为11个。开始的时候,搜索吞吐量增加(响应时间减少),但随着分片数量的增加,搜索吞吐量减少(响应时间增加)。

请注意,在这个测试中,就像在副本数量测试中一样,每个分片都有一个独占节点。如果这个条件不能满足,搜索吞吐量就不会像上图所示那样好。

在这种情况下,建议尝试一个小于优化值的分片数,因为如果使用大分片数,并且使每个分片都有一个独占数据节点,那么就需要很多个节点。

  • 节点查询缓存节点查询缓存只缓存正在过滤语境中使用的查询。与查询子句不同,过滤子句是“是”或“否”的问题。Elasticsearch使用一个位设置机制来缓存过滤结果,以便后面的查询使用相同的过滤条件进行加速。请注意,只有保存超过10,000个文档的分段(或文档总数的3%,以较大者为准)才能启用节点查询缓存。有关缓存的更多详细信息,请参阅 关于缓存

可以使用下面的请求来检验一个节点查询缓存是否有效。

GET index_name/_stats?filter_path=indices.**.query_cache
{"indices": {"index_name": {"primaries": {"query_cache": {"memory_size_in_bytes":46004616,"total_count":1588886,"hit_count":515001,"miss_count":1073885,"cache_size":630,"cache_count":630,"evictions":0}
      },"total": {"query_cache": {"memory_size_in_bytes":46004616,"total_count":1588886,"hit_count":515001,"miss_count":1073885,"cache_size":630,"cache_count":630,"evictions":0}
      }
    }
  }
}
  • 分片查询缓存。如果大多数查询是聚合查询,应该看看 分片查询缓存,它可以缓存聚合结果,以便Elasticsearch直接以低成本提供请求。有几件事情需要注意:

    o 设置“size”:0。分片查询缓存只缓存聚合结果和建议。它不会缓存操作过程,因此如果将大小设置为非零,则无法从缓存中获益。

    o 有效负载JSON必须相同。分片查询缓存使用JSON主体作为缓存键,因此需要确保JSON主体不会更改,并确保JSON主体中的键具有相同的顺序。

    o Round日期时间。不要直接在查询中使用像Date.now这样的变量,Round它。否则,每个请求都会有不同的有效负载主体,从而导致缓存始终无效。建议Round日期时间为小时或天,以便更有效地利用缓存。

可以使用下面的请求来检验分片查询缓存是否有效果。

GET index_name/_stats?filter_path=indices.**.request_cache
{"indices": {"index_name": {"primaries": {"request_cache": {"memory_size_in_bytes":0,"evictions":0,"hit_count":541,"miss_count":514098}
      },"total": {"request_cache": {"memory_size_in_bytes":0,"evictions":0,"hit_count":982,"miss_count":947321}
      }
    }
  }
}
  • 仅检索必要的字段。如果文档很大,并且只需要几个字段,请使用 stored_fields检索所需要的字段而不是所有字段。

  • 避免搜索停用词。诸如“a”和“the”这样的停用词可能导致查询命中结果计数爆炸。设想有一百万个文件,搜索“fox”可能会返回几十个结果,但搜索“the fox”可能会返回索引中的所有文件,因为“the”出现在几乎所有的文件中。Elasticsearch需要对所有命中的结果进行评分和排序,导致像“the fox”这样的查询减慢整个系统。可以使用停止标记过滤来删除停用词,或使用“和”运算符将查询从“the fox”更改为“the AND fox”,以获得更精确的结果。

如果某些词在索引中经常使用,但不在默认停用词列表中,则可以使用 截止频率来动态处理它们。

  • 如果不关心文档返回的顺序,则按_doc排序。Elasticsearch使用“_score”字段按默认分数排序。如果不关心顺序,可以使用“sort”:“_doc”让Elasticsearch按索引顺序返回。

  • 避免使用脚本查询来计算不固定的匹配。在索引时存储计算的字段。例如,有一个包含大量用户信息的索引,需要查询以“1234”开头的所有用户。或许想运行一个脚本查询,如“source”:“doc [‘num’].value.startsWith(’1234’)。” 这个查询是非常耗费资源的,并且减慢整个系统。索引时考虑添加一个名为“num_prefix”的字段,然后只需要查询“name_prefix”:“1234”。

  • 避免通配符查询

运行性能测试

对于每一次改变,都需要运行性能测试来验证变更是否适用。因为Elasticsearch是一个restful服务(基于RESTful web接口),所以可以使用诸如Rally,Apache Jmeter和Gatling等工具来运行性能测试。因为Pronto团队需要在每种类型的机器和Elasticsearch版本上运行大量的基准测试,而且需要在许多Elasticsearch集群上运行Elasticsearch配置参数组合的性能测试,所以这些工具并不能满足需求。

Pronto团队构建了基于 Gatling的在线性能分析服务 ,帮助客户和我们运行性能测试并进行回归。该服务提供的功能使我们能够:

  1. 轻松添加/编辑测试。用户可以根据自己的输入查询或文档结构生成测试,而无需具有Gatling或Scala知识。
  2. 按顺序运行多个测试,无需人工干预。它可以检查状态并在每次测试之前/之后更改Elasticsearch设置。
  3. 帮助用户比较和分析测试结果分析。测试期间的测试结果和集群统计信息将保留下来,并可以通过预定义的Kibana可视化进行分析。
  4. 从命令行或Web UI运行测试。Rest API还提供了与其它系统的集成功能。

下图是架构

这里写图片描述

性能测试服务架构

用户可以查看每个测试的Gatling报告,并查看Kibana预定义的可视化图像,以便进一步分析和比较,如下图所示。

这里写图片描述

Gatling报告

这里写图片描述

Gatling报告

总结

本文概述了索引/分片/副本设计以及在设计Elasticsearch集群时应该考虑的一些其它配置,以满足摄取和搜索性能的高期望。它还说明了Pronto团队如何在战略上帮助客户进行初始规模调整,索引设计和调优以及性能测试。截至今天,Pronto团队已经帮助包括订单管理系统(OMS)和搜索引擎优化(SEO)在内的众多客户实现了苛刻的性能目标,从而为eBay的关键业务做出了贡献。

Elasticsearch的性能取决于很多因素,包括文档结构,文档大小,索引设置/映射,请求率,数据集的大小,查询命中计数等等。针对一种情况的性能优化推荐不一定适用于另一种情况。彻底地测试性能,收集遥测数据,根据工作负载调整配置以及优化以满足性能要求非常重要。


sky walking 监听程序的性能开源项目 - 不忘初心,方得始终。 - ITeye博客

$
0
0

apm (Application Performance Managment :应用性能管理)简写,业界有很多成熟的收费工具,听云、OneAPM等。当然也有开源的apm,git地址: https://github.com/wu-sheng/sky-walking。功能方面当然和收费的没法比,但是了解一下也挺不错的,skywalking采用elasticsearch数据存储。了解es以及看过skywalking原码的肯定会说有很多弊端,毕竟是开源的嘛,分享精神以及设计架构挺值得学习的,希望越来越好,本期针对V3.1版本,整体大致分为 collector、web、agent、es 这几部分,应用到项目中也肯容易。v3.1支持es版本为5.2x,5.3x。官网文档 wiki地址

  1.首先搭建es 下载地址,Mac选择TAR,

   解压:

 

tar -zxvf elasticsearch-5.3.3.tar.gz
 cd elasticsearch-5.3.2

    config下面elasticsearch.yml为es的配置文件 

 

   配置  cluster.name: myesdb。此名称需要和collector配置文件一致。

    启动es

 

./bin/elasticsearch

 2安装部署collector

 

     下载 collector

     解压安装包 tar -xvf skywalking-collector.tar.gz,windows用户可以选择zip包;

     设置config目录下的 collector.config配置文件;

     

#配置es(节点,多个逗号分隔)
es.cluster.nodes=127.0.0.1:9300
#es集群名称
es.cluster.name = myesdb
es.cluster.transport.sniffer = true
#collector当前主机名或者IP地址,请使用真实的地址,默认为127.0.0.1
cluster.current.hostname = 127.0.0.1
# 监听端口.
cluster.current.port = 11800
#RESTful
http.hostname=127.0.0.1
http.port=12800

   启动:

 

./bin/startup.sh 
Starting collector....
Collector started successfully!       
注意:collector-service.sh文件中请检查自己的JAVA_HOME是否配置,.sh文件中名称也要对应到环境变量中

  3配置web

     下载地址

     解压tar -xvf skywalking-web.tar.gz 同样windows用户可以选择zip包;

     设置config目录下的collector_config.properties配置文件

#collector服务配置
collector.servers[0]=127.0.0.1:12800

   设置config目录下的application.properties 配置文件

#web服务端口
server.port = 8088

   启动

./bin/startup.sh
Starting web service....
Skywalking Web started successfully!

   打开http://localhost:8088/ 可以看到



 

    空白无所谓,因为还没有在项目中部署agent,没有收集到数据,下面我们开始部署 agent

     文档地址

    按照文档在agent.jar同级目录下创建sky-walking.config 文件

  

# 当前的应用编码,最终会显示在webui上。
# 建议一个应用的多个实例,使用有相同的application_code。请使用英文
agent.application_code=myproject

# 默认为1,表示启动采样机制,即每条调用链都会被追踪并上报
# 大于一时,则表示每N次访问,上报一条。
# 小于等于0位非法。
agent.sampling_cycle=1

# Collector REST-Service 服务地址.
# e.g.
# 单节点配置:SERVERS="127.0.0.1:8080"
# 集群配置:SERVERS="10.2.45.126:8080,10.2.45.127:7600"
collector.servers=127.0.0.1:12800

# Collector 接受追踪信息REST-Service 服务名称.
# 默认不需要修改
collector.service_name=/segments

# 向collector发送数据时,单次调用的最大容量
collector.batch_size=50

# 内部缓冲池大小,此值必须是2的指数倍。
# 相关资料: https://github.com/LMAX-Exchange/disruptor
buffer.size=512

# 日志文件名称
logging.file_name=skywalking-api.log

# 日志文件路径
# 默认为空, 使用"system.out"输出日志,一般会输出到中间件或者应用的控制台日志中。
logging.dir=

# 日志文件最大大小
# 如果超过此大小,则会生成新文件。
# 默认为300M
logging.max_file_size=314572800

# 日志级别,默认为DEBUG。
logging.level=DEBUG
    启动我们项目

 

    

java -javaagent:/Users/xxx/java/software/skywalking-agent.jar -jar  wp-xx-SNAPSHOT-exec.jar --server.port=8081 

 

 访问我们项目产生访问数据,刷新我们skywalking-web 页面 看到数据呈现效果:



 同样我们可以观察某次请求结果



 

以上是整个部署后的效果,其中很多程序在ping redis,检测线程的可用性。然后几分钟后统计了下es里的数据条数,没有正常访问数据的情况下产生了近 "hits": {"total": 13487, 条数据,用在生成环境堪忧,esindex没有按照规定生成,后期数据处理的话,很费劲。感谢开源,感谢分享,一些测试环境排查问题还是不错的。

索引表和ES的一点点思考 - CSDN博客

$
0
0

索引表设计


在电商项目中,物理库存系统是个极其重要的系统,订单支付后,就会开始来占用物理库存。一般情况下,库存系统都是要分库的,因为主要的操作是写操作,例如占用/释放/取消等写操作。使用分库可以降低数据库写的压力。尽管写操作为主,但是读操作也是有的。比如说,库存占用的时候,得先查询是否有库存,而这个查询操作并不都会带上分库因子(用于路由到具体的某个数据库),而是一些比较宽松的查询条件,这些查询条件对应的数据可能分布在不同的数据库上。这个时候为了查询的方便,会构建一个索引表。这个索引表是 存在另外的单独的一个数据库中,不会再分库了的。

索引表的设计也分不同情况,大体可以分三种。
1、查询字段+数据库主键

把查询字段放到索引表,还需要把对应的数据库主键ID也放置进去。当查询请求到来时,根据查询条件找出对应的数据主键,再根据数据主键路由到对应的存有完整业务数据的数据分库上。这种方案呢。索引表占用空间小,可以支撑很久。但是要找出业务数据,还是需要路由到分库上。另外,如果要把索引表的数据存储到ES搜索引擎上的话,这种方式就不行了。因为索引表中没有外部系统要的业务数据。所以当时的库存系统没有使用这种索引表设计方案。

2、查询字段+数据库主键+需要展示的业务字段

这种方案呢。当请求到来的时候,直接查询索引表即可。无需根据主键路由到分库了。同时如果要结合ES的话。可以直接把索引表的数据弄到ES上即可。后续直接让ES暴露查询接口即可。目前我在唯品会参与的物理库存项目中,是使用这种方式的。但是这个方案也有个缺点。就是索引表的体积比较大,后续数据量一大的话,也是个问题。能不能优化一下呢?

3、索引表拆分

上面说到的第二种方案,索引表的膨胀可能很快,可以考虑将索引表拆分。比如说:索引表只是保存查询条件和主键,而需要展示给外部系统的数据,另外存储在单独的表上。比如叫index_detail表。这个表拥有索引表的主键。这样的话,当查询请求到来的时候,先从索引表查询到主键,再根据主键从index_detail表中查询出数据。当然这样做的话。ES的数据来源就变成多张表了,不过这是可以接受的。


如何把业务数据写入到索引表


使用MQ


一般来说,构建索引数据是使用单独一个应用来做的。比如叫data-index域。这个域会从消息队列中读取消息,用于构建索引数据。当业务数据发生变化后,生产者发送MQ消息到队列上。
这里的消息设计也分两种情况。一种是消息只是带有数据主键和操作类型(ADD/Update/DELETE),消费者拿到主键后再去DB获取完整的数据并插入到索引表中。另一种方案呢,是消息包含了大部分需要的字段,消费者拿到消息后直接把数据插入到索引表中。这两种消息设计,我在实际项目中都有用过。


直接操作DB


这种方案呢就比较粗暴,直接配置一个索引表库的数据源,当业务数据发生变化时,使用Mybatis或者JDBC把数据更新到索引表中。一般不建议这么做,一来构建索引数据的逻辑跟数据的CRUD操作融合在一起了。二来,操作其他数据库的数据,要么通过接口的方式,要么由单独的域来做。建议还是使用MQ的方式来构建索引数据。


如何把索引表数据弄到ES上


监听数据库表的数据变化


像在唯品会这边,自研了一个叫VDP的组件,使用storm job去监听索引表数据的变化,一旦有变化,就把数据同步到队列中,ES直接从队列中获取数据,并存储到ES上。
这种方案的好处是,我们无需写任何代码,数据自动可以同步到ES上。


使用MQ


如果公司内部没有开发VDP这样的组件,可以通过发送MQ消息的方式来将索引表的数据同步数据到ES上。


让ES暴露CUD接口


另外一种方案是,让ES暴露CUD接口,用于同步索引表数据。但是这样就跟ES耦合在一块了。不太推荐这么做。


进一步的思考


1、ES不支持Group By这样的操作,所以在构建索引表的时候,可以事先计算好Group By的一些统计数据,并存储到索引表中;
2、一些后台应用中,如果数据库表的数量已经很大,好几个亿了,并且查询的SQL还非常变态,用数据库已经无法支持了,那么可以使用ES,查询速度快,也支持一些统计操作;
3、使用ES输出数据时,也有个坑。经常会拿到脏数据的。例如当数据发送变化后,构建索引数据并把索引数据同步到ES上是需要时间的,但是我们后台通常有将数据下架的操作,下架的操作操作完后,再次点击查询按钮,可能还是看到脏数据,因为数据同步到ES上没那么快。现在我还没想到很好的办法来解决这个问题。欢迎网友提些建议。

FastText 文本分类使用心得 - CSDN博客

$
0
0

最近在一个项目里使用了fasttext[1], 这是facebook今年开源的一个词向量与文本分类工具,在学术上没有什么创新点,但是好处就是模型简单,训练速度又非常快。我在最近的一个项目里尝试了一下,发现用起来真的很顺手,做出来的结果也可以达到上线使用的标准。

其实fasttext使用的模型与word2vec的模型在结构上是一样的,拿cbow来说,不同的只是在于word2vec cbow的目标是通过当前词的前后N个词来预测当前词,在使用层次softmax的时候,huffman树叶子节点处是训练语料里所有词的向量。

而fasttext在进行文本分类时,huffmax树叶子节点处是每一个类别标签的词向量,在训练的过程中,训练语料的每一个词也会得到对应的词向量,输入为一个window内的词对应的词向量,hidden layer为这几个词的线性相加,相加的结果作为该文档的向量,再通过层次softmax得到预测标签,结合文档的真实标签计算loss,梯度与迭代更新词向量。

fasttext有别于word2vec的另一点是加了ngram切分这个trick,将长词再通过ngram切分为几个短词,这样对于未登录词也可以通过切出来的ngram词向量合并为一个词。由于中文的词大多比较短,这对英文语料的用处会比中文语料更大。

此外,fasttext相比deep learning模型的优点是训练速度极快。我们目前使用fasttext来进行客户填写的订单地址到镇这一级别的分类。每一个省份建立一个模型,每个模型要分的类别都有1000多类,200万左右的训练数据,12个线程1分钟不到就可以训练完成,最终的分类准确率与模型鲁棒性都比较高(区县级别分类正确准确率高于99.5%, 镇级别高于98%),尤其是对缩写地名,或者漏写了市级行政区、区县级行政区的情况也都可以正确处理。

参数方面

  1. loss function选用hs(hierarchical softmax)要比ns(negative sampling) 训练速度要快很多倍,并且准确率也更高。

  2. wordNgrams 默认为1,设置为2以上可以明显提高准确率。

  3. 如果词数不是很多,可以把bucket设置的小一点,否则预留会预留太多bucket使模型太大。

因为facebook提供的只是C++版本的代码,原本还以为要自己封装一个python接口,结果上github一搜已经有封装的python接口了[2]。用起来特别方便,觉得还不能满足自己的使用要求,修改源码也非常方便。

对于同样的文本分类问题,后来还用单向LSTM做了一遍,输入pre-trained的embedding词向量,并且在训练的时候fine-tune,与fasttext对比,即使使用了GTX 980的GPU,训练速度还是要慢很多,并且,准确准确率和fasttext是差不多的。

所以对于文本分类,先用fasttext做一个简单的baseline是很适合的。

[1] https://github.com/facebookresearch/fastText
[2] https://github.com/salestock/fastText.py

NLP︱高级词向量表达(二)——FastText(简述、学习笔记) - CSDN博客

$
0
0

FastText是Facebook开发的一款快速文本分类器,提供简单而高效的文本分类和表征学习的方法,不过这个项目其实是有两部分组成的,一部分是这篇文章介绍的
fastText 文本分类(paper: A. Joulin, E. Grave, P. Bojanowski, T. Mikolov,
Bag of Tricks for Efficient Text
Classification(高效文本分类技巧)
),
另一部分是词嵌入学习(paper: P.
Bojanowski*, E. Grave*, A. Joulin, T. Mikolov, Enriching Word Vectors
with Subword
Information
(使用子字信息丰富词汇向量))。
按论文来说只有文本分类部分才是 fastText,但也有人把这两部分合在一起称为
fastText。笔者,在这即认为词嵌入学习属于FastText项目。
github链接: https://github.com/facebookresearch/fastText

.

高级词向量三部曲:

1、 NLP︱高级词向量表达(一)——GloVe(理论、相关测评结果、R&python实现、相关应用)
2、 NLP︱高级词向量表达(二)——FastText(简述、学习笔记)
3、 NLP︱高级词向量表达(三)——WordRank(简述)
4、其他NLP词表示方法paper: 从符号到分布式表示NLP中词各种表示方法综述


一、FastText架构

本节内容参考自:
1、开源中国社区 [ http://www.oschina.net]《Facebook 开源的快速文本分类器 FastTexT》
2、雷锋网文章:《 比深度学习快几个数量级,详解Facebook最新开源工具——fastText

.

1、fastText 架构原理

fastText 方法包含三部分:模型架构、层次 Softmax 和 N-gram 特征。

fastText 模型输入一个词的序列(一段文本或者一句话),输出这个词序列属于不同类别的概率。
序列中的词和词组组成特征向量,特征向量通过线性变换映射到中间层,中间层再映射到标签。
fastText 在预测标签时使用了非线性激活函数,但在中间层不使用非线性激活函数。
fastText 模型架构和 Word2Vec 中的 CBOW 模型很类似。不同之处在于,fastText 预测标签,而 CBOW 模型预测中间词。

这里写图片描述
.
.

2、改善运算效率——softmax层级

对于有大量类别的数据集,fastText使用了一个分层分类器(而非扁平式架构)。不同的类别被整合进树形结构中(想象下二叉树而非 list)。在某些文本分类任务中类别很多,计算线性分类器的复杂度高。为了改善运行时间,fastText 模型使用了层次 Softmax 技巧。层次 Softmax 技巧建立在哈弗曼编码的基础上,对标签进行编码,能够极大地缩小模型预测目标的数量。(参考 博客
这里写图片描述

考虑到线性以及多种类别的对数模型,这大大减少了训练复杂性和测试文本分类器的时间。fastText 也利用了类别(class)不均衡这个事实(一些类别出现次数比其他的更多),通过使用 Huffman 算法建立用于表征类别的树形结构。因此,频繁出现类别的树形结构的深度要比不频繁出现类别的树形结构的深度要小,这也使得进一步的计算效率更高。
这里写图片描述
.
.
.


二、FastText的词向量表征

1、FastText的N-gram特征

常用的特征是词袋模型。但词袋模型不能考虑词之间的顺序,因此 fastText 还加入了 N-gram 特征。
“我 爱 她” 这句话中的词袋模型特征是 “我”,“爱”, “她”。这些特征和句子 “她 爱 我” 的特征是一样的。
如果加入 2-Ngram,第一句话的特征还有 “我-爱” 和 “爱-她”,这两句话 “我 爱 她” 和 “她 爱 我” 就能区别开来了。当然,为了提高效率,我们需要过滤掉低频的 N-gram。

在 fastText 中一个低维度向量与每个单词都相关。隐藏表征在不同类别所有分类器中进行共享,使得文本信息在不同类别中能够共同使用。这类表征被称为词袋(bag of words)(此处忽视词序)。在 fastText中也使用向量表征单词 n-gram来将局部词序考虑在内,这对很多文本分类问题来说十分重要。

举例来说:fastText能够学会“男孩”、“女孩”、“男人”、“女人”指代的是特定的性别,并且能够将这些数值存在相关文档中。然后,当某个程序在提出一个用户请求(假设是“我女友现在在儿?”),它能够马上在fastText生成的文档中进行查找并且理解用户想要问的是有关女性的问题。
.
.

2、FastText词向量优势

(1)适合大型数据+高效的训练速度:能够训练模型“在使用标准多核CPU的情况下10分钟内处理超过10亿个词汇”,特别是与深度模型对比,fastText能将训练时间由数天缩短到几秒钟。使用一个标准多核 CPU,得到了在10分钟内训练完超过10亿词汇量模型的结果。此外, fastText还能在五分钟内将50万个句子分成超过30万个类别。
(2)支持多语言表达:利用其语言形态结构,fastText能够被设计用来支持包括英语、德语、西班牙语、法语以及捷克语等多种语言。它还使用了一种简单高效的纳入子字信息的方式,在用于像捷克语这样词态丰富的语言时,这种方式表现得非常好,这也证明了精心设计的字符 n-gram 特征是丰富词汇表征的重要来源。FastText的性能要比时下流行的word2vec工具明显好上不少,也比其他目前最先进的词态词汇表征要好。
这里写图片描述
(3)fastText专注于文本分类,在许多标准问题上实现当下最好的表现(例如文本倾向性分析或标签预测)。FastText与基于深度学习方法的Char-CNN以及VDCNN对比:
FastText与基于深度学习方法的Char-CNN以及VDCNN对比
(4)比word2vec更考虑了相似性,比如 fastText 的词嵌入学习能够考虑 english-born 和 british-born 之间有相同的后缀,但 word2vec 却不能(具体参考 paper)。
.
.

3、FastText词向量与word2vec对比

本节来源于博客: fasttext
FastText= word2vec中 cbow + h-softmax的灵活使用

灵活体现在两个方面:
1. 模型的输出层:word2vec的输出层,对应的是每一个term,计算某term的概率最大;而fasttext的输出层对应的是 分类的label。不过不管输出层对应的是什么内容,起对应的vector都不会被保留和使用;
2. 模型的输入层:word2vec的输出层,是 context window 内的term;而fasttext 对应的整个sentence的内容,包括term,也包括 n-gram的内容;

两者本质的不同,体现在 h-softmax的使用。
Wordvec的目的是得到词向量,该词向量 最终是在输入层得到,输出层对应的 h-softmax 也会生成一系列的向量,但最终都被抛弃,不会使用。
fasttext则充分利用了h-softmax的分类功能,遍历分类树的所有叶节点,找到概率最大的label(一个或者N个)

相关文章: Jupyter notebooks:Comparison of FastText and Word2Vec
.
.
.


三、FastText实现

github链接: https://github.com/facebookresearch/fastText
fastText基于Mac OS或者Linux系统构筑,使用 C++11 的特性。需要python 2.6 或者更高版本支持,以及numpy & scipy等软件支持。
FastText默认参数:

  $ ./fasttext supervised
Empty input or output path. The following arguments are mandatory: -input training file path -output output file path The following arguments are optional: -lr learning rate [0.1] -lrUpdateRate change the rate of updates for the learning rate [100] -dim size of word vectors [100] -ws size of the context window [5] -epoch number of epochs [5] -minCount minimal number of word occurences [1] -minCountLabel minimal number of label occurences [0] -neg number of negatives sampled [5] -wordNgrams max length of word ngram [1] -loss loss function {ns, hs, softmax} [ns] -bucket number of buckets [2000000] -minn min length of char ngram [0] -maxn max length of char ngram [0] -thread number of threads [12] -t sampling threshold [0.0001] -label labels prefix [__label__] -verbose verbosity level [2] -pretrainedVectors pretrained word vectors for supervised learning []

Mikolov 在 fastTetxt 的论文中报告了两个实验,其中一个实验和 Tagspace 模型进行对比。实验是在 YFCC100M 数据集上进行的, YFCC100M 数据集包含将近 1 亿张图片以及摘要、标题和标签。实验使用摘要和标题去预测标签。Tagspace 模型是建立在 Wsabie 模型的基础上的。Wsabie 模型除了利用 CNN 抽取特征之外,还提出了一个带权近似配对排序 (Weighted Approximate-Rank Pairwise, WARP) 损失函数用于处理预测目标数量巨大的问题。
这里写图片描述
上面就是实验结果,从实验结果来看 fastText 能够取得比 Tagspace 好的效果,并拥有无以伦比的训练测试速度。但严格来说,这个实验对 Tagspace 有些不公平。YFCC100M 数据集是关于多标记分类的,即需要模型能从多个类别里预测出多个类。Tagspace 确实是做多标记分类的;但 fastText 只能做多类别分类,从多个类别里预测出一个类。而评价指标 prec@1 只评价一个预测结果,刚好能够评价多类别分类。

YFCC100M 数据: https://research.facebook.com/research/fasttext/


.

延伸一:重磅:facebook公开了90种语言的Pre-trained word vectors

https://github.com/facebookresearch/fastText/blob/master/pretrained-vectors.md
可怕的facebook,用fasttext进行训练,使用默认参数,300维度

能不用事务就尽量别用 - CSDN博客

$
0
0

概述


以前在公司里,有个牛人对俺说:

事务就是个垃圾,能不用就尽量不用。

当时我刚从传统行业切换到互联网行业,对这个牛人说的这句话是嗤之以鼻的,怎么可能不用事务呢?后来随着开发了多个高并发应用后,才知道这个牛人说的是对的。下面说两个亲身经历的案例来说明这个问题。


库存扣减接口(写事务)


当时我们有个业务,在购物车阶段的时候,就开始占用库存了,这个库存占用接口的流量非常大。当时我开发完这个接口后,测试人员的压测结果是

2500/TPS

我觉得低了,就开始怀疑测试人员是否不够专业,测试用的服务端应用服务器和数据库是否不给力,发起请求的压测客户端机器是否给力,jmeter用的是否正确。全部检查完后,发现测试手法是完全OK的。

于是仔细的检查代码,也没发现啥。后来灵光一闪,想起那个牛人说过得的话,就尝试把事务去掉。我们用的是Spring的 @Transactional(rollbackFor = Exception.class)的开启事务的。我把这行代码删除掉后,压测结果是:

3700/QPS

提高了蛮多,这个压测结果已经能满足线上流量要求了。开了事务为啥性能差这么多,底层原因目前还没仔细去研究。但是从压测结果来看,去掉事务后,性能有提升。

这里部分网友可能有疑问,如果线程操作的是多张表的写入和更新操作,如果不用事务的话,一致性怎么保证? 好问题。当时我是使用 逻辑回滚的手段来保证数据的一致性,是需要自己写代码进行数据回滚的。因为当时要操作的表只有两张,代码写起来还好些。

如果很多张表的话,用逻辑回滚就比较麻烦了。对于这种很多张表的,而流量又不是很大那种的话,还是使用事务方便一些。


读接口(读事务)


当时有个输出商品的接口,读取逻辑如下:

先从memcache读取,如果没有读取到,则从DB读取。

开发完这个接口后,做压测时,我先预热一些数据到memcache上,想看看memcache的读取性能。压测结果不太理想。当时找了很久都没找到原因,后来我用top命令观察了一下,发现只要一压测,应用还是会跟mysql有交互。理论上是不会的,因为数据都在memcache上,一旦命中就走了,不会去读取数据库。

最后面发现原来用了Spring的

@Transactional(readOnly = true)

只要你在接口上面加了这个注解,Spring还是会跟Mysql打交道的。并发的量一旦大了,就开始影响性能了。

Viewing all 532 articles
Browse latest View live