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

阿里IoT使用总结 - alcoholdi的专栏 - CSDN博客

$
0
0

首先得感慨下写个App比之前真的简单方便多了。

需要推送功能直接考虑集成友盟、极光、个推、小米推送、华为推送。

需要IM功能直接考虑集成环信、融云、网易云信、腾讯云通、阿里云川等这些解决方案。

这些传统功能就不谈了,连这两年崛起的直播、娃娃机、答题业务,你都能找到好几家第三方解决方案,提供完整sdk直接集成。

 

物联网(英语:Internet of Things,缩写IoT)

理论上推送、即时通讯、物联网在技术层面都是通过xmpp、mqtt、CoAP这些协议建立个长连接从而可以双方互发消息,或者更集中地说是为了如何优雅的解决服务器实时发消息到客户端的问题(因为客户端发消息到服务端只需要Http协议就可以)。那么物联网,或者说以本文的阿里IoT为例,对比起来另外两者有什么区别呢?

 

我认为最重要的区别就是IoT有一个影子(shadow)的概念。

在亚马逊云里,这样描述的影子的。事物影子服务充当中介,支持设备和应用程序检索和更新事物影子。

阿里云里,是这么描述的。设备影子是一个 JSON 文档,用于存储设备上报状态、应用程序期望状态信息。

因为物联网里面的设备端,并不保证设备端24小时在线的。所以如果想查询设备信息,或者更新设备状态,发个请求过去返回超时回来,一看是设备不在线,是个很麻烦的事。所以设备需要一个缓存机制,当设备不在线时,能让我们①获取他的状态,②保存我们对设备期望状态以便下次设备上线能拿到。

阿里文档给了几个例子可以帮助理解一下: 设备影子介绍

 

IoT里面是根据『产品』对所有智能设备分类的。比如说智能台灯A是一个产品,智能插座B是另一个产品。每个产品根据productKey来区分。一个产品里面有很多台设备,每个设备都有一个deviceName和deviceSecret与之对应。

通常把这三者联合起来称作 三元组信息(productKey、deviceName及deviceSecret)。

 

阿里IoT套件支持的两种通信模式,PUB/SUB以及RRPC。RRPC稍后再提,先来讲讲这个PUB/SUB,对应中文发布/订阅的意思。注意区分Topic类和具体的Topic的。

Topic类:/${productKey}/${deviceName}/update
具体的Topic:/${productKey}/device1/update或者/pk/device2/update

阿里给产品默认定义了如下三个Topic类:

/${productKey}/${deviceName}/get:订阅
/${productKey}/${deviceName}/update:发布
/${productKey}/${deviceName}/update/error:发布

还有两个影子相关的的Topic类提供使用:
/shadow/update/${productKey}/${deviceName}   (设备或应用程序)更新影子的Topic
/shadow/get/${productKey}/${deviceName}     设备影子会更新状态到该Topic,设备订阅此Topic的消息。

当然可以自己添加更多的Topic类,按照这种格式写一个就行。

 



对于设备端接入,阿里提供了C-SDK、Java-SDK、Android-SDK和iOS-SDK。
Java-SDK直接是使用开源框架『Eclipse paho mqtt』。而Android-SDK则是阿里对这个开源框架进行了一层封装。
所以搞懂了这个框架怎么用,就能知道MQTT的连接原理。

看了下demo后,我总结为以下步骤。

1.引入eclipse.paho相关的库,库里有MqttClient但是针对Android有更适合MqttAndroidClient提供我们使用。
compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.1'

2.建立连接需要三元组信息(productKey、deviceName及deviceSecret)。对于设备板子,提前把这些烧录进去是件很麻烦并且很不灵活的事情。所以一般是设备确定一个自己的唯一标识,比如说MAC地址或者SN。发出一个请求自己的后台服务器,后台帮忙在IoT创建一个设备,并返回相应的三元信息。

3.通过请求『https://iot-auth.cn-shanghai.aliyuncs.com/auth/devicename』的返回字段,能得出最终的长连接Uri(一般为ssl://host:port)、username和password。

4.执行mClient = new MqttAndroidClient(context, serverUri, clientId)

5.设置mqtt的callback,mClient.setCallback( new MqttCallbackExtended(){//实现方法} )。里面需要的方法如下:
5.1 connectComplete表示连接成功。
5.2 connectionLost表示连接失败,会自动尝试重连所以不用管。
5.3 deliveryComplete应该是发送消息成功的返回,可以忽略。
5.4 messageArrived(String topic, MqttMessage message)  接受消息

6.配置MqttConnectOptions参数, 比如Username,password,automaticReconnect,最后执行MqttAndroidClient.connect(options, ...)

7.根据是否连接成功,开始订阅Topics,如/${productKey}/${deviceName}/get 和 /shadow/get/${productKey}/${deviceName} 

8.订阅了那些Topic,在前面callback类里的messageArrived方法中就是收到该Topic的消息。调用MqttAndroidClient的publish(String topic, MqttMessage message)方法,能往相应Topic中发送消息。

 

 

设备端连接成功后,就可以在IoT管理控制台查看当前设备是否在线,查看设备的影子信息,设置期望值给设备影子这些操作了。

 

除了在控制台能查看设备信息之外,我们在IoT的管理控制台,开启『配置服务端订阅功能』,勾选『设备上报消息』和『设备状态变化通知』(就是设备上下线消息),就能把这些消息推送到阿里的MNS消息队列里面。然后让我们自己的后台服务器订阅这个消息队列,就能实时获取到设备的当前在线状态和其功能状态(比如智能灯有没打开,智能插座有没通电)。

具体来说就是通过和阿里云账号绑定的AccessKeyId和AccessKeySecret,还有写代码通过队列名字比如『aliyun-iot-4CbI0012osF』获取到队列消息。

 

 

云端请求设备端(RRPC)

MQTT是使用于发布/订阅的的异步通信模式,但是有时候我们需要发送一个请求到设备端并希望他立刻响应返回。所以阿里为此在MQTT协议的基础上,封装了一套机制专门用于处理同步请求消息。具体来说就是

请求来的Topic会是:/sys/${productKey}/${deviceName}/rrpc/request/${messageId}
设备需要订阅/sys/${productKey}/${deviceName}/rrpc/request/+

设备需要响应回去的Topic是:/sys/${productKey}/${deviceName}/rrpc/response/${messageId}

按照这种格式接受响应和回复,就能实现同步的返回。

参考链接:

RRPC文档

 

M2M    分为处理数据(筛选出哪些需要转发的) 和 转发数据

想要手机控制一盏智能灯的开关,一般的做法就是手机发送一个Http请求到自己的后台服务器,后台服务器再发送命令到阿里IoT,IoT就会通知到智能灯设备打开关闭。

但是这样的做法会让我们服务器的压力过大,所以阿里为此推出了M2M这个解决方案。意思就是通过SQL的语法从某一个Topic里面筛选出想要的数据,像一个拦截器一样。然后转发到另一个Topic里面。这样我们就可以设计成,筛选出手机端发送到IoT的请求,判断出如果是打开关闭智能灯的请求,就把它转发到对应那个智能灯设备里。从而减少自己服务器的压力。

除此之外,阿里IoT其实提供了更多的转发操作。例如转发到Table Store、RDS、Message Queue、DataHub,能让我们更好的完成数据存储,转发,分析等操作。

M2M文档

 

 

 

 

 

 

 

 


KETTLE监控 - 月饼馅饺子 - 博客园

$
0
0
kettle单实例环境下自身没有监控工具,但在集群下自带了监控工具。

一、集群自带的监控
kettle自带的集群监控工具可以监控转换的执行情况。

配置好集群后,打开浏览器:输入 http://localhost:8080,输入子服务器的用户名和密码


进入后,点击show status:


点击转换名称可以看到转换的详情:


该方式有三个缺点:
(1)无法监控job的执行情况。
(2)另外,如果一个转换不使用集群执行,也不会被监控。
(3)该监控的获取的数据来取内存,在关闭carte服务器后,数据消失,之前的监控信息丢失
综合以上信息,该监控并不能满足ETL需求,需要手动做监控程序。


二、自己开发kettle监控程序。
自己开发监控程序,原理是在转换和任务中设置log,执行情况会记录在日志中,通过读取日志情况判断执行情况。
2.1.在转换和任务中设置日志
转换:

一共有5种日志

a.转换日志 
    显示转换名称、开始时间、结束时间、执行状态等
b.步骤日志
    显示步骤相关情况(集群下不写入该表)

c.运行日志
    在默认日志级别下不没有数据(集群下不写入该表)
d.通道日志
    各日志通道的输出情况(集群下不写入该表)

e.指标日志(略)

2.2 任务日志
作业:

日志:

a.作业日志表
    保存作业的开始时间、结束时间、状态等

b.作业项日志表
    作业中的项目运行情况

c.日志通道日志表





2.3 监控流程
2.3.1 从资源库读取job列表
  1. select id_job,name from kettle_res.r_job a ;
2.3.2 读取job中的转换的执行状态、上次执行时间
  1. select distinct a.id_job,a.name job_name,b.name trans_name,c.status trans_status,c.LOGDATE laste_exec_time
  2. from kettle_res.r_job a left join kettle_res.r_jobentry b on a.id_job=b.id_job and b.id_jobentry_type=87
  3. left join test.trans_logs c on b.name=SUBSTRING_INDEX(c.transname,'(',1)
  4. and c.logdate= (select max(logdate) from test.trans_logs d where SUBSTRING_INDEX(d.transname,'(',1) =SUBSTRING_INDEX(c.transname,'(',1) group by SUBSTRING_INDEX(d.transname,'(',1) )
  5. group by a.name order by a.id_job,b.id_jobentry
结果:

至此完成了最基本的监控。

2.3.2 job执行历史
select JOBNAME,status,LOGDATE from test.job_log where jobname=? order by LOGDATE desc

2.3.3 转换执行历史
select transname,status,logdate from test.trans_logs where SUBSTRING_INDEX(transname,'(',1)=? order by logdate desc

2.3.4 短信

在执行出错时可以发送邮件。


注意:看邮件提供商是否支持pop3/SMTP协议,是否需要使用SSL连接。

2.3.5 短信监控

浅谈软件研发管理体系建设 - 追求卓越 - CSDN博客

$
0
0

最近一段时间,我一直在反复思考一个问题:我们的软件研发管理体系应该是怎样的?在不断思考的过程中,逐步有一些粗浅的认识,在此将这些认识记录成文字,并期待能够与更多的伙伴碰撞,进一步完善这种认识,并逐步上升到理论高度,从而有利于指导具体实践。

1. 对软件研发管理体系的一些概念认知
1.1. 研发管理是什么
关于研发管理,百度百科中这样定义:研发管理就是在研发体系结构设计和各种管理理论基础之上,借助信息平台对研发过程中进行的团队建设、流程设计、绩效管理、风险管理、成本管理、项目管理和知识管理等的一系列协调活动。

也就是说,研发管理首要一点就是要根据公司业务的发展确定相应的研发体系结构,之后按照这种研发体系结构组件一支高水平的研发团队,设计高效合理的研发流程,借助合适的研发信息平台支持研发团队高效工作,以绩效管理调动研发团队的积极性,以风险管理控制研发风险,以成本管理使研发在成本预算范围内完成研发工作,以项目管理确保研发项目的顺利进行,而知识管理使得研发团队的智慧联网和知识沉淀。

纵观各类软件企业,由于自身所处环境不同,因此其软件研发管理模式也不尽相同,这其中有基于CMMI能力成熟度模型指导下构建的研发管理体系,也有基于IPD集成产品研发框架指导下构建的研发管理体系,当然也有一些目前不少小企业、互联网企业推崇的敏捷研发管理体系。不同的研发管理体系其实都会有相应的交叉部分,最终追求的目标都是能否适合企业的发展,给企业带来市场和财务上的成功。

1.2. 基于CMMI的研发管理
CMMI能力成熟度模型相信大家都不陌生,从一级到五级,覆盖了22个过程域,一般能达到CMMI3级别的基本上可以理解为各类流程、过程规则等已经达到一个较好的水平。当然,这里主要是指企业能够确实按照CMMI模型去实践,这种实践其实更适合于以瀑布式开发为主导的项目开发及产品研发模式。然则,实际上,大部分企业尤其是国内企业并不会严格按照这个模型去做,因为如果每一个过程域都不打折扣地执行地话,需要非常标准化的流程和强大的资源支撑,在这个讲究快速响应变化的时代其实是很难做到的,通常这个时候都会进行相应的裁剪,甚至会结合敏捷迭代等方面的模式,从而逐步形成自己公司的研发管理体系。

1.3. 基于敏捷模式的研发管理
在这个快鱼吃慢鱼的互联网时代,对用户和环境越来越要求要快速响应。敏捷研发是当前不少互联网企业、中小企业推行的研发管理体系,主要理念就是敏捷迭代、小步快跑,快速改进、拥抱变化,用户参与等等。目前这方面也有不少公司除了有相应的敏捷研发体系之外,还有相应的成熟工具做支撑。例如,腾讯的TAPD敏捷研发平台就是其中的代表。通过对用户故事的层级拆分,实现对需求的有效管控和分解,从而确保持续迭代上线。

敏捷研发管理在当前我们以业务为导向、项目为主的情况下,要全面实施尚有较大困难,当然并非是完全不能做,主要是当前所处的环境、所面向的业务、项目开发模式、人员结构等可能较难满足敏捷模式推行的需要。

1.4. 基于IPD的研发管理
之前有简单了解过IPD产品研发管理体系,我认为其中的核心就是“四四四”模型,四四四代表了四大团队、四个流程、四个支撑体系。

四大团队建设包括建立集成产品管理团队(IPMT)、建立产品市场团队(PMT)、建立产品开发团队(PDT)、建立技术开发团队(TDT)。

四大流程建设包括建立产品战略流程、建立需求管理流程、建立产品开发流程、建立技术开发及平台开发流程。

四个支撑体系建设包括建立项目管理体系、建立质量管理体系、建立绩效管理体系、建立成本管理体系。

个人感觉,基于IPD的产品研发管理从整体上来看是一个相对重量级的体系,要落地执行往往需要从整个公司层面去整体考虑和推动。

IPD的理念和敏捷开发理念在本质上是基本一致的,比如以市场需求(用户价值)为核心,将产品开发看成一项投资(商业价值),通过CBB—公共基础模块和跨部门的团队准确、快速、低成本、高质量地推出产品(各评审点的多团队参与和决策、通过各种技术改进提升产品开发效率和降低浪费、持续交付)。

从理论上来讲,IPD研发管理体系是一个较全面的体系,在当前我们的现状下也可能容易出现水土不服的情形,当然其中有一些好的做法是值得借鉴的。

2. 什么样的软件研发管理体系适合我们的发展
从项目及产品的研发角度来看,发展到一定阶段的传统IT企业在研发管理上多数都是基于瀑布型的传统研发模式,由于项目的特点及人员的组织结构等因素,项目开发及产品研发的周期往往较长,较难适应市场快速变化的需要,也较难做到对客户的需求进行快速响应。而大部分的互联网公司及一些大厂,推行了敏捷研发模式,或者是在标准化项目管理和敏捷迭代两者融合上进行了相应的实践。

那么,针对当前我们所面临的一系列问题,究竟什么样的软件研发管理体系在未来一定时期内适合我们的发展?我们需要重构我们的软件研发管理体系吗?我们有必要重构我们的软件研发管理体系吗?带着这些问题,我想主要思考几个方面的问题。

2.1. 能否快速适应未来业务的发展变化
技术是为业务发展而服务的,因此在考虑软件研发管理体系构建时,第一个要考虑的问题就是我们的软件研发管理体系能否快速适应公司未来业务的发展变化。特别是在传统IT业务与互联网新兴业务加速融合的大环境下,信息化能力是越来越多客户的第一选择,因此在业务的快速发展方面需要更加强有力的技术支撑,而这个支撑的背后就是需要我们能够有一套能够快速响应变化、敏捷高效的研发体系,特别是能够有一定的前瞻性并支撑到老业务的快速转型和新业务的拓展。

2.2. 在业务出现较大波动时能否弹性伸缩
另外一个问题就是,业务在发展过程中,受大环境等诸多因素的影响,定然很难一直都是呈现直线上升的发展趋势,这当中必然会有波峰波谷,只不过这个波峰波谷是大是小的问题。而我们面临的问题则是,当出现较大的波峰波谷的时候,我们的研发管理体系应该如何适应?特别是在软件业务处于相对低谷时,既能够继续保持对技术研发的持续投入,又能够在应用开发等方面有一定的可伸缩性,从而正确地处理好软件生产效益问题。这里面可能会涉及到中高层次软件人才的相对稳定和低层次软件人才的灵活流动等问题。特别是在我们业务多样化的背景下,不同业务单元的发展会有不同的发展路径,对软件研发能力的诉求也有所不同,那么这里面首先涉及到的一点就是如何有效平衡基础研发能力和行业研发能力。

对于基础研发能力,个人认为应该是一个软件公司最内在的核心技术能力,往往很多时候基础研发工作很难像做行业应用开发那样立竿见影,但这项工作干得不好往往又容易成为行业研发能力的掣肘,这也是我们当前在人工智能、区块链等新技术潮流背景下总感觉难以发力的原因之一。

对于行业研发能力,个人认为应该要从两个方面去考虑,一个是产品化的能力,其二才是应用开发能力。应用开发能力很好理解,就是目前我们这么多年以来一直在做的各种类型的项目开发,而这里面大部分的项目开发其实都是偏应用层面的开发。而产品化的能力则是最近一两年以来我们重新关注的一个内容,不过这条路上我们尚开始起步,还有很长的路要走,也还有不少坑要踩。个人认为,产品化的能力能否真正发展起来,其中很重要的一点就是要考虑如何与基础研发能力做充分融合。产品化不等同于应用开发,应用开发更多是定制化的开发,是客户导向的软件开发,通常面向的是一个或少数几个的客户;而产品化则是要综合行业、市场、客户群体、新技术等多方面因素的研发,是市场导向的软件开发,面向的是一个或多个的客户群体,甚至面向的是一个市场或跨界市场。

2.3. 新技术研发及成果转化能否跟上业务变化
最近几年,新技术层出不穷,在软件架构的发展方面也非常迅猛,历经了单体架构、垂直架构、SOA架构、微服务架构的演化。从我们公司目前的技术研发实际来看,我们有少量的项目/系统采用了SOA架构,然则大部分的项目/系统仍然采用的是单体架构和垂直架构。单从这一点来看,我们在技术领域的持续跟进及成果转化方面已然有落后趋势,这方面需要我们奋起直追才行。当然,出现如今这种局面固然由众多因素催生而成。比如,已有开发框架前端兼容性的问题最近一两年以来常常被诟病,诚然有它内在的好处,然则最近一两年以来,用户对系统的用户体验要求更高了,不再是单纯地满足于功能实现层面,而是开始追求良好的人机交互和界面展现。因此,这方面势必对新技术的要求更加迫切。最近几年,当不少团队都在往前后端分离走的时候,我们至今的绝大部分软件项目开发仍然停留在前后端分离之前,对不少用户界面展现要求高的软件项目而言,难以快速有效响应变化,同时对一些相对比较成熟的软件产品而言也难以做到接口自动化。

因此,能否在新技术的研发上抓住正确的方向并加快研发成果转化,为业务的快速变化提供强有力的技术支撑,是一个摆在我们面前急需解决的课题。从当今新技术的发展趋势来看,研发架构方面,我们虽说不能完全抛弃传统的单体/垂直架构,但我们必须要往微服务架构方向迈进,除了与最新技术接轨之外,更重要的是如何进行业务解耦,沉淀行业积累,并反向推动人员组织层次的变革,提升软件生产效率,提高软件质量。

除此之外,对于人工智能、区块链等新领域,也是需要综合业务应用场景打造适合我们自身发展的技术+业务融合之路。

2.4. 在标准化和敏捷迭代之间如何平衡
标准化的软件研发道路固然有不少好处,有严谨的流程、规范的体系、固定的套路,当然更多的则是瀑布开发模式,虽然最近几年也陆续有迭代开发的模式,但更多的是被动式响应,而且这种迭代开发模式基本上是大阶段的划分,在每一个大阶段里面依旧是一个典型的瀑布开发模式,即历经需求分析、交互原型设计、UI设计、Web前端开发、程序开发、系统测试、部署实施等步骤,横跨周期往往较长,一旦发生需求变更,变动的代价过高。

敏捷开发强调以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发。在敏捷开发中,软件项目在构建初期被切分成多个子项目,各个子项目的成果都经过测试,具备可视、可集成和可运行使用的特征。换言之,就是把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态。

那么,问题来了,既然标准化项目管理模式下存在太多流水线作业及效率低下等问题,那么我们能够直接转向敏捷迭代模式呢?世界上万事万物都是对立统一的,个人认为不论是标准化项目管理模式还是敏捷迭代项目管理模式都有其擅长的一面。一方面,在现有的以项目为主导的软件开发体系中,标准化模式是我们一直以来的主要做法,也积累了不少经验做法;另一方面,采用敏捷迭代模式对于产品复杂不断有新需求加入等场景是比较适合的。所以这里面更多的是考虑如何更好地平衡标准化项目管理和敏捷迭代两者之间的关系。基本的思路就是结合标准化项目管理和敏捷迭代的优缺点进行适度裁剪,既能提高软件质量和软件开发效率,也能够保留一定的规范性和软件过程文档。例如,针对项目管理,通常是五个过程组:启动、规划、执行、监控、收尾,那么我们其实可以结合实际将规划提前,将监控贯穿于执行过程,这样就势必要求在启动时也要做好项目计划相关工作,在执行过程中抓住关注点并定期监控其执行情况,在收尾阶段做好项目回顾总结。

不论采用何种模式,我们的根本目标就是达到更低的成本实现更快速、更可靠的交付。近年来比较火热的是DevOps。DevOps(Development和Operations的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。它是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

因此,我们的软件研发管理体系中是否应该引入DevOps,进而改善公司组织文化、提高员工的参与感、提高交付效率,我想这也是需要重点关注和考虑的。

2.5. 组织过程资产能否持续积累并盘活
组织过程资产指一个学习型组织在项目操作过程中所积累的无形资产。组织过程资产的累积程度是衡量一个项目组织管理体系成熟度的重要指标,项目组织在实践中形成自己独特的过程资产,构成组织的核心竞争力。

组织过程资产主要包括但不限于以下内容:项目组织在项目管理过程中指定的各种规章制度、指导方针、规范标准、操作程序、工作流程、行为准则和工具方法等。项目组织在项目操作过程中所获得的经验和教训,其中既包括已经形成文字的档案,也包括留在团队成员脑子中没有形成文字的思想。项目组织在项目管理过程中形成的所有文档,包括知识资料库、文档模板、标准化的表格、风险清单等。 项目组织在以往的项目操作过程中留下的历史信息。

经过多年的软件开发,我们做了大大小小形形色色的软件项目和产品,也逐渐积累了一些行业化的软件项目,但总的来看,能够形成规模化效应的软件产品尚较为匮乏,更多的是以定制化开发为主的软件系统,当然也积累了不少项目经验。在这过程中,也积累了不少标准、规范、流程、模板等各类软件过程资源。然而,从目前掌握的情况来看,这些资源是分散的,不够体系化的,还谈不上真正意义上的资产,至少在价值的发挥上还不充分。况且,软件行业这几年的人才流动率明显加快,人员更替的速度以及未能体系化的过程资产积累,加剧了组织过程资产的盘活难度。

那么,构建一个相对健全的、动态的、能够适应未来业务发展的组织过程资产库就显得尤为重要。这既是软件研发管理体系的一个重要组成部分,也是公司层面应该给予充分重视的。在组织过程资产库构建的过程中,其中很重要的一点就是如何让研发知识与经验成为公司的宝贵财产,这里就要充分考虑研发知识管理。知识管理把“隐形知识显性化”,是一项涉及知识库、过程资产、环境和交流等元素的整合过程,所管理的知识将作为一个团组织中过程资产的重要组成部分。对于软件研发而言,我们需要考虑怎么把业务人员和技术人员脑中的蓝图转化为显性知识。

3. 构建我们的软件研发管理体系应包含哪些内容
软件研发管理体系的建设离不开几个关键要素:人员、技术、过程、资源,并在此基础上配以相应的管理手段。进一步来看,要构建适合我们自身发展的软件研发管理体系,需要着重考虑几个能力体系的建设,即:人员组织能力、技术研发能力、过程管理能力和资源建设能力。

前面也有针对“什么样的软件研发管理体系适合我们的发展”进行了一些相对粗浅的探讨,那么在考虑如何构建适合我们发展的软件研发管理体系之前,我想这里首先要明确一下我们期待构建的软件研发管理体系。我们公司的业务涉及众多行业客户,一直以来主要以定制化项目开发为主,同时也涉及运维服务,而在产品研发等方面则处于起步阶段,且在一段时期内项目、产品、服务将会长期并存,因此,个人认为适合我们的软件研发管理体系应该至少经历三个阶段,包括初期的标准化软件研发管理体系、中期的标准化与敏捷相结合的软件研发管理体系和后期的敏捷化软件研发管理体系。

基于上述这样的考虑,正常来讲我们当前应该在标准化的软件研发管理体系中要做进一步强化,而考虑到市场的快速变化、技术的日益进步,个人认为我们当前就需要开始考虑标准化的与敏捷相结合的软件研发管理体系。为什么还需要考虑标准化的软件研发管理体系呢?主要是传统的定制化的软件项目开发依旧占据主体,且目前在这方面仍然有非常大的改进提升空间,然而标准化的模式常常是过于强调标准、规范、流程,开发模式过于线性化,因此需要引入敏捷开发模式。所以,我们又需要考虑敏捷的软件研发管理体系,这主要是为了更好地适应市场变化、更快速地响应客户需求,更好地提升软件开发生产效率。

3.1. 人员组织能力
关于人员组织能力,个人认为有两个关注点:一是团队的发展,二是个体的发展。这两者是相辅相成、互相融合促进的。综合来看,人员组织能力的建设主要包括设立与公司战略、业务、技术发展相适应的组织架构,并配以构建相对完整可行的岗位体系和对应的人员考核体系,同时在团队建设等方面持续改进与提升。

关于组织架构,当前的组织架构虽然解决了一些曾经的主要矛盾,但依然存在不少问题,突出的一点就是核心薄弱,即核心技术能力不强,仍旧需要投入大量的人力到各行业的应用开发中,当然这与我们一直以来承接定制化的软件项目开发不无关系。这是当前乃至未来一定时期需要解决的。

同时,最近几年来的组织架构主要是以职能型组织架构为主,产品线为主导的研发模式尚不成熟,针对项目及产品的团队构建主要是以项目经理来驱动,在项目团队的组成方面固然与互联网的项目团队截然不同。在团队建设方面,需要进一步打通团队之间的壁垒,强化团队的整体协同作战能力。

在岗位体系方面,特别是对人员的绩效评价方面,需要在已有的岗位体系基础上进一步考虑如何更好地执行落地,确保个人绩效目标与团队绩效目标的一致性和顺利达成。

3.2. 技术研发能力
结合我们的实际,我认为在技术研发能力方面要考虑四个方面:一是技术预研,二是技术开发,三是产品开发,四是定制开发。

关于技术预研,通俗来讲就是:预研=预先+研究。这种预先研究通常来源于几个方面,例如来自外部竞争对手的迫使、来自客户或市场的需求、来自公司高层的决策等。为什么要做技术预研呢?这是扫清前行障碍的过程,这为后续展开总体设计、详细设计指明了方向,也是持续积累公司技术能力、保持与新技术同步而不至于脱离轨道的方式之一。

关于技术开发,其实这里主要指与基础平台、公共组件、关键技术等方面的技术研发。另外一个方面来理解,技术开发是技术预研的延续,是在技术预研成果经论证的基础上开展的一系列能促进公司发展、业务发展、技术发展而开展的技术研发工作。

软件产品是指向用户提供的计算机软件、信息系统、套装软件或在提供计算机信息系统集成、应用服务等技术服务时提供的软件,是通用的产品应用于某一行业领域而不是像软件项目一样为某一需求或者单位定制开发。

软件项目主要为特定企业开发或者部署实施一套专用的系统,在进入项目开发之前需要与用户进行具体的交流和讨论,了解用户心中对于软件预期的样子,后经过招投标,签订合同,实施交付。

关于产品开发,这方面我们尚处于起步阶段,尚缺乏一套完整可行的产品研发流程及最佳实践,需要摸着石头过河,也需要长期坚持不懈地努力。

关于定制开发,当前主要是基于客户需求的软件项目定制开发,后续还会包括基于产品衍生出来的定制化开发。前面的这种方式是我们当前最熟悉的模式,主要面临的困境是两个:一是如何实现快速交付,二是如何实现成本可控,从而提升软件项目的利润。

做项目侧重于在最短的时间内,按照客户的需求开发出操作敏捷,用户体验良好的软件。而做产品则侧重于市场驱动,时间相对充足,但要开发出有竞争力,有自身特色,且受客户欢迎的产品,要求功能响应速度快,操作简单,界面美观。

技术预研+技术开发是强化内核的内在需要,定制开发是现阶段的生存根本,产品开发则是为未来发展铺路。

3.3. 过程管理能力
过程管理能力主要包括项目管理、开发管理、质量管理和配置管理等几个方面,需要一套完整合理的流程贯穿整个过程。

在项目管理方面,我们需要梳理当前项目管理体系的标准、规范、流程及相关实践,建立以过程为核心、以度量为基础、以人为本的可裁剪、受认可、能执行的信息集成项目管理体系,进一步规范公司的项目管理,提升项目群管理能力。结合项目管理的五大过程组(启动、计划、执行、监控、收尾),并结合敏捷迭代的思想,形成标准化项目管理与敏捷迭代相结合的具有实际指导意义的方法体系,同时将这套方法体系以指南性文件、规范性文件等形式传导到相关人员,确保可落地执行。此外,为加强过程管控、资源共享、工作协同,组建PMO团队,实现对项目群及重大项目的统一管控与决策支持。

在开发管理方面,一是要落实统一的软件开发规范,包括架构规范、设计规范、UI规范、编码规范、测试规范等。强化设计及开发关键环节的评审,包括对需求、概要设计、详细设计、UI设计等的设计方面的评审,对测试用例等方面的评审,对代码的评审检查(例如利用SonarQube进行代码的自动检查等)及发布评审等。同时通过试点+逐步铺开的方式着力推进CI/CD的落地。

在质量管理方面,进一步强化项目质量审计,逐步改进软件过程生产效能。而在配置方面,则加强对配置项的识别、配置空间的管理、变更控制等,规范软件开发过程,确保构建正确的系统。正确应用软件配置管理是开发高质量软件所不可缺少的。软件配置管理的过程是软件开发过程中质量管理的精髓。

综合来讲,在过程管理方面就是要形成一套适用的软件研发管理流程,并配以相应的节点管控,让不同开发角色之间即各司其职又相互融合促进,从而促进软件开发自组织能力的逐步提升,充分调动软件开发人员的主动性和积极性。

3.4. 资源建设能力
简单来讲,资源建设是软件研发管理体系中的支撑体系。资源建设主要包括了一系列的制度规范、工具、模板、过程资料及交付物(例如项目文档、源代码等),以及相应的经验、知识沉淀等。一是要适时梳理相应的制度、规程、标准、规范、文档模板等,形成标准化资源库;二是要对不同行业历年来的项目资料及源代码分门别类做好规划和归档管理,形成静态库(归档库)和活跃库,同时做好数据安全管理;三是要对软件研发人员及工作中的一些隐性知识转化为显性知识,并逐步构建软件研发的知识图谱,促进知识经验的持续积累与转化,并通过链条式、网状式等方式实现知识分享与传播,形成经验知识库。

系统架构之引言(墨菲定律、康威定律) - 小白进阶 - CSDN博客

$
0
0

系统设计的墨菲定律

  • 任何事都没有表面看起来那么简单
  • 所有的事都会比你预计的时间长;
  • 会出错的事总会出错;
  • 如果你担心某种情况发生,那么它就更有可能发生。

“墨菲定律”的根本内容是“凡是可能出错的事有很大几率会出错”,指的是任何一个事件,只要具有大于零的机率,就不能够假设它不会发生。

系统划分的康威定律

第一定律:组织沟通方式会通过系统设计表达出来

组织的沟通和系统设计之间的紧密联系,解决好人与人的沟通问题,才能有一个好的系统设计。在项目管理《人月神话》一书中有一句“Adding manpower to a late software project makes it later”( 向进度落后的项目中增加人手,只会使进度更加落后),并给出了说明,沟通渠道(成本) = n(n-1)/2,沟通渠道(成本)随着项目或者组织的人员增加呈指数级增长。

  • 5个人的项目组,需要沟通的渠道是 5*(5–1)/2 = 10
  • 15个人的项目组,需要沟通的渠道是15*(15–1)/2 = 105
  • 50个人的项目组,需要沟通的渠道是50*(50–1)/2 = 1,225

沟通的问题,会带来系统设计的问题,进而影响整个系统的开发效率和最终产品结果。

第二定律:时间再多一件事情也不可能做的完美,但总有时间做完一件事情

系统越做越复杂,功能越来越多,但人的智力是有上限的,即使再牛逼的人,融到钱再多也不一定招到足够多合适的人。对于一个巨复杂的系统,我们永远无法考虑周全。Eric Hollnagel 在2009年《Efficiency-Effectiveness Trade Offs》一书中提出“Problem too complicated? Ignore details. Not enough resources?Give up features.”( 问题太复杂了?忽略详细信息。资源不足?放弃特色。)。

对于一个分布式系统,我们几乎永远不可能找到并修复所有的bug,解决方法不是消灭这些问题,而是容忍这些问题,在问题发生时,能自动恢复,即所谓的高可用设计(High Availability),也叫弹性设计(Resilience)。

第三定律:线型系统和线型组织架构间有潜在的异质同态特性

想要什么样的系统,就搭建什么样的团队,可以按职能或者业务进行划分。微服务的理念团队间应该是内聚的,定义好系统的边界和接口,在一个团队内全栈,让团队自治,原因就是因为如果团队按照这样的方式组建,将沟通的成本维持在系统内部,每个子系统就会更加内聚,彼此的依赖耦合能变弱,跨系统的沟通成本也就能降低。

第四定律:大的系统组织总是比小系统更倾向于分解

人与人的沟通非常复杂,当我们面对复杂系统时,又只能通过增加人力来解决,可以通过分而治之来解决 一个大的组织因为沟通成本或管理问题,总为被拆分成一个个小团队。

总结:

  • 人与人的沟通是非常复杂的,一个人的沟通精力是有限的,所以当问题太复杂需要很多人解决的时候,我们需要做拆分组织来达成对沟通效率的管理。
  • 组织内人与人的沟通方式决定了他们参与的系统设计,管理者可以通过不同的拆分方式带来不同的团队间沟通方式,从而影响系统设计
  • 如果子系统是内聚的,和外部的沟通边界是明确的,能降低沟通成本,对应的设计也会更合理高效
  • 复杂的系统需要通过容错弹性的方式持续优化,不要指望一个大而全的设计或架构,好的架构和设计都是慢慢迭代出来的。

拆分原则

  • 应该按照业务闭环进行系统拆分或组织架构划分,实现闭环、高内聚、低耦合,减少沟通成本。
  • 如果沟通出现问题,那么应该考虑进行系统和组织架构的调整。
  • 在合适时机进行系统拆分,不要一开始就把系统或服务拆的非常细,虽然闭环,但是每个人维护的系统多,维护成本高。

基于datax的数据同步平台 - 黄小雪 - 博客园

$
0
0

一、需求

         由于公司各个部门对业务数据的需求,比如进行数据分析、报表展示等等,且公司没有相应的系统、数据仓库满足这些需求,最原始的办法就是把数据提取出来生成excel表发给各个部门,这个功能已经由脚本转成了平台,交给了DBA使用,而有些数据分析部门,则需要运维把生产库的数据同步到他们自己的库,并且需要对数据进行脱敏,比如客户的身份证号、手机号等等,且数据来源分散在不同的机器,不同的数据库实例里,这样就无法使用MySQL的多源复制,只能用写脚本通过SQL语句实现,随着业务的发展,导致堆积到运维部门的同步数据任务越来越多,一个任务对应一个脚本,有的脚本多达20多张表,脚本超过10个以后,每次同步失败、或者对脚本里的参数进行增删改查,都要从10多个脚本里的10多个SQL去找,这是一件非常痛苦的事情,耗费时间、没有效率,且容易改错,是一件吃力不讨好的事。为此开发了一个数据同步平台,将同步任务的增删改查、执行的历史日志全部放到平台里,然后交给DBA去自己去操作。

         市面上也有一些ETL工具,比如kettle,但是为了练手决定重新造轮子。

二、平台简介

          平台主要用于数据同步、数据处理等等ETL操作。

          平台基于阿里的开源同步工具datax3.0开发。

          开发语言:Python、Django、celery、bootstrap、jquery

          系统:Centos 7  64位

          注意:时间紧迫,平台只支持MySQL数据库,其它的sqlserver等等后期再开发。

          datax3.0 介绍: https://yq.aliyun.com/articles/59373

          datax3.0 github 地址: https://github.com/alibaba/DataX

          项目地址: https://github.com/hanson007/FirstBlood

三、功能模块

         1、数据同步

               主要用于数据同步

         2、SQL脚本(后期开发,包括备份模块等等。)

               保存并执行各种增删改查SQL语句。

         3、批处理作业

               将数据同步、SQL脚本等等各个模块的子任务组合成一个批处理作业。借鉴了数据库客户端工具Navicat Premium 的批处理作业功能。

               支持作业定时调度。

         4、数据库管理工具(web界面后期开发)

              主要用于管理生产数据库的IP、用户名、密码等等信息,供其它模块调用。

              目前模块的表已建好,生产库的信息需要通过其它平台同步或者用数据库客户端工具导入,web界面的增删改查后期开发。目前生产环境里是将其它平台保存的所有生产库IP、用户名、密码等等信息同步到此平台里。

         5、接口

               提供查询批处理作业执行历史的接口,供其它部门使用。(主要还是大数据部门,他们写了一个程序,根据我这边每次同步后的结果,是成功还是失败,再进行下一步的操作。)

               后续接口按业务部门的需求再开发。

         6、权限(Django自带)

               平台管理员账号拥有模块的所有权限,仅供运维部门使用。

               普通人员账号只能查看数据同步、批处理作业,以及执行历史,不能新增、修改、执行作业或任务。主要提供给业务部门使用。

               查看批处理作业的执行历史接口没有权限控制,普通人员也能调用。

四、表结构设计

          1、生产数据库信息

                功能:主要用于保存各种生产库的 ip、用户名、密码等等信息。

                表名:databaseinfo

           

名称类型约束条件说明
idint不允许为空自增主键
namevarchar不允许为空、不允许重复生产库英文标识。
descriptionvarchar不允许为空生产库的业务信息描述
hostvarchar不允许为空、不允许重复生产库的IP地址。
uservarchar不允许为空生产数据库的用户名
passwdvarchar不允许为空生产数据库的密码
dbvarchar不允许为空生产数据库中的某一个库
typevarchar不允许为空生产数据库类型。 比如MySQL、sqlserver
create_timedatetime不允许为空创建时间,默认为当前时间
modify_timedatetime不允许为空修改时间,默认为当前时间,数据变化时自动改为当前时间。

          2.数据库同步任务

            功能:用于保存数据库同步任务的各种参数,主要为datax的json配置文件里的各种参数。

            表名:datax_job

名称类型约束条件说明
idint不允许为空自增主键
namevarchar不允许为空,不允许重复数据同步任务的英文标识
descriptionvarchar不允许为空任务的详细描述
querySqllongtext不允许为空提取数据时的查询SQL
reader_databaseinfo_idint不允许为空读取数据库(从哪个生产库执行SQL提取数据,对应databaseinfo表的主键)
writer_tablevarchar不允许为空写入表名(提取的数据插入到哪张表里)
writer_databaseinfo_idint不允许为空写入数据库(提数据的数据插入到哪个数据库里)
writer_preSqllongtext允许为空写入前执行的SQL(比如同步数据前需要清空写入的表)
writer_postSqllongtext允许为空写入后执行的SQL(比如同步完数据后需要再结合其它表执行数据分析)
create_timedatetime不允许为空创建时间,默认为当前时间
modify-timedatetime不允许为空修改时间,默认为当前时间,数据变化时自动改为当前时间。

        3.写入表的列信息

          功能:保存同步任务时写入到表的哪些列。比如写入表有20个字段,此时只需要往其中的10个字段写入信息,就需要保存这10个列名。

                     注意:* 星号代码写入到表的所有字段。

          表名:datax_job_writer_column

名称类型约束条件说明
idint不允许为空自增主键
namevarchar不允许为空列名
datax_job_idint不允许为空数据同步任务ID,关联datax_job表的主键。
create_timedatetime不允许为空创建时间,默认为当前时间
modify_timedatetime不允许为空修改时间,默认为当前时间,随着数据的变化而变为当前时间。

       4.数据同步任务实例

          功能:用于保存数据同步任务的执行历史。

                    方便自己及业务部门进行任务的分析和排错,省的每次同步失败后还得帮他们查日志。现在直接将日志记录表里,在平台开个账号后,让业务部门自己去查。

                    每一个数据同步任务执行后,可以看成是一个实例,类似面向对象里实例化。将任务的执行时间、执行结果等等保存起来。借鉴了腾讯蓝鲸的作业平台表结构设计思想。(麻花藤啊麻花藤,给你冲了几十年的点卡,终于是回了一点点利息。)

          表名:datax_job_instance

          说明:instance_id也对应datax生成的日志文件名,当需要在页面查看datax生成的日志时就通过instance_id去查找日志文件,并将其实时输出到页面。

名称类型约束条件说明 
idint不允许为空 自增主键 
instance_id bigint任务实例ID ,不允许重复任务实例ID(由datax_job的id号+13位时间戳组成)
namevarchar不允许为空任务名称 (执行时,datax_job表的name,同下面的字段一样) 
descriptionvarchar不允许为空任务描述
querySqllongtext不允许为空 查询SQL语句
reader_databaseinfo_hostvarchar不允许为空读取数据库IP
reader_databaseinfo_descriptionvarchar不允许为空读取数据库描述
writer_tablevarchar不允许为空写入表
writer_databaseinfo_hostvarchar不允许为空写入数据库IP
writer_databaseinfo_descriptionvarchar不允许为空写入数据库描述
writer_preSqllongtext允许为空写入数据前执行的SQL语句
writer_postSqllongtext允许为空写入数据后执行的SQL语句
trigger_modeint不允许为空触发模式 1 自动 2 手动(默认自动)
statusint不允许为空状态 0 正在执行 1 执行完成
resultint不允许为空执行结果 0 成功 1 失败 2 未知
start_timedatetime不允许为空开始时间
end_timedatetime允许为空结束时间

        5.批处理作业

         功能:保存批处理作业。

         表名:batch_job

名称类型约束条件说明
idint不允许为空自增主键
namevarchar不允许为空,不允许重复名称
descriptionvarchar不允许为空描述
create_timedatetime不允许为空创建时间
modify_timedatetime不允许为空修改时间

        6.批处理作业详情

         功能:保存批处理作业的各个子任务。

                    比如一个批处理作业包含8个数据同步任务,一个SQL脚本任务,则将这几个任务的id保存起来。

         表名:batch_job_details

         说明:字段subjob_id,对应其它子任务的ID。比如,类型为数据同步,则对应datax_job表的主键。类型为SQL脚本,则对应SQL脚本表的主键。(SQL脚本后期开发)

名称类型约束条件说明
idint不允许为空自增主键
batch_job_idint不允许为空批处理作业ID,对应batch_job表的主键
subjob_idint不允许为空子作业ID,对应其它子任务的主键。
typeint不允许为空类型 1 数据同步 2 SQL脚本 3 备份。 主要用于后期扩展
create_timedatetime不允许为空创建时间
modify_timedatetime不允许为空修改时间

        7.批处理作业执行实例

        功能:保存批处理作业的执行历史日志。功能同数据同步实例一样。

        表名:batch_job_instance

名称类型约束条件说明
idint不允许为空自增主键
instance_idbigint不允许为空、不允许重复实例ID(由batch_job表的id号+13位时间戳组成)
namevarchar不允许为空名称
descriptionvarchar不允许为空描述
trigger_modeint不允许为空触发模式 1 自动 2 手动(默认自动)
statusint不允许为空状态 0 正在执行 1 执行完成
resultint不允许为空执行结果 0 成功 1 失败 2 未知
start_timedatetime不允许为空开始时间
end_timedatetime不允许为空结束时间

         8.批处理作业执行实例详情

         功能:保存批处理作业执行实例的各个子任务实例

         表名:batch_job_instance_details

         说明:每个批处理作业执行时,实际是执行各个其它功能模块的子任务,而每个子任务都会保存子任务实例ID。

                   比如一个批处理作业有8个数据同步任务,1个备份任务(后期开发),执行后,datax_job_instance表会保存这8个数据同步任务的实例,备份实例表则保存备份实例ID。然后再将8个同步任务实例的ID及1个备份实例ID保存到batch_job_instance_details表里,查询时只要通过各个子任务的实例ID关联查询。

名称类型约束条件说明
idint不允许为空自增主键
instance_idbigint不允许为空实例ID,对应batch_job_instance表的instance_id
subjob_instance_idbigint不允许为空子作业实例ID,比如datax_job_instance表的instance_id
typeint不允许为空类型 1 数据同步 2 SQL脚本 3 备份。 主要用于后期扩展

      9.建表语句

          

 

  

 

五、功能详解

2、数据同步

            功能:底层使用阿里的datax3.0工具进行同步。可以新增、修改同步任务。每个任务对应一张表。在页面添加任务后,执行时就在后台生成基于datax3.0的json配置文件。并且可以实时查看datax生成的同步日志,也可以查看任务的执行历史。

                 衍生:增量同步

                                 需要源表里增加时间戳字段,两种方案。

                              (1)如果历史数据不变,每次只同步前一天的数据。

                              (2)如果历史数据变化,需要在目标库里加一张临时表,每次同步时将前一天或前一个小时的时间戳有变化的数据插入到临时表里。再将临时表里的数据更新或插入到目标表里。

            操作

                (1)首页           

                         点击“数据同步->作业”,进入数据同步首页,可以查看所有的数据同步任务

 

 

               (2)新增同步任务

                        点击首页的“新增”按钮,进入新增任务页面,填完表单后点击保存。

 

(3)     更新、运行同步任务

              在数据同步首页点击“任务名称”,进入任务更新页面。可以对任务的SQL、数据库等等信息进行修改。

(1)     执行任务

              在更新页面点击“Run”按钮,可以执行任务。

(1)执行历史

点击“数据同步->执行历史”,在执行历史首页可以查看数据同步任务的执行历史,并且可以按照任务名称、描述、读取数据库、执行状态等等进行搜索。

衍生:由于执行历史是一个日志记录,随着时间推移,数据量会越来越多,为了减小平台数据库的压力,按照业务量大小可以只保存一年、或者半年的数据。

(1)同步日志

在执行历史首页点击“任务名称”,可以实时查看同步日志。

日志是由工具datax生成的日志文件,文件名为执行时任务的ID号+13位时间戳组成。平台只保存文件名,查看日志时,后台通过文件名将日志文件内容实时输出到页面。

2.批处理作业

   功能描述:

         将数据同步、SQL脚本(3.0版本后期开发)等等子任务组合成一个批处理作业,并发执行。并且支持linux crontab格式的定时执行。

         时间紧迫,暂时不支持任务串行,或者任务之间的依赖,比如A执行完成,并且成功后才能执行B,类似功能后期3.0版本开发。

  操作

(1)批处理作业首页

          点击“批处理作业->作业列表”,进入批处理作业首页

(2)新增批处理作业

点击“新增”按钮,进入新增批处理作业页面。

选择“执行时间”、勾选“是否启用”等等参数,填好表单后点击保存。后台会根据执行时间自动执行。

(3)更新、运行批处理作业

在批处理作业首页点击“任务名称”后,进入更新页面,可以修改批处理作业参数。

点击“Save”按钮,保存更新后的批处理作业。

在更新页面点击“Run”按钮可手动执行批处理作业。

(4)执行历史

点击“批处理作业->执行历史”,即可进入批处理作业 - 执行历史首页。

可以按照任务名称、执行结果等等搜索历史的执行作业。

点击“任务名称”进入批处理作业详情 - 执行历史,可查看批处理作业执行时它的子任务。

(5)执行日志

在“批处理作业详情 - 执行历史”页面,点击“任务名称”可查看每个子任务的日志。如类型为数据同步的子任务,它的日志就是调的datax的日志文件内容。

数据仓库系列之数据质量管理 - 黄昏前黎明后 - 博客园

$
0
0

数据质量一直是数据仓库领域一个比较令人头疼的问题,因为数据仓库上层对接很多业务系统,业务系统的脏数据,业务系统变更,都会直接影响数据仓库的数据质量。因此数据仓库的数据质量建设是一些公司的重点工作。

   一、数据质量

  数据质量的高低代表了该数据满足数据消费者期望的程度,这种程度基于他们对数据的使用预期。数据质量必须是可测量的,把测量的结果转化为可以理解的和可重复的数字,使我们能够在不同对象之间和跨越不同时间进行比较。 数据质量管理是通过计划、实施和控制活动,运用质量管理技术度量、评估、改进和保证数据的恰当使用。

   二、数据质量维度

  1、准确性:数据不正确或描述对象过期

  2、合规性:数据是否以非标准格式存储

  3、完备性:数据不存在

  4、及时性:关键数据是否能够及时传递到目标位置

  5、一致性:数据冲突

  6、重复性:记录了重复数据

 

   三、数据质量分析

  数据质量分析的主要任务就是检查数据中是否存在脏数据,脏数据一般是指不符合要求以及不能直接进行相关分析的数据。脏数据包括以下内容:

  1、缺省值

  2、异常值

  3、不一致的值

  4、重复数据以及含有特殊符号(如#、¥、*)的数据

   我们已经知道了脏数据有4个方面的内容,接下来我们逐一来看这些数据的产生原因,影响以及解决办法。

  第一、   缺省值分析

  产生原因:

  1、有些信息暂时无法获取,或者获取信息的代价太大

  2、有些信息是被遗漏的,人为或者信息采集机器故障

  3、属性值不存在,比如一个未婚者配偶的姓名、一个儿童的固定收入

  影响:

  1、会丢失大量的有用信息

  2、数据额挖掘模型表现出的不确定性更加显著,模型中蕴含的规律更加难以把握

  3、包含空值的数据回事建模过程陷入混乱,导致不可靠输出

        解决办法:

          通过简单的统计分析,可以得到含有缺失值的属性个数,以及每个属性的未缺失数、缺失数和缺失率。删除含有缺失值的记录、对可能值进行插补和不处理三种情况。

 

  第二、   异常值分析

  产生原因:业务系统检查不充分,导致异常数据输入数据库

  影响:不对异常值进行处理会导致整个分析过程的结果出现很大偏差

  解决办法:可以先对变量做一个描述性统计,进而查看哪些数据是不合理的。最常用的统计量是最大值和最小值,用力啊判断这个变量是否超出了合理的范围。如果数据是符合正态分布,在原则下,异常值被定义为一组测定值中与平均值的偏差超过3倍标准差的值,如果不符合正态分布,也可以用原理平均值的多少倍标准差来描述。

  第三、   不一致值分析

  产生原因:不一致的数据产生主要发生在数据集成过程中,这可能是由于被挖掘的数据是来自不同的数据源、对于重复性存放的数据未能进行一致性更新造成。例如,两张表中都存储了用户的电话号码,但在用户的号码发生改变时只更新了一张表中的数据,那么两张表中就有了不一致的数据。

  影响:直接对不一致的数据进行数据挖掘,可能会产生与实际相悖的数据挖掘结果。

  解决办法:注意数据抽取的规则,对于业务系统数据变动的控制应该保证数据仓库中数据抽取最新数据

  第四、   重复数据及特殊数据产生原因:

  产生原因:业务系统中未进行检查,用户在录入数据时多次保存。或者因为年度数据清理导致。特殊字符主要在输入时携带进入数据库系统。

  影响:统计结果不准确,造成数据仓库中无法统计数据

  解决办法:在ETL过程中过滤这一部分数据,特殊数据进行数据转换。

   四、数据质量管理

     大多数企业都没有一个很好的数据质量管理的机制,因为他们不理解其数据的价值,并且他们不认为数据是一个组织的资产,而把数据看作创建它的部门领域内的东西。缺乏数据质量管理将导致脏数据、冗余数据、不一致数据、无法整合、性能底下、可用性差、责任缺失、使用系统用户日益不满意IT的性能。

  在做数据分析之前一般都应该初步对数据进行评估。初步数据评估通过数据报告来完成的,数据报告通常在准备把数据存入数据仓库是做一次,它是全面跨数据集的,它描述了数据结构、内容、规则、和关系的概况。通过应用统计方法返回一组关于数据的标准特征,包括数据类型、字段长度、列基数、粒度、值、格式、模式、规则、跨列和跨表的数据关系,以及这些关系的基数。初步评估报告的目的是获得对数据和环境的了解,并对数据的状况进行描述。数据报告应该如下:

编号

数据质量维度

检查对象

检查项

检查项说明

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

完备性

数据处理

经过一个流程的数据集的完备性—— 数额字段的平衡

整个过程中的数额字段内容平衡,用于完全平衡的情况

 

  五、总结

​        数据报告中列出了很多的检查项都是围绕数据质量管理相关的检查,所以做一个数据分析项目前一定要知道客户的数据质量情况。如果数据质量很糟糕,最终影响的是项目分析的实际效果。例如,用户业务系统中客户信息只输入了客户名称,要分析客户类型就会存在缺省值。当然有一些维度属性我们可以通过事实表反算数据进入维度表来补充维度属性。个人建议在数据分析项目中一定要对维度属性进行评估,在项目处理前利用简单的模型告诉客户能够出具的效果。

初探Electron,从入门到实践 - 葡萄城技术团队博客 - OSCHINA

$
0
0


在开始之前,我想您一定会有这样的困惑:标题里的Electron 是什么?Electron能做什么?许多伟大的公司使用Electron框架的原因又是什么?

带着这些问题和疑惑,通过本文的介绍,可助您全面地认识Electron这门新兴的技术,迅速找到其入门途径,并理解Electron为何被称为当下开发桌面App的最佳选择。

 

初探Electron
一、Electron是什么?(为何称之为“跨平台桌面浏览器”)

前端开发的魅力,在于开发者随时要面临全新技术的挑战!

曾几何时,作为前端开发者的你可曾想过:如何利用HTML、CSS和JavaScript构建跨平台的桌面应用程序?借助 Electron,这项工作将比你想象的更加简单。

Electron作为一个使用新兴技术(包括JavaScript,HTML和CSS)创建桌面应用程序的框架,其负责处理硬件,开发者可以更专注于应用程序的核心并从底层更改其设计。

 

Electron设计之初便充分结合了当今最好的Web技术,作为一个跨平台的“集成框架”,它可以轻松地与Mac、Windows和Linux兼容。而所谓的“集成框架”也就是它将“Chromium”和“Node.js”很好的集成在了一起,并明确分工,Electron负责硬件部分,“Chromium”和“Node.js”负责界面与逻辑,大家井井有条,共同构成了一个成本低廉却十分高效的解决方案,在快速交付上甚至比Native还要快速。

 

Electron发展里程碑

·      2013年4月11日,Electron以Atom Shell为名起步。

·      2014年5月6日,Atom以及Atom Shell以MIT许可证开源。

·      2015年4月17日,Atom Shell改名为Electron。

·      2016年5月11日,1.0版本发布。

·      2016年5月20日,允许向Mac应用商店提交软件包。

·      2016年8月2日,支持Windows商店。

 

简而言之,Electron JS是一个运行时框架,它允许用户使用HTML5、CSS和JavaScript创建桌面套件应用程序,而大部分应用程序都是由两种非常受欢迎的技术混合而成:Node.js和Chromium。因此,您编写的任何Web应用程序都可以在Electron JS 上正常运行。

 

Electron的内置功能包括:

·      自动更新 - 使应用程序能够自动更新、升级

·      本机菜单和通知 - 创建本机应用程序菜单和上下文菜单

·      应用程序崩溃报告 - 您可以将崩溃报告提交给远程服务器

·      调试和分析 - Chromium的内容模块可以发现性能瓶颈和运行缓慢的原因。此外,您也可以在应用中使用自己喜欢的Chrome开发者工具

·      Windows安装程序 -您可以快速而简单创建安装包


二、Electron 可以用来做什么?(哪些场景需要使用Electron)

以Windows平台应用开发为例,大部分人首先会想到使用成熟的开发方案,如QT(C++)、WPF(C#) 等。但面临以下几种使用场景,这些方案将显得捉襟见肘:

·      公司要设计一个全新的APP, 但技术人员大部分由前端开发构成

·      公司原本就有在线的Web应用,但是想让该应用能够在桌面端直接打开(离线状态下也可使用),并增加一些与系统交互的功能

 

以我的亲身经历为例:

在SpreadJS项目中,我们需要将基于web版的表格编辑器封装成APP使用,同时增加文件操作的能力,如导入导出excel、导入PDF等,而SpreadJS是一个纯前端的表格控件,开发人员全部由前端开发组成,对C++和C#并不熟悉,如果投入过大的时间精力用来学习其他开发语言,整个项目的技术管理和项目管理将变得无法控制。除此之外,鉴于项目本身对应用的业务逻辑要求并不高,只是套一个具有浏览器属性的运行环境即可,因此,单独为此配置C++、C# 开发人员将无形中提升更多项目成本。

 

为此,我们引入了Electron框架:现有的前端开发人员能在不学习其他语言的情况下,直接搞定上述需求,这就是Electron 为我们带来的价值。


三、为什么选择 Electron?(Electron的出现为前端开发者谋得了一份好差事)

可以这么说,Electron这个框架让网路里流传很广的一句话不再是玩笑:“不要和老夫说什么C++、Java,老夫行走江湖就一把JS,遇到需求撸起袖子就是干”。Electron可以帮助前端开发者在不需要学习其他语言和技能的情况下,快速开发跨平台桌面应用。

 

Electron的出现将蚕食很大一部分桌面客户端领域的市场份额,鉴于它的跨平台特性,在不同系统之间仅需少量的优化工作。可想而知,这个成本到底有多低。

在开发的体验上,Electron是基于"Chromium"和"Node.js"的,所以几乎所有的Node.js模块都可以在Electron上运行,并很容易使用“npm”搭积木的方式快速交付一个产品。

 

四、大型应用使用Electron框架的成功案例

 
1. SpreadJS纯前端表格控件
 
SpreadJS 是一款基于 HTML5 的纯前端电子表格控件,以“高速低耗、高度类似Excel、可无限扩展”为产品特色,提供移动跨平台和浏览器支持,同时满足 .NET、Java、App 等应用程序中的 Web Excel 组件开发、数据填报、在线文档、图表公式联动、类 Excel UI 设计等业务场景,在数据可视化、Excel 导入导出、公式引用、数据绑定、框架集成等场景下无需大量代码开发和测试,极大降低了企业研发成本和项目交付风险。
 

2. WebTorrent

WebTorrent,作为第一个在浏览器中运行的torrent客户端,是一个完全由JavaScript编写并使用WebRTC进行点对点传输的客户端应用。无需任何插件,扩展或安装,WebTorrent将用户链接到分散的浏览器到浏览器网络,以确保有效的文件传输。

WebTorrent使用Electron框架开发,使其尽可能轻量、无广告且开源。此外,使用Electron还有助于流式传输,并充当混合客户端,将应用程序连接到所有流行BitTorrent和WebTorrent网络。

 

3. WordPress

WordPress 桌面是一个使用了Electron和React作为框架的桌面应用程序,提供无缝的跨平台体验,允许用户专注于他们的内容和设计,而不会被任何浏览器标签所分心。

 

4. Slack

Slack采用了Electron框架构建,鉴于其高性能表现和无框架外观,将带来与浏览器完全不同的体验方式。对于寻求更集中的工作空间的团队来说,Slack Desktop绝对是最适合的应用程序之一。

虽然Slack Desktop融合了很多技术,但大多数资源文件和代码都是远程加载的,它们结合了Chromium的渲染引擎和Node.js运行时和模块系统。

 

5. WhatsApp

WhatsApp作为下载量最高的Messenger应用程序,也是基于Electron框架构建的。Electron帮助WhatsApp开发人员以低廉的成本完成了几乎所有工作,并通过更加简化和创新的技术,为用户带来全新的桌面体验方式。


Electron 架构实现
 

Electron基本文件结构

Electron有一个基本的文件结构,类似于我们在创建网页时使用的文件结构:

electron-quick-start

- index.html 这是一个HTML5网页,目的用于提供画布(canvas)

- main.js 创建窗口并处理系统事件

- package.json 是我们应用程序的启动脚本。它将在主进程中运行,并包含有关应用程序的所有信息

- render.js 处理应用程序的渲染过程

 

Electron的架构主要分为两部分:主进程和渲染进程

回顾以往的web开发,我们的代码,无论是HTML、CSS还是Javascript,都是运行在浏览器沙盒中的,我们无法越过浏览器的权限访问系统本身的资源,代码的能力被限制在了浏览器中。浏览器之所以这么做,是为了安全的考虑。设想一下,我们在使用浏览器的时候,会打开各式各样不同来源的网站,如果JavaScript代码有能力访问并操作本地操作系统的资源,那将是多么可怕的事情。

 

假设:你在某天不小心打开了一个恶意的网站,可能你存储在硬盘上的文件就被偷走了(都用不着去修电脑)。

 

但我们要开发的是桌面应用程序,如果无法访问到本地的资源肯定是不行的。Electron将nodejs巧妙的融合了进来,让nodejs作为整个程序的管家。管家拥有较高的权限,可以访问和操作本地资源,使用原本在浏览器中不提供的高级API。同时管家也管理着渲染进程窗口的创建和销毁。所以,我们将这个管家称之为主进程。在使用Electron开发的程序中,会使用main.js作为程序的主入口,该文件内代码执行的内容,就是主进程中执行的内容。

 

主进程

主进程控制应用程序的生命周期。Electron 用来运行 package.json 的 main 脚本的进程被称为主进程。 在主进程中运行的脚本通过创建web页面来展示用户界面。它内置了完整的Node.js API,主要用于打开对话框以及创建渲染进程。此外,主进程还负责处理与其他操作系统交互、启动和退出应用程序。

 

主进程就像是应用程序的管家,负责管理整个应用程序的生命周期以及所有渲染进程的创建。

按照惯例,主进程位于名为main.js的文件中,你可以通过在package.json文件中修改配置属性来更改主进程文件。

比如,我们可以打开package.json并更改配置属性:

1

“main”: “main.js”, =》“main”: “mainTest.js”,

 

请注意,Electron有且只有一个主进程。且主进程销毁时,所有渲染进程也将一并销毁。在chrome浏览器的默认策略下,每一个tab都是独立的进程,Electron也正是利用了这一策略。

 

渲染进程

渲染进程是应用程序中的浏览器窗口。与主进程不同,Electron可以有许多渲染进程,且每个进程都是独立的。由于 Electron 使用了 Chromium 来展示web 页面,所以 Chromium 的多进程架构也被使用到。 每个Electron中的 web 页面运行在它自己的渲染进程中。

正是因为每个渲染进程都是独立的,因此一个崩溃不会影响另外一个,这些要归功于Chromium的多进程架构。

 

如何保持进程通信?

 

即便Electron中的所有进程同时存在并保持独立运行,但他们仍然需要以某种方式进行沟通,尤其是在他们负责不同任务的时候。file:///C:/Users/markxu/AppData/Local/Temp/msohtmlclip1/01/clip_image034.png

为了保持进程通信,Electron有一个进程间通信系统(IPC也就是内部进程通信)。您可以使用IPC在主进程和渲染进程之间传递信息。

1

2

3

4

5

6

7

8

// 在主进程中

global.sharedObject = {

someProperty: 'default value'

}

// 在第一个页面中

require('electron').remote.getGlobal('sharedObject').someProperty= 'new value'Copy

// 在第二个页面中

console.log(require('electron').remote.getGlobal('sharedObject').someProperty)

 

Electron 进程通信的实现方式:

·      主进程使用 BrowserWindow 实例创建页面。每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。

·      主进程管理所有的web页面和它们对应的渲染进程。 每个渲染进程都是独立的,它只关心它所运行的 web页面。

·      在页面中调用与 GUI 相关的原生 API 是不被允许的,因为在 web 页面里操作原生的GUI 资源是非常危险的,而且容易造成资源泄露。 如果你想在 web 页面里使用 GUI 操作,其对应的渲染进程必须与主进程进行通讯,请求主进程进行相关的 GUI 操作。

说句题外话:在两个网页(渲染进程)间共享数据最简单的方法是使用浏览器中已经实现的 HTML5 API。 其中比较好的方案是用 Storage API, localStorage,sessionStorage 或者 IndexedDB,但这些不是今天的主题。

 

如何构建 Electron系统架构?

为了降低构建整个 Chromium 带来的复杂度,Electron通过libchromiumcontent 来访问 Chromium 的Content API。libchromiumcontent 是一个独立的、引入了 Chromium Content 模块及其所有依赖的共享库。用户不需要一个强劲的机器来构建Electron。

Electron只用了Chromium的渲染库而不是其全部组件。这使得升Chromium更加容易,但也意味着Electron缺少了Google Chrome里的一些浏览器相关的特性。

 

打包

原来打包步骤略微繁琐,如今由于社区发展,产生了很多优秀的打包工具,让我们可以不用关注很多细节,(比如asar)

1

2

3

4

5

6

7

8

// 在主进程中

global.sharedObject = {

someProperty: 'default value'

}

// 在第一个页面中

require('electron').remote.getGlobal('sharedObject').someProperty= 'new value'Copy

// 在第二个页面中

console.log(require('electron').remote.getGlobal('sharedObject').someProperty)

 

main 端

1

2

3

4

ipcMain.on('readFile', (event, { filePath })=> {

content content = fs.readFileSync(filePath,'utf-8');

event.sender.send('readFileSuccess', { content});

});

  

renderer 端

1

2

3

4

5

6

ipcRenderer.on('readFileSuccess', (event, {content }) => {

console.log(`content: ${content}`);

});

ipcRender.send('readFile', {

filePath: '/path/to/file',

});

 

我们仅需做的 :将app 的目录结构整理好,提供对应的资源,如icon等,然后使用工具制作镜像即可将资源打包成为各个平台下的APP应用。

 

打包工具的选择

file:///C:/Users/markxu/AppData/Local/Temp/msohtmlclip1/01/clip_image036.jpg

通常情况下,我们选择Electron-builder (跨平台支持性较好,上手成本低)

 

Electron 快速上手实践  

这里我准备了一个Demo项目,这个Demo源码您可以在 葡萄城技术社区获取到。

这个演示我将以SpreadJS的一个应用为例,展示如何将Web应用转换为Electron桌面应用。

我这里使用electron-builder进行项目文件的打包,您可以直接在项目根目录通过 npx electron-builder命令执行打包命令。

项目打包过程可能需要些时间,在这期间,我向您介绍一下Electron 打包的配置文件,您可以根据您的实际情况配置如下文件以满足您的需求

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

"build": {

"appId""your.id"// appid

"productName""程序名称",// 程序名称

"files": [ // 打包需要的不过滤的文件

"build/**/*",

"main.js",

"node_modules/**/*"

],

"directories": {

"output""./dist-out"// 打包输出的目录

"app""./"// package所在路径

"buildResources""assets"

},

"nsis": {

"oneClick"false// 是否需要点击安装,自动更新需要关掉

"allowToChangeInstallationDirectory":true//是否能够选择安装路径

"perMachine"true  // 是否需要辅助安装页面

},

"win": {

"target": [

{

"target""nsis"// 输出目录的方式

"arch": [ // 输出的配置ia32或者x64/x86

"x64"

}

],

"publish": [ // 自动更新的配置

{

"provider""generic"// 自己配置更新的服务器要选generic

"url":"http://127.0.0.1:8080/updata/"  //更新配置的路径

}

}

}  

 

 缓慢的打包进程结束后,您应该可以在项目目录中的build目录看到生成的exe文件

点击安装,它就像一个普通的桌面应用程序一样开始了安装进程。(这里的软件名称和软件logo都是我们项目中配置好的)

 

安装完成后,打开程序,这里我们可以看到打包好的应用和在Web端访问时的效果别无二致,同时也能够像其他桌面应用程序一样,支持离线使用。

 

 

至此,初探Electron,从入门到实践教程结束,如果大家还有更多使用上的疑惑或想要了解更多高级用法,可以通过官方文档学习 https://electronjs.org/docs


注册中心 Consul 使用详解 - 纯洁的微笑博客

$
0
0

在上个月我们知道 Eureka 2.X 遇到困难停止开发了,但其实对国内的用户影响甚小,一方面国内大都使用的是 Eureka 1.X 系列,另一方面 Spring Cloud 支持很多服务发现的软件,Eureka 只是其中之一,下面是 Spring Cloud 支持的服务发现软件以及特性对比:

FeatureeuerkaConsulzookeeperetcd
服务健康检查可配支持服务状态,内存,硬盘等(弱)长连接,keepalive连接心跳
多数据中心支持
kv 存储服务支持支持支持
一致性raftpaxosraft
capapcpcpcp
使用接口(多语言能力)http(sidecar)支持 http 和 dns客户端http/grpc
watch 支持支持 long polling/大部分增量全量/支持long polling支持支持 long polling
自身监控metricsmetricsmetrics
安全acl /httpsaclhttps 支持(弱)
spring cloud 集成已支持已支持已支持已支持

在以上服务发现的软件中,Euerka 和 Consul 使用最为广泛。如果大家对注册中心的概念和 Euerka 不太了解的话, 可以参考我前期的文章: springcloud(二):注册中心Eureka,本篇文章主要给大家介绍 Spring Cloud Consul 的使用。

Consul 介绍

Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 等)。使用起来也较 为简单。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与 Docker 等轻量级容器可无缝配合。

Consul 的优势:

  • 使用 Raft 算法来保证一致性, 比复杂的 Paxos 算法更直接. 相比较而言, zookeeper 采用的是 Paxos, 而 etcd 使用的则是 Raft。
  • 支持多数据中心,内外网的服务采用不同的端口进行监听。 多数据中心集群可以避免单数据中心的单点故障,而其部署则需要考虑网络延迟, 分片等情况等。 zookeeper 和 etcd 均不提供多数据中心功能的支持。
  • 支持健康检查。 etcd 不提供此功能。
  • 支持 http 和 dns 协议接口。 zookeeper 的集成较为复杂, etcd 只支持 http 协议。
  • 官方提供 web 管理界面, etcd 无此功能。
  • 综合比较, Consul 作为服务注册和配置管理的新星, 比较值得关注和研究。

特性:

  • 服务发现
  • 健康检查
  • Key/Value 存储
  • 多数据中心

Consul 角色

  • client: 客户端, 无状态, 将 HTTP 和 DNS 接口请求转发给局域网内的服务端集群。
  • server: 服务端, 保存配置信息, 高可用集群, 在局域网内与本地客户端通讯, 通过广域网与其它数据中心通讯。 每个数据中心的 server 数量推荐为 3 个或是 5 个。

Consul 客户端、服务端还支持夸中心的使用,更加提高了它的高可用性。

Consul 工作原理:

  • 1、当 Producer 启动的时候,会向 Consul 发送一个 post 请求,告诉 Consul 自己的 IP 和 Port
  • 2、Consul 接收到 Producer 的注册后,每隔10s(默认)会向 Producer 发送一个健康检查的请求,检验Producer是否健康
  • 3、当 Consumer 发送 GET 方式请求 /api/address 到 Producer 时,会先从 Consul 中拿到一个存储服务 IP 和 Port 的临时表,从表中拿到 Producer 的 IP 和 Port 后再发送 GET 方式请求 /api/address
  • 4、该临时表每隔10s会更新,只包含有通过了健康检查的 Producer

Spring Cloud Consul 项目是针对 Consul 的服务治理实现。Consul 是一个分布式高可用的系统,它包含多个组件,但是作为一个整体,在微服务架构中为我们的基础设施提供服务发现和服务配置的工具。

Consul VS Eureka

Eureka 是一个服务发现工具。该体系结构主要是客户端/服务器,每个数据中心有一组 Eureka 服务器,通常每个可用区域一个。通常 Eureka 的客户使用嵌入式 SDK 来注册和发现服务。对于非本地集成的客户,官方提供的 Eureka 一些 REST 操作 API,其它语言可以使用这些 API 来实现对 Eureka Server 的操作从而实现一个非 jvm 语言的 Eureka Client。

Eureka 提供了一个弱一致的服务视图,尽可能的提供服务可用性。当客户端向服务器注册时,该服务器将尝试复制到其它服务器,但不提供保证复制完成。服务注册的生存时间(TTL)较短,要求客户端对服务器心跳检测。不健康的服务或节点停止心跳,导致它们超时并从注册表中删除。服务发现可以路由到注册的任何服务,由于心跳检测机制有时间间隔,可能会导致部分服务不可用。这个简化的模型允许简单的群集管理和高可扩展性。

Consul 提供了一些列特性,包括更丰富的健康检查,键值对存储以及多数据中心。Consul 需要每个数据中心都有一套服务,以及每个客户端的 agent,类似于使用像 Ribbon 这样的服务。Consul agent 允许大多数应用程序成为 Consul 不知情者,通过配置文件执行服务注册并通过 DNS 或负载平衡器 sidecars 发现。

Consul 提供强大的一致性保证,因为服务器使用 Raft 协议复制状态 。Consul 支持丰富的健康检查,包括 TCP,HTTP,Nagios / Sensu 兼容脚本或基于 Eureka 的 TTL。客户端节点参与基于 Gossip 协议的健康检查,该检查分发健康检查工作,而不像集中式心跳检测那样成为可扩展性挑战。发现请求被路由到选举出来的 leader,这使他们默认情况下强一致性。允许客户端过时读取取使任何服务器处理他们的请求,从而实现像 Eureka 这样的线性可伸缩性。

Consul 强烈的一致性意味着它可以作为领导选举和集群协调的锁定服务。Eureka 不提供类似的保证,并且通常需要为需要执行协调或具有更强一致性需求的服务运行 ZooKeeper。

Consul 提供了支持面向服务的体系结构所需的一系列功能。这包括服务发现,还包括丰富的运行状况检查,锁定,密钥/值,多数据中心联合,事件系统和 ACL。Consul 和 consul-template 和 envconsul 等工具生态系统都试图尽量减少集成所需的应用程序更改,以避免需要通过 SDK 进行本地集成。Eureka 是一个更大的 Netflix OSS 套件的一部分,该套件预计应用程序相对均匀且紧密集成。因此 Eureka 只解决了一小部分问题,可以和 ZooKeeper 等其它工具可以一起使用。

Consul 强一致性(C)带来的是:

服务注册相比 Eureka 会稍慢一些。因为 Consul 的 raft 协议要求必须过半数的节点都写入成功才认为注册成功 Leader 挂掉时,重新选举期间整个 Consul 不可用。保证了强一致性但牺牲了可用性。

Eureka 保证高可用(A)和最终一致性:

服务注册相对要快,因为不需要等注册信息 replicate 到其它节点,也不保证注册信息是否 replicate 成功 当数据出现不一致时,虽然 A, B 上的注册信息不完全相同,但每个 Eureka 节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求 A 查不到,但请求 B 就能查到。如此保证了可用性但牺牲了一致性。

其它方面,eureka 就是个 servlet 程序,跑在 servlet 容器中; Consul 则是 go 编写而成。

Consul 安装

Consul 不同于 Eureka 需要单独安装,访问 Consul 官网下载 Consul 的最新版本,我这里是 consul_1.2.1。

根据不同的系统类型选择不同的安装包,从下图也可以看出 Consul 支持所有主流系统。

我这里以 Windows 为例,下载下来是一个 consul_1.2.1_windows_amd64.zip 的压缩包,解压是是一个 consul.exe 的执行文件。

cd 到对应的目录下,使用 cmd 启动 Consul

cd D:\Common Files\consul
#cmd启动:
consul agent -dev        # -dev表示开发模式运行,另外还有-server表示服务模式运行

为了方便期间,可以在同级目录下创建一个 run.bat 脚本来启动,脚本内容如下:

consul agent -dev
pause

启动结果如下:

启动成功之后访问: http://localhost:8500,可以看到 Consul 的管理界面

这样就意味着我们的 Consul 服务启动成功了。

Consul 服务端

接下来我们开发 Consul 的服务端,我们创建一个 spring-cloud-consul-producer 项目

添加依赖包

依赖包如下:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-consul-discovery</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
  • spring-boot-starter-actuator健康检查依赖于此包。
  • spring-cloud-starter-consul-discoverySpring Cloud Consul 的支持。

Spring Boot 版本使用的是 2.0.3.RELEASE,Spring Cloud 最新版本是 Finchley.RELEASE 依赖于 Spring Boot 2.x.

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

完整的 pom.xml 文件大家可以参考示例源码。

配置文件

配置文件内容如下

spring.application.name=spring-cloud-consul-producer
server.port=8501
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
#注册到consul的服务名称
spring.cloud.consul.discovery.serviceName=service-producer

Consul 的地址和端口号默认是 localhost:8500 ,如果不是这个地址可以自行配置。 spring.cloud.consul.discovery.serviceName是指注册到 Consul 的服务名称,后期客户端会根据这个名称来进行服务调用。

启动类

@SpringBootApplication@EnableDiscoveryClientpublicclassConsulProducerApplication{publicstaticvoidmain(String[]args){SpringApplication.run(ConsulProducerApplication.class,args);}}

添加了 @EnableDiscoveryClient注解表示支持服务发现。

提供服务

我们在创建一个 Controller,推文提供 hello 的服务。

@RestControllerpublicclassHelloController{@RequestMapping("/hello")publicStringhello(){return"hello consul";}}

为了模拟注册均衡负载复制一份上面的项目重命名为 spring-cloud-consul-producer-2 ,修改对应的端口为 8502,修改 hello 方法的返回值为:”hello consul two”,修改完成后依次启动两个项目。

这时候我们再次在浏览器访问地址:http://localhost:8500,显示如下:

我们发现页面多了 service-producer 服务,点击进去后页面显示有两个服务提供者:

这样服务提供者就准备好了。

Consul 消费端

我们创建一个 spring-cloud-consul-consumer 项目,pom 文件和上面示例保持一致。

配置文件

配置文件内容如下

spring.application.name=spring-cloud-consul-consumer
server.port=8503
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
#设置不需要注册到 consul 中
spring.cloud.consul.discovery.register=false

客户端可以设置注册到 Consul 中,也可以不注册到 Consul 注册中心中,根据我们的业务来选择,只需要在使用服务时通过 Consul 对外提供的接口获取服务信息即可。

启动类

@SpringBootApplicationpublicclassConsulConsumerApplication{publicstaticvoidmain(String[]args){SpringApplication.run(ConsulConsumerApplication.class,args);}}

进行测试

我们先来创建一个 ServiceController ,试试如果去获取 Consul 中的服务。

@RestControllerpublicclassServiceController{@AutowiredprivateLoadBalancerClientloadBalancer;@AutowiredprivateDiscoveryClientdiscoveryClient;/**
     * 获取所有服务
     */@RequestMapping("/services")publicObjectservices(){returndiscoveryClient.getInstances("service-producer");}/**
     * 从所有服务中选择一个服务(轮询)
     */@RequestMapping("/discover")publicObjectdiscover(){returnloadBalancer.choose("service-producer").getUri().toString();}}

Controller 中有俩个方法,一个是获取所有服务名为 service-producer的服务信息并返回到页面,一个是随机从服务名为 service-producer的服务中获取一个并返回到页面。

添加完 ServiceController 之后我们启动项目,访问地址: http://localhost:8503/services,返回:

[{"serviceId":"service-producer","host":"windows10.microdone.cn","port":8501,"secure":false,"metadata":{"secure":"false"},"uri":"http://windows10.microdone.cn:8501","scheme":null},{"serviceId":"service-producer","host":"windows10.microdone.cn","port":8502,"secure":false,"metadata":{"secure":"false"},"uri":"http://windows10.microdone.cn:8502","scheme":null}]

发现我们刚才创建的端口为 8501 和 8502 的两个服务端都存在。

多次访问地址: http://localhost:8503/discover,页面会交替返回下面信息:

http://windows10.microdone.cn:8501
http://windows10.microdone.cn:8502
...

说明 8501 和 8502 的两个服务会交替出现,从而实现了获取服务端地址的均衡负载。

大多数情况下我们希望使用均衡负载的形式去获取服务端提供的服务,因此使用第二种方法来模拟调用服务端提供的 hello 方法。

创建 CallHelloController :

@RestControllerpublicclassCallHelloController{@AutowiredprivateLoadBalancerClientloadBalancer;@RequestMapping("/call")publicStringcall(){ServiceInstanceserviceInstance=loadBalancer.choose("service-producer");System.out.println("服务地址:"+serviceInstance.getUri());System.out.println("服务名称:"+serviceInstance.getServiceId());StringcallServiceResult=newRestTemplate().getForObject(serviceInstance.getUri().toString()+"/hello",String.class);System.out.println(callServiceResult);returncallServiceResult;}}

使用 RestTemplate 进行远程调用。添加完之后重启 spring-cloud-consul-consumer 项目。在浏览器中访问地址: http://localhost:8503/call,依次返回结果如下:

hello consul
hello consul two
...

说明我们已经成功的调用了 Consul 服务端提供的服务,并且实现了服务端的均衡负载功能。通过今天的实践我们发现 Consul 提供的服务发现易用、强大。

示例代码-github

示例代码-码云



Kafka笔记—可靠性、幂等性和事务 - luozhiyun - 博客园

$
0
0

这几天很忙,但是我现在给我的要求是一周至少要出一篇文章,所以先拿这篇笔记来做开胃菜,源码分析估计明后两天应该能写一篇。给自己加油~,即使没什么人看。

可靠性

如何保证消息不丢失

Kafka只对“已提交”的消息(committed message)做有限度的持久化保证。

已提交的消息
当Kafka的若干个Broker成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交。

有限度的持久化保证
假如一条消息保存在N个Kafka Broker上,那么至少这N个Broker至少有一个存活,才能保证消息不丢失。

丢失数据案例

生产者程序丢失数据

由于Kafka Producer是异步发送的,调用完producer.send(msg)并不能认为消息已经发送成功。

所以,在Producer永远要使用带有回调通知的发送API,使用producer.send(msg,callback)。一旦出现消息提交失败的情况,可以由针对性地进行处理。

消费者端丢失数据

消费者是先更新offset,再消费消息。如果这个时候消费者突然宕机了,那么这条消息就会丢失。

所以我们要先消费消息,再更新offset位置。但是这样会导致消息重复消费。

还有一种情况就是consumer获取到消息后开启了多个线程异步处理消息,而consumer自动地向前更新offset。假如其中某个线程运行失败了,那么消息就丢失了。

遇到这样的情况,consumer不要开启自动提交位移,而是要应用程序手动提交位移。

最佳实现

  1. 使用producer.send(msg,callback)。
  2. 设置acks = all。acks是Producer的参数,代表了所有副本Broker都要接收到消息,该消息才算是“已提交”。
  3. 设置retries为一个较大的值。是Producer的参数,对应Producer自动重试。如果出现网络抖动,那么可以自动重试消息发送,避免消息丢失。
  4. unclean.leader.election.enable = false。控制有哪些Broker有资格竞选分区的Leader。表示不允许落后太多的Broker竞选Leader。
  5. 设置replication.factor>=3。Broker参数,冗余Broker。
  6. 设置min.insync.replicas>1。Broker参数。控制消息至少要被写入到多少个副本才算是“已提交”。
  7. 确保replication.factor>min.insync.replicas。如果两个相等,那么只要有一个副本挂机,整个分区就无法正常工作了。推荐设置成replication.factor=min.insync.replicas+1.
  8. 确保消息消费完成在提交。Consumer端参数enbale.auto.commit,设置成false,手动提交位移。

解释第二条和第六条:
如果ISR中只有1个副本了,acks=all也就相当于acks=1了,引入min.insync.replicas的目的就是为了做一个下限的限制:不能只满足于ISR全部写入,还要保证ISR中的写入个数不少于min.insync.replicas。

幂等性

在0.11.0.0版本引入了创建幂等性Producer的功能。仅需要设置props.put(“enable.idempotence”,true),或props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true)。

enable.idempotence设置成true后,Producer自动升级成幂等性Producer。Kafka会自动去重。Broker会多保存一些字段。当Producer发送了相同字段值的消息后,Broker能够自动知晓这些消息已经重复了。

作用范围:

  1. 只能保证单分区上的幂等性,即一个幂等性Producer能够保证某个主题的一个分区上不出现重复消息。
  2. 只能实现单回话上的幂等性,这里的会话指的是Producer进程的一次运行。当重启了Producer进程之后,幂等性不保证。

事务

Kafka在0.11版本开始提供对事务的支持,提供是read committed隔离级别的事务。保证多条消息原子性地写入到目标分区,同时也能保证Consumer只能看到事务成功提交的消息。

事务性Producer

保证多条消息原子性地写入到多个分区中。这批消息要么全部成功,要不全部失败。事务性Producer也不惧进程重启。

Producer端的设置:

  1. 开启 enable.idempotence = true
  2. 设置Producer端参数 transactional.id

除此之外,还要加上调用事务API,如initTransaction、beginTransaction、commitTransaction和abortTransaction,分别应对事务的初始化、事务开始、事务提交以及事务终止。
如下:

producer.initTransactions();
try {
            producer.beginTransaction();
            producer.send(record1);
            producer.send(record2);
            producer.commitTransaction();
} catch (KafkaException e) {
            producer.abortTransaction();
}

这段代码能保证record1和record2被当做一个事务同一提交到Kafka,要么全部成功,要么全部写入失败。

Consumer端的设置:
设置isolation.level参数,目前有两个取值:

  1. read_uncommitted:默认值表明Consumer端无论事务型Producer提交事务还是终止事务,其写入的消息都可以读取。
  2. read_committed:表明Consumer只会读取事务型Producer成功提交事务写入的消息。注意,非事务型Producer写入的所有消息都能看到。

Spring Cloud Alibaba | 微服务分布式事务之Seata - 极客挖掘机 - 博客园

$
0
0

Spring Cloud Alibaba | 微服务分布式事务之Seata

本篇实战所使用Spring有关版本:

SpringBoot:2.1.7.RELEASE

Spring Cloud:Greenwich.SR2

Spring CLoud Alibaba:2.1.0.RELEASE

1. 概述

在构建微服务的过程中,不管是使用什么框架、组件来构建,都绕不开一个问题,跨服务的业务操作如何保持数据一致性。

2. 什么是分布式事务?

首先,设想一个传统的单体应用,无论多少内部调用,最后终归是在同一个数据库上进行操作来完成一向业务操作,如图:

随着业务量的发展,业务需求和架构发生了巨大的变化,整体架构由原来的单体应用逐渐拆分成为了微服务,原来的3个服务被从一个单体架构上拆开了,成为了3个独立的服务,分别使用独立的数据源,也不在之前共享同一个数据源了,具体的业务将由三个服务的调用来完成,如图:

此时,每一个服务的内部数据一致性仍然有本地事务来保证。但是面对整个业务流程上的事务应该如何保证呢?这就是在微服务架构下面临的挑战,如何保证在微服务中的数据一致性。

3. 常见的分布式事务解决方案

3.1 两阶段提交方案/XA方案

所谓的 XA 方案,即两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。

分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

  • 该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。

  • 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。

  • 所有节点不会永久性损坏,即使损坏后仍然可以恢复。

3.2 TCC 方案

TCC的全称是:Try、Confirm、Cancel。

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。
  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大。

TCC的理论有点抽象,下面我们借助一个账务拆分这个实际业务场景对TCC事务的流程做一个描述,希望对理解TCC有所帮助。

业务流程:分别位于三个不同分库的帐户A、B、C,A和B一起向C转帐共80元:

Try:尝试执行业务。

完成所有业务检查(一致性):检查A、B、C的帐户状态是否正常,帐户A的余额是否不少于30元,帐户B的余额是否不少于50元。

预留必须业务资源(准隔离性):帐户A的冻结金额增加30元,帐户B的冻结金额增加50元,这样就保证不会出现其他并发进程扣减了这两个帐户的余额而导致在后续的真正转帐操作过程中,帐户A和B的可用余额不够的情况。

Confirm:确认执行业务。

真正执行业务:如果Try阶段帐户A、B、C状态正常,且帐户A、B余额够用,则执行帐户A给账户C转账30元、帐户B给账户C转账50元的转帐操作。

不做任何业务检查:这时已经不需要做业务检查,Try阶段已经完成了业务检查。

只使用Try阶段预留的业务资源:只需要使用Try阶段帐户A和帐户B冻结的金额即可。

Cancel:取消执行业务。

释放Try阶段预留的业务资源:如果Try阶段部分成功,比如帐户A的余额够用,且冻结相应金额成功,帐户B的余额不够而冻结失败,则需要对帐户A做Cancel操作,将帐户A被冻结的金额解冻掉。

4. Spring Cloud Alibaba Seata

Seata 的方案其实一个 XA 两阶段提交的改进版,具体区别如下:

架构的层面

XA 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身(通过提供支持 XA 的驱动程序来供应用使用)。

而 Seata 的 RM 是以二方包的形式作为中间件层部署在应用程序这一侧的,不依赖与数据库本身对协议的支持,当然也不需要数据库支持 XA 协议。这点对于微服务化的架构来说是非常重要的:应用层不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。

这个设计,剥离了分布式事务方案对数据库在 协议支持 上的要求。

两阶段提交

无论 Phase2 的决议是 commit 还是 rollback,事务性资源的锁都要保持到 Phase2 完成才释放。

设想一个正常运行的业务,大概率是 90% 以上的事务最终应该是成功提交的,我们是否可以在 Phase1 就将本地事务提交呢?这样 90% 以上的情况下,可以省去 Phase2 持锁的时间,整体提高效率。

  • 分支事务中数据的 本地锁 由本地事务管理,在分支事务 Phase1 结束时释放。
  • 同时,随着本地事务结束,连接 也得以释放。
  • 分支事务中数据的 全局锁 在事务协调器侧管理,在决议 Phase2 全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁 才被持有至分支的 Phase2 结束。

这个设计,极大地减少了分支事务对资源(数据和连接)的锁定时间,给整体并发和吞吐的提升提供了基础。

5. Seata实战案例

5.1 目标介绍

在本节,我们将通过一个实战案例来具体介绍Seata的使用方式,我们将模拟一个简单的用户购买商品下单场景,创建3个子工程,分别是 order-server (下单服务)、storage-server(库存服务)和 pay-server (支付服务),具体流程图如图:

5.2 环境准备

在本次实战中,我们使用Nacos做为服务中心和配置中心,Nacos部署请参考本书的第十一章,这里不再赘述。

接下来我们需要部署Seata的Server端,下载地址为:https://github.com/seata/seata/releases ,建议选择最新版本下载,目前笔者看到的最新版本为 v0.8.0 ,下载 seata-server-0.8.0.tar.gz 解压后,打开 conf 文件夹,我们需对其中的一些配置做出修改。

5.2.1 registry.conf 文件修改,如下:

registry {
    type = "nacos"
    nacos {
    serverAddr = "192.168.0.128"
    namespace = "public"
    cluster = "default"
    }
}

config {
    type = "nacos"
    nacos {
    serverAddr = "192.168.0.128"
    namespace = "public"
    cluster = "default"
    }
}

这里我们选择使用Nacos作为服务中心和配置中心,这里做出对应的配置,同时可以看到Seata的注册服务支持:file 、nacos 、eureka、redis、zk、consul、etcd3、sofa等方式,配置支持:file、nacos 、apollo、zk、consul、etcd3等方式。

5.2.2 file.conf 文件修改

这里我们需要其中配置的数据库相关配置,具体如下:

## database store
db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://192.168.0.128:3306/seata"
    user = "root"
    password = "123456"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
}

这里数据库默认是使用mysql,需要配置对应的数据库连接、用户名和密码等。

5.2.3 nacos-config.txt 文件修改,具体如下:

service.vgroup_mapping.spring-cloud-pay-server=default
service.vgroup_mapping.spring-cloud-order-server=default
service.vgroup_mapping.spring-cloud-storage-server=default

这里的语法为: service.vgroup_mapping.${your-service-gruop}=default,中间的 ${your-service-gruop}为自己定义的服务组名称,这里需要我们在程序的配置文件中配置,笔者这里直接使用程序的 spring.application.name

5.2.4 数据库初始化

需要在刚才配置的数据库中执行数据初始脚本 db_store.sql ,这个是全局事务控制的表,需要提前初始化。

这里我们只是做演示,理论上上面三个业务服务应该分属不同的数据库,这里我们只是在同一台数据库下面创建三个 Schema ,分别为 db_account 、 db_order 和 db_storage ,具体如图:

5.2.5 服务启动

因为我们是使用的Nacos作为配置中心,所以这里需要先执行脚本来初始化Nacos的相关配置,命令如下:

cd conf
sh nacos-config.sh 192.168.0.128

执行成功后可以打开Nacos的控制台,在配置列表中,可以看到初始化了很多 Group 为 SEATA_GROUP 的配置,如图:

初始化成功后,可以使用下面的命令启动Seata的Server端:

cd bin
sh seata-server.sh -p 8091 -m file

启动后在 Nacos 的服务列表下面可以看到一个名为 serverAddr 的服务

到这里,我们的环境准备工作就做完了,接下来开始代码实战。

5.3 代码实战

由于本示例代码偏多,这里仅介绍核心代码和一些需要注意的代码,其余代码各位读者可以访问本书配套的代码仓库获取。

子工程common用来放置一些公共类,主要包含视图 VO 类和响应类 OperationResponse.java。

5.3.1 父工程 seata-nacos-jpa 依赖 pom.xml 文件

代码清单:Alibaba/seata-nacos-jpa/pom.xml


<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Cloud Nacos Service Discovery --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- Spring Cloud Nacos Config --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!-- Spring Cloud Seata --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

说明:本示例是使用 JPA 作为数据库访问 ORM 层, Mysql 作为数据库,需引入 JPA 和 Mysql 相关依赖, spring-cloud-alibaba-dependencies的版本是 2.1.0.RELEASE , 其中有关Seata的组件版本为 v0.7.1 ,虽然和服务端版本不符,经简单测试,未发现问题。

5.3.2 数据源配置

Seata 是通过代理数据源实现事务分支,所以需要配置 io.seata.rm.datasource.DataSourceProxy 的 Bean,且是 @Primary默认的数据源,否则事务不会回滚,无法实现分布式事务,数据源配置类DataSourceProxyConfig.java如下:

代码清单:Alibaba/seata-nacos-jpa/order-server/src/main/java/com/springcloud/orderserver/config/DataSourceProxyConfig.java
***

@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

5.3.3 开启全局事务

我们在order-server服务中开始整个业务流程,需要在这里的方法上增加全局事务的注解 @GlobalTransactional,具体代码如下:

代码清单:Alibaba/seata-nacos-jpa/order-server/src/main/java/com/springcloud/orderserver/service/impl/OrderServiceImpl.java
***

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private OrderDao orderDao;

    private final String STORAGE_SERVICE_HOST = "http://spring-cloud-storage-server/storage";
    private final String PAY_SERVICE_HOST = "http://spring-cloud-pay-server/pay";

    @Override
    @GlobalTransactional
    public OperationResponse placeOrder(PlaceOrderRequestVO placeOrderRequestVO) {
        Integer amount = 1;
        Integer price = placeOrderRequestVO.getPrice();

        Order order = Order.builder()
                .userId(placeOrderRequestVO.getUserId())
                .productId(placeOrderRequestVO.getProductId())
                .status(OrderStatus.INIT)
                .payAmount(price)
                .build();

        order = orderDao.save(order);

        log.info("保存订单{}", order.getId() != null ? "成功" : "失败");
        log.info("当前 XID: {}", RootContext.getXID());

        // 扣减库存
        log.info("开始扣减库存");
        ReduceStockRequestVO reduceStockRequestVO = ReduceStockRequestVO.builder()
                .productId(placeOrderRequestVO.getProductId())
                .amount(amount)
                .build();
        String storageReduceUrl = String.format("%s/reduceStock", STORAGE_SERVICE_HOST);
        OperationResponse storageOperationResponse = restTemplate.postForObject(storageReduceUrl, reduceStockRequestVO, OperationResponse.class);
        log.info("扣减库存结果:{}", storageOperationResponse);

        // 扣减余额
        log.info("开始扣减余额");
        ReduceBalanceRequestVO reduceBalanceRequestVO = ReduceBalanceRequestVO.builder()
                .userId(placeOrderRequestVO.getUserId())
                .price(price)
                .build();

        String reduceBalanceUrl = String.format("%s/reduceBalance", PAY_SERVICE_HOST);
        OperationResponse balanceOperationResponse = restTemplate.postForObject(reduceBalanceUrl, reduceBalanceRequestVO, OperationResponse.class);
        log.info("扣减余额结果:{}", balanceOperationResponse);

        Integer updateOrderRecord = orderDao.updateOrder(order.getId(), OrderStatus.SUCCESS);
        log.info("更新订单:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");

        return OperationResponse.builder()
                .success(balanceOperationResponse.isSuccess() && storageOperationResponse.isSuccess())
                .build();
    }
}

其次,我们需要在另外两个服务的方法中增加注解 @Transactional,表示开启事务。

这里的远端服务调用是通过 RestTemplate,需要在工程启动时将 RestTemplate注入 Spring 容器中管理。

5.3.4 配置文件

工程中需在 resources 目录下增加有关Seata的配置文件 registry.conf ,如下:

代码清单:Alibaba/seata-nacos-jpa/order-server/src/main/resources/registry.conf
***

registry {
  type = "nacos"
  nacos {
    serverAddr = "192.168.0.128"
    namespace = "public"
    cluster = "default"
  }
}

config {
  type = "nacos"
  nacos {
    serverAddr = "192.168.0.128"
    namespace = "public"
    cluster = "default"
  }
}

在 bootstrap.yml 中的配置如下:

代码清单:Alibaba/seata-nacos-jpa/order-server/src/main/resources/bootstrap.yml


spring:
  application:
    name: spring-cloud-order-server
  cloud:
    nacos:
      # nacos config
      config:
        server-addr: 192.168.0.128
        namespace: public
        group: SEATA_GROUP
      # nacos discovery
      discovery:
        server-addr: 192.168.0.128
        namespace: public
        enabled: true
    alibaba:
      seata:
        tx-service-group: ${spring.application.name}
  • spring.cloud.nacos.config.group :这里的 Group 是 SEATA_GROUP ,也就是我们前面在使用 nacos-config.sh 生成 Nacos 的配置时生成的配置,它的 Group 是 SEATA_GROUP。
  • spring.cloud.alibaba.seata.tx-service-group :这里是我们之前在修改 Seata Server 端配置文件 nacos-config.txt 时里面配置的 service.vgroup_mapping.${your-service-gruop}=default中间的 ${your-service-gruop}。这两处配置请务必一致,否则在启动工程后会一直报错 no available server to connect

5.3.5 业务数据库初始化

数据库初始脚本位于:Alibaba/seata-nacos-jpa/sql ,请分别在三个不同的 Schema 中执行。

5.3.6 测试

测试工具我们选择使用 PostMan ,启动三个服务,顺序无关 order-server、pay-server 和 storage-server 。

使用 PostMan 发送测试请求,如图:

数据库初始化余额为 10 ,这里每次下单将会消耗 5 ,我们可以正常下单两次,第三次应该下单失败,并且回滚 db_order 中的数据。数据库中数据如图:

我们进行第三次下单操作,如图:

这里看到直接报错500,查看数据库 db_order 中的数据,如图:

可以看到,这里的数据并未增加,我们看下子工程_rder-server的控制台打印:

日志已经过简化处理

Hibernate: insert into orders (pay_amount, product_id, status, user_id) values (?, ?, ?, ?)
c.s.b.c.service.impl.OrderServiceImpl    : 保存订单成功
c.s.b.c.service.impl.OrderServiceImpl    : 当前 XID: 192.168.0.102:8091:2021674307
c.s.b.c.service.impl.OrderServiceImpl    : 开始扣减库存
c.s.b.c.service.impl.OrderServiceImpl    : 扣减库存结果:OperationResponse(success=true, message=操作成功, data=null)
c.s.b.c.service.impl.OrderServiceImpl    : 开始扣减余额
i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.0.102:8091:2021674307,branchId=2021674308,branchType=AT,resourceId=jdbc:mysql://192.168.0.128:3306/db_order,applicationData=null
io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.0.102:8091:2021674307 2021674308 jdbc:mysql://192.168.0.128:3306/db_order
i.s.rm.datasource.undo.UndoLogManager    : xid 192.168.0.102:8091:2021674307 branch 2021674308, undo_log deleted with GlobalFinished
io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
i.seata.tm.api.DefaultGlobalTransaction  : [192.168.0.102:8091:2021674307] rollback status:Rollbacked

从日志中没有可以清楚的看到,在服务order-server是先执行了订单写入操作,并且调用扣减库存的接口,通过查看storage-server的日志也可以发现,一样是先执行了库存修改操作,直到扣减余额的时候发现余额不足,开始对 xid 为 192.168.0.102:8091:2021674307执行回滚操作,并且这个操作是全局回滚。

6. 注意

目前在 Seata v0.8.0 的版本中,Server端尚未支持集群部署,不建议应用于生产环境,并且开源团队计划在 v1.0.0 版本的时候可以使用与生产环境,各位读者可以持续关注这个开源项目。

7. 示例代码

Github-示例代码

Gitee-示例代码

参考资料: Seata官方文档

Java面试通关要点汇总集 - luozhiyun - 博客园

$
0
0

基础篇

基本功

  • 面向对象的特征
  • final, finally, finalize 的区别
  • int 和 Integer 有什么区别
  • 重载和重写的区别
  • 抽象类和接口有什么区别
  • 说说反射的用途及实现
  • 说说自定义注解的场景及实现
  • HTTP 请求的 GET 与 POST 方式的区别
  • session 与 cookie 区别
  • session 分布式处理
  • JDBC 流程
  • MVC 设计思想
  • equals 与 == 的区别

集合

  • List 和 Set 区别
  • List 和 Map 区别
  • Arraylist 与 LinkedList 区别
  • ArrayList 与 Vector 区别
  • HashMap 和 Hashtable 的区别
  • HashSet 和 HashMap 区别
  • HashMap 和 ConcurrentHashMap 的区别
  • HashMap 的工作原理及代码实现
  • ConcurrentHashMap 的工作原理及代码实现

线程

  • 创建线程的方式及实现
  • sleep() 、join()、yield()有什么区别
  • 说说 CountDownLatch 原理
  • 说说 CyclicBarrier 原理
  • 说说 Semaphore 原理
  • 说说 Exchanger 原理
  • 说说 CountDownLatch 与 CyclicBarrier 区别
  • ThreadLocal 原理分析
  • 讲讲线程池的实现原理
  • 线程池的几种方式
  • 线程的生命周期

锁机制

  • 说说线程安全问题
  • volatile 实现原理
  • synchronize 实现原理
  • synchronized 与 lock 的区别
  • CAS 乐观锁
  • ABA 问题
  • 乐观锁的业务场景及实现方式

核心篇

数据存储

  • MySQL 索引使用的注意事项
  • 说说反模式设计
  • 说说分库与分表设计
  • 分库与分表带来的分布式困境与应对之策
  • 说说 SQL 优化之道
  • MySQL 遇到的死锁问题
  • 存储引擎的 InnoDB 与 MyISAM
  • 数据库索引的原理
  • 为什么要用 B-tree
  • 聚集索引与非聚集索引的区别
  • limit 20000 加载很慢怎么解决
  • 选择合适的分布式主键方案
  • 选择合适的数据存储方案
  • ObjectId 规则
  • 聊聊 MongoDB 使用场景
  • 倒排索引
  • 聊聊 ElasticSearch 使用场景

缓存使用

  • Redis 有哪些类型
  • Redis 内部结构
  • 聊聊 Redis 使用场景
  • Redis 持久化机制
  • Redis 如何实现持久化
  • Redis 集群方案与实现
  • Redis 为什么是单线程的
  • 缓存奔溃
  • 缓存降级
  • 使用缓存的合理性问题

消息队列

  • 消息队列的使用场景
  • 消息的重发补偿解决思路
  • 消息的幂等性解决思路
  • 消息的堆积解决思路
  • 自己如何实现消息队列
  • 如何保证消息的有序性

框架篇

Spring

  • BeanFactory 和 ApplicationContext 有什么区别
  • Spring Bean 的生命周期
  • Spring IOC 如何实现
  • 说说 Spring AOP
  • Spring AOP 实现原理
  • 动态代理(cglib 与 JDK)
  • Spring 事务实现方式
  • Spring 事务底层原理
  • 如何自定义注解实现功能
  • Spring MVC 运行流程
  • Spring MVC 启动流程
  • Spring 的单例实现原理
  • Spring 框架中用到了哪些设计模式
  • Spring 其他产品(Srping Boot、Spring Cloud、Spring Secuirity、Spring Data、Spring AMQP 等)

Netty

  • 为什么选择 Netty
  • 说说业务中,Netty 的使用场景
  • 原生的 NIO 在 JDK 1.7 版本存在 epoll bug
  • 什么是TCP 粘包/拆包
  • TCP粘包/拆包的解决办法
  • Netty 线程模型
  • 说说 Netty 的零拷贝
  • Netty 内部执行流程
  • Netty 重连实现

微服务篇

微服务

  • 前后端分离是如何做的
  • 微服务哪些框架
  • 你怎么理解 RPC 框架
  • 说说 RPC 的实现原理
  • 说说 Dubbo 的实现原理
  • 你怎么理解 RESTful
  • 说说如何设计一个良好的 API
  • 如何理解 RESTful API 的幂等性
  • 如何保证接口的幂等性
  • 说说 CAP 定理、 BASE 理论
  • 怎么考虑数据一致性问题
  • 说说最终一致性的实现方案
  • 你怎么看待微服务
  • 微服务与 SOA 的区别
  • 如何拆分服务
  • 微服务如何进行数据库管理
  • 如何应对微服务的链式调用异常
  • 对于快速追踪与定位问题
  • 微服务的安全

分布式

  • 谈谈业务中使用分布式的场景
  • Session 分布式方案
  • 分布式锁的场景
  • 分布是锁的实现方案
  • 分布式事务
  • 集群与负载均衡的算法与实现
  • 说说分库与分表设计
  • 分库与分表带来的分布式困境与应对之策

安全问题

  • 安全要素与 STRIDE 威胁
  • 防范常见的 Web 攻击
  • 服务端通信安全攻防
  • HTTPS 原理剖析
  • HTTPS 降级攻击
  • 授权与认证
  • 基于角色的访问控制
  • 基于数据的访问控制

性能优化

  • 性能指标有哪些
  • 如何发现性能瓶颈
  • 性能调优的常见手段
  • 说说你在项目中如何进行性能调优

工程篇

需求分析

  • 你如何对需求原型进行理解和拆分
  • 说说你对功能性需求的理解
  • 说说你对非功能性需求的理解
  • 你针对产品提出哪些交互和改进意见
  • 你如何理解用户痛点

设计能力

  • 说说你在项目中使用过的 UML 图
  • 你如何考虑组件化
  • 你如何考虑服务化
  • 你如何进行领域建模
  • 你如何划分领域边界
  • 说说你项目中的领域建模
  • 说说概要设计

设计模式

  • 你项目中有使用哪些设计模式
  • 说说常用开源框架中设计模式使用分析
  • 说说你对设计原则的理解
  • 23种设计模式的设计理念
  • 设计模式之间的异同,例如策略模式与状态模式的区别
  • 设计模式之间的结合,例如策略模式+简单工厂模式的实践
  • 设计模式的性能,例如单例模式哪种性能更好。

业务工程

  • 你系统中的前后端分离是如何做的
  • 说说你的开发流程
  • 你和团队是如何沟通的
  • 你如何进行代码评审
  • 说说你对技术与业务的理解
  • 说说你在项目中经常遇到的 Exception
  • 说说你在项目中遇到感觉最难Bug,怎么解决的
  • 说说你在项目中遇到印象最深困难,怎么解决的
  • 你觉得你们项目还有哪些不足的地方
  • 你是否遇到过 CPU 100% ,如何排查与解决
  • 你是否遇到过 内存 OOM ,如何排查与解决
  • 说说你对敏捷开发的实践
  • 说说你对开发运维的实践
  • 介绍下工作中的一个对自己最有价值的项目,以及在这个过程中的角色

软实力

  • 说说你的亮点
  • 说说你最近在看什么书
  • 说说你觉得最有意义的技术书籍
  • 工作之余做什么事情
  • 说说个人发展方向方面的思考
  • 说说你认为的服务端开发工程师应该具备哪些能力
  • 说说你认为的架构师是什么样的,架构师主要做什么
  • 说说你所理解的技术专家

kafka消费者客户端 - sowhat1943 - 博客园

$
0
0

Kafka消费者

1.1 消费者与消费者组

消费者与消费者组之间的关系

​ 每一个消费者都隶属于某一个消费者组,一个消费者组可以包含一个或多个消费者,每一条消息只会被消费者组中的某一个消费者所消费。不同消费者组之间消息的消费是互不干扰的。

为什么会有消费者组的概念

​ 消费者组出现主要是出于两个目的:

​ (1) 使整体的消费能力具备横向的伸缩性。可以适当增加消费者组中消费者的数量,来提高整体的消费能力。但是每一个分区至多被消费者组的中一个消费者所消费,因此当消费者组中消费者数量超过分区数时,多出的消费者不会分配到任何一个分区。当然这是默认的分区分配策略,可通过partition.assignment.strategy进行配置。

​ (2) 实现消息消费的隔离。不同消费者组之间消息消费互不干扰,从而实现发布订阅这种消息投递模式。

注意:

​ 消费者隶属的消费者组可以通过 group.id进行配置。消费者组是一个逻辑上的概念,但消费者并不是一个逻辑上的概念, 它可以是一个线程,也可以是一个进程。同一个消费者组内的消费者可以部署在同一台机器上,也可以部署在不同的机器上。

1.2 消费者客户端开发

​ 一个正常的消费逻辑需要具备以下几个步骤:

  • 配置消费者客户端参数及创建相应的消费者实例。

  • 订阅主题

  • 拉取消息并消费

  • 提交消费位移

  • 关闭消费者实例

public class KafkaConsumerAnalysis {      
  public static final String brokerList="node112:9092,node113:9092,node114:9092";
  public static final String topic = "topic-demo";
  public static final String groupId = "group.demo";
  public static final AtomicBoolean isRunning = new AtomicBoolean(true);

  public static Properties initConfig() {
      Properties prop = new Properties();
      prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
      prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
      prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
      prop.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
      prop.put(ConsumerConfig.CLIENT_DNS_LOOKUP_CONFIG, "consumer.client.di.demo");
      return prop;
  }


  public static void main(String[] args) {
      KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(initConfig());
       
      for (ConsumerRecord<String, String> record : records) {
          System.out.println("topic = " + record.topic() + ", partition =" +                                               record.partition() + ", offset = " + record.offset());
          System.out.println("key = " + record.key() + ", value = " + record.value());
              }
          }
      } catch (Exception e) {
          e.printStackTrace();
      }finally {
          consumer.close();
      }
  }
}

1.2.1 订阅主题和分区

​ 先来说一下消费者订阅消息的粒度:一个消费者可以订阅一个主题、多个主题、或者多个主题的特定分区。主要通过subsribe和assign两个方法实现订阅。

(1)订阅一个主题:

​ public void subscribe(Collection<String>topics),当集合中有一个主题时。

(2)订阅多个主题:

​ public void subscribe(Collection<String>topics),当集合中有多个主题时。

​ public void subscribe(Pattern pattern),通过正则表达式实现消费者主题的匹配 。通过这种方式,如果在消息消费的过程中,又添加了新的能够匹配到正则的主题,那么消费者就可以消费到新添加的主题。consumer.subscribe(Pattern.compile("topic-.*"));

(3)多个主题的特定分区

​ public void assign(Collection<TopicPartition>partitions),可以实现订阅某些特定的主题分区。TopicPartition包括两个属性:topic(String)和partition(int)。

​ 如果事先不知道有多少分区该如何处理,KafkaConsumer中的partitionFor方法可以获得指定主题分区的元数据信息:

​ public List<PartitionInfo>partitionsFor(String topic)

​ PartitionInfo的属性如下:


public class PartitionInfo {
  private final String topic;//主题
  private final int partition;//分区
  private final Node leader;//分区leader
  private final Node[] replicas;//分区的AR
  private final Node[] inSyncReplicas;//分区的ISR
  private final Node[] offlineReplicas;//分区的OSR
}

​ 因此也可以通过这个方法实现某个主题的全部订阅。

​ 需要指出的是,subscribe(Collection)、subscirbe(Pattern)、assign(Collection)方法分别代表了三种不同的订阅状态:AUTO_TOPICS、AUTO_PATTREN和USER_ASSIGN,这三种方式是互斥的,消费者只能使用其中一种,否则会报出IllegalStateException。

​ subscirbe方法可以实现消费者自动再平衡的功能。多个消费者的情况下,可以根据分区分配策略自动分配消费者和分区的关系,当消费者增加或减少时,也能实现负载均衡和故障转移。

​ 如何实现取消订阅:

​ consumer.unsubscribe()

1.2.2 反序列化

​ KafkaProducer端生产消息进行序列化,同样消费者就要进行相应的反序列化。相当于根据定义的序列化格式的一个逆序提取数据的过程。


import com.gdy.kafka.producer.Company;
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Deserializer;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Map;

public class CompanyDeserializer implements Deserializer<Company> {
  @Override
  public void configure(Map<String, ?> configs, boolean isKey) {

  }

  @Override
  public Company deserialize(String topic, byte[] data) {
      if(data == null) {
          return null;
      }

      if(data.length < 8) {
          throw new SerializationException("size of data received by Deserializer is shorter than expected");
      }

      ByteBuffer buffer = ByteBuffer.wrap(data);
      int nameLength = buffer.getInt();
      byte[] nameBytes = new byte[nameLength];
      buffer.get(nameBytes);
      int addressLen = buffer.getInt();
      byte[] addressBytes = new byte[addressLen];
      buffer.get(addressBytes);
      String name,address;
      try {
          name = new String(nameBytes,"UTF-8");
          address = new String(addressBytes,"UTF-8");
      }catch (UnsupportedEncodingException e) {
          throw new SerializationException("Error accur when deserializing");
      }

      return new Company(name, address);
  }

  @Override
  public void close() {

  }
}

​ 实际生产中需要自定义序列化器和反序列化器时,推荐使用Avro、JSON、Thrift、ProtoBuf或者Protostuff等通用的序列化工具来包装。

1.2.3 消息消费

​ Kafka中消息的消费是基于拉模式的,kafka消息的消费是一个不断轮旋的过程,消费者需要做的就是重复的调用poll方法。


public ConsumerRecords<K, V> poll(final Duration timeout)

​ 这个方法需要注意的是,如果消费者的缓冲区中有可用的数据,则会立即返回,否则会阻塞至timeout。如果在阻塞时间内缓冲区仍没有数据,则返回一个空的消息集。timeout的设置取决于应用程序对效应速度的要求。如果应用线程的位移工作是从Kafka中拉取数据并进行消费可以将这个参数设置为Long.MAX_VALUE。

​ 每次poll都会返回一个ConsumerRecords对象,它是ConsumerRecord的集合。对于ConsumerRecord相比于ProducerRecord多了一些属性:


private final String topic;//主题
  private final int partition;//分区
  private final long offset;//偏移量
  private final long timestamp;//时间戳
  private final TimestampType timestampType;//时间戳类型
  private final int serializedKeySize;//序列化key的大小
  private final int serializedValueSize;//序列化value的大小
  private final Headers headers;//headers
  private final K key;//key
  private final V value;//value
  private volatile Long checksum;//CRC32校验和

​ 另外我们可以按照 分区维度对消息进行消费,通过ConsumerRecords.records(TopicPartiton)方法实现。


ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    Set<TopicPartition> partitions = records.partitions();
    for (TopicPartition tp : partitions) {
        for (ConsumerRecord<String, String> record : records.records(tp)) {
              System.out.println(record.partition() + " ," + record.value());
        }
    }

​ 另外还可以按照主题维度对消息进行消费,通过ConsumerRecords.records(Topic)实现。


for (String topic : topicList) {
      for (ConsumerRecord<String, String> record : records.records(topic)) {
              System.out.println(record.partition() + " ," + record.value());
      }
}

1.2.4 消费者位移提交

​ 首先要 明白一点,消费者位移是要做持久化处理的,否则当发生消费者崩溃或者消费者重平衡时,消费者消费位移无法获得。旧消费者客户端是将位移提交到zookeeper上,新消费者客户端将位移存储在Kafka内部主题_consumer_offsets中。

​ KafkaConsumer提供了两个方法position(TopicPatition)和commited(TopicPartition)。

​ public long position(TopicPartition partition)-----获得下一次拉取数据的偏移量

​ public OffsetAndMetadata committed(TopicPartition partition)-----给定分区的最后一次提交的偏移量。

还有一个概念称之为lastConsumedOffset,这个指的是最后一次消费的偏移量。

​ 在kafka提交方式有两种:自动提交和手动提交。

(1)自动位移提交

​ kafka默认情况下采用自动提交,enable.auto.commit的默认值为true。当然自动提交并不是没消费一次消息就进行提交,而是定期提交,这个定期的周期时间由auto.commit.intervals.ms参数进行配置,默认值为5s,当然这个参数生效的前提就是开启自动提交。

​ 自动提交会造成重复消费和消息丢失的情况。重复消费很容易理解,因为自动提交实际是延迟提交,因此很容易造成重复消费,然后 消息丢失是怎么产生的?

(2)手动位移提交

​ 开始手动提交的需要配置enable.auto.commit=false。手动提交消费者偏移量,又可分为同步提交和异步提交。

​ 同步提交:

​ 同步提交很简单,调用commitSync() 方法:


while (isRunning.get()) {
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
        for (ConsumerRecord<String, String> record : records) {
            //consume message
            consumer.commitSync();
        }
}

​ 这样,每消费一条消息,提交一个偏移量。当然可用过缓存消息的方式,实现批量处理+批量提交:


while (isRunning.get()) {
      ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
      for (ConsumerRecord<String, String> record : records) {
            buffer.add(record);
      }
      if (buffer.size() >= minBaches) {
          for (ConsumerRecord<String, String> record : records) {
              //consume message
          }
          consumer.commitSync();
          buffer.clear();
      }
}

​ 还可以通过public void commitSync(final Map<TopicPartition, OffsetAndMetadata>offsets)这个方法实现按照 分区粒度进行同步提交。


while (isRunning.get()) {
  ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
  for (TopicPartition tp : records.partitions()) {
      List<ConsumerRecord<String, String>> partitionRecords = records.records(tp);
      for (ConsumerRecord record : partitionRecords) {
          //consume message
      }
      long lastConsumerOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
      consumer.commitSync(Collections.singletonMap(tp,new                                                               OffsetAndMetadata(lastConsumerOffset+1)));
  }
}

​ 异步提交:

​ commitAsync异步提交的时候消费者线程不会被阻塞,即可能在提交偏移量的结果还未返回之前,就开始了新一次的拉取数据操作。异步提交可以提升消费者的性能。commitAsync有三个重载:

​ public void commitAsync()

​ public void commitAsync(OffsetCommitCallback callback)

​ public void commitAsync(final Map<TopicPartition, OffsetAndMetadata>offsets, OffsetCommitCallback )

​ 对照同步提交的方法参数,多了一个Callback回调参数,它提供了一个异步提交的回调方法,当消费者位移提交完成后回调OffsetCommitCallback的onComplement方法。以第二个方法为例:


ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
  for (ConsumerRecord<String, String> record : records) {
      //consume message
  }
  consumer.commitAsync(new OffsetCommitCallback() {
      @Override
      public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e) {
          if (e == null) {
              System.out.println(offsets);
          }else {
                e.printStackTrace();
          }
      }
});

1.2.5 控制和关闭消费

​ kafkaConsumer提供了pause()和resume() 方法分别实现暂停某些分区在拉取操作时返回数据给客户端和恢复某些分区向客户端返回数据的操作:

​ public void pause(Collection<TopicPartition>partitions)

​ public void resume(Collection<TopicPartition>partitions)

​ 优雅停止KafkaConsumer退出消费者循环的方式:

​ (1)不要使用while(true),而是使用while(isRunning.get()),isRunning是一个AtomicBoolean类型,可以在其他地方调用isRunning.set(false)方法退出循环。

​ (2)调用consumer.wakup()方法,wakeup方法是KafkaConsumer中唯一一个可以从其他线程里安全调用的方法,会抛出WakeupException,我们不需要处理这个异常。

​ 跳出循环后一定要显示的执行关闭动作和释放资源。

1.2.6 指定位移消费

KafkaConsumer可通过两种方式实现实现不同粒度的指定位移消费。第一种是通过auto.offset.reset参数,另一种通过一个重要的方法seek。

(1)auto.offset.reset

auto.offset.reset这个参数总共有三种可配置的值:latest、earliest、none。如果配置不在这三个值当中,就会抛出ConfigException。

latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset或位移越界时,消费新产生的该分区下的数据

earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset或位移越界时,从头开始消费

none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset或位移越界,则抛出NoOffsetForPartitionException异常

消息的消费是通过poll方法进行的,poll方法对于开发者来说就是一个黑盒,无法精确的掌控消费的起始位置。即使通过auto.offsets.reset参数也只能在找不到位移或者位移越界的情况下粗粒度的从头开始或者从末尾开始。因此,Kafka提供了另一种更细粒度的消费掌控:seek。

(2)seek

seek可以实现追前消费和回溯消费:


public void seek(TopicPartition partition, long offset)

可以通过seek方法实现指定分区的消费位移的控制。需要注意的一点是,seek方法只能重置消费者分配到的分区的偏移量,而分区的分配是在poll方法中实现的。因此在执行seek方法之前需要先执行一次poll方法获取消费者分配到的分区,但是并不是每次poll方法都能获得数据,所以可以采用如下的方法。


consumer.subscribe(topicList);
  Set<TopicPartition> assignment = new HashSet<>();
  while(assignment.size() == 0) {
      consumer.poll(Duration.ofMillis(100));
      assignment = consumer.assignment();//获取消费者分配到的分区,没有获取返回一个空集合
  }

  for (TopicPartition tp : assignment) {
      consumer.seek(tp, 10); //重置指定分区的位移
  }
  while (true) {
      ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
      //consume record
    }

如果对未分配到的分区执行了seek方法,那么会报出IllegalStateException异常。

在前面我们已经提到,使用auto.offsets.reset参数时,只有当消费者分配到的分区没有提交的位移或者位移越界时,才能从earliest消费或者从latest消费。seek方法可以弥补这一中情况,实现任意情况的从头或从尾部消费。

Set<TopicPartition> assignment = new HashSet<>();      
  while(assignment.size() == 0) {
      consumer.poll(Duration.ofMillis(100));
      assignment = consumer.assignment();
  }
  Map<TopicPartition, Long> offsets = consumer.endOffsets(assignment);//获取指定分区的末尾位置
  for (TopicPartition tp : assignment) {
      consumer.seek;
  }

与endOffset对应的方法是beginningOffset方法,可以获取指定分区的起始位置。其实kafka已经提供了一个从头和从尾消费的方法。


public void seekToBeginning(Collection<TopicPartition> partitions)
public void seekToEnd(Collection<TopicPartition> partitions)

还有一种场景是这样的,我们并不知道特定的消费位置,却知道一个相关的时间点。为解决这种场景遇到的问题,kafka提供了一个offsetsForTimes()方法,通过时间戳来查询分区消费的位移。

   Map<TopicPartition, Long> timestampToSearch = new HashMap<>();      
  for (TopicPartition tp : assignment) {
      timestampToSearch.put(tp, System.currentTimeMillis() - 24 * 3600 * 1000);
  }
//获得指定分区指定时间点的消费位移
  Map<TopicPartition, OffsetAndTimestamp> offsets =                                                                                   consumer.offsetsForTimes(timestampToSearch);
  for (TopicPartition tp : assignment) {
      OffsetAndTimestamp offsetAndTimestamp = offsets.get(tp);
      if (offsetAndTimestamp != null) {
              consumer.seek(tp, offsetAndTimestamp.offset());
      }
  }

由于seek方法的存在,使得消费者的消费位移可以存储在 任意的存储介质中,包括DB、文件系统等。

1.2.7 消费者的再均衡

再均衡是指分区的所属权 从一个消费者转移到另一消费者的行为,它为消费者组具备 高可用伸缩性提高保障。不过需要注意的地方有两点,第一是消费者发生再均衡期间,消费者组中的消费者是 无法读取消息的。第二点就是消费者发生再均衡可能会引起 重复消费问题,所以一般情况下要尽量避免不必要的再均衡。

KafkaConsumer的subscribe方法中有一个参数为ConsumerRebalanceListener,我们称之为再均衡监听器,它可以用来在设置发生再均衡动作前后的一些准备和收尾动作。

public interface ConsumerRebalanceListener {      
  void onPartitionsRevoked(Collection<TopicPartition> partitions);
  void onPartitionsAssigned(Collection<TopicPartition> partitions);
}

onPartitionsRevoked方法会在再均衡之前和消费者停止读取消息之后被调用。可以通过这个回调函数来处理消费位移的提交,以避免重复消费。参数partitions表示再均衡前分配到的分区。

onPartitionsAssigned方法会在再均衡之后和消费者消费之间进行调用。参数partitons表示再均衡之后所分配到的分区。

consumer.subscribe(topicList);      
  Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
  consumer.subscribe(topicList, new ConsumerRebalanceListener() {
      @Override
      public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
          consumer.commitSync(currentOffsets);//提交偏移量
      }

      @Override
      public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
          //do something
      }
  });

  try {
      while (isRunning.get()) {
          ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
          for (ConsumerRecord<String, String> record : records) {
              //process records
              //记录当前的偏移量
              currentOffsets.put(new TopicPartition(record.topic(), record.partition()),new                               OffsetAndMetadata( record.offset() + 1));
          }
          consumer.commitAsync(currentOffsets, null);
      }

      } catch (Exception e) {
          e.printStackTrace();
      }finally {
          consumer.close();
      }

1.2.8 消费者拦截器

消费者拦截器主要是在消费到消息或者提交消费位移时进行一些定制化的操作。消费者拦截器需要自定义实现org.apache.kafka.clients.consumer.ConsumerInterceptor接口。

public interface ConsumerInterceptor<K, V> extends Configurable {          
  public ConsumerRecords<K, V> onConsume(ConsumerRecords<K, V> records);
  public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets);
  public void close();
}

onConsume方法是在poll()方法返回之前被调用,比如修改消息的内容、过滤消息等。如果onConsume方法发生异常,异常会被捕获并记录到日志中,但是不会向上传递。

Kafka会在提交位移之后调用拦截器的onCommit方法,可以使用这个方法来记录和跟踪消费的位移信息。


public class ConsumerInterceptorTTL implements ConsumerInterceptor<String,String> {
  private static final long EXPIRE_INTERVAL = 10 * 1000; //10秒过期
  @Override
  public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
      long now = System.currentTimeMillis();
      Map<TopicPartition, List<ConsumerRecord<String, String>>> newRecords = new HashMap<>();

      for (TopicPartition tp : records.partitions()) {
          List<ConsumerRecord<String, String>> tpRecords = records.records(tp);
          List<ConsumerRecord<String, String>> newTpRecords = records.records(tp);
          for (ConsumerRecord<String, String> record : tpRecords) {
              if (now - record.timestamp() < EXPIRE_INTERVAL) {//判断是否超时
                  newTpRecords.add(record);
              }
          }
          if (!newRecords.isEmpty()) {
              newRecords.put(tp, newTpRecords);
          }


      }
      return new ConsumerRecords<>(newRecords);
  }

  @Override
  public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
      offsets.forEach((tp,offset) -> {
          System.out.println(tp + ":" + offset.offset());
      });
  }

  @Override
  public void close() {}

  @Override
  public void configure(Map<String, ?> configs) {}
}

使用这种TTL需要注意的是如果采用带参数的位移提交方式,有可能提交了错误的位移,可能poll拉取的最大位移已经被拦截器过滤掉。

1.2.9 消费者的多线程实现

KafkaProducer是线程安全的,然而KafkaConsumer是非线程安全的。KafkaConsumer中的acquire方法用于检测当前是否只有一个线程在操作,如果有就会抛出ConcurrentModifiedException。acuqire方法和我们通常所说的锁是不同的,它不会阻塞线程,我们可以把它看做是一个轻量级的锁,它通过线程操作计数标记的方式来检测是否发生了并发操作。acquire方法和release方法成对出现,分表表示加锁和解锁。

//标记当前正在操作consumer的线程      
private final AtomicLong currentThread = new AtomicLong(NO_CURRENT_THREAD);
//refcount is used to allow reentrant access by the thread who has acquired currentThread,
//大概可以理解我加锁的次数
private final AtomicInteger refcount = new AtomicInteger(0);
private void acquire() {
long threadId = Thread.currentThread().getId();
if (threadId != currentThread.get()&&!currentThread.compareAndSet(NO_CURRENT_THREAD, threadId))
      throw new ConcurrentModificationException("KafkaConsumer is not safe for multi-threaded access");
      refcount.incrementAndGet();
}

private void release() {
  if (refcount.decrementAndGet() == 0)
      currentThread.set(NO_CURRENT_THREAD);
}

kafkaConsumer中的每个共有方法在调用之前都会执行aquire方法,只有wakeup方法是个意外。

KafkaConsumer的非线程安全并不意味着消费消息的时候只能以单线程的方式执行。可以通过多种方式实现多线程消费。

(1)Kafka多线程消费第一种实现方式--------线程封锁

所谓线程封锁,就是为每个线程实例化一个KafkaConsumer对象。这种方式一个线程对应一个KafkaConsumer,一个线程(可就是一个consumer)可以消费一个或多个分区的消息。这种消费方式的并发度受限于分区的实际数量。当线程数量超过分分区数量时,就会出现线程限制额的情况。

import org.apache.kafka.clients.consumer.ConsumerConfig;      
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;

public class FirstMutiConsumerDemo {
  public static final String brokerList="node112:9092,node113:9092,node114:9092";
  public static final String topic = "topic-demo";
  public static final String groupId = "group.demo";

  public static Properties initConfig() {
      Properties prop = new Properties();
      prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
      prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
      prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
      prop.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
      prop.put(ConsumerConfig.CLIENT_DNS_LOOKUP_CONFIG, "consumer.client.di.demo");
      prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
      return prop;
  }

  public static void main(String[] args) {
      Properties prop = initConfig();
      int consumerThreadNum = 4;
      for (int i = 0; i < 4; i++) {
          new KafkaCoosumerThread(prop, topic).run();
      }
  }

  public static class KafkaCoosumerThread extends Thread {
  //每个消费者线程包含一个KakfaConsumer对象。
      private KafkaConsumer<String, String> kafkaConsumer;
      public KafkaCoosumerThread(Properties prop, String topic) {
          this.kafkaConsumer = new KafkaConsumer<String, String>(prop);
          this.kafkaConsumer.subscribe(Arrays.asList(topic));
      }

      @Override
      public void run() {
          try {
              while (true) {
                  ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofMillis(100));
                  for (ConsumerRecord<String, String> record : records) {
                      //处理消息模块
                  }
              }
          } catch (Exception e) {
              e.printStackTrace();
          }finally {
              kafkaConsumer.close();
          }
      }
  }
}

这种实现方式和开启多个消费进程的方式没有本质的区别,优点是每个线程可以按照顺序消费消费各个分区的消息。缺点是每个消费线程都要维护一个独立的TCP连接,如果分区数和线程数都很多,那么会造成不小的系统开销。

(2)Kafka多线程消费第二种实现方式--------多个消费线程同时消费同一分区

多个线程同时消费同一分区,通过assign方法和seek方法实现。这样就可以打破原有消费线程个数不能超过分区数的限制,进一步提高了消费的能力,但是这种方式对于位移提交和顺序控制的处理就会变得非常复杂。实际生产中很少使用。

(3)第三种实现方式-------创建一个消费者,records的处理使用多线程实现

一般而言,消费者通过poll拉取数据的速度相当快,而整体消费能力的瓶颈也正式在消息处理这一块。基于此

考虑第三种实现方式。

import org.apache.kafka.clients.consumer.ConsumerConfig;      
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThirdMutiConsumerThreadDemo {
  public static final String brokerList="node112:9092,node113:9092,node114:9092";
  public static final String topic = "topic-demo";
  public static final String groupId = "group.demo";

  public static Properties initConfig() {
      Properties prop = new Properties();
      prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
      prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
      prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
      prop.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
      prop.put(ConsumerConfig.CLIENT_DNS_LOOKUP_CONFIG, "consumer.client.di.demo");
      prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
      return prop;
  }

  public static void main(String[] args) {
      Properties prop = initConfig();
      KafkaConsumerThread consumerThread = new KafkaConsumerThread(prop, topic, Runtime.getRuntime().availableProcessors());
      consumerThread.start();
  }


  public static class KafkaConsumerThread extends Thread {
      private KafkaConsumer<String, String> kafkaConsumer;
      private ExecutorService executorService;
      private int threadNum;

      public KafkaConsumerThread(Properties prop, String topic, int threadNum) {
          this.kafkaConsumer = new KafkaConsumer<String, String>(prop);
          kafkaConsumer.subscribe(Arrays.asList(topic));
          this.threadNum = threadNum;
          executorService = new ThreadPoolExecutor(threadNum, threadNum, 0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
      }

      @Override
      public void run() {
          try {
              while (true) {
                  ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofMillis(100));
                  if (!records.isEmpty()) {
                      executorService.submit(new RecordHandler(records));
                  }
              }
          } catch (Exception e) {
              e.printStackTrace();
          }finally {
              kafkaConsumer.close();
          }
      }
  }

  public static class RecordHandler implements Runnable {
      public final ConsumerRecords<String,String> records;
      public RecordHandler(ConsumerRecords<String, String> records) {
          this.records = records;
      }
       
      @Override
      public void run() {
          //处理records
      }
  }
}

KafkaConsumerThread类对应一个消费者线程,里面通过线程池的方式调用RecordHandler处理一批批的消息。其中线程池采用的拒绝策略为CallerRunsPolicy,当阻塞队列填满时,由调用线程处理该任务,以防止总体的消费能力跟不上poll拉取的速度。这种方式还可以进行横向扩展,通过创建多个KafkaConsumerThread实例来进一步提升整体的消费能力。

这种方式还可以减少TCP连接的数量,但是对于消息的顺序处理就变得困难了。这种方式需要引入一个共享变量Map<TopicPartition,OffsetAndMetadata>offsets参与消费者的偏移量提交。每一个RecordHandler类在处理完消息后都将对应的消费位移保存到共享变量offsets中,KafkaConsumerThread在每一次poll()方法之后都要进读取offsets中的内容并对其进行提交。对于offsets的读写要采用加锁处理,防止出现并发问题。并且在写入offsets的时候需要注意位移覆盖的问题。针对这个问题,可以将RecordHandler的run方法做如下改变:

public void run() {      
          for (TopicPartition tp : records.partitions()) {
              List<ConsumerRecord<String, String>> tpRecords = this.records.records(tp);
              //处理tpRecords
              long lastConsumedOffset = tpRecords.get(tpRecords.size() - 1).offset();
              synchronized (offsets) {
                  if (offsets.containsKey(tp)) {
                      offsets.put(tp, new OffsetAndMetadata(lastConsumedOffset + 1));
                  }else {
                      long positioin = offsets.get(tp).offset();
                      if(positioin < lastConsumedOffset + 1) {
                      offsets.put(tp, new OffsetAndMetadata(lastConsumedOffset + 1));
                      }
                  }
              }
          }
      }

对应的位移提交代码也应该在KafkaConsumerThread的run方法中进行体现

public void run() {      
  try {
      while (true) {
          ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofMillis(100));
          if (!records.isEmpty()) {
              executorService.submit(new RecordHandler(records));
              synchronized (offsets) {
                  if (!offsets.isEmpty()) {
                      kafkaConsumer.commitSync(offsets);
                      offsets.clear();
                    }
              }
          }
      }
  } catch (Exception e) {
      e.printStackTrace();
    }finally {
        kafkaConsumer.close();
      }
    }
}

其实这种方式并不完美,可能造成数据丢失。可以通过更为复杂的滑动窗口的方式进行改进。

1.2.10 消费者重要参数

  • fetch.min.bytes

    kafkaConsumer一次拉拉取请求的最小数据量。适当增加,会提高吞吐量,但会造成额外延迟。

  • fetch.max.bytes

    kafkaConsumer一次拉拉取请求的最大数据量,如果kafka一条消息的大小超过这个值,仍然是可以拉取的。

  • fetch.max.wait.ms

    一次拉取的最长等待时间,配合fetch.min.bytes使用

  • max.partiton.fetch.bytes

    每个分区里返回consumer的最大数据量。

  • max.poll.records

    一次拉取的最大消息数

  • connection.max.idle.ms

    多久之后关闭限制的连接

  • exclude.internal.topics

    这个参数用于设置kafka中的两个内部主题能否被公开: consumer_offsets和transaction_state。如果设为true,可以使用Pattren订阅内部主题,如果是false,则没有这种限制。

  • receive.buffer.bytes

    socket接收缓冲区的大小

  • send.buffer.bytes

    socket发送缓冲区的大小

  • request.timeout.ms

    consumer等待请求响应的最长时间。

  • reconnect.backoff.ms

    重试连接指定主机的等待时间。

  • max.poll.interval.ms

    配置消费者等待拉取时间的最大值,如果超过这个期限,消费者组将剔除该消费者,进行再平衡。

  • auto.offset.reset

    自动偏移量重置

  • enable.auto.commit

    是否允许偏移量的自动提交

  • auto.commit.interval.ms

    自动偏移量提交的时间间隔

保证分布式系统数据一致性的6种方案 - 左正 - 博客园

$
0
0

编者按:本文由「高可用架构后花园」群讨论整理而成。

有人的地方,就有江湖

有江湖的地方,就有纷争

问题的起源

在电商等业务中,系统一般由多个独立的服务组成,如何解决分布式调用时候数据的一致性? 

具体业务场景如下,比如一个业务操作,如果同时调用服务 A、B、C,需要满足要么同时成功;要么同时失败。A、B、C 可能是多个不同部门开发、部署在不同服务器上的远程服务。

在分布式系统来说,如果不想牺牲一致性,CAP 理论告诉我们只能放弃可用性,这显然不能接受。为了便于讨论问题,先简单介绍下数据一致性的基础理论。

强一致

当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。

弱一致性

系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。

最终一致性

弱一致性的特定形式。系统保证在没有后续更新的前提下,系统 最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。

在工程实践上,为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。但在电商等场景中,对于数据一致性的解决方法和常见的互联网系统(如 MySQL 主从同步)又有一定区别,群友的讨论分成以下 6 种解决方案。

1. 规避分布式事务——业务整合

业务整合方案主要采用将接口整合到本地执行的方法。拿问题场景来说,则可以将服务 A、B、C 整合为一个服务 D 给业务,这个服务 D 再通过转换为本地事务的方式,比如服务 D 包含本地服务和服务 E,而服务 E 是本地服务 A ~ C 的整合。

优点:解决(规避)了分布式事务。

缺点:显而易见,把本来规划拆分好的业务,又耦合到了一起,业务职责不清晰,不利于维护。

由于这个方法存在明显缺点,通常不建议使用。

2. 经典方案 - eBay 模式

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

消息日志方案的核心是保证服务接口的幂等性。

考虑到网络通讯失败、数据丢包等原因,如果接口不能保证幂等性,数据的唯一性将很难保证。

eBay 方式的主要思路如下。

Base:一种 Acid 的替代方案

此方案是 eBay 的架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章,是一篇解释 BASE 原则,或者说最终一致性的经典文章。文中讨论了 BASE 与 ACID 原则在保证数据一致性的基本差异。

如果 ACID 为分区的数据库提供一致性的选择,那么如何实现可用性呢?答案是

BASE (basically available, soft state, eventually consistent)

BASE 的可用性是通过 支持局部故障而不是系统全局故障来实现的。下面是一个简单的例子:如果将用户分区在 5 个数据库服务器上,BASE 设计鼓励类似的处理方式,一个用户数据库的故障只影响这台特定主机那 20% 的用户。这里不涉及任何魔法,不过它确实可以带来更高的可感知的系统可用性。

文章中描述了一个最常见的场景,如果产生了一笔交易,需要在交易表增加记录,同时还要修改用户表的金额。这两个表属于不同的远程服务,所以就涉及到分布式事务一致性的问题。

 

 

文中提出了一个经典的解决方法,将主要修改操作以及更新用户表的消息 放在一个本地事务来完成。同时为了避免重复消费用户表消息带来的问题,达到多次重试的幂等性, 增加一个更新记录表 updates_applied 来记录已经处理过的消息。

 

 

系统的执行伪代码如下

 

 

(点击可全屏缩放图片)

基于以上方法,在第一阶段,通过本地的数据库的事务保障,增加了 transaction 表及消息队列 。

在第二阶段,分别读出消息队列(但不删除),通过判断更新记录表 updates_applied 来检测相关记录是否被执行,未被执行的记录会修改 user 表,然后增加一条操作记录到 updates_applied,事务执行成功之后再删除队列。

通过以上方法,达到了分布式系统的最终一致性。进一步了解 eBay 的方案可以参考文末链接。

3. 去哪儿网分布式事务方案

随着业务规模不断地扩大,电商网站一般都要面临拆分之路。就是将原来一个单体应用拆分成多个不同职责的子系统。比如以前可能将面向用户、客户和运营的功能都放在一个系统里,现在拆分为订单中心、代理商管理、运营系统、报价中心、库存管理等多个子系统。

拆分首先要面临的是什么呢?

最开始的单体应用所有功能都在一起,存储也在一起。比如运营要取消某个订单,那直接去更新订单表状态,然后更新库存表就 ok 了。因为是单体应用,库在一起,这些都可以在一个事务里,由关系数据库来保证一致性。

但拆分之后就不同了,不同的子系统都有自己的存储。比如订单中心就只管理自己的订单库,而库存管理也有自己的库。那么运营系统取消订单的时候就是通过接口调用等方式来调用订单中心和库存管理的服务了,而不是直接去操作库。这就涉及一个『 分布式事务』的问题。 

 

 

分布式事务有两种解决方式

1. 优先使用异步消息。

上文已经说过,使用异步消息 Consumer 端需要实现幂等。

幂等有两种方式, 一种方式是业务逻辑保证幂等。比如接到支付成功的消息订单状态变成支付完成,如果当前状态是支付完成,则再收到一个支付成功的消息则说明消息重复了,直接作为消息成功处理。

另外一种方式如果业务逻辑无法保证幂等,则要增加一个去重表或者类似的实现。对于 producer 端在业务数据库的同实例上放一个消息库,发消息和业务操作在同一个本地事务里。发消息的时候消息并不立即发出,而是向消息库插入一条消息记录,然后在事务提交的时候再异步将消息发出,发送消息如果成功则将消息库里的消息删除,如果遇到消息队列服务异常或网络问题,消息没有成功发出那么消息就留在这里了,会有另外一个服务不断地将这些消息扫出重新发送。

2. 有的业务不适合异步消息的方式,事务的各个参与方都需要同步的得到结果。这种情况的实现方式其实和上面类似,每个参与方的本地业务库的同实例上面放一个事务记录库。

比如 A 同步调用 B,C。A 本地事务成功的时候更新本地事务记录状态,B 和 C 同样。如果有一次 A 调用 B 失败了,这个失败可能是 B 真的失败了,也可能是调用超时,实际 B 成功。则由一个中心服务对比三方的事务记录表,做一个最终决定。假设现在三方的事务记录是 A 成功,B 失败,C 成功。那么最终决定有两种方式,根据具体场景:

  1. 重试 B,直到 B 成功,事务记录表里记录了各项调用参数等信息;

  2. 执行 A 和 B 的补偿操作(一种可行的补偿方式是回滚)。

对 b 场景做一个特殊说明:比如 B 是扣库存服务,在第一次调用的时候因为某种原因失败了,但是重试的时候库存已经变为 0,无法重试成功,这个时候只有回滚 A 和 C 了。

那么可能有人觉得在业务库的同实例里放消息库或事务记录库,会对业务侵入,业务还要关心这个库,是否一个合理的设计?

实际上可以依靠运维的手段来简化开发的侵入,我们的方法是让 DBA 在公司所有 MySQL 实例上预初始化这个库,通过框架层(消息的客户端或事务 RPC 框架)透明的在背后操作这个库,业务开发人员只需要关心自己的业务逻辑,不需要直接访问这个库。

总结起来,其实两种方式的根本原理是类似的,也就是 将分布式事务转换为多个本地事务,然后依靠重试等方式达到最终一致性

4. 蘑菇街交易创建过程中的分布式一致性方案

交易创建的一般性流程

我们把交易创建流程抽象出一系列可扩展的功能点,每个功能点都可以有多个实现(具体的实现之间有组合/互斥关系)。把各个功能点按照一定流程串起来,就完成了交易创建的过程。 

 

 

面临的问题

每个功能点的实现都可能会依赖外部服务。那么如何保证各个服务之间的数据是一致的呢?比如锁定优惠券服务调用超时了,不能确定到底有没有锁券成功,该如何处理?再比如锁券成功了,但是扣减库存失败了,该如何处理?

方案选型

服务依赖过多,会带来管理复杂性增加和稳定性风险增大的问题。试想如果我们强依赖 10 个服务,9 个都执行成功了,最后一个执行失败了,那么是不是前面 9 个都要回滚掉?这个成本还是非常高的。

所以在拆分大的流程为多个小的本地事务的前提下,对于非实时、非强一致性的关联业务写入,在本地事务执行成功后,我们选择发消息通知、关联事务异步化执行的方案。

消息通知往往不能保证 100% 成功;且消息通知后,接收方业务是否能执行成功还是未知数。前者问题可以通过重试解决;后者可以选用事务消息来保证。

但是事务消息框架本身会给业务代码带来侵入性和复杂性,所以我们选择 基于 DB 事件变化通知到 MQ 的方式做系统间解耦,通过订阅方消费 MQ 消息时的 ACK 机制,保证消息一定消费成功,达到最终一致性。由于消息可能会被重发,消息订阅方业务逻辑处理要做好幂等保证。

所以目前只剩下需要实时同步做、有强一致性要求的业务场景了。在交易创建过程中,锁券和扣减库存是这样的两个典型场景。

要保证多个系统间数据一致,乍一看,必须要引入分布式事务框架才能解决。但引入非常重的类似二阶段提交分布式事务框架会带来复杂性的急剧上升;在电商领域,绝对的强一致是过于理想化的,我们可以选择准实时的最终一致性。

我们在交易创建流程中, 首先创建一个不可见订单,然后在同步调用锁券和扣减库存时,针对调用异常(失败或者超时),发出废单消息到MQ。如果消息发送失败,本地会做时间阶梯式的异步重试;优惠券系统和库存系统收到消息后,会进行判断是否需要做业务回滚,这样就准实时地保证了多个本地事务的最终一致性。

 

 

 5. 支付宝及蚂蚁金融云的分布式服务  DTS 方案

业界常用的还有支付宝的一种 xts 方案,由支付宝在 2PC 的基础上改进而来。主要思路如下,大部分信息引用自官方网站。

分布式事务服务简介

分布式事务服务 (Distributed Transaction Service, DTS) 是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。DTS 从架构上分为 xts-client 和 xts-server 两部分,前者是一个嵌入客户端应用的 JAR 包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复。

核心特性

传统关系型数据库的事务模型必须遵守 ACID 原则。在单数据库模式下,ACID 模型能有效保障数据的完整性,但是在大规模分布式环境下,一个业务往往会跨越多个数据库,如何保证这多个数据库之间的数据一致性,需要其他行之有效的策略。在 JavaEE 规范中使用 2PC (2 Phase Commit, 两阶段提交) 来处理跨 DB 环境下的事务问题,但是 2PC 是反可伸缩模式,也就是说,在事务处理过程中,参与者需要一直持有资源直到整个分布式事务结束。这样,当业务规模达到千万级以上时,2PC 的局限性就越来越明显,系统可伸缩性会变得很差。基于此,我们采用 BASE 的思想实现了一套类似 2PC 的分布式事务方案,这就是 DTS。DTS在充分保障分布式环境下高可用性、高可靠性的同时兼顾数据一致性的要求,其最大的特点是保证数据最终一致 (Eventually consistent)。

简单的说,DTS 框架有如下特性:

  • 最终一致:事务处理过程中,会有短暂不一致的情况,但通过恢复系统,可以让事务的数据达到最终一致的目标。

  • 协议简单:DTS 定义了类似 2PC 的标准两阶段接口,业务系统只需要实现对应的接口就可以使用 DTS 的事务功能。

  • 与 RPC 服务协议无关:在 SOA 架构下,一个或多个 DB 操作往往被包装成一个一个的 Service,Service 与 Service 之间通过 RPC 协议通信。DTS 框架构建在 SOA 架构上,与底层协议无关。

  • 与底层事务实现无关: DTS 是一个抽象的基于 Service 层的概念,与底层事务实现无关,也就是说在 DTS 的范围内,无论是关系型数据库 MySQL,Oracle,还是 KV 存储 MemCache,或者列存数据库 HBase,只要将对其的操作包装成 DTS 的参与者,就可以接入到 DTS 事务范围内。

以下是分布式事务框架的流程图 

 

 


实现

  1. 一个完整的业务活动由一个主业务服务与若干从业务服务组成。

  2. 主业务服务负责发起并完成整个业务活动。

  3. 从业务服务提供 TCC 型业务操作。

  4. 业务活动管理器控制业务活动的一致性,它登记业务活动中的操作,并在活动提交时确认所有的两阶段事务的 confirm 操作,在业务活动取消时调用所有两阶段事务的 cancel 操作。”

与 2PC 协议比较

  1. 没有单独的 Prepare 阶段,降低协议成本

  2. 系统故障容忍度高,恢复简单

6. 农信网数据一致性方案

1. 电商业务

公司的支付部门,通过接入其它第三方支付系统来提供支付服务给业务部门,支付服务是一个基于 Dubbo 的 RPC 服务。

对于业务部门来说,电商部门的订单支付,需要调用

  1. 支付平台的支付接口来处理订单;

  2. 同时需要调用积分中心的接口,按照业务规则,给用户增加积分。

从业务规则上需要同时保证业务数据的实时性和一致性,也就是支付成功必须加积分。

我们采用的方式是同步调用,首先处理本地事务业务。考虑到积分业务比较单一且业务影响低于支付,由积分平台提供增加与回撤接口。

具体的流程是先调用积分平台增加用户积分,再调用支付平台进行支付处理,如果处理失败,catch 方法调用积分平台的回撤方法,将本次处理的积分订单回撤。

 

 

(点击图片可以全屏缩放)

2. 用户信息变更

公司的用户信息,统一由用户中心维护,而用户信息的变更需要同步给各业务子系统,业务子系统再根据变更内容,处理各自业务。用户中心作为 MQ 的 producer,添加通知给 MQ。APP Server 订阅该消息,同步本地数据信息,再处理相关业务比如 APP 退出下线等。

我们采用异步消息通知机制,目前主要使用 ActiveMQ,基于 Virtual Topic 的订阅方式,保证单个业务集群订阅的单次消费。

 

 

总结

分布式服务对衍生的配套系统要求比较多,特别是我们基于消息、日志的最终一致性方案,需要考虑消息的积压、消费情况、监控、报警等。

参考资料

  • Base: An Acid Alternative (eBay 方案)

In partitioned databases, trading some consistency for availability can lead to dramatic improvements in scalability.

英文版 :  http://queue.acm.org/detail.cfm?id=1394128 

中文版:  http://article.yeeyan.org/view/167444/125572

感谢 李玉福、余昭辉、蘑菇街七公提供方案,其他多位群成员对本文内容亦有贡献。

本文编辑李玉福、Tim Yang,转载请注明来自@高可用架构

利用kibana学习 elasticsearch restful api (DSL) - Ruthless - 博客园

$
0
0

利用kibana学习 elasticsearch restful api (DSL)

1、了解elasticsearch基本概念
Index: database
Type: table
Document: row
Filed: field

2、关键字:
PUT 创建索引,eg:PUT /movie_index 新建movie_index索引
GET 用于检索数据,eg:GET movie_index/movie/1
POST 用来修改数据,eg:POST movie_index/movie/3/_update
DELETE 用来删除数据

3、例子
下面通过电影来演示,一部电影有多个演员。
public class Movie {
String id;
//电影名称
String name;
//豆瓣评分
Double doubanScore;
//演员列表
List<Actor> actorList;
}

public class Actor{
String id;
//演员名称
String name;
}

3.1、添加索引
$ PUT /movie_index

3.2、删除索引
$ DELETE /movie_index

3.3、查看所有的索引库
$ GET _cat/indices?v

3.4、新增文档{新增索引库}
添加三部电影

PUT /movie_index/movie/1
{
"id":1,
"name":"operation red sea",
"doubanScore":8.5,
"actorList":[
{"id":1,"name":"zhang yi"},
{"id":2,"name":"hai qing"},
{"id":3,"name":"zhang han yu"}
]
}

PUT /movie_index/movie/2
{
"id":2,
"name":"operation meigong river",
"doubanScore":8.0,
"actorList":[
{"id":3,"name":"zhang han yu"}
]
}

PUT /movie_index/movie/3
{
"id":3,
"name":"incident red sea",
"doubanScore":5.0,
"actorList":[
{"id":4,"name":"liu de hua"}
]
}

3.4、直接用id查找
$ GET movie_index/movie/1
$ GET movie_index/movie/2
$ GET /movie_index/movie/3

3.5、修改——整体替换
和新增没有区别

PUT /movie_index/movie/3
{
"id":"3",
"name":"incident red sea",
"doubanScore":"5.0",
"actorList":[
{"id":"1","name":"zhang guo li 001"}
]
}

可以重新执行,_version一直递增。

3.6、修改——某个字段
POST movie_index/movie/3/_update
{
"doc": {
"doubanScore":"7.0"
}
}

3.7、删除一个document
DELETE movie_index/movie/3

3.8、搜索type全部数据 {select * from tname}
GET movie_index/movie/_search
{
"took": 1, //耗费时间 毫秒
"timed_out": false, //是否超时
"_shards": {
"total": 5, //发送给全部5个分片
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2, //命中2条数据
"max_score": 1, //最大评分
"hits": [ //查询结果
{
"_index": "movie_index",
"_type": "movie",
"_id": "2",
"_score": 1,
"_source": {
"id": 2,
"name": "operation meigong river",
"doubanScore": 8,
"actorList": [
{
"id": 3,
"name": "zhang han yu"
}
]
}
},
.....
]
}
}

3.9、按条件查询(全部)
GET movie_index/movie/_search
{
"query":{
"match_all": {}
}
}

3.10、按分词查询
{select * from tname where name like '%red%'}

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red"}
}
}

3.11、按分词子属性查询
GET movie_index/movie/_search
{
"query":{
"match": {"actorList.name":"zhang"}
}
}

3.12、fuzzy查询

校正匹配分词,当一个单词都无法准确匹配,es通过一种算法对非常接近的单词也给与一定的评分,能够查询出来,但是消耗更多的性能。
GET movie_index/movie/_search
{
"query":{
"fuzzy": {"name":"rad"}
}
}

通过rad可以匹配到red记录,匹配数据相近的记录。

3.13、过滤--查询后过滤
{select o.* from (select * from tname where name like '%red%') o where o.actorList.id=3 }

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red"}
},
"post_filter":{
"term": {
"actorList.id": 3
}
}
}

3.14、过滤--查询前过滤(推荐)
其实准确来说,ES中的查询操作分为2种:查询(query)和过滤(filter)。查询即是之前提到的query查询,它(查询)默认会计算每个返回文档的得分,然后根据得分排序。而过滤(filter)只会筛选出符合的数据,并不计算得分,且它可以缓存文档。所以,单从性能考虑,过滤比查询更快。

换句话说,过滤适合在大范围筛选数据,而查询则适合精确匹配数据。一般应用时,应先使用过滤操作过滤数据,然后使用查询匹配数据。

eg、查询演员ID包含1和3,且电影名称包含red的记录
{select * from tname where actorList.id in (1,3)}

GET movie_index/movie/_search
{
"query": {
"bool": {
"filter": [
{"term": {"actorList.id": "1"}},
{"term": {"actorList.id": "3"}}
]
}
}
}
注意:过滤(filter)只会筛选出符合的数据,并不计算得分,所以返回结果max_score字段永远为0。

{select * from tname where actorList.id in (1,3) and name like '%red%'}
GET movie_index/movie/_search
{
"query": {
//通过bool进行组合查询
"bool": {
//过滤两个条件
"filter": [
{"term": {"actorList.id": "1"}},
{"term": {"actorList.id": "3"}}
],
"must": {
"match": {"name": "red"}
}
}
}
}

3.15、排序
每种数据库都有排序:
Mysql,oracle,sqlserver默认的排序规则是升序,还是降序呢?
Mysql :升序

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
},
"sort": [
{
"doubanScore": {
"order": "desc"
}
}
]
}

3.16、分页查询
GET movie_index/movie/_search
{
"query": { "match_all": {} },
"from": 0,
"size": 1
}

from: 表示从第几条开始查询,默认从0开始
Size:表示每页显示的数据条数

3.17、指定查询的字段
GET movie_index/movie/_search
{
"query": { "match_all": {} },
"_source": ["name", "doubanScore"]
}
注意:_source: 查询结果的hits下面的_source

3.18、高亮
GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
},
"highlight": {
"fields": {"name":{} }
}
}

修改自定义高亮标签
GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
},
"highlight": {
"pre_tags": ["<span>"], //前缀标签
"post_tags": ["</span>"], //后缀标签
"fields": {"name":{} }
}
}

3.19、聚合
相当于 sql 语句中的分组!group by!

取出每个演员共参演了多少部电影
GET movie_index/movie/_search
{
"aggs": {
"groupby_actor": {
"terms": {
"field": "actorList.name.keyword"
}
}
}
}
注意:groupby_actor聚合别名,相当于变量,上下文引用


每个演员参演电影的平均分是多少,并按评分排序
GET movie_index/movie/_search
{
"aggs": {
"groupby_actor_id": {
"terms": {
"field": "actorList.name.keyword" ,
"order": {
"avg_score": "desc"
}
},
"aggs": {
"avg_score":{
"avg": {
"field": "doubanScore"
}
}
}
}
}
}

4、关于mapping
之前说type可以理解为table,那每个字段的数据类型是如何定义的呢

查看看mapping

自定义Type。{自定义表中字段的类型}
以后工作中都是自己定义,不建议不推荐使用 es 中自定的数据类型

GET movie_index/_mapping/movie
实际上每个type中的字段是什么数据类型,由mapping定义。

但是如果没有设定mapping系统会自动,根据一条数据的格式来推断出应该的数据格式。
true/false → boolean
1020 → long
20.1 → double,float
“2018-02-01” → date
“hello world” → text + keyword
默认只有text会进行分词,keyword是不会分词的字符串。

mapping除了自动定义,还可以手动定义,但是只能对新加的、没有数据的字段进行定义。一旦有了数据就无法再做修改了。

5、中文分词
elasticsearch本身自带的中文分词,就是单纯把中文一个字一个字的分开,根本没有词汇的概念。但是实际应用中,用户都是以词汇为条件,进行查询匹配的,如果能够把文章以词汇为单位切分开,那么与用户的查询条件能够更贴切的匹配上,查询速度也更加快速。

分词器下载网址:https://github.com/medcl/elasticsearch-analysis-ik/releases

https://www.cnblogs.com/linjiqin/p/10904876.html


5.1、安装中文分词
下载好的zip包,解压后放到/home/es/elasticsearch-6.2.2/plugins/目录下

注意:/home/es/elasticsearch-6.2.2/为elasticsearch安装所在目录。

$ cd /home/es/elasticsearch-6.2.2/plugins/
$ unzip elasticsearch-analysis-ik-6.2.2.zip

将压缩包文件删除!否则启动失败!
$ rm -rf elasticsearch-analysis-ik-6.2.2.zip

5.2、重启es,查看插件是否安装
$ sudo fuser -k -n tcp 9200
$ cd /home/es/elasticsearch-6.2.2/bin
$ ./elasticsearch &
$ $ curl http://localhost:9200/_cat/plugins
prMkj8M analysis-ik 6.2.2

5.3、测试使用
5.3.1、使用默认
GET movie_index/_analyze
{
"text": "我是中国人"
}
aaa

5.3.2、使用分词器 {简单的分词方式}
GET movie_index/_analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
bbb

5.3.3、另外一个分词器-ik_max_word
GET movie_index/_analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
ccc
能够看出不同的分词器,分词有明显的区别,所以以后定义一个type不能再使用默认的mapping了,要手工建立mapping, 因为要选择分词器。

网络安全系列 之 密钥安全管理 - eaglediao - 博客园

$
0
0

最近涉及到安全相关的知识,这里对安全秘钥管理要点做简单记录:

加密技术 是最常用的安全保密手段,利用技术手段把重要的数据变为乱码(加密)传送,到达目的地后再用相同或不同的手段还原(解密)。

0. 基本概念

加密包括两个元素: 算法密钥。一个加密算法是将消息与密钥(一串数字)结合,产生不可理解的密文的步骤。
密钥是结合密码算法一起使用的参数,拥有它的实体可以加密或恢复数据。

密钥可以分对称密钥和非对称密钥。

对称密钥: 加/解密使用相同密钥。 -- AES等算法
非对称密钥:需要两个密钥来进行加密和解密。(公钥和私钥) -- RSA等算法

1. 密钥分层管理结构

工作密钥 (WK/WorkKey)

密钥加密密钥 (KEK或MK/MasterKey)

根密钥(RK/RootKey)

  • 密钥分层管理至少选择两层结构进行管理:根密钥和工作密钥。

2. 密钥生命周期安全管理

密钥生命周期 由于不良设计可能导致的安全问题
生成: 生成算法随机性差,导致密钥可被预测,或攻击者可以自己生成密钥。
分发: 密钥明文分发,导致密钥存在被攻击者截获的风险。
更新: 密钥从不更新,导致攻击者更容易获取密钥,从而能够轻易获取敏感数据的明文。
存储: 密钥明文存储在数据库中,导致攻击者容易读取出密钥,从而能够轻易获取敏感数据的明文。
备份: 如果重要密钥从不备份,一旦密钥丢失,将导致原有加密的数据不能解密,大大降低了系统可靠性。
销毁: 密钥仅被普通删除,导致攻击者有可能恢复出密钥。

密钥的建立包括密钥的生成和分发。

2.1 生成

  1. 基于安全的随机数发生器
  2. 基于密钥导出函数
    PBKDF2是一个基于口令的密钥导出函数,导出密钥的计算公式: DK = PBKDF2(HashAlg, Password, Salt, count, dkLen) PBKDF2 :密钥导出函数名 输入: HashAlg :哈希算法(推荐使用SHA256) Password :用户输入的口令或者读取的一串字符串 Salt :盐值,为安全随机数,至少为8字节 count :迭代次数,正整数 dkLen :导出密钥的字节长度,正整数。 输出: DK :导出的密钥,长度为dkLen个字节的字符串。
    http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/pkcs/pkcs5/PBKDF2.html
  3. 基于标准的密钥协商机制
  4. 基于安全的密钥生成工具等

2.2 分发

密钥的分发是将密钥通过安全的方式传送到被授权的实体,一般通过安全传输协议或者使用数字信封等方式来完成。

数字信封是对称密码体制和非对称密码体制的一种混合应用,即解决了非对称密码体制加解密效率的问题,又妥善解决了密钥传送的安全问题。

  • 在密钥的分发过程中,对于对称密钥和非对称密钥的私钥而言,应保证其完整性和机密性;

    推荐产品开发人员使用安全的加密传输协议(如SSL、IPSec、SSH)来传输这些敏感数据。
    当产品的应用场景中不具备建立安全传输协议的条件时,也可以使用数字信封来完成密钥的分发。

数字信封加解密接口

接口iPSIOpenSSL
加密CRYPT_sealInit()EVP_SealInit()
加密CRYPT_sealUpdate()EVP_SealUpdate()
加密CRYPT_sealFinal()EVP_SealFinal()
解密CRYPT_openInit()EVP_OpenInit()
解密CRYPT_openUpdate()EVP_OpenUpdate()
解密CRYPT_openFinal()EVP_OpenFinal()

对于非对称密钥的公钥而言,应保证其完整性与真实性。

2.3 使用

  • 一个密钥只用于一个用途(如:加密、认证、随机数生成和数字签名等)。

  • 非对称加密算法私钥仅可被其拥有者掌握。

2.4 存储

  • 用于数据加解密的工作密钥不可硬编码在代码中。

  • 对称密钥、私钥、共享秘密等均属于敏感数据,在本地存储时均需提供机密性保护。

    上层密钥的机密保护由下层密钥提供 --> 根密钥的安全管理。
  • 密钥组件方式生成根密钥时,密钥组件需要分散存储,当密钥组件存储于文件中时,须对文件名做一般化处理。

2.5 更新

当密钥已经达到其使用期限或者密钥已经被破解时,密码系统需要有密钥更新机制来重新产生新的密钥

  • 密钥须支持可更新,并明确更新周期。

2.6 备份

密钥丢失将导致密文数据无法解密,这样便造成了数据的丢失。
应依据具体场景,来评估是否需要对密钥提供备份与恢复机制

2.7 销毁

  • 不再使用的密钥应当立即删除。

2.8 可审核

  • 密钥管理操作需要记录详细日志。

    密钥的生成、使用(作为管理用途,如加解密密钥、派生密钥必须记日志,作为业务用途,如加解密业务数据则不要求)、更新、销毁操作是重要的管理操作。
    日志中需详细记录密钥的各项管理操作,包括但不限于记录操作的主体(人或设备)、时间、目的、结果等可用于事件追溯的信息。

关于密钥管理的几个设计原则
网络安全有赖于密钥管理的有效性,即保证密码的产生,存储,传输和使用的安全性,这就要求对密钥进行有效的管理。



1,任何密码不以明文的方式进行存储,除非是放在足够安全的密码装置内。人工分配的密钥必须以密钥分量方式分别由不同的多个可信任的实体保管,不得直接以明文方式由单个实体掌握,对密码装置的任何操作均无法使得密钥以明文方式出现于密码装置之外;


2,保证密钥的分离性,不同通信实体之间使用不同的密钥,且这些密钥不能存在相关性,即一对通信实体之间的安全通信出现问题,不应引起另一对实体的安全通信,包含这四个实体中有两个实体相同的情况;


3,密钥需要具备一定的备份机制,当系统出现故障导致密钥的丢失,应该能通过对密钥备份的回复,来确保系统是可修复的,但密钥的备份不应该降低密钥管理的安全性。


4,密钥必须具备有效期,当旧密钥过期时,需要及时进行密钥的替换,同时,新密钥安全性和旧密钥的安全性应该分离,即旧密钥即使泄漏也不应该引起新密钥的安全性出现问题.


5,密钥管理需要具备层次性。
       网络要求每次交易的PIN保密.MAC的计算以及其它信息的加密所使用的密钥互不相同,作到一次一密.而为了保证交易的延续性,这些密钥均要由一个实体产生并安全地传输到另一个与之通信的实体, 这就要求通信双方必须共同使用一个加密密钥(KEK),以加密上述的各种工作密钥,KEK不能通过网络进行传输,而只能在系统使用前装入,或者通过两个实体各自分别产生一个相同的密钥作为KEK,有了KEK,便解决了会话密钥的传输问题。



       在网络中,商户、发卡行均需要与为数众多的实体进行安全通信,这就要求系统中具有大量的密钥(会话密钥和KEK),这些密钥无法全部保存在安全密码装置中,因此需要使用主密钥(MFK)对这些密钥加密存储于密码装置之外的主机数据库中。



   一级:MFK,主密钥,存储于密钥装作中,用于加密KEK和SK,以保存在密码装作外。
   二级:KEK,密钥加密密钥,用于SK的加密传送,每对通信实体都有一相同的KEK。
   三级:SK,会话密钥,用于加密PIN,产生MAC和验证MAC等。



6,密钥和密钥属性

       KEK和SK都具有密钥属性,用于功能分离和使用合法性检验,以提高系统的逻辑安全.密钥的属性包含此密钥的层次(标识KEK或SK).使用有效次数.MFK序号.密钥用途和密钥校验值等内容,密钥属性与密钥一起使用,密码装置根据密钥属性校验密钥使用的合法性.控制密钥的误用.密钥校验值由相应密钥值与属性在MFK的加密下产生.密钥属性仅与相应密钥的明文保存在主机中,也仅用于主机安全密码装置,不进行传输。


分库分表(5) ---SpringBoot + ShardingSphere 实现分库分表 - 雨点的名字 - 博客园

$
0
0

分库分表(5)--- ShardingSphere实现分库分表

有关分库分表前面写了四篇博客:

1、 分库分表(1) --- 理论

2、 分库分表(2) --- ShardingSphere(理论)

3、 分库分表(3) ---SpringBoot + ShardingSphere实现读写分离

4、 分库分表(4) ---SpringBoot + ShardingSphere 实现分表

这篇博客通过ShardingSphere 实现分库分表,并在文章最下方附上项目 Github地址

一、项目概述

1、技术架构

项目总体技术选型

SpringBoot2.0.6 + shardingsphere4.0.0-RC1 + Maven3.5.4  + MySQL + lombok(插件)

2、项目说明

场景在实际开发中,如果表的数据过大我们需要把一张表拆分成多张表,也可以垂直切分把一个库拆分成多个库,这里就是通过ShardingSphere实现 分库分表功能。

3、数据库设计

这里有个设计了两个库 ds0ds1,每个库中有两个表 tab_user0tab_user1

如图

ds0库

ds1库

具体的创建表SQL也会放到GitHub项目里


二、核心代码

说明完整的代码会放到GitHub上,这里只放一些核心代码。

1、application.properties

server.port=8084

#指定mybatis信息
mybatis.config-location=classpath:mybatis-config.xml
#打印sql
spring.shardingsphere.props.sql.show=true

spring.shardingsphere.datasource.names=ds0,ds1

spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://47.99.203.55:3306/ds0?characterEncoding=utf-8
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456

spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://47.99.203.55:3306/ds1?characterEncoding=utf-8
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456

#根据年龄分库
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=age
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$->{age % 2}
#根据id分表
spring.shardingsphere.sharding.tables.tab_user.actual-data-nodes=ds$->{0..1}.tab_user$->{0..1}
spring.shardingsphere.sharding.tables.tab_user.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.tab_user.table-strategy.inline.algorithm-expression=tab_user$->{id % 2}

Sharding-JDBC可以通过 JavaYAMLSpring命名空间Spring Boot Starter四种方式配置,开发者可根据场景选择适合的配置方式。具体可以看官网。

2、UserController

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 模拟插入数据
     */
    List<User> userList = Lists.newArrayList();
    /**
     * 初始化插入数据
     */
    @PostConstruct
    private void getData() {
        userList.add(new User(1L,"小小", "女", 3));
        userList.add(new User(2L,"爸爸", "男", 30));
        userList.add(new User(3L,"妈妈", "女", 28));
        userList.add(new User(4L,"爷爷", "男", 64));
        userList.add(new User(5L,"奶奶", "女", 62));
    }
    /**
     * @Description: 批量保存用户
     */
    @PostMapping("save-user")
    public Object saveUser() {
        return userService.insertForeach(userList);
    }
    /**
     * @Description: 获取用户列表
     */
    @GetMapping("list-user")
    public Object listUser() {
        return userService.list();
    }


三、测试验证

1、批量插入数据

请求接口

localhost:8084/save-user

我们可以从商品接口代码中可以看出,它会批量插入5条数据。我们先看控制台输出SQL语句

我们可以从SQL语句可以看出 ds0ds1库中都插入了数据。

我们再来看数据库

ds0.tab_user0

ds0.tab_user1

ds1.tab_user0

ds1.tab_user1

完成分库分表插入数据。

2、获取数据

这里获取列表接口的SQL,这里对SQL做了order排序操作,具体ShardingSphere分表实现order操作的原理可以看上面一篇博客。

select *  from tab_user order by age  <!--根据年龄排序-->

请求接口结果

我们可以看出虽然已经分库分表,但依然可以将多表数据聚合在一起并可以支持按 age排序

注意ShardingSphere并不支持 CASE WHENHAVINGUNION (ALL)有限支持子查询。这个官网有详细说明。

Github地址https://github.com/yudiandemingzi/spring-boot-sharding-sphere


参考

1、 ShardingSphere中文文档

2、 ShardingSphere官网

3、 Shardingsphere Github库




我相信,无论今后的道路多么坎坷,只要抓住今天,迟早会在奋斗中尝到人生的甘甜。抓住人生中的一分一秒,胜过虚度中的一月一年!(20)


Kylin构建Cube过程详解 - XIAO的博客 - 博客园

$
0
0

1 前言

在使用Kylin的时候,最重要的一步就是创建cube的模型定义,即指定度量和维度以及一些附加信息,然后对cube进行build,当然我们也可以根据原始表中的某一个string字段(这个字段的格式必须是日期格式,表示日期的含义)设定分区字段,这样一个cube就可以进行多次build,每一次的build会生成一个segment,每一个segment对应着一个时间区间的cube,这些segment的时间区间是连续并且不重合的,对于拥有多个segment的cube可以执行merge,相当于将一个时间区间内部的segment合并成一个。下面开始分析cube的build过程。

2 Cube示例

以手机销售为例,表SALE记录各手机品牌在各个国家,每年的销售情况。表PHONE是手机品牌,表COUNTRY是国家列表,两表通过外键与SALE表相关联。这三张表就构成星型模型,其中SALE是事实表,PHONE、COUNTRY是维度表。

现在需要知道各品牌手机于2010-2012年,在中国的总销量,那么查询sql为:

SELECT b.`name`, c.`NAME`, SUM(a.count)
FROM SALE AS a 
LEFT JOIN PHONE AS b ON a.`pId`=b.`id` 
LEFT JOIN COUNTRY AS c ON a.`cId`=c.`id` 
WHERE a.`time` >= 2010 AND a.`time` <= 2012 AND c.`NAME` = "中国"
GROUP BY b.`NAME`

其中时间(time), 手机品牌(b.name,后文用phone代替),国家(c.name,后文用country代替)是维度,而销售数量(a.count)是度量。手机品牌的个数可用于表示手机品牌列的基度。各手机品牌在各年各个国家的销量可作为一个cuboid,所有的cuboid组成一个cube,如下图所示:

上图展示了有3个维度的cube,每个小立方体代表一个cuboid,其中存储的是度量列聚合后的结果,比如苹果在中国2010年的销量就是一个cuboid。

3 入口介绍

在kylin的web页面上创建完成一个cube之后可以点击action下拉框执行build或者merge操作,这两个操作都会调用cube的rebuild接口,调用的参数包括:

  1. cube名,用于唯一标识一个cube,在当前的kylin版本中cube名是全局唯一的,而不是每一个project下唯一的;
  2. 本次构建的startTime和endTime,这两个时间区间标识本次构建的segment的数据源只选择这个时间范围内的数据;对于BUILD操作而言,startTime是不需要的,因为它总是会选择最后一个segment的结束时间作为当前segment的起始时间。
  3. buildType标识着操作的类型,可以是”BUILD”、”MERGE”和”REFRESH”。

4 构建Cube过程

Kylin中Cube的Build过程,是将所有的维度组合事先计算,存储于HBase中,以空间换时间,HTable对应的RowKey,就是各种维度组合,指标存在Column中,这样,将不同维度组合查询SQL,转换成基于RowKey的范围扫描,然后对指标进行汇总计算,以实现快速分析查询。整个过程如下图所示:

主要的步骤可以按照顺序分为几个阶段:

  1. 根据用户的cube信息计算出多个cuboid文件;
  2. 根据cuboid文件生成htable;
  3. 更新cube信息;
  4. 回收临时文件。
    每一个阶段操作的输入都需要依赖于上一步的输出,所以这些操作全是顺序执行的。下面对这几个阶段的内容细分为11步具体讲解一下:

4.1 创建Hive事实表中间表(Create Intermediate Flat Hive Table)

这一步的操作会新创建一个hive外部表,然后再根据cube中定义的星状模型,查询出维度和度量的值插入到新创建的表中,这个表是一个外部表,表的数据文件(存储在HDFS)作为下一个子任务的输入。

4.2 重新分配中间表(Redistribute Flat Hive Table)

在前面步骤,hive会在HDFS文件夹中生成数据文件,一些文件非常大,一些有些小,甚至是空的。文件分布不平衡会导致随后的MR作业不平衡:一些mappers作业很快执行完毕,但其它的则非常缓慢。为了平衡作业,kylin增加这一步“重新分配”数据。首先,kylin获取到这中间表的行数,然后根据行数的数量,它会重新分配文件需要的数据量。默认情况下,kylin分配每100万行一个文件。

4.3 提取事实表不同列值 (Extract Fact Table Distinct Columns)

在这一步是根据上一步生成的hive中间表计算出每一个出现在事实表中的维度列的distinct值,并写入到文件中,它是启动一个MR任务完成的,它关联的表就是上一步创建的临时表,如果某一个维度列的distinct值比较大,那么可能导致MR任务执行过程中的OOM。

4.4 创建维度字典(Build Dimension Dictionary)

这一步是根据上一步生成的distinct column文件和维度表计算出所有维度的子典信息,并以字典树的方式压缩编码,生成维度字典,子典是为了节约存储而设计的。
每一个cuboid的成员是一个key-value形式存储在hbase中,key是维度成员的组合,但是一般情况下维度是一些字符串之类的值(例如商品名),所以可以通过将每一个维度值转换成唯一整数而减少内存占用,在从hbase查找出对应的key之后再根据子典获取真正的成员值。

4.5 保存Cuboid的统计信息(Save Cuboid Statistics)

计算和统计所有的维度组合,并保存,其中,每一种维度组合,称为一个Cuboid。理论上来说,一个N维的Cube,便有2的N次方种维度组合,参考网上的一个例子,一个Cube包含time, item, location, supplier四个维度,那么组合(Cuboid)便有16种:

4.6 创建HTable

创建一个HTable的时候还需要考虑一下几个事情:

  1. 列簇的设置。
  2. 每一个列簇的压缩方式。
  3. 部署coprocessor。
  4. HTable中每一个region的大小。
    在这一步中,列簇的设置是根据用户创建cube时候设置的,在HBase中存储的数据key是维度成员的组合,value是对应聚合函数的结果,列簇针对的是value的,一般情况下在创建cube的时候只会设置一个列簇,该列包含所有的聚合函数的结果;
    在创建HTable时默认使用LZO压缩,如果不支持LZO则不进行压缩,在后面kylin的版本中支持更多的压缩方式;
    kylin强依赖于HBase的coprocessor,所以需要在创建HTable为该表部署coprocessor,这个文件会首先上传到HBase所在的HDFS上,然后在表的元信息中关联,这一步很容易出现错误,例如coprocessor找不到了就会导致整个regionServer无法启动,所以需要特别小心;region的划分已经在上一步确定了,所以这里不存在动态扩展的情况,所以kylin创建HTable使用的接口如下:
    public void createTable(final HTableDescriptor desc , byte [][] splitKeys)

4.7 用Spark引擎构建Cube(Build Cube with Spark)

在Kylin的Cube模型中,每一个cube是由多个cuboid组成的,理论上有N个普通维度的cube可以是由2的N次方个cuboid组成的,那么我们可以计算出最底层的cuboid,也就是包含全部维度的cuboid(相当于执行一个group by全部维度列的查询),然后在根据最底层的cuboid一层一层的向上计算,直到计算出最顶层的cuboid(相当于执行了一个不带group by的查询),其实这个阶段kylin的执行原理就是这个样子的,不过它需要将这些抽象成mapreduce模型,提交Spark作业执行。
使用Spark,生成每一种维度组合(Cuboid)的数据。
Build Base Cuboid Data;
Build N-Dimension Cuboid Data : 7-Dimension;
Build N-Dimension Cuboid Data : 6-Dimension;
……
Build N-Dimension Cuboid Data : 2-Dimension;
Build Cube。

4.8 将Cuboid数据转换成HFile(Convert Cuboid Data to HFile)

创建完了HTable之后一般会通过插入接口将数据插入到表中,但是由于cuboid中的数据量巨大,频繁的插入会对Hbase的性能有非常大的影响,所以kylin采取了首先将cuboid文件转换成HTable格式的Hfile文件,然后在通过bulkLoad的方式将文件和HTable进行关联,这样可以大大降低Hbase的负载,这个过程通过一个MR任务完成。

4.9 导HFile入HBase表(Load HFile to HBase Table)

将HFile文件load到HTable中,这一步完全依赖于HBase的工具。这一步完成之后,数据已经存储到HBase中了,key的格式由cuboid编号+每一个成员在字典树的id组成,value可能保存在多个列组里,包含在原始数据中按照这几个成员进行GROUP BY计算出的度量的值。

4.10 更新Cube信息(Update Cube Info)

更新cube的状态,其中需要更新的包括cube是否可用、以及本次构建的数据统计,包括构建完成的时间,输入的record数目,输入数据的大小,保存到Hbase中数据的大小等,并将这些信息持久到元数据库中。

4.11 清理Hive中间表(Hive Cleanup)

这一步是否成功对正确性不会有任何影响,因为经过上一步之后这个segment就可以在这个cube中被查找到了,但是在整个执行过程中产生了很多的垃圾文件,其中包括:

  1. 临时的hive表;
  2. 因为hive表是一个外部表,存储该表的文件也需要额外删除;
  3. fact distinct这一步将数据写入到HDFS上为建立子典做准备,这时候也可以删除了;
  4. rowKey统计的时候会生成一个文件,此时可以删除;
  5. 生成HFile时文件存储的路径和hbase真正存储的路径不同,虽然load是一个remove操作,但是上层的目录还是存在的,也需要删除。

至此整个Build过程结束。

HBase RowKey与索引设计 - 牧梦者 - 博客园

$
0
0

1. HBase的存储形式

hbase的内部使用KeyValue的形式存储,其key时rowKey:family:column:logTime,value是其存储的内容。

其在region内大多以升序的形式排列,唯一的时logTime是以降序的形式进行排列。

所以,rowKey里越靠近左边的信息越容易被检索到。其设计时,要考虑把重要的信息放左边,不重要的信息放到右边。这样可以提高查询数据的速度。最重要的提高索引速度的就是设计合适的rowKey。

在做RowKey设计时,请先考虑业务是读比写多,还是读比写少,HBase本身是为写优化的,即便是这样,也可能会出现热点问题,而如果我们读比较多的话,除了考虑以上RowKey设计原则外,还可以考虑HBase的Coprocessor甚至elasticSearch结合的方法,无论哪种方式,都建议做实际业务场景下数据的压力测试以得到最优结果。

2. RowKey的设计原则

2.1 长度原则

rowKey是一个二进制,RowKey的长度被很多开发者建议说设计在10~100个字节,以byte[]形式保存,最大不能超过64kb。建议越短越好,不要超过16个字节。

太长的影响有几点点:

  • 一是HBase的持久化文件HFile是按照KeyValue存储的,如果RowKey过长,比如说500个字节,1000万列数据,光是RowKey就要占用500*1000万=50亿个字节,将近1G数据,极大影响了HFile的存储效率。
  • 二是缓存MemStore缓存部分数据到内存中,如果RowKey字段过长,内存的有效利用率会降低,系统无法缓存更多的数据,降低检索效率。
  • 目前操作系统都是64位系统,内存8字节对齐,控制在16字节,8字节的整数倍利用了操作系统的最佳特性。

注意:不仅RowKey的长度是越短越好,而且列簇名、列名等尽量使用短名字,因为HBase属于列式数据库,这些名字都是会写入到HBase的持久化文件HFile中去,过长的RowKey、列簇、列名都会导致整体的存储量成倍增加。

2.2 唯一原则

保证rowKey的唯一性。由于在HBase中数据存储是Key-Value形式,若HBase中同一表插入相同RowKey,则原先的数据会被覆盖掉(如果表的version设置为1的话)。

2.3 散列原则

设计的RowKey应均匀分布在各个HBase节点上。如RowKey是按系统时间戳的方式递增,RowKey的第一部分如果是时间戳的话,将造成所有新数据都在一个RegionServer堆积的热点现象,也就是通常说的Region热点问题,热点发生在大量的client直接访问集中在个别RegionServer上(访问可能是读、写或者其他操作),导致单个RegionServer机器自身负载过高,引起性能下降甚至Region不可用,常见的是发生jvm full gc或者显示region too busy异常情况。

3. 在不同访问模式下设计行健

3.1 为写优化(解决热点问题)

当往HBase表写入大量数据时,需要在RegionServer上分散负载来进行优化。这并不难,但是你可能不得不在读模式优化上付出代价。比如,时间序列数据的例子,如果你的数据直接使用时间戳做行健,在写入时在单个region上会遇到热点问题。

许多使用场景下,并不需要基于单个时间戳访问数据。你可能要运行一个作业在一个时间区间上做聚合计算,如果对时间延迟不敏感,可以考虑跨多个region做并行扫描来完成任务。但问题是,应该如何把数据分散在多个region上呢?有几个选项可以考虑,答案取决于你想让行健包含什么信息。

  1. 散列 。 如果你愿意在行健里放弃时间戳信息(每次你做什么事情都要扫描全表,或者每次要读数据时你都知道精确的键,这些情况下也是可行的),使用原始数据的散列值作为行健是一种可能的解决方案:

          

           每次当你需要访问以这个散列值为键的行时,需要精确知道“TheRealMT”。时间序列数据一般不这样处理。当你访问数据时,可能记住了一个时间范围,但不大可能知道精确的时间戳。但是有些情况下,能够计算散列值从而找到正确的行。为了得到一种跨所有region的、优秀的分布策略,你可以使用MD5、SHA-1或者其他提供随机分布的散列数。

        2.salting。当你思考行健的构成时,salting是另一种技巧。让我们考虑之前的时间序列数据例子。假设你在读取时知道时间范围,但不想做全表扫描。对时间戳做散列运算然后把散列值作为行健的做法需要做全表扫描,这是很低效的,尤其是在你有办法限制扫描范围的时候。使用散列值作为行健在这里不是办法,但是你可以在时间戳前面加上一个随机数前缀。

         例如,你可以先计算时间戳的散列码,然后用RegionServer的数量取模来生成随机salt数:

         

           取到salt数后,加到时间戳的前面生成行健:

           

   现在行健如下所示:

          

   你可以想到,这些行将会基于键的第一部分,也就是随机salt数,分布在各个region。

   0|timestamp1,0|timestamp5和0|timestamp6将进入一个region,除非发生region拆分(拆分的情况下会分散到两个region)。1|timestamp2,1|timestamp9进入另一个不同的region,2|timestamp4,2|timestamp8进入第三个region。连续时间戳的数据散列进入了多个region。

   但并非一切都是完美的。现在读操作需要把扫描命令分散到所有region上来查找相应的行。因为它们不再存储在一起,所以一个短扫描不能解决问题了。这是一种权衡,为了搭建成功的应用你需要做出选择。这是一个利用信息的位置来获得跨region分布的经典例子。

           3. Reverse反转。针对固定长度的RowKey反转后存储,这样可以使RowKey中经常改变的部分放在最前面,可以有效的随机RowKey。反转RowKey的例子通常以手机举例,可以将手机号反转后的字符串作为RowKey,这样就避免了以手机号那样比较固定开头导致热点问题。这样做的缺点是牺牲了RowKey的有序性。

3.2 为读优化

           时间戳反转。一个常见的数据处理问题是快速获取数据的最新版本,使用反转的时间戳作为RowKey的一部分对这个问题十分有用,可以用Long.Max_Value - timestamp追加到key的末尾。举例,在设计推帖流表时,你的焦点是为读优化行健,目的是把推帖流里最新的推帖存储在一起,以便于它们可以被快速读取,而不用做开销很大的硬盘搜索。在推贴流表里,你使用倒序时间戳(Long.MAX_VALUE - 时间戳)然后附加上用户ID来构成行健。现在你基于用户ID扫描紧邻的n行就可以找到用户需要的n条最新推帖。这里行健的结构对于读性能很重要。把用户ID放在开头有助于你设置扫描,可以轻松定义起始键。

4. HBase的RowKey设计应用实例

4.1 设计订单状态表

设计模式:反转+时间戳反转

RowKey:reverser(order_id) + (Long.MAX_VALUE - timestamp)

这样设计的好处一是通过reverse订单号避免Region热点,二是可以按时间倒排显示,可以获取到最新的订单。

同样适用于需要保存一个用户的操作记录,按照操作时间倒序排序。设计的rowKey为:reverser(userId) + (Long.MAX_VALUE - timestamp)。如果需要查询某段时间的操作记录,startRow是[userId反转][Long.MAX_VALUE - 起始时间],stopRow是[userId反转][Long.MAX_VALUE - 结束时间]。

4.2 登录、下单等等统称事件(event)的临时存储

HBase只存储了最近10分钟的热数据

设计模式:salt加盐

RowKey:两位随机数Salt + eventId + Date + kafka的Offset

这样设计的好处是:设计加盐的目的是为了增加查询的并发性,假如Salt的范围是0~n,那我们在查询的时候,可以将数据分为n个split同时做scan操作。经过我们的多次测试验证,增加并发度能够将整体的查询速度提升5~20倍以上。随后的eventId和Date是用来做范围Scan来使用的。在我们的查询场景中,大部分都是指定了eventId的,因此我们在eventId放在了第二个位置上,同时呢,通过Salt + eventId的方式可以保证不会形成热点。把date放在RowKey的第三个位置上可以实现date做scan,批量Scan性能甚至可以做到毫秒级返回。

这样的RowKey设计能够很好的支持如下几个查询场景:

  1. 全表scan。在这种情况下,我们仍然可以将全表数据切分成n份并发查询,从而实现查询的实时响应。
  2. 只按照event_id查询。
  3. 按照event_id和date查询。

5. HBase索引设计

数据库查询可简单分解为两个步骤:1)键的查找;2) 数据的查找

因这两种数据组织方式的不同,在RDBMS领域有两种常见的数据组织表结构:

索引组织表:键与数据存放在一起,查找到键所在的位置则意味着查找到数据本身。

堆表:键的存储与数据的存储是分离的。查找到键的位置,只能获取到数据的物理地址,还需要基于该地址去获取数据。

HBase数据表其实是一种 索引组织表结构:查找到 RowKey所在的位置则意味着找到数据本身。因此, RowKey本身就是一种索引

5.1 RowKey查询的局限性/二级索引需求背景

如果提供的查询条件能够 尽可能丰富的描述RowKey的 前缀信息,则 查询时延越能得到保障。如下面几种组合条件场景:

  * Name + Phone + ID
  * Name + Phone
       * Name

如果查询条件不能提供Name信息,则RowKey的前缀条件是无法确定的,此时只能通过 全表扫描的方式来查找结果。

一种业务模型的用户数据RowKey,只能采用单一结构设计。但事实上,查询场景可能是多纬度的。例如,在上面的场景基础上,还需要单独基于Phone列进行查询。这是HBase二级索引出现的背景。即,二级索引是为了让HBase能够提供更多纬度的查询能力。

注:HBase原生并不支持二级索引方案,但基于HBase的KeyValue数据模型与API,可以轻易的构建出二级索引数据。Phoenix提供了两种索引方案,而一些大厂家也都提供了自己的二级索引实现。

5.2 HBase 二级索引方案

5.2.1 基于Coprocessor方案

从0.94版本,HBase官方文档已经提出了HBase上面实现二级索引的一种路径:

  • 基于Coprocessor(0.92版本引入,达到支持类似传统RDBMS的触发器的行为)。
  • 开发自定义数据处理逻辑,采用数据“双写”策略,在有数据写入同时同步到二级索引表。

5.2.1.1 开源方案:

业界比较知名的基于Coprocessor的开源方案:

  • 华为的hindex:基于0.94版本,但版本比较旧,github上几年都没更新过。
  • Apache Phoenix:功能围绕SQL On HBase,支持和兼容多个hbase版本,二级索引只是其中一块功能。二级索引的创建和管理直接有SQL语法支持,适用起来简便,该项目目前社区活跃度和版本更新迭代情况都比较好。

Apache Phoenix在目前开源的方案中,是一个比较优的选择,主打SQL On HBase,基于SQL能完成HBase的CRUD操作,支持JDBC协议。

5.2.1.2 Phoenix二级索引特点:

  • Covered Indexes(覆盖索引):把关注的数据字段也附在索引表上,只需要通过索引表就能返回所要查询的数据(列),所以索引的列必须包含所需查询的列(SELECT的列和WHERE的列)。
  • Functional Indexes(函数索引):索引不局限于列,支持任意的表达式来创建索引。
  • Global Indexes(全局索引):适用于读多写少场景。通过维护全局索引表,所有的更新和写操作都会引起索引的更新,写入性能受到影响。在读数据时,Phoenix SQL会基于索引字段,执行快速查询。
  • Local Indexes(本地索引):适用于写多读少场景。在数据写入时,索引数据和表数据都会存储在本地。在数据读取时,由于无法预先确定region的位置,所以在读取数据时需要检查每个region(以找到索引数据),会带来一定性能(网络)开销。

5.2.2 非Coprocessor方案

选择不基于Coprocessor开发,自行在外部构建和维护索引关系也是另外一种方式。

常见的是采用底层基于Apache Lucene的ElasticSearch(下面简称ES)或Apache Solr,来构建强大的索引能力、搜索能力,例如支持模糊查询、全文检索、组合查询、排序等。

其实对于在外部自定义构建二级索引的方式,有自己的大数据团队的公司一般都会针对自己的业务场景进行优化,自行构建ES/Solr的搜索集群。例如数说故事企业内部的百亿级数据全量库,就是基于ES构建海量索引和检索能力的案例。主要有优化点包括:

  • 对企业的索引集群面向的业务场景和模式定制,对通用数据模型进行抽象和平台话复用
  • 需要针对多业务、多项目场景进行ES集群资源的合理划分和运维管理
  • 查询需要针对多索引集群、跨集群查询进行优化
  • 共用集群场景需要做好防护、监控、限流

下面显示了数说基于ES做二级索引的两种构建流程,包含:

  • 增量索引:日常持续接入的数据源,进行增量的索引更新
  • 全量索引:配套基于Spark/MR的批量索引创建/更新程序,用于初次或重建已有HBase库表的索引。

数据查询流程:

6. HBase表设计关注点

HBase表设计通常可以是宽表(wide table)模式,即一行包括很多列。同样的信息也可以用高表(tall table)形式存储,通常高表的性能比宽表要高出50%以上,所以推荐大家使用高表来完成表设计。表设计时,我们也应该要考虑HBase数据库的一些特性:

  1. 在HBase表中是通过RowKey的字典序来进行数据排序的。
  2. 所有存储在HBase表中的数据都是二进制的字节。
  3. 原子性只在行内保证,HBase不支持跨行事务。
  4. 列簇(Column Family)在表创建之前就要定义好
  5. 列簇中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加。

总结

参考资料:

《HBase实战》

https://www.cnblogs.com/parent-absent-son/p/10200202.html

https://blog.csdn.net/wangshuminjava/article/details/80575864

https://www.cnblogs.com/yuguoshuo/p/6265649.html

http://www.nosqlnotes.com/technotes/hbase/hbase-rowkey/

https://zhuanlan.zhihu.com/p/43972378

基于Spring Boot的统一异常处理设计 - Grey Zeng - 博客园

$
0
0

基于Spring Boot的统一异常处理设计

作者: Grey

原文地址: https://www.cnblogs.com/greyzeng/p/11733327.html

Spring Boot中,支持RestControllerAdvice统一处理异常,在一个请求响应周期当中,如果Controller,Service,Repository出现任何异常,都会被RestControllerAdvice机制所捕获,进行统一处理。

进行统一异常处理的目的也就是为了将千奇百怪的异常信息转换成用户可识别的错误信息

统一异常拦截器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionTranslator {

}

系统中的两类异常处理

第一类:业务自定义的异常,遇到这种异常,拦截器记录后,将业务异常自己的信息抛出。

@ExceptionHandler(BusinessException.class)
public JSONObject handleError(BusinessException e) {
    log.error("Business Exception {}", getStackTraceAsString(e));
    return error(e);
}

第二类:未定义异常,拦截器负责统一屏蔽原来的异常信息,转为服务器内部异常抛出。

@ExceptionHandler(Throwable.class)
public JSONObject handleError(Throwable undefined) {
    log.error("Internal Server Error {}", getStackTraceAsString(undefined));
    return error(new BusinessException(FAILURE));
}

调用者收到error的结果后,直接显示msg内容为用户可见的错误信息即可。

如何自定义一个业务异常

在业务开发中,通常无需进行Try catch处理,有业务异常直接抛出即可。如果需要定义一类通用的异常,则需要在自己业务模块下新建异常类,继承于 BusinessException

public class PaymentException extends BusinessException {

    //重写构造函数,从而定义该自定义异常的用户可见的错误信息
    public PaymentException() {
        super("支付失败");
    }
}

如何自定义一个框架级异常

在系统框架层面,已经预定义了一些常见的异常类,如:

类名定义预置错误信息
PermissionDenyException用户访问未授权的内容权限不足
ServiceNotFoundException调用微服务失败调用相关服务失败
其他异常......

在定义框架级异常时,除了需要编写异常类之外,如需要前端根据error CODE做对应的处理,就可以在ResultCode中增加定义。示例如下:

@Getter
@AllArgsConstructor
public enum ResultCode {

    /**
     * 操作成功
     */
    SUCCESS(HTTP_OK, "操作成功"),
    /**
     * 因程序内部错误操作失败(如不指定,则默认这个异常)
     */
    FAILURE(HTTP_INTERNAL_ERROR, "系统运行异常,请联系管理员"),
    /**
     * 用户访问未授权的内容
     */
    UN_AUTHORIZED(HTTP_UNAUTHORIZED, "权限不足"),

    /**
     * 调用微服务失败
     */
    NOT_FOUND(HTTP_NOT_FOUND, "调用相关服务失败");

    final int code;

    final String msg;
}

一个框架级异常的实现类

public class PermissionDenyException extends BusinessException {
    public PermissionDenyException() {
        super(UN_AUTHORIZED);
    }
}

如需要框架对该异常定义统一的策略,则需要在GlobalExceptionTranslator实现该策略,示例如下:

public class GlobalExceptionTranslator {   
    @ExceptionHandler(NewGlobalException.class)
    public JSONObject handleError(NewGlobalException e) {
        // 这里可以实现自定义的异常策略
        return error(new BusinessException(e.getResultCode(),e.getMessage()));
    }
}

数据库用什么样的密码HASH算法才是最安全的? - andylau00j的专栏 - CSDN博客

$
0
0

以下是在公司内部技术分享时总结的,希望对你有用:

我们数据库的权限管理十分严格,敏感信息开发工程师都看不到,密码明文存储不行吗?

不行。存储在数据库的数据面临很多威胁,有应用程序层面、数据库层面的、操作系统层面的、机房层面的、员工层面的,想做到百分百不被黑客窃取,非常困难。

如果密码是加密之后再存储,那么即便被拖库,黑客也难以获取用户的明文密码。可以说,密码加密存储是用户账户系统的底裤,它的重要性,相当于你独自出远门时缝在内衣里钱,虽然你用到他们的概率不大,但关键时刻他们能救命。

那用加密算法比如AES,把密码加密下再存,需要明文的时候我再解密。

不行。这涉及到怎么保存用来加密解密的密钥,虽然密钥一般跟用户信息分开存储,且业界也有一些成熟的、基于软件或硬件的密钥存储方案。但跟用户信息的保存一样,想要密钥百分百不泄露,不可能做到。用这种方式加密密码,能够降低黑客获取明文密码的概率。但密钥一旦泄露,用户的明文密码也就泄露了,不是一个好方法。

另外,用户账户系统不应该保存用户的明文密码,在用户忘记密码的时候,提供重置密码的功能而不是找回密码。

保存所有密码的HASH值,比如MD5。是不是就可以了?

不是所有的HASH算法都可以,准确讲应该是 Cryptographic Hash。Cryptographic Hash具有如下几个特点:

  1. 给定任意大小任意类型的输入,计算hash非常快;
  2. 给定一个hash,没有办法计算得出该hash所对应的输入;
  3. 对输入做很小改动,hash就会发生很大变化;
  4. 没有办法计算得到两个hash相同的输入;

虽然不是为加密密码而设计,但其第2、3、4三个特性使得Cryptographic Hash非常适合用来加密用户密码。常见的Cryptographic Hash有MD5、SHA-1、SHA-2、SHA-3/Keccak、BLAKE2。

从1976年开始,业界开始使用Cryptographic Hash加密用户密码,最早见于Unix Crypt。但MD5、SHA-1已被破解,不适合再用来保存密码。

那我保存用户密码的SHA256值。

不行。黑客可以用查询表或彩虹表来破解用户密码。注意是破解密码不是破解sha256,能根据sha256破解密码的原因是,用户密码往往需要大脑记忆、手工输入,所以不会太复杂,往往具有有限的长度、确定的取值空间。

  • 短的取值简单的密码可以用查询表破解

比如8位数字密码,一共只有10^8=100000000种可能。一亿条数据并不算多,黑客可以提前吧0-99999999的sha256都计算好,并以sha256做key密码为value存储为一个查询表,当给定sha256需要破解时,从表中查询即可。

  • 取值相对复杂,且长度较长的密码,可以用彩虹表破解

比如10位,允许数字、字母大小写的密码,一共有(10+26+26)^10~=84亿亿种可能,记录非常之多难以用查询表全部保存起来。这时候黑客会用一种叫做彩虹表的技术来破解,彩虹表用了典型的计算机世界里解决问题的思路,时间空间妥协。在这个例子里面,空间不够,那就多花一些时间。在彩虹表中,可以将全部的sha256值转化为长度相同的若干条hash链,只保存hash链的头和尾,在破解的时候先查询得到sha256存在于哪条hash链中,然后计算这一条hash链上的所有sha256,通过实时比对来破解用户密码。

上图图展示了一个hash链长度为3的彩虹表,因为在hash链中需要将hash值使用R函数映射回密码取值空间,为了降低R函数的冲突概率,长度为K的hash链中,彩虹表会使用k个R函数,因为每次迭代映射回密码空间使用的R函数不一样,这种破解方法被称作彩虹表攻击。

实际的情况Hash链要比远比上例更长,比如我们的例子中全部的84亿亿个sha256存不下,可以转化为840亿条长度为1千万的sha链。对彩虹表原理感兴趣的话,可以阅读它的 维基百科

网路上甚至有一些 已经计算好的彩虹表可以直接使用,所以直接保存用户密码的sha256是非常不安全的。

怎样避免彩虹表攻击?

简单讲,就是加盐。一般来讲用户密码是个字符串key、盐是我们生成的字符串salt。原来我们保存的是key的hash值HASH(key),现在我们保存key和salt拼接在一起的hash值HASH(key+salt)。

这样黑客提前计算生成的彩虹表,就全都失效了。

盐应该怎么生成,随机生成一个字符串?

这是个好问题,并不是加个盐就安全了,盐的生成有很多讲究。

  • 使用CSPRNG(Cryptographically Secure Pseudo-Random Number Generator)生成盐,而不是普通的随机数算法;

CSPRNG跟普通的随机数生成算法,比如C语言标准库里面的rand()方法,有很大不同。正如它的名字所揭示,CSPRNG是加密安全的,这意味着用它产生的随机数更加随机,且不可预测。常见编程语言都提供了CSPRNG,如下表:

编程语言CSPRNGC/C++CryptGenRandomJavaJava.security.SecureRandomPHPmcrypt_create_ivErlangCrypt:strong_rand_bytesLinux/Unix上的任何编程语言读取/dev/random
  • 盐不能太短

想想查询表和彩虹表的原理,如果盐很短,那意味着密码+盐组成的字符串的长度和取值空间都有限。黑客完全可以为密码+盐的所有组合建立彩虹表。

  • 盐不能重复使用

如果所有用户的密码都使用同一个盐进行加密。那么不管盐有多复杂、多大的长度,黑客都可以很容易的使用这个固定盐重新建立彩虹表,破解你的所有用户的密码。如果你说,我可以把固定盐存起来,不让别人知道啊,那么你应该重新读一下我关于为什么使用AES加密不够安全的回答。

即便你为每一个用户生成一个随机盐,安全性仍然不够,因为这个盐在用户修改密码时重复使用了。应当在每一次需要保存新的密码时,都生成一个新的盐,并跟加密后的hash值保存在一起。

注意:有些系统用一个每个用户都不同的字段,uid、手机号、或者别的什么,来作为盐加密密码。这不是一个好主意,这几乎违背了上面全部三条盐的生成规则。

那我自己设计一个黑客不知道的HASH算法,这样你的那些破解方法就都失效了。

不可以。

首先如果你不是一个密码学专家,你很难设计出一个安全的hash算法。不服气的话,你可以再看一遍上面我关于Cryptographic Hash的描述,然后想一想自己怎么设计一个算法可以满足它的全部四种特性。就算你是基于已有的Cryptographic Hash的基础上去设计,设计完之后,也难以保证新算法仍然满足Cryptographic Hash的要求。而一旦你的算法不满足安全要求,那么你给了黑客更多更容易破解用户密码的方法。

即便你能设计出一个别人不知道的Cryptographic Hash算法,你也不能保证黑客永远都不知道你的算法。黑客往往都有能力访问你的代码,想想柯克霍夫原则或者香农公里:

密码系统应该就算被所有人知道系统的运作步骤,仍然是安全的。

为每一个密码都加上不同的高质量的盐,做HASH,然后保存。这样可以了吧?

以前是可以的,现在不行了。 计算机硬件飞速发展,一个现代通用CPU能以每月数百万次的速度计算sha256,而GPU集群计算sha256,更是可以达到每秒10亿次以上。这使得暴力破解密码成为可能,黑客不再依赖查询表或彩虹表,而是使用定制过的硬件和专用算法,直接计算每一种可能,实时破解用户密码。

那怎么办呢?回想上面关于Cryptographic Hash特性的描述,其中第一条:

给定任意大小任意类型的输入,计算hash非常快

Cryptographic Hash并不是为了加密密码而设计的,它计算非常快的这个特性,在其他应用场景中非常有用,而在现在的计算机硬件条件下,用来加密密码就显得不合适了。针对这一点,密码学家们设计了PBKDF2、BCRYPT、SCRYPT等用来加密密码的Hash算法,称作Password Hash。在他们的算法内部,通常都需要计算Cryptographic Hash很多次,从而减慢Hash的计算速度,增大黑客暴力破解的成本。可以说Password Hash有一条设计原则,就是计算过程能够按要求变慢,并且不容易被硬件加速。

应该使用哪一种Password Hash?

PBKDF2、BCRYPT、SCRYPT曾经是最常用的三种密码Hash算法,至于哪种算法最好,多年以来密码学家们并无定论。但可以确定的是,这三种算法都不完美,各有缺点。其中PBKDF2因为计算过程需要内存少所以可被GPU/ASIC加速,BCRYPT不支持内存占用调整且容易被FPGA加速,而SCRYPT不支持单独调整内存或计算时间占用且可能被ASIC加速并有被旁路攻击的可能。

2013年NIST(美国国家标准与技术研究院)邀请了一些密码学家一起,举办了密码hash算法大赛(Password Hashing Competition),意在寻找一种标准的用来加密密码的hash算法,并借此在业界宣传加密存储用户密码的重要性。大赛列出了参赛算法可能面临的攻击手段:

  • 加密算法破解(原值还原、哈希碰撞等,即应满足Cryptographic Hash的第2、3、4条特性);
  • 查询表/彩虹表攻击;
  • CPU优化攻击;
  • GPU、FPGA、ASIC等专用硬件攻击;
  • 旁路攻击;

最终在2015年7月,Argon2算法赢得了这项竞赛,被NIST认定为最好的密码hash算法。不过因为算法过新,目前还没听说哪家大公司在用Argon2做密码加密。

一路问过来好累,能不能给我举个例子,大公司是怎么加密用户密码的?

今年(2016)Dropbox曾发生部分用户密码数据泄露事件,当时其CTO表示他们对自己加密密码的方式很有信心,请用户放心。随后,Dropbox在其官方技术博客发表名为《How Dropbox securely stores your passwords》的文章,讲述了他们的用户密码加密存储方案。

如上图所示,Dropbox首先对用户密码做了一次sha512哈希将密码转化为64个字节,然后对sha512的结果使用Bcrypt算法(每个用户独立的盐、强度为10)计算,最后使用AES算法和全局唯一的密钥将Bcrypt算法的计算结果加密并保存。博文中,Dropbox描述了这三层加密的原因:

  • 首先使用sha512,将用户密码归一化为64字节hash值。因为两个原因:一个是Bcrypt算对输入敏感,如果用户输入的密码较长,可能导致Bcrypt计算过慢从而影响响应时间;另一个是有些Bcrypt算法的实现会将长输入直接截断为72字节,从信息论的角度讲,这导致用户信息的熵变小;
  • 然后使用Bcrypt算法。选择Bcrypt的原因,是Dropbox的工程师对这个算法更熟悉调优更有经验,参数选择的标准,是Dropbox的线上API服务器可以在100ms左右的时间可计算出结果。另外,关于Bcrypt和Scrypt哪个算法更优,密码学家也没有定论。同时,Dropbox也在关注密码hash算法新秀Argon2,并表示会在合适的时机引入;
  • 最后使用AES加密。因为Bcrypt不是完美的算法,所以Dropbox使用AES和全局密钥进一步降低密码被破解的风险,为了防止密钥泄露,Dropbox采用了专用的密钥保存硬件。Dropbox还提到了最后使用AES加密的另一个好处,即密钥可定时更换,以降低用户信息/密钥泄露带来的风险。

  最后,用复杂的慢哈希苏算法(如果你不知道那个更好,就用bcrypt),然后强制用户不能使用常见的密码(提示用户,不能使用简单密码,找个密码库坐下统计去除排名前50位)、规定至少使用12位的数字字母符号组合密码可有效避免数据库泄漏后带来的麻烦

转自
链接:https://www.zhihu.com/question/19984086/answer/142592260
来源:知乎

Viewing all 532 articles
Browse latest View live