SegmentFault 宜信技术学院最新的文章
2020-11-19T17:59:48+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
宜信支持多渠道前端方案介绍
https://segmentfault.com/a/1190000038228783
2020-11-19T17:59:48+08:00
2020-11-19T17:59:48+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p>【宜信技术沙龙】是由宜信技术学院主办的系列技术分享活动,活动包括线上和线下两种形式,每期技术沙龙都将邀请宜信及其他互联网公司的技术专家分享来自一线的实践经验,分享内容覆盖金融科技及软件研发等主要技术领域,旨在为金融科技行业提供可落地实践的解决方案,为金融科技从业者带来思路想法上的启发。</p><p><strong>一、分享话题及大纲</strong></p><p><strong>【分享话题】:宜信支持多渠道前端方案介绍</strong></p><p><strong>【话题介绍】:</strong></p><p>随着业务不断发展,在前端开发过程中,经常会面临同一个功能要面向不同的渠道,同时也要适应不同渠道多种的需求变化,为了提升开发效率更好的优化开发流程,我们研发了这套前端h5的打包方案,目前我们框架逐渐在支持了产品对比,产品选择,家庭计划书等功能,并在公司财富和普惠等多条业务线落地实践。<strong>这次我们主要从项目的底层框架,到项目搭建思路,到制定的模块化开发标准来由浅至深的介绍。</strong></p><p><strong>【分享大纲】:</strong></p><ul><li>项目框架介绍</li><li>组件管理,公共方法管理,局部方法管理,api管理介绍</li><li>搭积木思路实现多渠道业务支持</li><li>组件开发技巧介绍</li><li>总结</li></ul><p><strong>二、分享嘉宾</strong></p><p><strong>分享嘉宾:刘晓敏</strong></p><p><strong>宜信高级研发工程师</strong></p><p><strong>三、直播收益</strong></p><p><strong>通过本次直播分享,您将收获:</strong></p><p>1、了解前端vue脚手架的基本打包方案</p><p>2、了解多渠道前端打包方案的应用规范和技巧</p><p>3、了解vue组件开发的基本规范和开发技术开源产品</p><p><strong>四、时间及报名方式</strong></p><p>直播时间:11月20日16:00-17:30</p><p>报名方式:creditease_tech</p>
彻底深刻理解js原型链之prototype,__proto__以及constructor(二)
https://segmentfault.com/a/1190000037782939
2020-11-10T11:02:13+08:00
2020-11-10T11:02:13+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
10
<h2>前言</h2><p>如果你能够啃下教程一并且吃透原型链的几个概念的话说明你在前端飞仙的路上又进了一小步···学习最怕的不是慢而是站!这篇教程主要目的对原型链概念进一步加深理解</p><h2>巩固下教程一的知识</h2><p>来看下面的例子:</p><pre><code>var text=new String("我是文字");
function Persion(name,job){
this.name=name;
this.job=job;
}
Persion.myName="lxm";
Persion.prototype.sayName=function(){
alert(this.name);
}
var perison1=new Persion("lxm","20")</code></pre><blockquote>思考:判断下列表达式返回的值:<p>(两分钟之内对八道的算及格,剩下的同学回头接着理解教程一,传送门在此 [<a href="https://link.segmentfault.com/?enc=PGB3kT6VqbLhWCWt1hDcJA%3D%3D.jC%2BEqjIaFlBefc4%2FUSGvdr1eFJFacdKZImj%2F7mA8BCaMXaIdVYbO%2B%2FwdlT3zs9kMf%2BY1ep5ZIWZNTXWL2qCo%2FQ%3D%3D" rel="nofollow">http://0313.name/2017/01/13/p...</a>])</p></blockquote><pre><code>perison1.__proto__===Persion.prototype;
perison1.name===Persion.name;
perison1.prototype.__proto__===Object.prototype;
Persion.prototype.__proto__===Object.prototype;
Persion.__proto__===Function.prototype;
Persion.constructor===perison1;
Function.__proto__===Object.prototype;
Function.prototype.__proto__===Object.prototype;
typeof Persion.prototype;
typeof Function.prototype;</code></pre><h2>原型链图</h2><p>这个图绝对是网络上独一无二独一份,此乃小米飞升教程独家秘籍!因为博主在学习过程中发现对文字的理解和记忆远远不如一个图来的更深更直观,更加透彻,为了您更好的学习原型链,博主特意花了一上午的时间用mermaid绘制了这个原型链的关系图,而且通过这个图我们能够发现很多有意思的事情<br>为了关系图更加直观和清晰,隐去了一些引用线路,其中:</p><ul><li>圆形代表对象的名字</li><li>方形代表属性名</li><li>实线代表对象的分界</li><li>虚线代表引用</li><li>菱形代表基本值</li></ul><p><img src="/img/remote/1460000037782942" alt="git" title="git"></p><ol><li>原型链是单链,只往一个方向流向,没有回路</li><li>只有Function的__proto__指向自己的prototype,这也向我们解释了为什么Function.prototype类型是function</li><li>我们通过__proto__只能获取到原型对象中的方法和属性,所以persion1通过原型链是获取不到Persion的myName属性,但是我们可以通过原型对象的constructor来获取或者修改Persion的属性(这点太给力了)</li></ol><blockquote>请注意,有时候这个方法也不好使,因为原型对象的constructor是可以改变的,不一定指向原型对象所在的函数对象</blockquote><p>继续上面的例子:</p><pre><code>persion1.__proto__.constructor.myName="我变了耶!";
console.log(Persion.myName); //我变了耶</code></pre><ol><li>普通对象的_proto__一定指向创造它的函数对象的prototype</li><li>原型对象的__proto__一定指向Object.prototype!</li><li>通过图我们可以简单理解,拥有原型对象属性的对象是函数对象,否则为普通对象</li><li>原型链是有开始和尽头的,开始于null,结束于普通对象</li><li>所有的函数对象都是Function以new的方式创造出来了,包括Function自己且每个函数对象的__proto__都指向了Function.prototype</li><li>Object是所有对象的父类,我们也可以称之为基类,不过不要纠结于叫什么,因为我们通过图可以看到每一个对象(不管是原型对象还是普通对象还是函数对象)的通过原型链都可以引向Object.prototype</li></ol><p><strong> 以上九条我称为原型链之九句真言(不要太在意名字,我自己随便起的 ~) </strong></p><blockquote>意外收获:this.name和this.job难道不应该在Persion中也有一份吗?无数个日夜,愚笨的博主对this的用法都不甚了解,直到我画出了这种图,我tm彻底明白了this的含义,就是谁运行包含this的这个函数,this就把挂在它身上的包袱(属性)甩给谁!<br>看到了吗,persion1调用了Persion,那么自然多了2个属性,但是注意,name跟job并不是Persion的属性!!</blockquote><p>思考:图中没有画出Object.__proto__的指向,请问他指向哪?(请只依据九句真言解答)</p><h2>思考题解答</h2><h3><strong>思考:判断下列表达式返回的值:</strong></h3><pre><code>perison1.__proto__===Persion.prototype;</code></pre><blockquote>首先判断perison1是通过new方式被Persion创造出来的,依据九句真言第4条得出 :true</blockquote><pre><code>perison1.name===Persion.name;</code></pre><blockquote>通过关系图可以看到不相等,我已经在意外收获中解答了,答案为:false</blockquote><pre><code>perison1.prototype.__proto__===Object.prototype;</code></pre><blockquote>只看图可以看到perison1没有prototype,是普通对象所以答案为:js报错~~</blockquote><pre><code>Persion.prototype.__proto__===Object.prototype;</code></pre><blockquote>参考九句真言第5条:答案为:true</blockquote><pre><code>Persion.__proto__===Function.prototype;</code></pre><blockquote>Persion为函数对象,参考九句真言第8条,答案为:true</blockquote><pre><code>Persion.constructor===perison1;</code></pre><blockquote>Persion是由Function创造出来的所以Persion.constructor指向Function,答案为:false</blockquote><pre><code>Function.__proto__===Object.prototype;</code></pre><blockquote>Function我们已经反复强调是由自身创造所以Function.__proto__===Function.prototype;,答案为:false</blockquote><pre><code>Function.prototype.__proto__===Object.prototype;</code></pre><blockquote>根据九句真言第5条,答案为:true</blockquote><pre><code>typeof Persion.prototype;</code></pre><p>答案为:object</p><pre><code>typeof Function.prototype;</code></pre><p>答案为:function,注意这个是比较特殊的原型对象</p><h3><strong>思考:图中没有画出Object.__proto__的指向,请问他指向哪?(请只依据九句真言解答)</strong></h3><p>下面来分步解答</p><ol><li>Object属于函数对象</li><li>依据九句真言第八条得出函数对象的__proto__都指向了Function.prototype</li><li>所以Object.__proto__===Function.prototype</li></ol><p>这一点是不太好理解的,是Function创造了Object,然后Object创造了Function的原型对象prototype<br>所以就有了</p><pre><code>Object.__proto__===Function.prototype
Function.prototype.__proto__===Object.prototype</code></pre><p>不要太纠结于此,只要理解就好</p><h2>结束语</h2><p>好了,原型链的概念原理通过这2篇教程我相信大家已经滚瓜烂熟了!下面的教程,我们会着重研究下原型链在实际的应用!</p><blockquote>作者 宜信技术学院 刘晓敏</blockquote>
搭建node服务(四):Decorator装饰器
https://segmentfault.com/a/1190000037706768
2020-11-03T14:41:10+08:00
2020-11-03T14:41:10+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p>Decorator(装饰器)是ECMAScript中一种与class相关的语法,用于给对象在运行期间动态的增加功能。Node.js还不支持Decorator,可以使用Babel进行转换,也可以在TypeScript中使用Decorator。本示例则是基于TypeScript来介绍如何在node服务中使用Decorator。</p><h2>一、 TypeScript相关</h2><p>由于使用了 TypeScript ,需要安装TypeScript相关的依赖,并在根目录添加 tsconfig.json 配置文件,这里不再详细说明。要想在 TypeScript 中使用Decorator 装饰器,必须将 tsconfig.json 中 experimentalDecorators设置为true,如下所示:</p><h3>tsconfig.json</h3><pre><code>{
"compilerOptions": {
…
// 是否启用实验性的ES装饰器
"experimentalDecorators": true
}
}</code></pre><h2>二、 装饰器介绍</h2><h3>1. 简单示例</h3><p>Decorator实际是一种语法糖,下面是一个简单的用TypeScript编写的装饰器示例:</p><pre><code>
const Controller: ClassDecorator = (target: any) => {
target.isController = true;
};
@Controller
class MyClass {
}
console.log(MyClass.isController); // 输出结果:true</code></pre><p>Controller是一个类装饰器,在MyClass类声明前以 @Controller 的形式使用装饰器,添加装饰器后MyClass. isController 的值为true。<br>编译后的代码如下:</p><pre><code>var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
const Controller = (target) => {
target.isController = true;
};
let MyClass = class MyClass {
};
MyClass = __decorate([
Controller
], MyClass);</code></pre><h3>2. 工厂方法</h3><p>在使用装饰器的时候有时候需要给装饰器传递一些参数,这时可以使用装饰器工厂方法,示例如下:</p><pre><code>function controller ( label: string): ClassDecorator {
return (target: any) => {
target.isController = true;
target.controllerLabel = label;
};
}
@controller('My')
class MyClass {
}
console.log(MyClass.isController); // 输出结果为: true
console.log(MyClass.controllerLabel); // 输出结果为: "My"</code></pre><p>controller 方法是装饰器工厂方法,执行后返回一个类装饰器,通过在MyClass类上方以 @controller('My') 格式添加装饰器,添加后 MyClass.isController 的值为true,并且MyClass.controllerLabel 的值为 "My"。</p><h3>3. 类装饰器</h3><p>类装饰器的类型定义如下:</p><pre><code>type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;</code></pre><p>类装饰器只有一个参数target,target为类的构造函数。<br>类装饰器的返回值可以为空,也可以是一个新的构造函数。<br>下面是一个类装饰器示例:</p><pre><code>interface Mixinable {
[funcName: string]: Function;
}
function mixin ( list: Mixinable[]): ClassDecorator {
return (target: any) => {
Object.assign(target.prototype, ...list)
}
}
const mixin1 = {
fun1 () {
return 'fun1'
}
};
const mixin2 = {
fun2 () {
return 'fun2'
}
};
@mixin([ mixin1, mixin2 ])
class MyClass {
}
console.log(new MyClass().fun1()); // 输出:fun1
console.log(new MyClass().fun2()); // 输出:fun2</code></pre><p>mixin是一个类装饰器工厂,使用时以 @mixin() 格式添加到类声明前,作用是将参数数组中对象的方法添加到 MyClass 的原型对象上。</p><h3>4. 属性装饰器</h3><p>属性装饰器的类型定义如下:</p><pre><code>type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;</code></pre><p>属性装饰器有两个参数 target 和 propertyKey。</p><ul><li>target:静态属性是类的构造函数,实例属性是类的原型对象</li><li>propertyKey:属性名</li></ul><p>下面是一个属性装饰器示例:</p><pre><code>interface CheckRule {
required: boolean;
}
interface MetaData {
[key: string]: CheckRule;
}
const Required: PropertyDecorator = (target: any, key: string) => {
target.__metadata = target.__metadata ? target.__metadata : {};
target.__metadata[key] = { required: true };
};
class MyClass {
@Required
name: string;
@Required
type: string;
}</code></pre><p>@Required 是一个属性装饰器,使用时添加到属性声明前,作用是在 target 的自定义属性__metadata中添加对应属性的必填规则。上例添加装饰器后target.__metadata 的值为:{ name: { required: true }, type: { required: true } }。<br>通过读取 __metadata 可以获得设置的必填的属性,从而对实例对象进行校验,校验相关的代码如下:</p><pre><code>function validate(entity): boolean {
// @ts-ignore
const metadata: MetaData = entity.__metadata;
if(metadata) {
let i: number,
key: string,
rule: CheckRule;
const keys = Object.keys(metadata);
for (i = 0; i < keys.length; i++) {
key = keys[i];
rule = metadata[key];
if (rule.required && (entity[key] === undefined || entity[key] === null || entity[key] === '')) {
return false;
}
}
}
return true;
}
const entity: MyClass = new MyClass();
entity.name = 'name';
const result: boolean = validate(entity);
console.log(result); // 输出结果:false</code></pre><h3>5. 方法装饰器</h3><p>方法装饰器的类型定义如下:</p><pre><code>type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;</code></pre><p>方法装饰器有3个参数 target 、 propertyKey 和 descriptor。</p><ul><li>target:静态方法是类的构造函数,实例方法是类的原型对象</li><li>propertyKey:方法名</li><li>descriptor:属性描述符</li></ul><p>方法装饰器的返回值可以为空,也可以是一个新的属性描述符。<br>下面是一个方法装饰器示例:</p><pre><code>const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
const className = target.constructor.name;
const oldValue = descriptor.value;
descriptor.value = function(...params) {
console.log(`调用${className}.${key}()方法`);
return oldValue.apply(this, params);
};
};
class MyClass {
private name: string;
constructor(name: string) {
this.name = name;
}
@Log
getName (): string {
return 'Tom';
}
}
const entity = new MyClass('Tom');
const name = entity.getName();
// 输出: 调用MyClass.getName()方法
</code></pre><p>@Log 是一个方法装饰器,使用时添加到方法声明前,用于自动输出方法的调用日志。方法装饰器的第3个参数是属性描述符,属性描述符的value表示方法的执行函数,用一个新的函数替换了原来值,新的方法还会调用原方法,只是在调用原方法前输出了一个日志。</p><h3>6. 访问符装饰器</h3><p>访问符装饰器的使用与方法装饰器一致,参数和返回值相同,只是访问符装饰器用在访问符声明之前。需要注意的是,TypeScript不允许同时装饰一个成员的get和set访问符。下面是一个访问符装饰器的示例:</p><pre><code>
const Enumerable: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
descriptor.enumerable = true;
};
class MyClass {
createDate: Date;
constructor() {
this.createDate = new Date();
}
@Enumerable
get createTime () {
return this.createDate.getTime();
}
}
const entity = new MyClass();
for(let key in entity) {
console.log(`entity.${key} =`, entity[key]);
}
/* 输出:
entity.createDate = 2020-04-08T10:40:51.133Z
entity.createTime = 1586342451133
*/
</code></pre><p>MyClass 类中有一个属性createDate 为Date类型, 另外增加一个有 get 声明的createTime方法,就可以以 entity.createTime 方式获得 createDate 的毫秒值。但是 createTime 默认是不可枚举的,通过在声明前增加 @Enumerable 装饰器可以使 createTime 成为可枚举的属性。</p><h3>7. 参数装饰器</h3><p>参数装饰器的类型定义如下:</p><pre><code>type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;</code></pre><p>参数装饰器有3个参数 target 、 propertyKey 和 descriptor。</p><ul><li>target:静态方法的参数是类的构造函数,实例方法的参数是类的原型对象</li><li>propertyKey:参数所在方法的方法名</li><li>parameterIndex:在方法参数列表中的索引值</li></ul><p>在上面 @Log 方法装饰器示例的基础上,再利用参数装饰器对添加日志的功能进行扩展,增加参数信息的日志输出,代码如下:</p><pre><code>function logParam (paramName: string = ''): ParameterDecorator {
return (target: any, key: string, paramIndex: number) => {
if (!target.__metadata) {
target.__metadata = {};
}
if (!target.__metadata[key]) {
target.__metadata[key] = [];
}
target.__metadata[key].push({
paramName,
paramIndex
});
}
}
const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
const className = target.constructor.name;
const oldValue = descriptor.value;
descriptor.value = function(...params) {
let paramInfo = '';
if (target.__metadata && target.__metadata[key]) {
target.__metadata[key].forEach(item => {
paramInfo += `\n * 第${item.paramIndex}个参数${item.paramName}的值为: ${params[item.paramIndex]}`;
})
}
console.log(`调用${className}.${key}()方法` + paramInfo);
return oldValue.apply(this, params);
};
};
class MyClass {
private name: string;
constructor(name: string) {
this.name = name;
}
@Log
getName (): string {
return 'Tom';
}
@Log
setName(@logParam() name: string): void {
this.name = name;
}
@Log
setNames( @logParam('firstName') firstName: string, @logParam('lastName') lastName: string): void {
this.name = firstName + '' + lastName;
}
}
const entity = new MyClass('Tom');
const name = entity.getName();
// 输出:调用MyClass.getName()方法
entity.setName('Jone Brown');
/* 输出:
调用MyClass.setNames()方法
* 第0个参数的值为: Jone Brown
*/
entity.setNames('Jone', 'Brown');
/* 输出:
调用MyClass.setNames()方法
* 第1个参数lastName的值为: Brown
* 第0个参数firstName的值为: Jone
*/</code></pre><p>@logParam 是一个参数装饰器,使用时添加到参数声明前,用于输出参数信息日志。</p><h3>8. 执行顺序</h3><p>不同声明上的装饰器将按以下顺序执行:</p><ol><li>实例成员的装饰器:</li></ol><p>参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器</p><ol><li>静态成员的装饰器:</li></ol><p>参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器</p><ol><li>构造函数的参数装饰器</li><li>类装饰器</li></ol><p>如果同一个声明有多个装饰器,离声明越近的装饰器越早执行:</p><pre><code>const A: ClassDecorator = (target) => {
console.log('A');
};
const B: ClassDecorator = (target) => {
console.log('B');
};
@A
@B
class MyClass {
}
/* 输出结果:
B
A
*/</code></pre><h2>三、 Reflect Metadata</h2><h2>1. 安装依赖</h2><p>Reflect Metadata是的一个实验性接口,可以通过装饰器来给类添加一些自定义的信息。这个接口目前还不是 ECMAScript 标准的一部分,需要安装 reflect-metadata垫片才能使用。</p><pre><code>npm install reflect-metadata --save</code></pre><p>或者</p><pre><code>yarn add reflect-metadata</code></pre><p>另外,还需要在全局的位置导入此模块,例如:入口文件。</p><pre><code>import 'reflect-metadata';</code></pre><h3>2. 相关接口</h3><p>Reflect Metadata 提供的接口如下:</p><pre><code>// 定义元数据
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
// 检查指定关键字的元数据是否存在,会遍历继承链
let result1 = Reflect.hasMetadata(metadataKey, target);
let result2 = Reflect.hasMetadata(metadataKey, target, propertyKey);
// 检查指定关键字的元数据是否存在,只判断自己的,不会遍历继承链
let result3 = Reflect.hasOwnMetadata(metadataKey, target);
let result4 = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);
// 获取指定关键字的元数据值,会遍历继承链
let result5 = Reflect.getMetadata(metadataKey, target);
let result6 = Reflect.getMetadata(metadataKey, target, propertyKey);
// 获取指定关键字的元数据值,只查找自己的,不会遍历继承链
let result7 = Reflect.getOwnMetadata(metadataKey, target);
let result8 = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
// 获取元数据的所有关键字,会遍历继承链
let result9 = Reflect.getMetadataKeys(target);
let result10 = Reflect.getMetadataKeys(target, propertyKey);
// 获取元数据的所有关键字,只获取自己的,不会遍历继承链
let result11 = Reflect.getOwnMetadataKeys(target);
let result12 = Reflect.getOwnMetadataKeys(target, propertyKey);
// 删除指定关键字的元数据
let result13 = Reflect.deleteMetadata(metadataKey, target);
let result14 = Reflect.deleteMetadata(metadataKey, target, propertyKey);
// 装饰器方式设置元数据
@Reflect.metadata(metadataKey, metadataValue)
class C {
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}</code></pre><h3>3. design类型元数据</h3><p>要使用design类型元数据需要在tsconfig.json中设置emitDecoratorMetadata为true,如下所示:</p><ul><li>tsconfig.json</li></ul><pre><code>{
"compilerOptions": {
…
// 是否启用实验性的ES装饰器
"experimentalDecorators": true
// 是否自动设置design类型元数据(关键字有"design:type"、"design:paramtypes"、"design:returntype")
"emitDecoratorMetadata": true
}
}</code></pre><p>emitDecoratorMetadata 设为true后,会自动设置design类型的元数据,通过以下方式可以获取元数据的值:</p><pre><code>let result1 = Reflect.getMetadata('design:type', target, propertyKey);
let result2 = Reflect.getMetadata('design:paramtypes', target, propertyKey);
let result3 = Reflect.getMetadata('design:returntype', target, propertyKey);</code></pre><p>不同类型的装饰器获得的 design 类型的元数据值,如下表所示:</p><table><thead><tr><th>装饰器类型</th><th>design:type</th><th>design:paramtypes</th><th>design:returntype</th></tr></thead><tbody><tr><td>类装饰器</td><td> </td><td>构造函数所有参数类型组成的数组</td><td> </td></tr><tr><td>属性装饰器</td><td>属性的类型</td><td> </td><td> </td></tr><tr><td>方法装饰器</td><td>Function</td><td>方法所有参数的类型组成的数组</td><td>方法返回值的类型</td></tr><tr><td>参数装饰器</td><td>所属方法所有参数的类型组成的数组</td><td> </td><td> </td></tr></tbody></table><p>示例代码:</p><pre><code>const MyClassDecorator: ClassDecorator = (target: any) => {
const type = Reflect.getMetadata('design:type', target);
console.log(`类[${target.name}] design:type = ${type && type.name}`);
const paramTypes = Reflect.getMetadata('design:paramtypes', target);
console.log(`类[${target.name}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
const returnType = Reflect.getMetadata('design:returntype', target)
console.log(`类[${target.name}] design:returntype = ${returnType && returnType.name}`);
};
const MyPropertyDecorator: PropertyDecorator = (target: any, key: string) => {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`属性[${key}] design:type = ${type && type.name}`);
const paramTypes = Reflect.getMetadata('design:paramtypes', target, key);
console.log(`属性[${key}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
const returnType = Reflect.getMetadata('design:returntype', target, key);
console.log(`属性[${key}] design:returntype = ${returnType && returnType.name}`);
};
const MyMethodDecorator: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`方法[${key}] design:type = ${type && type.name}`);
const paramTypes = Reflect.getMetadata('design:paramtypes', target, key);
console.log(`方法[${key}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
const returnType = Reflect.getMetadata('design:returntype', target, key)
console.log(`方法[${key}] design:returntype = ${returnType && returnType.name}`);
};
const MyParameterDecorator: ParameterDecorator = (target: any, key: string, paramIndex: number) => {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`参数[${key} - ${paramIndex}] design:type = ${type && type.name}`);
const paramTypes = Reflect.getMetadata('design:paramtypes', target, key);
console.log(`参数[${key} - ${paramIndex}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
const returnType = Reflect.getMetadata('design:returntype', target, key)
console.log(`参数[${key} - ${paramIndex}] design:returntype = ${returnType && returnType.name}`);
};
@MyClassDecorator
class MyClass {
@MyPropertyDecorator
myProperty: string;
constructor (myProperty: string) {
this.myProperty = myProperty;
}
@MyMethodDecorator
myMethod (@MyParameterDecorator index: number, name: string): string {
return `${index} - ${name}`;
}
}
</code></pre><p>输出结果如下:</p><pre><code>属性[myProperty] design:type = String
属性[myProperty] design:paramtypes = undefined
属性[myProperty] design:returntype = undefined
参数[myMethod - 0] design:type = Function
参数[myMethod - 0] design:paramtypes = [ 'Number', 'String' ]
参数[myMethod - 0] design:returntype = String
方法[myMethod] design:type = Function
方法[myMethod] design:paramtypes = [ 'Number', 'String' ]
方法[myMethod] design:returntype = String
类[MyClass] design:type = undefined
类[MyClass] design:paramtypes = [ 'String' ]
类[MyClass] design:returntype = undefined</code></pre><h3>四、 装饰器应用</h3><p>使用装饰器可以实现自动注册路由,通过给Controller层的类和方法添加装饰器来定义路由信息,当创建路由时扫描指定目录下所有Controller,获取装饰器定义的路由信息,从而实现自动添加路由。</p><h3>装饰器代码</h3><ul><li>src/common/decorator/controller.ts</li></ul><pre><code>export interface Route {
propertyKey: string,
method: string;
path: string;
}
export function Controller(path: string = ''): ClassDecorator {
return (target: any) => {
Reflect.defineMetadata('basePath', path, target);
}
}
export type RouterDecoratorFactory = (path?: string) => MethodDecorator;
export function createRouterDecorator(method: string): RouterDecoratorFactory {
return (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const route: Route = {
propertyKey,
method,
path: path || ''
};
if (!Reflect.hasMetadata('routes', target)) {
Reflect.defineMetadata('routes', [], target);
}
const routes = Reflect.getMetadata('routes', target);
routes.push(route);
}
}
export const Get: RouterDecoratorFactory = createRouterDecorator('get');
export const Post: RouterDecoratorFactory = createRouterDecorator('post');
export const Put: RouterDecoratorFactory = createRouterDecorator('put');
export const Delete: RouterDecoratorFactory = createRouterDecorator('delete');
export const Patch: RouterDecoratorFactory = createRouterDecorator('patch');</code></pre><h3>控制器代码</h3><ul><li>src/controller/roleController.ts</li></ul><pre><code>import Koa from 'koa';
import { Controller, Get } from '../common/decorator/controller';
import RoleService from '../service/roleService';
@Controller()
export default class RoleController {
@Get('/roles')
static async getRoles (ctx: Koa.Context) {
const roles = await RoleService.findRoles();
ctx.body = roles;
}
@Get('/roles/:id')
static async getRoleById (ctx: Koa.Context) {
const id = ctx.params.id;
const role = await RoleService.findRoleById(id);
ctx.body = role;
}
}</code></pre><ul><li>src/controller/userController.ts</li></ul><pre><code>import Koa from 'koa';
import { Controller, Get } from '../common/decorator/controller';
import UserService from '../service/userService';
@Controller('/users')
export default class UserController {
@Get()
static async getUsers (ctx: Koa.Context) {
const users = await UserService.findUsers();
ctx.body = users;
}
@Get('/:id')
static async getUserById (ctx: Koa.Context) {
const id = ctx.params.id;
const user = await UserService.findUserById(id);
ctx.body = user;
}
}</code></pre><h3>路由器代码</h3><ul><li>src/common /scanRouter.ts</li></ul><pre><code>import fs from 'fs';
import path from 'path';
import KoaRouter from 'koa-router';
import { Route } from './decorator/controller';
// 扫描指定目录的Controller并添加路由
function scanController(dirPath: string, router: KoaRouter): void {
if (!fs.existsSync(dirPath)) {
console.warn(`目录不存在!${dirPath}`);
return;
}
const fileNames: string[] = fs.readdirSync(dirPath);
for (const name of fileNames) {
const curPath: string = path.join(dirPath, name);
if (fs.statSync(curPath).isDirectory()) {
scanController(curPath, router);
continue;
}
if (!(/(.js|.jsx|.ts|.tsx)$/.test(name))) {
continue;
}
try {
const scannedModule = require(curPath);
const controller = scannedModule.default || scannedModule;
const isController: boolean = Reflect.hasMetadata('basePath', controller);
const hasRoutes: boolean = Reflect.hasMetadata('routes', controller);
if (isController && hasRoutes) {
const basePath: string = Reflect.getMetadata('basePath', controller);
const routes: Route[] = Reflect.getMetadata('routes', controller);
let curPath: string, curRouteHandler;
routes.forEach( (route: Route) => {
curPath = path.posix.join('/', basePath, route.path);
curRouteHandler = controller[route.propertyKey];
router[route.method](curPath, curRouteHandler);
console.info(`router: ${controller.name}.${route.propertyKey} [${route.method}] ${curPath}`)
})
}
} catch (error) {
console.warn('文件读取失败!', curPath, error);
}
}
}
export default class ScanRouter extends KoaRouter {
constructor(opt?: KoaRouter.IRouterOptions) {
super(opt);
}
scan (scanDir: string | string[]) {
if (typeof scanDir === 'string') {
scanController(scanDir, this);
} else if (scanDir instanceof Array) {
scanDir.forEach(async (dir: string) => {
scanController(dir, this);
});
}
}
}</code></pre><h3>创建路由代码</h3><ul><li>src/router.ts</li></ul><pre><code>import path from 'path';
import ScanRouter from './common/scanRouter';
const router = new ScanRouter();
router.scan([path.resolve(__dirname, './controller')]);
export default router;</code></pre><h3>五、 说明</h3><p>本文介绍了如何在node服务中使用装饰器,当需要增加某些额外的功能时,就可以不修改代码,简单地通过添加装饰器来实现功能。本文相关的代码已提交到GitHub以供参考,项目地址:<a href="https://link.segmentfault.com/?enc=YZFZGZzI8q2dyj87ofExfw%3D%3D.bj4SJpaIlldA551Z%2B%2BANTL1zcSEjENixpE%2Bv2RnVuPecsrCcShZnttU%2BaMOz9zBD4QEP5lPVEZE1M%2Fdi%2F4bJXg%3D%3D" rel="nofollow">https://github.com/liulinsp/node-server-decorator-demo</a>。</p><blockquote>作者:宜信技术学院 刘琳</blockquote>
Redis migrate 数据迁移工具
https://segmentfault.com/a/1190000037655513
2020-10-29T16:41:30+08:00
2020-10-29T16:41:30+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p>在工作中可能会遇到单点Redis向Redis集群迁移数据的问题,但又不能老麻烦运维来做。为了方便研发自己迁移数据,我这里写了一个简单的Redis迁移工具,希望对有需要的人有用。</p><h4>本工具支持:</h4><ul><li>单点Redis到单点Redis迁移</li><li>单点Redis到Redis集群迁移</li><li>Redis集群到Redis集群迁移</li><li>Redis集群到单点Redis迁移</li></ul><p>该工具已经编译成了多平台命令,直接从Github下载二进制文件执行就好了。<br>项目地址:<a href="https://link.segmentfault.com/?enc=laiVIRxihfUcvo0SduO3Ww%3D%3D.ujA3IlK5iVe8rEJQwAiWIxUoPoEqjeqLvHIkjmnYrIZyc2DVH5HgFzwhbcY8R7pi" rel="nofollow"> https://github.com/icowan/redis-tool</a><br>把代码拉下来之后直接执行命令 make 就可以编译多个平台可执行文件,需要依赖golang编译器。</p><ul><li>Windows amd64: redis-tool-windows-amd64.exe</li><li>MacOS amd64: redis-tool-darwin-amd64</li><li>Linux amd64: redis-tool-linux-amd64</li><li>Linux arm64: redis-tool-linux-arm64</li></ul><p>查看使用方法:</p><pre><code> $ chmod a+x redis-tool-linux-amd64
$ ./redis-tool-linux-amd64 -h
</code></pre><h2>支持的数据类型</h2><ul><li>string 字符串</li><li>hash 散列列表</li><li>list 列表</li><li>sorted-set 有序集合</li></ul><h2>如何使用</h2><p>下载好命令并授权之后执行 ./redis-tool-linux-amd64 -h 可以查看该工具所支持的所有功能:</p><pre><code>$ ./redis-tool-darwin-amd64 migrate -h
数据迁移命令
Usage:
redis-tool migrate [command]
Examples:
支持命令:
[hash, set, sorted-set, list]
Available Commands:
all 迁移所有
hash 哈希列表迁移
list 列表迁移
set redis set 迁移
sorted-set 有序集合迁移
Flags:
-h, --help help for migrate
--source-auth string 源密码
--source-database int 源database
--source-hosts string 源redis地址, 多个ip用','隔开 (default "127.0.0.1:6379")
--source-prefix string 源redis前缀
--source-redis-cluster 源redis是否是集群
--target-auth string 目标密码
--target-database int 目标database
--target-hosts string 目标redis地址, 多个ip用','隔开 (default "127.0.0.1:6379")
--target-prefix string 目标redis前缀
--target-redis-cluster 目标redis是否是集群
Use "redis-tool migrate [command] --help" for more information about a command.
</code></pre><p>参数说明:</p><ul><li>--source-auth: 源redis密码,如果有的话就填</li><li>--source-database: 源database,默认是 0</li><li>--source-hosts: 源redis地址, 集群的多个ip用','隔开 (default "127.0.0.1:6379")</li><li>--source-prefix: 源redis前缀, 可不填</li><li>--source-redis-cluster: 源redis是否是集群, 默认 false</li><li>--target-auth: 迁移目标redis密码,如果有的话就填</li><li>--target-database: 迁移目标database,默认是 0</li><li>--target-hosts: 迁移目标redis地址, 集群的多个ip用','隔开 (default "127.0.0.1:6379")</li><li>--target-prefix: 迁移目标redis前缀, 可不填</li><li>--target-redis-cluster: 迁移目标redis是否是集群, 默认 false</li></ul><h2>迁移单个key的数据</h2><p>下面就举两个例子吧,其他的都差不太多。</p><h3>Hash类型</h3><p>可以通过命令 redis-tool migrate hash -h 查看使用说明</p><pre><code>$ redis-tool migrate hash helloworld \
--source-hosts 127.0.0.1:6379 \
--target-redis-cluster true \
--target-hosts 127.0.0.1:6379,127.0.0.1:7379 \
--target-auth 123456</code></pre><p><img src="/img/remote/1460000037655517" alt="" title=""></p><h3>有序集合</h3><p>可以通过命令 redis-tool migrate sorted-set -h 查看使用说明<br>有序集合的数据量可能会比较大,所以这里按 50000 为单位进行了切割。我这里测试过迁移近17000000万条的数据,用时40多分钟。</p><pre><code>$ redis-tool migrate hash helloworld \
--source-hosts 127.0.0.1:6379 \
--target-redis-cluster true \
--target-hosts 127.0.0.1:6379,127.0.0.1:7379 \
--target-auth 123456</code></pre><p><img src="/img/remote/1460000037655516" alt="" title=""></p><h3>迁移所有key的数据支持通配符过滤</h3><p>可以通过命令 redis-tool migrate all -h 查看使用说明</p><pre><code>$ redis-tool migrate all "ipdetect:*" \
--source-hosts 127.0.0.1:6379 \
--target-redis-cluster true \
--target-hosts 127.0.0.1:6379,127.0.0.1:7379 \
--target-auth 123456</code></pre><p>这个命令会编译匹配到的所有类型的key,再根据key的类型进行逐步迁移。</p><h2>尾巴</h2><p>使用golang写的一个比较简单的工具, 主要用于在Redis没有持久化或多套Redis向一套Redis迁移的情况下使用。</p><p>希望对大家有用,谢谢!</p><blockquote>作者:宜信技术学院 王聪</blockquote>
zookeeper浅谈
https://segmentfault.com/a/1190000037616360
2020-10-27T10:17:25+08:00
2020-10-27T10:17:25+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<h2>1、ZooKeeper是什么?</h2><p>ZooKeeper 是一个开源的分布式服务框架Hadoop的一个子项目,Zookeeper 实现诸如数据发布/订阅、统一命名服务、分布式协调/通知、配置管理、分布式锁和分布式队列等功能,通俗的讲zookeeper是一个支持增删查改的类似文件系统特点的数据库,按照规则去给节点分配任务。zookeeper底层实现了存储文件和通知回调功能它的数据结构类似于一个标准的文件系统,相比较文件系统zk的每个节点都可以存储数据,但是大小限制为1M。通常我们在使用dubbo的时候会建议使用zookeeper作为注册中心,也可以用redis,eureka作为注册中心,当然我只用过zookeeper,dubbo相当于搭载一个服务框架,zookeeper则是服务注册的中心。</p><p><img src="/img/remote/1460000037616364" alt="" title=""><br>zk的数据结构<br><img src="/img/remote/1460000037616363" alt="" title=""><br>zk服务的配置文件</p><p>上面提到zk就是一个数据库那么它的数据就储存在dataDir中,上图中的配置是一个集群配置,有server1,server2,server3三台服务器,我们这里是一个伪集群(同一台机器启动三个server),我们可以看到localhost:A:B,其中licalhost是我们的服务ip,A是专门用来选举的端口,B集群进行通信端口,clientPort是对client提供服务的端口。</p><h4>名词解释:</h4><p><strong>数据发布/订阅:</strong>初始化节点的时候在服务节点注册一个数据变更Watcher ,对节点进行变更操作的时候会将数据通知到客户端,客户端接受到变更通知后会重新读取变更后的数据。</p><p><strong>统一命名服务:</strong>获得全局的唯一名称,还可以借助znode顺序节点的特性产生的节点都会返回顺序编号,在按照给定的名字,生成具有特殊含义的统一名字,所有客户端可创建同一个名字的不同顺序节点。</p><h2>2、服务器的角色?以及状态</h2><p>服务器有Leader、Follower、Observer三种角色 ,其中Leader是集群内部各个服务的调度者,保证了事务处理的顺序性。Follower参与Proposal的投票,参与Leader选举投票,处理客户端的非事务请求,转发事务请求(增删改,数据变更的操作)给Leader服务器。Observer不参与投票,在不参与集群事务能力的基础上提升集群的非事务处理能力。</p><p>服务器的状态分别为LOOKING(认为进群中服务器没有Leader寻找Leader的状态)、FOLLOWING(服务器角色是Follower的状态)、LEADING(服务器角色是 Leader的状)、OBSERVING(服务器角色是Observer的状态)。</p><p>领导者选举发生的节点有Leader挂掉的时候,集群服务器启动的时候,Follower挂掉后Leader发现没有过半的Follower跟随了,这三种情况会触发领导者选举。</p><h2>3、zookeeper如何解决数据一致性问题?</h2><p>zookeeper server的启动过程经历了什么。</p><p>若要了解zookeepr如何解决数据一致性,zookeeper其实想达到的是强一致性,但是最终达到的是最终一致性,首先我们了解下什么是CAP?这个大家自行百度,ZK遵循的是CP原则,牺牲了可用性,满足了强一致性。如下图数据库A 的数据进行了变更为2后,在步骤2进行读取的时候不能读取到的是1,那么要求数据库之间同步非常迅速或者在步骤2上加上锁待数据同步完成后再读取到结果.</p><p><img src="/img/remote/1460000037616365" alt="" title=""></p><p>强一致性的例子</p><p>我们来大致跟下源码中的选举流程我用的是git上的3.6.1的版本,找到zkServer.sh</p><p><img src="/img/remote/1460000037616366" alt="" title=""><br>找到守护进程的启动脚本</p><p>找到参数中ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain"对应的这个类就是你查看源码服务的入口了。</p><p>在入口main方法中有一个初始化方法,main.initializeAndRun(args);这个方法进入以后图中标红的是进入集群模式的方法,我们来看这个方法。</p><p><img src="/img/remote/1460000037616367" alt="" title=""><br>判断为集群模式</p><p>进入方法之后你会看到一堆set,读取配置文件值到QuorumPeer这个对象中呢,然后是对象的start,在启动的时候就进行了调用选举方法。</p><h4>大家想一下zookeeper为何选择奇数服务器?</h4><p>这个要从zookeeper的过半机制说起,假如6台机器只最大允许集群中宕掉2台机器,5 台机器也是允许宕机两台,从资源利用的角度所以建议选择奇数台服务器.</p><p><img src="/img/remote/1460000037616369" alt="" title=""></p><p>标红的这块为//投票决定方式,默认超过半数就通过</p><p><img src="/img/remote/1460000037616370" alt="" title=""><br>标红的为leader选举方法</p><p><img src="/img/remote/1460000037616368" alt="" title=""><br>默认electionAlgorithm为3</p><p>在FastLeaderElection类中lookForLeader方法的case looking 条件下进行投票选举。private boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch)将收到的对方的投票与当前自己的投票对比,判断对方的投票是否优于自己的投票。</p><p><img src="/img/remote/1460000037616371" alt="" title=""><br>totalOrderPredicate</p><p>只要当前服务器状态为LOOKING,进入循环,不断地读取其它Server发来的通知、进行比较、更新自己的投票、发送自己的投票、统计投票结果,直到leader选出或出错退出。</p><h3>选举比重参数</h3><p>①Serverid:服务器ID比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大。</p><p>②Zxid:事务日志id,事务请求每次就会生成一条事务日志,服务器中存放的最大数据ID.值越大说明数据越新,在选举算法中数据越新权重越大。</p><p>③Epoch:逻辑时钟,或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比</p><h3>集群启动投票流程</h3><p>①每个Server会发出一个投票,因此对于Server1,Server2和Server3来说,都会将自己作为Leader服务器来进行投票,每次投票包含最基本的元素有:所推举的服务器的myid和zxid,我们以(myid,zxid)的形式来表示,即Server1的投票为(1,0),Server2的投票为(2,0),然后各自将这个投票发给集群中其他所有机器。</p><p>② 接收来自各个服务器的投票,判断该投票的有效性,包括检查是否是本轮投票,是否来自LOOKING状态的服务器。</p><p>③ pk投票,在接收到来自其他服务器的投票后,针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK:</p><ol><li>优先检查zxid,zxid比较大的服务器优先作为Leader。</li><li>如果zxid相同的话,那么就比较myid,myid比较大的服务器作为Leader服务器。结果Server1{(2,0),(2,0)},Server2{(2,0),2,0)}将票投给了Server2,那么Server3也就直接跟随投给了Sever2,最终确定了Leader。</li></ol><blockquote>作者:宜信技术学院 王巧敏</blockquote>
处理一次k8s、calico无法分配podIP的心路历程
https://segmentfault.com/a/1190000037455140
2020-10-13T14:08:01+08:00
2020-10-13T14:08:01+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p>又一次偷偷化解了可能发生的重大事故。不想看过程的可以直接跳到末尾看处理方案。</p><h2>一个网络错误</h2><p>某天,上kplcloud构建一个测试应用,构建完成之后发现新pod一直启动失败,并且抛出了以下错误信息:</p><pre><code>Failed create pod sandbox: rpc error: code = Unknown desc = NetworkPlugin cni failed to set up pod "xxxxxx-fc4cb949f-gpkm2_xxxxxxx" network: netplugin failed but error parsing its diagnostic message "": unexpected end of JSON input</code></pre><p><img src="/img/remote/1460000037455144" alt="" title=""></p><p>会k8s的运维同学早已不在,突然出问题了怎么办?</p><p>试着开始解决问题。</p><h4>一、有没有可能是镜像拉取失败,开始找问题:</h4><ol><li>登录集群所有服务器查看空间是否占满(然而并没有占满)</li><li>查询集群所有服务器网络情况(也没有问题)</li><li>再启一个pod试试?(起不来)</li></ol><p>这就尴尬了......,有没有可能是calico的问题?</p><h4>二、查看服务器报错信息</h4><p>尝试以下命令看服务器的报错信息:</p><pre><code>$ journalctl -exf </code></pre><p>确实有一些错误信息:</p><p><img src="/img/remote/1460000037455143" alt="" title=""></p><p>这个错误太广泛了,继续尝试从其他地方找找问题。</p><p><em>此时已经开始在思考如何跑路的问题了...</em></p><p>要不尝试从重启能否解决? </p><p>风险太大,不能冒险。虽然很多时候重启能解决大部分问题,但重启docker、k8s在这种情况下不是最佳选择。</p><p><img src="/img/remote/1460000037455146" alt="" title=""></p><p>继续搜刮日志,猜测是无法分配IP的问题,那目标转向calico</p><p>从calico-node上面找问题</p><p>查询ip池是否用完。</p><p>使用calicoamd命令查询calico是否正常正常运行</p><pre><code>$ calicoctl get ippools -o wide
CIDR NAT IPIP
172.20.0.0/16 true false
$ calicoctl node status</code></pre><p><img src="/img/remote/1460000037455145" alt="" title=""></p><p>似乎是没啥问题。</p><p>开始场外求助......</p><p>无果</p><p><img src="/img/remote/1460000037455147" alt="" title=""></p><p>既然calico-node都运行正常,应该不会是calico-etcd的问题吧。</p><h3>试试calico-etcd</h3><p>本着有疑问就查证试试的态度,下面开始对calico-etcd进行一顿骚操作。</p><blockquote>为了减少代码量方便阅读,以下etcdctl所需要加的证书及endpoints,就不一一添加了,大家参考一下就好:</blockquote><pre><code>ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem \
--cert=/etc/etcd/ssl/etcd.pem \
--key=/etc/etcd/ssl/etcd-key.pem \
--endpoints=http://10.xx.xx.1:2379,http://10.xx.xx.2:2379,http://10.xx.xx.3:2379</code></pre><p>calico并没有问题,试试calico 所使用的 ETCD是否正常,进入calico-etcd集群:</p><pre><code>$ ETCDCTL_API=3 etcdctl member list
bde98346d77cfa1: name=node-1 peerURLs=http://10.xx.xx.1:2380 clientURLs=http://10.xx.xx.1:2379 isLeader=true
299fcfbf514069ed: name=node-2 peerURLs=http://10.xx.xx.2:2380 clientURLs=http://10.xx.xx.2:2379 isLeader=false
954e5cdb2d25c491: name=node-3 peerURLs=http://10.xx.xx.3:2380 clientURLs=http://10.xx.xx.3:2379 isLeader=false</code></pre><p>似乎集群也运行正常,get数据也正常。</p><p>一切看起来都感觉是多么的正常,似乎没有什么毛病。</p><p>算了,算了,还是先写会简历吧,换换脑子。</p><p><img src="/img/remote/1460000037455148" alt="" title=""></p><p>那尝试向ETCD写入一条数据试试?</p><pre><code>$ ETCDCTL_API=3 etcdctl put /hello world
Error: etcdserver: mvcc: database space exceeded</code></pre><p>报了一个错<strong>Error: etcdserver: mvcc: database space exceeded</strong>???</p><p><img src="/img/remote/1460000037455150" alt="" title=""></p><p>似乎是找到原因了,既然定位到问题所在,那接下来就好办了。<em>(不用跑路了(⁎⁍̴̛ᴗ⁍̴̛⁎))</em>把简历先放一放。</p><p>感谢伟大的google,我从etcd官网找到了一些线索及解决方案,后面我贴上官网介绍,先解决问题:</p><p>使用<code>etcdctl endpoint status</code>查询etcd各个节点的使用状态:</p><pre><code>$ ETCDCTL_API=3 etcdctl endpoint status
http://10.xx.xx.1:2379, 299fcfbf514069ed, 3.2.18, 2.1 GB, false, 7, 8701663
http://10.xx.xx.2:2379, bde98346d77cfa1, 3.2.18, 2.1 GB, true, 7, 8701683
http://10.xx.xx.3:2379, 954e5cdb2d25c491, 3.2.18, 2.1 GB, false, 7, 8701687</code></pre><p>上面可以看到集群空间已经使用了<strong>2.1GB</strong>了,这个值需要留意一下。</p><p>查询etcd是否有告警信息使用命令<code>etcdctl alarm list</code>:</p><pre><code>$ ETCDCTL_API=3 etcdctl alarm list
memberID:2999344297460918765 alarm:NOSPACE</code></pre><p>显示了一个<code>alerm:NOSPACE</code>,这个表示没空间了,那是没什么空间呢?磁盘还是内存?先查询一下。</p><p><img src="/img/remote/1460000037455149" alt="" title=""></p><p>似乎磁盘、内存空间都足够的。从官网的信息了解到应该是etcd配额的问题,Etcd v3 的默认的 backend quota 2GB,也就是说etcd默认最大的配额是2GB,如果超过了则无法再写入数据,要么把旧数据删除,要么把数据压缩了。</p><p><strong>参考官方的解决方案</strong></p><p>ETCD官网参考:<a href="https://link.segmentfault.com/?enc=ouf5fCSyJdSBl9nOzUMUEg%3D%3D.y%2BSe4h8MRP7SRPj6t32JcmT%2BYir%2B7NMXe5QJ3h9L%2F%2Fpc8aXgXOCgPLavBG%2BRvwXFa7LPZ6XSmUs2ikfETpDUuQ%3D%3D" rel="nofollow">https://etcd.io/docs/v3.2.17/op-guide/maintenance/</a></p><ol><li><p>获取etcd的旧版本号</p><pre><code>$ ETCDCTL_API=3 etcdctl endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9].*'
5395771
5395771
5395771</code></pre></li><li><p>压缩旧版本</p><pre><code>$ ETCDCTL_API=3 etcdctl compact 5395771
compacted revision 5395771</code></pre></li><li><p>整理碎片</p><pre><code>$ ETCDCTL_API=3 etcdctl defrag
Finished defragmenting etcd member[http://10.xx.xx.1:2379]
Finished defragmenting etcd member[http://10.xx.xx.2:2379]
Finished defragmenting etcd member[http://10.xx.xx.3:2379]</code></pre></li><li><p>关闭告警</p><pre><code>$ ETCDCTL_API=3 etcdctl alarm disarm
memberID:2999344297460918765 alarm:NOSPACE
$ ETCDCTL_API=3 etcdctl alarm list</code></pre></li><li><p>测试数据是否可写入</p><pre><code>$ ETCDCTL_API=3 etcdctl put /hello world
OK
$ ETCDCTL_API=3 etcdctl get /hello
OK</code></pre></li></ol><p>回到k8s这边,删除那个失败的pod,并查看是否可正常分配ip。</p><p>一切正确,完美。</p><p><img src="/img/remote/1460000037455151" alt="" title=""></p><p>为了避免后续再出现类似问题,需要设置自动压缩,启动自动压缩功能需要在etcd启动参考上加上<code>xxxxx=1</code></p><p><a href="https://link.segmentfault.com/?enc=iDl2nR859RP7FFrsmV%2BSWQ%3D%3D.NXNPxL2Hu519%2Br6eVdi4eWR786wiTIN3IceFeIpGxf5FbIZUIr9VVqD4h0urRysS5ZG%2BHiaVlrVIT%2F9OUQvc1DFHPf1J0eWkoK4YMQqsqwEChuJwBmdYKI5%2FbHpdx1Fs" rel="nofollow">https://skyao.gitbooks.io/lea...</a></p><p><img src="/img/remote/1460000037455153" alt="" title=""></p><blockquote>etcd 默认不会自动 compact,需要设置启动参数,或者通过命令进行compact,如果变更频繁建议设置,否则会导致空间和内存的浪费以及错误。Etcd v3 的默认的 backend quota 2GB,如果不 compact,boltdb 文件大小超过这个限制后,就会报错:<code>”Error: etcdserver: mvcc: database space exceeded”</code>,导致数据无法写入。</blockquote><p>产生这么多垃圾数据的原因就是因为频繁的调度,我们集群有大量CronJob在执行,并且执行的非常活跃,每次产生新的Pod都会被分配到ip。有可能是因为pod时间太短或没有及时注销而导致calico-etcd产生了大量垃圾数据。</p><h2>尾巴</h2><p><img src="/img/remote/1460000037455152" alt="" title=""></p><p>因calico-etcd集群的的使用配额满了,在创建pod时calico所分配的IP无法写入到etcd里,从而导致pod创建失败也就无法注册到CoreDNS了。</p><p>为了不采坑,监控是非常重要的,我们有etcd集群的监控,却忽略了etcd配额的监控,幸运的是当时并没有应用重启动或升级,没有造成损失。</p><p>最后的建议就是,没事上去点点,说不定会有您意想不到的惊喜(惊吓)。</p><p><img src="/img/remote/1460000037455154" alt="" title=""></p><p><img src="/img/remote/1460000037455155" alt="" title=""></p><blockquote> 作者:宜信技术学院 王聪</blockquote>
彻底深刻理解js原型链之prototype,__proto__以及constructor(一)
https://segmentfault.com/a/1190000024562153
2020-09-23T18:42:43+08:00
2020-09-23T18:42:43+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
8
<h2>前言</h2><blockquote>以下概念请花费一定的时间彻底理解,才能进行下一步,思考题一定要思考,这样才能彻底掌握原型链的知识点,教程中如果有任何的错误不足请指正!</blockquote><h2>函数对象</h2><p>由function创造出来的函数,比如:</p><pre><code>
function a(){};
var b=function(){};
</code></pre><p>系统内置的函数对象</p><pre><code>Function,Object,Array,String,Number
</code></pre><p>只有函数对象才有 prototype属性 ,重要的事情说三遍!</p><blockquote>思考: js的引用数据类型都属于函数对象吗?</blockquote><h2>普通对象</h2><p>除开函数对象之外的对象都是普通对象</p><pre><code>var b='qwe'; // b 是字符串类型,属于普通对象
var c=123;; // c 是数字类型,属于普通对象
</code></pre><blockquote>思考:js有五种基本类型:Undefined,Null,Boolean,Number和String,他们都是属于普通对象吗?</blockquote><h2>原型对象</h2><p>prototype属性也叫原型对象,主要是为了实现继承和共享属性;</p><p>可以说我们的每一次编程,内在都有原型对象来发挥着作用,如果你没有掌握原型对象的含义,那么你的js还没有真正的入门!</p><pre><code>function a(){};
</code></pre><p>首先对象 a 是由Function创造出来,是函数对象;那么根据我们以上的教程,a 就有了prototype属性,那么这个原型对象是怎么创造出来的呢?<br>来看下面这个例子:</p><pre><code>var temp = new a();
a.prototype=new Object();
a.prototype = temp;
</code></pre><p>那么a的prototype属性就是这样创造出来的;</p><blockquote>思考:原型对象prototype 属于函数对象吗?</blockquote><h2>指针__proto__</h2><p>JavaScript中,万物皆对象!所有的对象obj都具有proto属性(null和undefined除外),可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型</p><p>请看以下例子帮助理解:</p><pre><code>function a(){};
var obj=new a();
console.log(a.__proto__===Function.prototype); //true
console.log(a.prototype.__proto__===Object.prototype); //true
console.log(obj.__proto__===a.prototype); //true</code></pre><blockquote>思考一下,var obj={}; obj.prototype.__proto__指向谁?</blockquote><h2>构造函数属性constructor</h2><p>假设 obj 是由函数对象 a 由new运算创造出来的,那么obj的constructor 的属性就存放着一个对 a 的引用,通过这个构造函数,我们还可以为 a添加其他属性和方法,<br>这个属性的最初设计是为了检测对象的数据类型,不过后来人们通过此属性的特性做了更多的事情</p><p>请看以下例子:</p><pre><code>function a(){};
var obj=new a();
obj.constructor.b=`我是a的新的属性`;
console.log(a.b); //我是a的新的属性
console.log(a.constructor===Function); //true
console.log(a.prototype.constructor===a); //true
console.log(obj.constructor===a); //true</code></pre><p>函数a是由Function创造出来,那么它的constructor指向的Function,obj是由new a()方式创造出来,那么obj.constructor理应指向a</p><blockquote>思考:a.prototype.__proto__.constructor指向谁?</blockquote><h2>思考题解答</h2><h3>函数对象思考题解答</h3><blockquote>思考: js的引用数据类型都属于函数对象吗?</blockquote><p>引用类型值:指的是那些保存在堆内存中的对象,意思是,变量中保存的实际上只是一个指针,这个指针执行内存中的另一个位置,由该位置保存对象</p><p>那么数组,普通对象,函数对象都算是引用数据类型,引用数据类型范围包含函数对象的范围</p><h3>普通对象思考题解答</h3><blockquote>思考:js有五种基本类型:Undefined,Null,Boolean,Number和String,他们都是属于普通对象吗?</blockquote><p>基本类型值:指的是保存在栈内存中的简单数据段;除开函数对象之外的对象都是普通对象,那么普通对象范围是包含基本数据类型的</p><blockquote>事实上(函数对象,普通对象)以及(基本数据类型,引用数据类型)是在不同角度对js变量进行的定义</blockquote><h3>原型对象思考题解答</h3><blockquote>思考:原型对象prototype 属于函数对象吗?</blockquote><p>事实上 这个问题要进行分别回答:</p><p>Function.prototype 属于函数对象,其他对象的prototype属于普通对象</p><pre><code>function a(){};
console.log(typeof Function.prototype); // function
console.log(typeof a.prototype); //object</code></pre><p>前面说过prototype的创造过程</p><pre><code> var temp = new a();
a.prototype = temp;</code></pre><p>这里temp当然就是普通对象啦,但是看下Function的prototype创造过程</p><pre><code>var a = new Function();
Function.prototype = a;</code></pre><p>看明白了把,Function的prototype为什么是函数对象了吧?回忆一下函数对象的定义吧!</p><h3>指针__proto__思考题解答</h3><blockquote>思考一下,var obj={}; obj.prototype.__proto__指向谁?</blockquote><p>这里分步思考:<br>1, obj只是一个普通对象<br>2, 什么类型的对象是有prototype属性的?当然是函数对象<br>3, 所以obj是没有prototype属性的<br>4, 所以obj.prototype===undefined<br>5, 所以此题的最终问题是:undefined.proto指向什么<br>6, 所有的对象obj都具有proto属性(null和undefined除外)!所以答案是 js报错(有没有一种被我坑了的感觉)</p><h3>构造器constructor思考题解答</h3><blockquote>思考:a.prototype.__proto__.constructor指向谁?</blockquote><pre><code>function a(){};</code></pre><p>这里继续分解题目:<br>1, a.prototype指向a的一个实例,我们已经多次强调了,而且属于普通对象<br>2, __proto__定义为:指向创造obj对象的函数对象的prototype属性,所以看下谁创造了a.prototype,因为a.prototype是普通对象,类型为object,那么是Object创造了它,<br>3, 那么显而易见a.prototype.__proto__指向了Object.prototype<br>4, 那么题目简化为Object.prototype.constructor指向谁<br>5, 继续分解题目,Object.prototype为基本对象,那么就是Object创造了它,那么它的constructor就指向了Object</p><pre><code>Object.prototype.constructor===Object //true</code></pre><p>不知道你晕不晕,我有点晕,这产生了蛋生鸡还是鸡生蛋的问题啦~</p><p>放心,还是有尽头的 :</p><pre><code>Object.prototype.__proto__===null //true</code></pre><p>这个例子告诉我们是 是null创造了一切““这不就是易经中的:道生一,一生二,二生三,三生万物!</p>
论程序的健壮性——就看Redis
https://segmentfault.com/a/1190000024481183
2020-09-16T16:21:00+08:00
2020-09-16T16:21:00+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
4
<blockquote>“众里寻他千百度,蓦然回首,那人却在,灯火阑珊处”。多年的IT生涯,一直希望自己写的程序能够有很强的健壮性,也一直希望能找到一个高可用的标杆程序去借鉴学习,不畏惧内存溢出、磁盘满了、断网、断电、机器重启等等情况。但意想不到的是,这个标杆程序竟然就是从一开始就在使用的分布式缓存——Redis。</blockquote><p><img src="/img/remote/1460000024481186" alt="" title=""> <br>Redis(Remote Dictionary Server ),即远程字典服务,是 C 语言开发的一个开源的高性能键值对(key-value)的内存数据库。由于它是基于内存的所以它要比基于磁盘读写的数据库效率更快。因此Redis也就成了大家解决数据库高并发访问、分布式读写和分布式锁等首选解决方案。</p><p>那么既然它是基于内存的,如果内存满了怎么办?程序会不会崩溃?既然它是基于内存的,如果服务器宕机了怎么办?数据是不是就丢失了?既然它是分布式的,这台Redis服务器断网了怎么办?</p><p>今天我们就一起来看看Redis的设计者,一名来自意大利的小伙,是如何打造出一个超强健壮性和高可用性的程序,从而不惧怕这些情况。</p><h2>一、 Redis的内存管理策略——内存永不溢出</h2><p>Redis主要有两种策略机制来保障存储的key-value数据不会把内存塞满,它们是:过期策略和淘汰策略。</p><h3>1、 过期策略</h3><p>用过Redis的人都知道,我们往Redis里添加key-value的数据时,会有个选填参数——过期时间。如果设置了这个参数的值,Redis到过期时间后会自行把过期的数据给清除掉。“过期策略”指的就是Redis内部是如何实现将过期的key对应的缓存数据清除的。</p><p>在Redis源码中有三个核心的对象结构:redisObject、redisDb和serverCron。</p><ul><li>redisObject:Redis 内部使用redisObject 对象来抽象表示所有的 key-value。简单地说,redisObject就是string、hash、list、set、zset的父类。为了便于操作,Redis采用redisObject结构来统一这五种不同的数据类型。</li></ul><p><img src="/img/remote/1460000024481187" alt="" title=""></p><ul><li>redisDb:Redis是一个键值对数据库服务器,这个数据库就是用redisDb抽象表示的。redisDb结构中有很多dict字典保存了数据库中的所有键值对,这些字典就叫做键空间。如下图所示其中有个“expires”的字典就保存了设置过期时间的键值对。而Redis的过期策略也是围绕它来进行的。<p><img src="/img/remote/1460000024481188" alt="" title=""></p></li><li>serverCron:Redis 将serverCron作为时间事件来运行,从而确保它每隔一段时间就会自动运行一次。因此redis中所有定时执行的事件任务都在serverCron中执行。</li></ul><p>了解完Redis的三大核心结构后,咱们回到“过期策略”的具体实现上,其实Redis主要是靠两种机制来处理过期的数据被清除:定期过期(主动清除)和惰性过期(被动清除)。</p><ul><li><strong>惰性过期(被动清除)</strong>:就是每次访问的时候都去判断一下该key是否过期,如果过期了就删除掉。该策略就可以最大化地节省CPU资源,但是却对内存非常不友好。因为不实时过期了,原本该过期删除的就可能一直堆积在内存里面!极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。</li><li><strong>定期过期(主动清除)</strong>:每隔一定的时间,会扫描Redis数据库的expires字典中一定数量的key,并清除其中已过期的 key。Redis默认配置会每100毫秒进行1次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典中的所有键,而是采用了如下方法:</li></ul><p>(1)从过期字典中随机取出20个键;<br>(server.h文件下<code>ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP</code>配置20)</p><p>(2)删除这20个键中过期的键;</p><p>(3)如果过期键的比例超过 25% ,重复步骤 1 和 2;</p><p>具体逻辑如下图:</p><p><img src="/img/remote/1460000024481189" alt="" title=""></p><p>因为Redis中同时使用了惰性过期和定期过期两种过期策略,所以在不同情况下使得 CPU 和内存资源达到最优的平衡效果的同时,保证过期的数据会被及时清除掉。</p><h3>2、淘汰策略</h3><p>在Redis可能没有需要过期的数据的情况下,还是会把我们的内存都占满。比如每个key设置的过期时间都很长或不过期,一直添加就有可能把内存给塞满。那么Redis又是怎么解决这个问题的呢?——那就是“淘汰策略”。</p><p><img src="/img/remote/1460000024481190" alt="" title=""></p><p>官网地址:<a href="https://link.segmentfault.com/?enc=Bnj40xjah013Vot%2BzLPuKw%3D%3D.NJ%2F04YFC%2BSm4nNaU%2BB3IYtUWTplOS66wd4ApuEyu1ShEfs%2B5VneWpETqVNxTDul0" rel="nofollow">https://redis.io/topics/lru-c...</a><br>Reids官网上面列出的淘汰策略一共有8种,但从实质算法来看只有两种实现算法,分别是LRU和LFU。</p><p><strong>LRU(Least Recently Used)</strong>:翻译过来是最久未使用,根据时间轴来走,淘汰那些距离上一次使用时间最久远的数据。<br>LRU的简单原理如下图:</p><p><img src="/img/remote/1460000024481191" alt="" title=""></p><p>从上图我们可以看出,在容器满了的情况下,距离上次读写时间最久远的E被淘汰掉了。那么数据每次读取或者插入都需要获取一下当前系统时间,以及每次淘汰的时候都需要拿当前系统时间和各个数据的最后操作时间做对比,这么干势必会增加CPU的负荷从而影响Redis的性能。Redis的设计者为了解决这一问题,做了一定的改善,整体的LRU思路如下:</p><p>(1)、Redis里设置了一个全局变量 server.lruclock 用来存放系统当前的时间戳。这个全局变量通过serverCron 每100毫秒调用一次updateCachedTime()更新一次值。</p><p>(2)、每当redisObject数据被读或写的时候,将当前的 server.lruclock值赋值给 redisObject 的lru属性,记录这个数据最后的lru值。</p><p>(3)、触发淘汰策略时,随机从数据库中选择采样值配置个数key, 淘汰其中热度最低的key对应的缓存数据。</p><blockquote>注:热度就是拿当前的全局server.lruclock 值与各个数据的lru属性做对比,相差最久远的就是热度最低的。</blockquote><p><img src="/img/remote/1460000024481192" alt="" title=""></p><p>Redis中所有对象结构都有一个lru字段, 且使用了unsigned的低24位,这个字段就是用来记录对象的热度。</p><p><strong>LFU(Least Frequently Used)</strong>:翻译成中文就是最不常用。是按着使用频次来算的,淘汰那些使用频次最低的数据。说白了就是“末尾淘汰制”!<br>刚才讲过的LRU按照最久未使用虽然能达到淘汰数据释放空间的目的,但是它有一个比较大的弊端,如下图:</p><p><img src="/img/remote/1460000024481193" alt="" title=""></p><p>如图所示A在10秒内被访问了5次,而B在10秒内被访问了3 次。因为 B 最后一次被访问的时间比A要晚,在同等的情况下,A反而先被回收。那么它就是不合理的。LFU就完美解决了LRU的这个弊端,具体原理如下:</p><p><img src="/img/remote/1460000024481194" alt="" title=""></p><p>上图是末尾淘汰的原理示意图,仅是按次数这个维度做的末尾淘汰,但如果Redis仅按使用次数,也会有一个问题,就是某个数据之前被访问过很多次比如上万次,但后续就一直不用了,它本身按使用频次来讲是应该被淘汰的。因此Redis在实现LFU时,用两部分数据来标记这个数据:使用频率和上次访问时间。整体思路就是:有读写我就增加热度,一段时间内没有读写我就减少相应热度。</p><p><img src="/img/remote/1460000024481195" alt="" title=""></p><p>不管是LRU还是LFU淘汰策略,Redis都是用lru这个字段实现的具体逻辑,如果配置的淘汰策略是LFU时,lru的低8位代表的是频率,高16位就是记录上次访问时间。整体的LRU思路如下:</p><p>(1)每当数据被写或读的时候都会调用LFULogIncr(counter)方法,增加lru低8位的访问频率数值;具体每次增加的数值在redis.conf中配置默认是10(# lfu-log-factor 10)</p><p>(2)还有另外一个配置lfu-decay-time 默认是1分钟,来控制每隔多久没人访问则热度会递减相应数值。这样就规避了一个超大访问次数的数据很久都不被淘汰的漏洞。</p><blockquote>小结:“过期策略” 保证过期的key对应的数据会被及时清除;“淘汰策略”保证内存满的时候会自动释放相应空间,因此Redis的内存可以自运行保证不会产生溢出异常。</blockquote><h2>二、 Redis的数据持久化策略——宕机可立即恢复数据到内存</h2><p>有了内存不会溢出保障后,我们再来看看Redis是如何保障服务器宕机或重启,原来缓存在内存中的数据是不会丢失的。也就是Redis的持久化机制。</p><p>Redis 的持久化策略有两种:RDB(快照全量持久化)和AOF(增量日志持久化)</p><h3>1、 RDB</h3><p>RDB 是 Redis 默认的持久化方案。RDB快照(Redis DataBase),当触发一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启会通过dump.rdb文件恢复数据。那那个一定的条件是啥呢?到底什么时候写入rdb 文件?</p><p>触发Redis执行rdb的方式有两类:自动触发和手动触发<br><strong>“自动触发”</strong>的情况有三种:达到配置文件触发规则时触发、执行shutdown命令时触发、执行flushall命令时触发。</p><blockquote>注:在redis.conf中有个 SNAPSHOTTING配置,其中定义了触发把数据保存到磁盘触发频率。</blockquote><p><strong>“手动触发”</strong>的方式有两种:执行save 或 bgsave命令。执行save命令在生成快照的时候会阻塞当前Redis服务器,Redis不能处理其他命令。如果内存中的数据比较多,会造成Redis长时间的阻塞。生产环境不建议使用这个命令。</p><p>为了解决这个问题,Redis 提供了第二种方式bgsave命令进行数据备份,执行bgsave时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。</p><p>具体操作是Redis进程执行fork(创建进程函数)操作创建子进程(copy-on-write),RDB持久化过程由子进程负责,完成后自动结束。它不会记录 fork 之后后续的命令。阻塞只发生在fork阶段,一般时间很短。手动触发的场景一般仅用在迁移数据时才会用到。</p><p>我们知道了RDB的实现的原理逻辑,那么我们就来分析下RDB到底有什么优劣势。</p><h4>优势:</h4><p>(1)RDB是一个非常紧凑(compact类型)的文件,它保存了redis在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。</p><p>(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。</p><p>(3)RDB在恢复大数据集时的速度比AOF的恢复速度要快。</p><h4>劣势:</h4><p>RDB方式数据没办法做到实时持久化/秒级持久化。在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照之后的所有修改</p><h3>2、 AOF(Append Only File)</h3><p>AOF采用日志的形式来记录每个写操作的命令,并追加到文件中。开启后,执行更改 Redis数据的命令时,就会把命令写入到AOF文件中。Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。</p><p><img src="/img/remote/1460000024481196" alt="" title=""></p><p>其实AOF也不一定是完全实时的备份操作命令,在redis.conf 我们可以配置选择 AOF的执行方式,主要有三种:always、everysec和no</p><p>AOF是追加更改命令文件,那么大家想下一直追加追加,就是会导致文件过大,那么Redis是怎么解决这个问题的呢?<br>Redis解决这个问题的方法是AOF下面有个机制叫做bgrewriteaof重写机制,我们来看下它是个啥</p><p><img src="/img/remote/1460000024481197" alt="" title=""></p><blockquote>注:AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。</blockquote><p>我们知道了AOF的实现原理,我们来分析下它的优缺点。</p><h4>优点:</h4><p>能最大限度的保证数据安全,就算用默认的配置everysec,也最多只会造成1s的数据丢失。</p><h4>缺点:</h4><p>数据量比RDB要大很多,所以性能没有RDB好!</p><p><img src="/img/remote/1460000024481200" alt="" title=""></p><blockquote>小结:因为有了持久化机制,因此Redis即使服务器宕机或重启了,也可以最大限度的恢复数据到内存中,提供给client继续使用。</blockquote><h2>三、Redis的哨兵模式——可战到最后一兵一卒的高可用集群</h2><p>内存满了不会挂,服务器宕机重启也没问题。足见Redis的程序健壮性已经足够强大。但Redis的设计者,在面向高可用面前,仍继续向前迈进了一步,那就是Redis的高可用集群方案——哨兵模式。</p><p>所谓的“哨兵模式”就是有一群哨兵(Sentinel)在Redis服务器前面帮我们监控这Redis集群各个机器的运行情况,并且哨兵间相互通告通知,并指引我们使用那些健康的服务。</p><p><img src="/img/remote/1460000024481198" alt="" title=""></p><h3>Sentinel工作原理:</h3><p>1、 Sentinel 默认以每秒钟1次的频率向Redis所有服务节点发送 PING 命令。如果在down-after-milliseconds 内都没有收到有效回复,Sentinel会将该服务器标记为下线(主观下线)。</p><p>2、 这个时候Sentinel节点会继续询问其他的Sentinel节点,确认这个节点是否下线, 如果多数 Sentinel节点都认为master下线,master才真正确认被下线(客观下线),这个时候就需要重新选举master。</p><h3>Sentinel的作用:</h3><p>1、监控:Sentinel 会不断检查主服务器和从服务器是否正常运行</p><p>2、故障处理:如果主服务器发生故障,Sentinel可以启动故障转移过程。把某台服务器升级为主服务器,并发出通知</p><p>3、配置管理:客户端连接到 Sentinel,获取当前的 Redis 主服务器的地址。我们不是直接去获取Redis主服务的地址,而是根据sentinel去自动获取谁是主机,即使主机发生故障后我们也不用改代码的连接!</p><p><img src="/img/remote/1460000024481199" alt="" title=""></p><blockquote>小结:有了“哨兵模式”只要集群中有一个Redis服务器还健康存活,哨兵就能把这个健康的Redis服务器提供给我们(如上图的1、2两步),那么我们客户端的链接就不会出错。因此,Redis集群可以战斗至最后一兵一卒。</blockquote><p>这就是Redis,一个“高可用、强健壮性”的标杆程序!</p><blockquote>作者:宜信技术学院 谭文涛</blockquote>
宜信OCR技术探索之版面分析业务实践|技术沙龙直播速记
https://segmentfault.com/a/1190000023919742
2020-09-07T14:58:54+08:00
2020-09-07T14:58:54+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p><a href="https://link.segmentfault.com/?enc=qVBxxJLm3jLjFSiTnGdw9A%3D%3D.laCzIGsEIKh9s5XCaMC%2B3nyP%2BpBmbx0oOUSrPkwV30I2ZEAkAit%2Buca5AVbXPs%2FI" rel="nofollow">直播视频回放:https://v.qq.com/x/page/i3135lgkagd.html</a></p><h2>一、项目背景</h2><p>业务端大量的新增数据来自纸质报告、电子邮件、文档、图像、视频等非结构化内容。据统计,业务线对于80%的非结构化内容无法有效管理,60%的管理人员在决策时无法获得关键信息,50%的信息内容无法为公司带来业务价值。</p><h3>解决痛点</h3><p>1、降本增效:帮助客户减少人力投入,解放传统OCR识别场景耗费的时间,提升工作效率。</p><p>2、关键信息提取:涉及多类复杂场景,理解识别文档内容、提取关键信息,为风险控制、营销扩展、流程优化做支撑。</p><p>3、识别准确率、速度、安全性、稳定性:基于人工智能的深度学习算法解决传统OCR识别率低、模版固定、设备依赖的问题。</p><h3>项目目标</h3><p><img src="/img/remote/1460000023919745" alt="" title=""></p><p>我们的目标是,由最左侧银行单据图像,经由AI模块,识别出带有坐标和文字内容的半结构化数据,再经版面分析模块解析出业务可理解的结构化数据。其中蓝色框的过程就是我们今天讲解的版面分析模块过程,也就是说从AI识别结果到版面分析结果。两种过程也是AI技术和编程技术的结合的一种表现。</p><h3>版面分析现状</h3><p>前期我们对行业内版面分析技术进行调研,查阅文档,查找一些大厂公开的解决方案,借鉴其中部分经验,结合实际场景需求,研发人员依次突破了行列识别、模板、结构化的技术难点,并进行总结、抽象和优化,提取出一套较为统一的OCR版面分析解决方案。</p><h2>二、抽象行列识别</h2><h3>行列识别介绍</h3><p><img src="/img/remote/1460000023919746" alt="" title=""></p><ul><li>那么什么是行列识别?</li></ul><p>行列识别即将AI模块识别回来的坐标块,依据一定方法,分辨出哪些块,在逻辑上属于同一行或同一列</p><ul><li>为什么要进行行列识别?</li></ul><p>版面分析开发中,行列识别是结构化的前提条件</p><ul><li>如何进行行列识别?</li></ul><p>在研发过程中,形成了很多行列识别方法,我们挑几个典型方法介绍</p><h3>行列识别抽象方案演进</h3><p><img src="/img/remote/1460000023919747" alt="" title=""></p><p>方法一:</p><p>按标题识别<br>根据已识别出的标题坐标,可以覆盖到该列范围,再根据列顺序判断行号</p><p>缺点:</p><p>1、标题文字识别不准确或未识别到标题</p><p>2、标题左右粘连(即识别到一个块中)</p><p>3、中间串行导致行号不正确</p><p><img src="/img/remote/1460000023919748" alt="" title=""></p><p>方法二:</p><p>属于标题法的升级版,针对多数场景,行的作用大于列,识别出行就可以进行结构化解析了,因标题过多,全识别成功率低,那么只要知道最后一列的位置横坐标范围,在根据纵坐标排序,一旦某一块属于最后一列,那么后面的就一定是属于下一行了</p><p>问题:<br>和方法一类似,最后一列标题也可能会识别失败,部分模板,最后一列还可能受盖章影响</p><p><img src="/img/remote/1460000023919749" alt="" title=""></p><p>方法三:</p><p>根据模板数据特点,参考经验值设置数据块平均高度,再从标题下边开始,把数据根据平均高度切割行</p><p>问题:</p><p>行高度是经验值,不一定靠谱,例如图片分辨率就可能会有影响</p><p><img src="/img/remote/1460000023919750" alt="" title=""></p><p>方法四:投影法</p><p>把所有数据块的竖边投射到右侧,重叠的部分即属于同一行</p><p>优点:<br>方法效率高,可封装,为开发屏蔽细节</p><p>缺点:</p><p>有较长干扰块,会把大部分块包含进去,密集数据也会混乱</p><p><img src="/img/remote/1460000023919752" alt="#### 俄罗斯方块方法" title="#### 俄罗斯方块方法"></p><p><img src="/img/remote/1460000023919751" alt="" title=""></p><p>俄罗斯方块法<br>1、按横坐标分别排序</p><p>2、从第一个数据块开始放入第i列集合</p><p>3、如果新数据满足下面条件则数据当前列,否则换列了</p><pre><code>3.1 在当前列所有数据的右侧 3.2 和当前列中数据在纵轴上有重叠
</code></pre><p>4、依次算完每个数据块</p><p>5、同理计算行数据</p><p>优点:</p><p>封装代码,对开发屏蔽细节<br>开发周期大幅缩短,从3-5天缩短为一小时提供可配置参数</p><p>缺点:</p><p>参数比较多,开发需要一定学习时间</p><p>问题:</p><p>1、条件2中,如果两块属于重叠,但是边缘压的不多,可以设置阈值,看成不重叠</p><p>2、图片上下左右可能会存在部分干扰,可以设置一些匹配规则,满足条件的外部区域可以裁剪掉,提高识别成功率</p><p>总结:</p><p>以上各个方法各有优缺点,适应场景各不相同,目前我们使用较多的方法是俄罗斯方块法和投影法</p><p>这些是我们初期探索出的一些方法,相信还会有更好的方法,我们也会继续探索</p><h2>三、模板开发</h2><h3>什么是模板</h3><p>模板:<br><img src="/img/remote/1460000023919753" alt="" title=""></p><ul><li>识别的目标文件可能有不同业务线的图片,例如流水、卡证、报告、其他单据等 – 我们叫业务类</li><li>每种业务线还有细化的类型,例如银行流水中的不同银行,保单中不同保险公司等 – 我们叫大类</li><li>每家银行或保险公司的单据在不同地点、时间上还可能不是一个样子,这每种图片样子叫做模板</li></ul><p>为了提高成功率我们需要针对模板定制化解析,要理解一点,专属的一定比公用的好<br>那么第一步我们就需要区分图片属于那种模板<br>针对刚才说的,到大类这一层比较固定,通过api层判断<br>现在来形象看下模板这层的问题</p><h3>模板举例</h3><p><img src="/img/remote/1460000023919754" alt="" title=""></p><p>看三张图片,针对同一个大类,分别是无表格、虚线表格和有表格的,需要通过训练验出来,有助于模板区分</p><h3>模板方法</h3><p>在开发中,总结了两种模板判断方法<br>当业务模板种类较少较固定时,我们采用大标题法</p><p>1、大标题判断方法,查找已知模板在大类中存在特殊的文字表示判断</p><p>缺点:1、可能找不出经验特点 2、可能识别失败</p><p>相反2、可配置的模板匹配度方法配置模板中各属性的内容和坐标范围等要素,计算出匹配评分,选取分高者</p><p>优点:<br>1、开发效率极高 2、对开发屏蔽了细节</p><p>缺点:<br>仅能区分已知模板</p><h2>四、结构化</h2><h3>什么是结构化</h3><h4>什么是结构化</h4><p>结构化是版面分析最后一步,在行列和模板识别完成后,把数据块转化为目标报文结构,用于存储、传输、分析等</p><h4>如何结构化</h4><p>通常使用标题和坐标来抽取数据,但有时一些特殊的模板会使结构化难度提高</p><h4>特殊模板举例</h4><p><img src="/img/remote/1460000023919755" alt="" title=""></p><p>有些图片有水印或印章,干扰结构化结果<br>目前我们只解决部分水印,盖章问题,还有没教好较统一解决方案,这也是目前我们重点要解决的课题,希望有机会同行交流交流经验</p><p><img src="/img/remote/1460000023919756" alt="" title=""></p><p><img src="/img/remote/1460000023919758" alt="#### 近行列粘连" title="#### 近行列粘连"></p><p><img src="/img/remote/1460000023919757" alt="#### 无标题" title="#### 无标题"></p><p><img src="/img/remote/1460000023919759" alt="" title=""></p><p>更有这种标题分多行的<br>针对上面几种场景,我们依据经验,采用模式匹配方式封装了一些常用方法来解析和抽取关键数据,最后组装数据<br><img src="/img/remote/1460000023919760" alt="#### 缺块" title="#### 缺块"></p><p>由于图片质量问题,会出现缺数据块的情况,这时即使模式匹配也无法抽取,目前我们AI模型在逐渐优化过程中,这种问题会越来越少</p><h3>语义矫正</h3><p><img src="/img/remote/1460000023919761" alt="" title=""></p><p>部分业务对文字准确率要求高,例如 工资 有时会识别成7资 7贝 1识别成I 0识别成o,遇到这种情况,我们综合利用全局及局部语义信息进行的NLP文字校正正<br>上期刘创老师有介绍过文字纠错内容,这里就不细讲了,有兴趣的同学可以翻回上期内容复习一下,至此版面分析技术侧内容分享完毕</p><h2>五、总结</h2><p><img src="/img/remote/1460000023919762" alt="" title=""></p><p>我们回顾一下今天讲解内容。先介绍了项目背景,又从版面分析技术角度,分别介绍了行列识别五种技术方案探索过程,并重点讲解了俄罗斯方块法,然后介绍什么是模板开发,并介绍了两种不同的模板,最后介绍什么是结构化及结构化遇到的问题和解决方案,至此我的分享结束感谢大家。</p><blockquote>作者:宜信技术学院 刘鹏飞</blockquote>
前置条件断言
https://segmentfault.com/a/1190000023784090
2020-08-27T10:05:35+08:00
2020-08-27T10:05:35+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<h2>1、什么是断言</h2><p>断言(assert),是编程术语,表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。</p><p>断言的使用通常在单元测试中,使用断言可以创建更稳定,品质更好且不易于出错的代码。</p><h2>2、断言特性:</h2><p>前置条件断言:代码执行之前必须具备的特性</p><p>后置条件断言:代码执行之后必须具备的特性</p><p>前后不变断言:代码执行前后不能变化的特性</p><h2>3、前置条件断言</h2><p>程序的业务逻辑处理,一般是有必须满足的条件,才能进行对应的处理,否则就不能正确的执行。</p><p>而代码开发中,如果不在业务处理前,对其所需的条件进行判定,则在后续中,就会出现各种隐患。</p><p>在PRD中,对于业务逻辑,也是有一定满足条件才能执行的。</p><p>在敏捷开发中,TDD是其一项核心实践。</p><p>在测试用例中,对于测试场景来说,也是应有前置条件的约束的。</p><p>那么,综上所诉,是不是在写业务功能之前,进行断言判断呢?</p><p>答案是肯定的,进行前置条件断言,不仅符合业务实际,也对代码规范进行了约束,同时,也会避免大量的不必要的隐患。</p><p>在项目中,我们通过在应用接口层进行对外交互。那么对应的,条件断言,也应在这里进行。</p><p>我们假设,进行断言,如果不通过,则抛异常码,并且显示在返回结果中。<br>那么,首先,应定义全局异常码,在项目中,每个码都应唯一并且有确定的含义。</p><p>在全局异常码,可以根据业务,进一步分为错误码,转向提示码。</p><p>错误码,很容易理解,他的信息可以由用户或上游调用方显示看到。</p><p>转向提示码,则是需要根据码,可以进行一些对应业务处理,比如,用户登录信息session超时,可以使用转向提示码,通知上游,直接转向登录页面。</p><h2>4、断言执行流程</h2><p><img src="/img/remote/1460000023784093" alt="" title=""></p><p>接下来,我们可以简单的尝试一下做个断言工具类 AssertUtils。</p><pre><code>public class AssertUtils {
/**
* check if source is equals target.<p>
* if source == null && target == null,will not throws Exception
* @param code
* @param source
* @param target
*/
public static void eq(int code,Object source,Object target) {
boolean eq = false;
eq = source == null ? target == null ? true : false : target == null ? false : source.equals(target) ;
if(!eq)
ExceptionUtils.throwSimpleEx(code);
} }
</code></pre><p>ExceptionUtils是自定义异常Utils,里面对异常进行了封装,并且对code进行了配置注册。<br>在使用上,我们可以这样来使用:</p><pre><code>int a1 = 12;
Integer a2 = 123;
AssertUtils.eq(1401,a1,a2);</code></pre><p>就对a1和a2进行了相等的断言,不符合,则抛出1401的异常码。</p>
从10个问题切入,带大家了解JVM的方方面面
https://segmentfault.com/a/1190000023681081
2020-08-19T09:58:22+08:00
2020-08-19T09:58:22+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>每个java开发同学不管是日常工作中还是面试里,都会遇到JDK、JVM和GC的问题。本文会从以下10个问题为切入点,带着大家一起全面了解一下JVM的方方面面。</p><ol><li>JVM、JRE和JDK的区别和联系</li><li>JVM是什么?以及它的主要作用</li><li>JVM的核心功能有哪些</li><li>类加载机制和过程</li><li>运行时数据区的逻辑结构</li><li>JVM的内存模型</li><li>如何确定对象是垃圾</li><li>垃圾收集的算法有哪些</li><li>各种问世的垃圾收集器</li><li>JVM调优的参数配置</li></ol><blockquote>上一篇文章结尾时我们谈到,就JVM的设计规范,从使用用途角度JVM的内存大体的分为:线程私有内存区 和 线程共享内存区。<p><img src="/img/remote/1460000023681084" alt="" title=""></p></blockquote><p>线程私有内存区在类加载器编译某个class文件时就确定了执行时需要的“程序计数器”和“虚拟栈帧”等所需的空间,并且会伴随着当前执行线程的产生而产生,执行线程的消亡而消亡,因此“线程私有内存区”并不需要考虑内存管理和垃圾回收的问题。线程共享内存区在虚拟机启动时创建,被所有线程共享,是Java虚拟机所管理内存中最应该关注的和最大的一块。首先我们来一起看一下“线程共享内存区”的内存模型是什么样的?</p><h2>6、JVM的内存模型</h2><p><img src="/img/remote/1460000023681085" alt="" title=""></p><p>如图所示,JVM的内存结构分为堆和非堆两大块区域。</p><ul><li>其中“非堆”就是上篇文章我们提到的方法区或叫元数据区,用来存储class类信息的。</li><li>而“堆”是用来存储JVM各线程执行期间所创建的实例对象或数组的。堆区分为两大块,一个是Old区,一个是Young区。Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区S0和S1一样大,也可以叫From和To。</li></ul><p>之所以这样划分,设计者的目的无非就是为了内存管理,也就是我们说的垃圾回收。那么什么样的对象是垃圾?垃圾回收算法有哪些?目前常用的垃圾回收器又有哪些?这篇文章我们一起弄清楚这些问题和知识点。</p><h2>7、如何确定一个对象是垃圾?</h2><p>要想进行垃圾回收,得先知道什么样的对象是垃圾。目前确认对象是否为垃圾的算法主要有两种:引用计数法和可达性分析法。</p><ul><li>1、引用计数法:在对象中添加了一个引用计数器,当有地方引用这个对象时,引用计数器的值就加1,当引用失效的时候,引用计数器的值就减1。当引用计数器的值为0时,JVM就开始回收这个对象。</li></ul><p>对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。这种方法虽然很简单、高效,但是JVM一般不会选择这个方法,因为这个方法会出现一个弊端:当对象之间相互指向时,两个对象的引用计数器的值都会加1,而由于两个对象时相互指向,所以引用不会失效,这样JVM就无法回收。</p><ul><li>2、可达性分析法:针对引用计数算法的弊端,JVM采用了另一种算法,以一些"GC Roots"的对象作为起始点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以进行垃圾回收。否则,证明这个对象有用,不是垃圾。<p><img src="/img/remote/1460000023681086" alt="" title=""></p></li></ul><p>上图中的obj7和obj8虽然它们互相引用,但从GC Roots出发这两个对象不可达,所以会被标记为垃圾。JVM会把以下几类对象作为GC Roots:</p><ul><li>(1) 虚拟机栈(栈帧中本地变量表)中引用的对象;</li><li>(2) 方法区中类静态属性引用的对象;</li><li>(3) 方法区中常量引用的对象;</li><li>(4) 本地方法栈中JNI(Native方法)引用的对象。</li></ul><blockquote>注:在可达性分析算法中不可达的对象,并不是直接被回收,这时它们处于缓刑状态,至少需要进行两次标记才会确定该对象是否被回收:<p>第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;</p><p>第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法(该方法可将此对象与GC Roots建立联系)。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。</p><p>第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。</p></blockquote><h2>8、垃圾收集的算法有哪些</h2><p>知道了如何JVM确定哪些对象是垃圾后,下面我们来看一下,面对这些垃圾对象,JVM的回收算法都有哪些。</p><h4>1、 标记-清除算法(Mark-Sweep)</h4><ul><li>第一步“标记”,如下图所示把堆里所有的对象都扫描一遍,找出哪些是垃圾需要回收的对象,并且把它们标记出来。<p><img src="/img/remote/1460000023681087" alt="" title=""></p></li><li>第二步“清除”,把第一步标记为“UnReference Object”(无引用或不可达)的对象清除掉,释放内存空间。<br><img src="/img/remote/1460000023681088" alt="" title=""></li></ul><p>这种算法的缺点主要有两点:</p><p>(1) 标记和清除两个过程都比较耗时,效率不高</p><p>(2) 清除后会产生大量不连续的内存碎片空间,碎片空间太多可能会导致当程序后续需要创建较大对象时,无法找到足够连续的内存空间而不得不再次触发垃圾回收。</p><h4>2、 标记-复制算法(Mark-Copying)</h4><p>将内存划分为两块区域,每次使用其中一块,当其中一块用满,触发垃圾回收的时候,将存活的对象复制到另一块上去,然后把之前使用的那一块进行格式化,一次性清除干净。<br> <img src="/img/remote/1460000023681094" alt="" title=""><br>(清除前)<br> <img src="/img/remote/1460000023681089" alt="" title=""><br>(清除后)</p><p>“标记-复制”算法的缺点显而易见,就是内存空间利用率低。</p><h4>3、 标记-整理算法(Mark-Compact)</h4><p>标记整理算法标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。<br> <img src="/img/remote/1460000023681090" alt="" title=""><br>将所有存活的对象向一边移动,清理掉存活边界以外的全部内存空间。<br> <img src="/img/remote/1460000023681091" alt="" title=""><br>结合这三种算法我们可以看到,</p><ul><li>“标记-复制”算法的优点是回收效率高,但空间利用率上有一定的浪费。</li><li>而“标记-整理”算法由于需要向一侧移动等一系列操作,其效率相对低一些,但对内存空间管理上十分优异。</li><li>因此,“标记-复制”算法适用于那些生命周期短、回收频率高的内存对象,</li><li>而标记-整理”算法适用于那些生命周期长、回收频率低,但注重回收一次内存空间得到足够释放的场景。</li></ul><p>因此JVM的设计者将JVM的堆内存,分为了两大块区域Young区和Old区,Young区存储的就是那些生命周期短,使用一两次就不再使用的对象,回收一次基本上该区域十之有八的对象全部被回收清理掉,因此Young区采用的垃圾回收算法也就是“标记-复制”算法。Old区存储的是那些生命周期长,经过多次回收后仍然存活的对象,就把它们放到Old区中,平时不再去判断这些对象的可达性,直到Old区不够用为止,再进行一次统一的回收,释放出足够的连续的内存空间。</p><h2>9、各种问世的垃圾收集器</h2><p>鉴于Young区和Old区需要采用不同的垃圾回收算法,因此在JVM的整个垃圾收集器的演进各个时代里,针对Young区和Old区每个时代都是不同的垃圾收集机制。从JDK1.3开始到目前,JVM垃圾收集器的演进大体分为四个时代:串行时代、并行时代、并发时代和G1时代。</p><p><img src="/img/remote/1460000023681092" alt="" title=""></p><h4>1、串行时代:Serial(Young区)+ Serial Old(Old区)</h4><p>JDK3(1.3)的时候,大概是2000年左右,那个时代基本计算机都是单核一个CPU的,因此垃圾回收最初的设计实现也是基于单核单线程工作的。并且垃圾回收线程的执行相对于正常业务线程执行来说还是STW(stop the world)的,使用一个CPU或者一条收集线程去完成垃圾收集工作,这个线程执行的时候其它线程需要停止。</p><p><img src="/img/remote/1460000023681093" alt="" title=""></p><p>串行收集器采用单线程stop-the-world的方式进行收集。当内存不足时,串行GC设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行GC开始工作,采用单线程方式回收空间并整理内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优势。因此,串行收集器特别适合堆内存不高、单核甚至双核CPU的场合。</p><h4>2、并行时代:Parallel Scavenge(Young区) + Parallel Old(Old区)</h4><p>并行收集器是以关注吞吐量为目标的垃圾收集器,也是server模式下的默认收集器配置,对吞吐量的关注主要体现在年轻代Parallel Scavenge收集器上。</p><p><img src="/img/remote/1460000023681095" alt="" title=""></p><p>并行收集器与串行收集器工作模式相似,都是stop-the-world方式,只是暂停时并行地进行垃圾收集。年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。关注吞吐量主要指年轻代的Parallel Scavenge收集器,通过两个目标参数-XX:MaxGCPauseMills和-XX:GCTimeRatio,调整新生代空间大小,来降低GC触发的频率。并行收集器适合对吞吐量要求远远高于延迟要求的场景,并且在满足最差延时的情况下,并行收集器将提供最佳的吞吐量。</p><h4>3、 并发时代:CMS(Old区)</h4><p>并发标记清除(CMS)是以关注延迟为目标、十分优秀的垃圾回收算法,CMS是针对Old区的垃圾回收实现。</p><p><img src="/img/remote/1460000023681098" alt="" title=""></p><p>老年代CMS每个收集周期都要经历:初始标记、并发标记、重新标记、并发清除。其中,初始标记以STW的方式标记所有的根对象;并发标记则同应用线程一起并行,标记出根对象的可达路径;在进行垃圾回收前,CMS再以一个STW进行重新标记,标记那些由mutator线程(指引起数据变化的线程,即应用线程)修改而可能错过的可达对象;最后得到的不可达对象将在并发清除阶段进行回收。值得注意的是,初始标记和重新标记都已优化为多线程执行。CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。</p><h4>- 但CMS有以下两个缺陷:</h4><ul><li>(1)由于它是标记-清除不是标记-整理,因此会产生内存碎片,Old区会随着时间的推移而终究被耗尽或产生无法分配大对象的情况。最后不得不通过底层的担保机制(CMS背后有串行的回收作为兜底)进行一次Full GC,并进行内存压缩。</li><li>(2)由于标记和清除都是通应用线程并发进行,两类线程同时执行时会增加堆内存的占用,一旦某一时刻内存不够用,就会触发底层担保机制,又采用串行回收进行一次STW的垃圾回收。</li></ul><h4>4、G1时代:Garbage First</h4><p>G1收集器时代,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。</p><p><img src="/img/remote/1460000023681096" alt="" title=""></p><p>如上图所示,每一个Region(分区)大小都是一样的,1~32M之间的数值,但必须是2的指数。设置Region大小通过以下参数:-XX:G1HeapRegionSize=M。<br>G1收集器的原理或特点主要有以下三点:</p><p>(1)内存逻辑上仍保留的分代的概念,每一个Region同一时间要么被标记为新生代,要么被标记为老年代,要么处于空闲;</p><p>(2)整体上采用了“标记-整理算法”,不会产生内存碎片</p><p>(3)可预测的停顿,G1整体采用的策略是“筛选回收”,也就是回收前会对各个待回收的Region的回收价值和成本进行排序,根据G1配置所期望的回收时间,选择排在前面的几个Region进行回收。</p><p><img src="/img/remote/1460000023681097" alt="" title=""></p><p>其实之所以叫G1(Garbage First)就是因为它优先选择回收垃圾比较多的Region分区。<br>整体G1的垃圾回收工作步骤分为:初始标记、并发标记、最终标记和筛选回收。</p><h4>5、ZGC:Zero GC</h4><p>这篇文章简单提一下这个最新问世的垃圾收集器,之所以叫“Zero GC”是因为它追求的是更低的GC停顿时间,追求的目标是:支持TB级堆内存(最大4T)、最大GC停顿10ms。JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题。由于其是JDK11和只能在64位的linux上使用,因此目前用得还比较少。</p><h2>结语</h2><p>以上总体两篇文章七千字,就是我从JVM的作用、设计框架到JVM内存管理的整体的体系化理解。感谢。</p><p>拓展阅读:<a href="https://link.segmentfault.com/?enc=nLTodY6nzMZe8FaTHSed2A%3D%3D.KQuuLqQ8oRHODp7SwCk02Hbl6zVM4GDpfGiTbcCF7mx%2BJKckfFwRB6Zz6ezYlmcnxjfY0Ld%2FPwxvvfHaVlzI6Q%3D%3D" rel="nofollow">十个问题弄清JVM&GC(一)</a></p><blockquote>作者:宜信技术学院 谭文涛</blockquote>
Spring事务的传播行为案例分析
https://segmentfault.com/a/1190000023559603
2020-08-10T09:43:19+08:00
2020-08-10T09:43:19+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
3
<blockquote>网上关于Spring事务传播性以及隔离型的文章漫天盖地,还有不负责任的直接复制名词意思,文章虽然很多却是看的云里雾里,我们今天将给出案例分别和大家一起学习。</blockquote><p>1、spring给出经常面试的考点Spring事务的4个特性含义---这个很容易理解</p><p>2、spring事务传播特性的定义以及案例分析 </p><h2>一、事务的特性ACID</h2><p>这四个英文单词拼写我一直记不住,求记忆方法</p><ul><li>原子性(Atomicity):事务是一系列原子操作,要么全部成功,要么全部失败。</li><li>一致性(Consistency):一旦完成(不管是成功还是失败),确保它所在的一系列业务状态保持一致,状态都是成功,或者都是失败,不能一部分成功一部分失败。</li><li>隔离性(Isolation):不同事务同时进行某项业务,处理相同的数据时候,需要保证事务之间相互独立,互相之间数据不影响。</li><li>持久性(Durability):一旦事务完成,无论发生什么系统性错误,事务执行后的数据都被持久化了,不会因为重启或其他操作对数据进行更改。</li></ul><h2>二、spring事务传播特性的定义以及案例分析 </h2><p>我们先给出定义再分别进行简单的代码分析</p><p>给出百度图片,请大家参考,首先生命力如果想在工程中运用事务spring 的xml必须开启事务,以下这些特性一般都是在xml属性中进行配置。</p><pre><code><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"></code></pre><p>类似这种的配置一定要有,否则万事具备只欠东风,这个是DB事务有JTA和JPA以及Herbernate等,这里就不展开说明,可自行百度。</p><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596528689192033502.png" alt="" title=""></p><h2>三、案例解析事务传播7大行为</h2><h4>1、无事务,这个时候id 为16的第一次插入成功,第二次插入的时候失败,库中留存第一次的数据</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529387455024157.jpeg" alt=" qqq" title=" qqq"><br> 无事务运行</p><h4>2、propagation_required,默认事务的传播行为required,在进行实验2的时候将表中id为16的数据先删除以免影响接下来的测试。@Transactional(propagation=Propagation.REQUIRED) == @Transactional这两个的作用是一样的没有事务创建一个事务执行,</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529605681003338.jpeg" alt="qq'q" title="qq'q"><br>事务的传播特行为为required<br>结果是因为主键冲突将事务进行了回滚,所以两条数据都没有插入进去。</p><h4>3、propagation_supports,如果当前程序存在事务就加入该事务运行,如果不存在事务则在非事务中运行</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529626446046329.jpeg" alt="qqq" title="qqq"></p><p>事务的传播行为性为supports<br>因为调用方未用事务那么就在非事务中运行,所以插入了first的第一条数据。</p><h4>4、propagation_mandatory,必须在一个事务中运行,否则就会抛出异常mandatory 这个单词有强制性的意思我们默认用required 而不用mandatory,是因为mandatory不能自动创建事务。</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529645635037524.jpeg" alt="qqq" title="qqq"><br>事务的传播行为为manatory<br>因为调用的外层没有事务,所以两条数据没有插入。大家想想下面这种写法会发生什么现象<br><img src="http://college.creditease.cn/resources/upload/image/20200804/1596530167599087257.jpeg" alt="qqq" title="qqq"><br>事务的传播行为mandatory</p><h4>5、propagation_ required _new,不管事务是不是存在,都会另起一个事务,如果事务存在则将当前事务挂起,重新执行新加的事务</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529860189079351.jpeg" alt="qqq" title="qqq"><br>事务的传播行为required_new<br>结果和require一样,两条数据都没有入库,唯一健冲突导致第一条数据回滚,大家可以思考下我下面这两种情况。</p><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529869197045302.jpeg" alt="qqq" title="qqq"></p><p>情景1新起的事务抛出异常会不会让外围事务回滚?</p><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529877936016308.jpeg" alt="" title=""></p><p>情景2外围事务失败会不会导致新起事务已提交的回滚?</p><h4>6、 propagation_ not _support,表示不在事务中运行,如果当前存在事务则将事务挂起</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596529886418082560.jpeg" alt="qqq" title="qqq"><br>事务的传播行为not_suppoted<br>这种情景下,如果你根据我的思路一步走的应该可以想到id 为17的入库,第二条主键冲突虽然然而notSupportSonTransationsl()这个方法没有事务所以不影响第一条入库情况,但是外围事务id为16的要进行回滚了,所以库中只有一条数据id=17的。</p><h4>7、 propagation_never,表示当前方法不能运行在事务当中,如果有事务则会抛出异常---->Existing transaction found for transaction marked with propagation 'never'</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596530344292092543.jpeg" alt="" title=""><br>事务的传播行为NEVER</p><h4>8、 propagation_nested,这种嵌套的事务,外围如果没有事务则自己另起一个事务,可独立与外围事务进行单独的提交或者回滚(这句话不要理解错了),下面这个案例同样的数据一条也没有落入库中,</h4><p><img src="http://college.creditease.cn/resources/upload/image/20200804/1596530320267025836.jpeg" alt="" title=""><br>事务的传播行为nested</p><p>事务的传播行为级别简单的演示完毕</p><blockquote>作者:宜信技术学院,王巧敏</blockquote>
宜信OCR技术探索与实践|直播速记
https://segmentfault.com/a/1190000023430404
2020-07-30T10:06:43+08:00
2020-07-30T10:06:43+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p><a href="https://link.segmentfault.com/?enc=1Wd5D90zdQeeM8ysn18UeQ%3D%3D.XGkr6jQMU4qevQkDBBVt9ziDZ6cw0P9jULl5mt7AJxa%2Fdn0%2B6ps0lTDFSxG85HEu" rel="nofollow">宜信OCR技术探索与实践|完整视频回放</a></p><h2>一、OCR概述</h2><h4>1.1 OCR技术演进</h4><p><img src="/img/remote/1460000023430407" alt="" title=""></p><ul><li>传统图像,冈萨雷斯的图像处理。</li><li>信号处理、频域分析以及各类算法:SIFT、HOG、HOUGH、Harris、Canny…都很赞。</li><li>从2016年以后业界基本上都已经转向深度了,因为效果真的特别好。</li></ul><h4>1.2 OCR技术商业服务</h4><p><img src="/img/remote/1460000023430408" alt="" title=""></p><ul><li>身份证卡证类相对容易些,但是要做到复杂场景的,也不是那么容易。</li><li>发票、业务单据相对复杂,除了识别,更重要的是版面分析。</li><li>最近表格识别比较火,各家都在努力实现,微软的开放tablebank数据集</li><li>移动端backboneMobileNet,或者是tesseract+opencv</li></ul><h2>二、我们的业务场景</h2><h4>2.1 业务需求</h4><p><img src="/img/remote/1460000023430409" alt="" title=""></p><p>满足业务是第一需要,不同于大厂,对外服务API,要求大并发那么强,多样性品类完备,我们更强调单品要做到尽量达到业务要求,更强调定制化,可以分布走,业务上可以给反馈不断改进。</p><h4>2.2 识别过程中需要解决的问题</h4><p><img src="/img/remote/1460000023430410" alt="" title=""></p><h2>三、OCR算法详解</h2><h4>3.1 算法概述——分享原则</h4><p><img src="/img/remote/1460000023430411" alt="" title=""><br>大家一定要自己弄细节,读代码、甚至自己动手撸,自己训练,调参,排错,才能有真正的体会和理解,只讲我认为每个算法里面不太好理解,重点,以及容易忽略的点,跟同行一起交流,沟通。</p><p>一个模型,要全面深入了解,需要:</p><ul><li>目标、目的、意义是啥?</li><li>网络结构啥样?</li><li>loss是啥?</li><li>样本咋做?</li><li>后处理干了啥</li></ul><h4>3.2 算法概述——三大板块</h4><p><img src="/img/remote/1460000023430412" alt="" title=""></p><ul><li>文字检测:把文字框住,缩小到最小范围内,从而降低识别难度。</li><li>文字识别:检测出文字后,就可以通过识别工具(算法)来识别出文字,如中间图。</li><li>版面分析:当文字识别出来后,我们得出的是文字和相应的坐标,可是当真正业务中要得到的不仅仅是这个,需要有一个结构,如何通过识别出的文字排版成为一个有逻辑结构的单据或者内容,这个工作也超级复杂。关于版面分析这方面,后面会有团队里面经验非常丰富的伙伴和大家分享。</li></ul><h4>3.3 算法概述——检测算法</h4><p><img src="/img/remote/1460000023430413" alt="" title=""></p><ul><li>表中从下往上的检测算法排序按照效果:越来越好</li><li>从anchorbased(也就是右边所示的最下面的那张图),现在逐渐转向pixel-based(像素级别)(右边所示的中间的那张图),主要是语义分割的技术效果实在是太好了。</li></ul><p><img src="/img/remote/1460000023430414" alt="" title=""></p><p>CTPN:找框的一个算法。</p><p>预测最终结果是:10个anchor的y坐标偏移,和高度的调整值,还有它是不是前景的概率。输出是前后景概率[N,10,2],y、w调整值[N,10,2]。它只适合横向,或者纵向,不能同时。</p><ul><li>一个模型主要从以下几个方面理解</li><li>亮点和核心思路是:预测框和文本线构造算法</li><li>loss是啥(损失函数):anchor前后景概率、y、w调整</li><li>label怎么做:大框,弄成小框,然后正负样本均衡</li><li>后处理</li></ul><p><img src="/img/remote/1460000023430415" alt="" title=""></p><ul><li>算法被命名为EAST(Efficient and Accuracy Scene Text),因为它是一个高效和准确的场景文本检测pipeline。</li><li>首先,将图像送到FCN网络结构中并且生成单通道像素级的文本分数特征图和多通道几何图形特征图。文本区域采用了两种几何形状:旋转框(RBOX)和水平(QUAD),并为每个几何形状设计了不同的损失函数;然后,将阈值应用于每个预测区域,其中评分超过预定阈值的几何形状被认为是有效的,并且保存以用于随后的非极大抑制。NMS之后的结果被认为是pipeline的最终结果。</li><li>最后预测:scoremap,textbox,textrotation</li><li>标注是:一个蒙版mask,一个4张图,上下左右的距离,还有个角度:一共3个。</li><li>对应就可以出loss了。每个点预测出来,加上角度,就是1个框,太多了框,所以要做LANMS(合并算法)的合并。为何不直接用socremap,我认为是置信度不够,所以要再加上bbox来加强验证。</li></ul><p><img src="/img/remote/1460000023430416" alt="" title=""></p><p>PSENet是一种新的实例分割网络,它有两方面的优势。首先,psenet作为一种基于分割的方法,能够对任意形状的文本进行定位.其次,该模型提出了一种渐进的尺度扩展算法,该算法可以成功地识别相邻文本实例。</p><ul><li>FPN,左面用resnet50。为何是resnet50,原因是效果不错,参数适中。</li><li>论文里是6个尺度,一个不行么?我理解是彻底分开不同行,逐渐扩大,渐进尺度可以防止彼此交叉哈</li><li>FPN和UNET都是concat,FCN是add,这个细节。</li></ul><p><img src="/img/remote/1460000023430419" alt="" title=""></p><ul><li>使用DB模块之后,二值化操作就变成了可微的,可以加到网络里一起训练。</li></ul><p><strong>网络输出</strong></p><ul><li>probabilitymap,代表像素点是文本的概率</li><li>thresholdmap,每个像素点的阈值</li><li>binarymap,由1,2计算得到,计算公式为DB公式</li></ul><p><strong>label制作</strong></p><ul><li>probabilitymap, 按照pse的方式制作即可,收缩比例设置为0.4</li><li>thresholdmap, 将文本框分别向内向外收缩和扩张d(根据第一步收缩时计算得到)个像素,然后计算收缩框和扩张框之间差集部分里每个像素点到原始图像边界的归一化距离。</li></ul><h4>3.4 算法概述——识别算法</h4><p><img src="/img/remote/1460000023430420" alt="" title=""></p><ul><li>Atttenion:Attention-basedExtraction of Structured Information from Street View Imagery-2017最早的尝试</li></ul><p><img src="/img/remote/1460000023430422" alt="" title=""></p><p>非常经典的算法,主要的核心是CTC算法:Connectionist Temporal Classification (CTC)适合那种不知道输入输出是否对齐的情况使用的算法,所以CTC适合语音识别和手写字符识别的任务。</p><p><img src="/img/remote/1460000023430421" alt="" title=""></p><p>缺点:不能精确地联系特征向量与输入图像中对应的目标区域,这种现象称为attention drift。</p><p><img src="/img/remote/1460000023430417" alt="" title=""></p><p>Muturaltraining:</p><ul><li>我们知道什么?什么字符,第几个?这个信息!</li><li>哪个字符?找到那个字符,第几个?然后和样本里的顺序比</li><li>第几个是啥字符?和对应位置的字符比</li><li>所以样本中不能存在重复字符。</li></ul><h2>四、我们的实践</h2><h4>4.1 实践之路</h4><p><img src="/img/remote/1460000023430418" alt="" title=""></p><p><img src="/img/remote/1460000023430427" alt="" title=""></p><ul><li>非单据:宽高比,白像素比例等</li><li>旋转角整:前面讲过了,通过旋转模型,以及投影分布</li><li>多单据:多张单据在一起,通过投影,阈值超参配置</li><li>表格识别:采用mask-rcnn的方法,来找出大表边缘</li><li>后处理:通过NLP纠错,后面会详细的讲</li></ul><h4>4.2 实践之路——旋转模型</h4><h5>大方向判断</h5><p>第一版:</p><ul><li>VGG做backbone,全连接,四分类</li><li>样本:人工标注、增强</li><li>正确率90%</li></ul><p>第二版:</p><ul><li>做切割,256x256</li><li>使用MSER找备选</li><li>训练小图</li><li>众数选出最可能方向</li><li>正确率99.7%</li></ul><p>微调</p><ul><li>每旋转1°做纵向投影</li><li>方差最大的角度为微调角度</li></ul><h4>4.3 我们遇到的坑</h4><p><img src="/img/remote/1460000023430425" alt="" title=""></p><p><img src="/img/remote/1460000023430426" alt="" title=""></p><ul><li>把crnn论文论文中的自定义cnn网络,换成resnet,但是resnet是缩小32倍,所以要拉长一些,到512。</li><li>首先是:样本集是1000万 (50万张,置信度单字95%+)100万真实 +100万常用字(造) + 200万数字时间英文(造)+ 600万其他汉字(造)大概需要3-4天</li><li>接下来进行训练:Resnet50,5-6天;Resize扩大,1024,=>512x8,256x8</li></ul><p>过程中需要对greedy算法进行改进:</p><pre><code> =>beam_search/merge_repeated=True
单独测是有问题,但是在置信度很高的情况下,两者差距很小,但是得到了极大的速度改进,28秒=>10秒,batch=128,size是512x32
</code></pre><p><img src="/img/remote/1460000023430428" alt="" title=""></p><p><img src="/img/remote/1460000023430423" alt="" title=""></p><ul><li>因为有crnn的prob,所以纠错就有的放矢,把怀疑的字,替换成某个字,</li><li>Prob有个细节,如果是挨着的字,“__ 我 我 __”,就取最大的prob,</li><li>是根据一个字画相近度,对怀疑字替换的原则,是和原来识别字笔画最相近的,又是通过编辑距离。</li></ul><h4>4.4 我们的经验</h4><p>1、 开发经验</p><p><img src="/img/remote/1460000023430424" alt="" title=""></p><p>2、生产经验</p><p><strong>Tensorflow容器</strong></p><ul><li>模型部署使用官方推荐的tensorflowserving,容器方式</li><li>没有开启Batching,自己控制batch</li><li>宿主机只需要显卡驱动•容器内包含CUDA、cuDNN,免去版本适配</li></ul><p><strong>服务容器:</strong></p><ul><li>自己定义了Web容器基础镜像</li><li>自动构建容器、动态编排</li></ul><blockquote>本文作者:宜信技术学院 刘创</blockquote>
十个问题弄清JVM&GC(一)
https://segmentfault.com/a/1190000023362769
2020-07-24T14:36:37+08:00
2020-07-24T14:36:37+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p>每个java开发同学不管是日常工作中还是面试里,都会遇到JDK、JVM和GC的问题。本文会从以下10个问题为切入点,带着大家一起全面了解一下JVM的方方面面。</p>
<ol>
<li>JVM、JRE和JDK的区别和联系</li>
<li>JVM是什么?以及它的主要作用</li>
<li>JVM的核心功能有哪些</li>
<li>类加载机制和过程</li>
<li>运行时数据区的逻辑结构</li>
<li>JVM的内存模型</li>
<li>如何确定对象是垃圾</li>
<li>垃圾收集的算法有哪些</li>
<li>各种问世的垃圾收集器</li>
<li>JVM调优的参数配置</li>
</ol>
<h3>1、JVM、JRE和JDK的区别和联系</h3>
<p>这个基本是步入java世界的入门级知识认知,首先我们来看一下来自java官网的一张图:</p>
<p><img src="/img/remote/1460000023362772" alt="" title=""><br>从这张图里我们基本就可以看出“JRE”是运行Java语言编写的程序所不可缺少的运行环境。有了JRE我们写的java程序才可以运行起来被用户所使用。</p>
<p>而“JDK”俗称java开发工具包,它包括了Java运行环境JRE(Java Runtime Envirnment)以及一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。</p>
<p>但不管是JRE还是JDK都是以JVM为基石的。可以说JVM是java程序可以在某台机器上得以运行的最底层的保障。</p>
<h2>2、那么什么是JVM?它的主要作用又是什么?</h2>
<p>JVM是Java Virtual Machine(Java虚拟机)的缩写,它的用途简单的说就是它能让我们写的java程序在不同的操作系统的不同CPU上运行。我们写的java程序会利用开发工具(如Intellij idea)把它编译成.class文件,但这个class文件是不能直接被操作系统识别运行的,需要利用jvm按jvm规范将编译好的.class文件转变成机器语言,再交由操作系统提交给cpu去执行。</p>
<p><img src="/img/remote/1460000023362773" alt="" title=""></p>
<p>用一句话评价JVM的主要作用就是:JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。</p>
<h2>3、这么牛的JVM的核心功能有哪些?</h2>
<p>JVM中核心的功能总体有三块:</p>
<ol>
<li>类加载器:在JVM启动时或者在类运行时将需要的class文件加载到JVM中</li>
<li>执行引擎:负责执行class文件,包括分配运行时数据区(如程序计数器、本地方法栈和虚拟栈)和 最终将class中的字节码指令转为机器指令通过操作系统交给CPU执行</li>
<li>垃圾回收器:对JVM的堆内存进行管理,及时回收调无用的资源释放内存空间</li>
</ol>
<h2>4、JVM类的加载机制和过程?</h2>
<p>首先,我们谈谈开发工具编译生成的class文件是如何被JVM加载的。所谓的类加载机制其实就是:虚拟机(JVM)把class文件加载到内存中,然后对它进行正确性的校验,检查通过再进行解析和初始化,最终把class文件变成一个内存中可以直接使用的java.lang.Class对象。</p>
<p>从一个class文件的装载到销毁,它的生命周期基本可以分为以下五个阶段:装载、链接(验证、准备和解析)、初始化、使用和卸载。</p>
<p><img src="/img/remote/1460000023362774" alt="" title=""></p>
<ol>
<li>装载:装载(Load)阶段总共有三项工作<p>(1)通过类的全限定名获取其定义的二进制字节流,需要借助类装载器(ClassLoader)完成;</p>
<p>(2)在运行时数据区的“方法区”中分配一块区域保存这个类的信息,包括类的基本信息、常量和静态变量等等;</p>
<p>(3)在“Java堆”内存上生成一个该类的java.lang.Class对象,用于对外暴露使用该类的入口。</p>
</li>
<li>链接:链接(link)阶段同样有三项工作<p>(1)验证(Verify),验证文件格式、元数据、字节码和符号引用,以保证被加载类的准确性;</p>
<p>(2)准备(Prepare),为静态变量分配内存并初始化为默认值。</p>
<p><img src="/img/remote/1460000023362776" alt="" title=""></p>
<p>(3)解析(Resolve),解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。</p>
</li>
<li>初始化:初始化(Initialize)阶段所做的工作就是对类的静态成员变量和静态方法进行初始化赋值或调用。</li>
</ol>
<p>比如上面的静态变量age初始化之后的值变为了10。</p>
<p>在装载阶段的第(2),(3)步可以发现有运行时数据区,堆,方法区等名词,那么究竟什么是“运行时数据区”,它有哪些结构构成?</p>
<h2>5、什么是JVM运行时数据区?及其逻辑结构</h2>
<p>“运行时数据区”是JVM在执行Java程序的过程中出于内存管理方面的目的,在设计上把内存分为若干个不同的区域。这些区域有着各自的用途,有的区域生命周期跟虚拟机一样,随着虚拟机进程的启动而存在,伴随这虚拟机的进程结束而消亡。而有些区域则依赖用户线程的启动和结束而建立和销毁。具体如下图:</p>
<p><img src="/img/remote/1460000023362775" alt="" title=""></p>
<ol><li>方法区(Method Area):</li></ol>
<p>(1)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;</p>
<p>(2)方法区是各个线程共享的内存区域,在虚拟机启动时创建,因为同一个class类信息只需要加载一份就够了;</p>
<p>(3)java虚拟机规范中把方法区描述为堆内存的一个逻辑部分,但它有另外一个别名叫“非堆”,用于与java堆区分开来。在JDK8之前方法区叫做Perm space,在JDK8及以后叫做Metaspace(即元数据区)。</p>
<ol>
<li>堆(Heap):Java堆是被所有线程共享,虚拟机启动时创建,此内存区域唯一的目的就是存放对象实例,在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也就变得不那么绝对了。</li>
<li>虚拟机栈(Java Virtual Machine Stacks):虚拟机栈是线程私有的或者说是独有的,随着线程的创建而创建。一个线程的运行状态(正在调用哪个方法),就是由这个线程对应的虚拟机栈来保存的。</li>
</ol>
<p>每一个被线程执行的方法,为虚拟机栈中的一个栈帧,调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。如下图解:</p>
<p><img src="/img/remote/1460000023362777" alt="" title=""></p>
<ol>
<li>程序计数器(The Pc Register):我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置,这就是程序计数器。</li>
<li>本地方法栈(Native Method Stacks):本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机中使用到的native方法服务。即如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。<p><img src="/img/remote/1460000023362778" alt="" title=""></p>
</li>
</ol>
<p>总结一下,就JVM的设计规范,从使用用途角度JVM的内存大体的分为:线程私有内存区 和 线程共享内存区。</p>
<p><img src="/img/remote/1460000023362779" alt="" title=""></p>
<p>线程私有内存区在类加载器编译某个class文件时就确定了执行时需要的“程序计数器”和“虚拟栈帧”等所需的空间,并且会伴随着当前执行线程的产生而产生,执行线程的消亡而消亡,因此“线程私有内存区”并不需要考虑内存管理和垃圾回收的问题。</p>
<p>线程共享内存区在虚拟机启动时创建,被所有线程共享,是Java虚拟机所管理内存中最应该关注的和最大的一块。</p>
<p>那么JVM内存模型是如何设计的?JVM又是如何进行内存管理(也就是垃圾回收)的?垃圾回收算法有哪些?目前常用的垃圾回收器又有哪些?我会在下篇文章跟您共同解答这些问题。</p>
<blockquote>作者:宜信技术学院 谭文涛</blockquote>
搭建node服务(三):使用TypeScript
https://segmentfault.com/a/1190000023270288
2020-07-17T11:51:46+08:00
2020-07-17T11:51:46+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
5
<blockquote>JavaScript 是一门动态弱类型语言,对变量的类型非常宽容。JavaScript使用灵活,开发速度快,但是由于类型思维的缺失,一点小的修改都有可能导致意想不到的错误,使用TypeScript可以很好的解决这种问题。TypeScript是JavaScript的一个超集,扩展了 JavaScript 的语法,增加了静态类型、类、模块、接口和类型注解等功能,可以编译成纯JavaScript。本文将介绍如何在node服务中使用TypeScript。</blockquote><h2>一、 安装依赖</h2><pre><code>npm install typescript --save
npm install ts-node --save
npm install nodemon --save</code></pre><p>或者</p><pre><code>yarn add typescript
yarn add ts-node
yarn add nodemon</code></pre><p>另外,还需要安装依赖模块的类型库:</p><pre><code>npm install @types/koa --save
npm install @types/koa-router --save
…</code></pre><p>或者</p><pre><code>yarn add @types/koa
yarn add @types/koa-router
…</code></pre><h2>二、 tsconfig.json</h2><p>当使用tsc命令进行编译时,如果未指定ts文件,编译器会从当前目录开始去查找tsconfig.json文件,并根据tsconfig.json的配置进行编译。</p><h3>1. 指定文件</h3><p>可以通过files属性来指定需要编译的文件,如下所示:</p><pre><code>{
"files": [
"src/server.ts"
]
}</code></pre><p>另外也可以通过使用"include"和"exclude"属性来指定,采用类似glob文件匹配模式,如下所示:</p><pre><code>{
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}</code></pre><p>支持的通配符:</p><ol><li><ul><li>匹配0或多个字符(不包括目录分隔符)</li></ul></li><li>? 匹配一个任意字符(不包括目录分隔符)</li><li>**/ 递归匹配任意子目录</li></ol><h3>2. 常用配置</h3><p>compilerOptions 属性用于配置编译选项,与tsc命令的选项一致,常用的配置如下所示:</p><pre><code>{
"compilerOptions": {
// 指定编译为ECMAScript的哪个版本。默认为"ES3"
"target": "ES6",
// 编译为哪种模块系统。如果target为"ES3"或者"ES5",默认为"CommonJS",否则默认为"ES6"
"module": "CommonJS",
// 模块解析策略,"Classic" 或者 "Node"。如果module为"AMD"、"System"或者"ES6",默认为"Classic",否则默认为"Node"
"moduleResolution": "Node",
// 是否支持使用import cjs from 'cjs'的方式引入commonjs包
"esModuleInterop": true,
// 编译过程中需要引入的库。target为"ES5"时,默认引入["DOM","ES5","ScriptHost"];target为"ES6"时,默认引入["DOM","ES6","DOM.Iterable","ScriptHost"]
"lib": ["ES6"],
// 编译生成的js文件所输出的根目录,默认输出到ts文件所在的目录
"outDir": "dist",
// 生成相应的.map文件
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}</code></pre><h4>1) target</h4><p>target是编译目标,可以指定编译为ECMAScript的哪个版本,默认为"ES3"。ECMAScript的版本有:"ES3" 、"ES5"、 "ES6" 或者 "ES2015"、 "ES2016"、 "ES2017"、"ES2018"、"ES2019"、 "ES2020"、"ESNext"。</p><h4>2) module</h4><p>module指定编译为哪种模块系统,如果target为"ES3"或者"ES5",默认为"CommonJS",否则默认为"ES6"。可选用的模块系统有:"None"、 "CommonJS"、 "AMD",、"System"、 "UMD"、"ES6"或者"ES2015"、"ESNext"。</p><h4>3) moduleResolution</h4><p>moduleResolution指定模块解析策略,模块解析策略有:"Classic"、"Node",如果module为"AMD"、"System"或者"ES6",默认为"Classic",否则默认为"Node"。</p><h4>示例1:</h4><p>在/root/src/moduleA.ts中以import { b } from "./moduleB" 方式相对引用一个模块。<br>Classic解析策略,查找过程:</p><pre><code>/root/src/moduleB.ts
/root/src/moduleB.d.ts</code></pre><p>Node解析策略,查找过程:</p><pre><code>/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (如果指定了"types"属性)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts</code></pre><h4>示例2:</h4><p>在/root/src/moduleA.ts中以import { b } from "moduleB" 方式非相对引用一个模块。<br>Classic解析策略,查找过程:</p><pre><code>/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts</code></pre><p>Node解析策略,查找过程:</p><pre><code>/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (如果指定了"types"属性)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (如果指定了"types"属性)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json (如果指定了"types"属性)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts</code></pre><h4>4) esModuleInterop</h4><p>esModuleInterop为true时,表示支持使用import d from 'cjs'的方式引入commonjs包。当commonjs模块转化为esm时,会增加 __importStar 和 __importDefault 方法来处理转化问题。</p><h4>示例:</h4><p>cjs为commonjs模块,代码如下:</p><pre><code>module.exports = { name: 'cjs' };</code></pre><p>另外一个模块以esm方式引用了cjs模块,代码如下:</p><pre><code>import cjsDefault from 'cjs';
import * as cjsStar from 'cjs';
console.log('cjsDefault =', cjsDefault);
console.log('cjsStar =', cjsStar);</code></pre><p>输出结果为:</p><pre><code>cjsDefault = { name: 'cjs' }
cjsStar = { name: 'cjs', default: { name: 'cjs' } }
</code></pre><p>编译后生成的代码如下:</p><pre><code>var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const cjs_1 = __importDefault(require("cjs"));
const cjsStar = __importStar(require("cjs"));
console.log('cjsDefault =', cjs_1.default);
console.log('cjsStar =', cjsStar);</code></pre><h4>5) lib</h4><p>lib指定编译过程中需要引入的库。target为"ES5"时,默认引入["DOM","ES5","ScriptHost"];target为"ES6"时,默认引入["DOM","ES6","DOM.Iterable","ScriptHost"]。由于本示例TypeScript是用于服务端的,不需要使用DOM和ScriptHost,所以lib设为["ES6"]。</p><h4>6) outDir</h4><p>输出目录,编译生成的js文件所输出的根目录,默认输出到ts文件所在的目录。</p><h4>7) sourceMap</h4><p>是否生成source map文件,通过使用source map 可以在错误信息中可以显示源码位置。<br>要想根据source map 显示错误信息源码位置,还需要在入口文件引入source-map-support 模块,如下:</p><pre><code>import 'source-map-support/register';</code></pre><h2>三、 脚本命令</h2><p>入口文件为src/server.ts,package.json中的scripts配置如下:</p><ul><li>package.json</li></ul><pre><code>{
"scripts": {
"dev": "nodemon --watch src -e ts,tsx --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
},
…
}</code></pre><ol><li>执行 npm run dev 命令可以启动开发环境,当src下的文件被修改后会自动重新启动服务。</li><li>执行 npm run build 命令会进行编译,由于tsconfig.json中 outDir 指定输出目录为dist,编译后的js文件将出输出到dist目录。</li><li>执行 npm run start 命令可以启动应用,启动前需要执行 npm run build 进行编译。</li></ol><h2>四、 自定义类型</h2><p>TypeScript 会自动从 node_modules/@types 目录获取模块的类型定义,引用的模块都需要安装对应类型库,如:</p><pre><code>npm install @types/koa --save</code></pre><p>安装后,会在node_modules/@types 目录下找到koa 文件夹,该文件夹下有koa相关的类型定义文件。当引用koa模块时会自动引入node_modules/ 和 node_modules/@types下的 koa 包。如果某个模块没有类型库或者对某个模块进行了扩展需要修改类型定义,这时需要引入自定义的类型。</p><h3>示例:给koa增加bodyparser中间件</h3><h3>1. 设置typeRoots</h3><ul><li>tsconfig.json</li></ul><pre><code>{
"compilerOptions": {
…
// 类型声明文件所在目录
"typeRoots": ["./node_modules/@types", "./src/types"],
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
</code></pre><p>src/types是存放自定义类型的目录,本示例中src/types目录已被include包含,如果自定义的类型目录未被include包含还需要在include中添加该目录。</p><h3>2. 编写类型定义文件</h3><ul><li>src/types/koa/index.d.ts</li></ul><pre><code>import * as Koa from "koa";
declare module "koa" {
interface Request {
body?: object;
rawBody: string;
}
}</code></pre><p>这里给koa的request对象增加body和rawBody两个属性,分别用于存放请求体的json对象和原始字符串。</p><h3>3. 编写 jsonBodyParser 插件</h3><ul><li>src/middleware/jsonBodyParser.ts</li></ul><pre><code>import Koa from "koa";
function getRawBody(ctx: Koa.Context): Promise<string> {
return new Promise((resolve, reject) => {
try {
let postData: string = '';
ctx.req.addListener('data', (data) => {
postData += data;
});
ctx.req.on('end', () => {
resolve(postData);
});
} catch (e) {
console.error('获取body内容失败', e);
reject(e);
}
})
}
export default function jsonBodyParser (): Koa.Middleware {
return async(ctx: Koa.Context, next: Koa.Next) => {
const rawBody: string = await getRawBody(ctx);
const request: Koa.Request = ctx.request;
request.rawBody = rawBody;
if (rawBody) {
try {
request.body = JSON.parse(rawBody);
} catch (e) {
request.body = {};
}
}
await next();
};
}</code></pre><p>jsonBodyParser()会返回一个koa中间件,这个中间件将获取请求体的内容,将原始内容字符串赋值到ctx.request.rawBody,将请求体内容json对象赋值到ctx.request.body。由于src/types/koa/index.d.ts自定义类型已经扩展了Koa.Request的这两个属性,执行npm run build命令,使用 tsc 进行编译,可以编译成功。但是当执行 npm run dev 时,会提示编译错误,那是因为ts-node默认不会根据配置中的files、include 和 exclude 加载所有ts文件,而是从入口文件开始根据引用和依赖加载文件。最简单的解决办法就是在 ts-node 命令后增加 --files 参数,表示按配置的files、include 和 exclude加载ts文件,如下:</p><ul><li>package.json</li></ul><pre><code>{
"scripts": {
"dev": " nodemon --watch src -e ts,tsx --exec ts-node --files src/server.ts",
}
}</code></pre><h2>五、 说明</h2><p>本文介绍了如何在node服务中使用TypeScript,具体的TypeScript语法规则网上有很多相关的资料,这里就不再介绍了。本文相关的代码已提交到GitHub以供参考,<br>项目地址:<a href="https://link.segmentfault.com/?enc=V48NJ%2BWBlLC%2FxWr65c%2BlOw%3D%3D.RG3CmZoTezO0mLEyMNfBl%2FSL89G2r8INwq2mVHmczfW8IUvCYmecedaRaHzZUZc02IwHcDO5LaPxswtptbPNIw%3D%3D" rel="nofollow">https://github.com/liulinsp/node-server-typescript-demo</a>。</p><blockquote>作者:宜信技术学院 刘琳</blockquote>
AI中台助力企业智能化转型
https://segmentfault.com/a/1190000023032787
2020-06-28T09:29:55+08:00
2020-06-28T09:29:55+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>AI中台助力企业智能化转型</p>
<blockquote>本文主要和大家分享 “AI中台如何助力企业数字化以及智能化转型”,以及我在构建 AI中台方面的一些心得和经验。<p>企业数字化旨在利用数字化技术改变企业业务模式,优化生产过程以及寻求新的商业价值。但能够做到真正数字化的企业并不是很多。那么在数字化的征途上,企业都需要做些什么呢?</p>
</blockquote>
<h2>从数字化到智能化</h2>
<p><img src="/img/remote/1460000023032794" alt="" title=""></p>
<p>企业首先要做的事情是数据连接,也就是进行数据的收集、整理,以及标准化和统一化的数据操作,形成统一的数据平台。进而基于这些数据去进行分析,制定数据指标,再基于这些数据指标进行一些挖掘和洞察,最后依据数据的洞察做决策。</p>
<h2>智能化赋能方向</h2>
<p><img src="/img/remote/1460000023032791" alt="" title=""><br>智能化其实想想已经离我们并不是那么遥远了,在很多领域我们都有一些智能化的赋能和应用。我将它总结成两个维度:</p>
<ul>
<li>从一个横向的维度来讲,有一些通用化的AI技术,比如说计算机视觉、CD还有NRP,这是自然语言处理。还有声学处理,这样的一些通用化的AI可以应用在各个领域。</li>
<li>从垂直的角度来说,企业可以对垂直领域的数据进行专门的分析挖掘。比如企业可以深入某一个营销场景,做一些业务探索等。</li>
</ul>
<p>这两个维度结合,就可以组合出各种各样的智能化赋能。第一大类赋能就是智能化的流程管理,比如智能运营、金融企业里的风控、运营过程中的一些业务助理,包括底层的技术运维等等。</p>
<p>第二大类就是我们非常熟悉且讨论很多的智能化精细营销。例如精准推荐、客户画像、客群分析,还有智能客服等等。</p>
<p>第三大类是智能化决策领域,这个领域里包括智能顾问、知识图谱,还有用人工智能技术进行报告分析并预测趋势,最后辅助企业做决策。</p>
<p>所以智能化赋能已经走进了企业的日常研发工作流程。</p>
<h2>企业进行智能化建设的痛点?</h2>
<ol>
<li>实施过程它比较复杂:研发环节比较多、流程重复、以及缺少过程的固化、优化与自动化,业务响应缓慢。</li>
<li>部署维护困难:模型研发与部署割裂,缺少统一的运行、监控平台,以及更新维护机制。</li>
<li>缺少反馈与更新:生产模型缺少持续的数据反馈,导致模型性能随时间偏移,难以更新。</li>
</ol>
<p>4、模型重复建设问题:“烟囱式”开发,目标重复、过度重复,缺乏资产复用与能力沉淀。</p>
<p>5、高投入低产出:项目建设相对缓慢,投入高,收效低,完成后应用范围小,无法扩大。</p>
<p>6、研究缺乏管理:缺少统一标准,研究力量分散,资源利用率低,AI资产缺乏管理,易流失。</p>
<h2>中台化思想结构</h2>
<p><img src="/img/remote/1460000023032792" alt="" title=""></p>
<p>中台化理论是几年前阿里提出的,他们在企业内部进行了一系列的中台化改造。阿里强调的是服务沉淀,能力共享,打破系统的壁垒,用大中台的能力去支持小前台不断变化的需求,实现对前台业务需求的敏捷化支持,最终实现业务的快速的创新。</p>
<h4><strong>那么我们平时听到最多的中台是什么?是数据中台。</strong></h4>
<p>数据中台统一了数据的标准,沉淀公共数据,实现数据实体化、标准化、统一化的融合。事实看来中台化是有效的,因为数据中台里固化下来的那些有价值的资产和能力,在支撑工具的帮助之下,并且加以统一的标准化约束,就能够向外充分地实现资产和能力的复用、共享,对外提供敏捷而高效的服务,不断地满足前台纷纷扰扰各种各样的业务需求。</p>
<p>总体来说,从资产向外输出,实现相关的复用和共享之后,再次进行能力的沉淀,纳入到中台,再把这些资源再重新地再共享出去,最终实现的是一个有机的循环,以及对于资产和能力的集约化管理。</p>
<h3>中台化的事项对于AI研发是否有帮助?</h3>
<p>我们构建 AI中台,上面对接业务前台,下面依赖于数据方的支持,对外提供一个标准化统一化的服务。我们只加工利用可复用的能力和资产来进行标准化产品的加工,并且整个过程是可追踪、可维护、可更新的,可以有效或者说合理地进行循环的,这就是AI中台的形成。</p>
<h2>为什么AI中台能解决问题</h2>
<p><img src="/img/remote/1460000023032793" alt="" title=""></p>
<p>首先,AI中台可以对复杂的实施过程进行标准化流程规约,也就是企业可以充分复用算法和特征,通过特征引擎进行固化,再通过可复用的模型和算法,利用数据来重训练模型。目的是实现复杂实施过程的标准化、流程化和自动化,并尽量加快其学习的过程。</p>
<p>另外针对流程化部署维护方面,AI中台可以提供一套标准化的模型封装工艺,对外企业只要维护一个统一的服务接口,实现统一模型运行维护统一的平台。</p>
<p>企业还可以在运行平台之上,建立一系列的监控机制和反馈机制,定期进行数据样本的收集与导出,性能指标定义与报表展示。这依赖的是对服务持续性的监控,AI中台实现的是持续健康地反馈和更新。最终这些持续的反馈更新反过来还会作用到最开始复杂实施过程的流程化与自动化的一个环节里去优化模型。</p>
<p>总而言之,AI中台是充分利用企业现有的能力。当前端有新的需求出现时,后端可以用自动化的工具迅速迭代功能。此外,AI中台还可以利用企业已有的一些能力来对前台提供支持,让企业服务越来越敏捷。如果有重复的需求提出,AI中台可以帮助企业直接利用现在已有的服务,通过一些简单的改造或者封装,提供一个充分复用、充分共享的服务,避免重复性建设。</p>
<h2>AI中台的建设</h2>
<p><img src="/img/remote/1460000023032795" alt="" title=""></p>
<ul>
<li>从战略维度上,企业在进行中台化建设的时候,必须有明确的决心和目标,以及针对中台化实施所制定的一系列长期或短期的规划和方针。</li>
<li>从技术维度上,中台化本身应该有一个核心的技术支撑,有时也包括中台化的产出物,是中台化实施的核心。</li>
<li>从组织维度上,实际上是通过对组织的调整,构建建设中台所需的组织基础,方便中台的建设和实施。但是反过来中台的建设和实施本身就包含了对组织的集约化重塑。</li>
</ul>
<p>从流程维度上,企业对相关的生产流程肯定需要进行适配改造,这样才能适应中台的建设。</p>
<blockquote>作者:宜信技术学院 井玉欣<p>AI中台领域建设路线参考示例,以及AI中台的实施案例,可以点击“<a href="https://link.segmentfault.com/?enc=sJr2ITs2XuGv3SLXLWgxzA%3D%3D.QE%2BH7libuMORAoF0DYwADrmnPxtXsFZm8%2FA%2FUFXoW%2FhkW%2F0fqt7V3DYD%2FHQeJ9D%2B4HG1zg5XdjCNPVrIygCSOzKVs33D%2BKiXNOeq9BnsZFFJmlWL3SgqjTl3UX9nwo9qvA%2FBc3XaDdDXG02sLDltfA%3D%3D" rel="nofollow">这里</a>”查看完整课程视频进行学习~</p>
</blockquote>
记一次通过Memory Analyzer分析内存泄漏的解决过程
https://segmentfault.com/a/1190000022974373
2020-06-19T14:01:47+08:00
2020-06-19T14:01:47+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
3
<h2>状况描述:</h2>
<p>最近项目新打的版本,过不了多长时间,项目就会挂掉。状况就是处于一种假死的状态。索引查询都很慢,几乎进行不了任何操作,慢慢卡死。<br>然后我们再发版时,只能基于之前打好的war包,替换或者增加class文件。</p>
<h2>情况对比及分析:</h2>
<p>由于之前代码做过一次大整顿,提交的代码比较多,所以通过回滚版本的方式解决,比较困难。一是因为整顿的成果不能白白抹杀;二是那么多文件,靠人工挨个对比查找,比较困难。</p>
<h3>解决方案一:</h3>
<p>之前, 一直对目前项目的打包方式心存质疑,所以这次发生问题时,我首先怀疑的对象是Jenkins和生产的Tomcat服务器。我通过堡垒机连接到生产时,发现通过Jenkins启动应用程序,会启动两个两个tomcat进程。</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298669444077954.png" alt="" title=""></p>
<p>然后,这似乎更加坚定了我的看法,马上就找到了运维,但是经确认后,是没有问题的。一个是用root用户启动的,一个是用tomcat用户启动的。一个守护进程,一个应用进程。</p>
<h3>解决方案二:</h3>
<p>排除了服务器的问题,开始正面考虑程序的问题。<br>重新发项目有问题的版本,Dump下来的日志,然后迅速回滚观察。单台机器的dump日志有5个G:</p>
<p>通过Memory Analyzer分析,在Leak Supects Report 视图中,有如下分析结果:</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298682003027073.png" alt="" title=""></p>
<p>上图所示,共有三类问题a、b、c;还有一些其他的,类型为d。</p>
<p>先来看第一个问题(后来发现,前几个问题都是同一个问题)</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298691147059654.png" alt="" title=""></p>
<p>先点开Details看一下:</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298705527077759.png" alt="" title=""></p>
<p>上图显示了一个很明显的有问题的线程:地址是0x7c8ff3df0 ,名称为pool-16-thread-1。<br>通过《Accumulated Objects in Dominator Tree》视图可以看出,在该线程中,存在一个大的List对象,List对象内存放了大量的mysql的jdbc对象。</p>
<p>我们想看看JDBC对象里面堆放了哪些数据。接下来我们打开《open dominator tree for entire heap》这个视图:</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298722438012273.png" alt="" title=""></p>
<p>找到名为0x7c8ff3df0 pool-16-thread-1的线程。如图也能发现,这个线程占用了大量的空间未释放。一层层打开里面的存放的对象:</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298737025090019.png" alt="" title=""></p>
<p>这里的数据,是我们的一张用户表的数据。所以这就可以得出结论:一个线程内,一个list内存放了大量从数据库中获取的用户对象!<br>想到这里,我们又去看了b、c的问题描述,也是同样的问题。估计是在不同时间点,通过gc已经回收了部分。</p>
<p>然后,我刚才看了a问题的details信息,接下来我们看下a的stacktrace 堆栈信息。</p>
<p><img src="http://college.creditease.cn/resources/upload/image/20200616/1592298746343031352.png" alt="" title=""></p>
<p>如上图,问题就很明显了,在Service的112行中,调用的findByCustomerID方法中,有扫描全表的操作。经过分析,找到对应的位置,对应的代码为:</p>
<pre><code>customerID = StringUtils.isNotBlank(customerID)?customer.getCustomer().getCustomerID():null;
Customer oldCutsomer = customerService.findByCustomerID(customerID);
</code></pre>
<p>显而易见,流程走到这里时,customerID永远为空,那么customerService.findByCustomerID(customerID)方法,会执行扫描全表操作。由于该表数据量巨大,开发所认为的用户每次执行的索引查询,实际上都成了慢查询,而且需要返回全表数据。大量线程过来,占用大量数据库连接,导致数据库连接数不够;而每个线程处理时,需要大量时间, 导致项目处于一种假死的状态。</p>
<h2>总结分析:</h2>
<p>1、在工作中一定要规范代码管理,才能提升开发效率,降低企业遭受损失的风险;</p>
<p>2、通过这次实践发现,目前开发权限小,导致跨部门协同效率不是很高,接下来的开发中,立项之初就建立项目开发流程,从而提升开发效率;</p>
<p>3、解决问题的方法遇到瓶颈,尝试第二种方法,多角度多层次对问题进行突破。</p>
<blockquote>作者:刘正权</blockquote>
宜信数据中台全揭秘(一)数据中台整体介绍|分享实录
https://segmentfault.com/a/1190000022905825
2020-06-11T16:46:23+08:00
2020-06-11T16:46:23+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<blockquote>内容来源:宜信技术学院第11期技术沙龙|宜信数据中台全揭秘(一)数据中台整体介绍<p>主讲人:宜信数据中台解决方案架构师 裴国强</p>
<p>PPT下载:链接: <a href="https://link.segmentfault.com/?enc=Z0SSzFLIwC5Ma7toG%2FNUGA%3D%3D.%2FdKTO9za1w53BHJy3%2BueQHEv9QJHMaPFdF%2B7Vf7qxZhYddzCq1hcfVFtdrodBzQo" rel="nofollow">https://pan.baidu.com/s/1eSkSdUo6FmYFmcE4xg0vjw</a> 密码: 99uh</p>
</blockquote>
<h2>一、数据中台定位</h2>
<h4>1.1 ADX整体简介-中台定位</h4>
<p><img src="/img/remote/1460000022905829" alt="" title=""><br>首先对中台的服务范围说明:</p>
<ul>
<li>企业级:针对是整个企业的所有业务部门,横向贯穿整个业务线的数据,纵向贯穿整个数据生命周期,从最开始的数据采集(DB,日志,消息,文件),入湖,标准化,开发(批量作业,流式作业)维度表,最后到数据服务和数据应用。</li>
<li>复用:复用的范围包括,能力的复用,逻辑的复用,数据资产的复用,算法的复用。</li>
<li>能力:对平台能力进行抽象,对于不同平台的对能力的抽象,业务平台(流程控制,管理,审批,权限「等级,继承」,调度),数据平台(批量,流式,UDF,UDAF,数据质量,血缘分析,数据地图,调度,数据资产管理,权限,数据服务)。</li>
</ul>
<h4>分横向和纵向两个方面:</h4>
<p><img src="/img/remote/1460000022905830" alt="" title=""></p>
<h4>横向划分</h4>
<ul>
<li>大数据基础集群:更贴近硬件的平台,负责提供稳定及高可用的计算运行环境,及安全的数据存储环境</li>
<li>HDFS-数据湖的基础存储,存放表每天的快照,和增量数据。</li>
<li>KUDU-最新快照,用于即席查询,数据服务,流式数据快照。</li>
<li>ClickHouse-Clickhouse做DW和DM层的存储。</li>
<li>数据中台 :对数据能力的抽象 ,数据的流式和批量加工,数据资产的发布,数据统一落湖,质量管理检测,脱敏加密,统一数据出口能力。</li>
<li>业务前台:对业务系统,业务线数据团队,提供各种不同的数据能力。使其能在中台上沉淀企业级数据资产。</li>
</ul>
<h4>纵向划分</h4>
<ul>
<li>数据管理委员会:对数据资产的质量认证,数据使用权限的授权,数据治理项目推动实施。</li>
<li>数据运营团队:客户标签,用户画像,产品画像,智能推荐,精细化管理。</li>
<li>数据安全团队:数据脱敏加密,安全密钥管控,数据风险的控制。</li>
</ul>
<h2>二、数据中台价值</h2>
<h4>2.1 数据中台价值</h4>
<p><img src="/img/remote/1460000022905828" alt="" title=""></p>
<ul><li>快:</li></ul>
<p>传统数仓定制化报表,排期周期长,响应需求慢,重复开发工作比较多。T+1的数据失效也满足不了现在互联网业务场景下对数据实时处理能力的需求。对中台平台自主化开发,可以提升数据加工能力沉淀,以及实时数据处理能力。</p>
<ul><li>准:</li></ul>
<p>数据获取准确性,通过统一数据抽取平台对数据实时抽取,同时完成标准化,入湖,脱敏发布。通过元数据和血缘分析准确获取数据地图。通过模型管理和统一模型口径。</p>
<ul><li>省:</li></ul>
<p>节省人力成本,大大降低大数据处理的技术门槛,使用户能够快速上手。节省需求排期时间,使数据能更快的响应业务需求。节省硬件资源,通过对平台资源的整合,规划,节省硬件使用维护成本。</p>
<h4>2.2 数据总线平台DBus</h4>
<p><img src="/img/remote/1460000022905831" alt="" title=""><br>DBus面向大数据项目开发和管理运维人员,致力于提供数据实时采集和分发解决方案。平台采用高可用流式计算框架,提供海量数据实时传输,可靠多路消息订阅分发,通过简单灵活的配置,无侵入接入源端数据,对各个IT系统在业务流程中产生的数据进行汇集,并统一处理转换成通过JSON描述的UMS格式,提供给不同下游客户订阅和消费。DBus可充当数仓平台、大数据分析平台、实时报表和实时营销等业务的数据源。目前dbus支持的数据源包括 mysql,Orale db2,Mongo,日志系统,文件系统等。</p>
<h4>2.3 流式处理平台Wormhole</h4>
<p><img src="/img/remote/1460000022905834" alt="" title=""></p>
<p>Wormhole面向大数据项目开发和管理运维人员,致力于提供数据流式处理解决方案。平台专注于简化和统一开发管理流程,提供可视化的操作界面,基于配置和SQL的业务开发方式,屏蔽底层技术实现细节,极大降低了开发门槛,使得大数据流式处理项目的开发和管理变得更加轻量敏捷、可控可靠。</p>
<h4>2.4 虚拟混算服务平台Moonbox</h4>
<p><img src="/img/remote/1460000022905832" alt="" title=""><br>Moonbox面向数据仓库工程师/数据分析师/数据科学家等,致力于提供数据虚拟化解决方案。既可作为数据应用底层数据查询计算统一入口,也可作为逻辑数据仓库与现有数据仓库互补。用户只需通过统一SQL服务调用和Moonbox交互,即可透明屏蔽异构数据系统异构交互方式,轻松实现跨异构数据系统透明混算。</p>
<h4>2.5 数据化可视应用平台Davinci</h4>
<p><img src="/img/remote/1460000022905835" alt="" title=""><br>Davinci面向业务人员/数据工程师/数据分析师/数据科学家,致力于提供一站式数据可视化解决方案。既可作为公有云/私有云独立部署使用,也可作为可视化插件集成到三方系统。用户只需在可视化UI上简单配置即可服务多种数据可视化应用,并支持高级交互/行业分析/模式探索/社交智能等可视化功能。</p>
<h2>三、数据中台模块架构</h2>
<h4>3.1 数据中台模块架构</h4>
<p><img src="/img/remote/1460000022905833" alt="" title=""><br>宜信中台整体底层采用wormhole+dbus+moonbox作为数据采集,加工,处理的底层引擎,通过服务的形式形成底层接口层提供数据实时处理的基础能力,在通过对接口层的整合,形成数据加工处理的子服务,使数据中台的后台服务完成调度,鉴权,认证,监控,告警。通过对不同组件层的能力整合完成了各项数据能力批量作业编排,调度,补数,手动重启,流式数据逻辑加工(source,lookup,transformation,union) flow在stream内的物理执行顺序,流式数据落湖,流式数据回溯。</p>
<h4>3.2 功能目录</h4>
<p><img src="/img/remote/1460000022905837" alt="" title=""><br>菜单划分<br>管理类(审批,库表,团队,规则,密钥,监控,预警,元数据);<br>功能类(批量作业,流式作业,即席查询,数据发布);<br>数据应用类(血缘分析,数据地图,数据模型,数据质量)。</p>
<h2>四、解决核心问题概览</h2>
<h4>4.1 批量作业处理</h4>
<p><img src="/img/remote/1460000022905836" alt="" title=""><br>专注于作业编辑编排,是数据项目的IDE,具体执行提交到对应中间件工具上执行。<br>简单一致的IDE体验</p>
<ul>
<li>批量作业、流式作业拖拽式编排</li>
<li>批量作业、流式作业SQL式开发</li>
<li>全局唯一表名,屏蔽异构数据系统</li>
<li>开发期可验证SQL和数据正确性</li>
</ul>
<h4>4.2 流式作业处理</h4>
<p><img src="/img/remote/1460000022905838" alt="" title=""><br>主要解决数据处理流程中错综复杂的依赖关系。</p>
<p>后面的沙龙我们将详细的介绍宜信数据中台的批量处理和流式处理功能请大家持续关注我们。</p>
搭建node服务(二):操作MySQL
https://segmentfault.com/a/1190000022823899
2020-06-03T12:43:28+08:00
2020-06-03T12:43:28+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
6
<blockquote>为了进行复杂信息的存储和查询,服务端系统往往需要数据库操作。数据库分为关系型数据库和非关系型数据库,关系型数据库有MySQL、Oracle、SQL Server等,非关系型数据库有Redis(常用来做缓存)、MongoDB等。MySQL是目前很流行的数据库,本文将要介绍如何在node服务中进行MySQL数据库操作。</blockquote>
<h2>一、 安装依赖</h2>
<pre><code>npm install mysql --save</code></pre>
<p>或者</p>
<pre><code>yarn add mysql</code></pre>
<h2>二、建立连接</h2>
<p>要想进行数据库操作就需要和数据库建立连接,然后通过连接进行数据库的操作。MySQL的数据库连接方式有以下几种:</p>
<ul>
<li>mysql.createConnection() 每次请求建立一个连接</li>
<li>mysql.createPool() 创建连接池,从连接池中获取连接</li>
<li>mysql.createPoolCluster() 创建连接池集群,连接池集群可以提供多个主机连接</li>
</ul>
<p>mysqljs文档中推荐使用第一种方式:每次请求建立一个连接,但是由于频繁的建立、关闭数据库连接,会极大的降低系统的性能,所以我选择了使用连接池的方式,如果对性能有更高的要求,安装了MySQL 集群,可以选择使用连接池集群。</p>
<h3>1. 数据库配置</h3>
<p>将数据库相关的配置添加到公用的配置文件中,方便项目的初始化。</p>
<ul><li>config.js</li></ul>
<pre><code>module.exports = {
…
// mysql数据库配置
mysql: {
// 主机
host: 'localhost',
// 端口
port: 3306,
// 用户名
user: 'root',
// 密码
password: '123456',
// 数据库名
database: 'server-demo',
// 连接池允许创建的最大连接数,默认值为10
connectionLimit: 50,
// 允许挂起的最大连接数,默认值为0,代表挂起的连接数无限制
queueLimit: 0
}
};
</code></pre>
<p><strong>connectionLimit 和 queueLimit 是数据连接池特有的配置项。</strong></p>
<ul>
<li>connectionLimit 是指连接池允许创建的最大连接数,默认值为10。当获取连接时,如果连接池中有空闲的连接则直接返回一个空闲连接。如果所有连接都被占用,则判断连接池中的连接数是否达到了允许的最大数,如果未达到则创建新的连接,如果已达到则获取连接的请求挂起,等待其他请求完成操作后释放的连接。</li>
<li>queueLimit 是指允许挂起的最大连接数,默认值为0,代表挂起的连接数无限制。当连接池中允许创建的所有连接都被占用时,获取连接的请求挂起,等待可用的连接,所有挂起的请求形成一个队列,queueLimit则是指这个队列的最大长度。需要注意的是,当queueLimit为0时并不表示不允许挂起,而是表示对挂起的数目没有限制。</li>
</ul>
<h3>2. 创建连接池</h3>
<ul><li>db/pool.js</li></ul>
<pre><code>/**
* 数据库连接池
*/
const mysql = require('mysql');
const config = require('../config');
// 创建数据库连接池
const pool = mysql.createPool(config.mysql);
pool.on('acquire', function (connection) {
console.log(`获取数据库连接 [${connection.threadId}]`);
});
pool.on('connection', function (connection) {
console.log(`创建数据库连接 [${connection.threadId}]`);
});
pool.on('enqueue', function () {
console.log('正在等待可用数据库连接');
});
pool.on('release', function (connection) {
console.log(`数据库连接 [${connection.threadId}] 已释放`);
});
module.exports = pool;</code></pre>
<p>创建数据库连接池pool后,就可以通过pool获取数据库连接了,另外通过监听连接池的事件可以了解连接池中连接的使用情况。<br>如果将connectionLimit 设为2,queueLimit 设为0,当同时有5个请求获取数据库连接时,线程池的事件日志如下:</p>
<pre><code>正在等待可用数据库连接
正在等待可用数据库连接
正在等待可用数据库连接
创建数据库连接 [1011]
获取数据库连接 [1011]
数据库连接 [1011] 已释放
获取数据库连接 [1011]
创建数据库连接 [1012]
获取数据库连接 [1012]
数据库连接 [1011] 已释放
获取数据库连接 [1011]
数据库连接 [1012] 已释放
获取数据库连接 [1012]
数据库连接 [1011] 已释放
数据库连接 [1012] 已释放</code></pre>
<p>由于线程池允许的最大连接数是2,5个请求中会有2个请求能够得到连接,另外3个请求挂起等待可用连接。由于创建数据库连接的代价比较大,线程池在创建连接时采用懒汉式,也就是,用到时才创建。先得到连接的请求在完成操作后释放连接,放回到连接池,然后挂起的请求从线程池取出空闲的连接进行操作。</p>
<h2>三、执行操作</h2>
<p>由于mysql 模块的接口都为回调方式的,为了操作方便简单地将接口封装为Promise,相关方法封装如下:</p>
<pre><code>const pool = require('./pool');
// 获取连接
function getConnection () {
return new Promise((resolve, reject) => {
pool.getConnection((err, connection) => {
if (err) {
console.error('获取数据库连接失败!', err)
reject(err);
} else {
resolve(connection);
}
});
});
}
// 开始数据库事务
function beginTransaction (connection) {
return new Promise((resolve, reject) => {
connection.beginTransaction(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
// 提交数据库操作
function commit (connection) {
return new Promise((resolve, reject) => {
connection.commit(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
})
}
// 回滚数据库操作
function rollback (connection) {
return new Promise((resolve, reject) => {
connection.rollback(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
})
}</code></pre>
<h3>1. 执行普通操作</h3>
<p>对于不需要使用事务的普通操作,获取数据库连接connection后,使用connection进行数据库操作,完成后释放连接到连接池,则执行完成一次操作。</p>
<ul><li>db/execute.js</li></ul>
<pre><code>
/**
* 执行数据库操作【适用于不需要事务的查询以及单条的增、删、改操作】
* 示例:
* let func = async function(conn, projectId, memberId) { ... };
* await execute( func, projectId, memberId);
* @param func 具体的数据库操作异步方法(第一个参数必须为数据库连接对象connection)
* @param params func方法的参数(不包含第一个参数 connection)
* @returns {Promise.<*>} func方法执行后的返回值
*/
async function execute (func, ...params) {
let connection = null;
try {
connection = await getConnection()
let result = await func(connection, ...params);
return result
} finally {
connection && connection.release && connection.release();
}
}</code></pre>
<h3>2. 执行事务操作</h3>
<p>对于很多业务都需要执行事务操作,例如:银行转账,A账户转账给B账户 100元,这个业务操作需要执行两步,从A账户减去100元,然后给B账户增加100元。两个子操作必须全部执行成功才能完成完整的业务操作,如果任意子操作执行失败就需要撤销之前的操作,进行回滚。</p>
<p>对于需要使用事务的操作,获取数据库连接connection后,首先需要调用connection.beginTransaction() 开始事务,然后使用connection进行多步操作,完成后执行connection.commit() 进行提交,则执行完成一次事务操作。如果在执行过程中出现了异常,则执行connection.rollback() 进行回滚操作。</p>
<ul><li>db/execute.js</li></ul>
<pre><code>/**
* 执行数据库事务操作【适用于增、删、改多个操作的执行,如果中间数据操作出现异常则之前的数据库操作全部回滚】
* 示例:
* let func = async function(conn) { ... };
* await executeTransaction(func);
* @param func 具体的数据库操作异步方法(第一个参数必须为数据库连接对象connection)
* @returns {Promise.<*>} func方法执行后的返回值
*/
async function executeTransaction(func) {
const connection = await getConnection();
await beginTransaction(connection);
let result = null;
try {
result = await func(connection);
await commit(connection);
return result
} catch (err) {
console.error('事务执行失败,操作回滚');
await rollback(connection);
throw err;
} finally {
connection && connection.release && connection.release();
}
}</code></pre>
<h2>四、增删改查</h2>
<p>增删改查是处理数据的基本原子操作,将这些操作根据操作的特点进行简单的封装。</p>
<ul><li>db/curd.js</li></ul>
<pre><code>/**
* 查询操作
* @param connection 连接
* @param sql SQL语句
* @param val SQL参数
* @returns {Promise} resolve查询到的数据数组
*/
function query (connection, sql, val) {
// console.info('sql执行query操作:\n', sql, '\n', val);
return new Promise((resolve, reject) => {
connection.query(sql, val, (err, rows) => {
if (err) {
console.error('sql执行失败!', sql, '\n', val);
reject(err);
} else {
let results = JSON.parse(JSON.stringify(rows));
resolve(results);
}
});
});
}
/**
* 查询单条数据操作
* @param connection 连接
* @param sql SQL语句
* @param val SQL参数
* @returns {Promise} resolve查询到的数据对象
*/
function queryOne (connection, sql, val) {
return new Promise((resolve, reject) => {
query(connection, sql, val).then(
results => {
let result = results.length > 0 ? results[0] : null;
resolve(result);
},
err => reject(err)
)
});
}
/**
* 新增数据操作
* @param connection 连接
* @param sql SQL语句
* @param val SQL参数
* @param {boolean} skipId 跳过自动添加ID, false: 自动添加id,true: 不添加id
* @returns {Promise} resolve 自动生成的id
*/
function insert (connection, sql, val, skipId) {
let id = val.id;
if (!id && !skipId) {
id = uuid();
val = {id, ...val};
}
return new Promise((resolve, reject) => {
// console.info('sql执行insert操作:\n', sql, '\n', val);
connection.query(sql, val, (err, results) => {
if (err) {
console.error('sql执行失败!', sql, '\n', val);
reject(err);
} else {
resolve(id);
}
});
});
}
/**
* 更新操作
* @param connection 连接
* @param sql SQL语句
* @param val SQL参数
* @returns {Promise} resolve 更新数据的行数
*/
function update (connection, sql, val) {
// console.info('sql执行update操作:\n', sql, '\n', val);
return new Promise((resolve, reject) => {
connection.query(sql, val, (err, results) => {
if (err) {
console.error('sql执行失败!', sql, '\n', val);
reject(err);
} else {
resolve(results.affectedRows);
}
});
});
}
/**
* 删除操作
* @param connection 连接
* @param sql SQL语句
* @param val SQL参数
* @returns {Promise} resolve 删除数据的行数
*/
function del (connection, sql, val) {
// console.info('sql执行delete操作:\n', sql, '\n', val);
return new Promise((resolve, reject) => {
connection.query(sql, val, (err, results) => {
if (err) {
console.error('sql执行失败!', sql, '\n', val);
reject(err);
} else {
// console.log('delete result', results);
resolve(results.affectedRows);
}
});
});
}</code></pre>
<h2>五、代码分层</h2>
<p>将代码分层可以降低代码的耦合度,提高可复用性、可维护性,这里将代码分成了3层:<strong>Dao层、Service层和Controller层。</strong></p>
<ul>
<li>
<strong>DAO层:</strong>主要负责数据持久化工作;</li>
<li>
<strong>Service层:</strong>主要负责业务模块的逻辑设计,此层的业务实现,可以调用DAO层的接口;</li>
<li>
<strong>Controller层:</strong>负责具体的业务模块流程的控制,在此层可以调用Service层的接口。</li>
</ul>
<h3>1.DAO层</h3>
<ul><li>dao/userDao.js</li></ul>
<pre><code>const { query, queryOne, update, insert, del } = require('../db/curd');
class UserDao {
static async queryUserById (connection, id) {
const sql = `SELECT user.id, user.account, user.name, user.email, user.phone,
user.birthday, user.enable, user.deleteFlag, user.creator,
user.createTime, user.updater, user.updateTime
FROM sys_user user
WHERE user.id = ?`;
const user = await queryOne(connection, sql, id);
return user;
}
…
}
module.exports = UserDao;</code></pre>
<h3>2.Service层</h3>
<ul><li>service/userService.js</li></ul>
<p>简单调用一个DAO层方法:</p>
<pre><code>const { execute, executeTransaction } = require('../db/execute');
const UserDao = require('../dao/userDao');
class UserService {
static async findUserById (id) {
return await execute(UserDao.queryUserById, id);
}
…
}
module.exports = UserService;</code></pre>
<p>对于复杂些的业务逻辑可以使用匿名函数来实现:</p>
<pre><code>static async findUserWithRoles (id) {
return await execute (async connection => {
const user = await UserDao.queryUserById(connection, id);
if (user) {
user.roles = await RoleDao.queryRolesByUserId(connection, id);
}
return user;
});
}</code></pre>
<p>如果要执行事务操作,则需要使用executeTransaction 方法:</p>
<pre><code>static async updateUserRoleRelations (userId, roleIds) {
return await executeTransaction(async connection => {
const relations = await UserDao.queryUserRoleRelations(connection, userId);
const oldRoleIds = relations.map(item => item.roleId);
const newRoleIds = roleIds || [];
// 新增的角色数组
const addList = [];
// 移除的角色数组
const removeList = [];
newRoleIds.forEach(roleId => {
if (oldRoleIds.indexOf(roleId) === -1) {
addList.push(roleId);
}
});
oldRoleIds.forEach(roleId => {
if (newRoleIds.indexOf(roleId) === -1) {
removeList.push(roleId);
}
});
if (addList.length > 0) {
await UserDao.insertUserRoleRelations(connection, userId, addList);
}
if (removeList.length > 0) {
await UserDao.deleteUserRoleRelations(connection, userId, removeList);
}
});
}</code></pre>
<h3>3.Controller层</h3>
<ul><li>controler/userController.js</li></ul>
<pre><code>const UserService = require('../service/userService');
class UserControler {
static async getUserById (ctx) {
// 用户ID
const id = ctx.params.id;
// 是否包含用户角色信息,如果withRoles 为 "1" 表示需要包含角色信息
const withRoles = ctx.query.withRoles;
let user;
if (withRoles === '1') {
user = await UserService.findUserWithRoles(id);
} else {
user = await UserService.findUserById(id);
}
if (user) {
ctx.body = user;
} else {
ctx.body = {
code: 1004,
msg: '用户不存在!'
}
}
}
…
}
module.exports = UserControler;</code></pre>
<p>此示例基于Koa框架,controller 层实现完成后需要添加路由:</p>
<pre><code>const router = new KoaRouter();
const UserController = require('./controler/userControler');
// 获取指定ID的用户
router.get('/users/:id', UserController.getUserById);
// 获取所有用户
router.get('/users', UserControler.getUsers);</code></pre>
<p>对于Koa框架如何使用,这里不再介绍,路由添加完毕后,启动服务,即可使用这些接口,如果本地服务启动的端口为3000,接口请求地址如下:</p>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=o4fFq%2FMBHwdG8xiHlCgVww%3D%3D.Mby2ip4v8LzrpDf09mnp3gJ2zo2XBYx%2F9Fmzp%2F4snqY%3D" rel="nofollow">http://localhost</a>:3000/users/3571a123-0454-49b4-a2bc-8b30a37f0b14</li>
<li>
<a href="https://link.segmentfault.com/?enc=QwWLpv%2FiTbFA00ZHkBJPNg%3D%3D.pj%2F6ZeCBnO54EHzsXkwLtpmva6MYFQC0VKiS5H%2Fkdks%3D" rel="nofollow">http://localhost</a>:3000/users/3571a123-0454-49b4-a2bc-8b30a37f0b14?withRoles=1</li>
<li>
<a href="https://link.segmentfault.com/?enc=mMuhe6jNuin67uGXbOK08g%3D%3D.UFzcbIL2pz2COC0MfXIsqik2VEkdQhjSRTqY7e%2FKbIc%3D" rel="nofollow">http://localhost</a>:3000/users/</li>
</ul>
<h2>六、说明</h2>
<p>本文介绍了mysql模块的基本使用,对其进行了简单封装,并提供了使用示例。除了使用mysql模块来操作数据库,也可以使用mysql2模块,mysql2的基本用法与mysql一致,另外mysql2还支持Promise,使用起来更方便。本文相关的代码已提交到GitHub以供参考,项目地址:<a href="https://link.segmentfault.com/?enc=qOUit%2FKdzn62lQwtVDgHQA%3D%3D.nuv%2BFn7QhEMUtOChsKEw7ufJ%2FGpvY0kZqzqvGvQPmRN0WIFJVf1ENvN%2F2%2BEmwYaYoEbYp4H9CP55%2F6b%2BF2Q4QQ%3D%3D" rel="nofollow">https://github.com/liulinsp/node-server-typeorm-demo</a>。</p>
<blockquote>作者:刘琳</blockquote>
一文读懂JAVA多线程
https://segmentfault.com/a/1190000022757840
2020-05-27T10:08:49+08:00
2020-05-27T10:08:49+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
3
<h2>一文读懂JAVA多线程</h2>
<h3>背景渊源</h3>
<h5>摩尔定律</h5>
<p>提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一Gordon Moore提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。</p>
<p>可是从2003年开始CPU主频已经不再翻倍,而是采用多核,而不是更快的主频。摩尔定律失效。那主频不再提高,核数增加的情况下要想让程序更快就要用到并行或并发编程。</p>
<h5>并行与并发</h5>
<p>如果CPU主频增加程序不用做任何改动就能变快。但核多的话程序不做改动不一定会变快。</p>
<p>CPU厂商生产更多的核的CPU是可以的,一百多核也是没有问题的,但是软件还没有准备好,不能更好的利用,所以没有生产太多核的CPU。随着多核时代的来临,软件开发越来越关注并行编程的领域。但要写一个真正并行的程序并不容易。</p>
<p>并行和并发的目标都是最大化CPU的使用率,并发可以认为是一种程序的逻辑结构的设计模式。可以用并发的设计方式去设计模型,然后运行在一个单核的系统上。可以将这种模型不加修改的运行在多核系统上,实现真正的并行,并行是程序执行的一种属性真正的同时执行,其重点的是充分利用CPU的多个核心。</p>
<p>多线程开发的时候会有一些问题,比如安全性问题,一致性问题等,重排序问题,因为这些问题然后大家在写代码的时候会加锁等等。这些基础概念大家都懂,本文不再描述。本文主要分享造成这些问题的原因和JAVA解决这些问题的底层逻辑。</p>
<h3>多线程</h3>
<h5>计算机存储体系</h5>
<p>要想明白数据一致性问题,要先缕下计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算。这个描述有点粗,下边画个图。</p>
<p><img src="/img/remote/1460000022757843" alt="" title=""></p>
<p>业内画这个图一般都是画的金字塔型状,为了证明是我自己画的我画个长方型的(其实我不会画金字塔)。</p>
<p>CPU多个核心和内存之间为了保证内部数据一致性还有一个缓存一致性协议(MESI),MESI其实就是指令状态中的首字母。M(Modified)修改,E(Exclusive)独享、互斥,S(Shared)共享,I(Invalid)无效。然后再看下边这个图。</p>
<p><img src="/img/remote/1460000022757845" alt="" title=""></p>
<p>太细的状态流转就不作描述了,扯这么多主要是为了说明白为什么会有数据一致性问题,就是因为有这么多级的缓存,CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。解决一致性问题怎么办呢,两个思路。</p>
<ol>
<li>锁住总线,操作时锁住总线,这样效率非常低,所以考虑第二个思路。</li>
<li>缓存一致性,每操作一次通知(一致性协议MESI),(但多线程的时候还是会有问题,后文讲)</li>
</ol>
<h5>JAVA内存模型</h5>
<p>上边稍微扯了一下存储体系是为了在这里写一下JAVA内存模型。</p>
<p>Java虚拟机规范中试图定义一种Java内存模型(java Memory Model) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。</p>
<p>内存模型是内存和线程之间的交互、规则。与编译器有关,有并发有关,与处理器有关。</p>
<p>Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。<strong>此处的变量与Java编程中所说的变量有所区别</strong>,它包括 了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,Java内存模型并没有限制执行引擎使用处理器特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。</p>
<p>Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。</p>
<p><strong>这里所说的主内存、工作内存和Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的。</strong> 如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存对应Java堆中的对象实例数据部分 ,而工作内存则对应于虚拟机栈中的部分区域。从更底层次上说,主内存就是直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。</p>
<p><img src="/img/remote/1460000022757844" alt="" title=""></p>
<p>前边说的都是和内存有关的内容,其实多线程有关系的还有指令重排序,指令重排序也会造成在多线程访问下结束和想的不一样的情况。大段的介绍就不写了要不篇幅太长了(JVM那里书里边有)。主要就是在CPU执行指令的时候会进行执行顺序的优化。画个图看一下吧。</p>
<p><img src="/img/remote/1460000022757846" alt="" title=""></p>
<p>具体理论后文再写先来点干货,直接上代码,一看就明白。</p>
<pre><code>public class HappendBeforeTest {
int a = 0;
int b = 0;
public static void main(String[] args) {
HappendBeforeTest test = new HappendBeforeTest();
Thread threada = new Thread() {
@Override
public void run() {
test.a = 1;
System.out.println("b=" + test.b);
}
};
Thread threadb = new Thread() {
@Override
public void run() {
test.b = 1;
System.out.println("a=" + test.a);
}
};
threada.start();
threadb.start();
}
}</code></pre>
<p>猜猜有可能输出什么?多选</p>
<pre><code>A:a=0,b=1
B:a=1,b=0
C:a=0,b=0
D:a=1,b=1
</code></pre>
<p>上边这段代码不太好调,然后我稍微改造了一下。</p>
<pre><code>public class HappendBeforeTest {
static int a = 0;
static int b = 0;
static int x = 0;
static int y = 0;
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
}
while (start + interval >= end);
}
public static void main(String[] args) throws InterruptedException {
for (; ; ) {
Thread threada = new Thread() {
@Override
public void run() {
a = 1;
x = b;
}
};
Thread threadb = new Thread() {
@Override
public void run() {
b = 1;
y = a;
}
};
Thread starta = new Thread() {
@Override
public void run() {
// 由于线程threada先启动
//下面这句话让它等一等线程startb
shortWait(100);
threada.start();
}
};
Thread startb = new Thread() {
@Override
public void run() {
threadb.start();
}
};
starta.start();
startb.start();
starta.join();
startb.join();
threada.join();
threadb.join();
a = 0;
b = 0;
System.out.print("x=" + x);
System.out.print("y=" + y);
if (x == 0 && y == 0) {
break;
}
x = 0;
y = 0;
System.out.println();
}
}
}
</code></pre>
<p>这段代码,a和b初始值为0,然后两个线程同时启动分别设置a=1,x=b和b=1,y=a。这个代码里边的starta和startb线程完全是为了让threada 和threadb 两个线程尽量同时启动而加的,里边只是分别调用了threada 和threadb 两个线程。然后无限循环只要x和y 不同时等于0就初始化所有值继续循环,直到x和y都是0的时候break。你猜猜会不会break。</p>
<p>结果看截图</p>
<p><img src="/img/remote/1460000022757848" alt="" title=""></p>
<p>因为我没有记录循环次数,不知道循环了几次,然后触发了条件break了。从代码上看,在输出A之前必然会把B设置成1,在输出B之前必然会把A设置为1。那为什么会出现同时是零的情况呢。这就很有可能是指令被重排序了。</p>
<p>指令重排序简单了说是就两行以上不相干的代码在执行的时候有可能先执行的不是第一条。也就是执行顺序会被优化。</p>
<p>如何判断你写的代码执行顺序会不会被优化,要看代码之间有没有<code>Happens-before</code>关系。<code>Happens-before</code>就是不无需任何干涉就可以保证有有序执行,由于篇幅限制<code>Happens-before</code>就不在这里多做介绍。</p>
<p>下面简单介绍一下java里边的一个关键字<code>volatile</code>。<code>volatile</code>简单来说就是来解决重排序问题的。对一个<code>volatile</code>变量的写,一定<code>happen-before</code>后续对它的读。也就是你在写代码的时候不希望你的代码被重排序就使用<code>volatile</code>关键字。<code>volatile</code>还解决了内存可见性问题,在执行执行的时候一共有8条指令lock(锁定)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)、unlock(解锁)(篇幅限制具体指令内容自行查询,看下图大概有个了解)。</p>
<p><img src="/img/remote/1460000022757847" alt="" title=""></p>
<p><code>volatile</code>主要是对其中4条指令做了处理。如下图</p>
<p><img src="/img/remote/1460000022757849" alt="" title=""></p>
<p>也就是把 load和use关联执行,把assign和store关联执行。众所周知有load必需有read现在load又和use关联也就是要在缓存中要use的时候就必须要load要load就必需要read。通俗讲就是要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。下面看写操作它是把assign和store做了关联,也就是在assign(赋值)后必需store(存储)。store(存储)后write(写入)。也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。</p>
<h5>无锁编程</h5>
<p>我在网上看到大部分写多线程的时候都会写到锁,AQS和线程池。由于网文太多本文就不多做介绍。下面简单写一写CAS。</p>
<p>CAS是一个比较魔性的操作,用的好可以让你的代码更优雅更高效。它就是无锁编程的核心。</p>
<p>CAS书上是这么介绍的:<strong>“CAS即Compare and Swap,是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性”</strong>。他是非阻塞的还是原子性,也就是说这玩意效率更高。还是通过硬件保证的说明这玩意更可靠。</p>
<p><img src="/img/remote/1460000022757850" alt="" title=""></p>
<p>从上图可以看出,在cas指令修改变量值的时候,先要进行值的判断,如果值和原来的值相等说明还没有被其它线程改过,则执行修改,如果被改过了,则不修改。在java里边<code>java.util.concurrent.atomic</code>包下边的类都使用了CAS操作。最常用的方法就是<code>compareAndSet</code>。其底层是调用的<code>Unsafe</code>类的<code>compareAndSwap</code>方法。</p>
<blockquote>作者:高玉珑</blockquote>
基于Ceph对象存储构建实践
https://segmentfault.com/a/1190000022694325
2020-05-20T13:49:43+08:00
2020-05-20T13:49:43+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<h2>存储发展</h2>
<p>数据存储是人类永恒的话题和不断探索的主题</p>
<ul>
<li>绳结记事<p>原始社会,文字未发明之前 ,人们所使用的一种记事方法,在绳子上打结记事。</p>
</li>
<li>穿孔卡<p>穿孔卡片是始于20世纪的主要存储方法,也是最早的机械化信息存储形式,进入20世纪60年代后,逐渐被其他存储手段取代。目前穿孔卡片已经极少使用,除非用于读出当年存储的历史数据。</p>
</li>
<li>磁鼓存储器<p>20世纪50年代,磁鼓作为内存储器应用于IBM 650。在后续的IBM 360/91和DEC PDP-11中,磁鼓也用作交换区存储和页面存储。磁鼓的代表性产品是IBM 2301固定头磁鼓存储器。磁鼓是利用铝鼓筒表面涂覆的磁性材料来存储数据的。鼓筒旋转速度很高,因此存取速度快。它采用饱和磁记录,从固定式磁头发展到浮动式磁头,从采用磁胶发展到采用电镀的连续磁介质。这些都为后来的磁盘存储器打下了基础。</p>
<p>磁鼓最大的缺点是存储容量太小。一个大圆柱体只有表面一层用于存储,而磁盘的两面都可用来存储,显然利用率要高得多。因此,当磁盘出现后,磁鼓就被淘汰了。</p>
</li>
<li>磁带<p>磁带是从1951年起被作为数据存储设备使用的,磁带是所有存储媒体中单位存储成本最低、容量最大、标准化程度最高的常用存储介质之一。从 20 世纪 70 年代后期到 80 年代出现了小型的盒式磁带,长度为 90 分钟的磁带每一面可以记录大约 660KB的数据。</p>
</li>
<li>软盘<p>软盘发明于1969年,直径是8英寸,单面容量80KB。4年后,5.25英寸、容量为320KB的软盘诞生了。软盘的发展趋势是盘片直径越来越小,而容量却越来越大,可靠性也越来越高。图2-10是三种典型的软盘,其中a为不同外观尺寸的软盘,b中3.5英寸软盘的容量为1.44MB,曾经作为主要的移动存储介质被广泛使用。到了20世纪90年代后期,出现了容量为250MB的3.5英寸软盘产品,但由于兼容性、可靠性、成本等原因,并未被广泛使用,如今已难寻踪迹。</p>
</li>
<li>光盘<p>早期光盘主要用于电影行业,第一张光盘于1987年进入市场, 直径为30cm,每一面可以记录60分钟的音视频。</p>
</li>
<li>硬盘存储器<p>第一款硬盘驱动器是IBM Model 350 Disk File, 于 1956 年制造,包含了 50 张 24 英寸的盘片,总容量不到5MB,机械硬盘发展至今,单盘容量已经超过16T了 。</p>
</li>
</ul>
<h2>存储的三种方式</h2>
<h3>块存储</h3>
<p><img src="/img/remote/1460000022694329" alt="块存储-图片来自redhat官网" title="块存储-图片来自redhat官网"></p>
<h4>DAS</h4>
<p>直接附加存储(Directed Attached Storage,DAS)作为一种最简单的外接存储方式,通过数据线直接连接在各种服务器或客户端扩展接口上。它本身是硬件的堆叠,不带有任何存储操作系统,因而也不能独立于服务器对外提供存储服务。DAS常见的形式是外置磁盘阵列,通常的配置就是RAID控制器+一堆磁盘。DAS安装方便、成本较低的特性使其特别适合于对存储容量要求不高、服务器数量较少的中小型数据中心。</p>
<h4>SAN</h4>
<p>存储区域网络(Storage Area Network,简称SAN),SAN默认指FC-SAN,SAN存储有两种结构:</p>
<ul>
<li>FC-SAN<p>典型的SAN利用光纤通道(Fiber Channel,FC)技术连接节点,并使用光纤通道交换机(FC Switch)提供网络交换。不同于通用的数据网络,存储区域网络中的数据传输基于FC协议栈。在FC协议栈之上运行的SCSI协议提供存储访问服务。与之相对的iSCSI存储协议,则提供了一种低成本的替代方式,即将SCSI协议运行于TCP/IP协议栈之上。为了区别这两种存储区域网络,前者通常称为FC SAN,后者称为IP SAN。</p>
</li>
<li>IP-SAN<p>由于FC-SAN的高成本,人们就开始考虑构建基于以太网技术的存储网络,使得的iSCSI可以实现在IP网络上运行SCSI协议。但是在SAN中,传输的指令是 SCSI的读写指令,不是IP数据包。iSCSI(互联网小型计算机系统接口)是一种在TCP/IP上进行数据块传输的标准。它是由Cisco和IBM两家发起的,并且得到了各大存储厂商的大力支持。iSCSI可以实现在IP网络上运行SCSI协议,使其能够在诸如高速千兆以太网上进行快速的数据存取备份操作。为了与之前基于光纤技术的FC SAN区分开来,这种技术被称为IP SAN。</p>
</li>
</ul>
<h5>优点</h5>
<ul><li>高性能,集中化的管理,稳定性和安全性得到保障</li></ul>
<h5>缺点</h5>
<ul><li>成本昂贵,磁盘阵列的兼容性限制了设备选择空间及资源共享</li></ul>
<h3>NAS存储</h3>
<p><img src="/img/remote/1460000022694328" alt="NAS存储-图片来自redhat官网" title="NAS存储-图片来自redhat官网"></p>
<p>图片来源:redhat官网</p>
<p>Network Attached Storage 网络附加存储,采用 <code>NFS</code> 或 <code>CIFS</code> 协议访问数据,以文件为传输协议,通过 <code>TCP/IP </code>实现网络化存储,可扩展性好、价格便宜、用户易管理,如目前在集群计算中应用较多的NFS文件系统。</p>
<h5>优点</h5>
<ul>
<li>造价成本低,有一个服务器,装上网络文件存储软件,就可以提供给其他服务器挂载访问。</li>
<li>文件级的数据共享</li>
</ul>
<h5>缺点</h5>
<ul><li>读写速率低</li></ul>
<h3>对象存储</h3>
<p><img src="/img/remote/1460000022694330" alt="对象存储" title="对象存储"></p>
<p>块存储读写快、不利于数据共享,文件存储数据共享方便、但是读写慢,能否弄一个读写快而且可以共享数据的存储,于是对象存储就诞生了。块存储和文件存储是我们比较熟悉的两种主流的存储类型,而对象存储(Object-based Storage)是一种新的网络存储架构。</p>
<h4>3个核心概念</h4>
<h5>对象</h5>
<p>对象是对象存储中的最小单元,比如照片就是一个对象,对象由元数据信息(MataData,包含Length,lastModify等),用户数据(Data),用户自定义的数据信息(拍摄者、拍摄设备等)和文件名(Key)组成。</p>
<p><img src="/img/remote/1460000022694332" alt="" title=""></p>
<h5>存储桶</h5>
<p>作为存放对象的容器</p>
<h5>用户</h5>
<p>对象存储的使用者,存储桶的拥有者,每个用户使用AccessKeyId 和 SecretAccessKey对称加密的方法来验证某个请求的发送者身份。</p>
<h3>对象存储适合存什么</h3>
<p>用来存海量非结构化数据的,对象存储将数据以对象的方式存储,而不是以传统的文件和数据块的形式存储,每个对象都要存储数据、元数据和一个唯一的标识符。</p>
<ul>
<li>图片</li>
<li>视频</li>
<li>音频</li>
<li>文档</li>
<li>代码js/html</li>
</ul>
<h5>缺点</h5>
<p>应用代码需要改动,无法修改对象,需要一次性完整写入</p>
<h5>优点</h5>
<p>无限扩容</p>
<h2>基于Ceph的对象存储构建实践</h2>
<h3>什么是Ceph</h3>
<p>加州大学 Santa Cruz 分校的 Sage Weil(DreamHost 的联合创始人)博士论文设计的新一代自由软件分布式文件系统。软件定义存储(Software Defined Storage, SDS)。统一的存储解决方案。 提供了三种存储方式:块存储、文件存储、对象存储。Ceph的架构如下:</p>
<p><img src="/img/remote/1460000022694331" alt="" title=""></p>
<p>图片来源:Ceph官网</p>
<h3>Ceph组件</h3>
<p><strong>Ceph Monitor(监视器,简称Mon)</strong></p>
<p>Mon通过保存一份集群状态映射来的维护整个集群的健康状态。它分别为每个组件维护映射信息。所有集群节点都向Mon节点汇报状态信息</p>
<p><strong>RADOS</strong></p>
<p>(Reliable Autonomix Distributed Object Store),是存储集群的基础。在Ceph中所有的数据都是以对象的形式存储,RADOS就负责存这些数据,不考虑它们的类型。</p>
<p><strong>Ceph对象存储设备OSD</strong></p>
<p>Ceph 分布式对象存储系统的对象存储守护进程。它负责把对象存储到本地文件系统,并使之通过网络可访问。</p>
<p><strong>RADOS网关(RGW)</strong></p>
<p>提供了兼容Amazon S3和OpenStack对象存储API(Swift)的restful API接口。支持多租户和OpenStack Keystone身份验证。</p>
<p><strong>MDS(Ceph元数据服务器)</strong></p>
<p>为CephFS跟踪文件层次结构和存储元数据。</p>
<p><strong>librados</strong></p>
<p>librados库为PHP,Ruby,Java,Python,C和C++这些编程语言提供了方便地访问RADOS接口的方式。</p>
<p><strong>RBD(RADOS块设备)</strong></p>
<p>Ceph块设备,原名是 RADOS 块设备,提供可靠的分布式和高性能块存储磁盘给客户端,将块数据以顺序条带化的形式分散存储在的多个 OSD 上,支持自动精简配置、动态调整大小、完整和增量快照、写实复制克隆等企业级特性,而且RBD服务已经被封装成了基于 librados 的一个原生接口。</p>
<p><strong>CephFS(Ceph Filesystem)</strong></p>
<p>Ceph文件系统提供了一个使用Ceph存储集群存储用户数据的与POSIX兼容的文件系统。和RBD、RGW一样,基于librados封装了原生接口。</p>
<h3>Ceph的特点</h3>
<ul>
<li>高性能<p>摒弃了传统的集中式存储元数据寻址的方案,采用CRUSH算法,数据分布均衡,并行度高。</p>
</li>
<li>高可用性<p>数据强一致性,多种故障场景自愈</p>
</li>
<li>高扩展性<p>去中心化、灵活扩展</p>
</li>
<li>特性丰富<p>支持三种存储接口:块存储、对象存储、文件存储</p>
<p>支持多种语言(Python、C++、Java、PHP、Ruby等)驱动,自定义接口</p>
</li>
</ul>
<h3>基于Ceph的对象存储实践</h3>
<p>客户端通过 4,7 层负载均衡,基于HTTP协议,将请求转发至对象存储网关(Rados GateWay), 对象存储网关通过Sockets与集群通信,至此,完成了整个数据的传输。</p>
<p><img src="/img/remote/1460000022694334" alt="" title=""></p>
<h4>用户认证</h4>
<p><img src="/img/remote/1460000022694333" alt="s3用户认证" title="s3用户认证"></p>
<ol>
<li>应用在发送请求前,使用用户私有秘钥(secret key)、请求内容等,采用与RGW网关约定好的算法计算出数字签名后,将数字签名以及用户访问秘钥access_key封装在请求中发送给RGW网关</li>
<li>RGW网关接受到请求后,使用用户访问秘钥作为索引送RADOS集群中读取用户信息,并从用户信息中获取到用户私有秘钥。</li>
<li>使用用户私有秘钥、请求内容等,采用与应用约定好的算法计算数字签名。</li>
<li>判断RGW生成的数字签名和请求的签名是否匹配,如果匹配,则认为请求是真实的,用户认证通过,如果匹配返回 S3 error: 403 (SignatureDoesNotMatch)</li>
</ol>
<h3>对象存储IO路径分析</h3>
<p><img src="/img/remote/1460000022694335" alt="对象存储IO路径分析" title="对象存储IO路径分析"></p>
<p>应用通过http协议将请求发送至对象存储网关,网关收到 I/O 请求后,从http语义中解析出S3或Swift数据并进行一系列检查,检查通过后,根据不同API操作请求执行不同的数据处理逻辑,通过 librados 接口从 RADOS Cluster中 GET 或者 PUT 数据,完成整个I/O过程。</p>
TestNG测试用例重跑详解及实践优化
https://segmentfault.com/a/1190000022553030
2020-05-06T10:24:24+08:00
2020-05-06T10:24:24+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
4
<blockquote>测试用例运行稳定性是自动化质量的一个重要指标,在运行中需要尽可能的剔除非bug造成的测试用例执行失败,对于失败用例进行重跑是常用策略之一。一种重跑策略是所有用例运行结束后对失败用例重跑,另一种重跑策略是在运行时监控用例运行状态,失败后实时重跑。</blockquote>
<p>下面,详细介绍TestNG如何对失败测试用例实时重跑并解决重跑过程中所遇到问题的实践和解决方案。对失败测试用例进行实时重跑,有以下几个方面需求:</p>
<ol>
<li>测试用例运行失败,监听到失败后立即进行重跑</li>
<li>测试用例通过<code>dependsOnMethods/dependsOnGroups</code>标记依赖其他测试用例,在被依赖的测试用例重跑运行成功后,该测试用例可以继续运行</li>
<li>对于重跑多次的测试用例,只记录最后一次运行成功或失败结果</li>
</ol>
<h2>第一部分 测试用例重跑</h2>
<h3>1.1 retryAnalyzer注解方式</h3>
<p>对于希望测试用例中的少量易失败,不稳定的测试用例进行重跑,可采用这种方式。</p>
<h3>1.1.1 原理</h3>
<p>以下是TestNG处理测试用例运行结果的部分代码。</p>
<pre><code>IRetryAnalyzer retryAnalyzer = testMethod.getRetryAnalyzer();
boolean willRetry = retryAnalyzer != null && status == ITestResult.FAILURE && failure.instances != null && retryAnalyzer.retry(testResult);
if (willRetry) {
resultsToRetry.add(testResult);
failure.count++;
failure.instances.add(testResult.getInstance());
testResult.setStatus(ITestResult.SKIP);
} else {
testResult.setStatus(status);
if (status == ITestResult.FAILURE && !handled) {
handleException(ite, testMethod, testResult, failure.count++);
}</code></pre>
<p>分析以上代码,其中,接口<code>IretryAnalyzer</code>的方法<code>retry()</code>的返回值作为是否对失败测试用例进行重跑的一个条件。如果<code>retry()</code>结果为<code>true</code>,则该失败测试用例会重跑,同时将本次失败结果修改为<code>Skip</code>;如果结果为<code>false</code>,则失败的测试用例保持失败结果,运行结束。因此,如果你希望失败测试用例重跑的话,需要把<code>IretryAnalyzer的retry()</code>方法重写,插入自己定义的逻辑,设置返回值为<code>true</code>。</p>
<h3>1.1.2 代码</h3>
<p>创建类<code>RetryImpl</code>,重写<code>retry()</code>方法,设置失败测试用例的重跑次数,代码如下,:</p>
<pre><code>public class RetryImpl implements IRetryAnalyzer {
private int count = 1;
private int max_count = 3; // Failed test cases could be run 3 times at most
@Override
public boolean retry(ITestResult result) {
System.out.println("Test case :"+result.getName()+",retry time: "+count+"");
if (count < max_count) {
count++;
return true;
}
return false;
}
}</code></pre>
<h3>1.1.3 实例</h3>
<pre><code>public class TestNGReRunDemo {
@Test(retryAnalyzer=RetryImpl.class)
public void test01(){
Assert.assertEquals("success","fail");
System.out.println("test01");
}
}</code></pre>
<p>以上测试用例test01可重复运行3次。</p>
<h3>1.2 实现接口IAnnotationTransformer方法</h3>
<p>如果希望所有失败的测试用例都进行重跑,采用<code>retryAnalyzer</code>注解方式对每个测试用例进行注解就比较麻烦。通过实现<code>IAnnotationTransformer</code>接口的方式,可以对全量测试用例的重试类进行设置。 <br>该接口是一个监听器接口,用来修改TestNG注解。<code>IAnnotationTransformer</code>监听器接口只有一个方法:<code>transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod)</code>. 上文中,我们自定义了类<code>RetryImpl</code> 实现接口<code>IRetryAnalyzer</code>。TestNG通过<code>transfrom()</code>方法修改<code>retryAnalyzer</code>注解。以下代码对<code>retryAnalyzer</code>注解进行修改设置。</p>
<h3>1.2.1代码</h3>
<p>创建类<code>RetryListener</code>,代码如下。</p>
<pre><code>public class RetryListener implements IAnnotationTransformer {
public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) {
IRetryAnalyzer retry = annotation.getRetryAnalyzer();
if (retry == null) {
annotation.setRetryAnalyzer(RetryImpl.class);
}
}
}</code></pre>
<h3>1.2.2 配置Listener</h3>
<p>TestNG可以在配置文件或者测试类中对<code>Listener</code>类进行配置。</p>
<ul><li>
<strong>方法一</strong>:<strong>在TestNG的配置XML中进行以下配置</strong>
</li></ul>
<pre><code><listeners>
<listener class-name="PackageName.RetryListener"></listener>
</listeners></code></pre>
<ul><li>
<strong>方法二</strong>:<strong>在测试类中通过@Listeners配置</strong>
</li></ul>
<pre><code>@Listeners({RetryListener.class})
public class TestNGReRunDemo {
@Test
public void test01(){
Assert.assertEquals("success","fail");
System.out.println("test01");
}
}</code></pre>
<p>配置完成后,运行测试用例test01,运行结果显示test01将重跑次数3次。</p>
<h2>第二部分 被依赖的测试用例重跑结果处理</h2>
<p>进一步分析TestNG的运行代码,其在对失败运行用例重跑时,逻辑如下图。<br><img src="/img/remote/1460000022553033" alt="" title=""><br>对于通过<code>dependsOnMethods</code> 或<code>dependsOnGroups</code>注解依赖于其他测试用例的测试用例来讲,测试用例执行分为两种情况:</p>
<ul>
<li>
<strong>alwaysRun=true</strong>,则无论所依赖的测试用例执行情况如何,该测试用例都会执行,即所依赖的测试用例重跑不会影响该测试用例的执行。</li>
<li>
<strong>alwaysRun=false</strong>,或者保持缺省值(false),依赖于其他测试用例或测试用例组的测试结果,在运行时TestNG获取所依赖的测试用例的运行结果,检查依赖的测试用例是否全部执行成功,如果不全部成功,则把该测试用例结果设置为Skipped。</li>
</ul>
<h3>2.1 场景分析:场景一</h3>
<p>被依赖的测试用例失败后进行了重跑,并重跑成功。(注:在<code>RetryImpl</code>类中,<strong>已设置最大重跑次数max_count = 3</strong>)</p>
<pre><code>public static int number =0;
@Test
public void test01(){
number++;
System.out.println(String.valueOf(number));
Assert.assertEquals(number,2);
System.out.println("test01");
}
@Test(dependsOnMethods = "test01") // alwaysRun = false by default
public void test02(){
System.out.println("test02 is running only if test01 is passed.");
}</code></pre>
<h4>1、TestNG测试报告</h4>
<p><img src="/img/remote/1460000022553034" alt="" title=""></p>
<h4>2、问题</h4>
<table>
<thead><tr>
<th>测试用例</th>
<th>运行次数</th>
<th>运行情况</th>
<th>测试报告</th>
</tr></thead>
<tbody>
<tr>
<td>Test01</td>
<td>2</td>
<td>第一次:skipped ; 第二次:passed</td>
<td>在Skipped 和Passed的统计数量中,test01被分别记录一次</td>
</tr>
<tr>
<td>Test02</td>
<td>0</td>
<td>Skipped</td>
<td>记录一次Skipped</td>
</tr>
</tbody>
</table>
<ul>
<li>
<strong>测试报告:</strong>test01运行结果全部被记录,而用例重跑,只希望记录最后的结果。</li>
<li>
<strong>运行情况:</strong>测试用例test02依赖于测试用例test01运行结果,在test01重跑成功后,测试用例test02没有执行,不符合需求预期。</li>
</ul>
<h3>2.2 场景分析:场景二</h3>
<p>被依赖的测试用例失败后进行了重跑,并且重跑没有成功。(注:在RetryImpl类中,已设置最大重跑次数max_count = 3)</p>
<pre><code>public static int number =0;
@Test
public void test01(){
number++;
System.out.println(String.valueOf(number));
Assert.assertEquals(number,10);
System.out.println("test01");
}
@Test(dependsOnMethods = "test01") // alwaysRun = false by default
public void test02(){
System.out.println("test02 is running only if test01 is passed.");
}</code></pre>
<h4>1、TestNG测试报告</h4>
<p><img src="/img/remote/1460000022553035" alt="" title=""></p>
<h4>2、问题</h4>
<table>
<thead><tr>
<th>测试用例</th>
<th>运行次数</th>
<th>运行结果</th>
<th>测试报告</th>
</tr></thead>
<tbody>
<tr>
<td>Test01</td>
<td>3</td>
<td>第一次:skipped;第二次:skipped;第三次:failed</td>
<td>在Skipped统计数量中,test01被被记录两次在failed统计中,test01被记录一次</td>
</tr>
<tr>
<td>Test02</td>
<td>0</td>
<td>Skipped</td>
<td>记录一次Skipped</td>
</tr>
</tbody>
</table>
<ul>
<li>
<strong>运行情况:</strong>测试用例test02依赖于测试用例test01运行结果,在test01重跑失败后,测试用例test02没有执行,这种情况符合需求预期。</li>
<li>
<strong>测试报告:</strong>同场景一,test01重跑失败,运行结果全部被记录,而用例重跑,只希望记录最后的结果。</li>
</ul>
<h2>第三部分 优化解决方案</h2>
<p>以下方案解决重跑测试用例成功后后继测试用例无法继续运行的问题,并对测试报告进行优化。</p>
<h3>3.1 <code>TestListenerAdapter</code>方法重写</h3>
<p>根据上面分析的TestNG逻辑,在对依赖测试用例的结果进行检查时,如果忽略重跑的中间结果只检查最后一次的运行结果,可以达到需求的目的。对于测试报告,同样的处理方式,忽略所有中间的测试用例运行结果,只记录最后结果。<br>测试用例的中间运行结果为<code>Skipped</code>,下面的代码通过重写<code>TestListenerAdapter</code>的<code>onTestSuccess()</code>和<code>onTestFailure()</code>方法,对测试用例的中间结果<code>skipped</code>进行了删除。代码如下:</p>
<pre><code>public class ResultListener extends TestListenerAdapter {
@Override
public void onTestFailure(ITestResult tr) {
if(tr.getMethod().getCurrentInvocationCount()==1)
{
super.onTestFailure(tr);
return;
}
processSkipResult(tr);
super.onTestFailure(tr);
}
@Override
public void onTestSuccess(ITestResult tr) {
if(tr.getMethod().getCurrentInvocationCount()==1)
{
super.onTestSuccess(tr);
return;
}
processSkipResult(tr);
super.onTestSuccess(tr);
}
// Remove all the dup Skipped results
public void processSkipResult(ITestResult tr)
{
ITestContext iTestContext = tr.getTestContext();
Iterator<ITestResult> processResults = iTestContext.getSkippedTests().getAllResults().iterator();
while (processResults.hasNext()) {
ITestResult skippedTest = (ITestResult) processResults.next();
if (skippedTest.getMethod().getMethodName().equalsIgnoreCase(tr.getMethod().getMethodName()) ) {
processResults.remove();
}
}
}
}</code></pre>
<h3>3.2 配置结果处理Listener类</h3>
<p>在配置文件进行全局设置或者在测试类中标记。</p>
<ul><li><strong>方法一:在TestNG的配置XML中进行以下配置</strong></li></ul>
<pre><code><listeners>
<listener class-name="PackageName.ResultListener"></listener>
</listeners></code></pre>
<ul><li><strong>方法二:在测试类中通过@Listeners配置</strong></li></ul>
<pre><code>@Listeners({ResultListener.class})
public class TestNGReRunDemo {
@Test
public void test01(){
Assert.assertEquals("success","fail");
System.out.println("test01");
}
}
</code></pre>
<h3>3.3 场景一</h3>
<h4>1、 <strong>结果验证</strong>
</h4>
<p><img src="/img/remote/1460000022553037" alt="" title=""></p>
<h4>2、<strong>结果分析:</strong>
</h4>
<table>
<thead><tr>
<th>测试用例</th>
<th>运行次数</th>
<th>运行结果</th>
<th>测试报告</th>
</tr></thead>
<tbody>
<tr>
<td>Test01</td>
<td>2</td>
<td>第一次:skipped;第二次:passed</td>
<td>只在Passed的统计数量中test01被记录一次</td>
</tr>
<tr>
<td>Test02</td>
<td>1</td>
<td>Passed</td>
<td>记录一次passed</td>
</tr>
</tbody>
</table>
<h3>3.4 场景二</h3>
<h4>1、<strong>结果验证</strong>
</h4>
<p><img src="/img/remote/1460000022553036" alt="" title=""></p>
<h4>2、<strong>结果分析:</strong>
</h4>
<table>
<thead><tr>
<th>测试用例</th>
<th>运行次数</th>
<th>运行结果</th>
<th>测试报告</th>
</tr></thead>
<tbody>
<tr>
<td>Test01</td>
<td>3</td>
<td>第一次:skipped;第二次:skipped;第三次:failed</td>
<td>test01只在failed统计中被记录一次</td>
</tr>
<tr>
<td>Test02</td>
<td>1</td>
<td>Skipped</td>
<td>依赖用例执行失败,test02结果为Skipped,只记录一次结果Skipped</td>
</tr>
</tbody>
</table>
<blockquote>作者:耿燕飞<br>来源:宜信技术学院</blockquote>
搭建node服务(一):日志处理
https://segmentfault.com/a/1190000022486950
2020-04-27T16:01:23+08:00
2020-04-27T16:01:23+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
10
<p>对于一个应用程序来说,日志记录是非常重要的。日志可以帮助开发人员快速定位线上问题,定制解决方案;日志中包含大量用户信息,通过日志分析还可以获取用户行为、兴趣偏好等信息,通过这些信息可以得到用户画像,对公司战略的制定提供参考。本文将要介绍如何在node服务中处理日志。</p>
<h3>一、技术选型</h3>
<p>选择了3种主流的技术进行对比:</p>
<h3>1.1 log4js</h3>
<p>log4js是一种node日志管理工具,可以将自定义格式的日志输出到各种渠道。对于控制台的日志输出可以呈现彩色日志,对于文件方式的日志输出,可以根据文件大小或者日期进行日志切割。</p>
<p>熟悉java的开发人员会发现log4js与一种常用的java日志工具log4j很像。没错,log4js是log4j的JavaScript版,使用方式也相似。</p>
<h3>1.2 winston</h3>
<p>winston也是一种非常流行的node日志管理工具,支持多传输。默认输出格式为json,也可以自定义输出格式。如果想要对日志进行切割还需要使用 winston-daily-rotate-file 模块。</p>
<h3>1.3 PM2</h3>
<p>PM2实际是node进程管理工具,具有性能监控、进程守护、负载均衡、日志管理等功能。使用PM2进行日志管理,只需要项目中增加console方法调用,无需添加额外的代码。要对日志进行切割,需要使用pm2-logrotate。</p>
<p>由于团队内部服务端系统很多是基于java的,这些系统大部分使用log4j生成日志。日志管理相关的日志归集系统和日志查询系统对log4j格式的日志支持的更好,所以自己最终选用了log4j的JavaScript版log4js来生成日志,下面会对log4js的基本使用进行介绍。</p>
<h2>二、基本概念</h2>
<h3>2.1 日志级别</h3>
<p>log4js的默认日志级别分为9级,按优先级从低到高排列如下:</p>
<p>ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < MARK < OFF</p>
<ul>
<li>当日志级别为ALL时,会输出所有级别的日志</li>
<li>当日志级别为OFF时,则会关闭日志,不会有任何日志输出</li>
<li>用户还可以根据自己的需要自定义日志级别</li>
</ul>
<h3>2.2 appender</h3>
<p>appender 主要是用来定义以怎样的方式输出,输出到哪里。可以将日志写入到文件、发送电子邮件、通过网络发送数据等。可以通过配置对象的appenders属性定义多个appender。</p>
<p>appender的常用类型有:</p>
<ul>
<li>console:控制台输出</li>
<li>file:文件输出</li>
<li>dateFile:按日期切割的文件输出</li>
</ul>
<h3>2.3 category</h3>
<p>category 是日志的类型,指定一个或者多个appender为某种类型的日志,不同类型的日志可以指定不同的日志级别。可以通过配置对象的categories属性定义多个category。必须指定default类型,用来获取默认的Logger实例,还可以通过类型名来获取指定类型的Logger实例。</p>
<p>综上所诉,appender 定义了日志输出到哪里,category 将appender 进行了分类,不同类型指定不同的日志级别。</p>
<h2>三、使用log4js</h2>
<h3>3.1 安装</h3>
<p><code>npm install log4js --save</code></p>
<p>或者</p>
<p><code>yarn add log4js</code></p>
<h3>3.2 简单使用</h3>
<p>下面示例利用log4js创建日志对象logger,通过调用logger.debug、logger.info、logger.warn、logger.error 等方法将日志输出到控制台和日志文件。</p>
<ul><li>util/log4jsLogger.js</li></ul>
<pre><code>const path = require('path');
const log4js = require('log4js');
// 配置log4js
log4js.configure({
appenders: {
// 控制台输出
console: { type: 'console' },
// 日志文件
file: { type: 'file', filename: path.join(__dirname, '../../logs/server.log')}
},
categories: {
// 默认日志
default: { appenders: [ 'file', 'console' ], level: 'debug' },
}
});
// 获取默认日志
const logger = log4js.getLogger();
module.exports = logger;</code></pre>
<ul><li>server.js</li></ul>
<p>再通过调用logger. info 输出INFO 级别的日志,这里web开发框架使用的Koa。</p>
<pre><code>const Koa = require('koa');
const router = require('./router');
const logger = require('./util/log4jsLogger');
const port = 3000;
const app = new Koa()
.use(router.routes())
.use(router.allowedMethods());
app.listen(port, () => {
logger.info(`Server running on port ${port}`);
});</code></pre>
<h3>3.3 日志格式</h3>
<p>log4js通过layout 设置日志格式,内置的layout有:</p>
<ul>
<li>basic 包含时间戳、日志级别、日志类型的基本日志格式</li>
<li>colored 格式与basic的一致,只是不同级别的日志显示不同的颜色</li>
<li>dummy 只输出第一个参数的内容,没有时间戳、日志级别、日志分类等信息</li>
<li>pattern 可以自定义格式的layout</li>
</ul>
<p>示例:</p>
<p>默认的日志格式:</p>
<pre><code>[2020-04-01T11:33:43.317] [INFO] default - Server running on port 3000</code></pre>
<p>自定义的日志格式:</p>
<pre><code>2020-04-01 11:33:43.317 [INFO] Server running on port 3000</code></pre>
<p>代码:</p>
<pre><code>// 自定义日志格式
const layout = {
type: 'pattern',
pattern: '%d{yyyy-MM-dd hh:mm:ss.SSS} [%p] %m'
};
log4js.configure({
appenders: {
// 控制台输出
console: { type: 'console' },
// 日志文件,通过设置layout 设置日志格式
file: { type: 'file', filename: path.join(__dirname, '../../logs/server.log'), layout}
},
categories: {
// 默认日志
default: { appenders: [ 'file', 'console' ], level: 'debug' },
}
});</code></pre>
<h3>3.4 日志切割</h3>
<p>如果日志全部输出到一个文件,日志文件会越来越大,导致日志的备份和查看都很不方便。通过将appender 指定为 dateFile 类型可以实现按日期将日志进行切割。</p>
<pre><code>// 日志配置
log4js.configure({
appenders: {
// 控制台输出
console: { type: 'console' },
// 日志文件
file: {
type: 'dateFile',
filename: path.join(__dirname, '../../logs/server.log'),
// 日志切割后文件名后缀格式
pattern: '.yyyy-MM-dd'
}
},
categories: {
// 默认日志
default: { appenders: [ 'file', 'console' ], level: 'debug' },
}
});</code></pre>
<p>假如4月1日部署的服务,日志会输出到service.log文件,到4月2日会将service.log更名为server.log.2020-04-01,然后创建新的service.log文件,新的日志将继续输出到service.log文件。</p>
<h3>3.5 输出多个文件</h3>
<p>下面示例除了将完整日志输出到server.log,还会将error及以上级别的日志输出到server-error.log。</p>
<ul><li>util/log4jsLogger.js</li></ul>
<pre><code>const path = require('path');
const log4js = require('log4js');
// 配置log4js
log4js.configure({
appenders: {
// 控制台输出
console: { type: 'console' },
// 全部日志文件
allFile: { type: 'file', filename: path.join(__dirname, '../../logs/server.log')},
// 错误日志文件
errorFile: { type: 'file', filename: path.join(__dirname, '../../logs/server-error.log')}
},
categories: {
// 默认日志,输出debug 及以上级别的日志
default: { appenders: [ 'allFile', 'console' ], level: 'debug' },
// 错误日志,输出error 及以上级别的日志
error: { appenders: [ 'errorFile' ], level: 'error' },
}
});
// 获取默认日志
const defaultLogger = log4js.getLogger();
// 获取错误级别日志
const errorLogger = log4js.getLogger('error');
// 日志代理,同时调用默认日志和错误日志
const loggerProxy = {};
const levels = log4js.levels.levels;
levels.forEach(level => {
const curLevel = level.levelStr.toLowerCase();
loggerProxy[curLevel] = (...params) => {
defaultLogger[curLevel](...params);
errorLogger[curLevel](...params);
}
});
module.exports = loggerProxy;</code></pre>
<h3>3.6 覆盖console</h3>
<p>由于使用log4js需要调用logger.debug、logger.info、logger.warn、logger.error等方法,对于已经调用console 方法输出日志的项目,全部改为调用logger的方法,改动起来很麻烦,可以通过覆盖console的方法来使用log4js输出日志。</p>
<pre><code>/**
* 创建日志代理方法
* @param logLevel 日志级别
* @param logger 日志对象
* @return {function}
*/
function createLogProxy (logLevel, logger) {
return (...param) => {
logger[logLevel](...param);
};
}
console.log = createLogProxy('debug', logger);
console.info = createLogProxy('info', logger);
console.warn = createLogProxy('warn', logger);
console.error = createLogProxy('error', logger);</code></pre>
<p>为了保证所有日志都能输出到日志文件,获取logger 对象和覆盖console方法要尽早执行。</p>
<h2>四、总结</h2>
<p>本章介绍了log4js的基本使用,并给出了常用功能的使用示例,要了解log4js的更多功能,请参考log4js的官网:<a href="https://link.segmentfault.com/?enc=DZFuBPW1hRv8sKQW1bR1%2FQ%3D%3D.RwW697EQAZHrkIl67S75OJWDMN5XN%2B3cPct8Gs1RHC0CMeWT%2Bhpv8lgP8YoDuUwd" rel="nofollow">https://log4js-node.github.io...</a>。另外,本文相关的代码已提交到GitHub以供参考,项目地址:<a href="https://link.segmentfault.com/?enc=CHgIR052oZMLEDswb%2B%2BP%2FA%3D%3D.IpFrJ9%2Fsw0sMItgbBSYB3bAtVIl732YiXji4ygU14053Q3d6A4LYoUthEWp%2Fa26crbcdk9UOilLWbQzMywaJWA%3D%3D" rel="nofollow">https://github.com/liulinsp/n...</a>。</p>
浅谈技术管理之日式管理的殊途同归
https://segmentfault.com/a/1190000022394051
2020-04-16T16:16:57+08:00
2020-04-16T16:16:57+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>摘要:人们的现状是歪的,管理者为他塑造一个新图像,当对方将现状扶正,与新图像吻合,管理的目的就达成了。</p>
<p>《周易》说,形而上者谓之道,形而下者谓之器;降龙十八掌里有履霜坚冰,夕惕若厉等招数;坤卦爻辞中也有含章可贞,或从王事等管理和做人规则。</p>
<p>看完上面几句,大家可能会想,不是说日式管理嘛,怎么说起中国传统哲学了?其实无论是西方的还是日式的管理方法与经验,其理论来源都是中国的哲学思想,无论是德鲁克的任务、责任、实践的管理理论,波特的差异竞争论,哈默尔的核心竞争力,还是明茨伯格的战略和经理人角色,科特的领导与变革,归根到底这只不过是一些管理的方法和手段而已,这些手段和方法,在浩淼的中国传统哲学中都能找到与它们几乎一致的理论,可以说中国的哲学思想是世界管理学的源头活水。</p>
<p>说到日式管理,很多人也都耳熟能详。日式管理强调和谐的人际关系,上下协商的决策制度,员工对组织忠诚与组织对社会负责;日式管理的三大核心:终身雇佣制、年功序列制和企业工会制。</p>
<p>笔者1998~2010年期间主要在日本工作,对日式管理相对熟悉,这里咱们不谈这些理念性的东西,以自己的亲身经历和体验跟大家分享一下在日本期间的实际感受。</p>
<p>首先举两个具体事例。</p>
<h3>事例1:点测法</h3>
<p>某日本物流企业的货车运输部门,向总部申请追加购买运货卡车,理由是现有卡车数量不足,总部能够看到的数据是每个月的运送单数、重量、距离等,并不能反映出卡车到底够不够。于是总部连续两周,在每天的工作时间内随机选择2-3个时间点,统计在该时间点正在开动的卡车和没有开动的卡车数量,统计结果是有37%的卡车处于停止状态,于是总部以运输部门没有很好地统筹安排工作为理由,驳回了购买卡车的申请。</p>
<p>在实际工作中,我们经常碰到需要衡量评价又找不到合适的量化考核标准,或者由于考核的成本过高无法经常使用的情况,这时就可以看看能否从点测法中得到些提示。</p>
<p>通过统计一段时期内的上午11点和下午3点不在工位(或会议室)工作而正在休息的人的休息次数,可以大致得到下属中工作自觉性的数据;</p>
<p>通过几个连续的点测横向比较群体的数据,就能了解前后几个阶段总体工作相对的紧张度和松缓度,方便按照劳逸结合原则布置工作。</p>
<p>在测试的bug列表中,加入测出bug的人名,就可以大致看到测试组每个成员的测试效果;</p>
<p>把Bug分成几个等级的难易度,测出较多高难度bug的成员也代表相对较高的测试水平;</p>
<p>再加一列标出发生此bug的研发人员,可以根据bug发生的数量和等级大致得到研发人员的工作量、研发水平、责任心等信息。</p>
<p>多维度点测更容易得到正确的信息,单维度则容易出现偏差,比如单纯看研发人员发生bug数量,会分不清是干活多还是不够仔细。</p>
<h3>事例2:工具选择</h3>
<p>某日本矿场做提高生产效率的研究,把每个工人的日产量进行统计排序后收集数据,进一步观察其中工作成绩较好的员工,寻找他们产出高的原因,发现除了身体素质达标等因素以外,50%以上的高产员工用的是自备铁锹,铁锹的长短、重量、弧度等都跟使用者的身高、体力等有一定的比例关系,更有一部分人在铁锹容易磨手的地方做了防护,在容易打滑的部位做了防滑处理。于是,公司改变了以前铁锹的单一尺寸,根据长短不同设计了几个型号的铁锹,并且增加了防护防滑处理,每个工人可以选择合适自己的铁锹型号。这样,虽然生产工具成本提高了将近10%,但整体工作效率提高了近30%,矿场的改革增长了公司的效益。</p>
<p>在软件开发行业,我们看到有些员工会使用自己的电脑工作,这些私人电脑往往比公司电脑的配置要高,一方面可能是因为用着顺手,另一方面加快了效率提高了工作质量,也节约了自己的时间,例如电脑启动这一项,配SSD的用1分钟,机械盘有时5分钟都起不来。</p>
<p>如果说电脑配置的例子有点简单,那下面用逆向思维拓展一下。比如,不同的人适合的工作也是不同的。笔者有一次招聘了一批做对日项目的应届生,做的是野村证券的COBOL项目,经过严格的面试和3个月的系统培训,留下的人都还可以,有一名应届生虽然留下了,但bug率、排名都不靠前,一度被认为资质平常,后来有机会参与Java项目,学习速度和对项目整体的理解脱颖而出。我也很高兴又发现了一名后辈中的优秀人才,他只是不善于做COBOL这种简单但要求十分细致的开发,最终把他安排在了需要很强学习能力和快速出成果的区块链团队,他就是宜信区块链团队的研发主力王同学,对区块链整体把握和实现方式都达到了较高的水平。</p>
<p>除了工作和人的匹配,人与人的匹配也是一门学问,谁的业务水平虽高但不适合管人,谁和谁配合会有火雷噬嗑之象,谁出活快但不注意细节需要配备较好的测试,什么样的产品可以管技术什么样的不行等等,相信大家在日常工作中都有体会。</p>
<p>当然,任何方式方法都忌讳生搬硬套,一味地追求单项指标往往得不到最优性价比。我在日本有一次就比较郁闷,一个大型建材销售系统中的某个修改,涉及30多个模块,需10人用3个月完成。后来由于工期和资源的问题,要求我1人在1个月内完成,我虽然倒吸一口冷气,但还是全心投入工作了,紧张程度可想而知,堪比高考,如临大敌,过程中我惊奇地发现干满8小时居然能喘,为保证第二天的效率不能加班太多,最后终于在3个星期后完成,但上线后发现一个低级bug,有个地方把商品名字段错填成商品号了。这时日方管理者隐约表示出不满,并且怕我不够仔细所以没有让我参与数据修复的工作。别人加班时我没跟着加到太晚,还点了我一下说“别人都在为你修复数据,你没事也不能先走啊”。于是,我这三个星期连做梦都在思考工作的状态瞬间被浇灭了... 这个事件验证了一个管理要点,即做事先做人,管人先关心。</p>
<p>从上面的例子大家应该可以感觉到一件事,无论是正面的成功案例还是反面的瑕疵案例,日本优秀管理者的一大特点是勤快,他们愿意思考和尝试。从这点来讲,管理跟其他很多事情是一样的,勤能补拙,熟能生巧。我们在职场做事,很多时候获得成功的原因不是因为你有多聪明,而是因为你足够勤劳;不光要手脚勤快,更重要的是脑子勤快,如果再配上些其他特征,比如正直、果断、情商高等,这样的人本身就容易成功。勤快是管理的一大要素。</p>
<p>总的来讲,日本人在工作上鼓励高效、做事前有计划、经常同时使用双手做不同的事情、枚举所有的异常情况、忠诚、长期合作、遵守规则、尊重强者。这跟我们平时常说的团队凝聚力、员工稳定性、核心竞争力、匠心精神、长期领跑行业等,很多都是一个意思。</p>
<p>早在17世纪,西方管理学者就已意识到管理学离不开中国智慧,他们便回过头来,求助于东方智慧。西方管理学家所论述的管理的实质其实就是中国传统哲学讲的“内圣外王”的哲学之道。管理对于人而言无外乎就是:“管好自己和管好别人”,对于企业这个组织而言就是“能形成产业,对外成为某一领域的王者-引领者”,也就是要做到“内圣”和“外王”。所以对管理有兴趣的朋友也可以接触一下中国古代文化,两者有很多都是相通的。</p>
<p>理论结合实际是提升的必经之路,不管是日式的、西方的,还是中国传统的管理理念,我们能够肉眼所见、能够模仿到的都是规章规则等外在表现,真正的内涵和精髓还需要在日常的工作中不断理解体会、归纳和提炼,正所谓为学日增,为道日损。</p>
<p>希望本文所述能给大家带来点滴启发,谢谢!</p>
<blockquote>人们的现状是歪的,管理者为他塑造一个新图像,图像越清晰,对方的反差就越强烈,就会产生认知不和谐,产生不舒服的感觉,产生压力,进而产生动力,自动自发,自行负责。当对方将现状扶正,与新图像吻合,管理的目的就达成了。</blockquote>
<p>作者:胡晔</p>
TCP漫谈之keepalive和time_wait
https://segmentfault.com/a/1190000022291539
2020-04-07T18:03:38+08:00
2020-04-07T18:03:38+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p>TCP是一个有状态通讯协议,所谓的有状态是指通信过程中通信的双方各自维护连接的状态。</p>
<h2>一、TCP keepalive</h2>
<p>先简单回顾一下TCP连接建立和断开的整个过程。(这里主要考虑主流程,关于丢包、拥塞、窗口、失败重试等情况后面详细讨论。)</p>
<p>首先是客户端发送syn(Synchronize Sequence Numbers:同步序列编号)包给服务端,告诉服务端我要连接你,syn包里面主要携带了客户端的seq序列号;服务端回发一个syn+ack,其中syn包和客户端原理类似,只不过携带的是服务端的seq序列号,ack包则是确认客户端允许连接;最后客户端再次发送一个ack确认接收到服务端的syn包。这样客户端和服务端就可以建立连接了。整个流程称为三次握手。</p>
<p><img src="/img/remote/1460000022291543" alt="" title=""></p>
<p>建立连接后,客户端或者服务端便可以通过已建立的socket连接发送数据,对端接收数据后,便可以通过ack确认已经收到数据。</p>
<p>数据交换完毕后,通常是客户端便可以发送FIN包,告诉另一端我要断开了;另一端先通过ack确认收到FIN包,然后发送FIN包告诉客户端我也关闭了;最后客户端回应ack确认连接终止。整个流程成为四次挥手。</p>
<p>TCP的性能经常为大家所诟病,除了TCP+IP额外的header以外,它建立连接需要三次握手,关闭连接需要四次挥手。如果只是发送很少的数据,那么传输的有效数据是非常少的。</p>
<p>是不是建立一次连接后续可以继续复用呢?的确可以这样做,但这又带来另一个问题,如果连接一直不释放,端口被占满了咋办。为此引入了今天讨论的第一个话题TCP keepalive。所谓的TCP keepalive是指TCP连接建立后会通过keepalive的方式一直保持,不会在数据传输完成后立刻中断,而是通过keepalive机制检测连接状态。</p>
<p>Linux控制keepalive有三个参数:保活时间net.ipv4.tcp_keepalive_time、保活时间间隔net.ipv4.tcp_keepalive_intvl、保活探测次数net.ipv4.tcp_keepalive_probes,默认值分别是 7200 秒(2 小时)、75 秒和 9 次探测。如果使用 TCP 自身的 keep-Alive 机制,在 Linux 系统中,最少需要经过 2 小时 + 9*75 秒后断开。譬如我们SSH登录一台服务器后可以看到这个TCP的keepalive时间是2个小时,并且会在2个小时后发送探测包,确认对端是否处于连接状态。</p>
<p><img src="/img/remote/1460000022291542" alt="" title=""></p>
<p>之所以会讨论TCP的keepalive,是因为发现服器上有泄露的TCP连接:</p>
<pre><code># ll /proc/11516/fd/10
lrwx------ 1 root root 64 Jan 3 19:04 /proc/11516/fd/10 -> socket:[1241854730]
# date
Sun Jan 5 17:39:51 CST 2020</code></pre>
<p>已经建立连接两天,但是对方已经断开了(非正常断开)。由于使用了比较老的go(1.9之前版本有问题)导致连接没有释放。</p>
<p>解决这类问题,可以借助TCP的keepalive机制。新版go语言支持在建立连接的时候设置keepalive时间。首先查看网络包中建立TCP连接的DialContext方法中</p>
<pre><code>if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 {
setKeepAlive(tc.fd, true)
ka := d.KeepAlive
if d.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(tc.fd, ka)
testHookSetKeepAlive(ka)
}</code></pre>
<p>其中defaultTCPKeepAlive是15s。如果是HTTP连接,使用默认client,那么它会将keepalive时间设置成30s。</p>
<pre><code>var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}</code></pre>
<p>下面通过一个简单的demo测试一下,代码如下:</p>
<pre><code>func main() {
wg := &sync.WaitGroup{}
c := http.DefaultClient
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
r, err := c.Get("http://10.143.135.95:8080")
if err != nil {
fmt.Println(err)
return
}
_, err = ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
fmt.Println(err)
return
}
time.Sleep(30 * time.Millisecond)
}
}()
}
wg.Wait()
}</code></pre>
<p>执行程序后,可以查看连接。初始设置keepalive为30s。</p>
<p><img src="/img/remote/1460000022291545" alt="" title=""></p>
<p>然后不断递减,至0后,又会重新获取30s。</p>
<p><img src="/img/remote/1460000022291544" alt="" title=""></p>
<p>整个过程可以通过tcpdump抓包获取。</p>
<pre><code># tcpdump -i bond0 port 35832 -nvv -A</code></pre>
<p>其实很多应用并非是通过TCP的keepalive机制探活的,因为默认的两个多小时检查时间对于很多实时系统是完全没法满足的,通常的做法是通过应用层的定时监测,如PING-PONG机制(就像打乒乓球,一来一回),应用层每隔一段时间发送心跳包,如websocket的ping-pong。</p>
<h2>二、TCP Time_wait</h2>
<p>第二个希望和大家分享的话题是TCP的Time_wait状态。、</p>
<p><img src="/img/remote/1460000022291546" alt="" title=""></p>
<p>为啥需要time_wait状态呢?为啥不直接进入closed状态呢?直接进入closed状态能更快地释放资源给新的连接使用了,而不是还需要等待2MSL(Linux默认)时间。</p>
<p>有两个原因:</p>
<p>一是为了防止“迷路的数据包”,如下图所示,如果在第一个连接里第三个数据包由于底层网络故障延迟送达。等待新的连接建立后,这个迟到的数据包才到达,那么将会导致接收数据紊乱。</p>
<p><img src="/img/remote/1460000022291547" alt="" title=""></p>
<p>第二个原因则更加简单,如果因为最后一个ack丢失,那么对方将一直处于last ack状态,如果此时重新发起新的连接,对方将返回RST包拒绝请求,将会导致无法建立新连接。</p>
<p><img src="/img/remote/1460000022291549" alt="" title=""></p>
<p>为此设计了time_wait状态。在高并发情况下,如果能将time_wait的TCP复用,<br>time_wait复用是指可以将处于time_wait状态的连接重复利用起来。从time_wait转化为established,继续复用。Linux内核通过net.ipv4.tcp_tw_reuse参数控制是否开启time_wait状态复用。</p>
<p>读者可能很好奇,之前不是说time_wait设计之初是为了解决上面两个问题的吗?如果直接复用不是反而会导致上面两个问题出现吗?这里先介绍Linux默认开启的一个TCP时间戳策略net.ipv4.tcp_timestamps = 1。</p>
<p><img src="/img/remote/1460000022291548" alt="" title=""></p>
<p>时间戳开启后,针对第一个迷路数据包的问题,由于晚到数据包的时间戳过早会被直接丢弃,不会导致新连接数据包紊乱;针对第二个问题,开启reuse后,当对方处于last-ack状态时,发送syn包会返回FIN,ACK包,然后客户端发送RST让服务端关闭请求,从而客户端可以再次发送syn建立新的连接。</p>
<p>最后还需要提醒读者的是,Linux 4.1内核版本之前除了tcp_tw_reuse以外,还有一个参数tcp_tw_recycle,这个参数就是强制回收time_wait状态的连接,它会导致NAT环境丢包,所以不建议开启。</p>
<blockquote>作者:陈晓宇<p>作者著作《云计算那些事儿:从IaaS到PaaS进阶》</p>
</blockquote>
干货|科技赋能硬核直播带货,助力宜信业绩逆势增长
https://segmentfault.com/a/1190000022251020
2020-04-03T10:41:45+08:00
2020-04-03T10:41:45+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>摘要:介绍直播项目开发的平台架构、前台技术、数据中台和运维保障支持等实践。</p>
<p>新年伊始,新冠疫情令许多企业遭遇生存危机,而每次危机和逆境,也都会带来新的商业机会。宜信财富迎难而上,勇于尝试线上销售新“路数”——直播带货,实现业绩逆势增长。2020年2月,宜信财富实现规模业绩同比2019年2月提升24%、环比1月增长8%;新增客户数同比2019年2月增长82%、环比1月增长23%的好成绩。回顾过去一个月,宜信财富没有浪费这场危机,做到了“危”中觅“机”。</p>
<p>下面我们就带大家了解一下宜信财富逆势增加背后的技术秘密。本文主要从本次直播项目开发的平台架构、前台技术、数据中台和运维保障支持这几个方面做了简单的总结和分享,希望可以给大家带来一些思考和启发。</p>
<h2>第一部分:宜信财富直播平台架构演进之路</h2>
<p>在此次项目中,直播平台的架构演进主要是应用服务层的演进,包括以下三个阶段。</p>
<p>1、第一阶段:在项目初期,为了产品快速上线、快速验证,架构是比较简单直接的,就是“单活动架构”,如下图:</p>
<p><img src="/img/remote/1460000022251023" alt="" title=""></p>
<p>“单活动架构”快速实现了产品的核心功能,可以提供给种子用户快速验证,但随着验证反馈->快速迭代,一旦业务量上来并提供给大范围的用户使用,这种架构也就不再满足需求了,它的主要问题是:无运营管理能力,每次业务调整均需要由研发同事上线来解决。每举办一场直播活动都需要技术同学开发一次、上线一次。</p>
<p>2、第二阶段:随着直播活动的逐步推广,使用用户增多,直播频率也越来越高,“单活动架构”在应对业务量上涨方面已经越来越吃力。因此本次优化的思路就是解耦,让共性通用的业务字段可以进行配置管理。这个阶段在这里暂且称“简业务架构”(因为它只能覆盖业务范畴的一部分)。</p>
<p><img src="/img/remote/1460000022251025" alt="" title=""></p>
<p>“简业务架构”虽然在一定程度上解耦了基础服务与业务服务,但业务服务能力还是过于单一,不同场直播活动稍微的业务特殊变化,都会带来前端的定制化开发改造。</p>
<p>3、第三阶段:为了使“基础功能服务化,复用服务化能力”去进一步提升直播活动快速生成快速发布的运营效率,本次优化的思路就是“服务化”。根据之前的业务积累,构造了“直播模板孵化器”和“实现与已有运营配置中心打通”,让运营同事可以通过后台直接构建一场直播活动,并且构建成功后运营配置中心可直接生效。依据以往经验,一般一个业务平台技术发展到这个阶段,后续基本就可以依据产品的变化而迭代开发了。所以这个阶段我们可以称之为“服务化架构”。</p>
<p><img src="/img/remote/1460000022251024" alt="" title=""></p>
<p>“服务化架构”已大大提高了可靠性和可用性,但是随着服务的深入使用和在线直播业务带来的实质用户增长数据,以直播平台为中心衍射出来的与外部系统交互的需求会越来越多。因此,此时需要技术研发人员对自己辛苦搭建的“服务化架构”进行复盘回看。需要对服务进行拆分、治理,提升它的监控能力、降低它的运维成本、思考它对外开放的标准。下一个阶段是从“服务化架构” 向“微服务架构演进”。</p>
<p>整个架构三个阶段的演进在一个月之内完成,目前来说在业务表现和用户体验上看是成功的。不过从来没有一个完美的架构能够一直支撑业务的发展,架构是动态的、变化的,是随着业务的发展不断演进的,不同阶段需要不同成熟度的技术架构去支持业务 ,从服务升级和技术优化两个方面共同完善我们的产品。</p>
<h2>第二部分:宜信财富直播平台前端技术实现</h2>
<p>前端主要处理页面的逻辑、样式的控制,以及给服务端发送用户的操作数据,本次项目主要是将直播系统接入到宜信财富app,实现app端用户观看直播,从而进一步支持财富端业务。在这里我主要说一下我们在直播项目中主要的技术点和遇到的坑。</p>
<h3>1、带监测代码的二维码图片合成的技术实现</h3>
<p>获取到动态带员工id的二维码,通过css布局,使背景图中围绕二维码周围的文字图片对齐,再通过canvas的技术进行合成,合成中需要调试图片大小、清晰度,以及生成后二维码及文字的准确性。</p>
<h3>2、vue的双向绑定的特性在直播中的应用</h3>
<p>vue.js 是采用数据监听结合发布者-订阅者模式的方式,通过Object.defineProperty()来监听各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。我们来看Object.defineProperty()这个方法。</p>
<p><img src="/img/remote/1460000022251026" alt="" title=""></p>
<p>已经了解到vue是通过数据监听的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的监听,那么在设置或者获取的时候我们就可以在get或者set方法里,调用其他的触发函数,达到监听数据变动的目的。(有兴趣的小伙伴可以再深入研究,这里篇幅有限只做简单介绍)</p>
<p>在整个直播项目中前端有很多地方都应用到了vue的机制,比如用户评论留言的展示、对用户信息的存储判断,包括是否实名制、是否绑定理财师、是否做过风险测评等信息。这里通过留言评论展示简单说一下,评论展示前端代码。</p>
<p><img src="/img/remote/1460000022251027" alt="" title=""></p>
<p><img src="/img/remote/1460000022251028" alt="" title=""></p>
<p>通过vue指令 v-if来判断是否有留言,msgListObj为留言数据,通过接口获取,v-for指令来循环显示出留言,nameChange为自定义过滤器,将用户姓名以姓**的形式匿名展示,过滤器代码如下:</p>
<p><img src="/img/remote/1460000022251031" alt="" title=""></p>
<h3>3、埋点技术在直播上的一些新扩展(或解决的数据埋点问题)</h3>
<p>以往只在APP里打通了渠道,埋点服务可以获取到用户在APP端登录的id,然后通过id记录到客户行为,但是在APP外,页面行为没有关联到用户。此次主要在APP外,获取了用户id,并且关联到埋点服务。</p>
<h3>4、直播中遇到的一个问题</h3>
<p>首先描述一下整个问题的经过:</p>
<p>第一次出现的问题:在某一天的回放视频播放过程中,突然画面切换了,导致视频内容不正确,发现问题后第一时间排查。发现同时间有新版发布,但是发布版本中没有任何关于直播流内容的修改,通过回退了版本,暂时解决问题。</p>
<p>第二次出现的问题:发布版本修改的内容部分客户端生效,还有一部分不生效。结合上一次的问题,我们排查了前端代码、线上代码、git版本,都没有查出问题。最后发现,手动清理CDN缓存,就解决了不生效的问题。</p>
<blockquote>在这里简单介绍下什么是CDN。CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。</blockquote>
<p>定位到问题原因,主要是因为在直播使用的域名,为了缓解服务器缓存压力,采取了CDN缓存机制,缓存页面路径中的html、js、css等文件,缓存时间为2天,但是在频繁上线操作的情境下,CDN的缓存会有负面影响,就是由于缓存,用户看到的内容不是最新的内容,而是缓存的内容。</p>
<p>为了解决这个问题,我们在每次发布版本的时候都会针对现有的地址清理缓存,然后在时间允许的情况下,重新生成一个新的链接,确保没有缓存。针对每天直播任务的频繁上线发版,我们最后开发了一套后台直播配置系统,把直播中需要的各种资源都从后台接口获取,使直播真正成为一种模板形式,完全通过配置即可,不用再频繁上线发布版本,从根本上解决了CDN缓存的问题。</p>
<p>其实这期间还发生了很多很多的事情,也第一时间解决了很多的问题,很多事情可能不值一提,也有很多事情一发生就牵动好多人的心绪,不论过程如何,就目前看来整个的支持来看是成功的。</p>
<h2>第三部分:数据中台在直播项目中的实践</h2>
<p>宜信财富在财富管理行业直播带货的实践,获得开创性成功,目前已经举办30+场直播,并带来了巨大的收益,同比去年销量不降反而有提升!线上对比线下,有非常明显的优势,产品服务可以借助互联网得到更广泛的传播,传播力度也有极大的提升,专家可以同时服务更多的客户,是建立客户信任的最佳时机。</p>
<p>其中,精准营销、数据化运营是必不可少的环节,宜信财富数据中台也获得了绝佳的实践机会,主要包含以下四个方面:</p>
<ul>
<li>精细化运营,利用客户画像,精准触达客户,减少客户骚扰;</li>
<li>直播前、中实时看板,使管理决策心中有数;</li>
<li>直播后数据集市建设,实现全场景直播数据应用;</li>
<li>直播分析,对参与客户的人群画像、活跃度、转化率等方面进行分析,从而进一步优化第一方面的策略,形成数据运营闭环。</li>
</ul>
<h3>1、精细化运营</h3>
<p>借助精准运营工具,以及数据中台客户标签,实现客户精准人群定位和全渠道传播预热;决策者通过线下长期积累的客户需求经验,精准总结了各类客户人群,比如:全职太太、子女教育、家族传承学院等;</p>
<p><img src="/img/remote/1460000022251029" alt="" title=""></p>
<p>再根据客户渠道偏好、时间偏好、兴趣偏好等制定精准的运营策略,保证在最适合的时间、给有需求的客户营销正确的内容,从而提升客户体验和转化率。</p>
<h3>2、直播实时看板(数据中台ADX+Davinci)</h3>
<p>使用端到端竖管的模式,对接直播相关各业务系统的数据,包括神策行为埋点数据、直播预约数据、开户注册数据、交易打款等数据,借助数据中台的实时流处理工具,监测源系统数据库日志变化,在流上开发逻辑,实现端到端数据打通并形成看板,减少仓库层流程,提高数据使用效率,使数据及时可用。</p>
<p><img src="/img/remote/1460000022251030" alt="" title=""></p>
<p>(直播看板,来自网络截图)</p>
<h3>3、数据集市建设</h3>
<p>传统数仓分层结构,ODS、PDM、GDM、DM。</p>
<ul>
<li>PDM:主数据层,核心包括客户、事件、活动、资产等;</li>
<li>GDM:公共数据集市,客户属性变化表,客户交易状态切片表,客户指标标签表等;</li>
<li>DM:各层管理决策的多维报表、功能系统报表、数据科学家分析报表等。</li>
</ul>
<h3>4、客户标签体系</h3>
<p>客户标签体系就是基于大数据挖掘(如统计算法、机器学习算法、关系网络分析、LBS分析、文本分析等)技术,通过对企业级数据仓库以及信息源中的客户属性、特征和信息(行为指标、偏好、价值指标、心理指标等)进行加工和运算,所得到的客户标签化信息,例如“有房一族”、“有车一族”、“有娃一族”、“出国消费”、“高档小区”、“高端商场”等标识。</p>
<p>从宜信财富的标签体系中,根据直播数据,对参与客户的人群画像、活跃度、转化率等方面进行分析,发掘和识别出高价值潜力客户,进行存量客户提升,防止潜在客户流失,从而进一步指导和优化宜信财富的精细化运营形成数据运营闭环。</p>
<p>总结:在日常经营管理中,一线销售人员很难快速地对大量数据的处理得到复盘结果,也缺少识别高价值潜力客户的工具和手段,同时有关客户画像的认知和了解也处于初级阶段。因此,高效的智能化数据分析复盘十分有必要,此次数据中台赋能财富直播是一个很好的数字化实践案例,希望以上的内容可以为企业大数据战略布局及开展基于客户标签体系建设的数字化营销应用提供有益的借鉴。</p>
<h3>第四部分:直播项目运维保障</h3>
<p>在疫情期间所有的线下拓客都停滞的状况下,宜信财富上线新的功能:硬核直播带货,疫情期间平台上几乎每天都会有一场直播。截至目前,几乎每一场直播都得以顺利流畅地进行,背后的功臣,就是我们整个的直播运维保障团队。</p>
<p>运维作为业务发展的后腰团队,很少为用户所关注,但却是支撑产品基础系统稳定运转的重要力量。今天我们就和大家说说运维保障团队为直播产品和正常服务保驾护航都做了哪些方面的努力。</p>
<p>首先介绍一下直播的流程。通过下图做一个简要的介绍,首先看左侧两图的部分,在一台满足推流条件的电脑上安装OBS直播推流软件,把要展示给大家看的内容,专业术语称为“源”,如微信视频、PPT、图片、视频文件等等在OBS中调整好布局,形成场景,合成为可以在互联网传播的视频流,通过网络(此处指OBS电脑所在的网络,一般是家庭宽带和公司办公用网)上传到视频云服务器(VDN,在全国乃至全球有多个节点,实现终端就近连接视频服务器观看,减少跨国或是跨地区带来的网络不稳定、质量不高等问题),然后咱们的客户和同事就可以通过手机上的宜信财富APP、财富夜话小程序、星火金服APP进行观看啦。</p>
<p><img src="/img/remote/1460000022251032" alt="" title=""></p>
<p>上面的介绍听起来好像也挺简单的,别急,还有很多事情没有说呢:</p>
<p>观众对于直播卡顿的耐受度越来越低,尤其是对商业价值更高、观众期望值更高的企业直播来说,直播过程中如果出现页面打不开、延迟高缓冲长等各种突发小状况,都会成为影响企业直播营销效果的安全“大问题”。</p>
<p>其中,最容易影响直播稳定性的一大原因,就是直播发起后是否正确推流。我们的运维伙伴在直播期间稳坐“导播椅”,眼盯屏幕,耳听声音,寸步不离;期间要根据说话内容切换到PPT或是视频,还要及时切回来保证音视频和画面的切换正确;由于目前是远程办公,电脑和显示器的配置、网络带宽的速度和质量参差不齐,也会导致直播卡顿等,因此每次直播,运维的伙伴都会提前进行两次或以上的测试,参与直播或测试的人员来自于不同的国家或地区,对于开播前的准备和网络流畅度等,都需要协调时间、多轮沟通、多次测试,这些都是每场直播前的标配任务。另外导播工作所用的硬件,均是自有的,尤其是显示器,为了更好地帮助画面调整,也从普通的22寸变成了34寸的大屏显示。</p>
<p>为了给大家提供更好的视听体验,直播画面的展现也是很重要的一环,把不同的元素放在一个布局里,调整成满意的可以展现给大家看的画面,这样的调整每次都要多次沟通和微调。</p>
<p>说了这么多,给大家配几张图吧,可以更直观地了解下:</p>
<p><img src="/img/remote/1460000022251033" alt="" title=""></p>
<p><img src="/img/remote/1460000022251034" alt="" title=""></p>
<p><img src="/img/remote/1460000022251036" alt="" title=""></p>
<p>上面谈到的都是线上直播,线下也有直播,相比来说,线下有场地或舞台的直播,所用的设备更全、更专业,直播过程也更稳定。从下图就可以看到,参与直播保障的人力也会更多。</p>
<p><img src="/img/remote/1460000022251035" alt="" title=""></p>
<p>和咱们平常所看到的球赛或是大型晚会直播类似,只是摄像机多少的区别。(如果大家想了解更多可以私信小助手哦)。好了关于运维方面的内容我们就说到这里。</p>
<h2>总结</h2>
<p>事实上,数据中台等技术只是宜信数字化的一部分,作为在金融行业头部的企业,宜信拥有很多领先的金融技术和成熟的科技团队。以强大的技术实力和实践能力为基石,宜信技术团队为本次直播开发了稳定的平台并提供了全方位的运维保障服务,有力地保障了宜信财富在特殊时期通过直播实现财富业绩逆势增长。未来宜信技术团队也会不断地创新,通过更多场景化的应用,提升我们的科技能力,助力宜信通过数字化方式快速实现品牌提升和业绩增长。</p>
<p>由于“突发疫情”而催生出如此频繁的视频直播,但也开辟了财富端获客的“第二战场”,这也给大家一个深思的“机会”,线上获客、视频直播获客能否长久可行?而作为技术人,我们又要提前做些什么样的思考和行动?</p>
<blockquote>来源:宜信财富技术团队 <p>作者米志华,方建国,郭树源,谭文涛,孙李强,刘春颖,刘桥</p>
</blockquote>
干货|漫画算法:LRU从实现到应用层层剖析(第一讲)
https://segmentfault.com/a/1190000022224139
2020-04-01T10:11:37+08:00
2020-04-01T10:11:37+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>今天为大家分享很出名的LRU算法,第一讲共包括4节。</p>
<ul>
<li>LRU概述</li>
<li>LRU使用</li>
<li>LRU实现</li>
<li>Redis近LRU概述</li>
</ul>
<h2>第一部分:LRU概述</h2>
<p>LRU是Least Recently Used的缩写,译为最近最少使用。它的理论基础为“最近使用的数据会在未来一段时期内仍然被使用,已经很久没有使用的数据大概率在未来很长一段时间仍然不会被使用”由于该思想非常契合业务场景 ,并且可以解决很多实际开发中的问题,所以我们经常通过LRU的思想来作缓存,一般也将其称为LRU缓存机制。因为恰好leetcode上有这道题,所以我干脆把题目贴这里。但是对于LRU而言,希望大家不要局限于本题(大家不用担心学不会,我希望能做一个全网最简单的版本,希望可以坚持看下去!)下面,我们一起学习一下。</p>
<p>题目:运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get 和 写入数据 put 。</p>
<blockquote>获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。<p>写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。</p>
<p>进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?</p>
</blockquote>
<p>示例:</p>
<blockquote>LRUCache cache = new LRUCache( 2 /<em> 缓存容量 </em>/ );<p>cache.put(1, 1);</p>
<p>cache.put(2, 2);</p>
<p>cache.get(1); // 返回 1</p>
<p>cache.put(3, 3); // 该操作会使得密钥 2 作废</p>
<p>cache.get(2); // 返回 -1 (未找到)</p>
<p>cache.put(4, 4); // 该操作会使得密钥 1 作废</p>
<p>cache.get(1); // 返回 -1 (未找到)</p>
<p>cache.get(3); // 返回 3</p>
<p>cache.get(4); // 返回 4</p>
</blockquote>
<h2>第二部分:LRU使用</h2>
<p>首先说一下LRUCache的示例解释一下。</p>
<ul><li>第一步:我们申明一个LRUCache,长度为2</li></ul>
<p><img src="/img/remote/1460000022224142" alt="" title=""></p>
<ul><li>第二步:我们分别向cache里边put(1,1)和put(2,2),这里因为最近使用的是2(put也算作使用)所以2在前,1在后。</li></ul>
<p><img src="/img/remote/1460000022224145" alt="" title=""></p>
<ul><li>第三步:我们get(1),也就是我们使用了1,所以需要将1移到前面。</li></ul>
<p><img src="/img/remote/1460000022224143" alt="" title=""></p>
<ul><li>第四步:此时我们put(3,3),因为2是最近最少使用的,所以我们需要将2进行作废。此时我们再get(2),就会返回-1。</li></ul>
<p><img src="/img/remote/1460000022224144" alt="" title=""></p>
<ul><li>第五步:我们继续put(4,4),同理我们将1作废。此时如果get(1),也是返回-1。</li></ul>
<p><img src="/img/remote/1460000022224147" alt="" title=""></p>
<ul><li>第六步:此时我们get(3),实际为调整3的位置。</li></ul>
<p><img src="/img/remote/1460000022224146" alt="" title=""></p>
<ul><li>第七步:同理,get(4),继续调整4的位置。</li></ul>
<p><img src="/img/remote/1460000022224150" alt="" title=""></p>
<h2>第三部分:LRU 实现(层层剖析)</h2>
<blockquote>通过上面的分析大家应该都能理解LRU的使用了。现在我们聊一下实现。LRU一般来讲,我们是使用双向链表实现。这里我要强调的是,其实在项目中,并不绝对是这样。比如Redis源码里,LRU的淘汰策略,就没有使用双向链表,而是使用一种模拟链表的方式。因为Redis大多是当内存在用(我知道可以持久化),如果再在内存中去维护一个链表,就平添了一些复杂性,同时也会多耗掉一些内存,后面我会单独拉出来Redis的源码给大家分析,这里不细说。<p>回到题目,为什么我们要选择双向链表来实现呢?看看上面的使用步骤图,大家会发现,在整个LRUCache的使用中,我们需要频繁的去调整首尾元素的位置。而双向链表的结构,刚好满足这一点(再啰嗦一下,前几天我刚好看了groupcache的源码,里边就是用双向链表来做的LRU,当然它里边做了一些改进。groupcache是memcache作者实现的go版本,如果有go的读者,可以去看看源码,还是有一些收获。)</p>
<p>下面,我们采用hashmap+双向链表的方式进行实现。</p>
</blockquote>
<p>首先,我们定义一个LinkNode,用以存储元素。因为是双向链表,自然我们要定义pre和next。同时,我们需要存储下元素的key和value。val大家应该都能理解,关键是为什么需要存储key?举个例子,比如当整个cache的元素满了,此时我们需要删除map中的数据,需要通过LinkNode中的key来进行查询,否则无法获取到key。</p>
<pre><code>type LRUCache struct {
m map[int]*LinkNode
cap int
head, tail *LinkNode
}</code></pre>
<p>现在有了LinkNode,自然需要一个Cache来存储所有的Node。我们定义cap为cache的长度,m用来存储元素。head和tail作为Cache的首尾。</p>
<pre><code>type LRUCache struct {
m map[int]*LinkNode
cap int
head, tail *LinkNode
}</code></pre>
<p>接下来我们对整个Cache进行初始化。在初始化head和tail的时候将它们连接在一起。</p>
<pre><code>func Constructor(capacity int) LRUCache {
head := &LinkNode{0, 0, nil, nil}
tail := &LinkNode{0, 0, nil, nil}
head.next = tail
tail.pre = head
return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}</code></pre>
<p>大概是这样:</p>
<p><img src="/img/remote/1460000022224148" alt="" title=""></p>
<p>现在我们已经完成了Cache的构造,剩下的就是添加它的API了。因为Get比较简单,我们先完成Get方法。这里分两种情况考虑,如果没有找到元素,我们返回-1。如果元素存在,我们需要把这个元素移动到首位置上去。</p>
<pre><code> func (this *LRUCache) Get(key int) int {
head := this.head
cache := this.m
if v, exist := cache[key]; exist {
v.pre.next = v.next
v.next.pre = v.pre
v.next = head.next
head.next.pre = v
v.pre = head
head.next = v
return v.val
} else {
return -1
}
}</code></pre>
<p>大概就是下面这个样子(假若2是我们get的元素)</p>
<p><img src="/img/remote/1460000022224151" alt="" title=""></p>
<p>我们很容易想到这个方法后面还会用到,所以将其抽出。</p>
<pre><code>func (this *LRUCache) moveToHead(node *LinkNode){
head := this.head
//从当前位置删除
node.pre.next = node.next
node.next.pre = node.pre
//移动到首位置
node.next = head.next
head.next.pre = node
node.pre = head
head.next = node
}
func (this *LRUCache) Get(key int) int {
cache := this.m
if v, exist := cache[key]; exist {
this.moveToHead(v)
return v.val
} else {
return -1
}
}</code></pre>
<p>现在我们开始完成Put。实现Put时,有两种情况需要考虑。假若元素存在,其实相当于做一个Get操作,也是移动到最前面(但是需要注意的是,这里多了一个更新值的步骤)。</p>
<pre><code>func (this *LRUCache) Put(key int, value int) {
head := this.head
tail := this.tail
cache := this.m
//假若元素存在
if v, exist := cache[key]; exist {
//1.更新值
v.val = value
//2.移动到最前
this.moveToHead(v)
} else {
//TODO
}
}</code></pre>
<p>假若元素不存在,我们将其插入到元素首,并把该元素值放入到map中。</p>
<pre><code>func (this *LRUCache) Put(key int, value int) {
head := this.head
tail := this.tail
cache := this.m
//存在
if v, exist := cache[key]; exist {
//1.更新值
v.val = value
//2.移动到最前
this.moveToHead(v)
} else {
v := &LinkNode{key, value, nil, nil}
v.next = head.next
v.pre = head
head.next.pre = v
head.next = v
cache[key] = v
}
}</code></pre>
<p>但是我们漏掉了一种情况,如果恰好此时Cache中元素满了,需要删掉最后的元素。处理完毕,附上Put函数完整代码。</p>
<pre><code>func (this *LRUCache) Put(key int, value int) {
head := this.head
tail := this.tail
cache := this.m
//存在
if v, exist := cache[key]; exist {
//1.更新值
v.val = value
//2.移动到最前
this.moveToHead(v)
} else {
v := &LinkNode{key, value, nil, nil}
if len(cache) == this.cap {
//删除最后元素
delete(cache, tail.pre.key)
tail.pre.pre.next = tail
tail.pre = tail.pre.pre
}
v.next = head.next
v.pre = head
head.next.pre = v
head.next = v
cache[key] = v
}
}</code></pre>
<p><img src="/img/remote/1460000022224149" alt="" title=""></p>
<p>最后,我们完成所有代码:</p>
<pre><code>type LinkNode struct {
key, val int
pre, next *LinkNode
}
type LRUCache struct {
m map[int]*LinkNode
cap int
head, tail *LinkNode
}
func Constructor(capacity int) LRUCache {
head := &LinkNode{0, 0, nil, nil}
tail := &LinkNode{0, 0, nil, nil}
head.next = tail
tail.pre = head
return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}
func (this *LRUCache) Get(key int) int {
cache := this.m
if v, exist := cache[key]; exist {
this.moveToHead(v)
return v.val
} else {
return -1
}
}
func (this *LRUCache) moveToHead(node *LinkNode) {
head := this.head
//从当前位置删除
node.pre.next = node.next
node.next.pre = node.pre
//移动到首位置
node.next = head.next
head.next.pre = node
node.pre = head
head.next = node
}
func (this *LRUCache) Put(key int, value int) {
head := this.head
tail := this.tail
cache := this.m
//存在
if v, exist := cache[key]; exist {
//1.更新值
v.val = value
//2.移动到最前
this.moveToHead(v)
} else {
v := &LinkNode{key, value, nil, nil}
if len(cache) == this.cap {
//删除末尾元素
delete(cache, tail.pre.key)
tail.pre.pre.next = tail
tail.pre = tail.pre.pre
}
v.next = head.next
v.pre = head
head.next.pre = v
head.next = v
cache[key] = v
}
}</code></pre>
<p>优化后:</p>
<pre><code>type LinkNode struct {
key, val int
pre, next *LinkNode
}
type LRUCache struct {
m map[int]*LinkNode
cap int
head, tail *LinkNode
}
func Constructor(capacity int) LRUCache {
head := &LinkNode{0, 0, nil, nil}
tail := &LinkNode{0, 0, nil, nil}
head.next = tail
tail.pre = head
return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}
func (this *LRUCache) Get(key int) int {
cache := this.m
if v, exist := cache[key]; exist {
this.MoveToHead(v)
return v.val
} else {
return -1
}
}
func (this *LRUCache) RemoveNode(node *LinkNode) {
node.pre.next = node.next
node.next.pre = node.pre
}
func (this *LRUCache) AddNode(node *LinkNode) {
head := this.head
node.next = head.next
head.next.pre = node
node.pre = head
head.next = node
}
func (this *LRUCache) MoveToHead(node *LinkNode) {
this.RemoveNode(node)
this.AddNode(node)
}
func (this *LRUCache) Put(key int, value int) {
tail := this.tail
cache := this.m
if v, exist := cache[key]; exist {
v.val = value
this.MoveToHead(v)
} else {
v := &LinkNode{key, value, nil, nil}
if len(cache) == this.cap {
delete(cache, tail.pre.key)
this.RemoveNode(tail.pre)
}
this.AddNode(v)
cache[key] = v
}
}</code></pre>
<p><img src="/img/remote/1460000022224152" alt="" title=""></p>
<p>因为该算法过于重要,给一个Java版本的:</p>
<pre><code>//java版本
public class LRUCache {
class LinkedNode {
int key;
int value;
LinkedNode prev;
LinkedNode next;
}
private void addNode(LinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(LinkedNode node){
LinkedNode prev = node.prev;
LinkedNode next = node.next;
prev.next = next;
next.prev = prev;
}
private void moveToHead(LinkedNode node){
removeNode(node);
addNode(node);
}
private LinkedNode popTail() {
LinkedNode res = tail.prev;
removeNode(res);
return res;
}
private Hashtable<Integer, LinkedNode> cache = new Hashtable<Integer, LinkedNode>();
private int size;
private int capacity;
private LinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new LinkedNode();
tail = new LinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
LinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
LinkedNode node = cache.get(key);
if(node == null) {
LinkedNode newNode = new LinkedNode();
newNode.key = key;
newNode.value = value;
cache.put(key, newNode);
addNode(newNode);
++size;
if(size > capacity) {
LinkedNode tail = popTail();
cache.remove(tail.key);
--size;
}
} else {
node.value = value;
moveToHead(node);
}
}
}</code></pre>
<h2>第四部分:Redis 近LRU 介绍</h2>
<blockquote>上文完成了咱们自己的LRU实现,现在现在聊一聊Redis中的近似LRU。由于真实LRU需要过多的内存(在数据量比较大时),所以Redis是使用一种随机抽样的方式,来实现一个近似LRU的效果。说白了,LRU根本只是一个预测键访问顺序的模型。</blockquote>
<p>在Redis中有一个参数,叫做 “maxmemory-samples”,是干嘛用的呢?</p>
<pre><code># LRU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. For default Redis will check five keys and pick the one that was
# used less recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs a bit more CPU. 3 is very fast but not very accurate.
#
maxmemory-samples 5</code></pre>
<p>上面我们说过了,近似LRU是用随机抽样的方式来实现一个近似的LRU效果。这个参数其实就是作者提供了一种方式,可以让我们人为干预样本数大小,将其设的越大,就越接近真实LRU的效果,当然也就意味着越耗内存。(初始值为5是作者默认的最佳)</p>
<p><img src="/img/remote/1460000022224153" alt="" title=""></p>
<p>这个图解释一下,绿色的点是新增加的元素,深灰色的点是没有被删除的元素,浅灰色的是被删除的元素。最下面的这张图,是真实LRU的效果,第二张图是默认该参数为5的效果,可以看到浅灰色部分和真实的契合还是不错的。第一张图是将该参数设置为10的效果,已经基本接近真实LRU的效果了。</p>
<p>由于时间关系本文基本就说到这里。那Redis中的近似LRU是如何实现的呢?请关注下一期的内容~</p>
<blockquote>文章来源:本文由小浩算法授权转载</blockquote>
做好技术的量的累积,实现业绩的质的飞跃|专访宜信财富技术负责人刘宝剑
https://segmentfault.com/a/1190000022202374
2020-03-30T15:43:26+08:00
2020-03-30T15:43:26+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<blockquote>回顾中国IT行业发展历史,你会发现,在这个时代,我们见证着一切的变化,而今天的变化之快有时候也会让我们猝不及防。同时,随着大数据、云计算、人工智能的发展,也带来了更多的可能,对于国内金融科技行业而言,众多的可能正在进行,而这其中的参与者、创新者,也正在主动变化或适应变化。</blockquote>
<p>本期专访人物宜信财富技术负责人刘宝剑,一位拥有十几年开发经验的老兵,加入宜信6年有余,目前是宜信财富研发团队和数据团队负责人。作为宜信财富技术领军人之一,在时代的浪涛下,用技术驱动产品创新、强化数据应用和组织化服务能力,带领团队不断探索前行。</p>
<p>宜信财富是宜信公司面向高净值人群的服务板块,过去的十几年,为几十万高净值人群提供专业的金融服务。</p>
<p><strong>刘老师您好,今年的春节和往年大有不同,全国各地的疫情发展情况,使我们的工作受到一定的影响。身为宜信财富科技端负责人,您是怎样克服疫情的影响,协调团队工作的呢?面对疫情宜信财富采取了哪些应对措施?</strong></p>
<p>刘宝剑:同学您好,叫我宝哥就行。这次突发疫情确实给工作的开展带来了一些不便。但在宜信CTO向江旭先生的主导下,宜信科技中心开始为远程办公进行技术准备,包括测试网络架构、扩容服务器等等,确保所有业务均可实现远程正常作业受理。开工前两天,宜信财富与各职能部门共同设计了疫情期间的产品策略、工作机制以及激励方案,确保员工在远程办公的情况下可以积极有序地进入工作状态。</p>
<p>面对疫情线下业务必须向线上转型,我们将直播作为优先级的高需求,快速推进。目前宜信财富全体员工已经实现远程办公,线上业务体系实现员工在线、服务在线、业务在线、管理在线,以积极有效地触达客户、服务客户、满足客户需求。</p>
<p><strong>有外部消息称,疫情期间,宜信财富的业绩却逆势增长。可以说说其中的“秘密”吗?</strong></p>
<p>刘宝剑:确实,在疫情爆发期的2月,NTC同比增长87%,交易增加24%,惊艳了同行业。但是,从我的角度看,取得这种成果并不意外,这是一种量变引起质变的必然结果。</p>
<p>首先,直播是引起质变的介子。在线下拓客停滞状况下,硬核直播带货无疑是最好的方向,通过直播,产品和服务借助互联网得到了更广泛的传播,专家可以同时服务更多的客户,这是建立客户信任的最佳时机。</p>
<p>本次直播项目在架构方面,财富工程师在一个月内升级了三次,并且依旧在不断更新迭代;数据方面,精准营销数字化运营也在本次项目中获得了绝佳的实践机会......这些技术都不是临时开发的,而是通过长期的项目实践演进而来的。截至目前业绩上得到了不小的提升。正是这次的直播使我们多年的技术沉淀在此刻厚积薄发。</p>
<p>第二点我们说说宜信多年的科技投入。从数据中台到容器云再到宜信财富一站式智能营销平台——财富灯塔。</p>
<p>1、高效的端到端数据跟进流程——直播实时看板(数据中台ADX+Davinci)</p>
<p>面对直播时复杂数据分析带来的挑战,宜信财富采用了端到端竖管的模式,对接直播相关各业务系统的数据,包括神策行为埋点数据、直播预约数据、开户注册数据、交易打款等数据,借助数据中台的实时流处理工具,监测源系统数据库日志变化,在流上开发逻辑,实现端到端数据打通并形成看板,减少仓库层流程,提高数据使用效率,使数据及时可用,进一步指导宜信财富直播获客的精细化运营。</p>
<p>2、完善的基础容器云平台——宜信财富业务上云</p>
<p>这里不得不说的是,2月份的时候有一个财富爆品抢购,事先并没有预料到突发的流量,在此之前我们都是用传统方式扩容:要申请虚拟机,再安装、部署上线发布等等。财富端上云后实现一键扩容,面对此次突发的流量自动进行秒级扩容,当流量恢复时,容器云自动恢复,而这些自我修复的一系列操作,业务端都是毫无感知的。</p>
<p><img src="/img/bVbFj0R" alt="1.png" title="1.png"></p>
<p>这是整个平台的整体功能图,底层是硬件层面的计算、存储和网络;在物理层之上是资源管理层,负责容器、存储、域名等生命周期管理;在资源层之上是中间件层和应用层,负责将容器云的能力提供给最终的用户。两侧分别是监控告警和DevOps工作流。</p>
<p>宜信财富端上云大大提高了服务器的资源利用率,提高了资源响应速度,有力地保障了系统数据安全。</p>
<p>3、一站式智能营销平台——财富灯塔</p>
<p>这个平台的主要功能是将运营事件,包括内容、数据、通道等因素,自动化结合在一起,通过大数据指导,实现“实时、全自动、精准”的智能运营,为每一个客户定制其专属的运营策略,同时根据客户的偏好,精准匹配财富管理方案,实现数据支持营销策略优化。</p>
<p><img src="/img/bVbFj0X" alt="2.png" title="2.png"></p>
<p>在整个的直播和营销中,增长黑客就是不断地测试、改进、学习、调整并重复这个流程;然后对这些看似微不足道的改动作出评估,通盘衡量得失。</p>
<p>科技能力是核心能力,也是宜信区别其他同类机构的“秘密武器”之一!</p>
<p>其实以上说的这些都仅仅是整个业绩增长的其中几个方面,宜信财富一直都在用技术驱动产品创新,规划新技术落地到新的金融科技场景这是宜信财富一直都没有停歇的尝试。面对充满挑战的市场,宜信财富有机会在这里创造属于我们自己的方法论,真的很荣幸。</p>
<p><strong>宜信财富在整个疫情期间“危”中觅“机”举措的落地对您有哪些启发,您会对接下来的工作做出怎样的调整?</strong></p>
<p>刘宝剑:我们“危”中觅“机”,找寻到更为高效的和客户互动的方式。通过直播,宜信财富能够在微信、快手等各大流量生态体系广泛触达C端用户,更加精准地将信息传递给目标受众,降低了直播观看门槛,拉近了我们与用户的距离,极大的提高了获客与转化的效率。因此,可以肯定的是,未来原创视频信息将呈现几何级增长,视频传播将成为宜信财富数字化营销的重要渠道。</p>
<p>疫情也给我带来了很多思考。</p>
<p>首先,科技如何更好地赋能我们的业务。无论财富APP、理财师APP,都是我们很有利的武器,我们的客户,无论内部的,还是外部的,每天都在使用它们。我们需要以客户为中心,提供更为有效的流程体系,解决客户的“缺失感”。</p>
<p>其次,未知的应用场景会随时发生,这就需要更科学地推进高效传播,实现科技的发展。结合中国移动互联网的最新特点:5G、下沉、全场景、私域流量,其中 5G 作为新一代移动通信技术,已经与人工智能、大数据、云计算等技术融合创新。5G的快速发展以及逐渐普及,也许将会重新构建现有的商业形态。宜信财富需要建立更为高效的远程服务流程和体系,通过5G直播积极拥抱未来发展趋势,把握5G时代私域流量,通过社交直播、视频沟通等方式,实现宜信财富的重大发展。</p>
<p>最后,金融科技的本质还是金融,科技如何影响改变金融,是我们一直在思考的问题,比如针对不同客群的定制化服务,基于机器学习,更多的精准预测和跟踪,基于风险的管理… … 很多能力的建设,我们都在规划和落地中。</p>
<p><strong>特殊时期,您想对并肩作战的小伙伴们说些什么?</strong></p>
<p>刘宝剑:拥抱变化:突发的“变化”,考验了我们团队多年的技术积累和沉淀,是一次大阅兵,很高兴,取得了阶段性的成功。变化已成为常态,通过这次的实践,做好准备,拥抱变化,拥抱未来。</p>
<p>保持思考:疫情将客群引导到线上,但其实我们的客户服务是O2O方式,疫情过后我们的客户还会需要深入的线下服务体系,我们要从整体上看待和思考,科技赋能,无论线上还是线下,都是为了更好地服务我们的客群。</p>
<p>保持信念:宜信有一个531战略,这里特别要说的是5,企业每5年重塑一次。过去的5年我们从‘未来先见’到勾勒发展路径、建立核心能力,坚持本心,不断推动宜信财富数字化建设,有过困难,经受过考验,终于一步一个脚印走到今天。下一个5年我们仍然有着同样重要的战略发展方向,但我们的路径更清晰、核心能力更强大。相信面向未来的5年,宜信将更加快速推进数字化建设,夯实科技能力,通过数字化工具提升宜信的金融科技创新能力。</p>
<p>最后,希望大家可以从疫情中学习,在困难中成长,快速适应变化。谢谢。</p>
<blockquote>来源:宜信技术学院</blockquote>
宜信CTO向江旭:数字化是企业成为强者的底层代码
https://segmentfault.com/a/1190000022202324
2020-03-30T15:39:03+08:00
2020-03-30T15:39:03+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p>从2000年到现在,全球爆发了5次重大危机:2000年互联网泡沫、2001年9.11事件、2003年SARS病毒、2008年金融危机、2020年新冠疫情。危机给社会、企业和个人造成了巨大的经济损失,但同时也给优秀的公司创造机会,给适者生存的公司带来发展。数字化转型成为企业发展刚需,企业该如何在危机中寻找机会, 宜信CTO向江旭通过危机下宜信的探索和实践,以科技人的独特视角分享企业如何成功实现数字化转型。</p>
<h2>危中有机适者生存</h2>
<p>2000年,光纤和通信行业受到重创,而新一代互联网公司迅速崛起,如Google、Facebook。20001年,保险公司因为9.11事件赔偿大约400亿美元,但国防科技、制药行业的股价却受益上涨,远程办公和视频会议风靡一时。2003年SARS爆发,线下实体经济遭到重创,而电商公司却迅速发展。2008年,金融危机导致雷曼公司破产,与此同时风控工作却特别抢手,美国银行、富国银行等传统金融机构甚至投入百亿美元招募技术人才、打造数字化科技。</p>
<p>危机之下,不是那些平时看似强大的公司活下去了,而是适者生存的公司发展了。同时我们可以看到,金融和科技是一对孪生兄弟,危机首先反映在金融市场,而后这些企业产生了对科技的强烈需求。所以无论是金融公司还是非金融公司,在危机来临的时候,科技能力和数字化能力将是它生存的根基。正如宜信公司CEO唐宁所说:“科技不会取代你,取代你的是善用科技的人”。所以金融公司必须要有自己的科技团队和核心的科技能力,要善用科技赋能业务。</p>
<h2>协作建立创新生态圈</h2>
<p>在监管趋严、持牌经营、股市暴跌、经济震荡的情况下,整个金融行业已经到了转型的新阶段——以产品为中心转变为以客户为中心。</p>
<p>巨头的碾压效应和规模效应也在快速显现,互联网公司能够运用资源禀赋跨界获得金融牌照,然后通过用户、流量、规模和消费场景提供金融服务。而以银行、保险和证券为代表的传统金融公司,以宜信为代表的金融科技公司,正处于三期叠加的状态。在这种形势下,数字化转型和创新需要一个特殊的解决办法——创新生态圈。</p>
<p>无论哪一类公司做数字化转型,都意味着人才的竞争、用户的竞争、场景的竞争甚至文化的竞争。所以我们希望利用这些不同类型公司的优势互补,形成金融科技创新生态圈。传统金融公司核心的金融服务能力和强大的资金优势,结合金融科技公司的创新能力和科技能力,把对用户场景的捕捉和赋能及技术结合在一起,形成内部的核心资源、创新孵化和外部的合作、战略投资能力,从而建立一个端到端的创新生态圈,使得模式创新、业务创新和科技创新更加系统化、常态化、持续化。创新生态圈也使得行业里面不同类型的公司找到自己的角色和定位,使得整个行业的科技创新和数字化转型获得加速成功。</p>
<h2>科技赋能企业产能释放</h2>
<p>疫情之下,我们的经济生活在慢进,但数字化转型却在加速。以宜信为例,疫情爆发后,科技中心快速响应公司全员在家办公的决策,开动马力部署远程办公,服务器扩容、增加VPN设备、提升安全措施等等,短短几天内使得在线办公的能力从支持几千人提升到上万人。</p>
<p>在家办公的效率不仅没有下降,反而更高了,而且远程办公使得我们的办公室边界无限拓展,无论是职场,还是家里车里,甚至高铁和飞机上,都可以进行办公和展业。所以未来的两个趋势,一个是无论员工在什么时候、什么地点,都能一起协同办公,进行内部的运营和管理;一个是无论用户在什么时候、什么地点,都可以用数字化的方式连接。</p>
<p>今年2月份,通过科技赋能的方式宜信财富业务获得了业绩逆势增长。业绩同比去年大比例提升。所以科技不仅赋能业务创新,帮助企业数字化转型,而且还能挖掘潜力,助力产能释放。</p>
<h2>科技助力营销数字化</h2>
<p>企业的数字化转型,也推动着营销数字化并实现线上人人营销。首先通过对用户画像进行技术分析,例如投资预期、风险偏好、理财周期等,帮助理财师更懂客户,进而推荐更合适的理财产品和资产配置组合给到不同的需求的客户。</p>
<p>例如宜信理财师APP,通过客户经理转发的文章、图片、视频等,能够精准的捕捉到客户对文章的兴趣偏好、内容标签、阅读时长等信息,帮助理财师精准的洞察用户的需求,从而提高转化率。</p>
<p>其次是直播。这段时间,宜信财富做了很多场直播。通过优质的直播内容,不仅促成了用户转换、新客转化、老客再投,而且和客户之间建立了信任感,让客户感受到宜信的专业度。</p>
<p>科技让金融更普惠,数字化金融的本质就是让金融更好的服务更多人,让更多人享受到个性化、场景化的金融服务。对于金融科技的未来发展趋势,向江旭认为,满足客户真实需求的企业,就会有生存和发展的空间。对市场上灵活、创新、满足用户刚需的创新型企业和技术有信心。 危机使得这种需求提前和放大,谁能够满足客户的这种需求,谁就能在危机中胜出,甚至成为20%的越来越强的企业。金融的数字化能力是帮助企业渡过危机,成为强者的底层代码。</p>
深入理解MySQL索引
https://segmentfault.com/a/1190000022054544
2020-03-18T10:41:07+08:00
2020-03-18T10:41:07+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
34
<h2>前言</h2>
<p>当提到MySQL数据库的时候,我们的脑海里会想起几个关键字:索引、事务、数据库锁等等,索引是MySQL的灵魂,是平时进行查询时的利器,也是面试中的重中之重。</p>
<p>可能你了解索引的底层是b+树,会加快查询,也会在表中建立索引,但这是远远不够的,这里列举几个索引常见的面试题:</p>
<p>1、索引为什么要用b+树这种数据结构?</p>
<p>2、聚集索引和非聚集索引的区别?</p>
<p>3、索引什么时候会失效,最左匹配原则是什么?</p>
<p>当遇到这些问题的时候,可能会发现自己对索引还是一知半解,今天我们一起学习MySQL的索引。</p>
<h2>一、一条查询语句是如何执行的</h2>
<p>首先来看在MySQL数据库中,一条查询语句是如何执行的,索引出现在哪个环节,起到了什么作用。</p>
<h3>1.1 应用程序发现SQL到服务端</h3>
<p>当执行SQL语句时,应用程序会连接到相应的数据库服务器,然后服务器对SQL进行处理。</p>
<h3>1.2 查询缓存</h3>
<p>接着数据库服务器会先去查询是否有该SQL语句的缓存,key是查询的语句,value是查询的结果。如果你的查询能够直接命中,就会直接从缓存中拿出value来返回客户端。</p>
<p>注:查询不会被解析、不会生成执行计划、不会被执行。</p>
<h3>1.3 查询优化处理,生成执行计划</h3>
<p>如果没有命中缓存,则开始第三步。</p>
<ul>
<li>解析SQL:生成解析树,验证关键字如select,where,left join 等)是否正确。</li>
<li>预处理:进一步检查解析树是否合法,如检查数据表和列是否存在,验证用户权限等。</li>
<li>优化SQL:决定使用哪个索引,或者在多个表相关联的时候决定表的连接顺序。紧接着,将SQL语句转成执行计划。</li>
</ul>
<h3>1.4 将查询结果返回客户端</h3>
<p>最后,数据库服务器将查询结果返回给客户端。(如果查询可以缓存,MySQL也会将结果放到查询缓存中)</p>
<p><img src="/img/remote/1460000022054547" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>这就是一条查询语句的执行流程,可以看到索引出现在优化SQL的流程步骤中,接下来了解索引到底是什么?</p>
<h2>二、索引概述</h2>
<p>先简单地了解一下索引的基本概念。</p>
<h3>2.1 索引是什么</h3>
<p>索引是帮助数据库高效获取数据的数据结构。</p>
<h3>2.2 索引的分类</h3>
<h4>1)从存储结构上来划分</h4>
<ul>
<li>Btree索引(B+tree,B-tree)</li>
<li>哈希索引</li>
<li>full-index全文索引</li>
<li>RTree</li>
</ul>
<h4>2)从应用层次上来划分</h4>
<ul>
<li>普通索引:即一个索引只包含单个列,一个表可以有多个单列索引。</li>
<li>唯一索引:索引列的值必须唯一,但允许有空值。</li>
<li>复合索引:一个索引包含多个列。</li>
</ul>
<h4>3)从表记录的排列顺序和索引的排列顺序是否一致来划分</h4>
<ul>
<li>聚集索引:表记录的排列顺序和索引的排列顺序一致。</li>
<li>非聚集索引:表记录的排列顺序和索引的排列顺序不一致。</li>
</ul>
<h3>2.3 聚集索引和非聚集索引</h3>
<h4>1)简单概括</h4>
<ul>
<li>聚集索引:就是以主键创建的索引。</li>
<li>非聚集索引:就是以非主键创建的索引(也叫做二级索引)。</li>
</ul>
<h4>2)详细概括</h4>
<ul><li>聚集索引</li></ul>
<p>聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,因为只要找到第一个索引值记录,其余的连续性的记录在物理表中也会连续存放,一起就可以查询到。</p>
<p>缺点:新增比较慢,因为为了保证表中记录的物理顺序和索引顺序一致,在记录插入的时候,会对数据页重新排序。</p>
<ul><li>非聚集索引</li></ul>
<p>索引的逻辑顺序与磁盘上行的物理存储顺序不同,非聚集索引在叶子节点存储的是主键和索引列,当我们使用非聚集索引查询数据时,需要拿到叶子上的主键再去表中查到想要查找的数据。这个过程就是我们所说的回表。</p>
<h4>3)聚集索引和非聚集索引的区别</h4>
<ul>
<li>聚集索引在叶子节点存储的是表中的数据。</li>
<li>非聚集索引在叶子节点存储的是主键和索引列。</li>
</ul>
<h5>举个例子</h5>
<p>比如汉语字典,想要查「阿」字,只需要翻到字典前几页,a开头的位置,接着「啊」「爱」都会出来。也就是说,字典的正文部分本身就是一个目录,不需要再去查其他目录来找到需要找的内容。我们把这种正文内容本身就是一种按照一定规则排列的目录称为==聚集索引==。</p>
<p>如果遇到不认识的字,只能根据“偏旁部首”进行查找,然后根据这个字后的页码直接翻到某页来找到要找的字。但结合部首目录和检字表而查到的字的排序并不是真正的正文的排序方法。</p>
<p><img src="/img/remote/1460000022054550" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>比如要查“玉”字,我们可以看到在查部首之后的检字表中“玉”的页码是587页,然后是珏,是251页。很显然,在字典中这两个字并没有挨着,现在看到的连续的“玉、珏、莹”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到结果所对应的页码。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为==非聚集索引==。</p>
<h3>2.4 MySQL如何添加索引</h3>
<h4>1)添加PRIMARY KEY(主键索引)</h4>
<pre><code>ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )</code></pre>
<h4>2)添加UNIQUE(唯一索引)</h4>
<pre><code>ALTER TABLE `table_name` ADD UNIQUE (`column`)</code></pre>
<h4>3)添加INDEX(普通索引)</h4>
<pre><code>ALTER TABLE `table_name` ADD INDEX index_name (`column` )</code></pre>
<h4>4)添加FULLTEXT(全文索引)</h4>
<pre><code>ALTER TABLE `table_name` ADD FULLTEXT (`column`)</code></pre>
<h4>5)添加多列索引</h4>
<pre><code>ALTER TABLE `table_name` ADD INDEX index_name (`column1`,`column2`,`column3`)</code></pre>
<h2>三、索引底层数据结构</h2>
<p>了解了索引的基本概念后,可能最好奇的就是索引的底层是怎么实现的呢?为什么索引可以如此高效地进行数据的查找?如何设计数据结构可以满足我们的要求?<br>下文通过一般程序员的思维来想一下如果是我们来设计索引,要如何设计来达到索引的效果。</p>
<h3>3.1 哈希索引</h3>
<p>可能直接想到的就是用哈希表来实现快速查找,就像我们平时用的hashmap一样,value = get(key) O(1)时间复杂度一步到位,确实,哈希索引是一种方式。</p>
<h4>1)定义</h4>
<p>哈希索引就是采用一定的哈希算法,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。本质上就是把键值换算成新的哈希值,根据这个哈希值来定位。</p>
<p><img src="/img/remote/1460000022054551" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>2)局限性</h4>
<ul>
<li>哈希索引没办法利用索引完成排序。</li>
<li>不能进行多字段查询。</li>
<li>在有大量重复键值的情况下,哈希索引的效率也是极低的(出现哈希碰撞问题)。</li>
<li>不支持范围查询。</li>
</ul>
<p>在MySQL常用的InnoDB引擎中,还是使用B+树索引比较多。InnoDB是自适应哈希索引的(hash索引的创建由==InnoDB存储引擎自动优化创建==,我们干预不了)。</p>
<h3>3.2 如何设计索引的数据结构呢</h3>
<p>假设要查询某个区间的数据,我们只需要拿到区间的起始值,然后在树中进行查找。</p>
<p>如数据为:</p>
<p><img src="/img/remote/1460000022054549" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1)查询[7,30]区间的数据</h4>
<p><img src="/img/remote/1460000022054552" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p><img src="/img/remote/1460000022054548" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>当查找到起点节点10后,再顺着链表进行遍历,直到链表中的节点数据大于区间的终止值为止。所有遍历到的数据,就是符合区间值的所有数据。</p>
<h4>2)还可以怎么优化呢?</h4>
<p>利用二叉查找树,区间查询的功能已经实现了。但是,为了节省内存,我们只能把树存储在硬盘中。</p>
<p>那么,每个节点的读取或者访问,都对应一次硬盘IO操作。每次查询数据时磁盘IO操作的次数,也叫做==IO渐进复杂度==,也就是==树的高度==。</p>
<p>所以,我们要减少磁盘IO操作的次数,也就是要==降低树的高度==。</p>
<p>结构优化过程如下图所示:</p>
<p><img src="/img/remote/1460000022054554" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p><img src="/img/remote/1460000022054553" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p><img src="/img/remote/1460000022054556" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>这里将二叉树变为了M叉树,降低了树的高度,那么这个M应该选择多少才合适呢?</p>
<p><strong>问题:对于相同个数的数据构建m叉树索引,m叉树中的m越大,那树的高度就越小,那m叉树中的m是不是越大越好呢?到底多大才合适呢?</strong></p>
<p>不管是内存中的数据还是磁盘中的数据,操作系统都是按页(一页的大小通常是4kb,这个值可以通过<code>getconfig(PAGE_SIZE)</code>命令查看)来读取的,一次只会读取一页的数据。</p>
<p>如果要读取的数据量超过了一页的大小,就会触发多次IO操作。所以在选择m大小的时候,要尽量让每个节点的大小等于一个页的大小。</p>
<p>一般实际应用中,出度d(树的分叉数)是非常大的数字,通常超过100;==树的高度(h)非常小,通常不超过3==。</p>
<h3>3.3 B树</h3>
<p>顺着解决问题的思路知道了我们想要的数据结构是什么。目前索引常用的数据结构是B+树,先介绍一下什么是B树(也就是B-树)。</p>
<h4>1)B树的特点:</h4>
<ul>
<li>关键字分布在整棵树的所有节点。</li>
<li>任何一个关键字出现且只出现在一个节点中。</li>
<li>搜索有可能在非叶子节点结束。</li>
<li>其搜索性能等价于在关键字全集内做一次二分查找。</li>
</ul>
<p>如下图所示:</p>
<p><img src="/img/remote/1460000022054557" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h3>3.4 B+树</h3>
<p>了解了B树,再来看一下B+树,也是MySQL索引大部分情况所使用的数据结构。</p>
<p><img src="/img/remote/1460000022054555" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p><img src="/img/remote/1460000022054558" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1)B+树基本特点</h4>
<ul>
<li>非叶子节点的子树指针与关键字个数相同。</li>
<li>非叶子节点的子树指针P[i],指向关键字属于 [k[i],K[i+1])的子树(注意:区间是前闭后开)。</li>
<li>为所有叶子节点增加一个链指针。</li>
<li>所有关键字都在叶子节点出现。</li>
</ul>
<p>这些基本特点是为了满足以下的特性。</p>
<h4>2)B+树的特性</h4>
<ul>
<li>所有的关键字都出现在叶子节点的链表中,且链表中的关键字是有序的。</li>
<li>搜索只在叶子节点命中。</li>
<li>非叶子节点相当于是叶子节点的索引层,叶子节点是存储关键字数据的数据层。</li>
</ul>
<h4>3)相对B树,B+树做索引的优势</h4>
<ul>
<li>B+树的磁盘读写代价更低。B+树的内部没有指向关键字具体信息的指针,所以其内部节点相对B树更小,如果把所有关键字存放在同一块盘中,那么盘中所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相应的,IO读写次数就降低了。</li>
<li>树的查询效率更加稳定。B+树所有数据都存在于叶子节点,所有关键字查询的路径长度相同,每次数据的查询效率相当。而B树可能在非叶子节点就停止查找了,所以查询效率不够稳定。</li>
<li>B+树只需要去遍历叶子节点就可以实现整棵树的遍历。</li>
</ul>
<h3>3.5 MongoDB的索引为什么选择B树,而MySQL的索引是B+树?</h3>
<p>因为MongoDB不是传统的关系型数据库,而是以Json格式作为存储的NoSQL非关系型数据库,目的就是高性能、高可用、易扩展。摆脱了关系模型,所以范围查询和遍历查询的需求就没那么强烈了。</p>
<h3>3.6 MyISAM存储引擎和InnoDB的索引有什么区别</h3>
<h4>1)MyISAM存储引擎</h4>
<p><img src="/img/remote/1460000022054559" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<ul><li>主键索引</li></ul>
<p>MyISAM的索引文件(.MYI)和数据文件(.MYD)文件是分离的,索引文件仅保存记录所在页的指针(物理位置),通过这些指针来读取页,进而读取被索引的行。</p>
<p>树中的叶子节点保存的是对应行的物理位置。通过该值,==存储引擎能顺利地进行回表查询,得到一行完整记录==。</p>
<p>同时,每个叶子也保存了指向下一个叶子的指针,从而方便叶子节点的范围遍历。</p>
<ul><li>辅助索引</li></ul>
<p>在MyISAM中,主键索引和辅助索引在结构上没有任何区别,==<strong>只是主键索引要求key是唯一的,而辅助索引的key可以重复</strong>==。</p>
<h4>1)Innodb存储引擎</h4>
<p>Innodb的主键索引和辅助索引之前提到过,再回顾一次。</p>
<ul><li>主键索引</li></ul>
<p><img src="/img/remote/1460000022054560" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>InnoDB主键索引中既存储了主健值,又存储了行数据。</p>
<ul><li>辅助索引</li></ul>
<p><img src="/img/remote/1460000022054562" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>对于辅助索引,InnoDB采用的方式是在叶子节点中保存主键值,通过这个主键值来回表查询到一条完整记录,因此按辅助索引检索其实进行了二次查询,效率是没有主键索引高的。</p>
<h2>四、MySQL索引失效</h2>
<p>在上一节中了解了索引的多种数据结构,以及B树和B+树的对比等,大家应该对索引的底层实现有了初步的了解。这一节从应用层的角度出发,看一下如何建索引更能满足我们的需求,以及MySQL索引什么时候会失效的问题。</p>
<p>先来思考一个小问题。</p>
<p><strong>问题:当查询条件为2个及2个以上时,是创建多个单列索引还是创建一个联合索引好呢?它们之间的区别是什么?哪个效率高呢?</strong></p>
<p>先来建立一些单列索引进行测试:</p>
<p><img src="/img/remote/1460000022054561" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>这里建立了一张表,里面建立了三个单列索引userId,mobile,billMonth。</p>
<p>然后进行多列查询。</p>
<pre><code>explain select * from `t_mobilesms_11` where userid = '1' and mobile = '13504679876' and billMonth = '1998-03'</code></pre>
<p><img src="/img/remote/1460000022054564" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>我们发现查询时只用到了userid这一个单列索引,这是为什么呢?因为这取决于MySQL优化器的优化策略。</p>
<p><strong>当多条件联合查询时,优化器会评估哪个条件的索引效率高,它会选择最佳的索引去使用。也就是说,此处三个索引列都可能被用到,只不过优化器判断只需要使用userid这一个索引就能完成本次查询,故最终explain展示的key为userid。</strong></p>
<h3>4.1 总结</h3>
<p>多个单列索引在多条件查询时优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引都用上。</p>
<p>但是多个单列索引底层会建立多个B+索引树,比较占用空间,也会浪费搜索效率<br>所以多条件联合查询时最好建联合索引。</p>
<p>那联合索引就可以三个条件都用到了吗?会出现索引失效的问题吗?</p>
<h3>4.2 联合索引失效问题</h3>
<p>该部分参考并引用文章:</p>
<p><a href="https://segmentfault.com/a/1190000021464570">一张图搞懂MySQL的索引失效</a></p>
<p>创建user表,然后建立 name, age, pos, phone 四个字段的联合索引<br>全值匹配(索引最佳)。</p>
<p><img src="/img/remote/1460000022054563" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>索引生效,这是最佳的查询。</p>
<p><strong>那么时候会失效呢?</strong></p>
<h4>1)违反最左匹配原则</h4>
<p>最左匹配原则:最左优先,以最左边的为起点任何连续的索引都能匹配上,如不连续,则匹配不上。</p>
<p>如:建立索引为(a,b)的联合索引,那么只查 where b = 2 则不生效。换句话说:如果建立的索引是(a,b,c),也只有(a),(a,b),(a,b,c)三种查询可以生效。</p>
<p><img src="/img/remote/1460000022054569" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>这里跳过了最左的name字段进行查询,发现索引失效了。</p>
<p>遇到范围查询(>、<、between、like)就会停止匹配。</p>
<p>比如:a= 1 and b = 2 and c>3 and d =4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,因为c字段是一个范围查询,它之后的字段会停止匹配。</p>
<h4>2)在索引列上做任何操作</h4>
<p>如计算、函数、(手动或自动)类型转换等操作,会导致索引失效而进行全表扫描。</p>
<pre><code>explain select * from user where left(name,3) = 'zhangsan' and age =20</code></pre>
<p><img src="/img/remote/1460000022054568" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>这里对name字段进行了left函数操作,导致索引失效。</p>
<h4>3)使用不等于(!= 、<>)</h4>
<pre><code>explain select * from user where age != 20;</code></pre>
<p><img src="/img/remote/1460000022054565" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<pre><code>explain select * from user where age <> 20;</code></pre>
<p><img src="/img/remote/1460000022054566" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>4)like中以通配符开头('%abc')</h4>
<p>索引失效</p>
<pre><code>explain select * from user where name like ‘%zhangsan’; </code></pre>
<p><img src="/img/remote/1460000022054570" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>索引生效</p>
<pre><code>explain select * from user where name like ‘zhangsan%’; </code></pre>
<p><img src="/img/remote/1460000022054567" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>5)字符串不加单引号索引失效</h4>
<pre><code>explain select * from user where name = 2000;</code></pre>
<p><img src="/img/remote/1460000022054571" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>6)or连接索引失效</h4>
<pre><code>explain select * from user where name = ‘2000’ or age = 20 or pos =‘cxy’; </code></pre>
<p><img src="/img/remote/1460000022054572" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>7)order by</h4>
<p>正常(索引参与了排序),没有违反最左匹配原则。</p>
<pre><code>explain select * from user where name = 'zhangsan' and age = 20 order by age,pos;</code></pre>
<p><img src="/img/remote/1460000022054574" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>违反最左前缀法则,导致额外的文件排序(会降低性能)。</p>
<pre><code>explain select name,age from user where name = 'zhangsan' order by pos;</code></pre>
<p><img src="/img/remote/1460000022054573" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>8)group by</h4>
<p>正常(索引参与了排序)。</p>
<pre><code>explain select name,age from user where name = 'zhangsan' group by age;</code></pre>
<p>违反最左前缀法则,导致产生临时表(会降低性能)。</p>
<pre><code>explain select name,age from user where name = 'zhangsan' group by pos,age;</code></pre>
<p><img src="/img/remote/1460000022054575" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h2>五、总结</h2>
<ul>
<li>了解一条查询语句是如何执行的,发现建立索引是一种可以高效查找的数据结构。</li>
<li>了解了索引的各种分类情况,聚集索引和非聚集索引的区别,如何创建各种索引。</li>
<li>通过需求一步步分析出为什么MySQL要选b+tree作为索引的数据结构,对比了btree和b+tree的区别、 MyISAM和innodb中索引的区别。</li>
<li>了解了索引会失效的多种情况,比较重要的最左匹配原则,相应地我们可以在建索引的时候做一些优化。</li>
</ul>
<p>希望大家能够多去使用索引进行SQL优化,有问题欢迎指出。</p>
<blockquote>来源:宜信技术学院<p>作者:杨亨</p>
</blockquote>
程序的一生:从源程序到进程的辛苦历程
https://segmentfault.com/a/1190000022040651
2020-03-17T10:10:45+08:00
2020-03-17T10:10:45+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
22
<p>摘要:一个程序的一生,从源程序到进程的辛苦历程!本文不深入研究编译原理、操作系统原理,主要聚焦于程序的加载和链接。</p>
<hr>
<h2>一、前言</h2>
<p>作为计算机专业的人,最遗憾的就是在学习编译原理的那个学期被别的老师拉去干活了,而对一个程序怎么就从源代码变成了一个在内存里活灵活现的进程,一直也心怀好奇。这种好奇驱使我要找个机会深入了解一下,所以便有了本文,来督促自己深入研究程序的一生。不过,<strong>本文没有深入研究编译原理、操作系统原理,而是主要聚焦于程序的链接和加载。</strong></p>
<p>学习的过程中主要参考了三本书、一个视频、一个音频(文末有列出),三本书里,最主要的还是<strong>《程序员的自我修养 - 链接、装载与库》</strong>,里面的代码放到了<a href="https://link.segmentfault.com/?enc=3s7pNK4ONQanMN6q5QosYA%3D%3D.RV2Kc2HQfYQg%2Bf9%2FYRotjLOL7aoEB8uHTEudlmVuYUo8BvqeN0TCe5di02CcSx7b7niO9pjEmnLodbWOMjOKhw%3D%3D" rel="nofollow">我的github</a>上,并且配有shell脚本和说明,运行后可以实操理解到更多内容。</p>
<p><strong>南大袁春风老师的计算机原理讲解</strong>对我帮助最大,视频是最直接传达知识的方式。另外,为了方便自己的实验,制作了一个ubuntu的环境,并且内置了代码,方便实验:阿里docker镜像</p>
<p><code>docker pull registry.cn-hangzhou.aliyuncs.com/piginzoo/learn:1.0</code></p>
<h2>二、概述</h2>
<p>每天都有无数的程序被编译、部署,不停地跑着,它们干着千奇百怪的事情。如同这个光怪陆离的世界,是由每个人、每个个体组成的,如果我们剖析每个人,会发现他们其实都是一样的结构,都是由细胞、组织组成,再深究便是基因了,DNA里那一个个的“核苷酸基”决定了他们。</p>
<p>同样,通过这个隐喻来认知计算机,我们可以知道,<strong>计算机的基因和本质就是冯诺依曼体系</strong>。啥是冯诺依曼体系呢?通俗地讲,就是定义了整个硬件体系(CPU、外存、输入输出),以及执行的运行流程等等。可是,<strong>一个程序怎么就与硬件亲密无间地运行起来了呢?</strong>应该很多人都不了解,甚至包括许多计算机专业的同学们。</p>
<p>本质上来说,这个过程其实就是<strong>“从代码编译,然后不同目标文件链接,最终加载到内存中,被操作系统管理起来的一个进程,可能还会动态地再去链接其他的一些程序(如动态链接库)的过程”</strong>。看起来似乎很简单,但其实每个部分都隐藏着很多细节,好奇心很强的你一定想知道,到底计算机是怎么做到的。</p>
<p>本文不打算讨论硬件、进程、网络等如此庞大的体系,只聚焦于探索程序的链接和加载这两个主题。</p>
<h2>三、基础</h2>
<p>探索之前需要交代一些基础知识,不然无法理解链接和加载。</p>
<h3>3.1 硬件基础</h3>
<h4>3.1.1 CPU</h4>
<p><img src="/img/bVbEDT3" alt="1.jpg" title="1.jpg"></p>
<p><strong>CPU由一大堆寄存器、算数逻辑单元(就是做运算的)、控制器组成。</strong>每次通过PC(程序计数器,存着指令地址)寄存器去内存里寻址可执行二进制代码,然后加载到指令寄存器里,如果涉及到地址的话,再去内存里加载数据,计算完后写回到内存里。每条指令都会放到指令寄存器(IR)中,等着CPU去取出来运行。</p>
<p>指令是从硬盘加载到内存里,又从内存里加载到IR里面的。指令运行过程中需要一些数据,这又要求从内存里取出一些数据放到通用寄存器中,然后交给ALU去运算,结果出来后又会放到寄存器或者内存中,周而复始。</p>
<p><strong>每一步都是一个时钟周期,</strong>现在的CPU一秒钟可以做1G次,是1000000000,几十亿次/秒。目前市场上的CPU主频据说到4GHz就到极限了,限于工艺,上不去了,所以慢慢转为多核,就是把几个CPU封装到一起共享内部缓存。</p>
<h4>3.1.2 主板</h4>
<p><img src="/img/bVbEDT4" alt="2.jpg" title="2.jpg"></p>
<p>如图,我们经常听说的“北桥、南桥”是什么?</p>
<p><strong>北桥</strong>其实就是一个计算机结构,准确地说是一个芯片,它连接的都是高速设备,通过PCI总线,把cpu、内存、显卡串在一起;而<strong>南桥</strong>就要慢很多了,连接的都是鼠标、键盘、硬盘等这些“穷慢”亲戚,它们之间用ISA总线串在一起。</p>
<h4>3.1.3 硬盘</h4>
<p>硬盘硬件上是盘片、磁道、扇区这样的一个结构,太复杂了,所以从头到尾给这些扇区编个号,就是所谓的<strong>“LBA(Logical Block Address)”逻辑扇区</strong>的概念,方便寻址。</p>
<p><strong>为了隔离,每个进程有一个自己的虚拟地址空间,然后想办法给它映射到物理内存里。</strong>如果内存不够怎么办?就想到了再细分,就是<strong>分页</strong>,分成4k的一个小页,常用的在内存里,不常用的交换到磁盘上。这就要经常用到<strong>地址映射计算(从虚拟地址到物理地址)</strong>,这个工作就是<strong>MMU(Memory Management Unit)</strong>,为了快都集成到CPU里面了。</p>
<h4>3.1.4 输入输出设备</h4>
<p>还有很多外设负责输入输出,一旦被外界输入或要输出东西,就得去告诉CPU:“我有东西了,来取吧”;“我要输出啦,来帮我输出吧”。这些工作就要靠一个叫<strong>“中断”</strong>的机制,可以将“中断”理解成一种消息机制,用于通知CPU来帮我干活。不是每个部分都可以直接骚扰CPU的,它们都要通过<strong>中断控制器</strong>来集中骚扰CPU。</p>
<p>这些外设都有自己的buffer,这些buffer也得有地址,这个地址叫<strong>端口</strong>。</p>
<p><img src="/img/bVbEDT6" alt="3.jpg" title="3.jpg"></p>
<p>还得给每个设备编个号,这样系统才能识别谁是谁。每次中断,CPU一看,噢,原来是05,05是键盘啊;06,06是鼠标啊。这个号,叫<strong>中断编号(IRQ)</strong>。</p>
<p>每次都必须要骚扰CPU吗?直接把数据从外设的buffer(端口)灌到内存里,不用CPU参与,多好啊!对,这个做法就是<strong>DMA</strong>。每个DMA设备也得编个号,这个编号就是<strong>DMA通道</strong>,这些号可不能冲突哦。</p>
<p><img src="/img/bVbEDT9" alt="4.jpg" title="4.jpg"></p>
<h3>3.2 汇编基础</h3>
<p>对于汇编,我其实也忘光了,所以得补补汇编知识了,起码要能读懂一些基础的汇编指令。</p>
<h4>3.2.1 汇编语法</h4>
<p>汇编分门派呢!<strong>”AT&T语法” vs “Intel语法”</strong>:GUN GCC使用传统的AT&T语法,它在Unix-like操作系统上使用,而不是dos和windows系统上通常使用的Intel语法。</p>
<p><strong>最常见的AT&T语法的指令:movl、%esp、%ebp</strong>。movl是一个最常见的汇编指令的名称,百分号表示esp和ebp是寄存器。在AT&T语法中,有两个参数的时候,始终先给出源<code>source</code>,然后再给出目标<code>destination</code>。</p>
<p>AT&T语法:</p>
<p><code><指令> [源] [目标]</code></p>
<h4>3.2.2 寄存器</h4>
<p><strong>寄存器是存放各种给cpu计算用的地址、数据用的,可以认为是为CPU计算准备数据用的。</strong>一般分为8类:</p>
<p><img src="/img/bVbEDUt" alt="5.png" title="5.png"></p>
<p><strong>命名上,x86一般是指32位;x86-64一般是指64位。</strong>32位寄存器,一般都是e开头,如eax、ebx;64位寄存器约定以r开头,如rax、rbx。</p>
<p><strong>1)32位寄存器</strong></p>
<p>32位CPU一共有8个寄存器。</p>
<p><img src="/img/bVbEDUK" alt="6.png" title="6.png"></p>
<p>详细的介绍:</p>
<p><img src="/img/bVbEDUP" alt="7.png" title="7.png"></p>
<p><strong>2)64位寄存器有:32个</strong></p>
<p><img src="/img/bVbEDUY" alt="8.png" title="8.png"></p>
<p><strong>两者的区别:</strong></p>
<ul>
<li>64位有16个寄存器,32位只有8个。但32位前8个都有不同的命名,分别是e _ ,而64位前8个使用了r代替e,也就是r 。e开头的寄存器命名依然可以直接运用于相应寄存器的低32位。而剩下的寄存器名则是从r8 - r15,其低位分别用d,w,b指定长度。</li>
<li>32位寄存器使用栈帧作为传递参数的保存位置,而64位寄存器分别用rdi、rsi、rdx、rcx、r8、r9作为第1-6个参数,rax作为返回值。</li>
<li>32位寄存器用ebp作为栈帧指针,64位寄存器取消了这个设定,没有栈帧的指针,rbp作为通用寄存器使用。</li>
<li>64位寄存器支持一些形式以PC相关的寻址,而32位只有在jmp的时候才会用到这种寻址方式。</li>
</ul>
<p>对了,<strong>寄存器可不是L1、L2 cache啊!</strong>Cache位于CPU与主内存间,分为一级Cache (L1Cache)和二级Cache (L2Cache),L1 Cache集成在CPU内部,L2 Cache早期在主板上,现在也都集成在CPU内部了,常见的容量有256KB或512KB。寄存器很少的,拿64位的来说,也就是16个,64x16,也就是1024,1K。</p>
<p><strong>总结:</strong>大致来说数据是通过内存-Cache-寄存器,Cache缓存是为了弥补CPU与内存之间运算速度的差异而设置的部件。</p>
<h4>3.2.3 寻址方式</h4>
<p>接下来说说寻址,<strong>寻址就是告诉CPU去哪里取指令、数据。</strong>比如<code>movl %rax %rbx</code>,这个涉及到寻址,寻址会寻“寄存器”、“内存”,可以是暴力的直接寻址,也可以是委婉的间接寻址。下面是各种寻址方式:</p>
<p><img src="/img/bVbEDUZ" alt="9.png" title="9.png"></p>
<p>你可能会看到这种指令<code>movl,movw,mov</code>后面的l、w是什么鬼?</p>
<p><img src="/img/bVbEDU1" alt="10.png" title="10.png"></p>
<p>就是一次搬运的数据数量。</p>
<h4>3.2.4 常用的指令</h4>
<p>最后说说指令本身,<strong>每个CPU类型都有自己的指令集,就是告诉CPU干啥,比如加、减、移动、调用函数等。</strong>下面是一些非常常用的指令:</p>
<p><img src="/img/bVbEDU6" alt="11.png" title="11.png"></p>
<p>参考:愿意自虐的同学,可以下载<a href="https://link.segmentfault.com/?enc=2HY6pFKlw6eJmTGfR6l4ng%3D%3D.CILXaaY8ETsvzBUb%2FmyzduATLM6R7QLb1j8EJ%2F7mIl4hF%2BsNf552HNQxC272s6pMMrMgkacTQwZcaIt5j%2Bl0tw%3D%3D" rel="nofollow">【Intel官方的指令集手册】</a>仔细研读。</p>
<h3>3.3 一些工具和玩法</h3>
<p>本文还会涉及到<strong>一些工具:</strong></p>
<ul>
<li>gcc:超级编译工具,可以做预编译、编译成汇编代码、静态链接、动态链接等,本质上是各种编译过程工具的一个封装器。</li>
<li>gdb:太强了,命令行的调试工具,简直是上天入地的利器。</li>
<li>readelf:可以把一个可执行文件、目标文件完全展示出来,让你观瞧。</li>
<li>objdump:跟readelf功能差不多,不过貌似它依赖一个叫“bfd库”的玩意儿,我也没研究,另外,它有个readelf不具备的功能:反编译。剩下的两者都差不多了。</li>
<li>ldd:这个小工具也很酷,可以让你看一个动态链接库文件依赖于哪些其它的动态链接库。</li>
<li>
<code>cat /proc/<PID>/maps</code>:这个命令很有趣,可以让你看到进程的内存分布。</li>
</ul>
<p>还有各种利器,自己去探索吧。</p>
<h3>3.4 其他</h3>
<h4>3.4.1 地址编码</h4>
<p>假如有个整形变量1234,16进制是0x000004d2,占4个字节,起始地址是0x10000,终止地址是0x10003,那么在外界看来,是它的地址是0x10000还是0x10003呢?答案是0x10000。</p>
<p><strong>那么问题来了,这4个字节里怎么放这个数?高地址放高位,还是低地址放高位?答案是,都可以!</strong></p>
<p>大端方式:高位在低地址,如 IBM360/370,MIPS</p>
<p><img src="/img/bVbEDU8" alt="12.png" title="12.png"></p>
<p>小端方式:高位在高地址,如 Intel 80x86</p>
<p><img src="/img/bVbEDVb" alt="13.png" title="13.png"></p>
<h2>四、编译</h2>
<p>由于我没学过编译,对词法分析、语法分析也不甚了解,找机会再深入吧,这里只是把大致知识梳理一下。</p>
<p><strong>词法分析->语法分析->语义分析->中间代码生成->目标代码生成</strong></p>
<h3>4.1 词法分析</h3>
<p>通过<strong>FSM(有限状态机)模型</strong>,就是按照语法定义好的样子,挨个扫描源代码,把其中的每个单词和符号做个归类,比如是关键字、标识符、字符串还是数字的值等,然后分门别类地放到各个表中(符号表、文字表)。如果不符合语法规则,在词法分析过程中就会给出各类警告,咱们在编译过程中看到的很多语法错误就是它干的。有个开源的lex的程序,可以体会这个过程。</p>
<h3>4.2 语法分析</h3>
<p>由词法分析的符号表,要形成一个抽象语法树,方法是<strong>“上下文无关语法(CFG)”</strong>。这过程就是把程序表示成一棵树,叶子节点就是符号和数字,自上而下组合成语句,也就是表达式,层层递归,从而形成整个程序的语法树。同上面的词法分析一样,也有个开源项目可以帮你做这个树的构建,就是yacc(Yet Another Compiler Compiler)。</p>
<h3>4.3 语义分析</h3>
<p>这个步骤,我理解要比语法分析工作量小一些,<strong>主要就是做一些类型匹配、类型转换的工作,然后把这些信息更新到语法树上。</strong></p>
<h3>4.4. 中间语言生成</h3>
<p><strong>把抽象语法树转成一条条顺序的中间代码</strong>,这种中间代码往往采用三地址码或者P-Code的格式,形如x = y op z。长成这个样子:</p>
<pre><code>t1 = 2 + 6
array[index] = t1</code></pre>
<p>不过这些代码是和硬件不相关的,还是“抽象”代码。</p>
<h3>4.5 目标代码生成</h3>
<p><strong>目标代码生成就是把中间代码转换成目标机器代码</strong>,这就需要和真正的硬件以及操作系统打交道了,要按照目标CPU和操作系统把中间代码翻译成符合目标硬件和操作系统的汇编指令,而且,还要给变量们分配寄存器、规定长度,最后得到了一堆汇编指令。 </p>
<p>对于整形、浮点、字符串,都可以翻译成把几个bytes的数据初始化到某某寄存器中,但是对于数组等其它的大的数据结构,就要涉及到为它们分配空间了,这样才可以确定数组中某个index的地址。不过,这事儿编译不做,留给链接去做。</p>
<p>编译不是本文重点,这里就不过多讨论了,感兴趣的同学,可以读读这篇:<a href="https://link.segmentfault.com/?enc=dN44A4YnNUOIrJuMjp2Ymg%3D%3D.dG8l0IFxxlbhfoTH88DqElJVp8bheZyGwgjjCvZFQBCtIts0sOvC5q1kt3l5LgMT" rel="nofollow">《自己动手写编译器》</a>。</p>
<h2>五、链接</h2>
<p>编译一个c源文件代码,就会对应得到一个目标文件。一个项目中会有一堆的c源代码,编译后会得到一堆的目标文件。这些目标文件是二进制的,就是一堆0、1的集合,到底这一堆0、1是如何排布的呢?<strong>接下来,我们得说一说,这些0、1组成的目标文件了。</strong></p>
<h3>5.1 目标文件</h3>
<p><strong>目标文件是没有链接的文件(一个目标文件可能会依赖其它目标文件,把它们“串”起来的过程,就是链接)。</strong>这些目标文件已经和这台电脑的硬件及操作系统相关了,比如寄存器、数据长度,但是,对应的变量的地址没有确定。</p>
<p>目标文件里有数据、机器指令代码、符号表(符号表就是源码里那些函数名、变量名和代码的对应关系,后面会细讲)和一些调试信息。</p>
<p><strong>目标代码的结构依据COFF(Common File Format)规范。</strong>Windows和Linux的可执行文件(PE和ELF)就是尊崇这种规范。大家用的都是COFF格式,动态链接库也是。通过linux下的file命令可以参看目标文件、elf可执行文件、shell文件等。</p>
<pre><code> file /lib/x86_64-linux-gnu/libc-2.27.so
/lib/x86_64-linux-gnu/libc-2.27.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/l, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped
file run.sh
run.sh: Bourne-Again shell script, UTF-8 Unicode text executable
file a.o
a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
file ab
ab: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped</code></pre>
<p>如上可以看到不同文件的区别。</p>
<h3>5.2 目标文件的结构</h3>
<blockquote>ELF是Executable LinkableFormat的缩写,是Linux的链接、可执行、共享库的格式标准,尊从COFF。</blockquote>
<p><strong>Linux下的目标ELF文件(或可执行ELF文件)的结构包括:</strong></p>
<ul>
<li>ELF头部</li>
<li>.text</li>
<li>.data</li>
<li>.bss</li>
<li>其他段</li>
<li>段表</li>
<li>符号表</li>
</ul>
<p>ELF文件的结构包含ELF的头部说明和各种“段”(section)。段是一个逻辑单元,包含各种各样的信息,比如代码(.text)、数据(.data)、符号等。</p>
<h4>5.2.1 文件头(ELF Header)</h4>
<p>先说说<strong>ELF文件开头部分的ELF头</strong>,它是一个总的ELF的说明,里面包含是否可执行、目标硬件、操作系统等信息,还包含一个重要的东西:“段表”,就是用来记录段(section)的信息。</p>
<p>看个例子:</p>
<pre><code> ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 816 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 11</code></pre>
<p>说明:</p>
<ul>
<li>其中,”7f 45 4c 46”是ELF魔法数,就是DEL字符加上“ELF”3个字母,表明它是一个elf目标或者可执行文件关于elf文件头格式。</li>
<li>还会说明诸如可执行代码起始的入口地址;段表的位置;程序表的位置;….多种信息。细节就不赘述了。</li>
</ul>
<p>关于更详细的elf文件头的内容,可以参考:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=vLKwZxSiRSxTOFqI3CL%2BzQ%3D%3D.8paI8BEAK2ixKBfN4EeY%2F%2Bdaym%2BN3kgoqDgG7QXigjxfZrH4YIt%2Fy%2BTVWt6nyk4giPxRb4%2BhY6oy3Ro6q1%2Ft5qhBFUeBQzTq%2FnqDEuPPbiI%3D" rel="nofollow">ELF 格式解析</a></li>
<li><a href="https://link.segmentfault.com/?enc=jl%2B%2F5G1LvyYynHyRHbg2qg%3D%3D.QptnRYJaDYWEc8vjp36LAd3nsxnws69cnk4zoydQC57JvDQsFTQPQcXH%2FFg4UcRjERZ3sfd%2B3uuoDm86748FIA%3D%3D" rel="nofollow">ELF文件格式解析</a></li>
<li><a href="https://link.segmentfault.com/?enc=ibIgjParM8ER57bNDrG%2Fng%3D%3D.swLtFi5ig9ejVxCxiP8c1T%2FgfaZVhuYEyTEYWSBTJt5zo7k7EXg5%2FmomnebEkRxG" rel="nofollow">ELF文件格式分析</a></li>
<li><a href="https://link.segmentfault.com/?enc=93Mb3jfRhGprdZb13O8GYg%3D%3D.QJ%2ByZkjPTRf42JAmAwl%2BNqtY95GseRedCYT83otrH96hpVfJX81ZPiCbbjckj3v%2F" rel="nofollow">ELF文件结构</a></li>
</ul>
<h4>5.2.2 段表(section table)</h4>
<p>除了elf文件头,就属段表重要了,各个段的信息都在这里。先看个例子:</p>
<p>命令<code>readelf -S ab</code>可以帮助查看ELF文件的段表。</p>
<pre><code> There are 9 section headers, starting at offset 0x1208:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 08048094 000094 000091 00 AX 0 0 1
[ 2] .eh_frame PROGBITS 08048128 000128 000080 00 A 0 0 4
[ 3] .got.plt PROGBITS 0804a000 001000 00000c 04 WA 0 0 4
[ 4] .data PROGBITS 0804a00c 00100c 000008 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 001014 00002b 01 MS 0 0 1
[ 6] .symtab SYMTAB 00000000 001040 000120 10 7 10 4
[ 7] .strtab STRTAB 00000000 001160 000063 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0011c3 000043 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)</code></pre>
<p>这个可执行文件里有9个段。常见的3个段:代码段、数据段、BSS段:</p>
<ul>
<li>代码段:.code或.text;</li>
<li>数据段:.data,放全局变量和局部静态变量;</li>
<li>BSS段:.bss,为未初始化的全局变量和局部静态变量预留位置,不占空间。</li>
</ul>
<p>还有其它段:</p>
<ul>
<li>.strtab : String Table 字符串表,用于存储 ELF 文件中用到的各种字符串;</li>
<li>.symtab : Symbol Table 符号表,从这里可以索引文件中的各个符号;</li>
<li>.shstrtab : 各个段的名称表,实际上是由各个段的名字组成的一个字符串数组;</li>
<li>.hash : 符号哈希表;</li>
<li>.line : 调试时的行号表,即源代码行号与编译后指令的对应表;</li>
<li>.dynamic : 动态链接信息;</li>
<li>.debug : 调试信息;</li>
<li>.comment : 存放编译器版本信息,比如 “GCC:GNU4.2.0”;</li>
<li>.plt 和 .got : 动态链接的跳转表和全局入口表;</li>
<li>.init 和 .fini : 程序初始化和终结代码段;</li>
<li>.rodata1 : Read Only Data,只读数据段,存放字符串常量,全局 const 变量,该段和 .rodata 一样。</li>
</ul>
<p><strong>段表里记录着每个段开始的位置和位移(offset)、长度,</strong>毕竟这些段都是紧密的放在二进制文件中,需要段表的描述信息才能把它们每个段分割开。</p>
<p>有了段,我们其实就对可执行文件了然于心了,其中.text代码段里放着可以运行的机器指令;而.data数据段里放着全局变量的初始值;.symtab里放着当初源代码中的函数名、变量名的代表的信息。</p>
<p>目标ELF文件和可执行ELF文件虽然规范是一致的,但还是有很多细微区别。</p>
<h4>5.2.3 目标ELF文件的重定位表</h4>
<p>在段表中,你会发现这种段:<strong>.rel.xxx,这些段就是链接用的!</strong>因为你需要把某个目标中出现的函数、变量等的地址,换成其它目标文件中的位置(也就是地址),这样才能正确地引用、调用这些变量。至于链接细节,后面讲链接的时候再说。</p>
<p><strong>一般有text、data两种重定位表:</strong></p>
<ul>
<li>.rel.text:代码段重定位表,描述代码段中出现的函数、变量的引用地址信息等;</li>
<li>.rel.data: 数据段重定位表。</li>
</ul>
<h4>5.2.4 字符串表</h4>
<p>.strtab、.shstrtab</p>
<p>ELF中很多字符串,比如函数名字、变量名字,都放到一个叫“字符串”表的段中。</p>
<h4>5.2.5 符号表</h4>
<p>注意:字符串表只是字符串,符号表跟它不一样,<strong>符号表更重要,它表示了各个函数、变量的名字对应的代码或者内存地址,在链接的时候,非常有用。</strong>因为链接就是要找各个变量和函数的位置,这样才可以更新编译阶段空出来的函数、变量的引用地址。</p>
<p>每个目标文件里都有这么一个符号表,用nm和readelf可以查看:</p>
<p><strong>1)a.o目标文件的符号表</strong></p>
<p><code>nm a.o</code></p>
<pre><code>
U _GLOBAL_OFFSET_TABLE_
U __stack_chk_fail
0000000000000000 T main
U shared
U swap</code></pre>
<p><strong>2)<code>readelf -s a.o</code> 目标文件的符号表:</strong></p>
<pre><code> Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 5
8: 00000000 85 FUNC GLOBAL DEFAULT 1 main
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
11: 00000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail</code></pre>
<p><strong>从这个目标ELF文件的符号表可以看到swap函数,Ndx是UND(Undefined的缩写),</strong>表明不知道它到底在哪个段,需要被重定位,就是写个1或3之类的数字表明段中的index;对于全局变量shared也是同样的定义。这些内容都会在静态链接的时候,被链接器修改。</p>
<p>为了对比,我们来看可执行文件ab的符号表的样子,看看静态链接后,这些符号的Ndx的变换。</p>
<p><strong>3)可执行文件ab的符号表</strong></p>
<p><code>nm ab</code></p>
<pre><code> 0804a000 d _GLOBAL_OFFSET_TABLE_
0804a014 D __bss_start
080480d7 T __x86.get_pc_thunk.ax
0804a014 D _edata
0804a014 D _end
080480db T main
0804a00c D shared
08048094 T swap
0804a010 D test</code></pre>
<p><code>readelf -s ab</code></p>
<pre><code> Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 08048094 0 SECTION LOCAL DEFAULT 1
2: 08048128 0 SECTION LOCAL DEFAULT 2
3: 0804a000 0 SECTION LOCAL DEFAULT 3
4: 0804a00c 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 FILE LOCAL DEFAULT ABS b.c
7: 00000000 0 FILE LOCAL DEFAULT ABS a.c
8: 00000000 0 FILE LOCAL DEFAULT ABS
9: 0804a000 0 OBJECT LOCAL DEFAULT 3 _GLOBAL_OFFSET_TABLE_
10: 08048094 67 FUNC GLOBAL DEFAULT 1 swap
11: 080480d7 0 FUNC GLOBAL HIDDEN 1 __x86.get_pc_thunk.ax
12: 0804a010 4 OBJECT GLOBAL DEFAULT 4 test
13: 0804a00c 4 OBJECT GLOBAL DEFAULT 4 shared
14: 0804a014 0 NOTYPE GLOBAL DEFAULT 4 __bss_start
15: 080480db 74 FUNC GLOBAL DEFAULT 1 main
16: 0804a014 0 NOTYPE GLOBAL DEFAULT 4 _edata
17: 0804a014 0 NOTYPE GLOBAL DEFAULT 4 _end</code></pre>
<p>可以看到,现在shared的Ndx是4,而swap的Ndx是1,对应的就是:4-数据段、1-代码段。</p>
<pre><code> 上面曾经显示过的段的编号
。。。。
[ 1] .text PROGBITS 08048094 000094 000091 00 AX 0 0 1
[ 2] .eh_frame PROGBITS 08048128 000128 000080 00 A 0 0 4
[ 3] .got.plt PROGBITS 0804a000 001000 00000c 04 WA 0 0 4
[ 4] .data PROGBITS 0804a00c 00100c 000008 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 001014 00002b 01 MS 0 0 1
。。。</code></pre>
<p>如上,对应的第一列的序号就标明了代码段是1,数据段是4。</p>
<p>另外,<strong>第二列Type也挺有用的:Object表示数据的符号,而Func是函数符号。</strong></p>
<h2>六、静态链接</h2>
<p>目标文件介绍得差不多了,我们得到了一大堆零散的目标ELF文件,是时候把它们“合体”了,这就需要链接过程了,就是要把这些目标文件“凑”到一起,也就是把各个段合并到一起。</p>
<p><img src="/img/bVbEDVx" alt="14.jpg" title="14.jpg"></p>
<p>合并开始!<strong>读每个目标文件的文件头,获得各个段的信息,然后做符号重定位。</strong></p>
<ul>
<li>读每个目标文件,收集各个段的信息,然后合并到一起,其实我理解就是压缩到一起,你的代码段挨着我的代码段,合并成一个新的,因为每个ELF目标文件都有文件头,是可以很严格合并到一起的;</li>
<li>符号重定位,简单来说就是把之前调用某个函数的地址给重新调整一下,或者某个变量在data段中的地址重新调整一下。因为合并的时候,各个代码段都合并了,对应代码中的地址都变了,所以要调整。这是链接最核心的一步!</li>
</ul>
<p><code>ld a.o b.o ab</code></p>
<p>详细介绍<strong>a.o+b.o=> ab的变化,特别是虚拟地址的变化。</strong></p>
<p>先看链接前的目标ELF文件:a.o,b.o。</p>
<pre><code>a.o的段属性(objdump -h a.o)
------------------------------------------------------------------------
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
ALLOC
b.o的段属性(objdump -h b.o)
------------------------------------------------------------------------
Idx Name Size VMA LMA File off Algn
0 .text 0000004b 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 0000008c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000094 2**0
ALLOC</code></pre>
<p>接下来是a.o + b.o,链接合体后的可执行ELF文件:ab。</p>
<pre><code>ab的段属性(objdump -h ab)
------------------------------------------------------------------------
Idx Name Size VMA LMA File off Algn
0 .text 00000091 08048094 08048094 00000094 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000080 08048128 08048128 00000128 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .got.plt 0000000c 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .data 00000008 0804a00c 0804a00c 0000100c 2**2
CONTENTS, ALLOC, LOAD, DATA</code></pre>
<p>我们来玩一玩“找不同”!<strong>可执行ELF文件ab的VMA填充了。</strong>VMA是啥?为何需要调整?看来是时候说一说可执行ELF文件了。</p>
<h3>6.1 目标ELF文件和可执行ELF文件</h3>
<p>上面一直刻意不区分目标ELF文件和可执行ELF文件,原因是想先介绍它们共同的ELF规范部分,但其实两者是有区别的,这一小节忍不住想介绍一下,希望不会打断看官的思路。</p>
<p><strong>目标ELF文件和可执行ELF文件,其实是两个目的、两个视角:</strong></p>
<p><img src="/img/bVbEDVA" alt="15.jpg" title="15.jpg"></p>
<ul>
<li>目标文件是为了进一步链接用的,我们可以用“链接视角”来看待它,它有各个sections,用段表section head table(SHT)来记录、归档不同的内容,还有重要的重定位表,用于链接;</li>
<li>可执行文件是为“进程视角”存在的,不需要重定位表,但它多了一个 “program header table(PHT)”,用来告诉操作系统如何把各个section加到进程空间的segment中。进程里专门有个“segment”的概念,定义出“虚拟内存区域”(VMA,Virtual Memory Area),每个VMA就是一个segement。这些segment是操作系统为了装载需要,专门又对sections们做了一次合并,定义出不同用途的VMA(如代码VMA、数据VMA、堆VMA、栈VMA)。</li>
<li>在目标文件中,你会看到地址都是从0开始的,但是在可执行文件中是0x8048000开始的,因为操作系统进程虚拟地址的开始地址就是这个数。关于虚拟地址空间,这里不展开了,后面讲装载的部分再详细讨论。</li>
</ul>
<p>虽然两者有区别,但大体的规范是一样的,都有ELF头、段表(section table)、节(section)等基本的组成部分。</p>
<p>可以参考这篇文章<a href="https://link.segmentfault.com/?enc=%2FoOoVyqjxqL9VyQBv9%2BG0g%3D%3D.D5ALjBAHfnAs%2BOzU7SYLBo58T4kBUxnwlVUaPS78wbPRL7a5uLTivXqzxQ6ZaoPRNwsF79jUF1taQvJ%2BONibXw%3D%3D" rel="nofollow">《ELF可执行文件的理解》</a>,加深理解。</p>
<h3>6.2 合体的ELF可执行文件</h3>
<p>回来看合体(链接)后的可执行ELF文件ab。</p>
<p>ab的段属性(<code>objdump -h ab</code>):</p>
<pre><code> Idx Name Size VMA LMA File off Algn
0 .text 00000091 08048094 08048094 00000094 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000080 08048128 08048128 00000128 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .got.plt 0000000c 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .data 00000008 0804a00c 0804a00c 0000100c 2**2
CONTENTS, ALLOC, LOAD, DATA</code></pre>
<p>可以看到,ab的代码段.text是从0x8048094开始的,长度是0x91,也就是145个字节长度的代码段。</p>
<p>段的开头地址确定了,接下来段里符号对应的地址就好找了(也就是.text段中的函数和.data段中的变量)。</p>
<p>回过头去看几个符号:swap函数、main函数、test变量、shared变量:</p>
<pre><code> Num: Value Size Type Bind Vis Ndx Name
10: 08048094 67 FUNC GLOBAL DEFAULT 1 swap
12: 0804a010 4 OBJECT GLOBAL DEFAULT 4 test
13: 0804a00c 4 OBJECT GLOBAL DEFAULT 4 shared
15: 080480db 74 FUNC GLOBAL DEFAULT 1 main</code></pre>
<ul>
<li>main函数:地址是080480db,Ndx=1,Type=FUNC,也就是说,main这个符号对应的是一个函数,在代码段.text,起始地址是080480db;</li>
<li>test变量:地址是0804a010,Ndx=4,Type=OBJECT,也就是说,test这个符号对应的是一个变量,在数据段,起始地址是0804a010。</li>
</ul>
<p>问题来了,这些地址是如何确定的呢?要知道目标ELF文件a.o、b.o里的地址还都是0作为基地址的,到合体后的可执行文件ab怎么就填充了这些东西呢?这就要引出<strong>“符号重定位”</strong>了。</p>
<h3>6.3 符号重定位</h3>
<p><strong>既然链接是把大家的代码段、数据段都合并到一起,那就需要修改对应的调用的地址,</strong>比如a.o要调用b.o中的函数,合并到一起成为ab的时候,就需要修改之前a.o中的调用的地址为一个新的ab中的地址,也就是之前b.o中的那个函数swap的地址。</p>
<p><strong>链接器通过“重定位 + 符号解析”完成上述工作。</strong></p>
<p>最开始编译完的目标文件,变量地址、函数地址的基准地址都是0。一旦链接,就不能从0开始了,而要从操作系统和应用进程规定的虚拟起始地址开始作为基准地址,这个规定是<code>0x08048094</code>。别问我为什么,真心不知~</p>
<p>另外,还有这几个目标文件的各个段,它们的函数、变量等的地址原本都是基于0,现在合体了,都要开始逐一调整!之前每个函数、变量的地址都是相对于0的,也就是说,你知道它们的偏移offset,这样的话,你只需要告诉它们新的基地址的调整值,就可以加上之前的offset算出新的地址,把所有涉及到被调用的地方都改一遍,就完成了这个重定位的过程。</p>
<p>具体怎么做呢?通过重定位表来完成。</p>
<h3>6.4 重定位表</h3>
<p>就是一个表,记着之前每个object目标文件中哪些函数、变量需要被重定位。这是一个单独的段,命名还有规律呢!就是.rel.xxx,比如.rel.data、.rel.text。</p>
<p>看个栗子:</p>
<pre><code> RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000025 R_X86_64_PC32 shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32 swap-0x0000000000000004</code></pre>
<p>shared变量和swap函数都在a.o的重定位表中被记录下来,说明它们的地址后期会被调整。offset中的25,就是shared变量对于数据段的起始位置的位移offset是25个字节;同样,swap函数相对于代码段开始的offset是32个字节。另外,VALUE这列的“shared、swap”会对应到符号表里面的shared、swap符号。</p>
<p>重定位表只记录哪些符号需要重定位,而关于这个函数、变量更详细的信息都在符号表中。</p>
<p>接下来精彩的事情发生了,也就是<strong>链接中最关键的一步:修改链接完成的文件中调用函数和变量引用的地址。</strong></p>
<h3>6.5 指令修改</h3>
<p>修改函数和数据的应用地址有很多方法,这涉及到各个平台的寻址指令差异,比如R_X86_64_PC32。但本质来讲就需要一种计算方法,计算出链接后的代码中对函数的调用地址、变量的应用地址、进行链接后的修改地址。</p>
<p>对于32位的程序来说,一共有10种重定位的类型。</p>
<p>举个例子可能更容易理解:文件a.c,b.c,链接成ab,我们来看链接过程中是如何做指令地址修改的。</p>
<p>先看看源代码:</p>
<p>a.c</p>
<pre><code> extern int shared;
int main()
{
int a = 0;
swap(&a, &shared);
}</code></pre>
<p>b.c</p>
<pre><code> int shared = 1;
int test = 3;
void swap(int* a, int* b) {
*a ^= *b ^= *a ^= *b;
}</code></pre>
<p>a.c的汇编文件</p>
<pre><code>00000000 <main>:
....
31: 89 c3 mov %eax,%ebx
33: e8 fc ff ff ff call 34 <main+0x34> <------------- 调用swap函数
38: 83 c4 10 add $0x10,%esp
....</code></pre>
<pre><code>Relocation section '.rel.text' at offset 0x24c contains 4 entries:
Offset Info Type Sym.Value Sym. Name
....
00000034 00000e04 R_386_PLT32 00000000 swap</code></pre>
<p>可以看到目标文件a.o中的汇编指令和重定位表中为<code>R_386_PLT32</code>的重定位方式。然后,链接后得到ab的代码。</p>
<p>链接后的 ab ELF可执行文件:</p>
<pre><code>08048094 <swap>:
8048094: 55 push %ebp
8048095: 89 e5 mov %esp,%ebp
....
080480db <main>:
....
804810c: 89 c3 mov %eax,%ebx
804810e: e8 81 ff ff ff call 8048094 <swap>
8048113: 83 c4 10 add $0x10,%esp
....</code></pre>
<p><strong>分析</strong></p>
<p>1)修正后的swap地址是:<code>0x08048094</code></p>
<p>2)修正后的代码地址是: <code>0x804810e</code></p>
<p>3)原来的调用代码: <code>33: e8 fc ff ff ff call 34 <main+0x34></code>,其实是0xfffffffc,补码表示的-4</p>
<p>4)先看修改完成的:ab中,<code>804810e: e8 81 ff ff ff call 8048094 <swap></code>。e8 fc ff ff ff 修改成了=> e8 81 ff ff ff,补码表示是-127</p>
<p>5)这个值是怎么算的?</p>
<p>a.o的重定位表中的信息是:<code>00000034 00000e04 R_386_PLT32 00000000 swap</code>。</p>
<p>所谓R_386_PLT32,是:L+A-P</p>
<ul>
<li>L:重定项中VALUE成员所指符号@plt的内存地址 => 8048094,就是修正后的swap函数地址;</li>
<li>A:被重定位处原值,表示”被重定位处”相对于”下一条指令”的偏移 => fcffffff,就是源代码上的地址,固定的,补码表示的,实际值是-4;</li>
<li>P:被重定位处的内存地址 => 804810e,就是修正后的main中调用swap的代码地址。</li>
</ul>
<p>按照这个公式计算修正后的调用地址:</p>
<p>L+A-P:8048094 + −4 - 804810e = - 127 = -0x7f,补码表示是 ffffff81,由于是小端表示,所以最终替换完的指令为:</p>
<p><code>804810e: e8 81 ff ff ff call 8048094 <swap></code></p>
<p>代码在执行的时候,会用当前地址的下一条指令的地址,加上偏移(-127),正好就是swap修正后的地址0x08048094。</p>
<h3>6.6 静态链接库</h3>
<p>我们自己写的程序可以编译成目标代码,然后等着链接。但是,我们可能会用到别的库,它们也是一个个的xxx.o文件么?链接的时候需要挨个都把它们指定链接进来么?</p>
<p>我们可能会用到c语言的核心库、操作系统提供的各种api的库,以及很多第三方的库。比如c的核心库,比较有名的是glibc,原始的glibc源代码很多,可以完成各种功能,如输入输出、日期、文件等等,它们其实就是一个个的xxx.o,如fread.o,time.o,printf.o,就是你想象的样子。</p>
<p>可是,它们被压缩到了一个大的zip文件里,叫libc.a:<code>./usr/lib/x86_64-linux-gnu/libc.a</code>,就是个大zip包,把各种*.o都压缩进去了,据说libc.a包含了1400多个目标文件。</p>
<pre><code> objdump -t ./usr/lib/x86_64-linux-gnu/libc.a|more
In archive ./usr/lib/x86_64-linux-gnu/libc.a:
init-first.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
.......</code></pre>
<p>我好奇地统计了一下,其实不止1400,我的这台ubuntu18.04上,有1690个!</p>
<pre><code> objdump -t ./usr/lib/x86_64-linux-gnu/libc.a|grep 'file format'|wc -l
1690</code></pre>
<p>如果以–verbose方式运行编译命令,你能看到整个细节过程:</p>
<p><code>gcc -static --verbose -fno-builtin a.c b.c -o ab</code></p>
<pre><code> ....
/usr/lib/gcc/x86_64-linux-gnu/7/cc1 -quiet -v -imultiarch x86_64-linux-gnu b.c -quiet -dumpbase b.c -mtune=generic -march=x86-64 -auxbase b -version -fno-builtin -fstack-protector-strong -Wformat -Wformat-security -o /tmp/cciXoNcB.s
....
as -v --64 -o /tmp/ccMLSHnt.o /tmp/cciXoNcB.s
.....
/usr/lib/gcc/x86_64-linux-gnu/7/collect2 -o ab /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginT.o ...</code></pre>
<p>整个过程分为3步:</p>
<ul>
<li>cc1做编译:编译成临时的汇编程序<code>/tmp/cciXoNcB.s</code>;</li>
<li>as汇编器:生成目标二进制代码;</li>
<li>collect2:实际上是一个ld的包装器,完成最后的链接。</li>
</ul>
<p>还会链接各类的静态库,其实它们都在libc.a这类静态库中。</p>
<h2>七、装载</h2>
<p>终于把一个程序编译、链接完,变成了一个可执行文件,接下来就要聊聊如何把它加载到内存,这就是“装载”的过程。</p>
<h3>7.1 虚拟地址空间</h3>
<p>在谈加载到内存之前,先了解进程虚拟地址空间。</p>
<p><strong>进程虚拟地址空间,在我看来是一个非常重要的概念,它的意义在于,让每个程序,甚至后面的进程,都变得独立起来,</strong>不需要考虑物理内存、硬盘、在文件中的绝对位置等。它关心的只是自己在一个虚拟空间的地址位置。这样链接器就好安排每个代码、数据的位置,装载器也好安排指令、数据、栈、堆的位置,与硬件无关。</p>
<p>这个地址编码也很简单,就是你总线多大,我就能编码多大。比如8位总线,地址就256个;到了32位,地址就可以是4G大小了;64位的话,地址就很大了...这么大的一个地址空间都给一个程序和进程用了!可是,真实内存可能也就16G、32G,还有那么多进程怎么办?怎么装载进来?别急,后面会介绍。</p>
<h3>7.2 如何载入内存</h3>
<p><strong>一个可执行文件地址空间硕大无比,怎么把这头大象装入只有16G大小的“冰箱”—-内存?!答案是映射。</strong></p>
<p><img src="/img/bVbEDVK" alt="16.jpeg" title="16.jpeg"></p>
<p>这样就可以把可执行文件中一块一块地装进内存里面了,前提是进程需要的块,比如正在或马上要执行的代码、数据等。那剩下的怎么办?如果内存满了怎么办?这些不用担心,操作系统负责调度,会判断是否用到,用到的就会加载;如果满了,就按照LRU算法替换旧的。</p>
<h3>7.3 进程视角</h3>
<p>切换到进程视角,进程也要有一个虚拟空间,叫<strong>“进程虚拟空间(Process Virtual Space)”</strong>。注意:我们又提到了虚拟空间,前面聊起过这个话题,链接器需要、进程加载也需要,链接的时候要给每段代码、数据编个地址,现在进程也需要一个虚拟地址。我的学习认知告诉我这俩不是一回事,但应该差不了多少,都是总线位数编码出来的空间大小,各个内容存放的位置也不会有太大变换。</p>
<p>但毕竟是不一样的,所以它们之间也需要映射。有了这个映射,进程发现自己所需要的可执行代码缺了,才能知道到可执行文件中的第几行加载。<strong>这个映射关系就存在可执行ELF的PHT(程序映射表 - Program Header Table)中</strong>,前面介绍过,就是个映射表。</p>
<p>我们再将PHT映射表细化一下。</p>
<p>如果能直接把可执行文件原封不动地映射到进程空间多好啊,这样映射多简单啊。事实不是这样的。</p>
<p>为了空间布局上的效率,链接器会把很多段(section)合并,规整成可执行的段(segment)、可读写的段、只读段等,合并后,空间利用率就高了。否则,即便是很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如4k)。所以链接器趁着链接就把小块们都合并了,这个合并信息就在可执行文件头的VMA信息里。</p>
<p>这里有2个段:section和segment,中文都叫段,但有很大区别:section是目标文件中的单元;而segement是可执行文件中的概念,是一个section的组合或集合,是为了将来加载到进程空间里用的。在我理解,segement和VMA是一个意思。</p>
<p><code>readelf -l ab</code> 可以查看程序映射表 - Program Header Table:</p>
<pre><code> Elf file type is EXEC (Executable file)
Entry point 0x80480db
There are 3 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x001a8 0x001a8 R E 0x1000
LOAD 0x001000 0x0804a000 0x0804a000 0x00014 0x00014 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
Section to Segment mapping:
Segment Sections...
00 .text .eh_frame
01 .got.plt .data</code></pre>
<p>“Segment Sections”就告诉你如何合并这些sections了。</p>
<p>上述示例有3个段(Segment),其中2个type是LOAD的Segment,一个是可执行的Segment,一个是只读的Segment。第一个可执行Segment到底合并哪些Section呢? 答案是:<code>00 .text .eh_frame</code>。</p>
<p>这个信息是存在可执行文件的<strong>“程序头表(Program Header Table - PHT)”</strong>里面的,就是用readelf -f看到的内容,告诉你sections如何合并成segments。</p>
<p><strong>总结:</strong></p>
<ul>
<li>目标文件有自己的sections,可执行文件也一样;</li>
<li>只不过可执行文件又创造了一个概念:segment,就是把sections做了一个合并;</li>
<li>真正装载放到内存里的时候,还要段地址对齐。</li>
</ul>
<h3>7.4 段(Segment)地址对齐</h3>
<p>内存都是一个一个4k的小页,便于分配,这涉及到内存管理,不展开详述。</p>
<p>操作系统就给你一摞4k小页,问题是即使将sections们压缩成了segment,也不正好就4k大小,就算多一点点,操作系统也得额外再分配一页,多浪费啊。</p>
<p>办法来了:<strong>段地址对齐</strong>。</p>
<p><img src="/img/bVbEDVY" alt="17.jpg" title="17.jpg"></p>
<p>一个物理页(4k)上不再是放一个segment,而是还放着别的,物理页和进程中的页是1:2的映射关系,浪费就浪费了,反正也是虚拟的。物理上就被“压缩”到了一起,过去需要5个才能放下的内容,现在只需要3个物理页了。</p>
<h3>7.5 堆和栈</h3>
<p>可执行文件加载到进程空间里之后,进程空间还有两个特殊的VMA区域,分别是<strong>堆和栈</strong>。</p>
<p><img src="/img/bVbEDWb" alt="18.jpg" title="18.jpg"></p>
<p>通过查看linux中的进程内存映射也可以看到这个信息:<code>cat /proc/555/maps</code></p>
<pre><code> 55bddb42d000-55bddb4f5000 rw-p 00000000 00:00 0 [heap]
...
7ffeb1c1a000-7ffeb1c3b000 rw-p 00000000 00:00 0 [stack]</code></pre>
<p>参考:<a href="https://link.segmentfault.com/?enc=9SzBWVN2NXeCO4nFon0VUA%3D%3D.imcb%2FCJeYplm43H2FaBox8GxgSFZ%2Fm8h6fwuwfFxYOrWlUbQANDh87v3bu3pS62Jzo6iyFH%2B0zzJmTmR5vT8IQ%3D%3D" rel="nofollow">Anatomy of a Program in Memory Gcc 编译的背后</a></p>
<h2>八、动态链接</h2>
<p>静态链接大致清楚了,接下来介绍动态链接。</p>
<p><strong>动态链接的好处很多:</strong></p>
<ul>
<li>代码段可以不用重复静态链接到需要它的可执行文件里面去了,省了磁盘空间;</li>
<li>运行期还可以共享动态链接库的代码段,也省了内存。</li>
</ul>
<h3>8.1 一个栗子</h3>
<p>先举个例子,看看动态链接库怎么写。</p>
<p>lib.c,动态链接库代码:</p>
<pre><code>#include <stdio.h>
void foobar(int i) {
printf("Printing from lib.so --> %d\n", i);
sleep(-1);
}</code></pre>
<p>为了让其他程序引用它,需要为它编写一个头文件:lib.h</p>
<pre><code> #ifndef LIB_H_
#define LIB_H_
void foobar(int i);
#endif // LIB_H_</code></pre>
<p>最后是调用代码:program1.c</p>
<pre><code>#include "lib.h"
int main() {
foobar(1);
return 0;
}</code></pre>
<p>编译这个动态链接库:<code>gcc -fPIC -shared -o lib.so lib.c</code>可以得到lib.so。然后编译引用它的程序的program1.c: <code>gcc -o program1 program1.c ./lib.so</code>,这样就可以顺利地引用这个动态链接库了。</p>
<p><img src="/img/bVbEDWc" alt="19.jpg" title="19.jpg"></p>
<p>这背后到底发生了什么?</p>
<p>编译program1.c时,引用了函数foobar,可这个函数在哪里呢?要在编译,也就是链接的时候,告诉这个program1程序,所需要的那个foobar在lib.so里面,也就是需要在编译参数中加入./lib.so这个文件的路径。据说链接器要拷贝so的符号表信息到可执行文件中。</p>
<p>在过去静态链接的时候,我们要在program1中对函数foobar的引用进行重定位,也就是修改program1中对函数foobar引用的地址。动态链接不需要做这件事,因为链接的时候,根本就没有foobar这个函数的代码在代码段中。</p>
<p>那什么时候再告诉program1 foobar的调用地址到底是多少呢?答案是运行的时候,也就是运行期,加载lib.so的时候,再告诉program1,你该去调用哪个地址上的lib.so中的函数。</p>
<p>我们可以通过/proc/$id/maps,查看运行期program1的样子:</p>
<p><code>cat /proc/690/maps</code></p>
<pre><code> 55d35c6f0000-55d35c6f1000 r-xp 00000000 08:01 3539248 /root/link/chapter7/program1
55d35c8f0000-55d35c8f1000 r--p 00000000 08:01 3539248 /root/link/chapter7/program1
55d35c8f1000-55d35c8f2000 rw-p 00001000 08:01 3539248 /root/link/chapter7/program1
55d35dc53000-55d35dc74000 rw-p 00000000 00:00 0 [heap]
7ff68e48e000-7ff68e675000 r-xp 00000000 08:01 3671326 /lib/x86_64-linux-gnu/libc-2.27.so
7ff68e675000-7ff68e875000 ---p 001e7000 08:01 3671326 /lib/x86_64-linux-gnu/libc-2.27.so
7ff68e875000-7ff68e879000 r--p 001e7000 08:01 3671326 /lib/x86_64-linux-gnu/libc-2.27.so
7ff68e879000-7ff68e87b000 rw-p 001eb000 08:01 3671326 /lib/x86_64-linux-gnu/libc-2.27.so
7ff68e87f000-7ff68e880000 r-xp 00000000 08:01 3539246 /root/link/chapter7/lib.so
7ff68ea81000-7ff68eaa8000 r-xp 00000000 08:01 3671308 /lib/x86_64-linux-gnu/ld-2.27.so
7ffc2a646000-7ffc2a667000 rw-p 00000000 00:00 0 [stack]
7ffc2a66c000-7ffc2a66e000 r--p 00000000 00:00 0 [vvar]
7ffc2a66e000-7ffc2a670000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]</code></pre>
<p>如上可以看到<strong>“ld-2.27.so”,动态连接器</strong>。系统开始的时候,它先接管控制权,加载完lib.so后,再把控制权返还给program1。凡是有动态链接库的程序,都会把它动态链接到程序的进程中,由它首先加载动态链接库。</p>
<h3>8.2 GOT和PLT</h3>
<p><img src="/img/bVbEDWf" alt="20.jpg" title="20.jpg"></p>
<p>GOT和PLT很复杂,细节很多,不太好理解,我也只是把大致的过程搞明白了,所以这里只是说一说我的理解,如果感兴趣可以看南大袁春风老师关于PLT的讲解。</p>
<p>GOT放在数据段里,而PLT在代码段里,所以GOT是可以改的,放的跳转用的函数地址;而PLT里面放的是告诉怎么调用动态链接库里函数的代码(不是函数的代码,是怎么调用的代码)。</p>
<p>假如主程序需要调用动态链接库lib.so里的1个函数:ext,那么在GOT表里和PLT表里都有1个条目,GOT表里是未来这个函数加载后的地址;而PLT里放的是如何调用这个函数的代码,这些代码是在链接期链接器生成的。</p>
<p>GOT里还有3个特殊的条目,PLT里还有1个特殊的条目。</p>
<p><strong>GOT里的3个特殊条目:</strong></p>
<ul>
<li>GOT[0]: .dynamic section的首地址,里面放着动态链接库的符号表的信息。</li>
<li>GOT[1]: 动态链接器的标识信息,link_map的数据结构,这个不是很明白,我理解就是链接库的so文件的信息,用于加载。</li>
<li>GOT[2]: 这个是调用动态库延迟绑定的代码的入口地址,延迟绑定的代码是一个特殊程序的入口,实际是一个叫“_dl_runtime_resolve”的函数的地址。</li>
</ul>
<p><strong>PLT里的特殊条目:</strong></p>
<ul><li>PLT[0]: 就是去调动“_dl_runtime_resolve”函数的代码,是链接器自动生成的。</li></ul>
<p>整个过程开始了:因为是延迟绑定,所以动态重定位这个过程就需要在第一次调用函数的时候触发。什么是<strong>动态重定位</strong>?就是要告诉进程加载程序,修改新载入的动态链接库被调用处的地址,谁知道你把so文件加载到进程空间的哪个位置了,你得把加载后的地址告诉我,我才能调用啊~这个过程就是动态重定位。</p>
<p>.text的主程序开始调用ext函数,ext函数的调用指令:</p>
<p><code>804845b: e8 ec fe ff ff call 804834c<ext></code></p>
<p>804834c是谁?原来是PLT[1]的地址,就是ext函数对应的PLT表里的代理函数,每个函数都会在PLT、GOT里对应一个条目。</p>
<p>现在跳转到这个函数(PLT[1])去。</p>
<p>PLT[1]:</p>
<pre><code>804834c: ff 25 90 95 04 08 jmp *0x8049590
8048352: 68 00 00 00 00 pushl $0x0
8048357: e9 e0 ff ff ff jmp 804833c</code></pre>
<p>这个函数首先跳到0x8049590里写的那个地址去了(jmp *xxx,不是跳到xxx,而是跳到xxx里面写的地址上去)。</p>
<p><strong>这里有2个细节:</strong></p>
<ul>
<li>0x8049590这个地址就是GOT[3],GOT[3]是ext函数对应的GOT条目;</li>
<li>0x8049590里写的那个地址就是PLT[1](ext对应的plt条目)的下一条。</li>
</ul>
<p>what?PLT[1]代码绕这么个圈子(用GOT[3]里的地址跳)jmp,其实就是跳到了自己的下一条?是,这次是可笑,但未来这个值会改的,改成真正的动态库的函数地址,直接去执行函数。</p>
<p>跳回来之后(PLT[1]),接下来是压栈了一个0,0表示是第一个函数,也就是ext的索引。</p>
<p>继续跳0x804833c,这是PLT[0],PLT[0]是去调用“_dl_runtime_resolve”函数。在调用之前还要干一件事:<code>push 0x8049588</code>,0x8049588是GOT[2]。GOT[2]里放着so的信息(我理解的不一定完全正确)。</p>
<p>至此,可以调用“_dl_runtime_resolve”函数去加载整个so了。</p>
<p>参数包括2个:一个是压栈的那个0,就是ext函数的索引,后续通过这个索引可以找到GOT表的位置,把真正的函数的地址回填回去;第二个参数是压栈的GOT[1],就是动态链接器的标识信息,我理解就是告诉加载器so名字叫啥,它好去加载。</p>
<p>加载完成,立刻回调安放到位置的so里,索引为0的ext函数的地址,到GOT[3]中,也就是索引0。</p>
<p>下次再调用这个函数的时候,还是先调用PLT[1](ext的代理代码),但里面的<code>jmp \*0x8049590</code> (jmp *GOT[3])可以直接跳转到真正的ext里去了。</p>
<p>终于捋完了,必须总结一下。</p>
<ul>
<li>动态链接库,动态把so加载到虚拟地址空间,因为地址是不定的,所以跟静态链接的思路一样,需要做重定位,也就是要修改调用的代码地址。</li>
<li>因为是动态链接,都已经是运行期了,不能修改内存代码段(.text)(只读),只能加载完之后,把加载的函数地址写到GOT表里。这就是在加载时修改GOT表的方法。</li>
<li>还有一种方法是:在主程序启动时不加载so,等第一次调用某个动态链接库的函数时再加载so,再更新GOT表。思路是:主程序调用某个动态链接库函数时,其实是先调用了一个代理代码(PLT[x]),它会记录自己的序号(确定是调哪个函数)和动态链接库的文件名这2个参数,然后转去调用“_dl_runtime_resolve”函数,这个函数负责把so加载到进程虚拟空间去,并回填加载后的函数地址到GOT表,以后再调用就可以直接去调用那个函数了。</li>
</ul>
<h3>8.3参考</h3>
<p><a href="https://link.segmentfault.com/?enc=fsl9lcA67F2CrimtZp7hxQ%3D%3D.MCQwMsZhbab60uwUddmgCcficFjnINftSqHx3duxFbi7J3CYTc0rT1BcqieWJk90" rel="nofollow">这个是一篇很赞的文章讲的PLT的内容</a>,引用过来:</p>
<p><strong>动态链接库中的函数动态解析过程如下:</strong></p>
<p>1)从调用该函数的指令跳转到该函数对应的PLT处;</p>
<p>2)该函数对应的PLT第一条指令执行它对应的.GOT.PLT里的指令。第一次调用时,该函数的.GOT.PLT里保存的是它对应的PLT里第二条指令的地址;</p>
<p>3)继续执行PLT第二条、第三条指令,其中第三条指令作用是跳转到公共的PLT(.PLT[0]);</p>
<p>4)公共的PLT(.PLT[0])执行.GOT.PLT[2]指向的代码,也就是执行动态链接器的代码;</p>
<p>5)动态链接器里的_dl_runtime_resolve_avx函数修改被调函数对应的.GOT.PLT里保存的地址,使之指向链接后的动态链接库里该函数的实际地址;</p>
<p>6)再次调用该函数对应的PLT第一条指令,跳转到它对应的.GOT.PLT里的指令(此时已经是该函数在动态链接库中的真正地址),从而实现该函数的调用。</p>
<h3>8.4 Linux的共享库组织</h3>
<p><strong>Linux为了管理动态链接库的各种版本,定义了一个so的版本共享方案。</strong></p>
<p><code>libname.so.x.y.z</code></p>
<ul>
<li>x是主版本号:重大升级才会变,不向前兼容,之前引用的程序都要重新编译;</li>
<li>y是次版本号:原有的不变,增加了一些东西而已,向前兼容;</li>
<li>z是发布版本号:任何接口都没变,只是修复了bug,改进了性能而已。</li>
</ul>
<p><strong>1)SO-NAME</strong></p>
<p>Linux有个命名机制,用来管理so之间的关系,这个机制叫SO-NAME。任何一个so都对应一个SO-NAME,就是<code>libname.so.x</code>。</p>
<p>一般系统的so,不管它的次版本号和发布版本号是多少,都会给它建立一个SO-NAME的软链接,例如 libfoo.so.2.6.1,系统就会给它建立一个叫libfoo.so.2的软链。</p>
<p>这个软链接会指向这个so的最新版本,比如我有2个libfoo,一个是libfoo.so.2.6.1,一个是libfoo.so.2.5.5,软链接默认指向版本最新的libfoo.so.2.6.1。</p>
<p>在编译的时候,我们往往需要引入依赖的链接库,这时依赖的so使用软链接的SO-NAME,而不使用详细的版本号。</p>
<p>在编译的ELF可执行文件中会存在.dynamic段,用来保存自己所依赖的so的SO-NAME。</p>
<p>编译时有个更简洁指定lib的方式,就是<code>gcc -lxxx</code>,xxx是libname中的name,比如<code>gcc -lfoo</code>是指链接的时候去链接一个叫libfoo.so的最新的库,当然这个是动态链接。如果加上-static: <code>gcc -static -lfoo</code>就会去默认静态链接libfoo.a的静态链接库,规则是一样的。</p>
<p><strong>2)ldconfig</strong></p>
<p>Linux提供了一个工具“ldconfig”,运行它,linux就会遍历所有的共享库目录,然后更新所有的so的软链,指向它们的最新版,所以一般安装了新的so,都会运行一遍ldconfig。</p>
<h3>8.5 系统的共享库路径</h3>
<p><strong>Linux尊崇FHS(File Hierarchy Standard)标准,来规定系统文件是如何存放的。</strong></p>
<ul>
<li>/lib:存放最关键的基础共享库,比如动态链接器、C语言运行库、数学库,都是/bin,/sbin里系统程序用到的库;</li>
<li>/usr/lib: 一般都是一些开发用到的 devel库;</li>
<li>/usr/local/lib:一般都是一些第三方库,GNU标准推荐第三方的库安装到这个目录下。</li>
</ul>
<p>另外/usr目录不是user的意思,而是“unix system resources”的缩写。</p>
<blockquote>/usr:/usr 是系统核心所在,包含了所有的共享文件。它是 unix 系统中最重要的目录之一,涵盖了二进制文件、各种文档、头文件、库文件;还有诸多程序,例如 ftp,telnet 等等。</blockquote>
<h2>九、后记</h2>
<p>研究这个话题,前前后后经历了一个月,文章只是把过程中的体会记录下来,同时在单位给同事们做了一次分享。虽然也只是浮光掠影,但终究是了结了多年的心愿,对可执行文件的格式、加载等基础知识做了一次梳理,还是收获满满的。这些知识对实际的工作有什么帮助吗?可能会有帮助,但可能也非常有限。“行无用之事,做时间的朋友”,做一些有意思的事情,过程本身就充满了乐趣。</p>
<p>文章可能会有纰漏和错误,能看到这里的同学,也请留言指出来,一起讨论学习,共同进步!</p>
<h3>参考</h3>
<ul>
<li>南京大学-袁春风老师-计算机系统基础</li>
<li>深入浅出计算机组成原理-极客时间</li>
<li>《程序是怎样跑起来的》</li>
<li>《程序员的自我修养》</li>
<li>《深入理解计算机系统》</li>
<li><a href="https://link.segmentfault.com/?enc=sGaB80z7mgYIC%2BGtVhwj6Q%3D%3D.eG4KMo8cOuKCmyih9%2BGPpEF%2Fv%2BSzRf0ROEsqLLhe9%2B8%3D" rel="nofollow">readlf、nm、ld、objdump、ldconfig、gcc命令</a></li>
</ul>
<blockquote>文章来源:宜信技术学院 & 宜信支付结算团队技术分享第14期-支付结算机器学习技术团队负责人 刘创 分享《程序的一生:从源程序到进程的辛苦历程》<p>分享者:宜信支付结算机器学习技术团队负责人 刘创</p>
<p>原文发布于个人博客:动物园的猪(www.piginzoo.com)</p>
</blockquote>
Serializable详解(1):代码验证Java序列化与反序列化
https://segmentfault.com/a/1190000021979090
2020-03-11T12:34:19+08:00
2020-03-11T12:34:19+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
3
<blockquote>说明:本文为Serializable详解(1),最后两段内容在翻译上出现歧义(暂时未翻译),将在后续的Serializable(2)文中补充。<p>介绍:本文根据JDK英文文档翻译而成,本译文并非完全按照原文档字面文字直译,而是结合文档内容及个人经验翻译成更为清晰和易于理解的文字,并附加代码验证,帮助大家更好地理解Serializable。</p>
</blockquote>
<p><strong>性质:接口类</strong></p>
<p>package java.io</p>
<p>public interface Serializable</p>
<h3>1.1 翻译文档</h3>
<blockquote>Serializability of a class is enabled by the class implementing the java.io.Serializable interface.</blockquote>
<p>通过实现java.io.Serializable interface接口来序列化一个类。</p>
<blockquote>Classes that do not implement this interface will not have any of their state serialized or deserialized.</blockquote>
<p>没有实现此接口的类任何状态都不会序列化或反序列化。</p>
<blockquote>All subtypes of a serializable class are themselves serializable.</blockquote>
<p>可序列化类的所有子类而本身都是可序列化的。</p>
<blockquote>The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.</blockquote>
<p>序列化接口没有方法或字段域,它仅用来标识可序列化的语义。</p>
<blockquote>(1)To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype's public, protected, and (if accessible) package fields. <p>(2)The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.</p>
<p>(3)During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.</p>
</blockquote>
<p>(1)为了让非序列化类的子类可以被序列化,这个子类可以承担保存和恢复超类的pulic,protected,package字段(如果可访问的话)。</p>
<p>(2)只有当它拓展的类具有可访问无参构造函数来初始化类的状态时,子类才可以承担这样的责任。如果不是这种情况,就不能声明一个类是可序列化的。这个错误将在运行的时候被检测出来。</p>
<p>(3)在反序列化期间,非序列化类的字段将通过类的以public或者protected修饰的空参构造函数实例化。无参数构造函数必须可访问可序列化的子类。序列化子类的字段能够从字符流里被还原。</p>
<h3>1.2 辅助理解</h3>
<p>(1)(2)(3)三块主要说了三件事:</p>
<ul>
<li>非序列化的父类,其子类实现序列化时承担保存和恢复父类public、protected、package等子类可访问到子类的字段;</li>
<li>非序列化的父类,其子类进行序列化时,父类需要有用public或者protected修饰的空参构造函数;</li>
<li>若无空参构造函数的父类,其子类在运行序列化时将正常进行,但反序列化时会发生错误,并抛出异常。但父类有空参构造函数,子类完成序列化,父类属性却没有参与到序列化中。</li>
</ul>
<h3>1.3 注意:此处有三个坑。</h3>
<ul>
<li>(1)中所述父类未实现序列化,实现序列化的子类会承担保存和恢复父类的public、protected、package等子类可访问到子类的字段。此处我个人理解为实现序列化的子类进行序列化的时候继承了未实现序列化的父类中子类可访问到的属性,但序列化时无法记录下父类对象的状态信息;</li>
<li>此处文档若要正确读取理解,切记(1)(2)(3)不可拆分,要放在一起去理解(上文之所以分开是便于翻译);</li>
<li>此处英文翻译成汉字,难以理解其真实含义,所以通过下面的代码验证来辅助理解</li>
</ul>
<h3>1.4 代码验证</h3>
<p>辅以A/B两套类型代码对比理解:</p>
<h4>1)A套</h4>
<p>父类:Biology 类</p>
<pre><code>package com.springboot.SpringBootDemo.serializable;
public class Biology {
public String type;
private int num;
public Biology(String type, int num) {
this.type = type;
this.num = num;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}</code></pre>
<p>子类:People 类</p>
<pre><code>package com.springboot.SpringBootDemo.serializable;
import java.io.Serializable;
public class People extends Biology implements Serializable{
private static final long serialVersionUID = -6623611040000763479L;
public String name;
protected String gender;
private int age;
public People(String type, int num, String name ,String gender ,int age) {
super(type, num);
this.name = name;
this.gender = gender;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}</code></pre>
<p>测试类:</p>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
People pp = new People("human",10000,"张三","男",25);
FileOutputStream fos = new FileOutputStream("test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(pp);
oos.flush();
oos.close();
//反序列化
FileInputStream sfis = new FileInputStream("test.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
People p = (People) sois.readObject();
System.out.println(
p.getType() +" "+
p.getNum() +" "+
p.getName() +" "+
p.getGender() +" "+
p.getAge()
);
}
}</code></pre>
<p>结果:</p>
<pre><code> Exception in thread "main" java.io.InvalidClassException: com.springboot.SpringBootDemo.serializable.People; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.springboot.SpringBootDemo.serializable.Test.main(Test.java:23)</code></pre>
<p>结果说明:在序列化时未发生异常,而在反序列化readObject()时发生异常。也就是说,父类没有无参构造函数时,序列化正常进行,但反序列化时抛出newInvalidClassException异常。</p>
<h4>2)B套</h4>
<p>父类:Person类</p>
<pre><code> public class Person {
public String name;
public String gender;
public int age;
float height;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}</code></pre>
<p>子类:Male类</p>
<pre><code> import java.io.Serializable;
public class Male extends Person implements Serializable{
/**
*
*/
private static final long serialVersionUID = -7361904256653535728L;
public boolean beard;
protected String weight;
public boolean havaBeard(int age){
boolean flag = false;
if(age>=18){
flag = true;
}
return flag;
}
public boolean isBeard() {
return beard;
}
public void setBeard(boolean beard) {
this.beard = beard;
}
public String getWeight() {
return weight;
}
public void setWeight(String weight) {
this.weight = weight;
}
}</code></pre>
<p>测试类:</p>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SubTypeSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException{
/**Male继承父类Person,自身实现序列化接口,其父类Person没有实现序列化接口*/
FileOutputStream fos = new FileOutputStream("male.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Male male = new Male();
/**
* 其父类的父类Person的属性
*
* public String name;
public String gender;
public int age;
float height;
* */
male.setName("张三");
male.setGender("男性");
male.setAge(25);
male.setHeight(175);
/**
* 其自身属性
* public boolean beard;
* */
male.setBeard(true);
oos.writeObject(male);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("male.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Male ml = (Male) ois.readObject();
System.out.println(ml.getName() +" "+ml.getGender()+" "+ml.getHeight() +" "+ml.getAge()+" "+male.isBeard());
}
} </code></pre>
<p>结果:</p>
<pre><code> ml.getName() == null
ml.getGender() == null
ml.getHeight() == 0.0
ml.getAge() == 0
male.isBeard() == true</code></pre>
<h3>1.5 测试分析</h3>
<h4>1)父类属性</h4>
<p>public String name;</p>
<p>public String gender;</p>
<p>public int age;</p>
<p>float height;</p>
<p>其状态信息均未被记录;</p>
<h4>2)自身属性</h4>
<p>public boolean beard;</p>
<p>其状态信息被记录</p>
<h3>2.1 翻译文档</h3>
<blockquote>When traversing a graph, an object may be encountered that does not support the Serializable interface. In this case the NotSerializableException will be thrown and will identify the class of the non-serializable object.</blockquote>
<p>当循环遍历一个数据结构图(数据结构图可理解为数据结构类型,比如二叉树)的时候,对象可能会遭遇到不支持实现序列化接口的情景。在这种情况下,将发生抛出NotSerializableException异常,并且该类被定义为不可序列化类。</p>
<blockquote>Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures:</blockquote>
<p>在实现序列化和反序列化过程中,特殊处理的类需要实现这些特殊的方法:</p>
<pre><code> private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;</code></pre>
<blockquote>The writeObject method is responsible for writing the state of the object for its particular class so that the corresponding readObject method can restore it. The default mechanism for saving the Object's fields can be invoked by calling out.defaultWriteObject. The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the writeObject method or by using the methods for primitive data types supported by DataOutput.</blockquote>
<p>witeObject方法负责写入特定类的Object对象状态信息,readObject方法可以还原该Object对象的状态信息。保存该Object对象字段的默认机制是通过调用out.defaultWriteObject来实现。该方法不需要关注属于其超类或子类的状态。通过使用writeObject方法将各个字段写入ObjectOutputStream,或使用DataOutput支持的基本数据类型的方法来保存状态。</p>
<blockquote>The readObject method is responsible for reading from the stream and restoring the classes fields. It may call in.defaultReadObject to invoke the default mechanism for restoring the object's non-static and non-transient fields. The defaultReadObject method uses information in the stream to assign the fields of the object saved in the stream with the correspondingly named fields in the current object. This handles the case when the class has evolved to add new fields. The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the writeObject method or by using the methods for primitive data types supported by DataOutput.</blockquote>
<p>readObject方法是负责读取数据流并恢复该类的字段。它可以通过调用in.defaultReadObject来恢复非static以及非transient修饰的字段。defaultReadObject方法通过数据流中的信息,把当前类保存在数据流中的字段信息分配到相对应的字段名(也就是说把字段的值分配给相对应的字段名)。这种处理方式也能处理该类新增字段的情况。该方法不需要关注属于其超类或子类的状态。通过使用writeObject方法将各个字段写入ObjectOutputStream,或使用DataOutput支持的基本数据类型的方法来保存状态。通过writeObject方法把对象Object的各个字段写入到ObjectOutputStream中,或者通过使用DataOutput支持的基本数据类型的方法来保存该Object对象的状态信息。</p>
<blockquote>The readObjectNoData method is responsible for initializing the state of the object for its particular class in the event that the serialization stream does not list the given class as a superclass of the object being deserialized. This may occur in cases where the receiving party uses a different version of the deserialized instance's class than the sending party, and the receiver's version extends classes that are not extended by the sender's version. This may also occur if the serialization stream has been tampered; hence, readObjectNoData is useful for initializing deserialized objects properly despite a "hostile" or incomplete source stream.</blockquote>
<p>(该处翻译有些吃力,所以直接软件翻译,会后续进行代码验证体悟)</p>
<p>当出现反序列化与序列化类的版本不一致的情况时,readObjectNoData()标签方法负责初始化对象的字段值。这种情况可能发生在反序列化时,接收方使用了发送方对象的类的不同版本,或者接收方继承的类的版本与发送方继承的类的版本不一致。另外,当序列化流被篡改了,也会发生这种情况。因此,当出现类不一致或者反序列化流不完全的情况时,readObjectNoData初始化反序列化对象的字段就非常有用了。</p>
<h3>2.2 代码验证</h3>
<h4>1)改变之前</h4>
<pre><code> public class Cat implements Serializable{
/**
*
*/
private static final long serialVersionUID = -5731096200028489933L;
public String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}</code></pre>
<p>改变之前测试类:</p>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class CatFamilylTest {
public static void main(String[] args) throws Exception {
serializable();
deSerializable();
}
public static void serializable() throws Exception{
Cat cat = new Cat();
cat.setColor("white");
FileOutputStream fos = new FileOutputStream("catFamily.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(cat);
oos.flush();
oos.close();
}
public static void deSerializable() throws Exception{
FileInputStream sfis = new FileInputStream("catFamily.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Cat cat = (Cat) sois.readObject();
System.out.println(cat.getColor());
}
}</code></pre>
<p>结果:white</p>
<h4>2)第一次改变</h4>
<p>第一次改变之增加父类:</p>
<pre><code> import java.io.Serializable;
public class CatFamily implements Serializable{
/**
*
*/
private static final long serialVersionUID = -7796480232179180594L;
public String catType;
public String getCatType() {
return catType;
}
public void setCatType(String catType) {
this.catType = catType;
}
private void readObjectNoData() {
this.catType = "tiger";
}
}</code></pre>
<p>第一次改变之后之Cat类变化:</p>
<pre><code> public class Cat extends CatFamily{
/**
*
*/
private static final long serialVersionUID = -5731096200028489933L;
public String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}</code></pre>
<p>第一次改变之读取已经存在的catFamily.txt文件:</p>
<pre><code> public class CatFamilylTest {
public static void main(String[] args) throws Exception {
deSerializable();
}
public static void deSerializable() throws Exception{
FileInputStream sfis = new FileInputStream("catFamily.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Cat cat = (Cat) sois.readObject();
System.out.println(cat.getColor()+" <---->"+cat.getCatType());
}
}</code></pre>
<p>第一次改变之结果:white <---->tiger</p>
<h4>3)第二次改变测试</h4>
<p>第二次改变之父类:</p>
<pre><code> public class CatFamily{
public String catType;
public String getCatType() {
return catType;
}
public void setCatType(String catType) {
this.catType = catType;
}
private void readObjectNoData() {
this.catType = "tiger";
}
}</code></pre>
<p>第二次改变之Cat类:</p>
<pre><code> import java.io.Serializable;
public class Cat extends CatFamily implements Serializable{
/**
*
*/
private static final long serialVersionUID = -5731096200028489933L;
public String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}</code></pre>
<p>第二次改变之测试类:</p>
<pre><code> public class CatFamilylTest {
public static void main(String[] args) throws Exception {
deSerializable();
}
public static void deSerializable() throws Exception{
FileInputStream sfis = new FileInputStream("catFamily.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Cat cat = (Cat) sois.readObject();
System.out.println(cat.getColor()+" <---->"+cat.getCatType());
}
}</code></pre>
<p>第二次改变之结果:white <---->null</p>
<h4>4)第三次改变举例对比验证</h4>
<p>第三次改变之抛弃父类,且Cat类改变:</p>
<pre><code> import java.io.Serializable;
public class Cat implements Serializable{
/**
*
*/
private static final long serialVersionUID = -5731096200028489933L;
public String type;
public String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
private void readObjectNoData() {
this.type = "hellokitty";
}
}</code></pre>
<p>第三次改变之测试类:</p>
<pre><code> public class CatFamilylTest {
public static void main(String[] args) throws Exception {
deSerializable();
}
public static void deSerializable() throws Exception{
FileInputStream sfis = new FileInputStream("catFamily.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Cat cat = (Cat) sois.readObject();
System.out.println(cat.getColor()+" <---->"+cat.getType());
}
}</code></pre>
<p>第三次改变之测试结果:white <---->null</p>
<h3>2.3 测试代码描述</h3>
<h4>1)第一种(改变之前)</h4>
<p>描述:建立实现序列化接口的Cat类,以及对应的测试类生产文件catFamily.txt</p>
<p>两个目的:</p>
<ul>
<li>用于建立变化的基础层代码;</li>
<li>生成序列化后的文件。</li>
</ul>
<p>结果:反序列化catFamily.txt文件,得出正常结果 write 。</p>
<h4>2)第二种(第一次改变对比未改变)</h4>
<p>改变之处:</p>
<ul>
<li>增加实现序列化接口的父类CatFamily类,增添readObjectNoData()方法,并且设置属性字段catType值为tiger;</li>
<li>Cat类不直接实现序列化Serializable接口,而是继承CatFamily类;</li>
<li>测试类对catFamily.txt进行反序列化读取。</li>
</ul>
<p>目的:验证readObjectNoData()标签方法结果。</p>
<p>结果:反序列化catFamily.txt文件,得出结果 white <---->tiger。</p>
<p>总结:实现readObjectNoData()标签方法。</p>
<h4>3)第三种(第二次改变对比第一次改变)</h4>
<p>改变之处:</p>
<ul>
<li>改变父类CatFamily类,去掉实现序列化Serializable接口;</li>
<li>子类Cat类依然继承父类CatFamily类,并且直接实现序列化Serializable接口;</li>
<li>测试类对catFamily.txt进行反序列化读取。</li>
</ul>
<p>目的:验证父类未实现序列化Serializable接口时,readObjectNoData()方法是否继续有效。</p>
<p>结果:反序列化catFamily.txt文件,得出结果 white <---->null 。</p>
<p>总结:readObjectNoData()方法没有得到体现。</p>
<h4>4)第四种(第三次改变对比未改变)</h4>
<p>改变之处:</p>
<ul>
<li>Cat类去掉父类CatFamily类,自身直接实现序列化Serializable接口;</li>
<li>Cat类实现readObjectNoData()方法;</li>
<li>测试类对catFamily.txt进行反序列化读取。</li>
</ul>
<p>目的:测试readObjectNoData()方法的作用域。</p>
<p>结果:反序列化catFamily.txt文件,得出结果 white <---->null。</p>
<p>总结:readObjectNoData()方法作用域为写入catFamily.txt文件的对象Object的实体类的实现序列化Serializable接口的父类。</p>
<h3>2.4 推测总结:</h3>
<ul>
<li>readObjectNoData()标签方法作用域为进行序列化对象的父类,并且其父类必须实现了序列化接口Serializable;</li>
<li>readObjectNoData()标签方法在上面测试的代码中体现作用类似于set属性;</li>
<li>readObjectNoData()标签方法内set的属性值为该类的属性值,也就是说当引用其他对象属性值进行set时,该方法是无效的。</li>
</ul>
<h3>3.1 翻译文档</h3>
<blockquote>Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;</blockquote>
<p>实现序列化的类,其Object对象被指定另外一个实现序列化非此类的对象进行替换的时候,在进行把该实体Object对象写入到数据流中时,需要实现Object writeReplace() throws ObjectStreamException;这个特殊的标签方法。</p>
<h3>3.2 代码验证</h3>
<p>注意:替换类和被替换类都需要实现序列化接口,否则在写入(writeObject)时会抛出java.io.NotSerializableException异常,且被替换类为彻底被替换。</p>
<h4>1)测试writeReplace()标签方法</h4>
<p>实体类:</p>
<pre><code> import java.io.ObjectStreamException;
import java.io.Serializable;
public class Dog implements Serializable{
/**
*
*/
private static final long serialVersionUID = -4094903168892128473L;
private String type;
private String color;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
private Object writeReplace() throws ObjectStreamException {
Wolf wolf = new Wolf();
wolf.setType(type);
wolf.setColor(color);
return wolf;
}
}
class Wolf implements Serializable{
/**
*
*/
private static final long serialVersionUID = -1501152003733531169L;
private String type;
private String color;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}</code></pre>
<p>测试类:</p>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class DogTest {
public static void serializable() throws IOException{
Dog dog = new Dog();
dog.setColor("white");
dog.setType("Chinese garden dog");
FileOutputStream fos = new FileOutputStream("dog.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dog);
oos.flush();
oos.close();
}
public static void deSerializable() throws IOException,ClassNotFoundException{
FileInputStream sfis = new FileInputStream("dog.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Wolf wolf = (Wolf) sois.readObject();
System.out.println(wolf.getType() +"<------->"+ wolf.getColor());
}
public static void main(String[] args) throws IOException ,ClassNotFoundException{
serializable();
deSerializable();
}
}</code></pre>
<p>代码实现结果:Chinese garden dog<------->white。</p>
<h4>2)测试是否被彻底替换</h4>
<p>代码说明:实体类不修改,只修改测试类的反序列化方法,在readObject()方法时由Wolf对象转变为Dog对象。</p>
<pre><code> public static void deSerializable() throws IOException,ClassNotFoundException{
FileInputStream sfis = new FileInputStream("dog.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Dog dog = (Dog) sois.readObject();
System.out.println(dog.getType() +"<------->"+ dog.getColor());
}</code></pre>
<p>代码实现结果:</p>
<pre><code> (
第25行:Dog dog = (Dog) sois.readObject();
第32行:deSerializable();
)
Exception in thread "main" java.lang.ClassCastException: com.springboot.SpringBootDemo.serializable.Wolf cannot be cast to com.springboot.SpringBootDemo.serializable.Dog
at com.springboot.SpringBootDemo.serializable.DogTest.deSerializable(DogTest.java:25)
at com.springboot.SpringBootDemo.serializable.DogTest.main(DogTest.java:32)</code></pre>
<p>序列化对象为Dog对象,而反序列化依然通过Dog对象,结果发生异常,此时可知在序列化时Dog对象被Wolf对象给替换了。</p>
<h3>4.1 翻译文档</h3>
<blockquote>This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private,protected and package-private access. Subclass access to this method follows java accessibility rules.</blockquote>
<p>在序列化对象时,其类的方法中如果有writeReplace标签方法存在的话,则该标签方法会在序列化写入时被调用。因此该方法可以具有private,protected和package-private访问。该类的子类访问该方法时会遵循java可访问性规则。</p>
<h3>4.2 代码验证</h3>
<p>注意:</p>
<ul>
<li>父类实现writeReplace标签方法;</li>
<li>子类拥有访问writeReplace标签方法的权限。</li>
</ul>
<h4>1)实体类</h4>
<pre><code> import java.io.ObjectStreamException;
import java.io.Serializable;
public class Dog implements Serializable{
/**
*
*/
private static final long serialVersionUID = -4094903168892128473L;
private String type;
private String color;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Object writeReplace() throws ObjectStreamException {
Wolf wolf = new Wolf();
wolf.setType(type);
wolf.setColor(color);
return wolf;
}
}
class ChineseGardenDog extends Dog {
private float height;
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}
class Wolf implements Serializable{
/**
*
*/
private static final long serialVersionUID = -1501152003733531169L;
private String type;
private String color;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}</code></pre>
<h4>2)测试类</h4>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class DogTest {
public static void serializable() throws IOException{
ChineseGardenDog dog = new ChineseGardenDog();
dog.setColor("white");
dog.setType("Chinese garden dog");
dog.setHeight(55);
FileOutputStream fos = new FileOutputStream("dog.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dog);
oos.flush();
oos.close();
}
public static void deSerializable() throws IOException,ClassNotFoundException{
FileInputStream sfis = new FileInputStream("dog.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Wolf wolf = (Wolf) sois.readObject();
System.out.println(wolf.getType() +"<------->"+ wolf.getColor());
}
public static void main(String[] args) throws IOException ,ClassNotFoundException{
serializable();
deSerializable();
}
}</code></pre>
<p>测试结果:Chinese garden dog<------->white。</p>
<h3>5.1 翻译文档</h3>
<blockquote>Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.<p>ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;</p>
<p>This readResolve method follows the same invocation rules and accessibility rules as writeReplace.</p>
</blockquote>
<p>当从数据流中读取一个实例的时候,指定替换的类需要实现此特殊方法。</p>
<p>ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;</p>
<p>readResolve标签方法遵循与writeReplace相同的调用规则和可访问性规则。</p>
<h3>5.2 代码验证</h3>
<p>注:该方法的写入对象实例和读取对象实例为同一个对象(适用于单例模式)。</p>
<h4>1)未实现标签方法</h4>
<p>实体类:</p>
<pre><code> import java.io.Serializable;
public class Mouse implements Serializable{
/**
*
*/
private static final long serialVersionUID = -8615238438948214201L;
private String name;
public static Mouse INSTANCE;
public static Mouse getInstance(){
if(INSTANCE == null){
INSTANCE = new Mouse();
}
return INSTANCE;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}</code></pre>
<p>测试类:</p>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class MouseTest {
public static void serializable() throws IOException{
Mouse mouse= Mouse.getInstance();
mouse.setName("Jerry");
FileOutputStream fos = new FileOutputStream("mouse.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
System.out.println("写入对象hash值 = "+ mouse.hashCode());
oos.writeObject(mouse);
oos.flush();
oos.close();
}
public static void deSerializable() throws IOException,ClassNotFoundException{
FileInputStream sfis = new FileInputStream("mouse.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Mouse mouse = (Mouse) sois.readObject();
System.out.println("读取对象hash值 = " +mouse.hashCode());
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializable();
deSerializable();
}
}</code></pre>
<p>测试结果:</p>
<p>写入对象hash值 = 366712642</p>
<p>读取对象hash值 = 1096979270</p>
<h4>2)实现标签方法:(测试类不变,实体类增加readResolve()方法)</h4>
<p>实体类:</p>
<pre><code> import java.io.ObjectStreamException;
import java.io.Serializable;
public class Mouse implements Serializable{
/**
*
*/
private static final long serialVersionUID = -8615238438948214201L;
private String name;
public static Mouse INSTANCE;
public static Mouse getInstance(){
if(INSTANCE == null){
INSTANCE = new Mouse();
}
return INSTANCE;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private Object readResolve() throws ObjectStreamException{
return INSTANCE;
}
}</code></pre>
<p>测试结果:</p>
<p>写入对象hash值 = 366712642</p>
<p>读取对象hash值 = 366712642</p>
<p>推测:指定写入的对象实例和读取指定的对象实例为同一个。</p>
<h3>6.1 翻译文档</h3>
<blockquote>The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender's class, then deserialization will result in an {@link InvalidClassException}. A serializable class can declare its own serialVersionUID explicitly by declaring a field named <code>"serialVersionUID"</code> that must be static, final, and of type <code>long</code>: <p>ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;</p>
<p>If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is <em>strongly recommended</em> that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected <code>InvalidClassException</code>s during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the <code>private</code> modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.</p>
</blockquote>
<h3>6.2 针对实现Serializable接口 代码验证</h3>
<p>父类:Person类</p>
<pre><code> public class Person {
public String name;
public String gender;
public int age;
float height;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}</code></pre>
<p>子类:Male类</p>
<pre><code> import java.io.Serializable;
public class Male extends Person implements Serializable{
/**
*
*/
private static final long serialVersionUID = -7361904256653535728L;
public boolean beard;
public boolean havaBeard(int age){
boolean flag = false;
if(age>=18){
flag = true;
}
return flag;
}
public boolean isBeard() {
return beard;
}
public void setBeard(boolean beard) {
this.beard = beard;
}
}</code></pre>
<p>三级子类:Students类</p>
<pre><code> public class Students extends Male{
private static final long serialVersionUID = -6982821977091370834L;
public String stuCard;
private int grades;
public String getStuCard() {
return stuCard;
}
public void setStuCard(String stuCard) {
this.stuCard = stuCard;
}
public int getGrades() {
return grades;
}
public void setGrades(int grades) {
this.grades = grades;
}
}</code></pre>
<p>类:Female类</p>
<pre><code> import java.io.Serializable;
public class Female implements Serializable{
private static final long serialVersionUID = 6907419491408608648L;
public String name;
public String gender;
public int age;
float height;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}</code></pre>
<p>测试类:SubTypeSerializable</p>
<pre><code> import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SubTypeSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException{
/**(一)、Person实体类,未实现序列化接口,无父类*/
FileOutputStream fo = new FileOutputStream("person.txt");
ObjectOutputStream ss = new ObjectOutputStream(fo);
Person person = new Person();
person.setAge(100);
person.setGender("性别");
person.setHeight(165);
person.setName("人类");
ss.writeObject(person);
ss.flush();
ss.close();
//反序列化
FileInputStream sfis = new FileInputStream("person.txt");
ObjectInputStream sois = new ObjectInputStream(sfis);
Person ps = (Person) sois.readObject();
System.out.println(ps.getName() +" "+ps.getGender()+" "+ps.getHeight() +" "+ps.getAge());
/**结果:
在执行writeObject(person)是发生异常
Exception in thread "main" java.io.NotSerializableException:
com.springboot.SpringBootDemo.serializable.Person
at java.io.ObjectOutputStream.writeObject0(Unknown Source)
at java.io.ObjectOutputStream.writeObject(Unknown Source)
at com.springboot.SpringBootDemo.serializable.SubTypeSerializable.main(SubTypeSerializable.java:21)
Exception in thread "main" java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: com.springboot.SpringBootDemo.serializable.Person
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.springboot.SpringBootDemo.serializable.SubTypeSerializable.main(SubTypeSerializable.java:28)
* */
System.out.println("<--------------------------------------------------------------------------->");
/**(二)、Male继承父类Person,自身实现序列化接口,其父类Person没有实现序列化接口*/
FileOutputStream fos = new FileOutputStream("male.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Male male = new Male();
/**
* 其父类的父类Person的属性
*
* public String name;
public String gender;
public int age;
float height;
* */
male.setName("张三");
male.setGender("男性");
male.setAge(25);
male.setHeight(175);
/**
* 其自身属性
* public boolean beard;
* */
male.setBeard(true);
oos.writeObject(male);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("male.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Male ml = (Male) ois.readObject();
System.out.println(ml.getName() +" "+ml.getGender()+" "+ml.getHeight() +" "+ml.getAge()+" "+male.isBeard());
/**结果:
* 父类没有被序列化,唯独子类属性被序列化
*
* ml.getName() == null
* ml.getGender() == null
* ml.getHeight() == 0.0
* ml.getAge() == 0
* male.isBeard() == true
* 父类属性:
* public String name;
public String gender;
public int age;
float height;
均未实现序列化;
自身属性:
public boolean beard;
实现序列化
* */
System.out.println("<--------------------------------------------------------------------------->");
/**(三)、Female实现序列化接口,无父类*/
FileOutputStream ffos = new FileOutputStream("female.txt");
ObjectOutputStream foos = new ObjectOutputStream(ffos);
Female female = new Female();
/**
* 其自身的属性
* public String name;
public String gender;
public int age;
float height;
**/
female.setAge(25);
female.setGender("女性");
female.setHeight(165);
female.setName("张芳");
foos.writeObject(female);
foos.flush();
foos.close();
//反序列化
FileInputStream ffis = new FileInputStream("female.txt");
ObjectInputStream fois = new ObjectInputStream(ffis);
Female fm = (Female) fois.readObject();
System.out.println(fm.getName() +" "+fm.getGender()+" "+fm.getHeight() +" "+fm.getAge());
/**结果:
* 自身属性均实现序列化
*
* fm.getName() == 张芳
* fm.getGender() == 女性
* fm.getHeight() == 165.0
* fm.getAge() == 25
* 所有属性均实现序列化*/
System.out.println("<--------------------------------------------------------------------------->");
/**(四)、Students未实现序列化接口,继承父类Male,其父类继承父类Person,自身实现序列化接口,其父类Person没有实现序列化接口*/
FileOutputStream stufos = new FileOutputStream("students.txt");
ObjectOutputStream stuoos = new ObjectOutputStream(stufos);
Students students = new Students();
/**
* 其父类的父类Person的属性
*
* public String name;
public String gender;
public int age;
float height;
* */
students.setName("王小明");
students.setGender("男性");
students.setAge(15);
students.setHeight(160);
/**
* 其父类Male属性
* public boolean beard;
* */
students.setBeard(true);
/**
* 自身属性
* public String stuCard;
private int grades;
* */
students.setStuCard("1234567890987");
students.setGrades(300);
stuoos.writeObject(students);
stuoos.flush();
stuoos.close();
//反序列化
FileInputStream stufis = new FileInputStream("students.txt");
ObjectInputStream stuois = new ObjectInputStream(stufis);
Students st = (Students) stuois.readObject();
System.out.println(st.getName() +" "+st.getGender()+" "+st.getAge()+" "+st.getHeight()+" "+st.isBeard()+" "+st.getStuCard()+" "+st.getGrades());
/**结果:
* 父类的父类属性未实现序列化,父类实现序列化,自身实现序列化
* st.getName() == null
* st.getGender() == null
* st.getAge() == 0
* st.getHeight() == 0.0
* st.isBeard() == true
* st.getStuCard() == 1234567890987
* st.getGrades() == 300
* 自身public String stuCard;
private int grades;
实现序列化;
而父类Male属性
public boolean beard
实现序列化;
父类的父类Person
public String name;
public String gender;
public int age;
float height;
未实现序列化
* */
}
}</code></pre>
<h3>6.3 回顾</h3>
<p>1)在使用ObjectInputStream、ObjectOutputStream对对象进行写入写出时,其写入的对象的类需要实现java.io.Serializable序列化接口,否则会报出 writeObject()异常:</p>
<pre><code> Exception in thread "main" java.io.NotSerializableException: com.springboot.SpringBootDemo.serializable.Person
at java.io.ObjectOutputStream.writeObject0(Unknown Source)
at java.io.ObjectOutputStream.writeObject(Unknown Source)
at com.springboot.SpringBootDemo.serializable.SubTypeSerializable.main(SubTypeSerializable.java:21)
readObject()异常:
Exception in thread "main" java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: com.springboot.SpringBootDemo.serializable.Person
java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.springboot.SpringBootDemo.serializable.SubTypeSerializable.main(SubTypeSerializable.java:28)</code></pre>
<p>2)父类未实现java.io.Serializable序列化接口,其子类依然可以进行序列化,但其子类进行对象序列化读写时,父类无法被序列化,只能自身实现序列化;</p>
<p>3)自身实现java.io.Serializable序列化接口,在进行对象读写时会被实现序列化;</p>
<p>4)父类实现java.io.Serializable序列化接口,其子类不需要再次申明实现序列化,子类在进行对象序列化读写时,父类和子类均被实现序列化。</p>
<h3>7.1 总结</h3>
<h4>1)java.io.Serializable接口</h4>
<p>首先,Serializable类是一个接口,所以对象的序列化并不是Serializable来实现的;</p>
<p>其次,Serializable是一个标签,各种序列化类在读取到这个标签的时候,会按照自己的方式进行序列化。</p>
<h4>2)序列化是干什么的,为什么需要序列化</h4>
<p>我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等,而这些数据都会以二进制序列的形式在网络上传送。</p>
<p>那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!</p>
<p>换句话说:一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。</p>
<p>当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。</p>
<ul>
<li>实现了数据的持久化,通过序列化可以记录下数据结构或者对象的状态(也就是实体变量Object[注:不是Class]某一个时间点的值),把数据临时或者永久地保存到硬盘上(通常存放在文件里);</li>
<li>利用序列化实现远程通信,并且在数据传递过程中或者使用时能够保证数据结构或者对象状态的完整性和可传递性,即在网络上传送对象的字节序列。</li>
</ul>
<blockquote>作者:忠胜<p>首发:「野指针」</p>
<p>来源:宜信技术学院</p>
</blockquote>
关于Java序列化的问题你真的会吗?
https://segmentfault.com/a/1190000021967784
2020-03-10T12:35:29+08:00
2020-03-10T12:35:29+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
4
<h2>引言</h2>
<p>在持久化数据对象的时候我们很少使用Java序列化,而是使用数据库等方式来实现。但是在我看来,Java 序列化是一个很重要的内容,序列化不仅可以保存对象到磁盘进行持久化,还可以通过网络传输。在平时的面试当中,序列化也是经常被谈及的一块内容。</p>
<p>谈到序列化时,大家可能知道将类实现Serializable接口就可以达到序列化的目的,但当看到关于序列化的面试题时我们却常常一脸懵逼。 </p>
<p>1)可序列化接口和可外部接口的区别是什么?</p>
<p>2)序列化时,你希望某些成员不要序列化?该如何实现?</p>
<p>3)什么是 serialVersionUID ?如果不定义serialVersionUID,会发生什么?</p>
<p>是不是突然发现我们对这些问题其实都还存在很多疑惑?本文将总结一些Java序列化的常见问题,并且通过demo来进行测试和解答。</p>
<h3>问题一:什么是 Java 序列化?</h3>
<p><strong>序列化是把对象改成可以存到磁盘或通过网络发送到其它运行中的 Java 虚拟机的二进制格式的过程</strong>,并可以通过反序列化恢复对象状态。Java 序列化API给开发人员提供了一个标准机制:通过实现 java.io.Serializable 或者 java.io.Externalizable 接口,ObjectInputStream 及ObjectOutputStream 处理对象序列化。实现java.io.Externalizable 接口的话,Java 程序员可自由选择基于类结构的标准序列化或是它们自定义的二进制格式,通常认为后者才是最佳实践,因为序列化的二进制文件格式成为类输出 API的一部分,可能破坏 Java 中私有和包可见的属性的封装。</p>
<p>序列化到底有什么用?</p>
<p>实现 java.io.Serializable。</p>
<p>定义用户类:</p>
<pre><code>class User implements Serializable {
private String username;
private String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}</code></pre>
<p>我们把对象序列化,通过ObjectOutputStream存储到txt文件中,再通过ObjectInputStream读取txt文件,反序列化成User对象。</p>
<pre><code>public class TestSerialize {
public static void main(String[] args) {
User user = new User();
user.setUsername("hengheng");
user.setPasswd("123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}</code></pre>
<p>运行结果如下:</p>
<pre><code>序列化前数据:
username: hengheng
password: 123456
序列化后数据:
username: hengheng
password: 123456</code></pre>
<p>到这里,我们大概知道了什么是序列化。</p>
<h3>问题二:序列化时,你希望某些成员不要序列化,该如何实现?</h3>
<p>答案:声明该成员为静态或瞬态,在 Java 序列化过程中则不会被序列化。</p>
<ul>
<li>
<strong>静态变量</strong>:加static关键字。</li>
<li>
<strong>瞬态变量:</strong> 加transient关键字。</li>
</ul>
<p>我们先尝试把变量声明为瞬态。</p>
<pre><code>class User implements Serializable {
private String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}</code></pre>
<p>在密码字段前加上了<strong>transient</strong>关键字再运行。运行结果:</p>
<pre><code>序列化前数据:
username: hengheng
password: 123456
序列化后数据:
username: hengheng
password: null</code></pre>
<p>通过运行结果发现密码没有被序列化,达到了我们的目的。</p>
<p>再尝试在用户名前加<strong>static</strong>关键字。</p>
<pre><code>class User implements Serializable {
private static String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}</code></pre>
<p>运行结果:</p>
<pre><code>序列化前数据:
username: hengheng
password: 123456
序列化后数据:
username: hengheng
password: null</code></pre>
<p>我们发现运行后的结果和预期的不一样,按理说username也应该变为null才对。是什么原因呢?</p>
<p>原因是:反序列化后类中static型变量username的值为当前JVM中对应的静态变量的值,而不是反序列化得出的。</p>
<p>我们来证明一下:</p>
<pre><code>public class TestSerialize {
public static void main(String[] args) {
User user = new User();
user.setUsername("hengheng");
user.setPasswd("123456");
System.out.println("序列化前数据: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
User.username = "小明";
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\n序列化后数据: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User implements Serializable {
public static String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}</code></pre>
<p>在反序列化前把静态变量username的值改为『小明』。</p>
<pre><code>User.username = "小明";</code></pre>
<p>再运行一次:</p>
<pre><code>序列化前数据:
username: hengheng
password: 123456
序列化后数据:
username: 小明
password: null</code></pre>
<p>果然,这里的username是JVM中静态变量的值,并不是反序列化得到的值。</p>
<h3>问题三:serialVersionUID有什么用?</h3>
<p>我们经常会在类中自定义一个serialVersionUID:</p>
<pre><code>private static final long serialVersionUID = 8294180014912103005L</code></pre>
<p>这个serialVersionUID有什么用呢?如果不设置的话会有什么后果?</p>
<p>serialVersionUID 是一个 private static final long 型 ID,当它被印在对象上时,它通常是对象的哈希码。serialVersionUID可以自己定义,也可以自己去生成。</p>
<p>不指定 serialVersionUID的后果是:当你添加或修改类中的任何字段时,已序列化类将无法恢复,因为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化的过程是依赖于正确的序列化对象恢复状态的,并在序列化对象序列版本不匹配的情况下引发 java.io.InvalidClassException 无效类异常。</p>
<p>举个例子大家就明白了:</p>
<p>我们保持之前保存的序列化文件不变,然后修改User类。</p>
<pre><code>class User implements Serializable {
public static String username;
private transient String passwd;
private String age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}</code></pre>
<p>加了一个属性age,然后单另写一个反序列化的方法:</p>
<pre><code>public static void main(String[] args) {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
User user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\n修改User类之后的数据: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}</code></pre>
<p><img src="/img/remote/1460000021967787" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>报错了,我们发现之前的User类生成的serialVersionUID和修改后的serialVersionUID不一样(因为是通过对象的哈希码生成的),导致了InvalidClassException异常。</p>
<p>自定义serialVersionUID:</p>
<pre><code>class User implements Serializable {
private static final long serialVersionUID = 4348344328769804325L;
public static String username;
private transient String passwd;
private String age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}</code></pre>
<p>再试一下:</p>
<pre><code>序列化前数据:
username: hengheng
password: 123456
序列化后数据:
username: 小明
password: null</code></pre>
<p>运行结果无报错,所以一般都要自定义serialVersionUID。</p>
<h3>问题四:是否可以自定义序列化过程?</h3>
<p>答案当然是可以的。</p>
<p>之前我们介绍了序列化的第二种方式:</p>
<p>实现Externalizable接口,然后重写writeExternal() 和readExternal()方法,这样就可以自定义序列化。</p>
<p>比如我们尝试把变量设为瞬态。</p>
<pre><code>public class ExternalizableTest implements Externalizable {
private transient String content = "我是被transient修饰的变量哦";
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
content = (String) in.readObject();
}
public static void main(String[] args) throws Exception {
ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et);
ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content);
out.close();
in.close();
}
}</code></pre>
<p>运行结果:</p>
<pre><code>我是被transient修饰的变量哦</code></pre>
<p>这里实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。</p>
<p>通过上述介绍,是不是对Java序列化有了更多的了解?</p>
<blockquote>作者:杨亨<p>来源:宜信技术学院</p>
</blockquote>
Dubbo源码解析之SPI(一):扩展类的加载过程
https://segmentfault.com/a/1190000021922646
2020-03-05T15:19:12+08:00
2020-03-05T15:19:12+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
5
<p>Dubbo是一款开源的、高性能且轻量级的Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用、智能容错和负载均衡,以及服务自动注册和发现。</p>
<p>Dubbo最早是阿里公司内部的RPC框架,于 2011 年开源,之后迅速成为国内该类开源项目的佼佼者,2018年2月,通过投票正式成为 Apache基金会孵化项目。目前宜信公司内部也有不少项目在使用Dubbo。</p>
<p>本系列文章通过拆解Dubbo源码,帮助大家了解Dubbo,做到知其然,并且知其所以然。</p>
<h2>一、JDK SPI</h2>
<h3>1.1 什么是SPI?</h3>
<p>SPI(Service Provider Interface),即服务提供方接口,是JDK内置的一种服务提供机制。在写程序的时候,一般都推荐面向接口编程,这样做的好处是:降低了程序的耦合性,有利于程序的扩展。</p>
<p>SPI也秉承这种理念,提供了统一的服务接口,服务提供商可以各自提供自己的具体实现。大家都熟知的JDBC中用的就是基于这种机制来发现驱动提供商,不管是Oracle也好,MySQL也罢,在编写代码时都一样,只不过引用的jar包不同而已。后来这种理念也被运用于各种架构之中,比如Dubbo、Eleasticsearch。</p>
<h3>1.2 JDK SPI的小栗子</h3>
<p>SPI 的实现方式是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类。 </p>
<p>了解了概念后,来看一个具体的例子。</p>
<p>1)定义一个接口</p>
<pre><code>public interface Operation {
int operate(int num1, int num2);
}</code></pre>
<p>2)写两个简单的实现</p>
<pre><code>public class DivisionOperation implements Operation {
public int operate(int num1, int num2) {
System.out.println("run division operation");
return num1/num2;
}
}</code></pre>
<p>3)添加一个配置文件</p>
<p>在ClassPath路径下添加一个配置文件,文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。</p>
<p>目录结构</p>
<p><img src="/img/remote/1460000021922653" alt="" title=""></p>
<p>文件内容</p>
<pre><code>com.api.impl.DivisionOperation
com.api.impl.PlusOperation</code></pre>
<p>4)测试程序</p>
<pre><code>public class JavaSpiTest {
@Test
public void testOperation() throws Exception {
ServiceLoader<Operation> operations = ServiceLoader.load(Operation.class);
operations.forEach(item->System.out.println("result: " + item.operate(2, 2)));
}
}</code></pre>
<p>5)测试结果</p>
<pre><code>run division operation
result:1
run plus operation
result:4</code></pre>
<h3>1.3 JDK SPI的源码分析</h3>
<p>例子很简单,实现的话,可以大胆猜测一下,看名字“ServiceLoader”应该就是用类加载器根据接口的类型加上配置文件里的具体实现名字将实现加载了进来。</p>
<p>接下来通过分析源码进一步了解其实现原理。</p>
<h4>1.3.1 ServiceLoader类</h4>
<p>PREFIX定义了加载路径,reload方法初始化了LazyIterator,LazyIterator是加载的核心,真正实现了加载。加载的模式从名字上就可以看出,是懒加载的模式,只有当真正调用迭代时才会加载。</p>
<p><img src="/img/remote/1460000021922652" alt="" title=""></p>
<h4>1.3.2 hasNextService方法</h4>
<p>LazyIterator中的hasNextService方法负责加载配置文件和解析具体的实现类名。 </p>
<p><img src="/img/remote/1460000021922650" alt="" title=""></p>
<h4>1.3.3 nextService方法</h4>
<p>LazyIterator中的nextService方法负责用反射加载实现类。</p>
<p><img src="/img/remote/1460000021922649" alt="" title=""></p>
<p>看完了源码,感觉这个代码是有优化空间的,实例化所有实现其实没啥必要,一来比较耗时,二来浪费资源。Dubbo就没有使用Java原生的SPI机制,而是对其进行了增强,使其能够更好地满足需求。</p>
<h2>二、Dubbo SPI</h2>
<h3>2.1 Dubbo SPI的小栗子</h3>
<p>老习惯,在拆解源码之前,先来个栗子。此处示例是在前文例子的基础上稍做了些修改。</p>
<p>1)定义一个接口</p>
<p>修改接口,加上了Dubbo的@SPI注解。</p>
<pre><code>@SPI
public interface Operation {
int operate(int num1, int num2);
}</code></pre>
<p>2)写两个简单的实现</p>
<p>沿用之前的两个实现类。</p>
<p>3)添加一个配置文件</p>
<p>新增配置文件放在dubbo目录下。</p>
<p>目录结构</p>
<p><img src="/img/remote/1460000021922651" alt="" title=""></p>
<p>文件内容</p>
<pre><code>division=com.api.impl.DivisionOperation
plus=com.api.impl.PlusOperation</code></pre>
<p>4)测试程序</p>
<pre><code>public class DubboSpiTest {
@Test
public void testOperation() throws Exception {
ExtensionLoader<Operation> loader = ExtensionLoader.getExtensionLoader(Operation.class);
Operation division = loader.getExtension("division");
System.out.println("result: " + division.operate(1, 2));
}
}</code></pre>
<p>5)测试结果</p>
<pre><code>run division operation
result:0</code></pre>
<h3>2.2 Dubbo SPI源码</h3>
<p>上面的测试例子也很简单,和JDK原生的SPI对比来看,Dubbo的SPI可以根据配置的kv值来获取。在没有拆解源码之前,考虑一下如何实现。</p>
<p>我可能会用双层Map来实现缓存:第一层的key为接口的class对象,value为一个map;第二层的key为扩展名(配置文件中的key),value为实现类的class。实现懒加载的方式,当运行方法的时候创建空map。在真正获取时先从缓存中查找具体实现类的class对象,找得到就直接返回、找不到就根据配置文件加载并缓存。</p>
<p>Dubbo又是如何实现的呢?</p>
<h4>2.2.1 getExtensionLoader方法</h4>
<p>首先来拆解getExtensionLoader方法。</p>
<p><img src="/img/remote/1460000021922654" alt="" title=""></p>
<p>这是一个静态的工厂方法,要求传入的类型必须为接口并且有SPI的注解,用map做了个缓存,key为接口的class对象,而value是 ExtensionLoader对象。</p>
<h4>2.2.2 getExtension方法</h4>
<p>再来拆解ExtensionLoader的getExtension方法。</p>
<p><img src="/img/remote/1460000021922655" alt="" title=""></p>
<p>这段代码也不复杂,如果传入的参数为'true',则返回默认的扩展类实例;否则,从缓存中获取实例,如果有就从缓存中获取,没有的话就新建。用map做缓存,缓存了holder对象,而holder对象中存放扩展类。用volatile关键字和双重检查来应对多线程创建问题,这也是单例模式的常用写法。</p>
<h4>2.2.3 createExtension方法</h4>
<p>重点分析createExtension方法。</p>
<p><img src="/img/remote/1460000021922657" alt="" title=""></p>
<p>这段代码由几部分组成:</p>
<ul>
<li>根据传入的扩展名获取对应的class。</li>
<li>根据class去缓存中获取实例,如果没有的话,通过反射创建对象并放入缓存。</li>
<li>依赖注入,完成对象实例的初始化。</li>
<li>创建wrapper对象。也就是说,此处返回的对象不一定是具体的实现类,可能是包装的对象。</li>
</ul>
<p>第二个没啥好说的,我们重点来分析一下1、3、4三个部分。</p>
<p>1)getExtensionClasses方法</p>
<p><img src="/img/remote/1460000021922658" alt="" title=""></p>
<p>老套路,从缓存获取,没有的话创建并加入缓存。这里缓存的是一个扩展名和class的关系。这个扩展名就是在配置文件中的key。创建之前,先缓存了一下接口的限定名。加载配置文件的路径是以下这几个。</p>
<p><img src="/img/remote/1460000021922656" alt="" title=""></p>
<p>2)loadDirectory方法</p>
<p><img src="/img/remote/1460000021922661" alt="" title=""></p>
<p>获取配置文件路径,获取classLoader,并使用loadResource方法做进一步处理。</p>
<p>3)loadResource方法</p>
<p><img src="/img/remote/1460000021922660" alt="" title=""></p>
<p><img src="/img/remote/1460000021922659" alt="" title=""></p>
<p>loadResource加载了配置文件,并解析了配置文件中的内容。loadClass 方法操作了不同的缓存。</p>
<p>首先判断是否有Adaptive注解,有的话缓存到cacheAdaptiveClass(缓存结构为class);然后判断是否wrapperclasses,是的话缓存到cacheWrapperClass中(缓存结构为Set);如果以上都不是,这个类就是个普通的类,存储class和名称的映射关系到cacheNames里(缓存结构为Map)。</p>
<p>基本上getExtensionClasses方法就分析完了,可以看出来,其实并不是很复杂。</p>
<h4>2.2.4 IOC</h4>
<p>1)injectExtension方法</p>
<p><img src="/img/remote/1460000021922663" alt="" title=""></p>
<p>这个方法实现了依赖注入,即IOC。首先通过反射获取到实例的方法;然后遍历,获取setter方法;接着从objectFactory中获取依赖对象;最后通过反射调用setter方法注入依赖。</p>
<p>objectFactory的变量类型为AdaptiveExtensionFactory。</p>
<p>2)AdaptiveExtensionFactory</p>
<p><img src="/img/remote/1460000021922662" alt="" title=""></p>
<p>这个类里面有个ExtensionFactory的列表,用来存储其他类型的 ExtensionFactory。Dubbo提供了两种ExtensionFactory,一种是SpiExtensionFactory, 用于创建自适应的扩展;另一种是SpringExtesionFactory,用于从Spring的IOC容器中获取扩展。配置文件一个在dubbo-common模块,一个在dubbo-config模块。 </p>
<p>配置文件</p>
<p><img src="/img/remote/1460000021922665" alt="" title=""></p>
<p><img src="/img/remote/1460000021922666" alt="" title=""></p>
<p>SpiExtensionFactory中的Spi方式前面已经解析过了。 </p>
<p><img src="/img/remote/1460000021922669" alt="" title=""></p>
<p>SpringExtesionFactory是从ApplicationContext中获取对应的实例。先根据名称查找,找不到的话,再根据类型查找。</p>
<p><img src="/img/remote/1460000021922668" alt="" title=""></p>
<p>依赖注入的部分也拆解完毕,看看这次拆解的最后一部分代码。</p>
<h4>2.2.5 AOP</h4>
<p>创建wrapper对象的部分,wrapper对象是从哪里来的呢?还记得之前拆解的第一步么,loadClass方法中有几个缓存,其中wrapperclasses就是缓存这些wrapper的class。</p>
<p><img src="/img/remote/1460000021922664" alt="" title=""></p>
<p>从代码中可以看出,只要构造方法里有且只有唯一参数,同时此参数为当前传入的接口类型,即为wrapper class。</p>
<p><img src="/img/remote/1460000021922667" alt="" title=""></p>
<p>此处循环创建wrapper实例,首先将instance做为构造函数的参数,通过反射来创建wrapper对象,然后再向wrapper中注入依赖。</p>
<p>看到这里,可能会有人有疑问:为什么要创建一个wrapper对象?其实很简单,系统要在真正调用的前后干点别的事呗。这个就有点类似于spring的aop了。</p>
<h2>三、总结</h2>
<p>本文简单介绍了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI源码和Dubbo的SPI源码。在拆解的过程中可以看出,Dubbo的源码还是很值得一读的。在实现方面考虑得很周全,不仅有对多线程的处理、多层缓存,也有IOC、AOP的过程。不过,Dubbo的SPI就这么简单么?当然不是,这篇只拆解了扩展类的加载过程,Dubbo的SPI中还有个很复杂的扩展点-自适应机制。欲知后事如何,请听下回分解~~</p>
<blockquote>来源:宜信技术学院<p>本文作者:宜信支付结算部支付研发团队Java研发高级工程师郑祥斌</p>
<p>原文首发于「野指针」</p>
</blockquote>
平台型产品功能设计的需求抽象与升维适配|专访宜信郭建伟
https://segmentfault.com/a/1190000021898925
2020-03-03T12:08:21+08:00
2020-03-03T12:08:21+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>摘要:平台型产品经理需要具备“对业务的结构化理解”,“先抽象提炼”再“反向适配具体业务需求”。</p>
<hr>
<p>前言:宜信技术人物专访是宜信技术学院推出的系列性专题,我们邀请软件研发行业的优秀技术人,分享自己在软件研发领域的实践经验和前瞻性观点。</p>
<p>第五期专访我们邀请到<strong>宜信科技中心普惠金融需求管理部负责人郭建伟,以“平台型产品的功能与体验设计”为主题,围绕宜信的产品开发实践,分享平台型产品经理的工作经验和能力要求。</strong></p>
<blockquote>嘉宾 | 宜信郭建伟<p>记者 | 宜信成芳</p>
<p>本文为采访实录。原创内容,转载请留言获取授权。</p>
</blockquote>
<p>记者:郭建伟老师您好,今天我们的采访将围绕“平台型产品的功能与体验设计”展开。业务模式的变化会对产品产生升级改造的需求。请您结合宜信的具体实践,介绍在业务模式发生颠覆性创新的情况下,平台型产品团队该如何处理跨多平台融合、多团队沟通的复杂问题,从而顺利推进和实现平台产品升级?</p>
<p><img src="/img/remote/1460000021898932" alt="" title=""></p>
<blockquote>
<p>郭建伟:正好在宜信的这段工作经历中,参与过一次公司战略级的业务模式创新项目——火凤凰项目,将网贷业务中间人模式升级为直接借贷模式。有一些经验体会可以分享给大家:</p>
<p><strong>首先,要明确自身在项目中的定位,目标清晰。</strong>通常业务模式升级类项目都是公司级项目,参与部门比较多,产品团队和技术部门虽然是落地过程中最重要的一环,但一般还会涉及公司内一系列系统外的工作推动,甚至外部合作方、监管机构的协调,往往不会由产品团队或技术部门来主导,所以<strong>要明确自身在项目中的角色,充分理解项目的业务目标,转化成具体的系统建设目标,并以此在项目前期和需求方达成一致,这是明确项目范围、管理预期的关键,也是系统建设的基础;</strong></p>
<p><strong>其次,是对旧业务模式、历史数据的兼容问题。</strong>在重大业务模式升级的项目中,对于平台型系统而言这是关键问题之一。这个方面我有两个体会:</p>
<ul>
<li>
<strong>要进一步细分兼容问题的类别,缩小范围、制定差异化解决方案,来降低兼容问题的复杂度、工作量。</strong>参与过类似项目的同事应该都会有这样的体会,对旧模式兼容、历史数据处理的工作量,完全不亚于投入在新模式建设上的工作量,所以作为负责人,优先考虑的不是“硬刚正面”,而是“战略性回避”,适当拉长新旧过渡期,在项目既定周期内,让团队聚焦新模式建设,为后续存量处理留出工作空间即可;</li>
<li>
<strong>与业务团队沟通讨论,充分挖掘业务方在存量处理上的作用。</strong>技术团队往往第一时间考虑的都是技术解决方案,但在存量处理的问题上,处在更上游的业务方的一些小变通,往往能给我们提供非常有效的支持,<strong>不要一味只在下游、自己熟悉的范围内寻找解决方案,最优方案往往需要上下游协同。</strong>
</li>
</ul>
<p><strong>最后,是试点支持的重要性。</strong>不论业务方是否要求试点,不论对自身研发、测试团队的能力多有信心,我的建议是面对大型业务模式升级,对试点功能的支持都是必不可少的。<strong>在IT层面,是对系统功能升级的验证,控制影响范围、监控性能问题等;在运营层面,新模式一方面平滑推广,一方面在逐步推广过程中,会持续调整,推广的过程也是一个收集反馈再升级的过程。</strong></p>
</blockquote>
<p>记者:有一种分类方式是将产品经理分为平台型产品经理和业务型产品经理:平台型产品经理主要对接开发、测试、设计团队,满足to B的需求;业务型产品经理主要对接市场、销售、运营团队,满足to C的需求。您认为在具体的产品工作上,这2类产品经理的区别是什么?</p>
<p><img src="/img/remote/1460000021898928" alt="" title=""></p>
<blockquote>郭建伟:我认为<strong>面向C端的业务产品经理最重要的是“对客群理解”,核心工作内容是通过产品的调整,增加“客群”与“产品”的匹配度。</strong>客群的特点与痛点是C端产品经理工作围绕的中心,并以此调整自己的产品;相反固守自己产品,而去不断教育用户、创造需求,成功的例子并不多。另外往往我们的产品经理并不是自己产品的目标客群,跳出自己的认知习惯,去精准把握另一个群体,是C端产品经理的重要能力。<p><strong>B端平台型产品经理最重要的是“对业务的结构化理解”。</strong>这是我个人的一个说法,从这里能看到两个层面的含义。一个是<strong>要对某“业务”领域知识足够熟悉,</strong>这是平台型产品经理的工作前提;另一个是<strong>“结构化理解”,也就是抽象能力,需要我们具备基于对业务流程的认识,抽象为对业务模式的结构化理解,这是平台适配性、扩展性的关键所在,是平台型产品经理的关键能力。</strong></p>
</blockquote>
<p>记者:宜信科技中心坚持以“科技赋能业务”为核心理念,作为平台型产品经理,该如何将业务需求转变为产品功能需求?</p>
<p><img src="/img/remote/1460000021898934" alt="" title=""></p>
<blockquote>
<p>郭建伟:我认为<strong>“科技赋能”要根据不同业务所处的不同阶段,采取不同的“赋能”方式,</strong>大家常看到由于技术落后业务发展进程而产生的问题,而容易忽略技术过于超前同样会引发问题。</p>
<p>一般情况下,我<strong>把“赋能”分为多个阶段:</strong></p>
<ul>
<li>
<strong>支持阶段</strong>,这个阶段一般在业务的起步期,业务流程不稳定,需求变更较多,<strong>系统的按时交付、保障业务开展是这个阶段的关键。</strong>
</li>
<li>
<strong>提炼阶段</strong>,这个阶段一般业务进入发展期,业务流程逐渐稳定,平台建设的技术团队对业务形成一定理解,进入对业务抽象提炼的阶段,<strong>这个阶段的目标还是通过对业务的理解、抽象,更好地建设系统,以适应业务的发展,着眼点还在系统提升本身。</strong>
</li>
<li>
<strong>赋能阶段</strong>,这个阶段业务已经持续发展,业务量稳定,业务本身促进增长方面出现瓶颈,<strong>需要技术不仅仅是在效率、自动化方面支持业务,而是基于对业务的理解,直接着眼业务,结合技术手段做出改进提升;</strong>
</li>
<li>
<strong>引领阶段</strong>,我认为并不是每一项业务都适合、或都能够进入到技术引领的阶段,处在这个阶段时,<strong>将打破“业务”与“技术”的边界,技术变革做出的引领,本身就是业务的一部分</strong>。</li>
</ul>
</blockquote>
<p>记者:您在之前的分享中提到:产品经理不仅要能满足当前业务需求,还要能够基于当前需求进行抽象提炼,要超前业务做面向未来需求的产品。要做到这一点,产品经理必须具备什么样的能力要求?如何做到抽象和洞察未来需求反向适配业务?</p>
<p><img src="/img/remote/1460000021898930" alt="" title=""></p>
<blockquote>郭建伟:我一直反复地和团队的平台产品经理强调一句话:<strong>系统建设不是照搬业务流程实现自动化的过程,而是先对业务流程做抽象提炼,再去反向适配具体业务需求的过程。</strong>这个认识也一直指导我在宜信的每一个系统建设工作。<p>抽象能力无疑是平台产品经理最关键的一项通用能力,谈抽象能力时,我常提的一个通俗例子就是<strong>“客户要一匹更快的马,我们应该给他一辆车”</strong>。这个例子中,一方面体现产品经理对客户真实需求“更快到达”的挖掘;</p>
<p>另一方面也体现了抽象作用,之所以从“要马”到“给车”,中间其实是有一个抽象升维的过程,就是抽象成“交通工具”,再从“交通工具”降维到“车”,这恰恰就是上面的<strong>“先抽象提炼”再“反向适配具体业务需求”;</strong></p>
<p>另外这里我还强调我们能顺利完成抽象、适配,其实还得益于我们对“交通工具”领域常识的了解,如果这个例子换到医疗领域“客户要某药品A,我们应该给他另一药品B”,如果我们不具备病理、药理的领域知识,就无法完成抽象、适配。所以<strong>“业务领域知识”+“抽象能力”都是平台产品经理的重要能力体现。</strong></p>
</blockquote>
<p>记者:关于to B与to C的产品功能设计,一直有一种说法是:to B重功能,to C重体验,您是否认同这种说法?平台型产品的用户体验设计主要体现在哪些方面?</p>
<p><img src="/img/remote/1460000021898929" alt="" title=""></p>
<blockquote>郭建伟:前面提及了C端产品经理和B端产品经理的区别,这里我简单谈一下我对用户体验的理解。<p><strong>对于平台型产品而言,谈“用户体验”时,我们谈的并不是界面美观与交互流畅与否,而应该是“用户是否能更便捷达成他的期望”,所以“用户的期望”才是用户体验的核心。</strong></p>
<p>举个简单的例子:一个界面美观、交互流畅、动效酷炫的纯手工功能,和一个界面粗糙的自动化功能,对于B端用户来说,会毫不犹豫选择后者,因为那更贴近他的“期望”。在这个理解的基础上,再去考虑提升界面美观、交互流畅等,这是B端产品经理应该注意的。</p>
</blockquote>
<p>记者:随着“中台”概念的兴起,“产品中台”也随之被提出,该如何理解“产品中台”这个概念?您认为产品经理的工作是否适合于中台化模式?</p>
<p><img src="/img/remote/1460000021898933" alt="" title=""></p>
<blockquote>郭建伟:中台建设我并不专业,宜信科技中心有团队专注这个领域,我简单说说我自己的一些认识。<p>首先,<strong>中台概念的兴起是和业务量级密不可分的,并不是所有业务都需要中台,为了建设中台而建设中台是没有意义的;</strong></p>
<p>其次,<strong>中台是和组织结构有关联的,中台建设本身也在促进“组织分层”,以便进一步在各层配置不同的能力模型和资源,各层或分散、或集中,达到提升组织整体运营效率的目的;</strong></p>
<p>最后才是<strong>系统建设方面,避免重复造车轮,增加系统共用,降低成本,加快业务需求交付速度等等。</strong></p>
</blockquote>
<p>记者:对于想要转型或刚刚从事平台型产品经理的同学,您有什么职业成长方面的建议想跟大家分享?</p>
<p><img src="/img/remote/1460000021898931" alt="" title=""></p>
<blockquote>郭建伟:B端产品经理的工作相比C端产品经理工作,有些枯燥,所以<p>首先,大家要有耐心,不断学习积累。</p>
<p>其次,<strong>要加强业务领域知识的学习,行业领域知识对B端产品经理的重要性,要高于C端产品经理,择业时尽量注意在某一大领域内选择;</strong></p>
<p>最后,不论是哪种产品经理、哪个行业,产品经理都应该是善于发现问题、解决问题的那类人,所以保持好奇心,勤于思考,提升商业敏感度是非常重要的。</p>
<p>希望大家除了在自己工作中是个合格的产品经理之外,动用产品经理赋予你的种种,让自己在“职业道路”、“人生道路”这个产品上,也能成为一名合格的产品经理,谢谢大家。</p>
</blockquote>
<p>拓展阅读:</p>
<p><a href="https://segmentfault.com/a/1190000019281195">专访宜信CTO向江旭:技术应当服务于场景,AI天生适合金融业</a></p>
<p><a href="https://segmentfault.com/a/1190000020125686">回归架构本质,重新理解微服务|专访宜信开发平台(SIA)负责人梁鑫</a></p>
<p><a href="https://segmentfault.com/a/1190000019845158">智慧金融时代,大数据和AI如何为业务赋能?|专访宜信AI中台团队负责人王东</a></p>
<p><a href="https://segmentfault.com/a/1190000020053819">一切技术创新都要以赋能业务为目标|专访宜信数据智能研发部负责人张军</a></p>
<p><a href="https://segmentfault.com/a/1190000020677003">市场变化驱动产品思维升级|专访宜信财富管理产品部负责人Bob</a></p>
小浩算法|一文让你学会如何用代码判断"24"点
https://segmentfault.com/a/1190000021841207
2020-02-26T10:15:18+08:00
2020-02-26T10:15:18+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p>“24点”是一种数学游戏,正如象棋、围棋一样是一种人们喜闻乐见的娱乐活动。它始于何年何月已无从考究,但它以自己独具的数学魅力和丰富的内涵正逐渐被越来越多的人们所接受。今天就为大家分享一道关于“24点”的算法题目。</p>
<p>话不多说,直接看题。</p>
<p><strong>题目:</strong>你有 4 张写有 1 到 9 数字的牌。你需要判断是否能通过 *,/,+,-,(,) 的运算得到 24。</p>
<blockquote>示例 1:<p>输入: [4, 1, 8, 7]</p>
<p>输出: True</p>
<p>解释: (8-4) * (7-1) = 24</p>
<p>示例 2:</p>
<p>输入: [1, 2, 1, 2]</p>
<p>输出: False</p>
</blockquote>
<p><strong>注意:</strong></p>
<ul>
<li>除法运算符 / 表示实数除法,而不是整数除法。例如 :4 / (1 - 2/3) = 12 。</li>
<li>每个运算符对两个数进行运算。特别是我们不能用 - 作为一元运算符。例如,[1, 1, 1, 1] 作为输入时,表达式 -1 - 1 - 1 - 1 是不允许的。</li>
<li>你不能将数字连接在一起。例如,输入为 [1, 2, 1, 2] 时,不能写成 12 + 12 。</li>
</ul>
<h2>题目分析</h2>
<p>拿到题目,第一反应就可以想到暴力求解。如果我们要判断给出的4张牌是否可以通过组合得到24,那我们只需找出所有的可组合的方式进行遍历。</p>
<p>4个数字,3个操作符,外加括号,基本目测就能想到组合数不会大到超出边界。所以,我们只要把他们统统列出来,不就可以进行求解了吗?说干就干!</p>
<p>我们首先定义个方法,用来判断两个数的的所有操作符组合是否可以得到24。</p>
<pre><code>func judgePoint24_2(a, b float64) bool {
return a+b == 24 || a*b == 24 || a-b == 24 || b-a == 24 || a/b == 24 || b/a == 24
}</code></pre>
<p>但是这个方法写的正确吗?其实不对!因为在计算机中,实数在计算和存储过程中会有一些微小的误差,对于一些与零作比较的语句来说,有时会因误差而导致原本是等于零但结果却小于或大于零之类的情况发生,所以常用一个很小的数 1e-6 代替 0,进行判读!</p>
<p>(1e-6:表示1乘以10的负6次方。Math.abs(x)<1e-6 其实相当于x==0。1e-6(也就是0.000001)叫做epslon,用来抵消浮点运算中因为误差造成的相等无法判断的情况。这个知识点需要掌握!)</p>
<p>举个例子:</p>
<pre><code>func main() {
var a float64
var b float64
b = 2.0
//math.Sqrt:开平方根
c := math.Sqrt(2)
a = b - c*c
fmt.Println(a == 0) //false
fmt.Println(a < 1e-6 && a > -(1e-6)) //true
}</code></pre>
<p>这里直接用 a==0 就会得到false,但是通过 a < 1e-6 && a > -(1e-6) 却可以进行准确的判断。</p>
<p>所以我们将上面的方法改写:</p>
<pre><code> //go语言
//judgePoint24_2:判断两个数的所有操作符组合是否可以得到24
func judgePoint24_2(a, b float64) bool {
return (a+b < 24+1e-6 && a+b > 24-1e-6) ||
(a*b < 24+1e-6 && a*b > 24-1e-6) ||
(a-b < 24+1e-6 && a-b > 24-1e-6) ||
(b-a < 24+1e-6 && b-a > 24-1e-6) ||
(a/b < 24+1e-6 && a/b > 24-1e-6) ||
(b/a < 24+1e-6 && b/a > 24-1e-6)
}</code></pre>
<p>完善了通过两个数来判断是否可以得到24的方法,现在我们加一个判断三个数是否可以得到24的方法。</p>
<pre><code>//硬核代码,不服来辩!
func judgePoint24_3(a, b, c float64) bool {
return judgePoint24_2(a+b, c) ||
judgePoint24_2(a-b, c) ||
judgePoint24_2(a*b, c) ||
judgePoint24_2(a/b, c) ||
judgePoint24_2(b-a, c) ||
judgePoint24_2(b/a, c) ||
judgePoint24_2(a+c, b) ||
judgePoint24_2(a-c, b) ||
judgePoint24_2(a*c, b) ||
judgePoint24_2(a/c, b) ||
judgePoint24_2(c-a, b) ||
judgePoint24_2(c/a, b) ||
judgePoint24_2(c+b, a) ||
judgePoint24_2(c-b, a) ||
judgePoint24_2(c*b, a) ||
judgePoint24_2(c/b, a) ||
judgePoint24_2(b-c, a) ||
judgePoint24_2(b/c, a)
}</code></pre>
<p>好了。三个数的也出来了,我们再加一个判断4个数为24点的方法:(排列组合,我想大家都会....)</p>
<p>前方高能!!!</p>
<p>前方高能!!!</p>
<p>前方高能!!!</p>
<pre><code>//硬核代码,不服来辩!
func judgePoint24(nums []int) bool {
return judgePoint24_3(float64(nums[0])+float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])-float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])*float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])/float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])-float64(nums[0]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])/float64(nums[0]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])+float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])-float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])*float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])/float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])-float64(nums[0]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])/float64(nums[0]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])+float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[0])-float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[0])*float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[0])/float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])-float64(nums[0]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])/float64(nums[0]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])+float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])-float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])*float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])/float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])-float64(nums[2]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])/float64(nums[2]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[1])+float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])-float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])*float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])/float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])-float64(nums[1]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])/float64(nums[1]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])+float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[1])-float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[1])*float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[1])/float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[3])-float64(nums[1]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[3])/float64(nums[1]), float64(nums[2]), float64(nums[0]))
}</code></pre>
<h2>Go语言示例</h2>
<p>搞定收工,我们整合全部代码如下:</p>
<pre><code>//硬核编程...
func judgePoint24(nums []int) bool {
return judgePoint24_3(float64(nums[0])+float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])-float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])*float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])/float64(nums[1]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])-float64(nums[0]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])/float64(nums[0]), float64(nums[2]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])+float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])-float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])*float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])/float64(nums[2]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])-float64(nums[0]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])/float64(nums[0]), float64(nums[1]), float64(nums[3])) ||
judgePoint24_3(float64(nums[0])+float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[0])-float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[0])*float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[0])/float64(nums[3]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])-float64(nums[0]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])/float64(nums[0]), float64(nums[2]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])+float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])-float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])*float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[2])/float64(nums[3]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])-float64(nums[2]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[3])/float64(nums[2]), float64(nums[0]), float64(nums[1])) ||
judgePoint24_3(float64(nums[1])+float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])-float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])*float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])/float64(nums[2]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])-float64(nums[1]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[2])/float64(nums[1]), float64(nums[0]), float64(nums[3])) ||
judgePoint24_3(float64(nums[1])+float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[1])-float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[1])*float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[1])/float64(nums[3]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[3])-float64(nums[1]), float64(nums[2]), float64(nums[0])) ||
judgePoint24_3(float64(nums[3])/float64(nums[1]), float64(nums[2]), float64(nums[0]))
}
func judgePoint24_3(a, b, c float64) bool {
return judgePoint24_2(a+b, c) ||
judgePoint24_2(a-b, c) ||
judgePoint24_2(a*b, c) ||
judgePoint24_2(a/b, c) ||
judgePoint24_2(b-a, c) ||
judgePoint24_2(b/a, c) ||
judgePoint24_2(a+c, b) ||
judgePoint24_2(a-c, b) ||
judgePoint24_2(a*c, b) ||
judgePoint24_2(a/c, b) ||
judgePoint24_2(c-a, b) ||
judgePoint24_2(c/a, b) ||
judgePoint24_2(c+b, a) ||
judgePoint24_2(c-b, a) ||
judgePoint24_2(c*b, a) ||
judgePoint24_2(c/b, a) ||
judgePoint24_2(b-c, a) ||
judgePoint24_2(b/c, a)
}
func judgePoint24_2(a, b float64) bool {
return (a+b < 24+1e-6 && a+b > 24-1e-6) ||
(a*b < 24+1e-6 && a*b > 24-1e-6) ||
(a-b < 24+1e-6 && a-b > 24-1e-6) ||
(b-a < 24+1e-6 && b-a > 24-1e-6) ||
(a/b < 24+1e-6 && a/b > 24-1e-6) ||
(b/a < 24+1e-6 && b/a > 24-1e-6)
}</code></pre>
<p>由于代码过于硬核,</p>
<p>我们直接击败100%的对手:</p>
<p>(没想到吧!代码还可以这么写~)</p>
<p><img src="/img/remote/1460000021841210" alt="" title=""></p>
<p>本期的题目应该都能看懂吗?</p>
<p>大家还有其他的方法来得到答案吗?</p>
<p>评论区留下你的想法吧!</p>
<blockquote>来源:宜信技术学院<p>小浩:宜信科技中心攻城狮一枚,热爱算法,热爱学习,不拘泥于枯燥编程代码,更喜欢用轻松方式把问题简单阐述,希望喜欢的小伙伴可以多多关注!</p>
<p>原文首发于:「小浩算法」</p>
</blockquote>
代码演示Mybatis-Generator 扩展自定义生成
https://segmentfault.com/a/1190000021832619
2020-02-25T11:00:52+08:00
2020-02-25T11:00:52+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>Mybatis-Generator 可自动生成Model、Dao、Mapper代码,但其自带生成的代码存在以下问题:</p>
<ul>
<li>生成的注释不是我们想要的,我们期望的是根据数据库表、字段生成不同的注释;</li>
<li>分页代码生成缺失,每个公司的分页方式不同,尤其是老久项目或已发布API,不能随意变动,那么如何自适应分页代码生成;</li>
<li>Mapper.xml没有group by相关代码生成;</li>
<li>重复生成代码时,Mapper.xml并不是覆盖原代码,而是对内容进行了追加;</li>
<li>序列化,<code>mybatis-generator</code>内置了<code>SerializablePlugin</code>,但仅对Model,并没有对 Example序列化,在一些开发中是不够的;</li>
<li>对Service Layer代码没有生成。</li>
</ul>
<p>实际上,<code>mybatis-generator</code>提供了<code>PluginAdapter</code>供我们来继承,进行个性化的一些扩展(<strong>Plugin的相关内容是阅读本文的前置条件</strong>)如果不熟悉的同学,请自行补充,本文不对其进行相关介绍)。同时,本文不可能涵盖所有业务所需的扩展点,但是基本样板已有,可参考本文代码继续进行扩展。</p>
<h2>一、注释的自定义生成</h2>
<p>根据数据库表或字段的<code>COMMENT</code>生成注释。@Date 生成的时间可根据需要自己定义格式。</p>
<pre><code class="java">package run.override;
import java.util.Date;
import java.util.Properties;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.CompilationUnit;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.InnerClass;
import org.mybatis.generator.api.dom.java.InnerEnum;
import org.mybatis.generator.api.dom.java.JavaElement;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.Parameter;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.internal.DefaultCommentGenerator;
import org.mybatis.generator.internal.util.StringUtility;
/**
* Comment Generator
* @ClassName CommentGenerator
* @Description
* @author Marvis
*/
public class CommentGenerator extends DefaultCommentGenerator {
private Properties properties;
private boolean suppressDate;
private boolean suppressAllComments;
public CommentGenerator() {
this.properties = new Properties();
this.suppressDate = false;
this.suppressAllComments = false;
}
public void addJavaFileComment(CompilationUnit compilationUnit) {
compilationUnit.addFileCommentLine("/*** copyright (c) 2019 Marvis ***/");
}
/**
* XML file Comment
*/
public void addComment(XmlElement xmlElement) {
if (this.suppressAllComments) {
return;
}
}
public void addRootComment(XmlElement rootElement) {
}
public void addConfigurationProperties(Properties properties) {
this.properties.putAll(properties);
this.suppressDate = StringUtility.isTrue(properties.getProperty("suppressDate"));
this.suppressAllComments = StringUtility.isTrue(properties.getProperty("suppressAllComments"));
}
protected void addJavadocTag(JavaElement javaElement, boolean markAsDoNotDelete) {
StringBuilder sb = new StringBuilder();
sb.append(" * ");
sb.append("@date");
String s = getDateString();
if (s != null) {
sb.append(' ');
sb.append(s);
}
javaElement.addJavaDocLine(sb.toString());
}
protected String getDateString() {
if (this.suppressDate) {
return null;
}
return new Date().toString();
}
/**
* Comment of Example inner class(GeneratedCriteria ,Criterion)
*/
public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable) {
if (this.suppressAllComments) {
return;
}
innerClass.addJavaDocLine("/**");
innerClass.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable().getDomainObjectName()+ "<p/>");
innerClass.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable().toString());
addJavadocTag(innerClass, false);
innerClass.addJavaDocLine(" */");
}
public void addEnumComment(InnerEnum innerEnum, IntrospectedTable introspectedTable) {
if (this.suppressAllComments) {
return;
}
StringBuilder sb = new StringBuilder();
innerEnum.addJavaDocLine("/**");
innerEnum.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable().getAlias()+ "<p/>");
innerEnum.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable());
innerEnum.addJavaDocLine(sb.toString());
addJavadocTag(innerEnum, false);
innerEnum.addJavaDocLine(" */");
}
/**
* entity filed Comment
*/
public void addFieldComment(Field field, IntrospectedTable introspectedTable,
IntrospectedColumn introspectedColumn) {
if (this.suppressAllComments) {
return;
}
// if(introspectedColumn.getRemarks() != null && !introspectedColumn.getRemarks().trim().equals(""))
field.addJavaDocLine("/**");
field.addJavaDocLine(" * " + introspectedColumn.getRemarks());
field.addJavaDocLine(" * @author " );
field.addJavaDocLine(" * @date " + getDateString() );
field.addJavaDocLine(" * @return");
field.addJavaDocLine(" */");
}
/**
* Comment of EXample filed
*/
public void addFieldComment(Field field, IntrospectedTable introspectedTable) {
if (this.suppressAllComments) {
return;
}
field.addJavaDocLine("/**");
addJavadocTag(field, false);
field.addJavaDocLine(" */");
}
/**
* Comment of Example method
*/
public void addGeneralMethodComment(Method method, IntrospectedTable introspectedTable) {
if (this.suppressAllComments) {
return;
}
}
/**
*
* entity Getter Comment
*/
public void addGetterComment(Method method, IntrospectedTable introspectedTable,
IntrospectedColumn introspectedColumn) {
if (this.suppressAllComments) {
return;
}
method.addJavaDocLine("/**");
method.addJavaDocLine(" * @return " + introspectedTable.getFullyQualifiedTable().getAlias() + " : " + introspectedColumn.getRemarks());
method.addJavaDocLine(" */");
}
public void addSetterComment(Method method, IntrospectedTable introspectedTable,
IntrospectedColumn introspectedColumn) {
if (this.suppressAllComments) {
return;
}
StringBuilder sb = new StringBuilder();
method.addJavaDocLine("/**");
Parameter parm = (Parameter) method.getParameters().get(0);
sb.append(" * @param ");
sb.append(parm.getName());
sb.append(" : ");
sb.append(introspectedColumn.getRemarks());
method.addJavaDocLine(sb.toString());
method.addJavaDocLine(" */");
}
/**
* Comment of Example inner class(Criteria)
*/
public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable, boolean markAsDoNotDelete) {
if (this.suppressAllComments) {
return;
}
innerClass.addJavaDocLine("/**");
innerClass.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable().getAlias()+ "<p/>");
innerClass.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable().toString());
addJavadocTag(innerClass, markAsDoNotDelete);
innerClass.addJavaDocLine(" */");
}</code></pre>
<p>Model 类注释(表的描述): MySQL。</p>
<h3>1)EntityCommentPlugin</h3>
<pre><code class="java">package run.override.model;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import java.util.List;
import org.mybatis.generator.api.FullyQualifiedTable;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.internal.JDBCConnectionFactory;
import org.mybatis.generator.internal.util.StringUtility;
/**
* Comment of Entity,only support MySQL
* @ClassName CommentPlugin
* @Description
* @author Marvis
*/
public class EntityCommentPlugin extends PluginAdapter {
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
addModelClassComment(topLevelClass, introspectedTable);
return super.modelBaseRecordClassGenerated(topLevelClass, introspectedTable);
}
@Override
public boolean modelRecordWithBLOBsClassGenerated(TopLevelClass topLevelClass,
IntrospectedTable introspectedTable) {
addModelClassComment(topLevelClass, introspectedTable);
return super.modelRecordWithBLOBsClassGenerated(topLevelClass, introspectedTable);
}
protected void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
FullyQualifiedTable table = introspectedTable.getFullyQualifiedTable();
String tableComment = getTableComment(table);
topLevelClass.addJavaDocLine("/**");
if(StringUtility.stringHasValue(tableComment))
topLevelClass.addJavaDocLine(" * " + tableComment + "<p/>");
topLevelClass.addJavaDocLine(" * " + table.toString() + "<p/>");
topLevelClass.addJavaDocLine(" * @date " + new Date().toString());
topLevelClass.addJavaDocLine(" *");
topLevelClass.addJavaDocLine(" */");
}
/**
* @author Marvis
* @date Jul 13, 2017 4:39:52 PM
* @param table
*/
private String getTableComment(FullyQualifiedTable table) {
String tableComment = "";
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
JDBCConnectionFactory jdbc = new JDBCConnectionFactory(context.getJdbcConnectionConfiguration());
connection = jdbc.getConnection();
statement = connection.createStatement();
rs = statement.executeQuery("SHOW CREATE TABLE " + table.getIntrospectedTableName());
if (rs != null && rs.next()) {
String createDDL = rs.getString(2);
int index = createDDL.indexOf("COMMENT='");
if (index < 0) {
tableComment = "";
} else {
tableComment = createDDL.substring(index + 9);
tableComment = tableComment.substring(0, tableComment.length() - 1);
}
}
} catch (SQLException e) {
} finally {
closeConnection(connection, statement, rs);
}
return tableComment;
}
/**
*
* @author Marvis
* @date Jul 13, 2017 4:45:26 PM
* @param connection
* @param statement
* @param rs
*/
private void closeConnection(Connection connection, Statement statement, ResultSet rs) {
try {
if (null != rs)
rs.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (statement != null)
statement.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (connection != null)
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
/**
* This plugin is always valid - no properties are required
*/
@Override
public boolean validate(List<String> warnings) {
return true;
}
}</code></pre>
<h2>二、分页和分组代码生成</h2>
<p>这里,我对Dao Model进行了通用方法的抽取,建立通用基类。同时,对其进行了一些扩展,增加分页和分组。</p>
<p>先对基类进行介绍。</p>
<h3>1)BaseMapper</h3>
<pre><code class="java">package cn.xxx.core.base.dao;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface BaseMapper<T, Example, ID> {
long countByExample(Example example);
int deleteByExample(Example example);
int deleteByPrimaryKey(ID id);
int insert(T record);
int insertSelective(T record);
List<T> selectByExample(Example example);
T selectByPrimaryKey(ID id);
int updateByExampleSelective(@Param("record") T record, @Param("example") Example example);
int updateByExample(@Param("record") T record, @Param("example") Example example);
int updateByPrimaryKeySelective(T record);
int updateByPrimaryKey(T record);
}</code></pre>
<h3>2)BaseExample</h3>
<pre><code class="java">package cn.xxx.core.base.model;
/**
* BaseExample 基类
* @ClassName BaseExample
* @Description 增加分页参数
* @author Marvis
* @date Jul 31, 2017 11:26:53 AM
*/
public abstract class BaseExample {
protected PageInfo pageInfo;
protected String groupByClause;
public PageInfo getPageInfo() {
return pageInfo;
}
public void setPageInfo(PageInfo pageInfo) {
this.pageInfo = pageInfo;
}
public String getGroupByClause() {
return groupByClause;
}
public void setGroupByClause(String groupByClause) {
this.groupByClause = groupByClause;
}
}</code></pre>
<h3>3)PageInfo</h3>
<pre><code class="java">package cn.xxx.core.base.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* 分页查询参数类
*
* @author
*
*/
public class PageInfo {
public static final int Default_PageSize = 20;
// 当前页码
protected int currentPage = 1;
// 总页数
protected int totalPage;
// 总记录数
protected int totalCount;
// 每页条数
protected int pageSize = Default_PageSize;
// 开始
protected int pageBegin = 0;
// 结束
protected int pageEnd = 20;
/**
* bean起始坐标(不包含)
*/
private Integer pageBeginId = null;
public static final String PageQuery_classname = "pageInfo";
/**
* 将分布参数传入处理,最终计算出当前页码PageQuery_currPage,开始坐标PageQuery_star,
* 结束坐标PageQuery_end,总页数PageQuery_Psize
* <p/>
* 页数从1开始计数
*
* @param totalCount
* 记录总数
* @param pageSize
* 每页显示个数
* @param currentPage
* 当前页码
*/
public void setPageParams(int totalCount, int pageSize, int currentPage) {
this.totalPage = pageSize == 0 ? 1 : (int) Math.ceil((double) totalCount / (double) pageSize);
this.totalCount = totalCount;
this.pageSize = pageSize;
this.currentPage = currentPage;
float Psize_l = totalCount / (float) (this.pageSize);
if (currentPage < 2) {
currentPage = 1;
pageBegin = 0;
} else if (currentPage > Psize_l) {
if (Psize_l == 0) {
currentPage = 1;
} else {
currentPage = (int) Math.ceil(Psize_l);
}
pageBegin = (currentPage - 1) * this.pageSize;
} else {
pageBegin = (currentPage - 1) * this.pageSize;
}
pageSize = (int) Math.ceil(Psize_l);
this.pageEnd = currentPage * this.pageSize;
if (this.currentPage <= 0 || this.currentPage > this.totalPage)
this.pageSize = 0;
}
/**
* 将分布参数传入处理,最终计算出当前页码PageQuery_currPage,开始坐标PageQuery_star,
* 结束坐标PageQuery_end,总页数PageQuery_Psize
*
* @param infoCount
* 记录总数
*/
public void setPageParams(int totalCount) {
this.setPageParams(totalCount, this.pageSize, this.currentPage);
}
@Override
public String toString() {
return "PageInfo [currentPage=" + currentPage + ", totalPage=" + totalPage + ", totalCount=" + totalCount
+ ", pageSize=" + pageSize + ", pageBegin=" + pageBegin + ", pageEnd=" + pageEnd + ", pageBeginId="
+ pageBeginId + "]";
}
public int getCurrentPage() {
return currentPage;
}
public int getTotalPage() {
return totalPage;
}
public int getTotalCount() {
return totalCount;
}
/**
* 每页显示个数
*/
public int getPageSize() {
return pageSize;
}
@JsonIgnore
public int getPageBegin() {
return pageBegin;
}
@JsonIgnore
public int getPageEnd() {
return pageEnd;
}
/**
* bean起始id(不包含)
*/
@JsonIgnore
public Integer getPageBeginId() {
return pageBeginId;
}
/**
* 请求页
*/
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
/**
* 每页显示个数
*/
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
}
</code></pre>
<h3>4)PaginationPlugin</h3>
<p>分页扩展。并且<code>Example</code>继承<code>BaseExample</code>。</p>
<pre><code class="java">package run.override.pagination;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.TextElement;
import org.mybatis.generator.api.dom.xml.XmlElement;
import run.override.mapper.SqlMapIsMergeablePlugin;
import run.override.proxyFactory.FullyQualifiedJavaTypeProxyFactory;
public class PaginationPlugin extends SqlMapIsMergeablePlugin {
@Override
public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
FullyQualifiedJavaType baseExampleType = FullyQualifiedJavaTypeProxyFactory.getBaseExampleInstance();
topLevelClass.setSuperClass(baseExampleType);
topLevelClass.addImportedType(baseExampleType);
return super.modelExampleClassGenerated(topLevelClass, introspectedTable);
}
@Override
public boolean sqlMapSelectByExampleWithBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
XmlElement isNotNullElement1 = new XmlElement("if");
isNotNullElement1.addAttribute(new Attribute("test", "groupByClause != null"));
isNotNullElement1.addElement(new TextElement("group by ${groupByClause}"));
element.addElement(5, isNotNullElement1);
XmlElement isNotNullElement = new XmlElement("if");
isNotNullElement.addAttribute(new Attribute("test", "pageInfo != null"));
isNotNullElement.addElement(new TextElement("limit #{pageInfo.pageBegin} , #{pageInfo.pageSize}"));
element.addElement(isNotNullElement);
return super.sqlMapUpdateByExampleWithBLOBsElementGenerated(element, introspectedTable);
}
@Override
public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
XmlElement isNotNullElement1 = new XmlElement("if");
isNotNullElement1.addAttribute(new Attribute("test", "groupByClause != null"));
isNotNullElement1.addElement(new TextElement("group by ${groupByClause}"));
element.addElement(5, isNotNullElement1);
XmlElement isNotNullElement = new XmlElement("if");
isNotNullElement.addAttribute(new Attribute("test", "pageInfo != null"));
isNotNullElement.addElement(new TextElement("limit #{pageInfo.pageBegin} , #{pageInfo.pageSize}"));
element.addElement(isNotNullElement);
return super.sqlMapUpdateByExampleWithoutBLOBsElementGenerated(element, introspectedTable);
}
@Override
public boolean sqlMapCountByExampleElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
XmlElement answer = new XmlElement("select");
String fqjt = introspectedTable.getExampleType();
answer.addAttribute(new Attribute("id", introspectedTable.getCountByExampleStatementId()));
answer.addAttribute(new Attribute("parameterType", fqjt));
answer.addAttribute(new Attribute("resultType", "java.lang.Integer"));
this.context.getCommentGenerator().addComment(answer);
StringBuilder sb = new StringBuilder();
sb.append("select count(1) from ");
sb.append(introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime());
XmlElement ifElement = new XmlElement("if");
ifElement.addAttribute(new Attribute("test", "_parameter != null"));
XmlElement includeElement = new XmlElement("include");
includeElement.addAttribute(new Attribute("refid", introspectedTable.getExampleWhereClauseId()));
ifElement.addElement(includeElement);
element.getElements().clear();
element.getElements().add(new TextElement(sb.toString()));
element.getElements().add(ifElement);
return super.sqlMapUpdateByExampleWithoutBLOBsElementGenerated(element, introspectedTable);
}
}</code></pre>
<h3>5)FullyQualifiedJavaTypeProxyFactory</h3>
<pre><code class="java">package run.override.proxyFactory;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
public class FullyQualifiedJavaTypeProxyFactory extends FullyQualifiedJavaType{
private static FullyQualifiedJavaType pageInfoInstance = new FullyQualifiedJavaType("cn.xxx.core.base.model.PageInfo");
private static FullyQualifiedJavaType baseExampleInstance = new FullyQualifiedJavaType("cn.xxx.core.base.model.BaseExample");
private static FullyQualifiedJavaType baseMapperInstance = new FullyQualifiedJavaType("cn.xxx.core.base.dao.BaseMapper");
private static FullyQualifiedJavaType baseServiceInstance = new FullyQualifiedJavaType("cn.xxx.core.base.service.BaseService");
private static FullyQualifiedJavaType baseServiceImplInstance = new FullyQualifiedJavaType("cn.xxx.core.base.service.impl.BaseServiceImpl");
public FullyQualifiedJavaTypeProxyFactory(String fullTypeSpecification) {
super(fullTypeSpecification);
}
public static final FullyQualifiedJavaType getPageInfoInstanceInstance() {
return pageInfoInstance;
}
public static final FullyQualifiedJavaType getBaseExampleInstance() {
return baseExampleInstance;
}
public static final FullyQualifiedJavaType getBaseMapperInstance() {
return baseMapperInstance;
}
public static final FullyQualifiedJavaType getBaseServiceInstance() {
return baseServiceInstance;
}
public static final FullyQualifiedJavaType getBaseServiceImplInstance() {
return baseServiceImplInstance;
}
}</code></pre>
<h2>三、Dao 生成代码简化</h2>
<h3>1)ClientDaoPlugin</h3>
<pre><code class="java">package run.override.dao;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.JavaTypeResolver;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl;
import run.override.model.EntityCommentPlugin;
import run.override.proxyFactory.FullyQualifiedJavaTypeProxyFactory;
/**
* javaClient("XMLMAPPER") extended
*
* @ClassName ClientDaoPlugin
* @Description Mapper.java
* @author Marvis
*/
public class ClientDaoPlugin extends EntityCommentPlugin {
@Override
public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass,
IntrospectedTable introspectedTable) {
JavaTypeResolver javaTypeResolver = new JavaTypeResolverDefaultImpl();
FullyQualifiedJavaType calculateJavaType = javaTypeResolver
.calculateJavaType(introspectedTable.getPrimaryKeyColumns().get(0));
FullyQualifiedJavaType superInterfaceType = new FullyQualifiedJavaType(
new StringBuilder("BaseMapper<")
.append(introspectedTable.getBaseRecordType())
.append(",")
.append(introspectedTable.getExampleType())
.append(",")
.append(calculateJavaType.getShortName())
.append(">")
.toString()
);
FullyQualifiedJavaType baseMapperInstance = FullyQualifiedJavaTypeProxyFactory.getBaseMapperInstance();
interfaze.addSuperInterface(superInterfaceType);
interfaze.addImportedType(baseMapperInstance);
List<Method> changeMethods = interfaze.getMethods().stream()
.filter(method -> method.getName().endsWith("WithBLOBs")
|| method.getReturnType().toString().endsWith("WithBLOBs")
|| Arrays.toString(method.getParameters().toArray()).contains("WithBLOBs"))
.collect(Collectors.toList());
interfaze.getMethods().retainAll(changeMethods);
if (changeMethods.isEmpty())
interfaze.getImportedTypes().removeIf(javaType -> javaType.getFullyQualifiedName().equals("java.util.List")
|| javaType.getFullyQualifiedName().equals("org.apache.ibatis.annotations.Param"));
return super.clientGenerated(interfaze, topLevelClass, introspectedTable);
}
}</code></pre>
<h2>四、修正</h2>
<p>重复生成时Mapper.xml不是覆盖原代码,而是对内容进行了追加。</p>
<h3>1)SqlMapIsMergeablePlugin</h3>
<pre><code class="java">package run.override.mapper;
import org.mybatis.generator.api.GeneratedXmlFile;
import org.mybatis.generator.api.IntrospectedTable;
import run.override.dao.ClientDaoPlugin;
public class SqlMapIsMergeablePlugin extends ClientDaoPlugin {
@Override
public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
//重新生成代码,xml内容覆盖
sqlMap.setMergeable(false);
return super.sqlMapGenerated(sqlMap, introspectedTable);
}
}</code></pre>
<h2>五、序列化自定义扩展</h2>
<p>增加<code>Example</code>的序列化,并增加<code>@SuppressWarnings("serial")</code>注解。</p>
<h3>1)SerializablePlugin</h3>
<pre><code class="java">package run.override;
import java.util.List;
import java.util.Properties;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.TopLevelClass;
public class SerializablePlugin extends PluginAdapter {
private FullyQualifiedJavaType serializable;
private FullyQualifiedJavaType gwtSerializable;
private boolean addGWTInterface;
private boolean suppressJavaInterface;
public SerializablePlugin() {
this.serializable = new FullyQualifiedJavaType("java.io.Serializable");
this.gwtSerializable = new FullyQualifiedJavaType("com.google.gwt.user.client.rpc.IsSerializable");
}
@Override
public void setProperties(Properties properties) {
super.setProperties(properties);
this.addGWTInterface = Boolean.valueOf(properties.getProperty("addGWTInterface")).booleanValue();
this.suppressJavaInterface = Boolean.valueOf(properties.getProperty("suppressJavaInterface")).booleanValue();
}
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
makeSerializable(topLevelClass, introspectedTable);
return true;
}
@Override
public boolean modelPrimaryKeyClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
makeSerializable(topLevelClass, introspectedTable);
return true;
}
@Override
public boolean modelRecordWithBLOBsClassGenerated(TopLevelClass topLevelClass,
IntrospectedTable introspectedTable) {
makeSerializable(topLevelClass, introspectedTable);
return true;
}
@Override
public boolean modelExampleClassGenerated(TopLevelClass topLevelClass,IntrospectedTable introspectedTable){
makeSerializable(topLevelClass, introspectedTable);
return true;
}
protected void makeSerializable(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
if (this.addGWTInterface) {
topLevelClass.addImportedType(this.gwtSerializable);
topLevelClass.addSuperInterface(this.gwtSerializable);
}
if (!(this.suppressJavaInterface)) {
topLevelClass.addImportedType(this.serializable);
topLevelClass.addSuperInterface(this.serializable);
topLevelClass.addAnnotation("@SuppressWarnings(\"serial\")");
}
}
/**
* This plugin is always valid - no properties are required
*/
@Override
public boolean validate(List<String> warnings) {
return true;
}
}</code></pre>
<h2>六、服务层代码自定义生成</h2>
<p>重写<code>Context</code>,<code>ConfigurationParser</code>,<code>MyBatisGeneratorConfigurationParser</code>,增加服务层生成逻辑。</p>
<p>先对Service基类进行介绍。</p>
<h3>1)BaseService</h3>
<pre><code class="java">package cn.xxx.core.base.service;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import cn.xxx.core.base.model.BaseExample;
import cn.xxx.core.base.model.PageInfo;
public interface BaseService<T, Example extends BaseExample, ID> {
long countByExample(Example example);
int deleteByExample(Example example);
int deleteByPrimaryKey(ID id);
int insert(T record);
int insertSelective(T record);
List<T> selectByExample(Example example);
/**
* return T object
* @author Marvis
* @date May 23, 2018 11:37:11 AM
* @param example
* @return
*/
T selectByCondition(Example example);
/**
* if pageInfo == null<p/>
* then return result of selectByExample(example)
* @author Marvis
* @date Jul 13, 2017 5:24:35 PM
* @param example
* @param pageInfo
* @return
*/
List<T> selectByPageExmple(Example example, PageInfo pageInfo);
T selectByPrimaryKey(ID id);
int updateByExampleSelective(@Param("record") T record, @Param("example") Example example);
int updateByExample(@Param("record") T record, @Param("example") Example example);
int updateByPrimaryKeySelective(T record);
int updateByPrimaryKey(T record);
}
</code></pre>
<h3>2)BaseServiceImpl</h3>
<pre><code class="java">package cn.xxx.core.base.service.impl;
import java.util.List;
import cn.xxx.core.base.dao.BaseMapper;
import cn.xxx.core.base.model.BaseExample;
import cn.xxx.core.base.model.PageInfo;
import cn.xxx.core.base.service.BaseService;
public abstract class BaseServiceImpl<T, Example extends BaseExample, ID> implements BaseService<T, Example, ID> {
private BaseMapper<T, Example, ID> mapper;
public void setMapper(BaseMapper<T, Example, ID> mapper) {
this.mapper = mapper;
}
public long countByExample(Example example) {
return mapper.countByExample(example);
}
@Override
public int deleteByExample(Example example) {
return mapper.deleteByExample(example);
}
@Override
public int deleteByPrimaryKey(ID id) {
return mapper.deleteByPrimaryKey(id);
}
@Override
public int insert(T record) {
return mapper.insert(record);
}
@Override
public int insertSelective(T record) {
return mapper.insertSelective(record);
}
@Override
public List<T> selectByExample(Example example) {
return mapper.selectByExample(example);
}
@Override
public T selectByCondition(Example example) {
List<T> datas = selectByExample(example);
return datas != null && datas.size() == 0 ? null : datas.get(0);
}
@Override
public List<T> selectByPageExmple(Example example, PageInfo pageInfo) {
if(pageInfo != null){
example.setPageInfo(pageInfo);
pageInfo.setPageParams(Long.valueOf(this.countByExample(example)).intValue());
}
return this.selectByExample(example);
}
@Override
public T selectByPrimaryKey(ID id) {
return mapper.selectByPrimaryKey(id);
}
@Override
public int updateByExampleSelective(T record, Example example) {
return mapper.updateByExampleSelective(record, example);
}
@Override
public int updateByExample(T record, Example example) {
return mapper.updateByExample(record, example);
}
@Override
public int updateByPrimaryKeySelective(T record) {
return mapper.updateByPrimaryKeySelective(record);
}
@Override
public int updateByPrimaryKey(T record) {
return mapper.updateByPrimaryKey(record);
}
}</code></pre>
<h3>3)ServiceLayerPlugin</h3>
<pre><code class="java">package run.override.service;
import org.mybatis.generator.api.GeneratedJavaFile;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.JavaTypeResolver;
import org.mybatis.generator.api.dom.java.CompilationUnit;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.JavaVisibility;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.Parameter;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl;
import run.override.pagination.PaginationPlugin;
import run.override.proxyFactory.FullyQualifiedJavaTypeProxyFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ServiceLayerPlugin extends PaginationPlugin {
/**
* 生成额外java文件
*/
@Override
public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles(IntrospectedTable introspectedTable) {
ContextOverride context = (ContextOverride) introspectedTable.getContext();
ServiceGeneratorConfiguration serviceGeneratorConfiguration;
if ((serviceGeneratorConfiguration = context.getServiceGeneratorConfiguration()) == null)
return null;
String targetPackage = serviceGeneratorConfiguration.getTargetPackage();
String targetProject = serviceGeneratorConfiguration.getTargetProject();
String implementationPackage = serviceGeneratorConfiguration.getImplementationPackage();
CompilationUnit addServiceInterface = addServiceInterface(introspectedTable, targetPackage);
CompilationUnit addServiceImplClazz = addServiceImplClazz(introspectedTable, targetPackage,
implementationPackage);
GeneratedJavaFile gjfServiceInterface = new GeneratedJavaFile(addServiceInterface, targetProject,
this.context.getProperty("javaFileEncoding"), this.context.getJavaFormatter());
GeneratedJavaFile gjfServiceImplClazz = new GeneratedJavaFile(addServiceImplClazz, targetProject,
this.context.getProperty("javaFileEncoding"), this.context.getJavaFormatter());
List<GeneratedJavaFile> list = new ArrayList<>();
list.add(gjfServiceInterface);
list.add(gjfServiceImplClazz);
return list;
}
protected CompilationUnit addServiceInterface(IntrospectedTable introspectedTable, String targetPackage) {
String entityClazzType = introspectedTable.getBaseRecordType();
String serviceSuperPackage = targetPackage;
String entityExampleClazzType = introspectedTable.getExampleType();
String domainObjectName = introspectedTable.getFullyQualifiedTable().getDomainObjectName();
JavaTypeResolver javaTypeResolver = new JavaTypeResolverDefaultImpl();
FullyQualifiedJavaType calculateJavaType = javaTypeResolver
.calculateJavaType(introspectedTable.getPrimaryKeyColumns().get(0));
StringBuilder builder = new StringBuilder();
FullyQualifiedJavaType superInterfaceType = new FullyQualifiedJavaType(
builder.append("BaseService<")
.append(entityClazzType)
.append(",")
.append(entityExampleClazzType)
.append(",")
.append(calculateJavaType.getShortName()).append(">").toString());
Interface serviceInterface = new Interface(
builder.delete(0, builder.length())
.append(serviceSuperPackage)
.append(".")
.append(domainObjectName)
.append("Service")
.toString()
);
serviceInterface.addSuperInterface(superInterfaceType);
serviceInterface.setVisibility(JavaVisibility.PUBLIC);
FullyQualifiedJavaType baseServiceInstance = FullyQualifiedJavaTypeProxyFactory.getBaseServiceInstance();
FullyQualifiedJavaType modelJavaType = new FullyQualifiedJavaType(entityClazzType);
FullyQualifiedJavaType exampleJavaType = new FullyQualifiedJavaType(entityExampleClazzType);
serviceInterface.addImportedType(baseServiceInstance);
serviceInterface.addImportedType(modelJavaType);
serviceInterface.addImportedType(exampleJavaType);
serviceInterface.addFileCommentLine("/*** copyright (c) 2019 Marvis ***/");
this.additionalServiceMethods(introspectedTable, serviceInterface);
return serviceInterface;
}
protected CompilationUnit addServiceImplClazz(IntrospectedTable introspectedTable, String targetPackage,
String implementationPackage) {
String entityClazzType = introspectedTable.getBaseRecordType();
String serviceSuperPackage = targetPackage;
String serviceImplSuperPackage = implementationPackage;
String entityExampleClazzType = introspectedTable.getExampleType();
String javaMapperType = introspectedTable.getMyBatis3JavaMapperType();
String domainObjectName = introspectedTable.getFullyQualifiedTable().getDomainObjectName();
JavaTypeResolver javaTypeResolver = new JavaTypeResolverDefaultImpl();
FullyQualifiedJavaType calculateJavaType = javaTypeResolver
.calculateJavaType(introspectedTable.getPrimaryKeyColumns().get(0));
StringBuilder builder = new StringBuilder();
FullyQualifiedJavaType superClazzType = new FullyQualifiedJavaType(
builder.append("BaseServiceImpl<")
.append(entityClazzType)
.append(",")
.append(entityExampleClazzType)
.append(",")
.append(calculateJavaType.getShortName()).append(">")
.toString()
);
FullyQualifiedJavaType implInterfaceType = new FullyQualifiedJavaType(
builder.delete(0, builder.length())
.append(serviceSuperPackage)
.append(".")
.append(domainObjectName)
.append("Service")
.toString()
);
TopLevelClass serviceImplClazz = new TopLevelClass(
builder.delete(0, builder.length())
.append(serviceImplSuperPackage)
.append(".")
.append(domainObjectName)
.append("ServiceImpl")
.toString()
);
serviceImplClazz.addSuperInterface(implInterfaceType);
serviceImplClazz.setSuperClass(superClazzType);
serviceImplClazz.setVisibility(JavaVisibility.PUBLIC);
serviceImplClazz.addAnnotation("@Service");
FullyQualifiedJavaType baseServiceInstance = FullyQualifiedJavaTypeProxyFactory.getBaseServiceImplInstance();
FullyQualifiedJavaType modelJavaType = new FullyQualifiedJavaType(entityClazzType);
FullyQualifiedJavaType exampleJavaType = new FullyQualifiedJavaType(entityExampleClazzType);
serviceImplClazz
.addImportedType(new FullyQualifiedJavaType("org.springframework.beans.factory.annotation.Autowired"));
serviceImplClazz.addImportedType(new FullyQualifiedJavaType("org.springframework.stereotype.Service"));
serviceImplClazz.addImportedType(baseServiceInstance);
serviceImplClazz.addImportedType(modelJavaType);
serviceImplClazz.addImportedType(exampleJavaType);
serviceImplClazz.addImportedType(implInterfaceType);
FullyQualifiedJavaType logType = new FullyQualifiedJavaType("org.slf4j.Logger");
FullyQualifiedJavaType logFactoryType = new FullyQualifiedJavaType("org.slf4j.LoggerFactory");
Field logField = new Field();
logField.setVisibility(JavaVisibility.PRIVATE);
logField.setStatic(true);
logField.setFinal(true);
logField.setType(logType);
logField.setName("logger");
logField.setInitializationString(
builder.delete(0, builder.length())
.append("LoggerFactory.getLogger(")
.append(domainObjectName)
.append("ServiceImpl.class)")
.toString()
);
logField.addAnnotation("");
logField.addAnnotation("@SuppressWarnings(\"unused\")");
serviceImplClazz.addField(logField);
serviceImplClazz.addImportedType(logType);
serviceImplClazz.addImportedType(logFactoryType);
String mapperName = builder.delete(0, builder.length())
.append(Character.toLowerCase(domainObjectName.charAt(0)))
.append(domainObjectName.substring(1))
.append("Mapper")
.toString();
FullyQualifiedJavaType JavaMapperType = new FullyQualifiedJavaType(javaMapperType);
Field mapperField = new Field();
mapperField.setVisibility(JavaVisibility.PUBLIC);
mapperField.setType(JavaMapperType);// Mapper.java
mapperField.setName(mapperName);
mapperField.addAnnotation("@Autowired");
serviceImplClazz.addField(mapperField);
serviceImplClazz.addImportedType(JavaMapperType);
Method mapperMethod = new Method();
mapperMethod.setVisibility(JavaVisibility.PUBLIC);
mapperMethod.setName("setMapper");
mapperMethod.addBodyLine("super.setMapper(" + mapperName + ");");
mapperMethod.addAnnotation("@Autowired");
serviceImplClazz.addMethod(mapperMethod);
serviceImplClazz.addFileCommentLine("/*** copyright (c) 2019 Marvis ***/");
serviceImplClazz
.addImportedType(new FullyQualifiedJavaType("org.springframework.beans.factory.annotation.Autowired"));
this.additionalServiceImplMethods(introspectedTable, serviceImplClazz, mapperName);
return serviceImplClazz;
}
protected void additionalServiceMethods(IntrospectedTable introspectedTable, Interface serviceInterface) {
if (this.notHasBLOBColumns(introspectedTable))
return;
introspectedTable.getGeneratedJavaFiles().stream().filter(file -> file.getCompilationUnit().isJavaInterface()
&& file.getCompilationUnit().getType().getShortName().endsWith("Mapper")).map(GeneratedJavaFile::getCompilationUnit).forEach(
compilationUnit -> ((Interface) compilationUnit).getMethods().forEach(
m -> serviceInterface.addMethod(this.additionalServiceLayerMethod(serviceInterface, m))));
}
protected void additionalServiceImplMethods(IntrospectedTable introspectedTable, TopLevelClass clazz,
String mapperName) {
if (this.notHasBLOBColumns(introspectedTable))
return;
introspectedTable.getGeneratedJavaFiles().stream().filter(file -> file.getCompilationUnit().isJavaInterface()
&& file.getCompilationUnit().getType().getShortName().endsWith("Mapper")).map(GeneratedJavaFile::getCompilationUnit).forEach(
compilationUnit -> ((Interface) compilationUnit).getMethods().forEach(m -> {
Method serviceImplMethod = this.additionalServiceLayerMethod(clazz, m);
serviceImplMethod.addAnnotation("@Override");
serviceImplMethod.addBodyLine(this.generateBodyForServiceImplMethod(mapperName, m));
clazz.addMethod(serviceImplMethod);
}));
}
private boolean notHasBLOBColumns(IntrospectedTable introspectedTable) {
return !introspectedTable.hasBLOBColumns();
}
private Method additionalServiceLayerMethod(CompilationUnit compilation, Method m) {
Method method = new Method();
method.setVisibility(JavaVisibility.PUBLIC);
method.setName(m.getName());
List<Parameter> parameters = m.getParameters();
method.getParameters().addAll(parameters.stream().peek(param -> param.getAnnotations().clear()).collect(Collectors.toList()));
method.setReturnType(m.getReturnType());
compilation.addImportedType(
new FullyQualifiedJavaType(m.getReturnType().getFullyQualifiedNameWithoutTypeParameters()));
return method;
}
private String generateBodyForServiceImplMethod(String mapperName, Method m) {
StringBuilder sbf = new StringBuilder("return ");
sbf.append(mapperName).append(".").append(m.getName()).append("(");
boolean singleParam = true;
for (Parameter parameter : m.getParameters()) {
if (singleParam)
singleParam = !singleParam;
else
sbf.append(", ");
sbf.append(parameter.getName());
}
sbf.append(");");
return sbf.toString();
}
}</code></pre>
<h3>4)ContextOverride</h3>
<pre><code class="java">package run.override.service;
import java.util.List;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.config.Context;
import org.mybatis.generator.config.ModelType;
public class ContextOverride extends Context{
//添加ServiceGeneratorConfiguration
private ServiceGeneratorConfiguration serviceGeneratorConfiguration;
public ContextOverride(ModelType defaultModelType) {
super(defaultModelType);
}
public ServiceGeneratorConfiguration getServiceGeneratorConfiguration() {
return serviceGeneratorConfiguration;
}
public void setServiceGeneratorConfiguration(ServiceGeneratorConfiguration serviceGeneratorConfiguration) {
this.serviceGeneratorConfiguration = serviceGeneratorConfiguration;
}
@Override
public void validate(List<String> errors) {
if(serviceGeneratorConfiguration != null)
serviceGeneratorConfiguration.validate(errors, this.getId());
super.validate(errors);
}
public XmlElement toXmlElement() {
XmlElement xmlElement = super.toXmlElement();
if (serviceGeneratorConfiguration != null)
xmlElement.addElement(serviceGeneratorConfiguration.toXmlElement());
return xmlElement;
}
}</code></pre>
<h3>5)MyBatisGeneratorConfigurationParserOverride</h3>
<pre><code class="java">package run.override.service;
import java.util.Properties;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.Context;
import org.mybatis.generator.config.JavaClientGeneratorConfiguration;
import org.mybatis.generator.config.ModelType;
import org.mybatis.generator.config.PluginConfiguration;
import org.mybatis.generator.config.xml.MyBatisGeneratorConfigurationParser;
import org.mybatis.generator.exception.XMLParserException;
import org.mybatis.generator.internal.util.StringUtility;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class MyBatisGeneratorConfigurationParserOverride extends MyBatisGeneratorConfigurationParser {
public MyBatisGeneratorConfigurationParserOverride(Properties extraProperties) {
super(extraProperties);
}
private void parseJavaServiceGenerator(Context context, Node node) {
ContextOverride contextOverride = ContextOverride.class.cast(context); ////替换Context
ServiceGeneratorConfiguration serviceGeneratorConfiguration = new ServiceGeneratorConfiguration();
contextOverride.setServiceGeneratorConfiguration(serviceGeneratorConfiguration);
Properties attributes = parseAttributes(node);
String targetPackage = attributes.getProperty("targetPackage");
String targetProject = attributes.getProperty("targetProject");
String implementationPackage = attributes.getProperty("implementationPackage");
serviceGeneratorConfiguration.setTargetPackage(targetPackage);
serviceGeneratorConfiguration.setTargetProject(targetProject);
serviceGeneratorConfiguration.setImplementationPackage(implementationPackage);
NodeList nodeList = node.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node childNode = nodeList.item(i);
if (childNode.getNodeType() == Node.ELEMENT_NODE && "property".equals(childNode.getNodeName()))
parseProperty(serviceGeneratorConfiguration, childNode);
}
}
@Override
public Configuration parseConfiguration(Element rootNode) throws XMLParserException {
Configuration configuration = new Configuration();
NodeList nodeList = rootNode.getChildNodes();
for (int i = 0; i < nodeList.getLength(); ++i) {
Node childNode = nodeList.item(i);
if (childNode.getNodeType() != 1) {
continue;
}
if ("properties".equals(childNode.getNodeName()))
parseProperties(configuration, childNode);
else if ("classPathEntry".equals(childNode.getNodeName()))
parseClassPathEntry(configuration, childNode);
else if ("context".equals(childNode.getNodeName())) {
parseContext(configuration, childNode);
}
}
return configuration;
}
private void parseContext(Configuration configuration, Node node) {
Properties attributes = parseAttributes(node);
String defaultModelType = attributes.getProperty("defaultModelType");
String targetRuntime = attributes.getProperty("targetRuntime");
String introspectedColumnImpl = attributes.getProperty("introspectedColumnImpl");
String id = attributes.getProperty("id");
ModelType mt = defaultModelType != null ? ModelType.getModelType(defaultModelType) : null;
Context context = new ContextOverride(mt);
context.setId(id);
if (StringUtility.stringHasValue(introspectedColumnImpl))
context.setIntrospectedColumnImpl(introspectedColumnImpl);
if (StringUtility.stringHasValue(targetRuntime))
context.setTargetRuntime(targetRuntime);
configuration.addContext(context);
NodeList nodeList = node.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node childNode = nodeList.item(i);
if (childNode.getNodeType() != 1)
continue;
if ("property".equals(childNode.getNodeName())) {
parseProperty(context, childNode);
continue;
}
if ("plugin".equals(childNode.getNodeName())) {
parsePlugin(context, childNode);
continue;
}
if ("commentGenerator".equals(childNode.getNodeName())) {
parseCommentGenerator(context, childNode);
continue;
}
if ("jdbcConnection".equals(childNode.getNodeName())) {
parseJdbcConnection(context, childNode);
continue;
}
if ("connectionFactory".equals(childNode.getNodeName())) {
parseConnectionFactory(context, childNode);
continue;
}
if ("javaModelGenerator".equals(childNode.getNodeName())) {
parseJavaModelGenerator(context, childNode);
continue;
}
if ("javaTypeResolver".equals(childNode.getNodeName())) {
parseJavaTypeResolver(context, childNode);
continue;
}
if ("sqlMapGenerator".equals(childNode.getNodeName())) {
parseSqlMapGenerator(context, childNode);
continue;
}
if ("javaClientGenerator".equals(childNode.getNodeName())) {
parseJavaClientGenerator(context, childNode);
continue;
}
if ("javaServiceGenerator".equals(childNode.getNodeName())) {
parseJavaServiceGenerator(context, childNode);
continue;
}
if ("table".equals(childNode.getNodeName()))
parseTable(context, childNode);
}
}
private void parsePlugin(Context context, Node node) {
PluginConfiguration pluginConfiguration = new PluginConfiguration();
context.addPluginConfiguration(pluginConfiguration);
Properties attributes = parseAttributes(node);
String type = attributes.getProperty("type");
pluginConfiguration.setConfigurationType(type);
NodeList nodeList = node.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node childNode = nodeList.item(i);
if (childNode.getNodeType() == 1 && "property".equals(childNode.getNodeName()))
parseProperty(pluginConfiguration, childNode);
}
}
private void parseJavaClientGenerator(Context context, Node node) {
JavaClientGeneratorConfiguration javaClientGeneratorConfiguration = new JavaClientGeneratorConfiguration();
context.setJavaClientGeneratorConfiguration(javaClientGeneratorConfiguration);
Properties attributes = parseAttributes(node);
String type = attributes.getProperty("type");
String targetPackage = attributes.getProperty("targetPackage");
String targetProject = attributes.getProperty("targetProject");
String implementationPackage = attributes.getProperty("implementationPackage");
javaClientGeneratorConfiguration.setConfigurationType(type);
javaClientGeneratorConfiguration.setTargetPackage(targetPackage);
javaClientGeneratorConfiguration.setTargetProject(targetProject);
javaClientGeneratorConfiguration.setImplementationPackage(implementationPackage);
NodeList nodeList = node.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node childNode = nodeList.item(i);
if (childNode.getNodeType() == 1 && "property".equals(childNode.getNodeName()))
parseProperty(javaClientGeneratorConfiguration, childNode);
}
}
}</code></pre>
<h3>6)ServiceGeneratorConfiguration</h3>
<pre><code class="java">package run.override.service;
import java.util.List;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.config.PropertyHolder;
import org.mybatis.generator.internal.util.StringUtility;
import org.mybatis.generator.internal.util.messages.Messages;
public class ServiceGeneratorConfiguration extends PropertyHolder {
private String targetPackage;
private String implementationPackage;
private String targetProject;
/**
*
*/
public ServiceGeneratorConfiguration() {
super();
}
public String getTargetPackage() {
return targetPackage;
}
public void setTargetPackage(String targetPackage) {
this.targetPackage = targetPackage;
}
public String getImplementationPackage() {
return implementationPackage;
}
public void setImplementationPackage(String implementationPackage) {
this.implementationPackage = implementationPackage;
}
public String getTargetProject() {
return targetProject;
}
public void setTargetProject(String targetProject) {
this.targetProject = targetProject;
}
public XmlElement toXmlElement() {
XmlElement answer = new XmlElement("javaServiceGenerator");
if (targetPackage != null) {
answer.addAttribute(new Attribute("targetPackage", targetPackage));
}
if (implementationPackage != null) {
answer.addAttribute(new Attribute("implementationPackage", targetPackage));
}
if (targetProject != null) {
answer.addAttribute(new Attribute("targetProject", targetProject));
}
addPropertyXmlElements(answer);
return answer;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public void validate(List errors, String contextId) {
if (!StringUtility.stringHasValue(getTargetProject()))
errors.add(Messages.getString("ValidationError.102", contextId));
if (!StringUtility.stringHasValue(getTargetPackage()))
errors.add(Messages.getString("ValidationError.112", "ServiceGenerator", contextId));
if (!StringUtility.stringHasValue(getImplementationPackage()))
errors.add(Messages.getString("ValidationError.120", contextId));
}
}</code></pre>
<h3>7)ConfigurationParserOverride</h3>
<pre><code class="java">package run.override.service;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.config.xml.MyBatisGeneratorConfigurationParser;
import org.mybatis.generator.config.xml.ParserEntityResolver;
import org.mybatis.generator.config.xml.ParserErrorHandler;
import org.mybatis.generator.exception.XMLParserException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
public class ConfigurationParserOverride extends ConfigurationParser {
private List<String> warnings;
private List<String> parseErrors;
private Properties extraProperties;
public ConfigurationParserOverride(List<String> warnings) {
this(null, warnings);
}
public ConfigurationParserOverride(Properties extraProperties, List<String> warnings) {
super(extraProperties, warnings);
this.extraProperties = extraProperties;
if (warnings == null)
this.warnings = new ArrayList<>();
else {
this.warnings = warnings;
}
this.parseErrors = new ArrayList<>();
}
@Override
public Configuration parseConfiguration(File inputFile) throws IOException, XMLParserException {
FileReader fr = new FileReader(inputFile);
return parseConfiguration(fr);
}
@Override
public Configuration parseConfiguration(InputStream inputStream) throws IOException, XMLParserException {
InputSource is = new InputSource(inputStream);
return parseConfiguration(is);
}
@Override
public Configuration parseConfiguration(Reader reader) throws IOException, XMLParserException {
InputSource is = new InputSource(reader);
return parseConfiguration(is);
}
private Configuration parseConfiguration(InputSource inputSource) throws IOException, XMLParserException {
this.parseErrors.clear();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(true);
try {
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(new ParserEntityResolver());
ParserErrorHandler handler = new ParserErrorHandler(this.warnings, this.parseErrors);
builder.setErrorHandler(handler);
Document document = null;
try {
document = builder.parse(inputSource);
} catch (SAXParseException e) {
throw new XMLParserException(this.parseErrors);
} catch (SAXException e) {
if (e.getException() == null)
this.parseErrors.add(e.getMessage());
else {
this.parseErrors.add(e.getException().getMessage());
}
}
if (this.parseErrors.size() > 0) {
throw new XMLParserException(this.parseErrors);
}
Element rootNode = document.getDocumentElement();
Configuration config = parseMyBatisGeneratorConfiguration(rootNode);
if (this.parseErrors.size() > 0) {
throw new XMLParserException(this.parseErrors);
}
return config;
} catch (ParserConfigurationException e) {
this.parseErrors.add(e.getMessage());
throw new XMLParserException(this.parseErrors);
}
}
private Configuration parseMyBatisGeneratorConfiguration(Element rootNode) throws XMLParserException {
//替换MyBatisGeneratorConfigurationParser
MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParserOverride(
this.extraProperties);
return parser.parseConfiguration(rootNode);
}
}</code></pre>
<h2>七、PluginChain</h2>
<p>通过继承,把以上扩展Plugin串起来(<code>SerializablePlugin</code>一些项目中可能不需要,故不加入Chain。同时,其他也可以根据需要对Chain进行更改)。</p>
<pre><code class="java">package run.override;
import run.override.service.ServiceLayerPlugin;
public class PluginChain extends ServiceLayerPlugin {
}
</code></pre>
<h2>八、generatorConfig.xml</h2>
<p>增加<code>javaServiceGenerator</code>相关配置标签。本文使用内部DTD做示例,亦可通过外部DTD或xsd来实现。</p>
<h3>1)generatorConfig.xml</h3>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<!-- 内部DTD 亦可通过外部DTD来实现-->
<!DOCTYPE generatorConfiguration
[
<!ELEMENT generatorConfiguration (properties?, classPathEntry*, context+)>
<!ELEMENT properties EMPTY>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED>
<!--
括号里是声明出现的次序:
*: 出现任意次,包括0次
?: 出现最多一次
|:选择之一
+: 出现最少1次
如果没有上述符号:必须且只能出现一次
-->
<!ELEMENT context (property*, plugin*, commentGenerator?, (connectionFactory | jdbcConnection), javaTypeResolver?,
javaModelGenerator, sqlMapGenerator, javaClientGenerator, javaServiceGenerator,table+)>
<!ATTLIST context id ID #REQUIRED
defaultModelType CDATA #IMPLIED
targetRuntime CDATA #IMPLIED
introspectedColumnImpl CDATA #IMPLIED>
<!ELEMENT connectionFactory (property*)>
<!ATTLIST connectionFactory
type CDATA #IMPLIED>
<!ELEMENT jdbcConnection (property*)>
<!ATTLIST jdbcConnection
driverClass CDATA #REQUIRED
connectionURL CDATA #REQUIRED
userId CDATA #IMPLIED
password CDATA #IMPLIED>
<!ELEMENT classPathEntry EMPTY>
<!ATTLIST classPathEntry
location CDATA #REQUIRED>
<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED>
<!ELEMENT plugin (property*)>
<!ATTLIST plugin
type CDATA #REQUIRED>
<!ELEMENT javaModelGenerator (property*)>
<!ATTLIST javaModelGenerator
targetPackage CDATA #REQUIRED
targetProject CDATA #REQUIRED>
<!ELEMENT javaTypeResolver (property*)>
<!ATTLIST javaTypeResolver
type CDATA #IMPLIED>
<!ELEMENT sqlMapGenerator (property*)>
<!ATTLIST sqlMapGenerator
targetPackage CDATA #REQUIRED
targetProject CDATA #REQUIRED>
<!ELEMENT javaClientGenerator (property*)>
<!ATTLIST javaClientGenerator
type CDATA #REQUIRED
targetPackage CDATA #REQUIRED
targetProject CDATA #REQUIRED
implementationPackage CDATA #IMPLIED>
<!ELEMENT javaServiceGenerator (property*)>
<!ATTLIST javaServiceGenerator
targetPackage CDATA #REQUIRED
implementationPackage CDATA #REQUIRED
targetProject CDATA #REQUIRED>
<!ELEMENT table (property*, generatedKey?, domainObjectRenamingRule?, columnRenamingRule?, (columnOverride | ignoreColumn | ignoreColumnsByRegex)*) >
<!ATTLIST table
catalog CDATA #IMPLIED
schema CDATA #IMPLIED
tableName CDATA #REQUIRED
alias CDATA #IMPLIED
domainObjectName CDATA #IMPLIED
mapperName CDATA #IMPLIED
sqlProviderName CDATA #IMPLIED
enableInsert CDATA #IMPLIED
enableSelectByPrimaryKey CDATA #IMPLIED
enableSelectByExample CDATA #IMPLIED
enableUpdateByPrimaryKey CDATA #IMPLIED
enableDeleteByPrimaryKey CDATA #IMPLIED
enableDeleteByExample CDATA #IMPLIED
enableCountByExample CDATA #IMPLIED
enableUpdateByExample CDATA #IMPLIED
selectByPrimaryKeyQueryId CDATA #IMPLIED
selectByExampleQueryId CDATA #IMPLIED
modelType CDATA #IMPLIED
escapeWildcards CDATA #IMPLIED
delimitIdentifiers CDATA #IMPLIED
delimitAllColumns CDATA #IMPLIED>
<!ELEMENT columnOverride (property*)>
<!ATTLIST columnOverride
column CDATA #REQUIRED
property CDATA #IMPLIED
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
isGeneratedAlways CDATA #IMPLIED
delimitedColumnName CDATA #IMPLIED>
<!ELEMENT ignoreColumn EMPTY>
<!ATTLIST ignoreColumn
column CDATA #REQUIRED
delimitedColumnName CDATA #IMPLIED>
<!ELEMENT ignoreColumnsByRegex (except*)>
<!ATTLIST ignoreColumnsByRegex
pattern CDATA #REQUIRED>
<!ELEMENT except EMPTY>
<!ATTLIST except
column CDATA #REQUIRED
delimitedColumnName CDATA #IMPLIED>
<!ELEMENT generatedKey EMPTY>
<!ATTLIST generatedKey
column CDATA #REQUIRED
sqlStatement CDATA #REQUIRED
identity CDATA #IMPLIED
type CDATA #IMPLIED>
<!ELEMENT domainObjectRenamingRule EMPTY>
<!ATTLIST domainObjectRenamingRule
searchString CDATA #REQUIRED
replaceString CDATA #IMPLIED>
<!ELEMENT columnRenamingRule EMPTY>
<!ATTLIST columnRenamingRule
searchString CDATA #REQUIRED
replaceString CDATA #IMPLIED>
<!ELEMENT commentGenerator (property*)>
<!ATTLIST commentGenerator
type CDATA #IMPLIED>
]
>
<generatorConfiguration>
<context id="ables" targetRuntime="MyBatis3">
<!--
添加Plugin
-->
<plugin type="run.override.PluginChain" />
<plugin type="run.override.SerializablePlugin" />
<plugin type="org.mybatis.generator.plugins.ToStringPlugin" />
<commentGenerator type="run.override.CommentGenerator"/>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxx?characterEncoding=utf8"
userId="xxx" password="xxx">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<javaModelGenerator targetPackage="cn.xxx.elecsign.model" targetProject=".\src">
<property name="enableSubPackages" value="false" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<sqlMapGenerator targetPackage="mapper.cn.xxx.elecsign.dao" targetProject=".\src">
<property name="enableSubPackages" value="false" />
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="cn.xxx.elecsign.dao" targetProject=".\src">
<property name="enableSubPackages" value="false" />
</javaClientGenerator>
<!-- javaServiceGenerator -->
<javaServiceGenerator targetPackage="cn.xxx.elecsign.dly.service"
implementationPackage = "cn.xxx.elecsign.dly.service.impl" targetProject=".\src">
<property name="enableSubPackages" value="false" />
</javaServiceGenerator>
<!-- 批次表,针对批量的异步操作 -->
<table tableName="table" domainObjectName="Table"
alias="table">
<generatedKey column="id" sqlStatement="MySql" identity="true" />
</table>
</context>
</generatorConfiguration>
</code></pre>
<h2>九、main启动</h2>
<pre><code class="java"> package run.generator;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.internal.DefaultShellCallback;
import run.override.service.ConfigurationParserOverride;
public class Generator {
public void generator() throws Exception{
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("generatorConfig.xml");
//替换ConfigurationParser
ConfigurationParserOverride cp = new ConfigurationParserOverride(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
public static void main(String[] args) throws Exception {
try {
Generator generator = new Generator();
generator.generator();
} catch (Exception e) {
e.printStackTrace();
}
}
}</code></pre>
<p>至此,对mybatis-generator的扩展生成代码完成。</p>
<blockquote>来源:宜信技术学院<p>作者:马伟伟</p>
</blockquote>
宜信如何做到既满足远程办公的短时便利性需求,又不丧失安全性
https://segmentfault.com/a/1190000021780142
2020-02-18T21:19:53+08:00
2020-02-18T21:19:53+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p>对于IT互联网企业来说远程办公并不陌生,但是疫情的突然爆发,直接大规模的使用远程办公应用,势必会带来一系列的安全问题,尤其是大量隐私数据安全问题,因为此次的疫情,大量的企业内部人员,需要从企业安全边界外部访问任何能够支持正常工作的账户文档或者数据,而如果相关的网络安全措施规则没有随之调整将产生巨大的安全隐患。另一方面的挑战来源于办公模式改变带来的安全威胁,现在要求既要满足远程办公的短时便利性需求,又不能过分的丧失安全性。</p>
<p>安全同事主要是配合远程办公下部分业务系统的网络变更工作,比如防火墙的实施工作、策略的开通等,这部分工作在2月2号之前都顺利验收通过,确保开工日远程办公的客服同事可以正常进行语音等工作。同时,正式远程办公前,对VPN这类关键的网络设备,进行了安全扫描,包括管理员页面、系统版本是否存在高危漏洞等。</p>
<p>之前大家多数是在下班后或临时不在职场时,使用VPN接入公司内网解决临时远程办公的需求,这个场景下的访问次数和人数都相对较小而分散。但是这次面对疫情下的远程办公模式,则是“全民皆兵”,各个方面的规模提升至少是之前的十倍、二十倍以上。对公司的安全设计来说,“远程接入”的威胁本来就大于“本地接入”的威胁,所以,对我们来说的变化就是:以前要对日常的远程用户做检查,做好防护工作,现在一下子变成了全体员工,因此检查手段、应对方式都得更新,否则很可能在远程办公期间发生安全事故。</p>
<p>由于很多人之前没有使用过远程办公这一整套系统,所以为了减轻大家上手时的工作量,我们当即决定简化了一些安全验证的手段和环节,优先确保大家可以正常接入公司网络。但是简化不等于放纵,所以我们比平时针对远程接入这部分场景进行了更深入、更密集的监控工作。针对VPN、堡垒机这两个关键的入口处,我们通过安全大数据平台,做了人员统计、访问资源统计的可视化,也包括了一些异常行为的统计分析和告警,比如:多次尝试登录失败可能意味着暴力破解用户口令、同一天一个账号关联到不同地区的IP可能意味着账号被盗用等。</p>
<p><img src="/img/remote/1460000021780146" alt="" title=""></p>
<p>《图:访问统计可视化》</p>
<p><img src="/img/remote/1460000021780148" alt="" title=""></p>
<p>《图:异常行为分析》</p>
<p>远程办公第一天,一大早我们就进行了防病毒的应急响应,这个是我们提前就预料到的,因为远程办公势必有大量的非公司电脑接入,病毒威胁会有所上升。通过监控告警,发现有几台终端尝试访问了内网上千个IP资源,同时关联发现防火墙有拦截到部分到服务器的请求,第一时间判断是计算机病毒在发起蠕虫攻击。我们随即将上午发现的携带病毒的计算机,进行了封禁账号,并通知员工对个人电脑安装防病毒软件进行杀毒后再使用VPN,并进行了远程办公期间安装防病毒软件的推广。</p>
<p><img src="/img/remote/1460000021780147" alt="" title=""></p>
<p>《图:访问资源数量异常》</p>
<p><img src="/img/remote/1460000021780145" alt="" title=""></p>
<p>《图:远程办公期间宣导安装防病毒软件》</p>
<p>疫情期间远程办公,还得重点防御一些伪装当前热点信息进行的邮件攻击,如在附件中植入病毒文件、发送钓鱼邮件等,这些攻击在远程办公期间的威胁也会比平时更大。所以对邮件主题、附件文档等,也加强了监控,比如“武汉”、“卫生部”这些关键字等等。</p>
<p>几周的远程办公下来,整体还是比较顺利的,离不开之前的准备和大家的相互配合,当然也还有很多需要提升的地方。最大的收获应该是改变了之前的一些观念上的限制。比如,如果长期维持在这种“零信任”的远程网络环境下,如何来设计安全的架构,都是值得继续更深入思考的课题。借用一起配合的邮件组的一位技术同事的话:“平时技术储备足,到了打硬仗的时候才能不慌”。</p>
<p>拓展阅读:</p>
<ul>
<li><a href="https://segmentfault.com/a/1190000021715857">揭秘:宜信科技中心如何支持公司史上最大规模全员远程办公|上篇</a></li>
<li><a href="https://segmentfault.com/a/1190000021721455">揭秘:宜信科技中心如何支持公司史上最大规模全员远程办公|下篇</a></li>
</ul>
干货:图解算法——动态规划系列
https://segmentfault.com/a/1190000021739367
2020-02-13T14:03:12+08:00
2020-02-13T14:03:12+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
25
<blockquote>小浩:宜信科技中心攻城狮一枚,热爱算法,热爱学习,不拘泥于枯燥编程代码,更喜欢用轻松方式把问题简单阐述,希望喜欢的小伙伴可以多多关注!</blockquote>
<h2>动态规划系列一:爬楼梯</h2>
<h3>1.1 概念讲解</h3>
<p>讲解动态规划的资料很多,官方的定义是指把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。概念中的各阶段之间的关系,其实指的就是状态转移方程。很多人觉得DP难(下文统称动态规划为DP),根本原因是因为DP区别于一些固定形式的算法(比如DFS、二分法、KMP),没有实际的步骤规定第一步第二步来做什么,所以准确的说,DP其实是一种解决问题的思想。</p>
<p>这种思想的本质是:一个规模比较大的问题(可以用两三个参数表示的问题),可以通过若干规模较小的问题的结果来得到的(通常会寻求到一些特殊的计算逻辑,如求最值等)</p>
<p><img src="/img/remote/1460000021739373" alt="" title=""></p>
<p>所以我们一般看到的状态转移方程,基本都是这样:</p>
<blockquote>opt :指代特殊的计算逻辑,通常为max or min。<p>i,j,k 都是在定义DP方程中用到的参数。</p>
<p>dp[i] = opt(dp[i-1])+1</p>
<p>dpi = w(i,j,k) + opt(dpi-1)</p>
<p>dpi = opt(dpi-1 + xi, dpi + yj, ...)</p>
</blockquote>
<p>每一个状态转移方程,多少都有一些细微的差别。这个其实很容易理解,世间的关系多了去了,不可能抽象出完全可以套用的公式。所以我个人其实不建议去死记硬背各种类型的状态转移方程。但是DP的题型真的就完全无法掌握,无法归类进行分析吗?我认为不是的。在本系列中,我将由简入深为大家讲解动态规划这个主题。</p>
<p>我们先看上一道最简单的DP题目,熟悉DP的概念:</p>
<p><strong>题目:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。</strong></p>
<blockquote>
<p><strong>示例 1:</strong></p>
<p>输入:2输出:2解释:有两种方法可以爬到楼顶。</p>
<ol>
<li>1 阶 + 1 阶</li>
<li>2 阶</li>
</ol>
<p><strong>示例 2:</strong></p>
<p>输入:3输出:3解释:有三种方法可以爬到楼顶。</p>
<ol>
<li>1 阶 + 1 阶 + 1 阶</li>
<li>1 阶 + 2 阶</li>
<li>2 阶 + 1 阶</li>
</ol>
</blockquote>
<h3>1.2 题目图解</h3>
<p>通过分析我们可以明确,该题可以被分解为一些包含最优子结构的子问题,即它的<strong>最优解可以从其子问题的最优解来有效地构建。</strong>满足<strong>“将大问题分解为若干个规模较小的问题”</strong>的条件。所以我们令 dp[n] 表示能到达第 n 阶的方法总数,可以得到如下状态转移方程:</p>
<blockquote>dp[n]=dp[n-1]+dp[n-2]</blockquote>
<ul>
<li>上 1 阶台阶:有1种方式。</li>
<li>上 2 阶台阶:有1+1和2两种方式。</li>
<li>上 3 阶台阶:到达第3阶的方法总数就是到第1阶和第2阶的方法数之和。</li>
<li>上 n 阶台阶,到达第n阶的方法总数就是到第 (n-1) 阶和第 (n-2) 阶的方法数之和。</li>
</ul>
<p><img src="/img/remote/1460000021739371" alt="" title=""></p>
<h3>1.3 Go语言示例</h3>
<p>根据分析,得到代码如下:</p>
<pre><code>func climbStairs(n int) int {
if n == 1 {
return 1
}
dp := make([]int, n+1)
dp[1] = 1
dp[2] = 2
for i := 3; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}</code></pre>
<h2>动态规划系列二:最大子序和</h2>
<h3>2.1 最大子序和</h3>
<p><strong>题目:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。</strong></p>
<blockquote>
<strong>示例 :</strong><p>输入: [-2,1,-3,4,-1,2,1,-5,4],</p>
<p>输出: 6</p>
<p>解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。</p>
</blockquote>
<p>拿到题目请不要看下方题解,先自行思考2-3分钟....</p>
<h3>2.2 题目图解</h3>
<p>首先我们分析题目,一个连续子数组一定要以一个数作为结尾,那么我们可以将状态定义成如下:</p>
<blockquote>dp[i]:表示以 nums[i] 结尾的连续子数组的最大和。</blockquote>
<p>那么为什么这么定义呢?因为这样定义其实是最容易想到的!在上一节中我们提到,状态转移方程其实是通过1-3个参数的方程来描述小规模问题和大规模问题间的关系。</p>
<p>当然,如果你没有想到,其实也非常正常!因为 "该问题最早于 1977 年提出,但是直到 1984 年才被发现了线性时间的最优解法。" </p>
<p>根据状态的定义,我们继续进行分析:</p>
<p>如果要得到dp[i],那么nums[i]一定会被选取。并且 dp[i] 所表示的连续子序列与 dp[i-1] 所表示的连续子序列很可能就差一个 nums[i] 。即</p>
<blockquote>dp[i] = dp[i-1]+nums[i] , if (dp[i-1] >= 0)</blockquote>
<p>但是这里我们遇到一个问题,<strong>很有可能dp[i-1]本身是一个负数。那这种情况的话,如果dp[i]通过dp[i-1]+nums[i]来推导,那么结果其实反而变小了,因为我们dp[i]要求的是最大和。</strong>所以在这种情况下,如果dp[i-1]<0,那么dp[i]其实就是nums[i]的值。即</p>
<blockquote>dp[i] = nums[i] , if (dp[i-1] < 0)</blockquote>
<p>综上分析,我们可以得到:</p>
<blockquote>dp[i]=max(nums[i], dp[i−1]+nums[i])</blockquote>
<p>得到了状态转移方程,但是我们还需要通过一个已有的状态的进行推导,我们可以想到 <strong>dp[0] 一定是以 nums[0] 进行结尾,</strong>所以</p>
<blockquote>dp[0] = nums[0]</blockquote>
<p>在很多题目中,因为dp[i]本身就定义成了题目中的问题,所以dp[i]最终就是要的答案。但是这里状态中的定义,并不是题目中要的问题,不能直接返回最后的一个状态 (这一步经常有初学者会摔跟头)。所以最终的答案,其实我们是寻找:</p>
<blockquote>max(dp[0], dp[1], ..., d[i-1], dp[i])</blockquote>
<p>分析完毕,我们绘制成图:</p>
<p>假定 nums 为 [-2,1,-3,4,-1,2,1,-5,4]</p>
<p><img src="/img/remote/1460000021739372" alt="" title=""></p>
<h3>2.3 Go语言示例</h3>
<p>根据分析,得到代码如下:</p>
<pre><code>func maxSubArray(nums []int) int {
if len(nums) < 1 {
return 0
}
dp := make([]int, len(nums))
//设置初始化值
dp[0] = nums[0]
for i := 1; i < len(nums); i++ {
//处理 dp[i-1] < 0 的情况
if dp[i-1] < 0 {
dp[i] = nums[i]
} else {
dp[i] = dp[i-1] + nums[i]
}
}
result := -1 << 31
for _, k := range dp {
result = max(result, k)
}
return result
}
func max(a, b int) int {
if a > b {
return a
}
return b
}</code></pre>
<p>我们可以进一步精简代码为:</p>
<pre><code>func maxSubArray(nums []int) int {
if len(nums) < 1 {
return 0
}
dp := make([]int, len(nums))
result := nums[0]
dp[0] = nums[0]
for i := 1; i < len(nums); i++ {
dp[i] = max(dp[i-1]+nums[i], nums[i])
result = max(dp[i], result)
}
return result
}
func max(a, b int) int {
if a > b {
return a
}
return b
}</code></pre>
<p>复杂度分析:时间复杂度:O(N)。空间复杂度:O(N)。</p>
<h2>动态规划系列三:最长上升子序列</h2>
<h3>3.1 最长上升子序列</h3>
<p><strong>题目:给定一个无序的整数数组,找到其中最长上升子序列的长度。</strong></p>
<blockquote>
<strong>示例:</strong><p>输入: [10,9,2,5,3,7,101,18]</p>
<p>输出: 4 </p>
<p>解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。</p>
<p>说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。</p>
</blockquote>
<p>本题有一定难度!</p>
<p>如果没有思路请回顾上一篇的学习内容!</p>
<p>不建议直接看题解!</p>
<h3>3.2 题目图解</h3>
<p>首先我们分析题目,要找的是最长上升子序列(Longest Increasing Subsequence,LIS)。因为题目中没有要求连续,所以LIS可能是连续的,也可能是非连续的。同时,LIS符合可以从其子问题的最优解来进行构建的条件。所以我们可以尝试用动态规划来进行求解。首先我们定义状态:</p>
<blockquote>dp[i] :表示以nums[i]结尾的最长上升子序列的长度</blockquote>
<p>我们假定nums为[1,9,5,9,3]</p>
<p><img src="/img/remote/1460000021739370" alt="" title=""></p>
<p>我们分两种情况进行讨论:</p>
<ul>
<li>如果nums[i]比前面的所有元素都小,那么dp[i]等于1(即它本身)(该结论正确)</li>
<li>如果nums[i]前面存在比他小的元素nums[j],那么dp[i]就等于dp[j]+1<strong>(该结论错误,比如nums[3]>nums[0],即9>1,但是dp[3]并不等于dp[0]+1)</strong>
</li>
</ul>
<p>我们先初步得出上面的结论,但是我们发现了一些问题。因为dp[i]前面比他小的元素,不一定只有一个!</p>
<p>可能除了nums[j],还包括nums[k],nums[p] 等等等等。所以dp[i]除了可能等于dp[j]+1,还有可能等于dp[k]+1,dp[p]+1 等等等等。所以我们求dp[i],需要找到dp[j]+1,dp[k]+1,dp[p]+1 等等等等中的最大值。(我在3个等等等等上都进行了加粗,主要是因为初学者非常容易在这里摔跟斗!这里强调的目的是希望能记住这道题型!)</p>
<p>即:</p>
<blockquote>dp[i] = max(dp[j]+1,dp[k]+1,dp[p]+1,.....)<p>只要满足:</p>
<p>nums[i] > nums[j]</p>
<p>nums[i] > nums[k]</p>
<p>nums[i] > nums[p]</p>
<p>....</p>
</blockquote>
<p>最后,我们只需要找到dp数组中的最大值,就是我们要找的答案。</p>
<p>分析完毕,我们绘制成图:</p>
<p><img src="/img/remote/1460000021739374" alt="" title=""></p>
<h3>3.3 Go语言示例</h3>
<p>根据分析,得到代码如下:</p>
<pre><code>func lengthOfLIS(nums []int) int {
if len(nums) < 1 {
return 0
}
dp := make([]int, len(nums))
result := 1
for i := 0; i < len(nums); i++ {
dp[i] = 1
for j := 0; j < i; j++ {
//这行代码就是上文中那个 等等等等
if nums[j] < nums[i] {
dp[i] = max(dp[j]+1, dp[i])
}
}
result = max(result, dp[i])
}
return result
}
func max(a, b int) int {
if a > b {
return a
}
return b
}</code></pre>
<h2>动态规划系列四:三角形最小路径和</h2>
<p>前面章节我们通过题目“最长上升子序列”以及"最大子序和",学习了DP(动态规划)在线性关系中的分析方法。这种分析方法,也在运筹学中被称为“线性动态规划”,具体指的是 “目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值”。这点大家作为了解即可,不需要死记,更不要生搬硬套!</p>
<p>在本节中,我们将继续分析一道略微区别于之前的题型,希望可以由此题与之前的题目进行对比论证,进而顺利求解!</p>
<h3>4.1 三角形最小路径和</h3>
<p><strong>题目:给定一个三角形,找出自顶向下的最小路径和。</strong></p>
<blockquote>
<strong>示例:</strong><p>每一步只能移动到下一行中相邻的结点上。</p>
<p>例如,给定三角形:</p>
<p><img src="/img/remote/1460000021739375" alt="" title=""></p>
<p>自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。</p>
</blockquote>
<h3>4.2 自顶向下图解分析</h3>
<p>首先我们分析题目,要找的是<strong>三角形最小路径和</strong>,这是个啥意思呢?假设我们有一个三角形:[[2], [3,4], [6,5,7], [4,1,8,3]]</p>
<p><img src="/img/remote/1460000021739379" alt="" title=""></p>
<p>那从上到下的最小路径和就是2-3-5-1,等于11。</p>
<p>由于我们是使用数组来定义一个三角形,所以便于我们分析,我们将三角形稍微进行改动:</p>
<p><img src="/img/remote/1460000021739377" alt="" title=""></p>
<p>这样相当于我们将整个三角形进行了拉伸。这时候,我们根据题目中给出的条件:每一步只能移动到下一行中相邻的结点上。其实也就等同于,每一步我们只能往下移动一格或者右下移动一格。将其转化成代码,假如2所在的元素位置为[0,0],那我们往下移动就只能移动到[1,0]或者[1,1]的位置上。假如5所在的位置为[2,1],同样也只能移动到[3,1]和[3,2]的位置上。如下图所示:</p>
<p><img src="/img/remote/1460000021739376" alt="" title=""></p>
<p>题目明确了之后,现在我们开始进行分析。题目很明显是一个找最优解的问题,并且可以从子问题的最优解进行构建。所以我们通过动态规划进行求解。首先,我们定义状态:</p>
<blockquote>dpi : 表示包含第i行j列元素的最小路径和</blockquote>
<p>我们很容易想到可以自顶向下进行分析。并且,无论最后的路径是哪一条,它一定要经过最顶上的元素,即[0,0]。所以我们需要对dp0进行初始化。</p>
<blockquote>dp0 = 0位置所在的元素值</blockquote>
<p>继续分析,如果我们要求dpi,那么其一定会从自己头顶上的两个元素移动而来。</p>
<p><img src="/img/remote/1460000021739378" alt="" title=""></p>
<p>如5这个位置的最小路径和,要么是从2-3-5而来,要么是从2-4-5而来。然后取两条路径和中较小的一个即可。进而我们得到状态转移方程:</p>
<blockquote>dpi = min(dpi-1,dpi-1) + trianglei</blockquote>
<p>但是,我们这里会遇到一个问题!除了最顶上的元素之外,</p>
<p><img src="/img/remote/1460000021739380" alt="" title=""></p>
<p>最左边的元素只能从自己头顶而来。(2-3-6-4)</p>
<p><img src="/img/remote/1460000021739381" alt="" title=""></p>
<p>最右边的元素只能从自己左上角而来。(2-4-7-3)</p>
<p>然后,我们观察发现,位于第2行的元素,都是特殊元素(因为都只能从[0,0]的元素走过来)</p>
<p><img src="/img/remote/1460000021739382" alt="" title=""></p>
<p>我们可以直接将其特殊处理,得到:</p>
<blockquote>dp1 = triangle1 + triangle0<p>dp1 = triangle1 + triangle0</p>
</blockquote>
<p>最后,我们只要找到最后一行元素中,路径和最小的一个,就是我们的答案。即:</p>
<blockquote>l:dp数组长度<p>result = min(dp[l-1,0],dp[l-1,1],dp[l-1,2]....)</p>
</blockquote>
<p>综上我们就分析完了,我们总共进行了4步:</p>
<ul>
<li>定义状态</li>
<li>总结状态转移方程</li>
<li>分析状态转移方程不能满足的特殊情况。</li>
<li>得到最终解</li>
</ul>
<h3>4.3 代码分析</h3>
<p>分析完毕,代码自成:</p>
<pre><code>func minimumTotal(triangle [][]int) int {
if len(triangle) < 1 {
return 0
}
if len(triangle) == 1 {
return triangle[0][0]
}
dp := make([][]int, len(triangle))
for i, arr := range triangle {
dp[i] = make([]int, len(arr))
}
result := 1<<31 - 1
dp[0][0] = triangle[0][0]
dp[1][1] = triangle[1][1] + triangle[0][0]
dp[1][0] = triangle[1][0] + triangle[0][0]
for i := 2; i < len(triangle); i++ {
for j := 0; j < len(triangle[i]); j++ {
if j == 0 {
dp[i][j] = dp[i-1][j] + triangle[i][j]
} else if j == (len(triangle[i]) - 1) {
dp[i][j] = dp[i-1][j-1] + triangle[i][j]
} else {
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
}
}
}
for _,k := range dp[len(dp)-1] {
result = min(result, k)
}
return result
}
func min(a, b int) int {
if a > b {
return b
}
return a
}</code></pre>
<p><img src="/img/remote/1460000021739384" alt="" title=""></p>
<p>运行上面的代码,我们发现使用的内存过大。我们有没有什么办法可以压缩内存呢?通过观察我们发现,<strong>在我们自顶向下的过程中,其实我们只需要使用到上一层中已经累积计算完毕的数据,并且不会再次访问之前的元素数据。</strong>绘制成图如下:</p>
<p><img src="/img/remote/1460000021739383" alt="" title=""></p>
<p>优化后的代码如下:</p>
<pre><code>func minimumTotal(triangle [][]int) int {
l := len(triangle)
if l < 1 {
return 0
}
if l == 1 {
return triangle[0][0]
}
result := 1<<31 - 1
triangle[0][0] = triangle[0][0]
triangle[1][1] = triangle[1][1] + triangle[0][0]
triangle[1][0] = triangle[1][0] + triangle[0][0]
for i := 2; i < l; i++ {
for j := 0; j < len(triangle[i]); j++ {
if j == 0 {
triangle[i][j] = triangle[i-1][j] + triangle[i][j]
} else if j == (len(triangle[i]) - 1) {
triangle[i][j] = triangle[i-1][j-1] + triangle[i][j]
} else {
triangle[i][j] = min(triangle[i-1][j-1], triangle[i-1][j]) + triangle[i][j]
}
}
}
for _,k := range triangle[l-1] {
result = min(result, k)
}
return result
}
func min(a, b int) int {
if a > b {
return b
}
return a
}</code></pre>
<p><img src="/img/remote/1460000021739389" alt="" title=""></p>
<h2>动态规划系列五:最小路径和</h2>
<p>在上节中,我们通过分析,顺利完成了“三角形最小路径和”的动态规划题解。在本节中,我们继续看一道相似题型,以求能完全掌握这种“路径和”的问题。话不多说,先看题目:</p>
<h3>5.1 最小路径和</h3>
<p><strong>题目:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。</strong></p>
<blockquote>
<strong>示例:</strong><p>输入:</p>
<p>[</p>
<p>[1,3,1],</p>
<p>[1,5,1],</p>
<p>[4,2,1]</p>
<p>]</p>
<p>输出: 7</p>
<p>解释: 因为路径 1→3→1→1→1 的总和最小。</p>
</blockquote>
<h3>5.2 图解分析</h3>
<p>首先我们分析题目,要找的是 最小路径和,这是个啥意思呢?假设我们有一个 m*n 的矩形 :[[1,3,1],[1,5,1],[4,2,1]]</p>
<p><img src="/img/remote/1460000021739387" alt="" title=""></p>
<p>那从左上角到右下角的最小路径和,我们可以很容易看出就是1-3-1-1-1,这一条路径,结果等于7。</p>
<p>题目明确了,我们继续进行分析。该题与上一道求三角形最小路径和一样,题目明显符合可以从子问题的最优解进行构建,所以我们考虑使用动态规划进行求解。首先,我们定义状态:</p>
<blockquote>dpi : 表示包含第i行j列元素的最小路径和</blockquote>
<p>同样,因为任何一条到达右下角的路径,都会经过[0,0]这个元素。所以我们需要对dp0进行初始化。</p>
<blockquote>dp0 = 0位置所在的元素值</blockquote>
<p>继续分析,根据题目给的条件,如果我们要求dpi,那么它一定是从自己的上方或者左边移动而来。如下图所示:</p>
<p><img src="/img/remote/1460000021739388" alt="" title=""></p>
<ul>
<li>5,只能从3或者1移动而来</li>
<li>2,只能从5或者4移动而来</li>
<li>4,从1移动而来</li>
<li>3,从1移动而来</li>
<li>红色位置必须从蓝色位置移动而来</li>
</ul>
<p>进而我们得到状态转移方程:</p>
<blockquote>dpi = min(dpi-1,dpi) + gridi</blockquote>
<p>同样我们需要考虑两种特殊情况:</p>
<ul>
<li>最上面一行,只能由左边移动而来(1-3-1)</li>
<li>最左边一列,只能由上面移动而来(1-1-4)</li>
</ul>
<p><img src="/img/remote/1460000021739390" alt="" title=""></p>
<p>最后,因为我们的目标是从左上角走到右下角,整个网格的最小路径和其实就是包含右下角元素的最小路径和。即:</p>
<blockquote>设:dp的长度为l<p>最终结果就是:dpl-1)-1]</p>
</blockquote>
<p>综上我们就分析完了,我们总共进行了4步:</p>
<ul>
<li>定义状态</li>
<li>总结状态转移方程</li>
<li>分析状态转移方程不能满足的特殊情况。</li>
<li>得到最终解</li>
</ul>
<h3>5.3 代码分析</h3>
<p>分析完毕,代码自成:</p>
<pre><code>func minPathSum(grid [][]int) int {
l := len(grid)
if l < 1 {
return 0
}
dp := make([][]int, l)
for i, arr := range grid {
dp[i] = make([]int, len(arr))
}
dp[0][0] = grid[0][0]
for i := 0; i < l; i++ {
for j := 0; j < len(grid[i]); j++ {
if i == 0 && j != 0 {
dp[i][j] = dp[i][j-1] + grid[i][j]
} else if j == 0 && i != 0 {
dp[i][j] = dp[i-1][j] + grid[i][j]
} else if i != 0 && j != 0 {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
}
}
}
return dp[l-1][len(dp[l-1])-1]
}
func min(a, b int) int {
if a > b {
return b
}
return a
}</code></pre>
<p><img src="/img/remote/1460000021739391" alt="" title=""></p>
<p>同样,运行上面的代码,我们发现使用的内存过大。有没有什么办法可以压缩内存呢?通过观察我们发现,<strong>在我们自左上角到右下角计算各个节点的最小路径和的过程中,我们只需要使用到之前已经累积计算完毕的数据,</strong>并且不会再次访问之前的元素数据。绘制成图如下:(大家看这个过程像不像扫雷,其实如果大家研究扫雷外挂的话,就会发现在扫雷的核心算法中,就有一处颇为类似这种分析方法,这里就不深究了)</p>
<p><img src="/img/remote/1460000021739385" alt="" title=""></p>
<p>优化后的代码如下:</p>
<pre><code>func minPathSum(grid [][]int) int {
l := len(grid)
if l < 1 {
return 0
}
for i := 0; i < l; i++ {
for j := 0; j < len(grid[i]); j++ {
if i == 0 && j != 0 {
grid[i][j] = grid[i][j-1] + grid[i][j]
} else if j == 0 && i != 0 {
grid[i][j] = grid[i-1][j] + grid[i][j]
} else if i != 0 && j != 0 {
grid[i][j] = min(grid[i-1][j], grid[i][j-1]) + grid[i][j]
}
}
}
return grid[l-1][len(grid[l-1])-1]
}
func min(a, b int) int {
if a > b {
return b
}
return a
}</code></pre>
<p><img src="/img/remote/1460000021739386" alt="" title=""></p>
<blockquote>本系列所有教程中都不会用到复杂的语言特性,大家不需要担心没有学过go。算法思想最重要,使用go纯属作者爱好。<p>原文首发于公众号-浩仔讲算法</p>
</blockquote>
揭秘:宜信科技中心如何支持公司史上最大规模全员远程办公|下篇
https://segmentfault.com/a/1190000021721455
2020-02-11T10:43:00+08:00
2020-02-11T10:43:00+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<p><a href="https://segmentfault.com/a/1190000021715857">揭秘:宜信科技中心如何支持公司史上最大规模全员远程办公|上篇 </a>中,我们介绍了宜信科技中心支持史上最大规模远程办公的方案:通过SSLVPN 实现远程访问数据中心和业务系统,同时辅助办公协同系统、会议系统等信息传递,确保足够的网络带宽保障数据正常传输,并介绍了我们在IT运维方面的部署。</p>
<p>客户服务是金融业务重要组成部分,亦是劳动密集型业务,如何保证业务和数据安全,不影响公司正常运营,实现安全顺畅的远程办公,成为当下较为突出的挑战。今天我们分享宜信科技中心在保证呼叫中心远程办公方面的详细部署以及宜信科技中心在远程办公安全方面的关键举措。</p>
<h2>一、AI机器人全面上线</h2>
<p>客户服务是金融业务重要组成部分,亦是劳动密集型业务,如何保证业务和数据安全,不影响公司正常运营,成为当下较为突出的挑战。为了应对挑战,科技中心团队联合作战,充分利用AI、云计算、大数据等科技手段突围。</p>
<p>宜信技术团队快速响应调整,紧急协调并部署了AI机器人,替代人工,每日对全平台所有客户进行智能化的业务操作提醒。</p>
<p>AI机器人通过NLP语言构建的对话管理系统,标准的业务话术机器人与客户智能对话方式,这套智能服务系统不仅7x24小时在线服务,同时还结合大数据图谱技术构建了超过68种金融业务场景,应用于公司内部的全部业务的自动化客户服务过程,对比人工服务的效果,可联率与接通率得到了较大的提升;与此同时,通过机器人的话术进行标准业务服务大幅度降低了人工操作风险及合规风险,让AI机器人服务变得更加有温度。</p>
<p>对系统进行紧急部署后还不够,针对此次疫情,技术团队又增加了两项紧急措施:</p>
<ul>
<li>针对疫情期间因限制无法正常进行业务操作的客户,紧急新增“疫情专题”场景话术,快速的引导客户完成相关业务操作,以最大化的保障整体业务指标稳定;</li>
<li>为了保障服务质量,针对对话异常的客户,系统会引导进入人工客服协助解决问题,AI机器人+少量人工辅助,业务伙伴实时在线值守AI机器人的外呼情况。</li>
</ul>
<p>这两项紧急措施,很大程度上缓解了客服人员的咨询压力。下面一节我们就分享一下科技中心是如何保障人工客服在远程办公条件下稳定提供服务的。</p>
<h2>二、公网呼叫中心的部署</h2>
<p>对于宜信公司的大部分的伙伴,通过VPN访问数据中心,保障网络安全,就基本可以解决远程办公方面的痛点,但是对于金融机构的呼叫中心团队这还远远不够。</p>
<p>目前呼叫中心的部署采用的是本地部署,系统性能扩容与硬件设备紧密相关,立即部署远程坐席需要临时采购设备,从采购订单、货运物流、实施部署直至项目交付,建设成本高,实施周期长,在应急状态下不具备操作可能性。呼叫中心的坐席要远程登陆,必须接入公司本地网络,严重依赖公司本地网络的带宽和质量,很难在短时间内进行应急扩容及设置安全策略,品质很难保证,且容易造成安全隐患。另外在这样的突发状态下,呼叫中心话务量有可能突增。本地部署呼叫中心,中继资源是有限的,无法应对话务的变化。</p>
<p>客服、售后体系作为连接金融机构与投资者的重要纽带,为了支持客服人员在疫情期间正常开展业务,春节期间,技术团队用了一天时间配合网络,机房,安全等团队制定方案,建立基于公网的呼叫中心。通过安全团队的协助有效做到网络安全,抵御外界黑客攻击、并将员工所能接触到的客户信息做了有效屏蔽处理,防止用户信息泄,保证信息安全。</p>
<p>在此期间,技术团队迅速部署开展工作:机房搬迁,设备上架,调试,书写Q&A文档,对使用系统的伙伴进行培训及协调组织运维支持等相关内容。在24小时内完成了基于公网接入的呼叫中心系统的搭建工作,使得所有需要通过软电话进行沟通的业务,均可在家通过公网办公。</p>
<p>数据层面,实现通过VPN方式接入。通过数据测算和分析出在线人数峰值和普通值,设计部署接入容量。在这样的紧急时刻值班工程师支撑了全部呼叫中心全部业务人员系统的接入和安装服务。呼叫中心系统的技术同事一直全力支持业务人员的业务开展,目前系统运行平稳,保障了信审、CSC、RSC、反欺诈、理财等业务服务保持在线。</p>
<h2>三、安全方面的关键举措</h2>
<p>对于IT互联网企业来说远程办公并不陌生,但是疫情的突然爆发,直接大规模的使用远程办公应用,势必会带来一系列的安全问题,尤其是大量隐私数据安全问题,因为此次的疫情,大量的企业内部人员,需要从企业安全边界外部访问任何能够支持正常工作的账户文档或者数据,而如果相关的网络安全措施规则没有随之调整将产生巨大的安全隐患。另一方面的挑战来源于办公模式改变带来的安全威胁,现在要求既要满足远程办公的短时便利性需求,又不能过分的丧失安全性。由于“人”是远程办公环境中最不受控的因素,因此安全意识宣导尤为重要,让员工具备保护公司数据资产的意识和识别安全威胁、判断高风险行为的能力,比如识别钓鱼邮件(伪装热点信息邮件攻击在邮件过滤黑名单中加入“武汉”、“卫生部”等关键词)、网站和WIFI、防止监窥、禁止随意拷贝存储公司数据等。</p>
<p>对于VPN的使用:一方面解决了身份认证的问题以保证接入用户的合法性,对传输通道进行加密,保证传输中的数据无法遭到窃听。无论您连接的是哪种WiFi,VPN都可以通过安全连接来提高数据通讯的安全。保护远程团队的数据、信息和密码,免遭各种埋伏在公共WiFi网络中的不怀好意的(中间)攻击者窥视和窃取。另外一方面:我们通过安全大数据平台,做了人员统计、访问资源统计的可视化,也包括了一些异常行为的统计分析和告警。比如:多次尝试登录失败可能意味着暴力破解用户口令、同一天一个账号关联到不同地区的IP可能意味着账号被盗用等。同时,在正式开始远程办公前,对VPN这类关键的网络设备,进行了安全扫描,包括管理员页面、系统版本是否存在高危漏洞等。</p>
<p>防病毒应急响应+要求远程办公期间安装防病毒软件:我们提前预料到远程办公的第一天,势必有大量的非公司电脑接入,病毒威胁会有所上升。通过监控告警,判断是计算机病毒发起的蠕虫攻击,立即采取,封禁账号并通知员工对个人电脑安装防病毒软件,进行杀毒后,再使用VPN。</p>
<p>这几天远程办公下来,整体还是比较顺利的,离不开之前的准备和大家的相互配合,当然也还有很多需要提升的地方。最大的收获应该是改变了之前的一些观念上的限制,比如,如果长期维持在这种“零信任”的远程网络环境下,如何来设计安全的架构,都是值得继续更深入思考的课题。借用一起配合的邮件组的一位技术同事的话:“平时技术储备足,到了打硬仗的时候才能不慌”。</p>
<h2>四、总结&未来</h2>
<p>总结:就目前的反馈来看,科技中心为宜信全员远程办公提供支持这件事,获得了阶段性的成功,这次成功离不开领导在面对对突发事件的决策,各个技术团队的密切配合,宜信伙伴们长久以来形成的默契,以及公司管理层面部署的积累。对于未来我们也有更深的思考。</p>
<p>突如其来的疫情对一些行业带来了极大的影响,使其不得不按下暂停键,在这些行业中,不乏中小微企业的身影,作为社会经济的一份子,中小微企业的抗风险能力通常较弱。很多企业在高速发展的同时,为了节约成本,降低基础运维人员成本,导致各项链条绷得太紧,一旦出现特殊情况,就不知道如何应对,希望宜信本次的探索可以给中小微企业提供一些技术层面的参考和借鉴。</p>
<p>互联网环境下,企业管理方式变革已成为一个新的研究焦点。随着互联网企业增多,远程办公作为新的办公模式开始进入研究者的视野。新一代信息技术为远程办公的发展提供了良好的环境。远程办公作为与互联网时代相适应的办公模式,为新环境下企业变革提供了新的思路。</p>
<p>希望宜信科技中心在本次远程办公中技术层面的探索,可以作为企业管理研究的一部分参考样本,也希望在新的环境下的互联网技术可以与企业管理方法相整合、开拓远程办公管理和应急突发事件管理的新视野,最终提高远程办公效率和中小微企业抵抗风险的能力。相信,在互联网时代,远程办公和企业的管理有新型信息技术的加持,未来一定会焕发出新的生命力。</p>
<blockquote>写在最后<p>在写这篇文章之前,我采访了很多科技中心的伙伴,今天之所以写这篇文章,是因为我听到了很多让我很感动的东西,这次的超大规模的技术支持,发生了太多太多的事,这里有太多宜信人的默默付出,我们统筹全局的领导们,连夜进行网络部署的IT运维小哥哥们,忙碌于编写代码的程序猿欧巴们,还有一直为我们的安全守护的网络骑士们,还有... ...他们都做了很多很多的事情,才实现了史上最大规模的远程办公的成功。在这里我想说,当我们去面对这些突发情况的时候,也请大家记住一些人,记住一些事情,这些都一定会成为以后让我们走的更加坚定的信念。</p>
</blockquote>
揭秘:宜信科技中心如何支持公司史上最大规模全员远程办公|上篇
https://segmentfault.com/a/1190000021715857
2020-02-10T13:18:16+08:00
2020-02-10T13:18:16+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
1
<p>疫情还在继续,大部分人已经开启为期一周,甚至更长时间的远程办公生涯。远程办公最重要的,是自我隔离可以有效防止疫情的传播。但远程办公,其实没有那么容易,作为一家金融科技旗舰企业,远程办公所面临的挑战不仅仅是沟通交流的不便,团队协同性的维持,管理效率的稳定,还有更重要的是:<strong>如何做到在最短的时间内支持上万人同时在线办公;如何解决多样网络以及各类电脑、操作系统全部安全快速部署;如何高效保障全部业务团队的技术支持等等... ...</strong> 今天我们就带大家<strong>揭秘宜信科技中心如何支持公司史上最大规模远程办公——上篇。</strong></p>
<h3>快速部署,制定方案</h3>
<p>此次远程办公由<strong>宜信CTO向江旭</strong>亲自主导、统筹、协调资源、制定解决方案并迅速部署。主要方案为:<strong>通过SSLVPN 实现远程访问数据中心和业务系统,同时辅助办公协同系统、会议系统等信息传递,确保足够的网络带宽保障数据正常传输,实现全体宜信人在2月3日顺利开展远程办公。</strong></p>
<h3>详解远程办公方案</h3>
<p><strong>VPN是虚拟私有网络</strong>(Virtual Private Network)英文缩写,SSL VPN的是Security Socket Layer-SSL VPN的缩写,是一种加密的隧道技术,在非公司网络环境下,可以个人电脑上运行SSL VPN客户端,<strong>帮助我们直接访问在公司才能访问的办公系统或是业务系统,整个过程是安全加密的。</strong></p>
<p>宜信公司有非常多的职能部门,分散在各地的办公职场,职能部门越多,相应的业务方面的信息系统也越多,这些信息系统都部署在北京的数据中心里。对于宜信而言,<strong>实现全员远程办公的关键在于如何保障让分布在全国乃至全球各地的宜信小伙伴能够使用家庭网络,安全、稳定的访问到位于数据中心的办公系统、业务系统等。</strong></p>
<p>在正常的办公环境下,分散在各地的职能部门可以通过多种途径接入数据中心,比如专线、IPSEC VPN等方式, 这些接入方式在数据中心网络架构层面通常都是规划到不同的入口上,带宽、负载也会分担到不同的设备。</p>
<p>通过SSLVPN接入到数据中心,实现远程办公是一个非常成熟的方案,这种方式简单易用,有浏览器,访问一个网址,完成认证和授权,就可以进入数据中心内部访问相关的数据资源。<strong>但SSLVPN方案通常只是一种辅助手段,是用户进入数据中心的其中一途径,主要是为了满足8小时外的应急处理,</strong>在线人数不会很多,相应的资源,比如带宽满足正常需求就可以了。</p>
<p>在正常工作条件情况下,支撑公司全员同时接入到数据中心并不是一件难事,比如可以通过新增带宽、新购设备、改变网络架构等各种方法。但在春节假期武汉疫情突然爆发的场景下,无论是公司内部,还是供应商,人力资源都变得很少,满足春节后的万人 在线办公就成为了一个巨大的挑战。巧妇难为无米之炊,能够在短时间内解决这些问题,这就需要依托之前的积累,基础设施的可靠性、冗余度,良好的供应商管理,以及春节假期前的应急人力资源安排;<strong>除此之外,最重要是根据掌握的调研数据,对事态进行预判,评估SSLVPN的接入人数,测算出对资源的需求。</strong></p>
<p><strong>通过对事态的预判并结合调研的数据</strong>,评估出需要接入数据中心的<strong>总人数和在线人数的峰值</strong>,和保障良好办公状态下宽带应该满足的接入带宽、和遇到突发情况涉及到各个团队情况下可能会达到的<strong>带宽峰值</strong>。根据数据分析结果带宽和在线人数许可进行扩容,<strong>同时进行实时监控,随时进行动态调整。虽然历经波折,但是最终都顺利通过。在此也要非常感谢我们的供应商,非常时期为我们提供免费带宽、SSLVPN许可和免费软件SSLVPN解决方案等资源支持。</strong></p>
<h3>IT运维服务团队支持</h3>
<p>VPN的计划和方案都落地后,就开始全员接入VPN。考虑到本次远程办公由于公司网络环境和在家办公环境不同,不但网络有改变,电脑也有改变,因此可能会面临大量的软件安装、业务系统使用、账号密码不记得等问题,IT运维服务团队的同事提前编辑使用说明书和知识文档,并通过不同的方式对本次远程办公知识文档进行宣导,比如发全员邮件通知、蜜蜂集结号等。</p>
<p>同时,安排系统运维部IT团队、网络团队、服务台等团队做好电话、微信、蜜蜂支持准备,并根据业务分类、员工人数的不同,提供定制化支持安排。<strong>其中通过社群将对系统软件安装和远程办公服务工作了解不深入的同事聚集,在群内直接分享问题解决问题,缓解了IT运维服务团队很大的咨询压力。</strong>从目前工作反馈情况来看,算是成功的。</p>
<blockquote>截至2月2日,可以满足全体宜信人在家办公技术条件已经完全成熟,通过5天的实际运行,此方案成功支持了宜信全员远程办公的日常工作,但是这并不是结束,这是一个新的开始,技术团队的伙伴将持续为全体宜信人提供技术支持。<p>整个远程办公技术支持过程的内容梳理还在进行中,后面我们会继续分享本次远程办公在安全方面的关键举措,以及在这样的突发状态下,呼叫中心话务量有可能突增,而本地部署呼叫中心中继资源有限无法应对话务的变化,宜信的技术团队是如何保障呼叫中心团队的伙伴远程办公的?希望可以给大家提供一个参考。</p>
</blockquote>
<p><strong>加油武汉!加油中国!</strong> </p>
<p><strong>加油宜家人!</strong></p>
从操作系统层面理解Linux下的网络IO模型
https://segmentfault.com/a/1190000021587865
2020-01-15T11:25:53+08:00
2020-01-15T11:25:53+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
62
<p>I/O( INPUT OUTPUT),包括文件I/O、网络I/O。</p>
<p>计算机世界里的速度鄙视:</p>
<ul>
<li>内存读数据:纳秒级别。</li>
<li>千兆网卡读数据:微妙级别。1微秒=1000纳秒,网卡比内存慢了千倍。</li>
<li>磁盘读数据:毫秒级别。1毫秒=10万纳秒 ,硬盘比内存慢了10万倍。</li>
<li>CPU一个时钟周期1纳秒上下,内存算是比较接近CPU的,其他都等不起。</li>
</ul>
<p>CPU 处理数据的速度远大于I/O准备数据的速度 。</p>
<p>任何编程语言都会遇到这种CPU处理速度和I/O速度不匹配的问题!</p>
<p>在网络编程中如何进行网络I/O优化:怎么高效地利用CPU进行网络数据处理???</p>
<h2>一、相关概念</h2>
<p>从操作系统层面怎么理解网络I/O呢?计算机的世界有一套自己定义的概念。如果不明白这些概念,就无法真正明白技术的设计思路和本质。所以在我看来,这些概念是了解技术和计算机世界的基础。</p>
<h3>1.1 同步与异步,阻塞与非阻塞</h3>
<p>理解网络I/O避不开的话题:同步与异步,阻塞与非阻塞。</p>
<p>拿山治烧水举例来说,(山治的行为好比用户程序,烧水好比内核提供的系统调用),这两组概念翻译成大白话可以这么理解。</p>
<ul>
<li>同步/异步关注的是水烧开之后需不需要我来处理。</li>
<li>阻塞/非阻塞关注的是在水烧开的这段时间是不是干了其他事。</li>
</ul>
<h3>1.1.1 同步阻塞</h3>
<p>点火后,傻等,不等到水开坚决不干任何事(阻塞),水开了关火(同步)。</p>
<p><img src="/img/remote/1460000021587872" alt="" title=""></p>
<h4>1.1.2 同步非阻塞</h4>
<p>点火后,去看电视(非阻塞),时不时看水开了没有,水开后关火(同步)。</p>
<p><img src="/img/remote/1460000021587873" alt="" title=""></p>
<h4>1.1.3 异步阻塞</h4>
<p>按下开关后,傻等水开(阻塞),水开后自动断电(异步)。</p>
<p><img src="/img/remote/1460000021587868" alt="" title=""></p>
<p>网络编程中不存在的模型。</p>
<h4>1.1.4 异步非阻塞</h4>
<p>按下开关后,该干嘛干嘛 (非阻塞),水开后自动断电(异步)。</p>
<p><img src="/img/remote/1460000021587875" alt="" title=""></p>
<h3>1.2 内核空间 、用户空间</h3>
<p><img src="/img/remote/1460000021587869" alt="" title=""></p>
<ul>
<li>内核负责网络和文件数据的读写。</li>
<li>用户程序通过系统调用获得网络和文件的数据。</li>
</ul>
<h4>1.2.1 内核态 用户态</h4>
<p><img src="/img/remote/1460000021587870" alt="" title=""></p>
<ul>
<li>程序为读写数据不得不发生系统调用。</li>
<li>通过系统调用接口,线程从用户态切换到内核态,内核读写数据后,再切换回来。</li>
<li>进程或线程的不同空间状态。</li>
</ul>
<h4>1.2.2 线程的切换</h4>
<p><img src="/img/remote/1460000021587871" alt="" title=""></p>
<p>用户态和内核态的切换耗时,费资源(内存、CPU)</p>
<p>优化建议:</p>
<ul>
<li>更少的切换。</li>
<li>共享空间。</li>
</ul>
<h3>1.3 套接字 – socket</h3>
<p><img src="/img/remote/1460000021587877" alt="" title=""></p>
<ul>
<li>有了套接字,才可以进行网络编程。</li>
<li>应用程序通过系统调用socket(),建立连接,接收和发送数据(I / O)。</li>
<li>SOCKET 支持了非阻塞,应用程序才能非阻塞调用,支持了异步,应用程序才能异步调用</li>
</ul>
<h3>1.4 文件描述符 –FD 句柄</h3>
<p><img src="/img/remote/1460000021587876" alt="" title=""></p>
<p><img src="/img/remote/1460000021587874" alt="" title=""></p>
<p><img src="/img/remote/1460000021587881" alt="" title=""></p>
<p>网络编程都需要知道FD??? FD是个什么鬼???</p>
<p>Linux:万物都是文件,FD就是文件的引用。像不像JAVA中万物都是对象?程序中操作的是对象的引用。JAVA中创建对象的个数有内存的限制,同样FD的个数也是有限制的。</p>
<p><img src="/img/remote/1460000021587879" alt="" title=""></p>
<p>Linux在处理文件和网络连接时,都需要打开和关闭FD。</p>
<p>每个进程都会有默认的FD:</p>
<ul>
<li>0 标准输入 stdin</li>
<li>1 标准输出 stdout</li>
<li>2 错误输出 stderr</li>
</ul>
<h3>1.5 服务端处理网络请求的过程</h3>
<p><img src="/img/remote/1460000021587880" alt="" title=""></p>
<ul>
<li>连接建立后。</li>
<li>等待数据准备好(CPU 闲置)。</li>
<li>将数据从内核拷贝到进程中(CPU闲置)。</li>
</ul>
<p>怎么优化呢?</p>
<p>对于一次I/O访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。</p>
<p>所以说,当一个read操作发生时,它会经历两个阶段:</p>
<ul>
<li>等待数据准备 (Waiting for the data to be ready)。</li>
<li>将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。</li>
</ul>
<p>正是因为这两个阶段,Linux系统升级迭代中出现了下面三种网络模式的解决方案。</p>
<h2>二、IO模型介绍</h2>
<h3>2.1 阻塞 I/O - Blocking I/O</h3>
<p><img src="/img/remote/1460000021587882" alt="" title=""></p>
<p>简介:最原始的网络I/O模型。进程会一直阻塞,直到数据拷贝完成。</p>
<p>缺点:高并发时,服务端与客户端对等连接,线程多带来的问题:</p>
<ul>
<li>CPU资源浪费,上下文切换。</li>
<li>内存成本几何上升,JVM一个线程的成本约1MB。</li>
</ul>
<pre><code class="java">public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(Constant.HOST, Constant.PORT));
int idx =0;
while (true) {
final Socket socket = ss.accept();//阻塞方法
new Thread(() -> {
handle(socket);
},"线程["+idx+"]" ).start();
}
}
static void handle(Socket socket) {
byte[] bytes = new byte[1024];
try {
String serverMsg = " server sss[ 线程:"+ Thread.currentThread().getName() +"]";
socket.getOutputStream().write(serverMsg.getBytes());//阻塞方法
socket.getOutputStream().flush();
} catch (Exception e) {
e.printStackTrace();
}
}</code></pre>
<h3>2.2 非阻塞 I/O - Non Blocking IO</h3>
<p><img src="/img/remote/1460000021587878" alt="" title=""></p>
<p>简介:进程反复系统调用,并马上返回结果。</p>
<p>缺点:当进程有1000fds,代表用户进程轮询发生系统调用1000次kernel,来回的用户态和内核态的切换,成本几何上升。</p>
<pre><code class="java">public static void main(String[] args) throws IOException {
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(Constant.HOST, Constant.PORT));
System.out.println(" NIO server started ... ");
ss.configureBlocking(false);
int idx =0;
while (true) {
final SocketChannel socket = ss.accept();//阻塞方法
new Thread(() -> {
handle(socket);
},"线程["+idx+"]" ).start();
}
}
static void handle(SocketChannel socket) {
try {
socket.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socket.read(byteBuffer);
byteBuffer.flip();
System.out.println("请求:" + new String(byteBuffer.array()));
String resp = "服务器响应";
byteBuffer.get(resp.getBytes());
socket.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}</code></pre>
<h3>2.3 I/O 多路复用 - IO multiplexing</h3>
<p><img src="/img/remote/1460000021587884" alt="" title=""></p>
<p>简介:单个线程就可以同时处理多个网络连接。内核负责轮询所有socket,当某个socket有数据到达了,就通知用户进程。多路复用在Linux内核代码迭代过程中依次支持了三种调用,即SELECT、POLL、EPOLL三种多路复用的网络I/O模型。下文将画图结合Java代码解释。</p>
<h4>2.3.1 I/O 多路复用- select</h4>
<p><img src="/img/remote/1460000021587883" alt="" title=""></p>
<p>简介:有连接请求抵达了再检查处理。</p>
<p>缺点:</p>
<ul>
<li>句柄上限- 默认打开的FD有限制,1024个。</li>
<li>重复初始化-每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,内核进行遍历。</li>
<li>逐个排查所有FD状态效率不高。</li>
</ul>
<p>服务端的select 就像一块布满插口的插排,client端的连接连上其中一个插口,建立了一个通道,然后再在通道依次注册读写事件。一个就绪、读或写事件处理时一定记得删除,要不下次还能处理。</p>
<pre><code class="java">public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();//管道型ServerSocket
ssc.socket().bind(new InetSocketAddress(Constant.HOST, Constant.PORT));
ssc.configureBlocking(false);//设置非阻塞
System.out.println(" NIO single server started, listening on :" + ssc.getLocalAddress());
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);//在建立好的管道上,注册关心的事件 就绪
while(true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
it.remove();//处理的事件,必须删除
handle(key);
}
}
}
private static void handle(SelectionKey key) throws IOException {
if(key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);//设置非阻塞
sc.register(key.selector(), SelectionKey.OP_READ );//在建立好的管道上,注册关心的事件 可读
} else if (key.isReadable()) { //flip
SocketChannel sc = null;
sc = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(512);
buffer.clear();
int len = sc.read(buffer);
if(len != -1) {
System.out.println("[" +Thread.currentThread().getName()+"] recv :"+ new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
}
}</code></pre>
<h4>2.3.2 I/O 多路复用 – poll</h4>
<p><img src="/img/remote/1460000021587885" alt="" title=""></p>
<p>简介:设计新的数据结构(链表)提供使用效率。</p>
<p>poll和select相比在本质上变化不大,只是poll没有了select方式的最大文件描述符数量的限制。</p>
<p>缺点:逐个排查所有FD状态效率不高。</p>
<h4>2.3.3 I/O 多路复用- epoll</h4>
<p>简介:没有fd个数限制,用户态拷贝到内核态只需要一次,使用事件通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的I/O操作。</p>
<p>缺点:</p>
<ul>
<li>跨平台,Linux 支持最好。</li>
<li>底层实现复杂。</li>
<li>同步。</li>
</ul>
<pre><code class="java"> public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(Constant.HOST, Constant.PORT));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(final AsynchronousSocketChannel client, Object attachment) {
serverChannel.accept(null, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
client.write(ByteBuffer.wrap("HelloClient".getBytes()));//业务逻辑
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println(exc.getMessage());//失败处理
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();//失败处理
}
});
while (true) {
//不while true main方法一瞬间结束
}
}</code></pre>
<p>当然上面的缺点相比较它优点都可以忽略。JDK提供了异步方式实现,但在实际的Linux环境中底层还是epoll,只不过多了一层循环,不算真正的异步非阻塞。而且就像上图中代码调用,处理网络连接的代码和业务代码解耦得不够好。Netty提供了简洁、解耦、结构清晰的API。</p>
<pre><code class="java"> public static void main(String[] args) {
new NettyServer().serverStart();
System.out.println("Netty server started !");
}
public void serverStart() {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new Handler());
}
});
try {
ChannelFuture f = b.localAddress(Constant.HOST, Constant.PORT).bind().sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
class Handler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
ctx.writeAndFlush(msg);
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}</code></pre>
<p>bossGroup 处理网络请求的大管家(们),网络连接就绪时,交给workGroup干活的工人(们)。</p>
<h2>三、总结</h2>
<h3>回顾</h3>
<ul>
<li>同步/异步,连接建立后,用户程序读写时,如果最终还是需要用户程序来调用系统read()来读数据,那就是同步的,反之是异步。Windows实现了真正的异步,内核代码甚为复杂,但对用户程序来说是透明的。</li>
<li>阻塞/非阻塞,连接建立后,用户程序在等待可读可写时,是不是可以干别的事儿。如果可以就是非阻塞,反之阻塞。大多数操作系统都支持的。</li>
</ul>
<h3>Redis,Nginx,Netty,Node.js 为什么这么香?</h3>
<p>这些技术都是伴随Linux内核迭代中提供了高效处理网络请求的系统调用而出现的。了解计算机底层的知识才能更深刻地理解I/O,知其然,更要知其所以然。与君共勉!</p>
<blockquote>文章来源:宜信技术学院 & 宜信支付结算团队技术分享第8期-宜信支付结算部支付研发团队高级工程师周胜帅《从操作系统层面理解Linux的网络IO模型》<p>分享者:宜信支付结算部支付研发团队高级工程师周胜帅</p>
<p>原文首发于支付结算团队技术公号“野指针”</p>
</blockquote>
宜信科技年货包|精选49篇干货文章+全年沙龙视频+速记+ppt
https://segmentfault.com/a/1190000021577597
2020-01-14T11:52:19+08:00
2020-01-14T11:52:19+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p>还有10天就要到春节了,前几天被各种年度盘点刷屏,有人感慨我哪来这么些钱,有人惊讶自己的回答竟然帮助过那么多人,有人看到了自己的“9”后真言开心地点点头,有人则回想起深夜里单曲循环的那首歌... ...数据记录着多少尘封的念想,又镌刻下多少成长的记忆,或许盘点是为了更好地上路,让一切过往都成为重新出发的力量!细数这一年宜信技术学院在公众号发布的文章,<strong>从数据中台“C”位出道,到区块链热度重燃,从宜信数据中台建设实践,到宜信用区块链技术构建可信商业环境</strong>,这里的每一篇文章都值得再一次回看,如果你错过了之前的推送,那这一篇你一定不能错过~</p>
<p><img src="/img/bVbCHtR" alt="科技年货.jpeg" title="科技年货.jpeg"></p>
<p><strong>【年货包1:精选49篇硬核干货(戳文字直达)】</strong> </p>
<p><strong>年度干货文章:</strong><a href="https://link.segmentfault.com/?enc=cv0s9o9M5Mbyz6fIfvHBhg%3D%3D.Ca3smZWkedbOukqKxDnewe3Z24m%2Fnfod4%2FVE%2F0Le6c%2BtBRRR9vXGEVhrlCrVy%2BKqov06LmeCiwWgoGgMVbwabwqa55p1cA6JVsHBVygyT8f6V3kxR7zgNoetZUejWsRoNjV9Sp7WRRubt%2BeBnK1F7nzq%2BQaT8NpoaM2IPVMn9RdbhT5iOEIHeOOwfQ136104ZLudWWKeZ%2FLOiTgeY13LsyL7U2R9%2FG4TsHT7cec6Uq5%2FNFPQ1Z5e%2BWCazt%2BEaVHFhQYAuOyQAo9dRLBskyEfXhvx%2FMJta%2FkWkJPeCHp8VGIEN%2BJhH1uCRV88bw76jeAz" rel="nofollow">Sharding-JDBC 使用入门和基本配置</a> </p>
<p><strong>年度主题活动:</strong><a href="https://link.segmentfault.com/?enc=ugLwmPKO8kgQBvbJpMK0vQ%3D%3D.FHecitIJwqANU3lKQ1arTOTbQ9sEXeBx0wKqwYJrvCgOKtNESlaC3Y%2FsBDKxbguVpCfKNk9BkSU0VQlupAj%2FV3x%2FYemF%2FDwAj24dxDoQLQOxuAPsOIThVR455N4YM1letDNHcGVuOlavagv1YW0OKUVn6k4H8CHnfduxkI4Aed3jI95rh%2FWreh4oKcNvxgRgZ7uEg5pGFP75tihuw11NHqrId6bKKlOq0K7d0hESlKSyWSqvTqL0YssyrBd3t4qbww9l9MXE1i0pbICyr4u487wOPJvLJQihkB8yz%2FTLGuO2nzSWQEZ9EmRnc8qJu9fL" rel="nofollow">宜信敏捷数据中台建设实践</a>[](<a href="https://link.segmentfault.com/?enc=LN94187TK26UKZ7qoxulcQ%3D%3D.hl76pjVYyjA6Z5HOok6Z3TadbsNz7%2FjWaG2%2B0slO1rx1o%2BYs2oEST76gNQlJVC8SNbpQqGCdUnQEqsYPQ%2FjN8VkqYsh0r98oU5KE1DcySP6UJs4GV640uzDd2dk%2BpKP61Ar2hd8FjfeZwrcfALqcxdFdbj%2BrVboTRg%2BJWMJ8yBSLfHg4x%2FjcxJvxO9zznxvWIk%2FE2gsw7mBW8m1NnGkvnopz4ICPiFsNICE8NpdgPfZbGoFjeCgI1WGkIHquiiIXKjQM3YjfX%2BMlsIlUKjQs7jtdTePuB8YJMvhzzLQkhbnkbKVxSKk1N1FmPt4juESM" rel="nofollow">http://mp.weixin.qq.com/s?__b...</a></p>
<p><strong>人物专访(4):</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=377MkKrVOe39fTWAg2ZQqw%3D%3D.kxRhfUSFut6bHvXtInpH9QSPfoHlpNXHklEtljtZxuAjfiTWqyQQBofM7z0o6tLNvU4uIQMsnHaCTp66GwhqRm9SWhqvD%2FSeYhdwPmZw77ApMHhJZrhABZT1v%2FWu4yGrlaPd9wUfQ%2F6BAfw3lbwIMOKUkkk90I%2BDa4uZvBIFB7Z0bFuRzMKHi4EiN2WoZEIFESiFvE9ypySr%2Byb0EF93qFccobLPkvuqiY%2BQFf0aESIjEUt4FArKmOTm6%2F45oxgrrjVdfye5u4XqJEdmtOl7Xt3AIZHeH2KGWP3m9e3lckYQ0566kORVMGV%2B8pMXR2Eg" rel="nofollow">智慧金融时代,大数据和AI如何为业务赋能?|专访宜信AI中台团队负责人王东</a></li>
<li><a href="https://link.segmentfault.com/?enc=BPRe7ueY2GYPGrXivuZDjQ%3D%3D.fpQJUjNOqCC487isL%2F9TjPfhGVRcQnBXKYAxCuZPXjCm3yVILNPCZoXvCFzCGiD6uP0XeH%2Bi0tbtzjzKTj2bSKNNeRJiDYGOaIpQFdxRTe5aXf7vZfB6jOe0t1%2FEBDWKwQmcj5KAIOoXSYcr8rkTsJiHd4hXRpWXX63sxVATdYyXFZk8sOAZxktS%2BYhKg%2B2nRj0IqzVgMWrIPHtkzBU4A2htEiDjA%2BxDTsOFNNn75YNTu5PIX1m2mG6QI0SfenpviAHJAFyh6LLIURWTedCxAG00JYHjCU6XtBKj5hX2SFmoo%2FFjH7LzW0pmh8dK%2BQQX" rel="nofollow">一切技术创新都要以赋能业务为目标|专访宜信数据智能研发部负责人张军</a></li>
<li><a href="https://link.segmentfault.com/?enc=CbTUKxzmXeA3f5Fh32dEoQ%3D%3D.ZL09K1ag0A0UXZuIdHu4GZSDkY7aCgWvMfZjbhtxtwms46DX%2BHsIOBv%2FeS1mAiahIwldRHZygdrsqltXsE2qXbF7lSdi%2B3xA%2BVXEgMUH%2FUb6dQvVRa%2BTmaLD6dy7zjI37alD9lThop1LoSSX9z1nxEoGPTGPAF95P%2FQPj%2BjkaJcsACMORGRkuVlTISJQOm3NFoSN6fwutY70OJA9d%2FZE0K6p7F7Lmn5BTozxNgGHlNZ%2BxbqnVG6%2BBAssG5DCKq0X6xxGaYesxFZf8KbaljicUhV%2BiuSk%2FY%2BxDrgRuYB1E%2FrDngWtd286z0zS0MhEj6GD" rel="nofollow">回归架构本质,重新理解微服务|专访宜信开发平台(SIA)负责人梁鑫</a></li>
<li><a href="https://link.segmentfault.com/?enc=GUXECtJmjekKLXqUV3M5jQ%3D%3D.VRMYbQS3uJHDjH2bDf5XVSCqZyyvU%2Fz9%2FzDHlZyt2afIjI3Rdd76TKwTqDeQlUDVoCOVt7lp2x0y%2F8B2dCe4itFHyLLmLEXnUG8Py6bF%2BLBics0XfQ%2BzL3B5Mi1KuJWon1yrN%2FmhDDWUJVm0NM4gDyNWGw7SfKinjxVLDGFjinBU1psRVLadbx3MBLE5XplPHfaSWx3m1TOhyU6WdGDrSLxPFfPaZRzMtK384dVvIrXix%2FcmwCeeoJGfHhky5GtYhkoQNPsbNdvAFfH5yLN%2B%2F%2BIoMvf3arVRxNIzvjHN6xSsMAGzeSl57%2BeXNvhYX1M4" rel="nofollow">市场变化驱动产品思维升级|专访宜信财富管理产品部负责人Bob</a></li>
</ul>
<p><strong>宜信技术沙龙分享实录(7):</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=a2bZyijdf65C1axVeCzy2Q%3D%3D.ow%2F%2FOeoE0IJuwlfmblpvvULs0c%2Fdnlsk9b2S%2BLaH86A5fWWXG1WYPoDbdiCRUkrdBvTnTwBIJ8aZ1dwc8Wk%2FrVbK7uUenwSdTAbWYLNMmCeocp7OdwUK7ON8MmJ66HOZuDQj7j2jea%2F0zpbiGasXliTe7beSNY6P2HEiPUX%2BvJ8SIpIxBnwsHuGUI1DjCXrEOm3jLkji7STsQDXLen3g72gJoJWX12AmBC1xqgR0VrP4ySat84nBrkrvvkS5%2B0MT5ziM480gSNIzkMuw4L61MAKx88aawD9uA1mu%2FDI9gQWCPbM5YflM36vvwVCkPpA0" rel="nofollow">AI中台:一种敏捷的智能业务支持方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=c0fi6GiyzLdVCVy3bV3KCQ%3D%3D.fHHr%2FOq9%2Be4JZjSOBiNvVfgr%2Fin2BVzzCL6NGp2RS2PRrBnDM7oV964SqVhckf7AEJDfLDbc4Y6i7cTNNQe%2FjlcUYRoUbgsnLRVYoq%2B8HiKLXOXzseP2faOU8P%2FtyeRK8x4SRS5GXoLQ2m4jTziT080GXHNTCSyXqZJC%2BmIXMc%2BgEYh2taY%2BC%2FRHxyHYmOesKsXInHsgJfJxf8KUixUymTph42G83g3QwL4XVIom7aiJd8XZqeP6nHJZzFl1iDGqggdsinwdpw%2Fk3YHcx3ur0NYCsWVWwpxbvniMpZQuNwja1emma%2BUu7JEVuOKnM3DR" rel="nofollow">数据中台:宜信敏捷数据中台建设实践</a></li>
<li><a href="https://link.segmentfault.com/?enc=5c0gMmVmqzEmJKSqjbFXcA%3D%3D.Ii8OWAlIjCLQZ0LL3JjVnh9iZduh4qDbNUlOpyRXD5kiNFlcoi%2BNnMLyK5uZmDW1M%2FnW81%2FWEHkgwxOW%2Byy25qYK7wrEyEdpRLkp3SxD0B6jI4HeQSwOm3VXP5CMZgN%2Bn7sTKFZUtZoFkGMQ%2B2Fh6wtDNTWcRlCamXnuG%2B3GWVO4DLavpFL3IkwPd%2FtyOplVibeTT9JC163TLHTYbCF7eEUm0IZGT4E7I5e%2F9Cdmjk6cZIlLviH8%2F8769F%2BzaG%2FrJ%2FT9mvFwTLm9W2b3Q6QTAe6bp4XtCow4yXCby%2BhF8bZwWr9pBiLfjH8kV2GdeWVE" rel="nofollow">AI中台——智能聊天机器人平台的架构与应用</a></li>
<li><a href="https://link.segmentfault.com/?enc=aL41WmFh6bdp%2Bwlgtw382Q%3D%3D.B2ECJ65x4LgKE%2FBOoo8jrsobf31Eg1h0ZVUcbDkGGg%2B00r%2FDZ5VyZx2OYyDBzuq2hAAowZS7wrxrq1LOXmaQLAGok%2F7k91R7qKkHUABXWWa5XHsDj4E%2FiAbK3LQB2%2FO5GGxvBreLlOve%2B0FD12Szzj9%2FOqJMxfkzH9SzQBpZt3YrmMGXuOAO3wU5d7HICKevyyFE6%2FWUTqsTdCtdnkljemkCqP032%2F5jsjaxLJikfWPUoEfjSQslJIusi4vSXHDdwG8%2BlgyYpBM0LYLY2R8tAxGv6idXEXtkqiEoFG4v4FU8LRpmw6eIfjBi%2F1LlLVrg" rel="nofollow">宜信微服务任务调度平台建设实践</a></li>
<li><a href="https://link.segmentfault.com/?enc=oJDdNS%2B%2FCqxWAyzt9Dd64A%3D%3D.lwOKRJOww%2B6DAl5gphhvFxy1gJYRw%2Fy5Yge4y%2FhsQqDvlDEFn6HXlsFyU2ixvQBt4cfiOC7hn%2FWzonO9h%2BBzySdFmKrd%2BaItE1Y9%2BdQx8ugEKJzcuc9up%2BV7h5Lpii9LBDSFSVHhbRFzTGtlqae2X9qv%2B7CyZBmR%2FWML1zG07Mq8gD8kv6baBWYz14gY3YR%2FnqaHiMrw1OK7WboTPQOJ6EOpvbf%2BtinS9YtgIkk9aGvgTtQaxtl0nV0Dm%2Frfn6Ah4hEwl1M9hdJltGpX94DQkVJHl3dPYztAgrEB%2Bda%2BvkbFZwr2XZrjIH9O%2Bsk8JqBP" rel="nofollow">宜信智能监控平台建设实践</a></li>
<li><a href="https://link.segmentfault.com/?enc=qVSg7mEr7mVA%2BMdKY0ODJQ%3D%3D.aQB89G6iVMI5%2BLTpVcXIvzaw2F%2FBnFjPqTxRjPYsI4viQWMz7ldHBoIUhC2Ql4I6GsM59hfOnNeZeDxqsdIh6W77SChBiII%2BcVGggsZAo%2FY03oYuRXBpxr%2Fks03OBuvF7mBE90gyNTGgLpi%2FSKiAx8UG6rkqCHfbv7glou18%2FzZwIfp9Wxp%2F4KDc52h2hPPNceVtPcMsvfp0B%2FVMz8wTmzuFUyzb%2Bfd2oVrl2Nolec5LV0EPWnb796%2BzLmnpCC5NIop9bOASweMX%2BWqORa8hkfFd%2FX%2FcnsFveue8WL5V1BWBKsqtfGiW2GfiUWzKkF%2F1" rel="nofollow">宜信容器云的A点与B点</a></li>
<li><a href="https://link.segmentfault.com/?enc=tnLzHdZQs8XGLzejivDNgw%3D%3D.hVy95Hk3FHk172qT%2FxfzJ0db%2FoYzmgMFVPqfWJEXYqNUHTlWYgxezzsGw4MWBWXKUntiXDumAtxsQjYA60DEfV2uyB6iAd3KL0CycjZrwUPEop5bLAAb%2FD4OH8FXQ52glewUCAi3QeMmazU%2F2uECYP%2BfsO%2BFqo%2B1JQFtAT8MMKjCXbA2fNqyUWZpJtFZH%2FGj1gLkiONHvd8%2Bmt%2Fl%2B%2FezPIzTKiY6%2FRD%2FI2am09pME%2BRVYUDIn33fSI9QQyhXAqlHuVMMx9OuvOrr2JDK1vTjYvbgnKwja2rn%2FlnZbTrVE2OIHuiqOgYDYa02tlQuSXg6" rel="nofollow">宜信微服务架构落地及其演进</a></li>
</ul>
<p><strong>文章干货(38):</strong> </p>
<p><strong><em><em>△技术管理:</em></em></strong></p>
<ul><li><a href="https://link.segmentfault.com/?enc=9Fg8xBtu9ZYZcGI6yj3Dpg%3D%3D.oSGaFpup1vD6oDTtEw2jJiNJbPM9zIJGo5pvzHx3RYMjuf%2FCSRfSJ82z76Plr3xp4YqnPXyMOS8E2uJziJDkvInK0eyM4HQidiocsiBrN6rqz189o7OkNzxZkr70T8wd0I%2FssWEiWCPiOWHRHqgRJBLPvNSUA%2BGN8lYz%2Fwbl4Qrq8B%2FL7wFOFLCprDIXO%2BKtQHhstvZExSRnEY6UCXTfYPHY1c%2FmWhmRErvy0SH4IgBEM42tQFLluJ1p1VDHEBhGIfcD5YbZhBMopG9d5etgW%2BJlUN8kyRNr%2Bl0%2BcsyexlXdwmYanXD%2FPObKmQggTH1o" rel="nofollow">大型科技团队的管理|分享实录</a></li></ul>
<p><strong>△区块链:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=NxIXOswFsQfg%2FJ2QYg3Xfw%3D%3D.Lpaq8z58P1waIN38eIxcCuwJacDgNgCy9GhcvwOgUEkqAT7d8Pbn39d%2FUNvg%2BgQGodPIYWSdJGDGAZeAAuyFfxPAR9bSi4y3CUnliM5%2FBPbuwMMSMZBc0yd3zDUOp0Rvn%2FUjpvrcaFvmB4N%2F3Xqn%2F6e6bPXw3p2ofqOg1FUnvHVRyL6hBaIqdEgit%2FdrNSwG%2BXZNgWNiI4JqaT8L1unX6xNwZdupvlFKXznm1z41Y1IA%2Fa2DSp3DqvzPlCHEZav3GDsNVelv4SPhKpeEGkgDgtvpUvzX7cLF1hZed36B7dYjbL%2B5p1HVVeDGK%2BGOCl3c" rel="nofollow">供应链金融+区块链双链合璧</a></li>
<li><a href="https://link.segmentfault.com/?enc=TC9VifHh2IFcheqon0GSBg%3D%3D.CmA5QzBo8w6969DFi7QIe8ETZSx0EyaGhBe%2BwhO5%2BuYI08g2MQPqNqlMU9rfSNpXK%2FkU4XjDe93F21nhWFdL4EL1HGCd%2FL2or8WsKlmjPNCLEK9WRoLNVjZVCDyLuA%2BJogIyZdxWzGQGO%2BYQm0kjxkflwJdjE60F7sv%2FwSoS64BSW%2F0XptG7IZxyZwQHg35b3Fhfu8VfUNqePT3s%2BQSFj61txl8L5tS4IYmSxKEQj5BmY7vSuhQNCiQ4lIFKIOX26WnWM%2FnGocDka6yaLIIPneol95XOTHK2N7cwrHggzL1RAmV%2FA7GqvVhqva6PnFrA" rel="nofollow">一篇干货文读懂宜信的区块链实践 |宜信CEO座谈会</a></li>
<li><a href="https://link.segmentfault.com/?enc=RoaJ2RZhmSd5BMZkxuHppw%3D%3D.GGHI0V7udWfJ9SstCzDYJrQnp7WSYGit%2BE0OTg%2FBuH3s9e4Ebjb3U9ob7JbqZ7vesvCLj7ZUuM9XBGheaXjTMtSbaqBAOx1gw2XX8tluNpJvh0rJRqTiAMrXm2ZSrgPckmPOgCF7cHBQcQNJWV3pxdvVkziAsDh7BDg0JGl2Cn68xN8p48Zriz%2FbSxQsFS04U3JkGj%2BiBEpv5pjfSI1F1sasMOu4K8CPuZ41j2fU3dtwuqPrVRSmdOx%2FfRdK8iFPwPspcuWFyIq52SXoa4haowRovgOvsvEizcKe8jkrLmBTW7yLhaVH9mIaMAkLpEHm" rel="nofollow">宜信Blockworm BaaS:用区块链技术构建可信商业环境</a></li>
</ul>
<p><strong>△微服务:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=iCiMJGySK4DyOgEMUNZdyA%3D%3D.UR1W0DZbJAPZCw886bzvi9oLoAhw2Kl%2FiOuMdJeJZwHXUr1KJBu%2B5zTBVAyJKShhrCKgv2eqCsQEvn3jmzCUkZJdWnNHpMr0H7ykUBzYS%2Bh%2BVq5EmhVwNPBujpmCYkazLP1om%2F4161a%2Bel%2FJORJ6hFBR0jn2Ol55D%2BofmDK9kULvf0PP9qA93rwMQ67s9I2SEyR2guHeBlysJoeUja4UBpzH%2FFOkhab2bc98pb49xEabb2c7n8MN4m0DZS2NWKJzLj1deovrFMIOvIm7Qy7hiNTzHDTEiASriV987dCt6uxvJPCnuNXxCm6cV0uO8iXA" rel="nofollow">详解Eureka缓存机制</a></li>
<li><a href="https://link.segmentfault.com/?enc=yM5skmD3Pyo6ioTDA61Ldw%3D%3D.l9GbvHD4D048BJdxaunZIt1Asnu%2BLPGx7q%2FH2%2BwqAVQrBMNL45OwV9kEzax%2FpVkSEaFYILvBKSfF66I0kwSTHcsNJHReLZ8wncGfdB9ki%2F9riYRDNBhNwk0YBQb%2BQxQ5ak4PJ3h4QW0swCwmNPXiBsWriNj6G93yDW%2BBPJVTVJr4c%2F%2Bm23qdCd782rT%2F%2BPH3hjoP72MSxrpZskPMMpkjDtwt6YQQ01LMnpAk6ipc6l28hj0ZifETLdgWQBrFkTSA2lgucyoil9NqYYBywlNUW%2FZvU4SeJqKvhRRBgWaJ80b2J2WG6zs%2FT50xal3z2L9C" rel="nofollow">一次Zookeeper 扩展之殇</a></li>
<li><a href="https://link.segmentfault.com/?enc=pQlZHqf5%2FguEBLdRKRl09A%3D%3D.2eP6KfhFawlUlxplkhj4PXVmd%2BgAWSPyucsMQL0RRq0e0Ri0v%2FKeY3SMZ%2BjhpwofQp4OQvES9VUeiuAGMaAyzhUABROoJnEXFop4P8sgglfFjHbV8YWScpTbd1LH67foy1TGFOqGi9p0EOYl0XvyST7Cju8DYaisogM4PdFCIAAXE5wX9zc7Dv0csMbbdt9sLB70NNwLdlbWMo0b6Jay7Ur3EH51sEYzHM6nGswgu47Bp0omZto9szXeUyG%2BEUxaPs7nfAFfmm%2FpvI%2FXvb37qrErSxolgGBSA7fuLElb%2BwMrrAwXS4oH%2BxrX7pxtMvF%2F" rel="nofollow">宜信开源|微服务任务调度平台SIA-TASK入手实践</a></li>
<li><a href="https://link.segmentfault.com/?enc=SI2BkzuaGSxy2rSaObMc%2Bg%3D%3D.y9%2F%2FmxWpzWdJr5vwmkc%2BfGq8xZ8cfaVa%2FAhDv8Ov9tDUyw9L6UByVJOqXNhz3WpJkaXiLpdG%2Ba8JN6LOvudj7KAG88UNdFFfVxDmh509imFPaP%2FX%2BC2LauYHz1ge7UvHhTe%2Bi6pPWDkHHUzAab9eig4lSfMqgwaufEq%2FJoD3yL4eJpa3bP5Z0lnf1%2BEYs1qmSjrQeMbOfhPhwsUjhIjf58i4%2FttmxKEiWsyrTAu2ufQg0KrUpgd8hiQ4TfyZ6D64UGWnilh30%2F93A9R5QvLFAdYc9yUSsTwrtOmd7tvnJoXG2j9wSx%2BOi%2B9%2BBlqzrCjn" rel="nofollow">一个项目的SpringCloud微服务改造过程</a></li>
<li><a href="https://link.segmentfault.com/?enc=zWt6DRhMXCGVa%2BBdHDx%2Fcw%3D%3D.26xLeUuMJ%2BJmPgiyFt1AjfX0nhemevpAlROFL8zLdZMr7Pow30TMuxH9e4OaZGh%2BWfFS6nBQcTetQ%2BcrXKHQ6VMb4%2FraNnmh74mkzfyrwB2tDK0aJx06Z38IOGhAp8AUeRVHoUbehbIlZwpR5juk2nxcKsvfxW%2BNXh68TwLxb9nwrkI4mOeeXWv%2FOgLjLsmTpf0YhpFg7%2FxmKC2sIY4uOkYUJYHAYvlE%2FWSI6u67BzQZGJCqyezGrBFducplgOW%2BIUvHVOvrtUKswtaHKqRAwz7wi8EtTP6zx8EwWQClHxFtmzI4xvVU5wG1hkxhArZu" rel="nofollow">微服务与网关技术(SIA-GateWay)</a></li>
<li><a href="https://link.segmentfault.com/?enc=ommWGl1HKz%2Ff3Tj12AsY7A%3D%3D.ryXrUMfDzP1WSlrpWfSWkFRuv6dNCif1qo9dq9nJY%2Fq%2FUrITZpAEj6Xc0aEc8jswInNN4shI4lIAnT%2BN0RijWTSovhpjMqZcPNKf3aYhHaldt1D1irlbmaUS7ktR4EggmRS2QYBilqvd6W03oSp4blC68muuZh%2F%2FBvBmWxH9kKlafJsUrr9f3hol%2BEjUZEzUMzyJY5NA9cMOhN7D9xOr%2BjB4zQpqTLr04ifU9dERS%2FvjjOcicS75E%2FResoSsQwcxi1KApDirg5MgeyGOnI6X1UBgrCl6LLV8YIpCaXSCM%2Be8ON2suzLS8%2B9RH7rrBPW8" rel="nofollow">82天突破1000star,项目团队梳理出软件开源必须注意的8个方面</a></li>
</ul>
<p><strong>△Spring专题:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=bA28vz1zOifYHgIeGVAPTQ%3D%3D.RdlZO6YkMaq3koExdnipLeE82z53q6v5PEgqHhSk7IJUiHKvRTjOIa4CVZg03obm2qSQDgyfagZ2PvXK2FbkWgijqeJb4yIdGM1JwoLr3dU3c0ee2YMFLq5l2gph75L%2F6fa8P4lHgE4dPZXfBsfN9XFQsL30DRsqukaf9kgxjCAWTtD1yo7G9m1sDRwe%2BWHrTmJ%2FU%2Fx%2B4%2BB4kcPf4IjQKvUCemTazPt3iOnKGEMPe6eoSOCE%2BJ3mX5vdl6jSVk4t7GgyHkivKjKa778Vk1k5jMF2utHE6TiQdM1Gj48bBMq%2By%2BRlkab1EbovrqCtjr%2FW" rel="nofollow">深入理解Spring异常处理</a></li>
<li><a href="https://link.segmentfault.com/?enc=3XHPrKdHXbRJ27gYGbQ0cw%3D%3D.wMKoCnFb0OTUFYxEhn9GyPJFLdOkEI%2BOEmBokkq24NPRsyNLq3zICh%2Fkt5kpcBRu7ejKsXUnTMxm7JCnwJ84y3yUUAI8%2BDgzqryvffo9zZrS56CxcuZ2jdQqWLYmvH9mpN8kGmEfePafOfyi2B8mHwOw58q7F1NlQOMshw1JLkkLlq2lBINNUo043L1I7YaeqN9bD%2F4xwmPooNo7NrMxEHW%2BXLW64KtHs1UUzc1%2FaT%2BZ2KmZXYo%2F2SS0AZHVuR%2BQPlPXfd6FxjNpAwsqISS7p%2BhTo0wXDehTHhCbwS%2FAWc94HolYT80LoIJarjxZDnIa" rel="nofollow">Sharding-JDBC 使用入门和基本配置</a></li>
<li><a href="https://link.segmentfault.com/?enc=dKNbSz2b%2BUQuepzueMZS3Q%3D%3D.ITWl6NwWmd0ysAF1QYveiyxY9j0dtkqcPJomMydjOGoRSsusFZcuavRZZguWHLbrAE5qfMx9y0Hhvv8BImTVT3YpMRh92lUB1moXBeiS%2B%2FMZJnmWPc70F8V8N0JZh6kLZfunUqrJi4pUfTZmkhksdrWFQNgg%2FtDRxyCcK7WpmA7EHydLL98COilYX17FHhIbuqCV%2F7O6IC2eyCbfUivlM6uK4V%2FJKHB0yaxuD6ej3AZULu6fqClh%2FH11NGpUaqFm49JmBkZowQOMb%2B9s52Ad2z9i6hGFw0onEnHuNTX3g0tEIaCHSMbFM4Uz0Cw1DuiK" rel="nofollow">Spring基本概念速览</a></li>
<li><a href="https://link.segmentfault.com/?enc=qvjAaFo93bRICxwVW4b7EA%3D%3D.uMLFVskSfFjsMagf14ea0wL8hel1zoIAG78KLzns3QoflTkE2jKor7lMEOoDIkMdrutIMzDwmAxFbJCPR20oWCe9vzDXImobnXwGmgZwh4zFycATtMDKWzkvXv5wwjoyOlBk6XksnCLlkkkaXmR4cUWvaJzDdBrUFlSpievARHLyGkNbpnwiGxg7lxrIrAUSN8D3gYE4ui%2BQeAdtiSvTjI92KZUJx%2BYGrfnhwd62tCfciwQqNaY7nzECTC15oG3Mowyd3ft7eHBawdIeb2LoDahtNQfE9Raeuai8S8%2FcrP9LU7wA8ESvu3QD6jtGsfx0" rel="nofollow">springboot 之常用注解</a></li>
<li><a href="https://link.segmentfault.com/?enc=5koOCgRGJMiVusUoYWZpbQ%3D%3D.qOu46VGUPLNicpE%2BJW9kPc5HBqy7JqkeZ1Oy2c0G1Lq8VSRXJC1nlV0ZVfCJ5XPZsfvkReXHsgNoB8GDmsiwDYmknshkP%2BcFMYfyYx3vcocimwcGuvFoDbgUGDtlWIdlXNeMZgORWrp8mMsPJ9E52QCcJSS5x9vlndOXqG9v0FS1JbzSQnGh3nyoDrqa6A05HAQPezKrVKS1c14AzofVeW6zc0oJq3FM%2BtsOGyJlA6ZWZaU06b1nacrTxF3gKhDmMNobzTo%2FVxv5XgDs%2F0IuLaL6QVzhPIFKLiA4hZ7bzxHynJNNezIq59sycXwYI1by" rel="nofollow">如何编写优雅的Dockerfile</a></li>
</ul>
<p><strong>△安全:</strong></p>
<ul><li><a href="https://link.segmentfault.com/?enc=kCRXTOybukla91N%2FMyEfHg%3D%3D.NRqGugH7O%2B7Dhu8QUaPCntj%2F%2FtkT4phmdVQ5UWpvzgJ9TXuaz4FnwqxXhxzyCwtxAzlggFVrdySmVOohU%2BqR86YSZ9lwTZX1GgsWqillydlRT1Jfh2%2B2UJzkGu6h2oz9MfZgsVLhwnBVoLMMAxUvgJ2bXz9LadTAxvtxM%2BzJ%2BHPREoUZum4d4HJjpYUKtYUkjLhMQX9sT53hzEw4Jq3qOuhqJ%2FgiALZ2o8sHJUWZ%2B7bINDPWD9Rr%2BzmdD1THFb9VSf3sTKuT1ijPUEP1IpjiMG%2BoF93sCz%2Fjbt7pJ8ViIRubsw7D3YeLBT8yF8feVP%2Bx" rel="nofollow">宜信SDL实践:产品经理如何驱动产品安全建设</a></li></ul>
<p><strong>△Redis专题:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=3eiVILWnzilA0PBFcJf2eQ%3D%3D.RYUwtZYK3UffwZKt9eyq8ld6ZWvmk5uaurBD4vMgWHW78xMTkJJoxIdddBbkpBE8OqlqCK4%2B%2BnCIY6V%2Faj6bUsHK5wCbNndBRNglvJepSKYZNxtsNPbp8osQgIrEfD0Fh5rjNhWxjBHQoepqqX36IX4o7ZhUmXJ03kJ8W2rbPMqNP%2BJUwJsmWzc89TeewUNZagawQDGmH%2BBU9ljgfM0tzb6U3rfXERZL3kIV3iOwVxkccJmX5XzsZam587mRWusEA8dwXuRxqve5cJ5751HLecaV5Ti0cZGA9crxH4PQfPktsdKHoBcnpYqaqR7Eh6V0" rel="nofollow">Redis闲谈(1):构建知识图谱</a></li>
<li><a href="https://link.segmentfault.com/?enc=LPVaxxQ%2Ffw%2FS1JHaqcW1xA%3D%3D.IqLzArj8G9bRoIluPvAmbn8T8MbJ%2FTP0A9wkFO%2BlX7vNGulh8EibNOb4kZ0%2BW3KoOVQ4djXFsrejFG%2Fpzf19f26pZisG5V7Ym%2BqgKG7EC5mUsqCzYayhM1%2B52z3T%2BiLJN%2B3Iu15Xgrk3hPrjgGoem06p9dHID6yd5r33x%2FKxDVSVgJPWP80bIsNNjGiBiE308adEMticPz%2FxjNp6QgVf9%2FyBJAm6eGe8iMs8ARCqnONxwN6GN2s2tIpr%2F6dY%2B4tj3AhV4ywC759j11iRdtbpmQPb6lEQ8sVHZFzI3h0Bt60Oixvx7LVXuGy3QZ%2B6Xibk" rel="nofollow">Redis专题(2):Redis数据结构底层探秘</a></li>
<li><a href="https://link.segmentfault.com/?enc=KvB4KiiPsTT5fUX2z%2BklXQ%3D%3D.cEoc69%2Fi%2FznS%2FZ%2BimlcHulPUkH6MH1VAP7V9aTWwW%2FXEVHA7TPE0EMea%2FG0TJz5tB9L%2FpkZrbe429c1W%2Fp6VTkEA3jMnrpIo%2BXs1fO9vVjR6howjk72kgZMu%2FkJuA40u7FFU6IYn7Gwn23F0YoCbR%2FpsIXI4adTRzK3ouR%2BkU6fGvHvZgNQQnUErSW5cABFqMd2%2FJzVZhgpRil5KP%2B8hDs49Z1LvHvAAWhz6Hbe%2BuJYJiC%2ByvAlp471Emq3JpX0EUXQpoorKtnYhg3%2FkI%2B6Fp%2FfjWzsY7LXjUq%2BXpNnSBwmbBSmpQubBPKz8Et3y2OOb" rel="nofollow">Redis专题(3):锁的基本概念到Redis分布式锁实现</a></li>
</ul>
<p><strong>△容器云:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=QY5CTABhNBy7Y%2Fiyra2XPg%3D%3D.uuQLhVGuSco2Icp7ngastDoadsgsgQMAUZA8h1%2BviKLxDh6OTp4CMzz21uZv2ZM7jRwDJzT%2FVRPTphXsRrRE4p8Z4sYjj0TnI%2F2MgeOIxSWCp%2FTcywbCpZOwlbNOYrDeHwoz3KpR0uUxu%2BECTC4RILLEfQCus4NRvb8dvdyYijPxOk11aspEfo3HQQ3cnOZH9TCeK0kZICnUUGEZEJkNl0dTies8Cg7wOOLlrDrUpXesmo7LWvhdx%2FV8gFXWCtJeDUdFUMixLJkcby5WaLbNOHYzZOAtl%2BAECykqiLmzfsD6COBtxiCrVc6vPAPvk4sM" rel="nofollow">如何快速部署容器化应用</a></li>
<li><a href="https://link.segmentfault.com/?enc=B%2FZ9W0w5WdNQO1I8Bv%2F2ZA%3D%3D.DSyNBhUPt4lihX0OPbb893kB6XYPDiP2H8U7bgHnmdCAqpd0ezbaHNEOtz35aKDBZ%2FcJ70N%2F%2Bet8XjABfjx1SVAEtW7Qwe1GR5z8Z4HuGOHKHDn9KCpXxWKx4sLQt6vhceURiHaDts2uEET1AOMZS6damOdk%2Bw5EjDYh1DB66%2BqrmzGr2qs5Q1YH5%2F01IOId%2BL8%2BdVp5El%2Bv5gZXCqTD7N1god9jBf5rPB4Iiz7AnhecHDFco1dYy%2F9uvnekt5x%2FItnD04Ld%2FLCAOZXnHUoklxDs714OWGGBBGx7U8winYOc5qptc9KT9PxHE86TST7b" rel="nofollow">宜信容器云排错工具集</a></li>
</ul>
<p><strong>△测试&运维:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=BDgulZg6iMuwhsFiaU6wcA%3D%3D.A32hc0%2B4QI4luzlBTr7ZhDS073rjyrCepDFp8sANvS0P%2BsxInXwltbpT%2By4iwSYhsb3j3wyHP%2BbJtrzWS0TOqNVpfNB5r1iIn73rKkXR2EZPk4pi6iBwg%2BviqhWfQa%2FNdlA9mZfXytuR0majwk0DXw5lg59m4bW5Y6tBumhy9TbitnMRK5h32yrdp5TqZe34zvAXLzzOqMRHUHkkKDuNCa%2FkQunIas1W1qAmT40NPkXlH2dTa99XdI9SJxv%2FvFzEheh0M6LRamNNA0VzXZLqJCahf%2FPOD2TAUc5TfwJFaozTWf4H8tb0wWILp6Vq0UEW" rel="nofollow">Linux三剑客之awk(1):awk简介与表达式实例</a></li>
<li><a href="https://link.segmentfault.com/?enc=Qq%2FngdduSpYLkBpcH%2FE87A%3D%3D.8F0K5vX8Hgew53RT%2BN4Jx4MtjY%2BRBpIAe80A3G8QOKojFadUSRd%2Bfh6gKK5nQcVmE5xy0sSuWZWSZPat6bYtsLkacwD%2Bt%2BJAK%2F1cmNhch%2FKjpd4v8feyri3J9uBrYANFHhKd6DzbZ1P5Z9hPga2pJ%2F3LCHLa7lndbuCWEQX5q3YAVo2meq7pQ97yj7ONC7vngyZPvH3mEOGqLxxIrzUNyh1tSUIr%2BsA%2BQOopy1uUfG5b7vjgS%2FGsbTdcN4ncpTEjD8YKR36lZFEzx8wFN2zu%2B1MM79oszCcW4hiYiRDigVWryhRuMdcROcx78RGQHOwd" rel="nofollow">Linux三剑客之awk(2):awk进阶-变量与执行、数组与语法</a></li>
<li><a href="https://link.segmentfault.com/?enc=B6ME5YWslKKWT5xQ5HF7NQ%3D%3D.BIvUeqWYJKzxxkJF15EqIa3iaK7pzBMtWyHTeialaEQdObp2opKwLFhVk0oVz5ztx0iEzTcOjRo7Azdz0KaQbg1FMn3dKeMNoveJZJeTmD5%2B9n8jZ3hA4Hk%2Fl1RM7vuM5CuWuzhQYw1Zx6r0vnx%2BTnUygQCf%2BrOavk82UlUFfQkrvorNgMpXDP%2FYGPSj6EIEpnaIdMSbtGFULriGXSg43iQmDOtP00xFO1m3DS8v0IoH8V8B2hmhHYS3L%2BxJ7z%2Bnue3ZC24KLAJEKaRZatl%2FCIAYmSaUCPf8h5ryqjNOWTVO5qe7YD0Btyg8gfwyQ9Jk" rel="nofollow">浅析安全测试</a></li>
<li><a href="https://link.segmentfault.com/?enc=eHqdSjWbhbYObYs1OpONVA%3D%3D.scBEY9z6qhcJ%2FEFjnNd4eVldTur%2BaT%2BQhgAvS8qLo7tMFE5I1DsO2TAq8pTaQw0D5fapzOFWSi%2BGRKelmRxWvRm8qqwqk1%2FpeJG3P2sNJJBUg1Uu1dZFT8pVYBD0aWnWYgYZ%2BhvXgYuqWNQR3CmvWPwL0dvvpEwD3eSSaByejsrzwj%2FTdjA%2Bl83L0pg6TvOn9f6eWW3UyxDwoLBNCuYF0iXhJhf4Yl%2FLk8DqWcS5MvA4xBxdxian1tThYX%2F%2FWZRNtT0azslsfSyzNPrSL3B0gJ73uvVYuWtkOXBw3606sQ4GW2rkA2xoddN4rsPLQ%2B%2Bi" rel="nofollow">API自动化测试实践</a></li>
<li><a href="https://link.segmentfault.com/?enc=RwbCKBXZeqg5qMmjuxE8Xw%3D%3D.zPp8vwgq1%2BogVo0fK%2BOJ7CPZwRXOVirJHXFUHW1FXFri75p%2B6YZlbOFb9VXR4GlckdAeyThbe2VmROLqtRNzWiEaPrxEoecOHIBGcbtRFNk5p3I5kl%2BsR2jhjryqZl3HkZYz%2Fb8o%2BbdQ4B1IxLLtssyOFFiph6ST1JI27uBuhPJKE9%2B8SDapkepDto56tJuQ38r%2BRI0OQHeiqjSbGV51%2BNrq26%2FUMHGiBepYDGqOZaXhnpl536Qry%2BuOGrrZHAdXO01h20zGX8yTYX6P5q3TtqVws41ePCLFetjmyH6MMqerNkERGPBeZOyldhTwCeLo" rel="nofollow">自动化最佳实践(一):从纺锤模型到金字塔模型</a></li>
<li><a href="https://link.segmentfault.com/?enc=f%2FFPgsjodK1EUJzjNwEysQ%3D%3D.3laBjaujY7y57s5UiciHK8KN6uo84g%2BTl%2FxD88XPLDPedbW7RUI1OeFwScQNoCzf%2B%2FAvi7LNk8jKGJcHSJ9UyE2smlM6B55eAsrgiV1Ic1zLwu7KzWlnAAreQFVkikJPVY3yC0O9QzmXoFM6%2FKMJIg5VpGRRgOHvVARYUJlhvph8xHS3y%2BTfzGBB2QBhMwL6HpXxhMRfv9%2BIgPdNBIDmgp1rHYykMBUN7Q1Kjr9B42RdWBExjrxWah0fsZqrgwWE5Q6wbTWgzvBGxaETKTOHYMahDSvYAGQ%2BAnqLP%2FHJC4yntWz855FDDGR2%2F%2B2vEv2l" rel="nofollow">实例解说AngularJS在自动化测试中的应用</a></li>
<li><a href="https://link.segmentfault.com/?enc=bAkpGU1D93HpfhBHMPsLVg%3D%3D.X3lBik2bqSclA5V7wO8h4FmjgOLZkHgNqJq3Fa4nPLkwNqbP4KwQq9yMsHINIlFHUhIUaygZ4kxfbbjmtT2LtOGhKW%2BtUyBXo5MGcWrZR%2B%2FyLm9sXxGv1N9vApd2fplor9KZq6f%2FVchqtOFDq09DBM9RJSkK%2FUg9EYtrRrsq4kl%2Bree8qbDQ4EEjo0THhs2o6LeNWY6OK5GWLcl0eSHsBK3FHeXfFdlvyoB4Oqgr%2FM4o7qqFmBkG4DvLgwCy6JdEEIqFSyToyWAg1iHwrJIiNcMBvD5FDbEvL0kD%2F6rw1%2BBUp58yXe4iXVMtqPftCc2L" rel="nofollow">巧用自动化测试组合拳保证产品质量</a></li>
<li><a href="https://link.segmentfault.com/?enc=5TNPZgaVsX6WrDN%2FGFWXGg%3D%3D.AQVB3iD8TxCmugXVyvDL7uYQHDfWN1gYdmBsUoa3JZDfgeYeE514EhaVoSnrLNLmNNj%2BBTdk7YL%2FLEyPW0zNCQPPA4DbLQjWY6vPIqD%2FZYiAdVWaUfuIzv%2BwTSjF50qenPDhRx1LJVqtveq4l%2BaJJoEoug%2F50HcOu%2B8NBvMYmEU7ZGucTSKtLQtzumYWH5RhxJL8iSaZhcVahU2Ms%2FqJmfxH3M6BUSfOYBBp%2FCEA1Cpi46ppHjkWp3K33Wi41Qj7KQv9lq%2FP3f4ht65tyVAjRYM4aNKUuG99K6AxLtcZ8nW%2Fpl%2BZ5kB9lyV8P4IlXU7q" rel="nofollow">分布式主动感知在智能运维中的实践</a></li>
</ul>
<p><strong>△程序员笔记:</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=6eeljTgYnDHWOAq7wk1EOg%3D%3D.dIqB%2FQ9vZnUuo6xBlUJu1WOjflL0QjEC1kLDZvcJucoTNEsywCaytxX8PtrRSBUPLMMSxvb7%2BsR6PCxIeON24XHrcFqjOHBYmScAzyel%2Fk1nQGR7OWGM7rRyaT2QsazLPvWi1c93Ow2QR%2FckL1JqEWSz6uHaSjufGmoRfrHtLMglCqZCDgo57xRaiJBNWyih2sx%2F%2BiN%2BRz8kKNgNbkestX475XwOWeL60bxZWoZ72OITUhEnMVUAKmGZ6qCaw7sGsZ3E57sD7CiQ5eTmgmorcZS9c50zZPWE0uy3WINcBY53Q2FCvaLKQJ6973zJGn9s" rel="nofollow">简洁方便的集合处理——Java 8 stream流</a></li>
<li><a href="https://link.segmentfault.com/?enc=083%2BZOyhKuI1gX32b4AF%2FQ%3D%3D.tnbqZGXrxE%2BiOff5J5yaYyE%2B5W7qp5%2FCHhf6BKRzxMmtxTagEk9M8CDF98sUzCdvT6m8hSPtgLcQ0AkG8Art7mE%2BDrjTlnai3j9FrAFlxHEvPpRXzDPYDsfr6tW2sN3PRJW1liRqDInXNanuP%2Bu7Gb88NzEtUU6gcEZyk6%2FY9Dv9Yu43E9PGLRx4Bc8PXARDUIRHqjJEQ8ThGD52iZxJ7ecmtQE%2BaH1aa47hicVs2VlKk4TFrzh%2Bf1SfClOLpAnQvevuZQumHIUojq52UuZz8hpzjfwVb%2Fa0pIBRektL40h2IG1rll0X4L8TuTxcyTQP" rel="nofollow">MyCat数据库的基础配置及使用</a></li>
<li><a href="https://link.segmentfault.com/?enc=dg%2FP1bb%2F1qAoBJ400l0%2Fig%3D%3D.77UwfQrw0uRqqw9C4jHE%2B02%2B3E8JhloEBcCGoeeJfRoHdLnMfWGGf%2Flm7equ6gJ9IP3PUwgrzEibWj9sJKNLsxIpq2MszV2pXym1xLWYsBN%2FcEDvAtlzlpFVHuIa4fdwtblSWkJ5fvdQQsUtI4knhzrckZUDkQrao85jV2Oo9JRUMSB0ijsPDBHy0FIt3TmSMiFrXIPfD5k%2FgMCq1kKAoK0A66H1E6Oa5ul9h53rMwxexeP70o0lAcjzDUcYI8aJDC%2Be5AU7YN3Hxt2rWv31e%2BReCoSIrprf%2BRav3zkUMwCu9DA77QVDrw%2Fa6lUkj9mI" rel="nofollow">快速入门开发实现订单类图片识别结果抽象解析</a></li>
<li><a href="https://link.segmentfault.com/?enc=wE3UlMlQD409WzCUmey3zw%3D%3D.a%2B6O73xVcBAzK9%2B4WVomoo0aNjLe9G0XivPAxEfTfa1VIIqKI%2FFF2y3pR%2Bb%2F4em8VAe4b4DX1jbX7Jp5ruwultOIIPq%2FWLJhQrJxkWmaENrO7y6lz57GTiMgEgSMcWcXTUlpKxGup%2Fv9XzFPSUDJ9zx8%2Fnt2fHgqpFQ%2FzWIXLI71zLRau%2BJBjGZWFm0OteIvYsfwYdPxNqpgNY3d2gUan7xMip2RP6Xm%2BRNdUrb%2BkuLdHlZfNf11ccNa3Axa%2FL61Qi%2BdpdiM7PnEdYKE6wovw1VoKbgQ6ZpiV69PaZn3iDF%2BZll80q44zpxHThH6pDcv" rel="nofollow">专注业务逻辑的轻量级服务框架nextsystem4</a></li>
<li><a href="https://link.segmentfault.com/?enc=L4%2FSbVnHYuxQ7qNXAB5Uag%3D%3D.g1rLW85GI1YSJ12YA3cp5QVzQ5ykX6mMl%2FlL5E3HBm%2BZSTEosdFGjibZQqrq%2B38H5gAHDwjUfhEFl2LB%2BuhVeKR531MWq3TebwqO5UhE4FSSRK0GOyPU8w4so7HOGd6AUVCYdd6Moq8v24gXURKnawiSRX7l1EqawrpDkk6EeNavU2fpDpeEtJNWFZL2Q5FJZCTNMIWcQWW0KSj5JLPJYRnnfzHCB2RI8OPtcEf4XwEcDaBY7164yCaRPbaj7ae5icuXjUfFzL04usfpWfGG2TG7dDLaGZ2%2FSOxBqctdK%2BSGMg60W%2FdT4PTuPytPPjkv" rel="nofollow">如何写一个无配置格式统一的日志</a></li>
<li><a href="https://link.segmentfault.com/?enc=ZxC8Odm84fyftjlXWxMJ4g%3D%3D.pQEViCeYVFIdf6pw6jPVJ131mzfiSP1Cin6pxsamf%2B7oYFhOa8OHnwgj2p2BRht1T2kIRXT1ryqKotGU17vG4cy75HuSMaC9PWVj%2B06NbRLH8gecQY%2FXHGc67YB8uASZCQ344Od7YsP9xiR803U0cvNsLGGT71jN%2B5y640WvpomgcnKGj2al3oAhc4kwEbEl7gxSNfEypgyDu5LSxvDaHMVwNFZoPnJhYx2pdaqLdDohjaqj5SvP%2FT18pLOu76e9jOsy4f58LFWgMyWhwHAACdZTlDnhp74mvWyRVUY41L2pmaUcmO3gx9Ua%2Bcu5RBT9" rel="nofollow">3个问题带你入门数据建模</a></li>
<li><a href="https://link.segmentfault.com/?enc=Vkyw%2B8kpNYRNPvSi9OvWkQ%3D%3D.wkgWyd7BKZW%2BweQt%2FAGwPkvGBRpW75ipNcYfJH44daLDs8%2BlOhDakQS7e%2B1DJMuj8SPEhwFeOZsBCYvX6tczpK2b8pQOk8Ei%2B%2BSwRf65YWT%2FXEbs6dPcsLU2iyaT1oMJNpsXkNUrye%2FKPH3lTIPibhhsVEe3rP0TRsVSqkyRzF%2BB43zTk5o2F2T3qd4C7iRzn%2BdNFSpTiVd4Vj6tNi0wJUd2nGHIyKssW11xU8sMSr5tXU3jReUGVzC9oqpQ%2B0gqHijXxZDSQ4Q1zh6AlWq1UIp6XkJEyUwR1U2A0CG55Sxy06jtXlMMPmdmgpGIxPpv" rel="nofollow">程序员笔记|API网关如何实现对服务下线的实时感知</a></li>
<li><a href="https://link.segmentfault.com/?enc=e2UQOS7YaLrNNN8AofgK3A%3D%3D.WsTOAUQltcVt8xCDHC8VRy5jsgI0F4%2BAvEypYXvqGwnaxb1kM1d19sKcXGJpTVodj62Tnl6pWDb9MGbPrisTfgSnR7AGSR6r7QXuytsL1mJW7xEIxg1FJ7S4YqsOBPjFhGW5o4SuAD3tG09snwLWlioHlNujRAfTzwbBYGadaYi6j%2FUXjCdntZu2Dmr37NIWeTxdWWLMgp8KGr8EZC7iIVyvtWBPteFvfIcv8t01s6YK3N3beVItz6N8tWi%2F4H1G1GkeYhvJyZtBGIDbvrmhPXqpKyCn2bRQD3yaLJmOaiMkDArzi4hS3nmXOJlzWbOh" rel="nofollow">程序员笔记|三步实现Django Paginator 分页</a></li>
<li><a href="https://link.segmentfault.com/?enc=6WEleB84l8v92euFiq%2BVww%3D%3D.dWnTKE3RlW%2FzRlNZAVjWuO2RKyGUDYiLNqlFRS7q7Or3QGCdIMGuFA%2FR3OL2YFsN4koPwTJL0YjHHOGsCeJziXdqI54W%2BeaJ0fQkaDYCyajaQSHZEM2hmaGfH3d%2BckUejnt8ZRnvs65UsLwzdHO6uzKPrf%2BODjlyp2B%2BviXY1Cfhxdop7T6nTC1HVdIBx4tL2LE7HY8mBqa8PCLem98NEjDirhylN5iJP%2F9KkzkW%2Fo89SldZmS6XbPEvruQ1Yv%2FyaMhlE26BOaiujS3Quc3ArEGkpB%2F1eCLhb6FizX1M%2BoCX8oSHwgVQXoNHA1TP46dQ" rel="nofollow">安卓跨平台开发实践</a></li>
</ul>
<p><strong>【年货包2:2019全年技术沙龙 视频回放+速记+ppt全套内容】</strong></p>
<p><strong>获取方式:</strong></p>
<p>关注公众号“<strong>宜信技术学院</strong>”回复关键字“<strong>科技年货</strong>”,即可获取。</p>
案例解读宜信如何运用区块链双链技术重构供应链金融服务
https://segmentfault.com/a/1190000021522286
2020-01-08T11:45:44+08:00
2020-01-08T11:45:44+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p>近日,银保监会下发了《中国银保监会办公厅关于推动供应链金融服务实体经济的指导意见》(以下简称“《意见》”),该《意见》提出,鼓励银行保险机构将物联网、区块链等新技术嵌入交易环节,提升智能风控水平。</p>
<p>区块链技术具有去中心化、不可篡改、高透明度等多种特性,在供应链金融领域有天然优势,能够真正解决交易环节的信任问题,提升供应链上下游企业的融资效率与效果。<br>供应链金融模式早已有之,围绕核心企业来管理上下游中小企业的资金流和物流,将单个企业的不可控风险转变为供应链企业整体的可控风险。</p>
<p>而事实上,供应链金融仍然不能彻底解决中小微企业融资难、融资贵的难题,近年来市场竞争日益激烈,中小微企业的资金周转和融资问题愈发凸显,缺少抵押品、征信记录及交易凭证等现实情况导致中小微企业很难从银行获得贷款。</p>
<p>针对上述痛点,宜信公司推出Blockworm BaaS平台,自主研发区块链+供应链双链技术,双链模式能够将生产编号、出库订单、运输订单、签收订单等生产销售相关的数据上链,还可以将供应链全流程的资产生产和资产流转信息流上链,供应流程中的数据即可作为中小微企业融资的信托凭证。双链的结合真正实现了数据有信用、信用有价值,并帮助金融科技更好地服务于实体经济。</p>
<h2>案例背景</h2>
<p>供应链上下游企业中,中小企业不仅对信息流有一定的要求,而且对资产流也有不同程度的需求。传统的供应链金融由于信息不透明,不能及时掌握上下游的生产编号、出库订单、运输订单、签收订单、票据信息、保理服务等信息,所以核心企业的信用很难传递到整个链条中,进一步加剧了融资“难、贵、慢”等问题。</p>
<h2>技术方案</h2>
<p>供应链金融的基础就是供应链,宜信通过区块链技术打通供应链条中的相关协作方,首先可以提高整个供应链的协作效率,其次多方的参与还可以为交易的真实性增信。</p>
<p>宜信Blockworm BaaS平台的双链技术,通过创建基于供应链的区块链环境,帮助客户打通系统间的数据壁垒。为了更好地实现交易闭环,还提供了翼融链小程序,买家可以基于翼融链对链上订单进行签收,并上传相应的凭证,整个过程所产生的数据都保存到区块链中。不仅如此,翼融链还记录了买家的位置信息、登录信息,为上层应用进行风险预警,为大数据分析提供了可信的基础数据。</p>
<p>当中小微企业需要融资时,即可提供相应的订单编号到资产链上,双链的结合让信息流转化成了资产流。资金方会根据订单编号等信息,在供应链(区块链)上溯源整个交易过程,交叉验证交易的真实性,以及相关企业的协作信息等,这不仅起到了增信的作用,而且还降低了融资成本,提高了融资效率。</p>
<p><img src="/img/remote/1460000021522289" alt="" title=""></p>
<h2>成功案例</h2>
<p>某大宗电商撮合平台,买家下单后,卖家会去上游工厂采购商品,并通知物流公司安排运输,最终买家进行货物的签收。在整个协作过程中,会产生订单、出库单、运输单、签收单等单据,这些纸质单据都充当着信用传递和数据传递的媒介,任何一个环节出现了不可信的情况,都会对整个供应链造成风险。</p>
<p>宜信Blockworm BaaS平台借助区块链双链技术,打通了各个参与方系统的数据壁垒,让每个参与方都在区块链上进行数据协作。由于区块链提供了可信的环境,整个过程中可以省去相应的协作单据,从而大大提高了供应链的协作效率。另外,通过资产产生信息和资产流转信息的交叉验证,结合参与方的身份信息和交易内容,共同作为金融机构进行风控和授信的依据,促进了资金的快速运转。</p>
<p><img src="/img/remote/1460000021522290" alt="" title=""></p>
<p>(供应链+资产链双链结构模式)</p>
<h2>实现效果</h2>
<p>区块链双链技术提高了整个供应链的协作效率,打通了信息流和资产流,不仅提高了数据的安全性和保密性,降低了融资的风险和成本,提高了融资效率,同时促进了资金的快速运转,也为金融科技助力实体经济的发展提供了新的尝试。</p>
<blockquote>作者:宜信区块链实验室张一杰<p>来源:宜信技术学院</p>
</blockquote>
敏捷开发流程之Scrum:3个角色、5个会议、12原则
https://segmentfault.com/a/1190000021512361
2020-01-07T14:22:18+08:00
2020-01-07T14:22:18+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
5
<p>本文主要从Scrum的定义和目的、敏捷宣言、Scrum中的人员角色、Scrum开发流程、敏捷的12原则等几方面帮助大家理解Scrum敏捷开发的全过程。</p>
<h2>一、Scrum的定义和目的</h2>
<p>Scrum是一个用于开发和维护复杂产品的框架,是一个增量的、迭代的开发过程,目的是让开发人员像打橄榄球一样迅猛并充满激情,通过团队合作,提高工作效率。通过团队间的有效交互,为企业创造价值。</p>
<h2>二、敏捷宣言</h2>
<p>其实,在发表《敏捷宣言》之前,很多的敏捷实践都已经存在且使用了,比如:Scrum、XP、KanBan等。之所以发表《敏捷宣言》,是因为这些实践都是在单打独斗地推进敏捷开发,而不是以一个联合体的形式,且没有一个统一的指导方针。所以17位敏捷联合创始人决定发表《敏捷宣言》,共同在全世界推进敏捷开发运动。下面是敏捷宣言的4句话:</p>
<p><img src="/img/remote/1460000021512364" alt="" title=""></p>
<h2>三、Scrum中的人员角色</h2>
<h3>3个角色</h3>
<p>Scrum中的人员分为3个角色:产品所有者(Product Owner), Scrum Master,开发团队(Team)。</p>
<ul>
<li>产品所有者:定义所有产品功能,决定产品发布的内容以及日期,对产品的投入产出负责,根据市场变化对需要开发的功能排列优先顺序,合理地调整产品功能和迭代顺序,认同或者拒绝迭代的交付。</li>
<li>ScrumMaster :ScrumMaster不是项目经理,他没有分配任务的权力,没有考核的权力,没有下命令的权力,他指导项目组的成员按照Scrum的原则、方法做事情,领导团队完成Scrum的实践以及体现其价值,排除团队遇到的困难,确保团队胜任其工作,并保持高效的生产率,使得团队紧密合作,使得团队个人具有多方面职能的工作能力,保护团队不受到外来无端影响。</li>
<li>开发团队:经典团队拥有 5-9 人,团队成员包含程序员、测试员、用户体验设计等等,团队关系在一个迭代中应该是固定的,个人的职能可以在新迭代开始时发生调整,团队自我组织和管理(自组织,自驱动),团队成员都全职工作。</li>
</ul>
<h2>四、Scrum的开发流程</h2>
<p><img src="/img/remote/1460000021512365" alt="" title=""></p>
<p>(图片源自网络)</p>
<p>不同于瀑布模型将开发过程划分为需求、设计、编码、测试等阶段,Scrum将整个开发过程分为多次迭代(称为Sprint,冲刺),一般为期2~4周,最常见的为2周。Scrum并非以一段时间集中完成一个过程,而是将所有过程中必须的每一部分集中在这段时间内完成。需求、设计、编码、测试、上线都必须在一个迭代中完成,每个迭代必须产生一个可以工作的软件。</p>
<h3>4.1 五个会议</h3>
<p>Scrum 整个开发过程分为五个会议:</p>
<p>1)待办事项整理会议(Backlog Grooming Meeting)</p>
<p>迭代计划会议开始之前3天召开,Product Owner与Scrum Master必须参加,关键开发者或架构师需要参加;时间控制在30分钟到1小时。</p>
<p>由Product Owner将一批希望团队在下次迭代时实现的用户故事,按照实现顺序描述给在场的团队成员,Scrum Master与在场成员分析用户故事,明确指出团队认为需求不明确的地方,Product Owner现场记录,会后补全,Scrum Master与架构师,还有在场成员分析用户故事需要包含哪些技术任务,Scrum Master先把子任务建立,方便迭代计划会议的时候团队可以更准确地预估任务故事点。</p>
<p>会议结束时,Product Owner确保在迭代计划会议开始之前团队提出的问题都能被解决,会议重点如果团队发现需要加强或是完善的地方,Product Owner还有两到三天的时间可以补强,而不是浪费迭代计划会议的时间去做这件事情。</p>
<p>2)迭代计划会议(Sprint Planning Meeting)</p>
<p>产品负责人建立产品功能列表(Product Backlog)。产品功能列表是一组条目化需求,它必须从客户价值角度描述,并按优先级排序。</p>
<p>Scrum Master召集相关人员召开迭代计划会,迭代计划会在每个迭代第一天召开,目的是选择本次迭代的Backlog和估算本次迭代的工作量。</p>
<p>产品负责人逐条讲解最重要的产品功能,开发团队共同估算Backlog所需工作量,直到本迭代工作量达到饱和。产品负责人参与讨论并回答和需求相关的问题,但不干扰估算结果。队员认领任务(或由组长协商分发),独立或与别人一起完成任务;会议时间控制在1-2小时内。</p>
<p>3)每日站会(Standup Meeting)</p>
<p>团队内部利用每日立会来沟通进度,15分钟结束,开发团队利用燃尽图来展示整体进度;如无特殊原因,迭代期内无变更,在每日站会上团队成员需要回答以下3个问题:</p>
<ul>
<li>昨天你做了什么?</li>
<li>今天你将要做什么?</li>
<li>你有需要帮助的地方吗?</li>
</ul>
<p>这些都是团队成员的彼此承诺。</p>
<p>4)评审会(Retrospective Meeting)</p>
<p>小组向产品负责人展示迭代工作结果,产品负责人给出评价和反馈。以用户故事是否能成功交付来评价任务完成情况。整个团队都需要参加,ScrumMaster、产品所有者、团队,可能还有客户,时间控制在1-2小时内。</p>
<p>5)反思会(Retrospective Meeting)</p>
<p>在每个迭代后召开简短的反思会,总结哪些事情做得好,哪些事情做得不好。做得好的保留,不好的摒弃。会议得出这样的结论:开始做什么、继续做什么、停止做什么,一般控制在15-30分钟。</p>
<p>Scrum是一套开发流程,是敏捷的一种,实施主要还是看人,强调是自组织、自驱动的,只有不断的在实际应用中仔细体会,才能理解Scrum的真谛,把Scrum用好。</p>
<h3>4.2 12原则</h3>
<p>下面给出敏捷开发的12原则,这12原则作为敏捷开发对于软件开发流程的指导性纲领,也是对敏捷宣言进行了具有实际操作意义的解释,希望大家在实际应用中仔细体会。</p>
<p>我们遵循以下准则:</p>
<ul>
<li>我们的最高目标是,通过尽早和持续地交付有价值的软件来满足客户。</li>
<li>欢迎对需求提出变更——即使是在项目开发后期。要善于利用需求变更,帮助客户获得竞争优势。</li>
<li>要不断交付可用的软件,周期从几周到几个月不等,且越短越好。</li>
<li>项目过程中,业务人员与开发人员必须在一起工作。</li>
<li>要善于激励项目人员,给他们以所需要的环境和支持,并相信他们能够完成任务。</li>
<li>无论是团队内还是团队间,最有效的沟通方法是面对面的交谈。</li>
<li>可用的软件是衡量进度的主要指标。</li>
<li>敏捷过程提倡可持续的开发。项目方、开发人员和用户应该能够保持恒久稳定的进展速度。</li>
<li>对技术的精益求精以及对设计的不断完善将提升敏捷性。</li>
<li>要做到简洁,即尽最大可能减少不必要的工作。这是一门艺术。</li>
<li>最佳的架构、需求和设计出自于自组织的团队。</li>
<li>团队要定期反省如何能够做到更有效,并相应地调整团队的行为。</li>
</ul>
<blockquote>作者:史文帅<p>来源:宜信技术学院</p>
</blockquote>
揭秘宜信财富年度账单的技术实现
https://segmentfault.com/a/1190000021476477
2020-01-03T11:57:20+08:00
2020-01-03T11:57:20+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<h2>一、背景</h2>
<p>年底很多移动产品都会推出自己的年度账单,每年都会引起大众媒体的关注。今年有幸参与了宜信财富APP年度账单的开发,本文将带大家探索宜信财富年度账单背后的技术架构和研发逻辑,希望可以给大家带来一些思路上的启发。</p>
<h2>二、前端整体架构和执行流程</h2>
<p>宜信财富年度账单前端架构所采用的技术栈包括:</p>
<ul>
<li>前端页面是用H5制作;</li>
<li>数据加载进度百分比,技术用到swiper插件和一些CSS3动效;</li>
<li>海报生成用到了canvas图片合成,把海报背景和二维码合二为一。</li>
<li>为了完成MGM的追踪,在二维码中嵌入M1信息。</li>
</ul>
<h2>三、数据来源和数据处理</h2>
<p>本次年度账单涉及客户维度、销售维度、客户标签等数据,其中客户维度包括活动参与、文章、视频浏览等数据。这部分数据的整合来源于数据中台的主题数据。以下是数据中台的架构图:</p>
<p><img src="/img/remote/1460000021476481" alt="" title=""></p>
<ul>
<li>ODS:数据来源层,存放从业务系统抽取过来的数据,业务系统中的原始数据经过抽取、洗净、传输装入本层。这层数据接近原始数据,却不等同原始数据,数据装入的时候进行了去重、去噪、表命名、字段命名等一系列规范操作。</li>
<li>DW:数据仓库层,该层是数据仓库的主体,将ODS层的数据按照主题建立数据模型,是为企业所有级别的决策制定过程,提供所有类型数据支持的战略集合,是一个包含所有主题的通用的集合。</li>
<li>DM:数据集市层,是以某个业务应用为出发点而生成的字段比较的宽表,用于提供后续的业务查询、OLAP分析、数据分发等,该层数据主要由轻度汇总层和明细层数据计算生成。</li>
</ul>
<p>在数据中台的架构上,我们建立了“以客户为中心”的标签体系。该套标签体系按照人口属性、价值指标、地理指标、心理指标等几大类对数据进行分层管理,标签的加工方式主要来源于DW和DM层数据的轻量汇总或者衍生加工,以及部分模型生成的产品预测等标签。这套标签体系支持360度客户画像以及分析关键接触点,提供基于跨渠道全流客户体验优化和关键触点优化。</p>
<p><img src="/img/remote/1460000021476482" alt="" title=""></p>
<p>本次账单的数据主要来源于业务操作、用户管理等源系统数据,这些数据被结构化地存储在数据库集群中,且都已接入数据中台,并按照定时任务或者实时数据落入对应主题域。账单数据通过其相应主题数据加工而成,前端通过接口API访问数据。</p>
<p>账单需求里的“销售评价消息实时推送”和“账单传播短信发送”都是通过智能运营系统支持的,该系统是集运营活动创建、执行、管理、反馈、迭代为一体的自动化平台,能够通过用户属性、标签、计划、操作等数据筛选客群,实现目标的精准触达,提升关键指标和运营效率。</p>
<p>下面是智能运营系统创建运营计划流程图:</p>
<p><img src="/img/remote/1460000021476480" alt="" title=""></p>
<ul>
<li>销售评价消息实时推送:该功能依赖wormhole实时平台将数据落到数据库,然后在智能运营系统里配置数据,最终通过消息中心和极光将消息推送到产品终端。</li>
<li>账单传播短信发送:按照业务规则筛选符合条件的客群,在智能运营系统里配置短信模板等内容,然后调用notify通过短信平台将短信发送给客户。</li>
</ul>
<h2>四、技术后台</h2>
<p>用户数据来源于宜信财富平台本身数据,包含:基础信息、浏览信息、参与活动等多项数据,如何保证数据准确、高效地传达到前端是后端开发所必须保障的。资产平台采用了spring+jersery+oracle+redis+jetCache的技术架构,为了提升用户体验度,加快响应时间,数据存储上该项目采用了缓存、非关系数据库和传统关系数据库灵活结合的方式,更好地提供数据支撑。</p>
<p>在对接年度账单需求时,我们也着重考虑了接口响应时间。年度账单用户数据包括用户活动数据及操作数据两张表,其中操作数据是一个重量级表格,为了减少数据库的IO操作,采用了两种方式来减少IO时间:</p>
<ul>
<li>根据数据组提供的标签,尽量减少访问资产数据表的几率;</li>
<li>利用java8的Stream的新特性,将复杂的SQL逻辑放进代码中进行处理。</li>
</ul>
<p>Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,更像一个高级版本的 Iterator。</p>
<p>此外Stream还提供了并行技术,在不关注集合内部数据顺序的时候,可以采用并行Stream拆解任务来加速处理过程。例如在做统计,需要将子产品进行汇总,或其他操作时。</p>
<p>如果将复杂的代码逻辑直接用SQL来实现,代码会非常冗长,执行效率也不高。代码的逻辑是使用并行流Stream,根据类型对相关数据进行分类汇总,并且根据本次需求的业务场景将某一子类划分到另一个类别下。</p>
<p>使用Stream并行流代替SQL逻辑可以加速执行效率,减少响应时间。感兴趣的同学如果想了解Stream的更多特性,可以参考技术文档。Stream的应用能够让代码逻辑更加清晰,提高速度。</p>
<h2>五、总结</h2>
<p>此项目是由多个团队共同协作完成,本文对年度账单需求做了一次技术层面的梳理,由于时间比较匆忙,内容不太详细,希望可以给大家带来一些开发思路,也希望用户可以真切感受到我们的用心。</p>
<blockquote>来源:宜信财富管理技术团队<p>作者:米志华、孙李强、李力、赵全超</p>
</blockquote>
揭秘“撩”大数据的正确姿势:生动示例解说大数据“三驾马车”
https://segmentfault.com/a/1190000021449959
2019-12-31T10:53:29+08:00
2019-12-31T10:53:29+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
0
<blockquote>我是我:“缘起于美丽,相识于邂逅,厮守到白头!” <p>众听众:“呃,难道今天是要分享如何作诗?!”</p>
<p>我是我:“大家不要误会,今天主要的分享不是如何作诗,而是《揭秘:‘撩’大数据的正确姿势》,下面进入正题。”</p>
</blockquote>
<p>话说当下技术圈的朋友,一起聚个会聊个天,如果不会点大数据的知识,感觉都融入不了圈子,为了以后聚会时让你有聊有料,接下来就跟随我的讲述,一起与大数据混个脸熟吧,不过在“撩”大数据之前,还是先揭秘一下研发这些年我们都经历了啥?</p>
<h2>缘起:应用系统架构的从 0 到 1</h2>
<h3>揭秘:研发这些年我们都经历了啥?</h3>
<p>大道至简。生活在技术圈里,大家静下来想想,无论一个应用系统多庞大、多复杂,无非也就是由一个漂亮的网站门面 + 一个丑陋的管理模块 + 一个闷头干活的定时任务三大板块组成。</p>
<p>我们负责的应用系统当然也不例外,起初设计的时候三大模块绑在一起(All in one),线上跑一个 Tomcat 轻松就搞定,可谓是像极了一个大泥球。</p>
<p><img src="/img/remote/1460000021449964" alt="" title=""></p>
<p>衍化至繁。由于网站模块、管理平台、定时任务三大模块绑定在一起,开发协作会比较麻烦,时不时会有代码合并冲突出现;线上应用升级时,也会导致其它模块暂时不能使用,例如如果修改了一个定时任务的配置,可能会导致网站、管理平台的服务暂时不能用。面对诸多的不便,就不得不对 All in one 的大泥球系统进行拆解。</p>
<p><img src="/img/remote/1460000021449962" alt="" title=""></p>
<p>随着产品需求的快速迭代,网站 WEB 功能逐渐增多,我们起初设计时雄心勃勃(All in one 的单体架构),以为直接按模块设计叠加实现就好了,谁成想系统越发显得臃肿(想想也是走弯路啦!)。所以不得不改变实现思路,让模块服务下沉,分布式思想若现——让原来网站 WEB 一个系统做的事,变成由子系统分担去完成。</p>
<p><img src="/img/remote/1460000021449965" alt="" title=""></p>
<p>应用架构的演变,服务模块化拆分,随之而来的就是业务日志、业务数据散落在各处。随着业务的推广,业务量逐日增多,沉淀的数据日益庞大,在业务层面、运维层面上的很多问题,逐渐开始暴露。</p>
<ul>
<li>在业务层面上,面对监管机构的监管,整合提取散落在各地的海量数据稍显困难;海量数据散落,想做个统计分析报表也非常不易。</li>
<li>在运维层面上,由于缺少统一的日志归档,想基于日志做快速分析也比较困难;如果想从散落在各模块的日志中,进行调用链路的分析也是相当费劲。</li>
</ul>
<p>面对上述问题,此时一个硕大的红色问号出现在我们面前,到底该如何解决?</p>
<p><img src="/img/remote/1460000021449963" alt="" title=""></p>
<p>面对结构化的业务数据,不妨先考虑采用国内比较成熟的开源数据库中间件 Sharding-JDBC、MyCat 看是否能够解决业务问题;面对日志数据,可以考虑采用 ELK 等开源组件。如果以上方案或者能尝试的方式都无法帮我们解决,尝试搬出大数据吧。</p>
<p><img src="/img/remote/1460000021449967" alt="" title=""></p>
<p>那到底什么时候需要用大数据呢?大数据到底能帮我们解决什么问题呢?注意,前方高能预警,门外汉“撩”大数据的正确姿势即将开启。</p>
<h2>邂逅:一起撬开大数据之门</h2>
<h3>槽点:门外汉“撩”大数据的正确姿势</h3>
<p>与大数据的邂逅,源于两个头痛的问题。第一个问题是海量数据的存储,如何解决?第二个问题是海量数据的计算,如何解决?</p>
<p><img src="/img/remote/1460000021449968" alt="" title=""></p>
<p>面对这两个头痛的问题,不得不提及谷歌的“三驾马车”(分布式文件系统 GFS、MapReduce 和 BigTable),谷歌“三驾马车”的出现,奠定了大数据发展的基石,毫不夸张地说,没有谷歌的“三驾马车”就没有大数据,所以接下来很有必要逐一认识。</p>
<p>大家都知道,谷歌搜索引擎每天要抓取数以亿计的网页,那么抓取的海量数据该怎么存储?</p>
<p><img src="/img/remote/1460000021449971" alt="" title=""></p>
<p>谷歌痛则思变,重磅推出分布式文件系统 GFS。面对谷歌推出的分布式文件系统 GFS 架构,如 PPT 中示意,参与角色着实很简单,主要分为 GFS Master(主服务器)、GFS Chunkserver(块存储服务器)、GFS Client(客户端)。</p>
<p>不过对于首次接触这个的你,可能还是一脸懵 ,大家心莫慌,接下来容我抽象一下。</p>
<p><img src="/img/remote/1460000021449966" alt="" title=""></p>
<p>GFS Master 我们姑且认为是古代的皇上,统筹全局,运筹帷幄。主要负责掌控管理所有文件系统的元数据,包括文件和块的命名空间、从文件到块的映射、每个块所在的节点位置。说白了,就是要维护哪个文件存在哪些文件服务器上的元数据信息,并且定期通过心跳机制与每一个 GFS Chunkserver 通信,向其发送指令并收集其状态。</p>
<p>GFS Chunkserver 可以认为是宰相,因为宰相肚子里面能撑船,能够海纳百川。主要提供数据块的存储服务,以文件的形式存储于 Chunkserver 上。</p>
<p>GFS Client 可以认为是使者,对外提供一套类似传统文件系统的 API 接口,对内主要通过与皇帝通信来获取元数据,然后直接和宰相交互,来进行所有的数据操作。</p>
<p>为了让大家对 GFS 背后的读写流程有更多认识,献上两首歌谣。</p>
<p><img src="/img/remote/1460000021449970" alt="" title=""></p>
<p>到这里,大家应该对分布式文件系统 GFS 不再陌生,以后在饭桌上讨论该话题时,也能与朋友交涉两嗓子啦。</p>
<p>不过这还只是了解了海量数据怎么存储,那如何从海量数据存储中,快速计算出我们想要的结果呢?</p>
<p><img src="/img/remote/1460000021449969" alt="" title=""></p>
<p>面对海量数据的计算,谷歌再次创新,推出了 MapReduce 编程模型及实现。</p>
<p>MapReduce 主要是采取分而治之的思想,通俗地讲,主要是将一个大规模的问题,分成多个小规模的问题,把多个小规模问题解决,然后再合并小规模问题的结果,就能够解决大规模的问题。</p>
<p>也有人说 MapReduce 就像光头强的锯子和锤子,世界上的万事万物都可以先锯几下,然后再锤几下,就能轻松搞定,至于锯子怎么锯,锤子怎么锤,那就是个人的手艺了。</p>
<p>这么解释不免显得枯燥乏味,我们不妨换种方式,走进生活真实感受 MapReduce。</p>
<p><img src="/img/remote/1460000021449972" alt="" title=""></p>
<p>斗地主估计大家都玩过,每次开玩之前,都会统计一副牌的张数到底够不够,最快的步骤莫过于:分几份给大家一起数,最后大家把数累加,算总张数,接着就可以愉快地玩耍啦... ...这不就是分而治之的思想吗?!不得不说架构思想来源于人们的生活!</p>
<p>再举个不太贴切的例子来感受MapReduce 背后的运转流程,估计很多人掰过玉米,每当玉米成熟的季节,地主家就开始忙碌起来。</p>
<p><img src="/img/remote/1460000021449974" alt="" title=""></p>
<p>首先地主将一亩地的玉米分给处于空闲状态的长工来处理;专门负责掰玉米的长工领取任务,开始掰玉米操作(Map 操作),并把掰好的玉米放到在麻袋里(缓冲区),麻袋装不下时,会被装到木桶中(溢写),木桶被划分为蓝色的生玉米木桶、红色的熟玉米木桶(分区),地主通知二当家来“收”属于自己的那部分玉米,二当家收到地主的通知后,就到相应的长工那儿“拿回”属于自己的那部分玉米(Fetch 操作),二当家对收取的玉米进行处理(Reduce 操作),并把处理后的结果放入粮仓。</p>
<p>一个不太贴切的生活体验 + 一张画得不太对的丑图 = 苦涩难懂的技术,也不知道这样解释,你了解了多少?不过如果以后再谈大数据,知道 MapReduce 这个词的存在,那这次的分享就算成功(哈哈)。</p>
<p>MapReduce 解决了海量数据的计算问题,可谓是力作,但谷歌新的业务需求一直在不断出现。众所周知,谷歌要存储爬取的海量网页,由于网页会不断更新,所以要不断地针对同一个 URL 进行爬取,那么就需要能够存储一个 URL 不同时期的多个版本的网页内容。谷歌面临很多诸如此类的业务场景,面对此类头痛的需求,该怎么办?</p>
<p><img src="/img/remote/1460000021449973" alt="" title=""></p>
<p>谷歌重磅打造了一款类似以“URL + contents + time stamp”为 key,以“html 网页内容”为值的存储系统,于是就有了 BigTable 这个键值系统的存在(本文不展开详述)。</p>
<p><img src="/img/remote/1460000021449975" alt="" title=""></p>
<p>至此,两个头痛的问题就算解决了。面对海量数据存储难题,谷歌推出了分布式文件系统 GFS、结构化存储系统 BigTable;面对海量数据的计算难题,谷歌推出了 MapReduce。</p>
<p>不过静下来想想,GFS 也好、MapReduce 也罢,无非都是秉承了大道至简、一人掌权、其它人办事、人多力量大的设计理念。另外画龙画虎难画骨,建议闲暇之余也多些思考:为什么架构要这么设计?架构设计的目标到底是如何体现的?</p>
<p><img src="/img/remote/1460000021449977" alt="" title=""></p>
<p>基于谷歌的“三驾马车”,出现了一大堆开源的轮子,不得不说谷歌的“三驾马车”开启了大数据时代。了解了谷歌的“三驾马车”的设计理念后,再去看这些开源的轮子,应该会比较好上手。</p>
<p>好了,门外汉“撩”大数据就聊到这儿吧,希望通过上文的分享能够了解几个关键词:大道至简、衍化至繁、谷歌三驾马车(GFS、MapReduce、BigTable)、痛则思变、开源轮子。</p>
<h2>白头:番外篇</h2>
<h3>扯淡:不妨换一种态度</h3>
<p>本文至此也即将接近尾声,最后是番外篇~</p>
<p><img src="/img/remote/1460000021449976" alt="" title=""></p>
<p>首先,借用日本剑道学习心诀“守、破、离”,希望我们一起做一个精进的人。</p>
<p><img src="/img/remote/1460000021449978" alt="" title=""></p>
<p>最后,在有限的时间内要多学习,不要停下学习的脚步,在了解和使用已经有的成熟技术之时,更要多思考,开创适合自己工作场景的解决方案。</p>
<blockquote>文章来源:宜信技术学院 & 宜信支付结算团队技术分享第6期-宜信支付结算部支付研发团队高级工程师许赛赛《揭秘:“撩”大数据的正确姿势》<p>分享者:宜信支付结算部支付研发团队高级工程师许赛赛</p>
<p>原文首发于公号-野指针</p>
</blockquote>
速度提升50%!宜信区块链Blockworm BaaS平台的架构与特性解析
https://segmentfault.com/a/1190000021416555
2019-12-27T10:05:42+08:00
2019-12-27T10:05:42+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
3
<p>2008年,中本聪发表了一篇题为《比特币:点对点的电子现金系统》的论文,首次提出了关于区块链概念的描述。</p>
<p>从2008年至今,区块链技术因其去中心化、不可篡改、可追溯等技术特性,获得了越来越多的关注,也逐渐被应用于各个领域。</p>
<p>2018年,工业和信息化部信息中心编写发布了《2018年中国区块链产业白皮书》,白皮书中提到,区块链作为一项颠覆性技术,正在引领全球新一轮技术变革和产业变革,有望成为全球技术创新和模式创新的“策源地”,推动“信息互联网”向“价值互联网”变迁。</p>
<p>据《2018年中国区块链产业白皮书》显示,截至2018年3月底,中国以区块链业务为主营的区块链公司数量已经达到了456家,产业初步形成规模。</p>
<p>宜信对于区块链的研究早已开始,公司专门设立区块链实验室。2018年6月份,宜信发布Blockworm BaaS平台,帮助企业快速构建适合自己的区块链服务,同时打通数据孤岛,为企业提供可信的区块链协作平台。</p>
<h2>Blockworm BaaS</h2>
<p>由于区块链本身技术复杂,一般企业要使用区块链解决自身业务问题,有很高的技术门槛和管理门槛。为了帮助企业快速地使用区块链,方便地运营和管理区块链,降低使用门槛,宜信开发了Blockworm BaaS 区块链云平台系统。核心主旨就是:帮助企业快速创建区块链底层平台,实现应用的快速部署,提高使用效率,降低使用门槛,减少企业部署和运维的成本。</p>
<p><img src="/img/remote/1460000021416558" alt="" title=""></p>
<p>(Blockworm BaaS 平台架构图)</p>
<p>Blockworm BaaS是一个区块链基础服务云平台,可以帮助企业在云上快速创建区块链底层运行平台,配置和管理各种区块链组件,降低企业使用区块链的门槛,减少企业部署和运维区块链的成本。同时基于可视化的平台,对于区块链上的节点、联盟参与方、智能合约、操作日志等都可以进行管理、查看、审计等。</p>
<p>使用Blockworm BaaS,用户可以针对不同的业务场景构建不同的链,并可进行分区存储,在保证企业隐私的同时满足个性化需求。</p>
<p>Blockworm BaaS区块链平台的主要特点:</p>
<ul>
<li>一键部署,快速部署区块链环境。</li>
<li>可视化管理,方便快捷运营和运维区块链。</li>
<li>权限管理,多维度管理智能合约和区块链权限。</li>
<li>快速接入,提供基于https的安全调用,满足各种系统对接。</li>
<li>多密码算法支持,支持国际ECC和国内密码算法,满足不同企业需要。</li>
<li>混合云部署,支持国内主流云厂商环境部署。</li>
<li>性能强力优化,通过对Fabric的改造,基于负载均衡的方式,提高区块链交易速度,实现50%的性能提升。</li>
<li>支持跨链的资产调用。</li>
<li>共识协议的动态插拔,用户可以灵活选用需要的共识算法。</li>
</ul>
<h2>翼融链</h2>
<p>由于很多中小企业信息系统建设不完整,甚至没有系统,另外部分中大型企业也面临系统上链的对接困难、实施周期长等问题。为了解决上述痛点,我们研发了另外一款产品-翼融链,帮助企业解决供应链过程中的签收问题,同时保证了交易信息的链上闭环。</p>
<p><img src="/img/remote/1460000021416559" alt="" title=""></p>
<p>翼融链的主要特点:</p>
<ul>
<li>一键签收。读取链上订单信息,快速确认签收。</li>
<li>全程上链。凭证、位置、状态等证据全部链上存储。</li>
<li>动态监测。基于智能合约实时跟踪签收异常信息。</li>
</ul>
<h2>宜信Blockworm BaaS,构建可信商业环境</h2>
<p>随着区块链技术应用范围的扩大,当前各个领域对区块链技术的需求不断升温,而由于区块链本身技术复杂,一般企业使用区块链解决业务问题,有很高的技术门槛和管理门槛。宜信Blockworm BaaS平台以其一键部署、可视化管理的优势,帮助企业快速构建区块链底层平台;Blockworm BaaS还具备灵活、高性能等技术优势,满足各种场景下以区块链为底层技术的应用开发需求,帮助企业实现应用的快速部署,保障安全、提高效率、降低运维和管理成本。</p>
<p>目前,宜信Blockworm BaaS平台已在宜信内部多个业务场景中落地使用,同时在供应链金融、存证、溯源、数字资产、征信、支付结算等多领域有成功案例。“人人有信用,信用有价值”,宜信Blockworm BaaS平台旨在助力企业快速创建区块链可信平台,用区块链技术构建可信商业环境和完善的社会信用体系。</p>
<blockquote>作者:宜信区块链实验室张一杰<p>来源:宜信技术学院</p>
</blockquote>
宜信技术学院上榜「2019中国技术品牌影响力企业榜」
https://segmentfault.com/a/1190000021407199
2019-12-26T12:09:25+08:00
2019-12-26T12:09:25+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
2
<p><img src="/img/bVbB0rS" alt="思否技术品牌影响力企业证书.png" title="思否技术品牌影响力企业证书.png"></p>
<p>12月25日,中国最大的新一代开发者社区和专业技术媒体SegmentFault发布了2019 中国技术品牌影响力企业榜。凭借过去一年对宜信科技成果和技术实践的传播以及在金融科技领域探索方面的积极努力,宜信技术学院登上榜单前10。</p>
<p><img src="/img/remote/1460000021407202" alt="" title=""></p>
<p>SegmentFault作为中国最大的新一代开发者社区和专业技术媒体,是国内 DGC (Developer Generated Content) 内容最丰富、技术问答板块最活跃的开发者社区。目前已经覆盖和服务了超过 300 万开发者、1000 家科技企业,帮助开发者解决了超过数十万个技术问题,每日增长数千条问答数据。用户原创产生的优质技术文章已累积超过10万篇,上百家科技企业技术团队入驻。每月开发者访问流量超千万,Alexa国内网站排名90位。</p>
<p>「SF·中国技术品牌影响力企业」是由SegmentFault独家策划,依据社区数百万用户行为数据及技术品牌在国内市场的行为大数据进行分析得出最终榜单排行。</p>
<p><img src="/img/remote/1460000021407203" alt="" title=""></p>
<p>宜信技术学院成立于2017年,专注于分享与传播宜信技术团队的研发实践成果与技术解决方案。经过多年的探索和实践,宜信技术团队沉淀了海量的软件研发经验,特别是在金融科技领域,积极探索软件技术在金融服务与金融安全保障方面的智能化、创新性的技术解决方案,并取得了非常多的技术成果。2019年,围绕“科技支持、科技赋能、科技引领”的理念,宜信技术团队通过数据中台、AI中台等中台化产品的落地,开发出智能聊天机器人等AI产品,对实际业务场景中的用户体验提升起到了非常大的作用。宜信技术学院通过宜信技术成果开源、研发实践经验分享、技术沙龙活动3大核心模块对外输出这些优秀的经验与成果。</p>
<p>宜信技术学院作为宜信技术团队在软件研发实践经验上的传播窗口,持续专注于宜信技术实践经验的萃取、沉淀与传播,面向金融科技行业及软件研发行业,分享软件技术在宜信业务场景中落地的经验与思路,最终实现技术赋能业务发展和服务升级的目标。</p>
宜信微服务架构落地及其演进|分享实录
https://segmentfault.com/a/1190000021395868
2019-12-25T14:21:04+08:00
2019-12-25T14:21:04+08:00
宜信技术学院
https://segmentfault.com/u/crediteasetech
3
<h2>一、应用服务架构演进及微服务架构介绍</h2>
<h3>1.1 应用架构的演进历程</h3>
<p><img src="/img/remote/1460000021395871" alt="" title=""></p>
<p>应用服务架构一直处于不断演进的过程中,上图通过对比5种比较主流的架构模式,展示应用架构的演进历程和变化。</p>
<ul>
<li>单体架构(All in One)。在业务发展初期,为了快速落地应用,满足客户需求,一般会使用All in One的单体架构。单体架构的特点是:所有模块都耦合在一个进程里,系统完全封闭且很复杂,牵一发动全局。</li>
<li>竖井式架构(Vertical Application)。随着业务的增长,单体架构越来越臃肿,我们对系统做了垂直化的拆分,应用架构进入第二阶段即竖井式架构。竖井式架构,就是根据业务属性将一个大的单体拆分成一些不同的模块或子系统,子系统之间没有直接关联。竖井式架构依然存在紧耦合的问题,系统也是完全封闭的,且存在大量的重复代码拷贝及模块功能需大量重复造轮子的情况。</li>
</ul>
<p>单体架构和竖井式架构都是围绕web容器打包及部署的架构模式,随着业务的快速发展,要求实现服务的快速迭代和快速交付,应用架构也由此演进为以服务为中心的架构模式。主流的面向服务的架构模式有:RPC架构、ESB中心化架构和微服务架构。</p>
<ul>
<li>RPC架构。RPC架构在现在的应用系统中还是比较常见的架构模式,适用于高并发场景,性能比较好。Dubbo就是一个典型的RPC架构。RPC架构也存在一些问题:通过共享分布式对象实现远程方法调用,如果在其中一个对象里添加一个属性,就会对共享对象的生产者与消费者产生影响,所以RPC架构也是紧耦合的模式,系统交互采用RPC私有TCP协议,服务生产者和消费者存在强代码依赖,异构系统集成不友好。</li>
<li>ESB中心化架构。ESB中心化架构实现了松耦合,依赖于ESB消息总线技术实现异构系统的信息交互和集成集中式架构管理,因此它虽然是面向服务的,但它本质上依旧是一个中心化的架构。其优势在于:基于WebService技术,重量级的消息通讯机制,我们称之为“智能管道哑终端”,当团队规模比较大、要实现异构系统集成时,它可以提供统一的解决方案和技术实现方式,快速集成异构系统对外服务。ESB中心化架构的问题也比较明显:中心化架构难以满足灵活性的服务迭代和需求交付。</li>
<li>微服务架构。微服务架构实现了系统解耦和持续集成,有清晰的服务边界,粒度相对ESB架构和传统SOA架构来说更小,使用轻量级的通讯机制交互,具备更强的扩展性和弹性,能够更灵活、更快响应业务变化。</li>
</ul>
<p>通过上述对比,我们不难发现,应用服务架构是在不断演进的,而且其演进背后存在一定的逻辑,服务架构的演进主要取决于以下2个维度:</p>
<ul>
<li>业务维度,技术架构是由业务发展所处的时期和阶段决定的,技术架构要能够解决业务发展过程中的痛点。在进行架构选型时,需要考虑这个架构是否能满足当前业务的需求,业务需求能否随着架构的演进实现增量式的迭代。</li>
<li>技术维度,一方面要满足非功能需求,使得业务快速跟上技术生态的发展;另一方面出于商业的技术考量,比如去IOE、 去V、 采用开源的技术解决方案的需求,逐渐完成服务底层使用的商业软件的技术隔离,满足业务快速交付。</li>
</ul>
<h3>1.2 微服务架构的定义</h3>
<p>关于微服务的定义,此处引用ThoughtWorks首席科学家Martin Fowler给出的描述。</p>
<p><img src="/img/remote/1460000021395875" alt="" title=""></p>
<p>其中以下特性值得特别注意:</p>
<ul>
<li>微服务架构是一种架构模式,提倡将单⼀应⽤程序划分成⼀组⼩的服务。</li>
<li>服务之间采用轻量级的通信机制。</li>
<li>服务可以独立部署。</li>
<li>应当尽量避免统⼀的、集中式的服务管理机制。</li>
<li>具体的⼀个服务可以选择合适的语⾔、⼯具对其进⾏构建。</li>
</ul>
<p>Martin Fowler对于微服务架构的表述更偏向学术上的定义,没有给出明确的落地标准或规范,只是提供了一些构建微服务架构的原则。</p>
<h4>1)面向开发者和业务实现</h4>
<ul>
<li>拆分成粒度小的服务。</li>
<li>各自独立的进程实现服务运行隔离。</li>
<li>服务运行通过轻量级的基于HTTP协议的RESTful API的通信机制进行。</li>
<li>服务关注围绕业务领域之上构建。</li>
</ul>
<h4>2)面向服务的交付和运维</h4>
<ul>
<li>在服务交付方式上,强调可独立部署。</li>
<li>支持去中心化的设计,服务一般不需要集中化的管理。</li>
</ul>
<h3>1.3 如何筛选微服务</h3>
<p>微服务架构模式有如此多的优点,那是不是所有的业务都要采用这种架构模式呢?又该如何筛选微服务?</p>
<p><img src="/img/remote/1460000021395872" alt="" title=""></p>
<p>左边图中,横坐标代表系统复杂度、纵坐标代表开发生产力、蓝色线表示微服务架构、绿色线表示单体架构。由图可知,当项目复杂度较低时,单体架构的生产力更高;随着项目复杂度越来越高,单体架构的生产力逐渐下降,微服务架构的生产力则显著提高。</p>
<h4>1)3种场景可以考虑使用微服务(Are you tall enough? )</h4>
<ul>
<li>规模大,团队超过10人。</li>
<li>业务复杂度高,系统超过5个子模块。</li>
<li>需要长期演进,项目周期超过半年。</li>
</ul>
<h4>2)其他因素筛选微服务</h4>
<ul>
<li>软件功能变化频繁,以快速迭代、缩短交付周期为核心的业务。</li>
<li>模块有独立的生命周期,微服务强调服务复用,减少重复造轮子,实现降本增效。</li>
<li>有独立的隔离性需求和扩展性需求(容错)。</li>
<li>简化的外部依赖。比如Facade模式场景,后端系统使用统一的对外暴露的形式提供服务。</li>
</ul>
<h3>1.4 如何拆分构建微服务</h3>
<p>鉴别出哪些业务需要使用微服务架构模式后,需要决定如何拆分和构建微服务。</p>
<p><img src="/img/remote/1460000021395874" alt="" title=""></p>
<h4>1)服务拆分</h4>
<p>如何进行服务拆分,是在微服务过程中业务方经常会问到的问题。</p>
<p>其实很多团队已经开始在做一些微服务化的工作,比如把大的工程拆分成不同的模块或子系统,这种对业务模块进行的静态划分,相当于已经完成了微服务改造的第一步拆分。</p>
<p>上图是DDD(领域驱动设计)的开发模式,如果业务方案已经确定采用微服务的架构模式,在整个工程领域我们倾向于使用DDD模式来对业务架构和服务进行拆分。</p>
<p>DDD是基于领域模型的建模而不是数据库表驱动的建模,需要我们对业务领域有深刻的洞察,了解服务的边界和上下文信息传递。</p>
<p>康威定律指出:在微服务架构和设计系统组织,其产生的设计等价于组织间的沟通结构。就是说微服务架构不仅是技术上的演进,同时对使用技术的组织提出了要求,拆分的服务是我们和服务之间的沟通方式。</p>
<h4>2)微服务构建</h4>
<p>我们采用微服务的12因子作为微服务建设的架构原则。微服务的12因子也叫云原生12因子,它提供了一种业务上云或微服务改造的最佳实践。重点介绍其中几个因子:</p>
<ul>
<li>基准代码,建议项目使用一份基准代码、多份部署。项目在拆分时可能会存在多份基准代码,造成大量重复性的不同版本的代码共存的现象。在进行微服务构建时,需要把公共服务和公共代码抽取出来,统一支撑不同版本的业务。</li>
<li>显式依赖,显式声明依赖关系。对于 Java 程序,在 Gradle 或者Maven中写明依赖关系;</li>
<li>配置,在环境中存储配置。根据当前的环境变量决定使用什么样的配置文件。</li>
<li>后端服务,把后端服务当成一个附加资源。</li>
<li>构建、发布、运行的分离机制,强调服务和构建在运行时,不可以直接修改运行中的代码,而是需要通过构建发布流程统一发布。</li>
<li>无状态进程,以一个或多个无状态的进程运行应用。</li>
</ul>
<h3>1.5 微服务架构2种建设思路</h3>
<p>微服务看起来非常好,但其实是需要一个技术体系或平台体系来支撑的,如果没有这样一个服务架构平台体系的建设,不推荐使用微服务。</p>
<p><img src="/img/remote/1460000021395876" alt="" title=""></p>
<p>微服务架构建设分为2种思路:SDK模式、ServiceMesh模式。</p>
<h4>1)SDK模式</h4>
<p>典型代表是SpringCloud,SpringCloud是基于SpringBoot的一整套实现微服务的框架。SDK模式的底层运行平台可以是PaaS平台,也可以是Kuberneters平台或Docker容器。</p>
<ul>
<li>优势:面向应用和开发人员,定制化、协议支持灵活,适合完全自治的服务状态,方便线下调试,对操作系统平台无依赖。</li>
<li>缺点:应用需引入额外SDK依赖包,SDK本身占用内存及系统资源,对业务是侵入性的;需要构建微服务基础设施做业务能力支撑;需要使用SideCar模式实现异构系统集成。</li>
</ul>
<h4>2)ServiceMesh模式</h4>
<p>Istio 是ServiceMesh模式的典型代表。ServiceMesh模式的优缺点与SDK模式正好相反。</p>
<ul>
<li>优势:不需要额外引入SDK依赖包,对应用无侵入,且对Kubernetes天然友好支持。</li>
<li>缺点:部署比较复杂,对底层系统有一定的依赖;通讯协议类型支持受限,需要依赖Mesh平台兼容。</li>
</ul>
<h2>二、SpringCloud微服务生态体系介绍</h2>
<p>SpringCloud是基于SpringBoot发展而来的一整套成熟的微服务架构解决方案。SpringCloud具有以下优势:</p>
<ul>
<li>面向开发者,以开发者为中心,开发者生态友好。</li>
<li>以Java技术栈为主要开发语言,代码可复用,微服务转型成本低。</li>
<li>基础设施完备,提供端到端的微服务架构解决方案。</li>
<li>有很多大厂做背书,如Pivital、Netflix,、alibaba等公司都是其生态和源代码贡献者,技术经历过大规模商业应用的考验。</li>
</ul>
<h3>2.1 SpringCloud的技术生态</h3>
<p><img src="/img/remote/1460000021395873" alt="" title=""></p>
<p>SpringCloud本身也是一个逐渐演进的架构模式:最早是基于IOC/AOP的编程思想产生的;然后在Spring的基础上发展出SpringBoot,基于注解的方式实现快速的应用开发;后来在SpringBoot的基础上开发出SpringCloud底层微服务构架。</p>
<p>上图展示了SpringCloud的技术生态,SpringCloud技术栈包含了很多技术模块,比如Ribbon、Zuul、Eureka、SpringCloud Stream等,这些技术模块共同组成了SpringCloud生态圈,为开发者提供丰富的微服务架构基础设施支撑。</p>
<h3>2.2 微服务和SpirngCloud架构的复杂性</h3>
<p><img src="/img/remote/1460000021395878" alt="" title=""></p>
<p>微服务和SpirngCloud的架构是比较复杂的,如配置管理、服务注册与发现、API网关、打包部署调度、安全、服务故障自愈、流量控制和弹性伸缩等非功能需求,都是微服务需要包含的架构模块。上图中蓝色字表示SpirngCloud、Kubernetes等用来解决云原生和微服务架构问题的技术方案。</p>
<p>从图中可以看出微服务架构的复杂性,要想实现一套微服务架构来支撑和交付业务,需要在底层封装很多基础组件,构建一套底层基础架构来隔离底层的非功能需求,做到让业务系统无感知、平滑地对外提供服务。</p>
<h2>三、宜信微服务架构和SIA网关的4种模式</h2>
<p>SpringCloud提供的框架或基础设施是一个半成品,我们在SpirngCloud的基础上进行了二次开发,抽象和封装了一些微服务架构的通用基础设施平台,不同的业务团队共享这些基础设施,降低技术学习和接入成本,让业务团队更专注于业务逻辑的实现,聚焦业务开发。</p>
<h3>3.1 宜信微服务架构</h3>
<p><img src="/img/remote/1460000021395877" alt="" title=""></p>
<p>上图所示为宜信的微服务架构:</p>
<ul>
<li>微服务网关,sia-gateway使用了去中心化的网关接入方案。</li>
<li>元数据服务层,用Eureka-plus和sia-config对注册中心Eureka和配置中心做了增强与优化。</li>
<li>任务管理,基于微服务的思想,开发和开源了微服务任务调度平台SIA-TASK。</li>
<li>容器平台,实现了快速的自动化构建、部署和服务编排。</li>
<li>DevOps,微服务的交付频率、速度比较快,需要有持续集成、持续开发工具和手段来保障项目质量和服务正常运行,对此我们有自研的UAVStack监控系统、自动化测试系统等工具链。</li>
</ul>
<h3>3.2 SIA微服务网关架构</h3>
<p><img src="/img/remote/1460000021395879" alt="" title=""></p>
<p>有别于其他的架构模式,微服务架构里出现了一个重要的基础设施变化-增加了微服务网关模块。网关主要解决的问题是:服务拆分之后,每一个服务粒度都比较小,服务之间的交互会呈现网状的结构,需要一个聚合的节点来聚合这些微服务。</p>
<p>因此我们在SpringCloud微服务架构的基础上二次开发出SIA微服务网关,如图所示,重点介绍其中的2个核心模块:</p>
<ul>
<li>网关组,网关组里封装了两种网关:同步网关和异步网关。</li>
<li>OAM,自助式网关管理平台,所有业务节点的生命周期管理都通过这个模块来进行。</li>
</ul>
<h3>3.3 SIA微服务网关的4种模式</h3>
<p><img src="/img/remote/1460000021395880" alt="" title=""></p>
<p>SIA微服务网关的4种模式:同步托管式、同步注解式、异步托管式、异步注解式。</p>
<h4>1)同步托管式</h4>
<p>采用单一源码库进行代码管理,交付方式是容器交付,去中心化的设计。目前大部分生产环境和业务都采用同步托管模式。</p>
<ul>
<li>优点:网关交付对业务方完全透明;只要在容器平台申请一个网关资源,就可以得到网关服务;基于Filter开发运维简单,容量小。</li>
<li>缺点:定制化不够强。</li>
</ul>
<h4>2)同步注解式</h4>
<p>在跟业务团队对接时,我们发现很多业务系统已经实现了一些独特的业务逻辑,难以迁移到网关,所以我们采用一种比较兼容的注解的方式去适应这些业务逻辑,在原有项目的基础上加一个注解,将它们纳入到整个网关管理体系中来。同步注解式是基于SpringCloud-Zuul 1实现的分布式微网关体系,管理业务方源码库,根据业务方环境进行交付。</p>
<ul>
<li>优点:对项目的控制力比较强,业务团队独立运维,支持扩展定制化;基于Filter开发运维简单,容量小。</li>
<li>场景:业务定制化场景比较多。如果要加载初始化、加大资源,或对业务的Filter拦截机制有定制化需求,都可以用同步注解模式。</li>
</ul>
<p>SpringCloud和Zuul使用的后端技术是基于Servlet,其线程处理模型是一个请求对应一个线程,当请求量过多,线程栈溢出,就会占用非常多的资源,导致网关无法提供额外的线程资源来处理新进来的请求。因此我们采用了SpringCloud自研的SCG技术方案。</p>
<p>SpringCloud-Gateway基于Netty和反应式编程模式,采用收敛式的线程处理模型,只要用少数线程就可以处理高并发的流量请求。目前已经实现了基于SpringCloud-Gateway的异步模式,当同步模式在线上运行过程中出现资源透支的情况,就选择使用异步模式。异步模式也分为2种:异步托管式、异步注解式。</p>
<h3>3)异步托管式</h3>
<p>通过单一源码库进行代码管理,采用容器交付。主要使用场景是流量型,如果业务多对高并发、高吞吐场景,建议使用异步托管式。</p>
<h4>4)异步注解式</h4>
<p>如果想在异步网关基础上做定制开发,可以使用异步注解的模式。</p>
<p>网关的4种模式来源于业务的需求:为兼容业务已有逻辑演进出注解模式;当出现性能瓶颈、资源浪费时,采用异步模式应对高并发流量。</p>
<p><img src="/img/remote/1460000021395882" alt="" title=""></p>
<p>上图是网关测试环境的一个截图, 包括上述4种模式。每一个小方格代表业务的一个网关组,方格中的小圆圈代表它属于哪一种网关。业务系统在选择网关模式时要做一个判断:诉求是支持业务的快速集成,还是对流量有一定要求。</p>
<h3>3.4 SIA微网关的核心Feature</h3>
<p><img src="/img/remote/1460000021395881" alt="" title=""></p>
<p>如图将SIA微网关的核心Feature分成2个层面:</p>
<ul>
<li>数据面,负责数据包的处理逻辑。路由转发、负载均衡、灰度发布、日志审计、熔断限流等都是从数据层面对流量进行管理。</li>
<li>控制面,负责服务粒度上的管理。统一视图管理、多租户管理、注册中心、配置管理、路由组件绑定等是从控制层面来保障和管理服务。</li>
</ul>
<h3>3.5 SIA微网关对微服务的生命周期管理</h3>
<p>微服务网关贯穿了整个微服务生命周期的管理。</p>
<p><img src="/img/remote/1460000021395884" alt="" title=""></p>
<p>SIA微服务网关的功能包括:</p>
<ul>
<li>Swagger UI、模块复用对应服务文档中心模块的功能。</li>
<li>动态路由、共享能力集中在分布式网关节点。</li>
<li>日志管理、管控组件是网关Master提供的功能。</li>
</ul>
<p>各功能模块对应的生命周期:</p>
<ul>
<li>服务文档中心,对应整个软件生命周期的初期开发和设计阶段,网关提供一个个统一的API视图,前端和后台可以通过Swagger UI来联调开发。</li>
<li>分布式网关节点,在开发和部署阶段会涉及到一些共享能力的部署和运行。</li>
<li>网关Master,在运维阶段通过自动化运维提高运维效率;在管理方面,提供数据统计的功能,生成数据报告用于管控。</li>
</ul>
<p>SIA微服务网关作用于软件生命周期的各个阶段,通过标准协同、业务测试/前端后端沟通、服务模块复用、可视化管理、数据统计管控等实现业务的统一融合、降本增效。</p>
<h3>3.6 总结:微服务架构与中台&后台</h3>
<p><img src="/img/remote/1460000021395883" alt="" title=""></p>
<p>2016年,Gartner 发布了一个关于应用变化速率的报告《Pace-Layered Application Strategy》,以应用变化速率为标准将业务应用分为三层:</p>
<ul>
<li>SOI-敏态业务:比如互联网业务,需求变更快,要求快速迭代 、快速交付。</li>
<li>SOR-稳态业务: 比如传统业务,变更周期⻓、变化频率低、变化成本高、变化⻛险高。</li>
<li>SOD-中台业务: 齿轮匹配失衡,中台就像是在前台与后台之间添加的⼀组“变速⻮轮”,将前台与后台的速率进行匹配,提升⽤户响应力。</li>
</ul>
<p>中台的目标是围绕业务组织进行可复用能力的有机整合,协助业务落地实施、改造、试错、转型,提升组织效率,降低系统成本。</p>
<p>中台和微服务有什么关系呢?微服务架构是面向开发的架构,很多基础服务可以沉淀到微服务架构里,同时,微服务架构把中台的能力快速释放出来,满足敏态业务快速变更的业务需求。</p>
<h2>四、应用场景及典例分析</h2>
<h3>4.1 分解&聚合</h3>
<p><img src="/img/remote/1460000021395885" alt="" title=""></p>
<p>上图是路由管理里的一个截图,当一个大的单体或不同的服务要对外提供统一服务时,可以把服务聚合到网关上;同时一个巨型应用也可以通过网关分解成微服务。</p>
<h3>4.2 复用&个性化</h3>
<p><img src="/img/remote/1460000021395887" alt="" title=""></p>
<p>微服务架构中有很多非功能需求,或者说是技术导向型的需求,包括日志管理、限流、蓝绿部署、版本管理等,可以通过组件的方式下沉到网关上,业务系统通过将服务与组件绑定实现对组件功能的复用。</p>
<p>我们还提供了一个插件机制,当业务有独特的需求,可以根据其业务逻辑在网关上进行功能的个性化定制。</p>
<h3>4.3 API&契约</h3>
<p><img src="/img/remote/1460000021395886" alt="" title=""></p>
<p>在开发或前后端联调时,前后端可以通过网关服务文档中心的Swagger UI功能模块访问后端服务调用接口的分析。</p>
<p>只要在后端服务之上加一个Swagger注解,网关就可以把所有对外暴露的服务抓取出来,这相当于是一种契约式的开发。</p>
<h3>4.4 容错&保护</h3>
<p><img src="/img/remote/1460000021395888" alt="" title=""></p>
<p>我们对网关应用做了容错和保护机制,当然这也是SpringCloud本身自带的一个技术模块,我们的容错机制是基于SpringCloud的Hystrix实现的,当发现后端服务调用请求一直在返回错误时,会开启熔断,避免由于一直发送错误请求导致雪崩的情况发生。</p>
<p>除此之外,我们还会采用Guava限流的方式对服务进行保护。在大促或秒杀的场景下,会有大量请求进来,这时会通过限流来保护服务的稳定。</p>
<h3>4.5 监控&治理</h3>
<p><img src="/img/remote/1460000021395889" alt="" title=""></p>
<p>宜信微服务架构平台有一个很重要的功能是网关服务运行状态和后端连接状态可观测,提供了很多监控方面的功能组件,如图所示,可以统计当前请求的频率、服务健康度。</p>
<p>预警方面重点介绍网关拓扑图。当请求失败,当前链路出现异常,通过网关拓扑图可以快速跟踪和判断业务系统哪个节点出现问题,然后对有问题的节点进行摘除或其他操作。</p>
<p>我们的网关运行在Docker平台上,Docker平台在出现问题或重启之后日志会丢失,我们的日志系统会把日志归集,存储到ES中,便于对历史日志溯源。</p>
<h3>4.6 统计&分析</h3>
<p><img src="/img/remote/1460000021395890" alt="" title=""></p>
<p>网关中有一个组件叫“监控统计”,这个模块默认是不打开的,如果你想对请求做延时,或者想看请求的明细调用情况,可以通过组件管理中打开这个组件,对容器的请求做统计和分析。监控统计组件会对当前请求的最大延迟、最小延迟、失败个数、平均延迟进行排序,一目了然。</p>
<h2>五、微服务化架构建设遇到的问题</h2>
<h3>5.1 设计初期的问题</h3>
<h4>1)如何隔离业务网关,同时有统一的管理视图?</h4>
<p>构建微服务网关初期,业务同事比较关注我们的业务网关和别人的网关是否存在耦合问题,他的业务请求是否会影响到我。我们选用去中心的网关设计方式,同时通过OAM实现对所有网关节点的统一管理。</p>
<h4>2)平台型系统建设初期是否要考虑同步异步网关融合?</h4>
<p>我们的微服务网关是按照DDD领域驱动模式来建设的,没有把网关绑定在某一个特殊的技术实现上,而是把它作为一个抽象封装来统一管理后端的节点,如果换一种技术实现也不会影响到前端业务的正常工作。因此在架构建设初期要考虑清楚你的业务系统和后端技术架构之间是否解耦。</p>
<h3>5.2 使用开源方案的问题</h3>
<h4>3)开源系统本身的bug</h4>
<p>虽然每一种开源方案在开源之前都经过了长时间的考验,但其实依然可能存在bug,基于这些开源方案进行二次开发时仍可能遇到一些坑,我们会不断对开源系统进行bug修复和功能增强。</p>
<h4>4)系统的性能问题</h4>
<p>Zuul本身存在性能瓶颈,当出现性能问题时,我们考虑是不是要用线程收敛的模式来增强网关的性能。</p>
<h3>5.3 上线生产运维方面的问题</h3>
<h4>5)如何克服Eureka注册中心的CAP问题?</h4>
<p>在网关应用中会遇到Eureka的CAP问题,因为Eureka消息注册以可用性(Availability)优先,在一致性(Consistency)上相对较弱。为解决这个问题,我们基于Eureka的特点提供SynchSpeed服务,如果业务需要保证状态一致性,可以开启这个服务。</p>
<h4>6)SpringCloud与SpirngBoot不同版本的兼容问题? K8S平台与微服务注册中心状态同步?</h4>
<p>这两个问题是指当云容器平台的状态发生变更,却没有及时通知到注册中心,导致服务在两个平台的状态不一致,这就需要做上下文关联系统(StakeHolder)的整合。</p>
<blockquote>内容来源:宜信技术学院第8期技术沙龙-线上直播|宜信微服务架构落地及其演进<p>主讲人:宜信高级架构师 & 宜信科技中心基础研发部SIA微服务网关负责人王佩华</p>
</blockquote>