Block使用

一.概述

闭包 = 一个函数(或者指向函数的指针) + 改函数执行的外部上下文变量; Block 是 OC对于闭包的实现。
其中 ,Block:
    1. 可以嵌套定义,定义Block方式和定义函数方法类似
    1. Block可以定义在方法内部或者外部
    1. 只有调用Block时候,才会执行{}体内的代码

    使用clang将oc代码转换为c++文件查看block的方法:
    在命令行输入命令: clang -rewrite-objc 需要编译的oc文件

二. Block的定义与使用

1.无参数无返回值
    void(^block1)(void) = ^(void){
        NSLog(@"无参数,无返回值");
    };
    //block1调用
    block1();
2.有参数无返回值
    void(^block2)(int a) = ^(int a){
        NSLog(@"%d",a);
    };
    //block2调用
    block2(10);
3.有参数有返回值
    int(^block3)(int a,int b) = ^(int a, int b){
        return  a+b;
    };
    //block3调用
    int c = block3(10,5);
4.无参数有返回值
 int(^block4)(void) = ^(){
        return  20;
    };
    //block4调用
    int d = block4();
5.开发中可以用typedef定义block

例如定义

 typedef int (^MyBlock)(int,NSString *);

此时MyBlock就可以定义这种类型的block

@property (nonatomic, strong) MyBlock myBlock;
//使用时
   self.myBlock = ^int(int a, NSString *str) {
        return 10;
    };

三. Block使用外部变量

1.截获自动变量(局部变量)值

block使用外部变量时,将外部变量复制到其内部来访问的。只有使用的时候才会截获,不使用则不截获,截获自动变量会是block的体积变大。默认情况下,block只能访问局部变量,而不能修改局部变量的值,如果在block修改局部变量,会报错。

    int a = 10;
    void(^block5)(void) = ^(){
        NSLog(@"%d",a);
    };
    a = 15;
    block5(); // 输出 10
(2).__block修饰的外部变量

用__block修饰的外部变量,block引用的是这个变量的地址,是地址传递,是可以修改这个变量的值的,也可以获取修改后的最新值

   __block int a = 10;
    void(^block5)(void) = ^(){
        NSLog(@"%d",a);
    };
    a = 15;
    block5(); // 输出 15

四.Block的类型

block有三种类型:
  • 1.全局block(NSGlobalBlock
  • 2.栈block(NSStackBlock
  • 3.堆block(NSConcreteMallBlock
    全局block存在于全局内存中,栈block存在于栈内存中,超出作用域马上被销毁,堆block存在于堆内存中,需要管理内存

(1)block不访问外部变量,此时block就是全局block

    void(^block6)(void) = ^(){

    };
    block6();
    NSLog(@"%@",block6); //  <__NSGlobalBlock__: 0x10ace6190>

(2)block访问外部变量
MRC环境下:block存储在栈中
ARC环境下:block存储在堆中

    // MRC 栈block
   __block int a = 10;
   self.myBlock = ^(){
        NSLog(@"%d",a);
    };


    NSLog(@"%@",self.myBlock); //   <__NSStackBlock__: 0x7ffeef3f3a20>
   // ARC 堆block
   __block int a = 10;
    void(^block6)(void) = ^(){
        NSLog(@"%d",a);
    };

    block6();
    NSLog(@"%@",block6); //   <__NSStackBlock__: 0x7ffeef3f3a20>

ARC环境下,访问外界变量,block会自动从栈区拷贝到堆区,栈区的block在所属的变量作用域结束后,block就会被释放,__block变量也会释放。

我们需要把block复制到堆中,延长block的生命周期,block调用copy方法,栈block就会变成堆block,在ARC环境下,strong和copy修饰的栈block,其实都是堆block。

__block变量和__forwarding

在copy操作之后,__block变量会被复制到堆中,那么在访问__block变量是访问栈上的还是堆上的?

通过__forwarding,无论在block中还是block外访问__block变量,无论是栈block还是堆block,都是访问同一个__block变量

五.block循环引用

block循环引用的情况:
某个类将block作为自己的属性,然后在block方法体内又使用了类本身,就会造成循环引用,例如

 self.myBlock = ^(){
        NSLog(@"%@",self);
    };

解决方案
(1)ARC下:使用__weak

    __weak typeof(self) weakSelf = self;
   self.myBlock = ^(){
        NSLog(@"%@",weakSelf);
    };

(2)MRC下:使用__block

   __block typeof(self) blockSelf = self;
   self.myBlock = ^(){
        NSLog(@"%@",blockSelf);
    };

当UI自动化遇上BDD(一)

首先介绍一下何为BDD,百度百科给的解释是:Behavior Driven Development,行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。
抛开学术定义,在自动化项目中,可以理解为使用自然语言(英语或者中文)编写的测试用例脚本。光说不练假把式,上一段脚本让各位看官有个直观感受。

//场景名称,相当于一条测试用例
Scenario: 用户登录
//打开iPhone6SP手机的少儿英语
Given open_device”iPhone6SP”
//判断是否存在登录按钮
Given if_exists”accessibility,登录”
//点击登录按钮
When click”accessibility,登录”
//清除手机号文本框
When clear”accessibility,edit_phone_number”
//在手机号文本框中输入72166668581
When input”accessibility,edit_phone_number,72166668581″
//清除密码文本框
When clear”accessibility,edit_password”
//在密码文本框输入密码,generatePassword是一个类中的方法,通过java反射调用
When input”accessibility,edit_password,{generatePassword}”
//点击登录按钮
When click”accessibility,btn_login”
//休眠3秒
When sleep”3″
//判断是否存在“我的”按钮,若存在代表进入到首页了
Then assert_exists”accessibility,我的”

脚本中的英文如果不喜欢的话完全可以换成中文,有木有感觉很直观明了。相比通过代码来写自动化,BDD可以降低技术难度,在不了解自动化框架代码的情况下,也可以编写自动化脚本。
我们选择的BDD框架是Cucumber,自动化框架是Appium,两者结合,还有4个优点:
1、多平台支持(IOS、Android、H5)
2、稳定性
3、维护成本
4、可扩展性
相对其他框架来看,Cucumber+Appium在这四个方面做得还是不错的,我们尤其看重可维护性,因为随着项目越做越久,自动化用例会越来越多。只有通俗易懂,最好是和java技术无关,才能拉进更多的人用最低的成本维护更多的测试用例。
时间有限,本篇文章先写这么多,下一期和大家剖析一下脚本背后的代码实现,看看我们是如何通过Cucumber实现的自动化。

使用Mock.js模拟接口数据

前言

敏捷开发过程中,经常出现后端和前端需要并行开发的情况。如果等后端开发完成再进行接口联调,会大大拖慢开发进度。这时候前端就需要要进行后端数据的模拟。

##通用做法

目前大概有两种做法,一种是在前端直接模拟数据,通过拦截xhr的方式来注入数据,如mock.js。另外一种是通过第三方服务去模拟数据请求,如json-server,阿里的RAP,去哪的YApi。今天咱们就说一下使用第一种方式,用Mock.js怎样来模拟数据。

Mock.js介绍

Mock.js 最初的灵感来自 Elijah Manor 的博文 Mocking Introduction,语法参考了 mennovanslooten/mockJSON,随机数据参考了 victorquinn/chancejs

使用Mock.js

安装

yarn add mockjs

引用

import Mock from 'mockjs';

模拟数据

let response = Mock.mock({
  code: 200,
  msg: '',
  data: {
    // 属性 list 的值是一个数组,其中含有 1 到 10 个元素
    'list|1-10': [{
       // 属性 id 是一个自增数,起始值为 1,每次增 1
      'id|+1' :1 
    }]
  }
})

拦截请求

// 通过覆盖和模拟XMLHttpRequest的行为来拦截api/product请求,并返回response
Mock.mock(/api\/product/, response);

有时候会考虑网络较差的情况下,接口返回比较慢时页面的展现优化

// 设置接口返回时间,在100-1500毫秒中随机
Mock.setup({
    timeout: '100-1500'
});

其它

有时候服务端返回的不是一个随机时间,而是一个随机时间戳,mock.js文档中并没有提及怎么实现,所以需要我们处理一下

// 先获取一个随机时间的毫秒数,再去掉毫秒
timestamp: Mock.Random.Data('T')/1000

mock.js的图片模拟是像dummyimage这样的纯色图,在模拟头像时并不能很好的还原真实的场景,所以我们要借用第三方的place image服务

// 传入一个随机参数来保证每条数据返回不同的图片
avatr: `https://placeimg.com/100/100/any?rand=${Math.random()}`

Android通用翻页组件的实现

在伴鱼绘本1.0版中,有两个很重要的核心功能是听绘本和录绘本,这两个功能在页面展现上形式类似,都是模拟看实体绘本书,一页一页的展示,其中有一个非常核心的交互体验设计是翻页效果的设计。当时找了几个效果给大家评估,其中非常复杂的逼近真实翻书体验的、带纸张卷曲效果的翻页效果,然而大家非常明智的认为,这个效果太复杂了,翻页还是应该简单直接一些,因此最终决定,还是简单一些,对于竖向的绘本(图片宽<)手机竖屏展示,模拟书脊在左边,对于横向(图片宽>)绘本手机横屏展示,书脊在中间,用户左右滑动屏幕时,跟随用户手势,做一个页面翻起并落下的效果即可。

然而,这个简单的效果也不是Android自带的,简单的翻了下Github也没有找到类似的实现,看来只能自己造轮子了。因为要左右滑动翻页来展示整个绘本内容,多的可能要20~30页,同时每页也要展示文本、页码等元素,使用ViewPager来实现这个左右滑动的效果是最恰当的。

ViewPager中,默认的页面切换效果是滑动,页面从左到右排列,跟随手势做滑动操作,翻了下ViewPagerAPI文档,以及源码,可以看到setPageTransformer可以设置自定义的页面切换效果,允许在scroll的时候自定义实现页面切换效果。ViewPagersetPageTransformer方法定义如下,

PageTransformer这个接口定义如下:

根据文档在页面Scroll的时候,会调用这个自定义的接口,来根据-1 1 的区间映射到页面从最左边到最右边的位置,这样应用可以添加自定义的变换到页面上以实现自定义的页面切换效果。查看ViewPager的源码,在onPageScrolled方法实现中找到mPageTransformer的调用如下:

也就是说每次onPageScroll 所有ViewPager的子View,除了标记为isDecor的, 也就是说所有的页都会调用一次mPageTransformer.transformPage,用来设置变换。看明白这个原理,很容易就能想到对于竖向的绘本页,在滑动时将每一页的X坐标位置都置到左上角,然后对于正在翻页的那一页绕Y轴做恰当角度的旋转变换,再加上一个透视视角,就能达到竖向绘本沿着书左书脊翻书的效果,代码如下:

主要是3部分逻辑,对于不显示的页,将其透明度置为0,将可能之前设置的Rotation, translation都清除,防止其影响画面展示, 对于画面中显示的两页,首先保证两页显示X坐标都为0且不变,由于ViewPager默认已经将两个页面移动了位置(源码见ViewPageronPageScrolled方法),所以需要做一个反向的setTranslationX操作,将页面复位,然后对于前边一页(正在翻的页),计算旋转角度,设置恰当的camera位置,对于后边一页(不动的页),清空旋转角度。

这个竖向的绘本页翻页动画关键逻辑就这么点了,相对比较简单。但是仔细想下横向的翻页,由于书脊在中间,在页面切换的过程中,某一页绘本半页做Y轴旋转,半页不动,ViewPager提供的这中自定义的扩展不灵了起码不是很直接就能解决问题了。

在绘本页上除了绘本图片还有相关文本的展示,因此不能只绘制图片,还需要处理文本,最好的方案是按整页View处理,而不是手动去绘制图片和文本。有一个思路是当监测到用户滑动页面的时候,不绘制ViewPager的页面,而是将需要显示每一页View截图成一张Bitmap,分成左右两个Bitmap,然后根据滑动距离对Bitmap做一定的变换,绘制出翻页效果。但是这样做有一个问题是每一页里边的绘本图片是异步加载的,可能用户滑动的过程中图片加载出来,这样也需要处理图片加载出来时再更新一下Bitmap,需要将图片下载的逻辑和Bitmap更新的逻辑耦合起来,没办法做成通用的组件,将页面的内容和页面切换效果隔离开来。

更通用的方案是在View的绘制周期做特殊处理,绘制出翻页效果,这样也利用到了View的内容刷新机制,只要View刷新就会自动触发重绘流程。 那么利用View/ViewGroup的那个绘制函数?因为整个翻页效果涉及到多个页元素的自定义绘制,因此自定义ViewPager(继承自ViewGroup)的某个绘制方法可能是合适的方案。一般可以重写drawChild,重写绘制单个子View的过程或者dispatchDraw完整的重写整个ViewGroup的绘制子View的过程,dispatchDraw的灵活性会更大一些,可以完整的控制绘制整个子View的过程,可以以任意顺序绘制。于是就可以根据滑动的比例计算出应该旋转的角度,然后canvasclip一半子View大小,这样就能半个半个的绘制子View实现从View中间翻转的效果。

还有一个点是ViewPager是动态新增和消耗View,甚至重用View的,如何根据页面position找到对应是哪个View在负责显示这一页,从而绘制正确的页?翻看ViewPager的源码,可以看到ViewPager.LayoutParams 有一个position 的变量 用来保存当前View显示的数据的position,但是这个变量是保护的,没办法直接取到,可以通过反射方式来取得具体的值。代码如下:

取得这个值以后,就能通过遍历所有子View的方式找到正确的View

绘制的过程并不复杂,分成两种情况,当前页翻转过程未到50%时,后一个页面右半部分可以显示出来部分,需要最先绘制(简单处理可以全部绘制或者只绘制右半部分,因为会被其他部分覆盖),当前页面左半边是正常绘制,右半边绕中心点旋转一定角度翻转透视,  然后绘制。代码如下(current表示当前第一个显示的页面):

第二种情况是翻转过了50%时, 当前页面只绘制左半边页面(优先绘制),后一页的左半页做一定旋转和透视变换,右半页正常绘制,代码如下:

这样整个动画的绘制就完成了,如果理解了整个View的绘制流程,以及坐标变换的一些知识,即便这种可能半个View需要做变换,另外半个View不太一样的情况,是不是也比较简单就能实现了?

go 堆栈浅析

go 语言堆栈浅析
运行中的程序崩溃了,想要快速定位问题,最直接有效的方法就是查看崩溃时堆栈状态。

下图是一个C++程序崩溃后打印出的内存堆栈信息
http://blog.ipalfish.com/wp-content/uploads/2018/12/1.png
如上图所示,堆栈信息的业务相关部分都由进程名,函数名和调用地址阻成,由下向上显示出调用关系,这样就很容易定位问题,本例的问题就在业务函数的最后一层,也就是红框标注的地方(再向上就是标准库),到代码里查调用关系中的函数名,解决问题的大门也就打开了。

上面的例子核心在于调用关系,其实还有其它有用的信息,比如函数名后面的地址,可以用来定位文件行号等。多了解这些细节,可以让查找过程更加便利。

go语言的堆栈内容和C++的对比起来大同小异,但是go程序要“崩”出这些信息,则要简单的多。
看下面的程序:
http://blog.ipalfish.com/wp-content/uploads/2018/12/2.png
panic一下,程序就崩溃了,堆栈信息也到了标准错误里,重定向一下就可以保存在文件中,非常方便,如果是C/C++的实现,要么修改系统设置,要么调用一些复杂的系统调用,非常麻烦,go的便捷覆已经盖了很多细节的点。

回到主题,上面的go程序运行一下,就会崩溃,然后得到如下堆栈信息
http://blog.ipalfish.com/wp-content/uploads/2018/12/3.png
核心依旧是调用关系,由下到上。
相比C++,go的堆栈信息要更直白,包名+函数名,文件行号,最上面还打印出了panic的内容,定位起问题要方便的多。
再看一个例子
http://blog.ipalfish.com/wp-content/uploads/2018/12/4.png
这例里给函数f加了个参数
崩溃后
http://blog.ipalfish.com/wp-content/uploads/2018/12/5.png
发现函数参数值也被打出来了,这样在出现问题时,有可能连LOG都不用查就知道问题原因了,非常方便。
上面的函数带了一个int参数,复杂一点的函数会如何呢?
看下面程序
http://blog.ipalfish.com/wp-content/uploads/2018/12/6.png
这个函数f基本涵盖了我们能遇到的大部分情况,看看它的堆栈
http://blog.ipalfish.com/wp-content/uploads/2018/12/7-1.png
看main.(*A).f这个函数调用,它对应代码里的f函数
第一个参数是它所属对像的指针
第二个参数是一个优化后的值,把int8,int16,int32这三个值组成一个int64进行传递
第三个参数无法组合优化,就传递了原值4
第四,五两个参数代表了string的在址和长度
第六七八三个参数代表了数组的地址,长度和容量,
第九个参数代表了map的地址。

了解了这些信息,在查找问题时会更加得心应手。

最后一个例子覆盖了一部分情况,其实还有一些更加复杂的情况,比如interface参数,多返回值函数等等,这些需要内容相对复杂,需要结合golang汇编代码去逐个分析,在实际应用中帮助不大,这里就不多说了。

附:
输出.go文件的汇编代码方法
go tool compile –S –S main.go >m.s

iOS多个上下滚动页面详情页的实现

app中这种详情页比较常见:一个页面包含多个可以上下滑动的列表,通过左右滑动页面来切换列表,这些列表又有一个公共的顶部视图。

实现原理

  1. 页面添加一个scrollView用来左右滑动切换不同的列表
  2. 每个列表用tableView或者collectionView来展示数据,将其从左到右添加到scrollView中,如果列表特别复杂,也可生成tableViewControllercollectionViewController,把controllerview从左到右添加到scrollView
  3. 创建一个公共的头部视图,每个列表创建一个空的头部视图,用来装载这个公共的视图
  4. 开始左右滑动时,将公共视图放在主页面的view上,滑动结束后,再放到当前列表页的头部视图上,并设置好公共头部视图的frame
  5. 上下滚动时,同步几个列表页的页面偏移值contentOffset

具体代码如下

  • – (void)viewDidLoad
  • {
  •     [super viewDidLoad];
  •     [self buildScrollView];
  •     [self buildTableView1];
  •     [self buildTableView2];
  •     [self buildCommonHeaderView];
  • }
  • – (void)viewDidLayoutSubviews
  • {
  •     [super viewDidLayoutSubviews];
  •     [self.view bringSubviewToFront:self.xc_navigationBar];
  • }
  • – (void)buildScrollView
  • {
  •     self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, kNavigationBarHeight, self.view.frame.size.width, self.view.frame.size.height – kNavigationBarHeight)];
  •     self.scrollView.contentSize = CGSizeMake(kScreenWidth * 2, 0);
  •     self.scrollView.delegate = self;
  •     self.scrollView.pagingEnabled = YES;
  •     self.scrollView.bounces = NO;
  •     self.scrollView.alwaysBounceHorizontal = NO;
  •     self.scrollView.showsHorizontalScrollIndicator = NO;
  •     if (@available(iOS 11.0, *)) {
  •         self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  •     }
  •     [self.view addSubview:self.scrollView];
  • }
  • – (void)buildTableView1
  • {
  •     self.tableView1 = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
  •     self.tableView1.backgroundColor = [UIColor redColor];
  •     self.tableView1.delegate = self;
  •     self.tableView1.dataSource = self;
  •     [self.scrollView addSubview:self.tableView1];
  •     
  •     self.tableView1.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, HEADER_VIEW_HEIGHT)];
  • }
  • – (void)buildTableView2
  • {
  •     self.tableView2 = [[UITableView alloc] initWithFrame:CGRectMake(self.scrollView.frame.size.width, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
  •     self.tableView2.backgroundColor = [UIColor redColor];
  •     self.tableView2.delegate = self;
  •     self.tableView2.dataSource = self;
  •     [self.scrollView addSubview:self.tableView2];
  •     
  •     self.tableView2.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, HEADER_VIEW_HEIGHT)];
  • }
  • – (void)buildCommonHeaderView
  • {
  •     self.commonHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, HEADER_VIEW_HEIGHT)];
  •     self.commonHeaderView.backgroundColor = [UIColor grayColor];
  •     [self.tableView1.tableHeaderView addSubview:self.commonHeaderView];
  • }
  • #pragma mark – UIScrollViewDelegate
  • – (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
  • {
  •     if (scrollView == self.scrollView) {
  •         CGRect rect = [self.commonHeaderView convertRect:self.commonHeaderView.bounds toView:self.view];
  •         self.commonHeaderView.frame = rect;
  •         [self.view addSubview:self.commonHeaderView];
  •     }
  • }
  • – (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  • {
  •     if (!decelerate) {
  •         if (scrollView == self.scrollView) {
  •             [self addCommonHeaderViewToTableViewHeader];
  •         } else {
  •             [self configContentOffsets:scrollView];
  •         }
  •     }
  • }
  • – (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
  • {
  •     if (scrollView == self.scrollView) {
  •         [self addCommonHeaderViewToTableViewHeader];
  •     } else {
  •         [self configContentOffsets:scrollView];
  •     }
  • }
  • – (void)addCommonHeaderViewToTableViewHeader
  • {
  •     NSInteger index = self.scrollView.contentOffset.x / self.scrollView.frame.size.width;
  •     switch (index) {
  •         case 0:
  •         {
  •             [self.tableView1.tableHeaderView addSubview:self.commonHeaderView];
  •             self.commonHeaderView.frame = self.tableView1.tableHeaderView.bounds;
  •             break;
  •         }
  •         case 1:
  •         {
  •             [self.tableView2.tableHeaderView addSubview:self.commonHeaderView];
  •             self.commonHeaderView.frame = self.tableView2.tableHeaderView.bounds;
  •             break;
  •         }
  •             
  •         default:
  •             break;
  •     }
  • }
  • – (void)configContentOffsets:(UIScrollView *)scrollView
  • {
  •     if (scrollView == self.tableView1) {
  •         if (self.tableView1.contentOffset.y > HEADER_VIEW_HEIGHT) {
  •             if (self.tableView2.contentOffset.y <= HEADER_VIEW_HEIGHT) {
  •                 self.tableView2.contentOffset = CGPointMake(0, HEADER_VIEW_HEIGHT);
  •             }
  •         } else {
  •             self.tableView2.contentOffset = self.tableView1.contentOffset;
  •         }
  •     } else if (scrollView == self.tableView2) {
  •         if (self.tableView2.contentOffset.y > HEADER_VIEW_HEIGHT) {
  •             if (self.tableView1.contentOffset.y <= HEADER_VIEW_HEIGHT) {
  •                 self.tableView1.contentOffset = CGPointMake(0, HEADER_VIEW_HEIGHT);
  •             }
  •         } else {
  •             self.tableView1.contentOffset = self.tableView2.contentOffset;
  •         }
  •     }
  • }
  • #pragma mark – UITableViewDataSource
  • – (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  • {
  •     return 44;
  • }
  • – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  • {
  •     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@”ViewControllerIdentifier”];
  •     if (cell == nil) {
  •         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@”ViewControllerIdentifier”];
  •         cell.backgroundColor = [UIColor clearColor];
  •         cell.selectionStyle = UITableViewCellSelectionStyleNone;
  •     }
  •     
  •     cell.textLabel.text = [NSString stringWithFormat:@”%@-%@”,
  •                            ((tableView == self.tableView1) ? @(1) : @(2)),
  •                            @(indexPath.row)];
  •     
  •     return cell;
  • }

了解JSON Schema

JSON? Schema?

了解JSON Schema首先要知道什么是JSON?

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language,Standard ECMA-262 3rd Edition – December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。

以上是json官网上的说明,简单来说它是一种简化的数据交换格式,是目前互联网服务间进行数据交换最常见的一种交换格式,具有简洁、可读性好等特点。

一个示例json格式比如

{
"employees": [
{
"firstName":"John" ,
"lastName":"Doe"
},
{ 
"firstName":"Anna" ,
"lastName":"Smith"
},
{
firstName":"Peter" ,
"lastName":"Jones" 
}
]
}

现在json相信大家大概明白是什么了 ,现在我们再来看看什么是json schema

如前文所述,json是目前应用非常广泛的数据狡猾格式。既然是用于数据交换的格式,那么就存在数据交换的是双方,如何约定或校验对方的数据格式符合要求,就成了服务交互需要解决的一个问题。所以Json Schema就是用来定义json数据约束的一个标准。根据这个约定模式,交换数据的双方可以理解json数据的要求和约束,也可以据此对数据进行验证,保证数据交换的正确性。

目前最新的Json-schema版本是draft 7,发布于2018-03-19。下面我们就以官网的一个实例来看看Json-schema是如何进行数据约束以及其应用

下面是一个schema示例

{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/product.schema.json",
"title": "Product",
"description": "A product from Acme's catalog",
"type": "object",
"properties": {
"productId": {
"description": "The unique identifier for a product",
"type": "integer"
},
"productName": {
"description": "Name of the product",
"type": "string"
},
"price": {
"description": "The price of the product",
"type": "number",
"exclusiveMinimum": 0
},
"tags": {
"description": "Tags for the product",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
},
"dimensions": {
"type": "object",
"properties": {
"length": {
"type": "number"
},
"width": {
"type": "number"
},
"height": {
"type": "number"
}
},
"required": [ "length", "width", "height" ]
}
},
"required": [ "productId", "productName", "price" ]
}

示例解释

$schema: 说明当前使用的schema版本,可以不包含

$id: 当前schema的唯一id标识,一般指向一个自主域名。方便后续引用,可以不包含

title: 当前schema的标题,简要描述信息,可不包含

description: 详细描述信息,可不包含

type: 约束对象是object,也就是在 { } 中的数据

properties: object中具体属性的约束,description是描述信息,不产生具体约束。type约束productid属性类型为整型

productName: 约束productName属性类型为字符型

price: 约束price属性类型为数字型,可以是整型或浮点型。exclusiveMinimum约束该数字>0(不包含0)

tag: 约束tag属性是array数组。items是数组项约束,这里约束数组项均为字符型
minItems数组至少包含1项。uniqueItems约束数组中每项不得重复
dimensions: 约束dimensions嵌套对象,其中length,width,height均为数字类型且这三个字段在dimensions对象中必须包含
required: 当前数据对象必须包含productId,productName,price三个字段
JSON Schema的应用

对数据做验证

在实际开发中,前端和后端会约定接口,前端根据约定的接口,使用 mock 的数据来开发 demo,而后端去实现接口,前端和后端可以同步进行。等后端开发完毕后,可以通过预先 写好的脚本对返回接口进行批量的数据校验。

根据JSON Schema 生成数据采集UI

Schema的描述已经包含了相当丰富的数据信息,每一种schema类型其实可以对应了一种UI展示。比如生成一个表单,表单的ui逻辑中保证在提交表单前,数据是符合schema规则的,表单通过验证后,得到的就是符合schema的json数据。

如下图

Git基本命令

创建git仓库:

git init 本地文件夹

git clone 远程目录到本地

创建分支

git checkout -b new-branch old-branch

在old-branch分支上创建new-branch分支:

删除分支

git branch -D branchname

分支切换

git checkout 分支名称

git别名设置:git config -global alias.br branch

  • EX:git branch 命令可以用:git br来替换

设置方式:

git config -global alias.br branch

设置成功后,在需要 git branch 命令的地方直接使用 git br

撤销操作:

git commit –amend

代码提交后发现有代码为提交或需要修改提交备注信息,这时先执行git add .命令,
将要提交的文件放到暂存区,然后执行: git commit –amend;

git checkout HEAD files

取消放入暂存区的所有文件执行:git checkout HEAD files

git checkout files

取消对某个文件的修改:git checkout files

git reset HEAD

将暂存区的所有文件都放回工作区:

git reset HEAD 文件路径

将某个文件从暂存区放回工作区:

git reset –soft commit-id

撤销一个commit,工作区和暂存区的内容不变:

git reset –mixed commit-id

错误commit,撤销commit和add,暂存区变化,工作区不变:

git reset –hard commit-id

错误commit之后,想恢复到某个版本库的代码,暂存区,工作区均变化:

git reset –hard origin/master

将本地的状态回退到和远程的一样

git checkout .

撤销工作区所有文件的修改

git checkout –file

撤销某个文件的修改:

Git Tag

列出所有tag: git tag

打tag:git tag v3.1.12

推送到服务器:git push origin v3.1.12

打标签:git tag tagName

创建轻量标签

轻量标签本质上是将提交校验和存储到一个文件中 – 没有保存任何其他信息。

创建轻量标签,不需要使用 -a、-s 或 -m 选项,只需要提供标签名字

EX:git tag v1.4-lw

附注标签:git tag -a v1.4 -m ‘name’

在 Git 中创建一个附注标签是很简单的。 最简单的方式是当你在运行 tag 命令时指定 -a 选项:

EX:git tag -a v1.4 -m ‘my version 1.4’

推送某个tag到服务器:git push origin [tagname]

在创建完标签后你必须显式地推送标签到共享服务器上。

git push origin [v1.4-lw]

推送所有的tag到服务器:git push origin –tags

把所有不在远程仓库服务器上的标签全部传送到那里。

切换标签:git checkout dailyRelease

查看标签的版本信息:git show dailyRelease

删除本地标签:git tag -d name

EX: git tag -d dailyRelease

删除远程标签:git push origin refs/tags/源标签: refs/tags/目标标签

删除远程标签,相当于推送一个空的标签,到目标便签

git push origin :refs/tags/dailyRelease

给指定的commit打标签:git tag -a tagname commitID

EX: git tag -a dailyRelease commitID

补打标签:git tag -a tag-name commit-d

忘记打标签,可以用此命令补充打标签;

Git log

git log /xxx/xxx/filename

查看指定文件或目录的提交信息

git log –graph

以图表形式输出提交日志

git log -p

查看Commit对文件的改变

Git cherry-pick

单个commit只需要git cherry-pick commitid

多个commit 只需要git cherry-pick commitid1..commitid100

分支B做了一个commit,想把这个commit放在分支A上:

git checkout B

git log

git checkout A

git cherry-pick commit-id

release版本;

1、git merge master –no-commit (合并master文件)

2、git commit -m “合并master代码” (提交commit)

3、git push origin release (推送到release)

将本地dev分支合并到master分支然后提交

git checkout dev

git add .

git commit -m “sdfddsaf”

git commit -m “dadsfasd” –amend // 用同一个commit提交(将本次修改和上次合并到同一个commit)

git checkout master

git pull origin master —rebase //拉取远程master分支和本地的master进行合并

git checkout dev

git rebase master //将master分支合并到dev分支上

git checkout master

git merge dev //将dev分支合并到master分支上

git push origin HEAD:refs/for/master //将本地master分支推到远程master上

docker容器原理分析—隔离与限制

docker的使用非常简单,docker run 命令就可以轻松的启动一个docker容器,但是执行docker run背后到底发生了什么,docker是怎么实现容器化的呢?这正是这篇文章想要讨论的主题。

docker是一个C/S架构,在执行docker run命令的时候,其实是docker client通过网络将启动参数提交到docker daemon,由docker daemon来启动我们想要运行的docker容器,当然这是一个非常复杂的过程,但是所有最关键的启动容器操作会落到

int clone(int *child_func)(void *),void *child_stack, int flags, void *arg);

这个系统函数的调用上。熟悉linux的同学都会知道,clonelinux系统调用fork()的一种更通用的实现,用来创建新进程的。这样我们的第一个问题来了,docker容器和虚拟机的区别是什么?

通过上面的我知道docker容器其实是一个进程,而虚拟机是运行在hypervisor之上一个操作系统。虚拟机本身是一个操作系统,在虚拟机上运行的进程天然是以操作系统的粒度进行隔离的,虚拟机除了共享宿主机的硬件资源外,在软件上是完全隔离的。而docker容器只是一个进程,那么容器与宿主机的关系则要复杂的多,容器不仅需要使用宿主机的cpu,内存等硬件资源,还需要宿主机的操作系统为它提供运行时的环境。这样我们的第二个问题来了,docker容器和宿主机是怎么样进行隔离的呢?

docker容器和宿主机需要进行哪些隔离呢?最简单的答案当然是越隔离越好,它们之间最好老死不相往来,最好都不知道对方的存在,但是这明显是做不到的,毕竟docker容器在宿主机上的一个进程,这是一个天生的依赖关系,那么务实来说,docker容器需要哪些隔离?

首先,容器内部不能看到其他的进程,则进程之间的关系需要隔离;其次,容器内部不能看到其他进程对文件系统的修改,则文件系统需要隔离;再次,容器需要有自己的ip、端口和路由等,则网络需要隔离;另外,容器需要自己独立的主机名以便在网络中标识自己,则主机名和域名需要隔离;还有,容器内部也不能看到其他的用户和组,并且容器的创建需要有内部的超级用户权限,但是在宿主机上则只是普通权限,则用户ID、用户组IDroot目录,key以及特效权限等等需要进行隔离;最后,容器内部也不同和宿主机的进程进行进程间的通信,则信号,管道已经共享内存等需要隔离。这个正好是linux支持的利用namespace进行隔离的方法,下面我们一起来看看docker是怎么利用namespace进行实现的。

linux中一个PID namespace对应linux内核是一颗描述进程的层次体系的树,同样PID namespace本身也是一颗树状结构,顶层是系统启动时创建的root namespace,下面依次是新创建的PID namespace。我们在启动容器操作的函数cloneflags增加一个CLONE_NEWPIDflag,操作系统就会对新创建的进程进行PID namespace隔离,对它的进程PID进行重新编号,即该容器进程在宿主机的PID namespace和容器进程新的PID namespace分别有不同的pid,在宿主机中该容器为普通的pid(比如10000),在容器的PID namespace中则是PID1PID1的进程在linux中是一个特殊的进程(进行子进程回收等操作),在容器的PID namespace只能看到属于该PID namespace的进程,这样就实现的进程之间的隔离。

文件系统的隔离和进程之间的隔离也类似,容器进程在启动的时候会增加CLONE_NSflag,通过mount namespace进行隔离的,不同的是容器进程启动后,还是能看到宿主机的文件系统,但是在容器进程的mount namespace进行mount的时候,宿主机是无感知的,这样对于同一个目录,如果在容器中mount后,看到的最新mount的内容,宿主机看到的还是原来的内容,从而实现了文件系统的隔离。

网络隔离,主机名和域名的隔离,用户和组的隔离,进程间通信的隔离分别可以利用linuxnamespaceNetworkUTSUserPID进行隔离,这里就不再一一叙述了。

这样,我们就为docker容器提供了一个独立空间,让容器进程感觉自己独占了一个操作系统,但是作为一个容器,现在宿主机和容器进程还共享cpu,内存等硬件资源,相互之间会影响,所有仅有上面的隔离还是不够的。这样我们的第是三个问题来了,docker容器和宿主机是怎么样进行资源限制的呢?

docker容器和宿主机之间的资源限制,可以通过linuxcgroup机制来解决这个问题。linux Cgroups的全称是linux control group。它主要的作用是限制一个进程组能够使用的资源上限,包括cpu、内存、磁盘和网络带宽等等。另外cgroup还能够对进程进行优先级设置、审计等操作。下面我们来看看怎么使用cgroup对进程的资源进行限制。

linux cgroups的设计是比较简单易用的,可以简单理解为一个子系统目录加一个一组资源限制文件的组合,我们简单看看cgroup的使用方式。

首先,LinuxCGroup这个实现成了一个file system,你可以mount。如果是系统是Ubuntu 14.04,你输入以下命令你就可以看到cgroup已为你mount好了。

mount -t cgroup

cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset)

cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)

cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,relatime,cpuacct)

cgroup on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory)

cgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)

cgroup on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)

cgroup on /sys/fs/cgroup/blkio type cgroup (rw,relatime,blkio)

cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,net_prio)

cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,net_cls)

cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,relatime,perf_event)

cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,relatime,hugetlb)

我们可以看到,在/sys/fs下有一个cgroup的目录,这个目录下还有很多子目录,比如: cpucpusetmemoryblkio……这些,这些都是cgroup的子系统。分别用于干不同的事的。如果你没有看到上述的目录,也可以自己mount。然后,我们这前不是在/sys/fs/cgroup/cpu下创建了一个palfishgroup。我们先设置一下这个groupcpu利用的限制:

cat /sys/fs/cgroup/cpu/palfish/cpu.cfs_quota_us

-1

(默认为-1,表示没有限制)

echo 20000 > /sys/fs/cgroup/cpu/palfish/cpu.cfs_quota_us

(表示一个cpu periodgroup最多可以运行20000us,默认cpu period100ms,即表示限制为20%

如果我们需要限制某一个进程,那么将这个进程的pid加到这个cgroup中,下面以pid3529为例:

echo 3529 >> /sys/fs/cgroup/cpu/palfish/tasks

这样,就会3529进程消耗的cpu最大为20%了。

按上面的cgroup的使用方式,对于docker容器来说,只需要为每一个子系统下面为每一个容器创建一个控制组,然后在容器进程启动之后,把进程的pid写入对于控制组的tasks文件中就可以了。这样我们就实现了docker容器和宿主机之间的资源限制。

通过上面的分析,docker容器只是宿主机上的一个普通进程,然后通过namespace对该进程进行隔离,通过cgroup对硬件资源进行限制而实现的一个容器,和虚拟机以一个独立的操作系统进行隔离的方式是完全不一样的技术。这样docker容器的实现很轻量,一个容器和一个进程消耗的资源差不多,使得docker容器具有秒级启动并且单个宿主机可以启动上千个docker容器的优点。但是docker容器由于只是宿主机上的一个进程,那么docker容器必须依赖宿主机的操作系统内核,所有在windows上运行linuxdocker容器是不行,并且一个依赖高版本linux内核的的docker容器在低版本宿主机是不能运行的,而这些问题对于虚拟机来说是不存在的。另外需要利用namespacedocker容器进行了pid,网络等方面的隔离,但是有一些资源没有办法隔离的,比如时间等等,所以在个docker容器修改了时间,那么宿主机上所有的docker容器都会感知。并且由于系统调用是直接操作内核的,所以docker容器操作系统调用这个是不能完全隔离的。

浅谈分布式系统ID生成算法

ID是什么?简单来说ID是一个唯一标识,类似我们的身份证号码,不只是在分布式系统中,在各类普通应用场景中,ID都是必须的。ID重要性就不再特殊强调了。
从ID特性角度
  1. 唯一性,ID应该是唯一的,这里的唯一有一个冲突概率问题,只要低于应用场景的冲突概率就可以认为其满足唯一性。
  2. 单调性,除了唯一性之外,一般的ID都有单调性的需求,例如DB的主键,极端的分布式系统中的时间判定,则必须要给出严格的单调特性才满足需求。当然对于某些特殊出于安全考虑场景,可能要求ID是全随机的,不能有规律可寻找,比如优惠券的兑换码
从ID生成方式
  1. 分布式,不需要集中的服务器,ID生成可以在各个节点独立完成
  2. 集中式,基于集中的服务器来实现,需要ID的人统一去集中服务器获取
言归正传,闲话少说,下面列举集中ID生成方式,分别从冲突概率、时间因素、安全性、单调性、性能几个角度说明。

一、UUID

通用唯一识别码(英语:Universally Unique Identifier,UUID),需要16字节存储。UUID根据标准方法生成,不依赖中央机构的注册和分配,UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符。目前有5个版本的生成方式,具体细节可以参考 https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81
linux有个命令工具uuidgen可以直接生成
92a78bc3-b608-4445-894b-f4895de78bd8
  1. 冲突概率,非常低,可以忽略
  2. 时间因素,版本1、2实现包含了时间参数,可以从ID中获取到时间,依赖机器本地时钟
  3. 安全性,版本1、2实现包含了MAC地址,有地址泄漏的风险
  4. 单调性,版本3、4、5实现完全基于随机算法,ID生成没有任何的趋势。携带时间参数的实现,依赖机器本地时钟的精度
  5. 性能,非中心化的生成方式,性能非常高,不适合做DB的主键,如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能

二、Git SHA1 ID

这个可能有点牵强,与其说Git是个版本管理系统,不如说Git是个文件系统,Git系统中没种类型的对象都有一个ID,Git系统中ID有两方面作用,一个是标识另一个更重要的是HASH校验。世界上任何一个被放入Git系统中的文件都会有一个自己唯一的标识,标识自己、标识在整个结构中的目录关系,标识自己在版本历史中的位置。为什么说Git是分布式版本管理,这个是一个很大的原因,不过你是版本库a,还是版本库b,你可以把他们认为是独立的概念,你同样可以把他们定义为一个整体的概念,想象一下全世界所有的Git仓库,共有的、私有的,都是一体的,都是可以共存的,原因不是因为他们物理隔离性,是为Git的id系统,这里不详细展开,有兴趣的同学可以先从Git的各个对象概念入手。
  1. 冲突概率,这里引用Pro Git中的一段话来说明:“举例说一下怎样才能产生一次 SHA-1 冲突。 如果地球上 65 亿个人类都在编 程,每人每秒都在产生等价于整个 Linux 内核历史(360 万个 Git 对象)的代 码,并将之提交到一个巨大的 Git 仓库里面,这样持续两年的时间才会产生足够 的对象,使其拥有 50% 的概率产生一次 SHA-1 对象冲突。 这要比你编程团队的 成员同一个晚上在互不相干的意外中被狼袭击并杀死的机率还要小。”这个就不用我在多说什么了吧?
  2. 时间因素,SHA1不携带任何时间因素概念,对生成不依赖机器时钟
  3. 安全性,安全性非常高,无规律课寻找
  4. 单调性,纯随机没有任何单调性
  5. 性能,非中心化的方式,但是因为生成需要进行哈希计算,所以速度比较慢,Git的应用场景决定的。存储的长度是固定的,需要20个字节
题外话
虽然冲突不太可能发生,但已经被google 强大的分布式计算资源实现了,虽然是取巧类似比特币挖矿,但是有说明一定的问题,google应该也去挖比特币去。
对于这个问题Linus Torvalds给予了回复 https://plus.google.com/+LinusTorvalds/posts/7tp2gYWQugL,这个回复写的非常好,千万大家点开来看看。
大家还可以看两个讨论
stackoverflow:
知呼上:
总之不同怕,如Linus所说, The sky isn’t falling.

三、MongoDB ObjectId

MongoDB采用了一个称之为ObjectId的类型来做主键。ObjectId需要12字节进行存储。按照字节顺序:
4字节:UNIX时间戳
3字节:表示运行MongoDB的机器
2字节:表示生成此_id的进程
3字节:由一个随机数开始的计数器生成的值
  1. 冲突概率,基本不会发生冲突
  2. 时间因素,ID里面携带了时间戳,可以通过ID方便获取到数据产生的时间,ID生成依赖了机器本地的时钟。
  3. 安全性,
  4. 单调性,满足一般的单调性,在秒内id生成的序列是随时间单调递增的,也就是如果想依赖单调特性,至少需要有1000ms的延时确认。另外因为id生成依赖本地时钟,目前的服务器一般都部署了NTP,NTP在分布式系统需要给150ms的误差。综上可以看出,ObjectId在需要严格单调性的ID场景是不满足的需求的。
  5. 性能,性能非常好,不依赖中心获取,在本地就可以按照算法完成ID生成任务。

四、基于DB键值自增生成

中心化的生成方式,基于DB生成的id有个很大优点就是简洁,存储长度可控,可读性比较强。
基于mongo 生成
function getNextSequenceValue(sequenceName){
var sequenceDocument = db.counters.findAndModify(
{
query:{_id: sequenceName },
update: {$inc:{sequence_value:1}},
“new”:true
});
return sequenceDocument.sequence_value;}
基于mysql
以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。
begin;
REPLACE INTO Tickets64 (stub) VALUES (‘a’);
SELECT LAST_INSERT_ID();
commit;
注意!如果,基于TiDB生成不能保证自增键的连续性,
  1. 冲突概率,不会发生冲突
  2. 时间因素,不携带任何时间参数,生成也不依赖本地时钟
  3. 安全性,安全性比较差,尤其不能将此类生成的Id用到类似于优惠券Id等通过id标识权益的场景,否则很容易根据其简单的生成规律倒推出来
  4. 单调性,如果基于单节点生成,满足严格的单调性,但是如果采用多节点不同步长方式则不能保证严格单调性,只能基本保证其单调有序
  5. 性能,性能非常差,因为每次获取都要从DB获取,整个ID生成系统依赖DB本身的性能,同时DB的特性决定了ID生成的可用性程度
如何优化?单纯的依赖db肯定是不行的,针对DB生成的性能问题,目前有很多优化的实现,网上有一些关于基于多组mysql 设定不同的步长的实现方式,扩展性和实现都非常复杂,有兴趣的可以去了解。比较好的方式是DB+内存的实现方式,有个美团的Leaf介绍,有兴趣的可以具体了解https://tech.meituan.com/MT_Leaf.html

五、twitter-snowflake算法

存储长度为8个字节,一个int64既可以存储。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id
  1. 冲突概率,理论上只要在算法中41bit的时间区间没有出现溢出,不会发生冲突。但是因为依赖了机器时钟,时钟的波动很有可能引起ID冲突。
  2. 时间因素,携带时间参数,可以方便的从id获取到id产生时间,依赖本地时钟。
  3. 安全性,安全性一般,可以很方便的通过算法实现构造出可用的ID探测
  4. 单调性,毫秒级别的单调性,严格单调性依赖机器时钟,类似上面提到的MongoDB ObjectId,需要忍受NTP 150ms精度误差。
  5. 性能,性能非常好,去中心化的实现方式,支持每毫秒产生4095个
Snowflake生成算法虽然是去中心化的,但是如果要实现Worker Id不依赖手动配置的方式,一般都需要etcd、Zookeeper等工具的配合才能完成。
伴鱼很多系统使用Snowflake生成算法,Worker id使用的是微服务的副本号来填充的。对于snowflake生成的id对大部分的业务场景是能够满足需求的。
snowflake有很多扩展Boundary flake,simple flake有兴趣的可以自己去google。
snowflake算法还有个问题,这里在伴鱼遇到的比较多,对于弱类型的语言,比如常见的JavaScript,int64的长度产生精度丢失问题!这个问题其实挺严重的,解决办法,可以反给前端转化成字符串方式,另一种方式,我们修改了snowflake为slowflake,基本思想是降低自增id区间,让ID能够在浮点数的有效数字位52位完成表示,但是这么做的后果是1ms只能产生2个ID,对于非常高并发的场景是不适用的。

六、TrueTime

严格来说TrueTime ID生成器范畴,TrueTime是用来解决分布式系统的时间问题,其应用场景是为了解决分布式系统的时间问题,非简单的业务场景,但本质其实还是ID。注意TrueTime不适用于业务ID生成。
  1. TureTime 需要依赖 atomic clock 和 GPS,这属于硬件方案,而 Google 并没有论文说明如果构造 TrueTime,对于其他用户的实际并没有太多参考意义。
  2. TureTime 也会有误差范围,虽然非常的小,在毫秒级别以下,所以我们需要等待一个最大的误差时间,才能确保事务的相关顺序。
Spanner通过使用GPS + Atomic Clock来对集群的机器进行校时,精度误差范围能控制在ms级别,通过提供一套TrueTime API给外面使用。TrueTime API很简单,只有三个函数:
Method
Return
TT.now()
TTinterval: [earliest, latest]
TT.after(t)
true if t has definitely passed
TT.before(t)
true if t has definitely not arrived
首先now得到当前的一个时间区间,spanner不能得到精确的一个时间点,只能得到一段区间,但这个区间误差范围很小,也就是ms级别,我们用ε来表示,也就是[t – ε, t + ε]这个范围,假设事件a发生绝对时间为tt.a,那么我们只能知道tt.a.earliest <= tt.a <= tt.a.latest, 所以对于另一个事件b,只要tt.b.earliest > tt.a.latest,我们就能确定b一定是在a之后发生的,也就是说,我们需要等待大概2ε的事件才能去提交b,这个就是spanner里面说的commit wait time。
可以看到,虽然spanner引入了TrueTime可以得到全球范围的时序一致性,但相关事务在提交的时候会有一个wait时间,只是这个时间很短,而且spanner后续都准备将其优化到 ε < 1ms,也就是对于关联事务,仅仅在上一个事务commit之后等待2ms之后就能执行,性能还是很强悍的。
但spanner有一个最大的问题,TrueTime是基于硬件的,而现在对于很多企业来说,是没有办法搞定这套部署的。所以如果Google能将TrueTime的硬件设计开源,那我觉得更加造福社区了。
  1. 单调性,2ms误差范围,严格单调递增,对比之前NTP的150ms性能差了2个数量级
  2. 性能,完全去中心化的生成,如果你觉得2ms太慢,那你要考虑其场景是为洲际的分布式系统提供服务,2ms在整个时延中是微乎其微的的。不能用于普通的业务场景

七、HLC(Hybrid Logic Clock)

HLC的应用场景和TrueTime是一样的,因为TrueTime这种硬件方案很多人搞不定,另外google也没有公开其实现的细节,所以出现了HLC,Cockroachdb使用了HLC,
HLC是基于NTP的,但它只会读取当前系统时间,而不会去修改,同时HLC又能保证在NTP出现同步问题的时候仍能够很好的进行容错处理。对于一个HLC的时间t来时,它总是大于等于当前的系统时间,并且与其在一个很小的误差范围里面,也就是 |l – pt| < ε,HLC由两部分组成,physical clock + logic clock.

八、TSO(Timestamp Oracle)

TSO 是一个全局的时间戳,在Google Percolator系统这,他们就提到使用了TSO的服务来提供统一的授时服务。使用TSO的好处在于因为只有一个中心授时,所以我们一定能确定所有时间的时间。
所以,如果我们没法实现TrueTime,同时又觉得HLC太复杂,但又想获取全局时间,TSO没准是一个很好的选择,因为它足够简单高效。
  1. 冲突概率,绝对不会产生冲突
  2. 时间因素,携带时间参数,可以方便的从id获取到id产生时间,依赖PD的本地时钟,时钟错误不会造成id错误,但是可能会造成ID生成服务一段时间的不可用。
  3. 安全性,安全性一般,可以很方便的通过算法实现构造出可用的ID探测
  4. 单调性,严格单调递增
  5. 性能,算法本身支持每毫秒产生262144,但是TSO本质上是中心化的实现方式,需要考虑网络延时,所以一般不适合洲际部署,另外TSO是一个单点,所以不得不考虑容错
TSO的模式决定了他不但可以用于分布式事务,同样可以将ID用于普通业务,实际我们可以在自己的系统中部署一套,来作为ID生成的中心!
TSO另一个典型应用TiDB,TiDB的PD模块实现了TSO的功能,它是 TiDB 实现分布式事务的基石。
对于 PD 来说,我们首先要保证它能快速大量的为事务分配 TSO,同时也需要保证分配的 TSO 一定是单调递增的,不可能出现回退的情况。这里简单介绍下PD的实现,细节可以参考https://zhuanlan.zhihu.com/p/24809131
TSO 是一个 int64 的整形,它由 physical time + logical time 两个部分组成。Physical time 是当前 unix time 的毫秒时间,而 logical time 则是一个最大 1 << 18 的计数器。也就是说 1ms,PD 最多可以分配 262144 个 TSO,这个能满足绝大多数情况了。
对于 TSO 的保存于分配,PD 会做如下处理:
  1. 当 PD 成为 leader 之后,会从 etcd 上面获取上一次保存的时间,如果发现本地的时间比这个大,则会继续等待直到当前的时间大于这个值:
  2. 当 PD 能分配 TSO 之后,首先会向 etcd 申请一个最大的时间,譬如,假设当前时间是 t1,每次最多能申请 3s 的时间窗口,PD 会向 etcd 保存 t1 + 3s 的时间值,然后 PD 就能在内存里面直接使用这一段时间窗口.当当前的时间 t2 大于 t1 + 3s 之后,PD 就会在向 etcd 继续更新为 t2 + 3s,这么处理的好处在于,即使 PD 当掉,新启动的 PD 也会从上一次保存的最大的时间之后开始分配 TSO,也就是 1 处理的情况。
因为PD在内存里面保存了一个可分配的时间窗口,所以外面请求 TSO 的时候,PD 能直接在内存里面计算 TSO 并返回。
因为是在内存里面计算的,所以性能很高,我们自己内部测试每秒能分配百万级别的 TSO。