All posts by dotte

zookeeper + dubbo+ spring boot

构建 Zookeeper + Dubbo + Spring Boot 的分布式调用项目(一)

构建 Zookeeper + Dubbo + Spring Boot 的分布式调用项目(二)

zookeeper + dubbo + spring boot

基于Spring+SpringMVC+Mybatis分布式敏捷开发系统架构 (配置)

spring boot集成dubbo

大型分布式网站术语分析

1. I/O优化

  1. 增加缓存,减少磁盘的访问次数。
  2. 优化磁盘的管理系统,设计最优的磁盘方式策略,以及磁盘的寻址策略,这是在底层操作系统层面考虑的。
  3. 设计合理的磁盘存储数据块,以及访问这些数据库的策略,这是在应用层面考虑的。例如,我们可以给存放的数据设计索引,通过寻址索引来加快和减少磁盘的访问量,还可以采用异步和非阻塞的方式加快磁盘的访问速度。
  4. 应用合理的RAID策略提升磁盘I/O。

2. Web前端调优

  1. 减少网络交互的次数(多次请求合并)
  2. 减少网络传输数据量的大小(压缩)
  3. 尽量减少编码(尽量提前将字符转化为字节,或者减少从字符到字节的转化过程。)
  4. 使用浏览器缓存
  5. 减少Cookie传输
  6. 合理布局页面
  7. 使用页面压缩
  8. 延迟加载页面
  9. CSS在最上面,JS在最下面
  10. CDN
  11. 反向代理
  12. 页面静态化
  13. 异地部署

3.服务降级(自动优雅降级)

拒绝服务和关闭服务

4.幂等性设计

有些服务天然具有幂等性,比如讲用户性别设置为男性,不管设置多少次,结果都一样。但是对转账交易等操作,问题就会比较复杂,需要通过交易编号等信息进行服务调用有效性校验,只有有效的操作才能继续执行。

(注:幂等性是系统的接口对外一种承诺(而不是实现), 承诺只要调用接口成功, 外部多次调用对系统的影响是一致的. 声明为幂等的接口会认为外部调用失败是常态, 并且失败之后必然会有重试.)

5.失效转移

若数据服务器集群中任何一台服务器宕机,那么应用程序针对这台服务器的所有读写操作都需要重新路由到其他服务器,保证数据访问不会失败,这个过程叫失效转移。
失效转移包括:失效确认(心跳检测和应用程序访问失败报告)、访问转移、数据恢复。
失效转移保证当一个数据副本不可访问时,可以快速切换访问数据的其他副本,保证系统可用。

6.性能优化

根据网站分层架构,性能优化可分为:web前端性能优化、应用服务器性能优化、存储服务器性能优化。

  1. web前端性能优化
    • 浏览器访问优化:减少http请求;使用浏览器缓存;启用压缩;css放在页面最上面、javaScript放在页面最下面;减少Cookie传输
    • CDN加速
    • 反向代理
  2. 应用服务器性能优化
    • 分布式缓存(Redis等)
    • 异步操作(消息队列)
    • 使用集群(负载均衡)
    • 代码优化
  3. 存储性能优化
    • 机械硬盘vs固态硬盘
    • B+树 vs LSM树
    • RAID vs HDFS

7. 代码优化

  • 多线程(Q:怎么确保线程安全?无锁机制有哪些?)
  • 资源复用(单例模式,连接池,线程池)
  • 数据结构
  • 垃圾回收

8. 负载均衡

  • HTTP重定向负载均衡
    当用户发来请求的时候,Web服务器通过修改HTTP响应头中的Location标记来返回一个新的url,然后浏览器再继续请求这个新url,实际上就是页面重定向。通过重定向,来达到“负载均衡”的目标。例如,我们在下载PHP源码包的时候,点击下载链接时,为了解决不同国家和地域下载速度的问题,它会返回一个离我们近的下载地址。重定向的HTTP返回码是302。
    优点:比较简单。
    缺点:浏览器需要两次请求服务器才能完成一次访问,性能较差。重定向服务自身的处理能力有可能成为瓶颈,整个集群的伸缩性国模有限;使用HTTP302响应码重定向,有可能使搜索引擎判断为SEO作弊,降低搜索排名。
  • DNS域名解析负载均衡
    DNS(Domain Name System)负责域名解析的服务,域名url实际上是服务器的别名,实际映射是一个IP地址,解析过程,就是DNS完成域名到IP的映射。而一个域名是可以配置成对应多个IP的。因此,DNS也就可以作为负载均衡服务。
    事实上,大型网站总是部分使用DNS域名解析,利用域名解析作为第一级负载均衡手段,即域名解析得到的一组服务器并不是实际提供Web服务的物理服务器,而是同样提供负载均衡服务的内部服务器,这组内部负载均衡服务器再进行负载均衡,将请求分发到真是的Web服务器上。
    优点:将负载均衡的工作转交给DNS,省掉了网站管理维护负载均衡服务器的麻烦,同时许多DNS还支持基于地理位置的域名解析,即会将域名解析成举例用户地理最近的一个服务器地址,这样可以加快用户访问速度,改善性能。
    缺点:不能自由定义规则,而且变更被映射的IP或者机器故障时很麻烦,还存在DNS生效延迟的问题。而且DNS负载均衡的控制权在域名服务商那里,网站无法对其做更多改善和更强大的管理。
  • 反向代理负载均衡
    反向代理服务可以缓存资源以改善网站性能。实际上,在部署位置上,反向代理服务器处于Web服务器前面(这样才可能缓存Web相应,加速访问),这个位置也正好是负载均衡服务器的位置,所以大多数反向代理服务器同时提供负载均衡的功能,管理一组Web服务器,将请求根据负载均衡算法转发到不同的Web服务器上。Web服务器处理完成的响应也需要通过反向代理服务器返回给用户。由于web服务器不直接对外提供访问,因此Web服务器不需要使用外部ip地址,而反向代理服务器则需要配置双网卡和内部外部两套IP地址。
    优点:和反向代理服务器功能集成在一起,部署简单。
    缺点:反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。
  • LVS-NAT:修改IP地址
  • LVS-TUN: 一个IP报文封装在另一个IP报文的技术。
  • LVS-DR:将数据帧的MAC地址改为选出服务器的MAC地址,再将修改后的数据帧在与服务器组的局域网上发送。

9.缓存

缓存就是将数据存放在距离计算最近的位置以加快处理速度。缓存是改善软件性能的第一手段,现在CPU越来越快的一个重要因素就是使用了更多的缓存,在复杂的软件设计中,缓存几乎无处不在。大型网站架构设计在很多方面都使用了缓存设计。

  • CDN: 及内容分发网络,部署在距离终端用户最近的网络服务商,用户的网络请求总是先到达他的网络服务商哪里,在这里缓存网站的一些静态资源(较少变化的数据),可以就近以最快速度返回给用户,如视频网站和门户网站会将用户访问量大的热点内容缓存在CDN中。
  • 反向代理:反向代理属于网站前端架构的一部分,部署在网站的前端,当用户请求到达网站的数据中心时,最先访问到的就是反向代理服务器,这里缓存网站的静态资源,无需将请求继续转发给应用服务器就能返回给用户。
  • 本地缓存:在应用服务器本地缓存着热点数据,应用程序可以在本机内存中直接访问数据,而无需访问数据库。
  • 分布式缓存:大型网站的数据量非常庞大,即使只缓存一小部分,需要的内存空间也不是单机能承受的,所以除了本地缓存,还需要分布式缓存,将数据缓存在一个专门的分布式缓存集群中,应用程序通过网络通信访问缓存数据。

使用缓存有两个前提条件,一是数据访问热点不均衡,某些数据会被更频繁的访问,这些数据应该放在缓存中;二是数据在某个时间段内有效,不会很快过期,否则缓存的数据就会因已经失效而产生脏读,影响结果的正确性。网站应用中,缓存处理可以加快数据访问速度,还可以减轻后端应用和数据存储的负载压力,这一点对网站数据库架构至关重要,网站数据库几乎都是按照有缓存的前提进行负载能力设计的。

10. 负载均衡算法

轮询 Round Robin
加强轮询 Weight Round Robin
随机 Random
加强随机 Weight Random
最少连接 Least Connections
加强最少连接
源地址散列 Hash
其他算法

  • 最快算法(Fastest):传递连接给那些响应最快的服务器。当其中某个服务器发生第二到第7 层的故障,BIG-IP 就把其从服务器队列中拿出,不参加下一次的用户请求的分配,直到其恢复正常。
  • 观察算法(Observed):连接数目和响应时间以这两项的最佳平衡为依据为新的请求选择服务器。当其中某个服务器发生第二到第7 层的故障,BIG-IP就把其从服务器队列中拿出,不参加下一次的用户请求的分配,直到其恢复正常。
  • 预测算法(Predictive):BIG-IP利用收集到的服务器当前的性能指标,进行预测分析,选择一台服务器在下一个时间片内,其性能将达到最佳的服务器相应用户的请求。(被BIG-IP 进行检测)
  • 动态性能分配算法(Dynamic Ratio-APM):BIG-IP 收集到的应用程序和应用服务器的各项性能参数,动态调整流量分配。
  • 动态服务器补充算法(Dynamic Server Act.):当主服务器群中因故障导致数量减少时,动态地将备份服务器补充至主服务器群。
  • 服务质量算法(QoS):按不同的优先级对数据流进行分配。
  • 服务类型算法(ToS): 按不同的服务类型(在Type of Field中标识)负载均衡对数据流进行分配。
  • 规则模式算法:针对不同的数据流设置导向规则,用户可自行

11. 扩展性和伸缩性的区别

扩展性:指对现有系统影响最小的情况下,系统功能可持续扩展或替身的能力。表现在系统基础设施稳定不需要经常变更,应用之间较少依赖和耦合,对需求变更可以敏捷响应。它是系统架构设计层面的开闭原则(对扩展开放,对修改关闭),架构设计考虑未来功能扩展,当系统增加新功能时,不需要对现有系统的结构和代码进行修改。

衡量网站架构扩展性好坏的主要标准就是在网站增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或者很少改动既有业务功能就可以上线新产品。不同产品之间是否很少耦合,一个产品改动对其他产品无影响,其他产品和功能不需要受牵连进行改动。

伸缩性:所谓网站的伸缩性指是不需要改变网站的软硬件设计,仅仅通过改变部署的服务器数量就可以扩大或者缩小网站的服务处理能力。

指系统能够增加(减少)自身资源规模的方式增强(减少)自己计算处理事务的能力。如果这种增减是成比例的,就被称作线性伸缩性。在网站架构中,通常指利用集群的方式增加服务器数量、提高系统的整体事务吞吐能力。

衡量架构伸缩性的主要标准就是可以用多台服务器构建集群,是否容易向集群中添加新的服务器。加入新的服务器后是否可以提供和原来服务无差别的服务、集群中的可容纳的总的服务器数量是否有限制。

12.分布式缓存的一致性hash

具体算法过程:先构造一个长度为2^32的整数环(这个环被称作一致性Hash环)根据节点名称的Hash值(其分布范围为[0,2^32 – 1])将缓存服务器阶段设置在这个Hash环上。然后根据需要缓存的数据的Key值计算得到Hash值(其分布范围也同样为[0,2^32 – 1]),然后在Hash环上顺时针查找举例这个KEY的hash值最近的缓存服务器节点,完成KEY到服务器的Hash映射查找。

优化策略:将每台物理服务器虚拟为一组虚拟缓存服务器,将虚拟服务器的Hash值放置在Hash环上,key在换上先找到虚拟服务器节点,再得到物理服务器的信息。

一台物理服务器设置多少个虚拟服务器节点合适呢?经验值:150。

13. 网络安全

  1. XSS攻击
    跨站点脚本攻击(Cross Site Script),指黑客通过篡改网页,注入恶意的HTML脚本,在用户浏览网页时,控制用户浏览器进行恶意操作的一种攻击方式。
    防范手段:消毒(XSS攻击者一般都是通过在请求中嵌入恶意脚本大道攻击的目的,这些脚本是一般用户输入中不使用的,如果进行过滤和消毒处理,即对某些html危险字符转移,如“>”转译为“& gt;”);HttpOnly(防止XSS攻击者窃取Cookie).
  2. 注入攻击:SQL注入和OS注入
    SQL防范:预编译语句PreparedStatement; ORM;避免密码明文存放;处理好相应的异常。
  3. CSRF(Cross Site Request Forgery,跨站点请求伪造)。听起来与XSS有点相似,事实上两者区别很大,XSS利用的是站内的信任用户,而CSRF则是通过伪装来自受信任用户的请求来利用受信任的网站。
    防范:httpOnly;增加token;通过Referer识别。
  4. 文件上传漏洞
  5. DDos攻击

14. 加密技术

  1. 摘要加密:MD5, SHA
  2. 对称加密:DES算法,RC算法, AES
  3. 非对称加密:RSA
    非对称加密技术通常用在信息安全传输,数字签名等场合。
    HTTPS传输中浏览器使用的数字证书实质上是经过权威机构认证的非对称加密的公钥。

15. 流控(流量控制)

  1. 流量丢弃
  2. 通过单机内存队列来进行有限的等待,直接丢弃用户请求的处理方式显得简单而粗暴,并且如果是I/O密集型应用(包括网络I/O和磁盘I/O),瓶颈一般不再CPU和内存。因此,适当的等待,既能够替身用户体验,又能够提高资源利用率。
  3. 通过分布式消息队列来将用户的请求异步化。

参考资料
1. LVS:三种负载均衡方式比较+另三种负载均衡方式
2. 《大型网站技术架构——核心原理与技术分析》李智慧 著。
3. 亿级Web系统搭建:单机到分布式集群
4. 《大型分布式网站架构设计与实现》陈康贤 著。

from:http://www.importnew.com/24198.html

Gradle

Gradle 用户指南官方文档中文版

Gradle does not find tools.jar

How/when to generate Gradle wrapper files?

 在 Eclipse 下利用 gradle 构建系统

使用Gradle管理老J2EE项目(一)
Gradle脚本基础全攻略
Gradle 10分钟上手指南

用gradle来管理java项目的示例

gradle 命令及技巧 (gradle-tips)
Gradle学习系列之一——Gradle快速入门

Gradle入门系列(5):创建多项目构建

Vim入门教程

Where to buy 🚀 aged domains and backlinks 🔥 from Best-SEO-Domains | 0083-0608

尽管网上有成打的Vim在线教程,但是要么艰深晦涩,要么太过肤浅。本教程的目标让每个阶段都有斩获,从理解它的哲学(将和你终身相伴)到超越现在编辑技巧,成为其中的牛人。

简单来说,本教程的学习方式将使你终身受益。

为什么选择vim

我相信大多数人可能由于以下三点原因而使用vim:

1、vim无所不在。学习vim你无需担心到了其他平台需要学习新编辑器。

2、可扩展性。你可以只用它来编辑配置文件,也可以将它当做你的开发平台。

3、功能强大。它的工作方式与自然语言类似。即使一开始你对vim一无所知,经过一段时间的使用学习,很快也会成为牛人。

一句话,我相信一旦你开始学习,你会觉得就像掌握母语还有基础数学这些基本技能一样,也能自然而然地掌握vim。好了,在了解这些知识之后,让我们正式开始vim技术的学习吧。

Approach

Kana大神说过,对vim的掌握有五个层次:

  • 层次0: 对vim一无所知
  • 层次1: 了解vim的基本使用
  • 层次2: 知道可视模式
  • 层次3: 知道多种移动动作
  • 层次4: 不再需要可视模式

对此我并不了解,不过我认为这种观点值得我们注意。毕竟,kana是vim大神。在本教程中,我将通过下面四个主要模块来向你展示vim。

  1. 基础介绍:这部分基础介绍能快速建立起你对vim的正确认知。
  2. 具体操作: 这块是干货。做好准备来享用吧。
  3. 进阶: 这部分我会教你怎样成为vim牛人。
  4. 常见问题: 这里会讲述一些常见问题解决技巧。

换句话说,如果你已经对vim有所了解的话,建议你从具体操作这部分开始。如果你熟练掌握这部分的话,就直接进入进阶开始学功夫。假如你看这篇教程是为了某个具体问题,那么请进入常见问题章节。

所以呢,建立vim世界观,vim基本操作,高手进阶以及常见问题——这几部分任君挑选。

配置

我曾说过,不希望这是一本超级vim配置手册,网上已经有很多这类教程了。本教程的目的在于强化你对vim的理解,帮助你熟练掌握这个工具。不过我们也会稍微介绍一些基本的配置。

首先,我建议你安装(几乎)自我管理的vim。我就曾试过Janus(vim的发行版),但是我无法确定它究竟在做什么,这让我感到很挫败。我偏好的配置就像vim自身一样,简单而优雅。

为了达到这个目的,我直接将用户主目录下的.vim目录和.vimrc作为配置文件。

A few key ~/.vimrc changes

./vimrc修改常用键

首先呢,我觉得使用键来退出插入模式实在是太老土了。Vim关注的是效率,如果不是特殊情况,手指尽量不要离开键盘中间排。而这种ESC操作实在是没效率,所以不要用这个。

inoremap jk

【注意: 有些人喜欢将映射为jj,但是我觉得映射为jk可能更自然一点】

修改leader键

反斜杠也同样存在这种问题,所以就像其它指导文档推荐的那样,我也喜欢将引导键重映射为逗号(,)。

let mapleader = “,”

通过这样的配置,可以简化我们接下来的命令执行,比如执行映射的缩写命令时,只需右手中指往下一格就可以轻松输入逗号,而不需要向上再向右去远远地按反斜杠键。

重映射CAPSLOCK

大写键不在vim的配置文件中,不过这个键的位置比较好,而它的默认功能我们又常常用不上。对我来说,CAPSLOCK键没啥用处,所以我将它映射为Ctrl键(从操作系统层面来设置)。这样如果我要执行Ctrl-XX操作时只需将左小指左移就可以了。

此外,推荐其他几项基础设置,这样使用更加方便。

filetype plugin indent on

syntax on

set encoding=utf-8

要知道,优化vimrc文件是一个非常值得花时间去研究的问题,这里说的几个建议仅仅是皮毛。推荐查看我的设置或者阅读这几篇引用文章

使用Pathogen管理插件

【请注意:如果你不熟悉或者并不喜欢插件,可以跳过这一节。等你下次想通了再来看看。】

远离Janus

对我来说,Janus最能吸引我的地方在于它的插件管理,不过我可以通过Pathogen来实现。基本上,通过以下几步就可以了:

  1. 安装Pathogen
  2. 将你需要的插件git clone 克隆到~/.vim/bundle
  3. 在配置文件~/.vimrc中添加execute pathogen#infect()

利用github来实现备份和可移植性

为了备份vim设置,我将整个~/.vim目录通过git库保存在这里。这样即便我新装了系统,也可以很方便的使用git clone https://github.com/danielmiessler/vim获取我的偏好设置。

也许你也会喜欢这样做。

简单一步clone之后,将~/.vimrc软连接到~/.vim/vimrc就可以了。

Vim即语言

Vim最成功的闪光点莫过于一旦你开始使用它,它就会让你欲罢不能。Vim就像语言一样,拥有名词、动词、副词等属性。

虽然我的这种说法从技术层面可能并不准确,但是却能帮助你更好地理解vim的工作机制。重申一次,本教程的目的不是为了取代其他教程或帮助手册——而是为了帮助你理解其他这些文档晦涩难懂的部分。

动词

所谓动词指的是我们执行的动作,这些动作可以施加在名词之上。可以看看下面几个常见的动作:

  • d: 删除
  • c: 修改
  • y: 拖拉(拷贝)
  • v: 可视化选择 (V for line vs. character)

修饰语

定语用在名词之前,表明以哪种方式来执行动作。几个例子:

  • i: 内部
  • a: 周围
  • NUM: 数字 (e.g.: 1, 2, 10)
  • t: 查找到指定字符,并跳转到这个字符的前面
  • f: 查找到指定字符,并跳转到字符所处的位置
  • /: 查找字符串 (literal or regex)

名词

在英语中,名词用来表示你所操作的对象。它们都是客体。在vim中也是这样。下面列出vim中的名词:

  • w: 单词
  • s: 句子
  • ): 句子 (另一种操作方式)
  • p: 段落
  • }: 段落 (另一种操作方式)
  • t: 标签 ( HTML/XML)
  • b: 块 (编程语言)

Nouns as motion

同样,你也可以将名词视为移动动作,这意味着你可以用名词表示在文本的跳转范围。我们可以在“移动”章节中看到例子说明。

使用vim语言来组成语句(命令)

好了,现在我们有句子的不同组成部分,该怎样将这几部分组成完整的句子呢?就像英语一样,凭感觉将动词、修饰语和名词组合起来。

对于下面这些符号,根据颜色的不同来区分句子的不同组成部分:

删除两个单词

d2w

修改所在的句子(删除当前句子并进入插入模式)

cis

拷贝当前所在的段落

yip

修改当前光标所在位置到下一个括号之间的文本内容

ct< 记住,这里的“目标”是尖括号,除此之外还可以是任意字符。从语法上来说,使用单个字母”t”来表示这个向前查找的“目标”,这样我就可以使用“dt.”表示删除当前位置到下一个句号之间的内容,”yt;”表示拷贝当前位置到下一个分号之间的内容。 看起来多漂亮!当编辑文本时采用这样的思维方式可以使得操作更加优雅,符合我们的直觉思维,而且就像其他语言一样,熟能生巧。

具体操作

现在,在了解这些基本操作后,让我们结合具体操作来更进一步学习。

处理文件

一些对文件的基本操作。

  • Vi file :使用vim打开要编辑的文件
  • :w :保存修改
  • :q! : 退出vim,后面加感叹号表示不保存修改强制退出
  • :wq: 保存修改并退出
  • :saveas ~/some/path/: 将文件保存到其他位置

【注意:相比较:wq 我更喜欢使用ZZ,因为我觉得不需要输入冒号我觉得会快一点。也可以使用:x】

  • ZZ: 同:wq

查找文本

对于任意一款编辑器,查找文本都是最基本需求。Vim拥有很强大的文本搜索功能,这一节将着重讨论vim的文本搜索。

搜索字符串

Vim中最基本也是最强大的搜索方式是”/”命令,在窗口最底端输入你需要查找的字符串并回车就可以了。

查找include字符串

/include

查找结果如下所示,所以匹配结果都被高亮显示:

搜索完成后,输入”n”依次向下查找,或者”N”向上查找。如果想要逆向查找字符串也可以使用”?”来代替”/”.

跳转到指定字符

Vim有一招特别酷,不管你在哪儿,都能查找特定字符并直接跳转过去。比如说,我正在编辑HTML文件,

向前查找并直接跳转到<字符 f< 向前查找并跳转到<的前一个字符 t< 你可以这么理解,第一种找出目标(f),直接定位到查找对象;第二种到达目标(t),向着目标对象前进,定位到目标的前面。 比较怪异的一点是,可以将它们作为命令中的名词。就比如刚才我在编辑这句时: 修改到下一个”<”之间的内容 ct< 对任意字符都可以这么做,比如说,句号,尖括号,圆括号,普通字母等等。这样你就可以查找文本并跳转,或者你知道具体位置并直接跳转。 【注意:当你搜索指定字符时,可以使用分号跳转到下一处——不管是t搜索还是f搜索。逗号可以用来反向跳转到上一处。】

搜索总结

  • /{string}: 搜索字符串
  • t : 跳转到某一字符前
  • F : 跳转到某一字符处
  • *:搜索当前光标位置单词的其他实例
  • n : 搜索完成后,跳转到下一个匹配实例
  • N :搜索完成后,跳转到上一条匹配实例
  • ; : 跳转到指定字符时,使用分号跳转到下一实例
  • , :跳转到指定字符时,使用逗号跳转到上一实例

游走于文本中

提高编辑效率至关重要的一点是:在文本内的自由移动跳转。对vim来说,充分利用我们上文所说的vim语言的基本准则,就可以既简单又优雅地做到。首先,介绍基础概念。

基本移动动作

我们从键盘中间排开始介绍。受过训练的打字员一般都习惯将右手指放在jkl;这几个键上,就让我们从这里开始使用vim。

  • j : 向下移动一行
  • k :向上移动一行
  • h :向左移动一个字符
  • l :向右移动一个字符

刚开始可能使用得不太习惯,不过只需几分钟的练习之后,就会习惯成自然,你甚至会想要在word文档里面也这样用(顺便说一下,word文档不支持)。

总结起来就是:右手食指和中指用来上移下移,右手食指和无名指用来左移右移。

行内移动

你可以在当前行内自由的移动。

  • 0: 移动到行首
  • $: 移动到行末
  • ^: 移动到行首非空字符
  • t”: 跳转到下一个引号前
  • f”: 跳转到下一个引号处

【注意: 逗号和分号会重复之前的t和f跳转】

按单词移动

除了按行和字符移动外,还可以按单词移动:

  • w: 先前移动一个单词
  • b: 向后移动一个单词
  • e: 移动到当前单词末尾

当使用大写字母时,vim会忽略掉单词间的分隔符,这样原本被分隔符隔开的多个单词会因此被视为一个大单词。

  • W: 向前移动一个大单词
  • B: 向后移动一个大单词

这种大写命令的优势在我们实际工作中会频繁地碰到。

按句子或段落移动

  • ):向前移动一条句子
  • }: 向前移动一个段落

在屏幕间移动

  • H:向上移动一屏
  • M:移动到屏幕中间
  • L:移动到屏幕底端
  • gg: 移动到文件顶端
  • G:移动到文件结尾
  • ^U: 向上移动半屏
  • ^D: 向下移动半屏
  • ^F: 下一页
  • ^B: 上一页

来回跳转

在普通模式下,可以在两点间来回跳转,这在实际操作中非常有用。

  • Ctrl-i: 跳转到之前所在的位置
  • Ctrl-o: 跳回你实际的位置

其他动作

  • :$line_numberH: 移动到指定行号
  • M: 移动到屏幕中间
  • L: 移动到屏幕底端
  • ^E: 向上滚动一行
  • ^Y: 向下滚动一行
  • ^U: 向上移动半屏
  • ^D: 向下移动半屏
  • ^F: 向下移动一页
  • ^B: 向上移动一页

现在将所以移动动作总结到一起:

移动命令总结

  • j: move down one line
  • k: move up one line
  • h: move left one character
  • l: move right one character
  • 0: move to the beginning of the line
  • $: move to the end of the line
  • w: move forward one word
  • b: move back one word
  • e: move to the end of your word
  • ): move forward one sentence
  • }: move forward one paragraph
  • :line_number: move to a given line number
  • H: move to the top of the screen
  • M: move to the middle of the screen
  • L: move to the bottom of the screen
  • ^E: scroll up one line
  • ^Y: scroll down one line
  • gg: go to the top of the file
  • G: go to the bottom of the file
  • ^U: move up half a page
  • ^D: move down half a page
  • ^F: move down a page
  • ^B: move up a page
  • Ctrl-i: jump to your previous navigation location
  • Ctrl-o: jump back to where you were

【注意:前面说过我将CAPSLOCK键映射到Ctrl,所有这些基于Ctrl的命令,我都可以用CASLOCK来代替】

修改文本

好了,了解一堆在文本中移动的命令后,我们可以开始学习对文本的修改了。需要记住这一点:这些移动命令会一直陪伴着我们——它们是vim语言的一部分(它们是上文提到的修饰语)。

Understanding modes

首先我们要弄清楚模式的概念。刚开始说到模式可能有点违反我们的直觉,不过一旦习惯之后就会使用得非常的自然。很多vim指南都从介绍模式开始,不过我发现如果直接从模式开始有点不好理解,从普通模式与插入模式之间的切换入手会好一点。

  • 从普通模式开始。对新手来说vim最让人郁闷一点莫过于打开文档却不能直接编辑。其实,你也可以输入一些个东西,不过如果你真这么做的话你的文档可能会变得一团糟。
  • 普通模式也叫命令行模式,因为通常在这个模式下输入vim命令。这些命令可以是移动、删除或者其他操作,之后进入插入模式。
  • 插入模式下可以直接对文件进行修改,有很多种方式可以从普通模式切换到插入模式。不过不用担心,虽然方式多样,只要多加练习就可以熟练使用了。
  • 可视化模式下可以选择文本。这个模式看起来与普通模式非常像,不过这个模式下移动操作会改变高亮选择的区域。你可以逐行选择也可以逐字符选择,不管哪种选择模式下,移动操作都会高亮选择更多的文本内容。
  • 选中之后,可视化模式的优势就体现出来了,我们可以对选中的全部文本做操作,大大提高了效率。
  • Ex模式下,光标处于屏幕最下方,冒号后可以输入命令。具体内容在后面做介绍。现在只需要知道在这种模式下可以运行一些命令行任务就可以了。

除了上面介绍的模式外vim还有一些其他的模式,不过本教程不做介绍因为它们不是首要任务。

记住vim语言

现在让我们一起回忆vim语言: 动词、修饰符、名词。假设我们从普通模式开始,现在想要切换到插入模式下修改文本。

动词可以实现这种切换,并且有好几种选择。我们可以修改(c),插入(i),或者追加(a),还有其他变种。

基础修改/插入命令

让我们从这几个可选命令开始。

  • i : 从当前光标位置之前插入
  • a : 从当前光标位置之后插入
  • I :从当前行开始位置插入
  • A :从当前行行末插入
  • o : 在当前行下新增一行,并从新行插入
  • O :在当前行上新增一行,并从新行插入
  • r : 替换当前光标所在字符
  • R :替换当前光标的字符,并一直向后替换(替换模式)
  • cm:修改文本,其中m表示移动的动作,比如,一个单词,一句或者一个段落。
  • C :修改当前所在行
  • ct?: 修改问号前的这条问句内容
  • s:替换当前字符
  • S: 替换整行

修改句子

cis

移动到行首并进入插入模式

I

在当前光标位置之后进入插入模式

a

从上面例子可知,有多种方式可以进入插入模式输入文本。还有一些简写来执行多条操作,比如说删除并进入删除模式。

删除从光标所在位置开始到行末的所有内容,并进入插入模式

C

删除当前行并进入插入模式

S

修改大小写

可以通过波浪符来变更大小写。就像你猜想的那种方式工作:不管你光标所在字符还是可视模式下选择的文本都可以。

格式化文本

Vim还可以快速格式化文本,比如格式化一整段文本,使用下面这个命令就可以很容易实现:

格式化当前段落

gq ap

gp根据预设的文本宽度testwidth以及参数指定的文本范围来工作,这意味着对于你指定的任意范围的文本,gp都可以自动校准宽度,使得文档看起来干净又整洁。

【注意: ap表示文本对象”整个段落”的覆盖范围】

删除文本

在了解怎样修改文本后,接下来看看怎样直接删除文本。可能你已经知道了,跟前面差不多——只不过实现的动作不同罢了。

基本删除命令

  • x: 删除当前光标的字符
  • X:删除当前光标的前一个字符
  • dm: 根据定义的m来确定删除范围,一个单词或者一句话或者一个段落。
  • dd: 删除当前行
  • dt. : 删除当前位置到下一个句号之间的所有内容
  • D: 删除当前光标位置到本行末的所有内容(对比上面的C,D不会进入插入模式)
  • J: 将上下两行合并(删除两行之间的换行符)

足够简单吧?

撤销与重做

如果一个编辑器连撤销和重做的功能都没有,你肯定不会用它。vim就像我们前面注意到的那样,尽其所能让我们更顺手地操作,撤销与重做也不例外。

  • u: 撤销上一个动作
  • Ctrl-r: 重做上一个动作

【注意:记得将CAPSLOCK键映射为Ctrl,这样你才能更方便快速的使用组合键。】

这两个动作都可以重复操作,你可以一直撤销啊撤销回到最初保存点,也可以一直重做啊重做到撤销前的状态。

重复动作

Vim所有命令中最强大的那个莫过于点命令,听起来有点奇怪是不是?解释一下,通过“.”这个小点可以让你做一些很棒的事情——重复之前你的任何一个操作。

使用“.”来重复上一次操作

你所做的许多操作可能都有重复的价值。比如进入插入模式添加一些文本内容。你只需执行一次,然后一直执行点命令来重复就可以了。请看下面的例子。

删除一个单词

dw

紧接着执行,再删除5个单词

5.

多么给力! 如果结合可视化模式,发挥的能力更让你大开眼界。

复制与粘贴

快速拷贝与粘贴文本同样也是文本编辑器的必备功能,vim当然也是这方面的高手。

【注意:除了点命令外,&也是一种强大的重复命令,用于重复ex模式下的上一条命令(还记得ex模式吗?)】

拷贝文本

Vim的拷贝与大家预期的方式可能略有不同。拷贝命令不是大家料想的c。不知你还记不记得,上文提到c表示“修改”,已经被占用了。Vim用拉取来表示拷贝,简写为y。

  • y: 拷贝选中内容
  • yy: 拷贝当前行

记住,这种拷贝动作与你熟悉的那种拷贝并无不同——你只是在目的处复制了一份而已。

剪切文本

剪切很简单:跟删除操作差不多。所以呢,剪切实质上就是将删除的内容挪到缓冲区,然后将这段删除的内容再粘贴就可以了。

粘贴文本

粘贴跟我们想的一样——使用p命令。所以呢,如果你执行dd命令删除了一行,你可以用p粘贴回去。

有一点要记住:粘贴的起始位置从你当前光标位置之后开始,根据你拷贝的内容,你要么粘贴字符或单词,要么粘贴行或列。如果想撤销粘贴,使用通用撤销命令“u”就可以了。

拷贝复制命令总结

  • 从当前行拷贝到下一个命令(名词)
  • 拷贝当前行的快捷键
  • 在当前光标所在行下一行黏贴拷贝或者删除的内容
  • 在当前光标所在行前一行黏贴拷贝或者删除的内容

交换两行的位置

ddp

使用这条命令可以很快速的交换两行的位置。第一部分删除你所在的行,第二部分将它粘贴回之前所在位置的下面。(ps:文章中用的是above,但是实际上拷贝回下方位置)

拼写检查

如果没有拼写检查的话,感觉会很糟糕,还好vim这方面做得很好。当然我们需要先在配置文件中配置拼写检查选项。

在~/.vimrc的某个位置中添加

找出拼写错误

如果在配置文件中设置了拼写检查的选项,那么拼错的单词会自动添加下划线。你也可以手动输入:set spell和:set nospell来启用或禁用。

不管怎么说,一旦开启了拼写检查,如果存在拼写错误的话,你就可以使用以下命令来跳转:

跳转到下一处拼写错误

]s

跳转到上一处拼写错误

[s

若光标处是错误单词,给出修改建议

z=

将识别的错误单词标记正确

zg

将vim视为正确的单词标记为错误单词

zw

我习惯在~/.vimrc中添加一些拼写相关的简写命令。首先就是让修正拼写错误的命令变得简单:

将拼写修正标记为f

在某些情况下,比如说我需要创造性的拼写一些单词时,我会使用下面的命令来取消拼写检查。想要重新打开拼写检查只要再执行一次这个命令就可以了。

Toggle spelling visuals with s

nnoremap s :set spell!

替换

Vim的另一个强大的功能是替换。命令分为三部分,首先指定将要被替换的字符串,之后是替换的新字符串,最后指定替换范围。

基本设置为:%s

将每行的foo替换为bar

:%s /foo/bar/g

将当前行中的foo替换为bar

:s /foo/bar/g

【注意: 两个命令的不同之处在于,单行替换s前少了%】

还有许多其他的选项,不过这些是最基本的。

PS: 前面&命令重复此类操作(:进入ex模式后执行的命令)

进阶

太棒了。我们已经讨论了大部分文本编辑器的基本功能,以及在vim中如何使用这些功能。现在可以进行高级vim的任务了——请注意,我说的高级进阶是对初学者而言,不是Kana这样的大神级人物。

重复操作

在稍早之前我们讨论了“.”点命令的重复功能。有些命令适合使用点命令来重复,有些则不适合,区分这两种命令很重要。

通常,如果你想要使用“.”(或者如Drew Neil称之为点命令)实现命令重复,说明你想用点命令缓存的命令来做比较细致的操作。

比如说你要向若干行行末添加一些内容,但是添加的前提是被添加的行包含指定的字符串。你可以使用如下方式来实现:

搜索字符串

/delinquent

现在,只要你按下“n”键都会跳转到下一个搜索到的实例。那么,从第一个匹配实例开始,我们给它追加文本。

在行末添加文本

A[DO NOT PAY] [Esc]

好,这条命令执行完成了。不过接下来还有12个地方需要你来做。点命令允许我们简单地重复执行上一条命令,而且我们还需要保存搜索结果,可以将这两条命令结合起来。

跳转到下一个匹配实例并向行末添加文本

n.

注意,这种方式将动作与缓存的命令结合起来,所以我们能按照预期地那样跳转并执行命令。

文本对象

文本对象很特殊。这些文本对象的存在使得你可以操作(动词)更加复杂的对象(名词)。比起选中某个单词并删除、跳转到句首并删除句子等简单操作来说,不管你在何种文本对象的区域内,你都可以对它们执行这些操作。

说是说不清,实践出真知嘛,我们可以看几个例子。

单词文本对象

首先来看基于单词的对象

  • iw : 单词内
  • aw :单词周围

这些都是目标(名词),所以我们可以对它们做一些操作,比如说删除、修改等等。

删除单词(包括单词周围的空格)

daw

【注意: 对象内部与对象周围这两者之间的区别在于单词旁边是否有空格】

句子文本对象

  • is: 句子内部
  • as: 句子周围

句子对象的工作原理与单词对象是一致的,想象一下,你敲了一大段句子,敲了一半后悔了想要删掉。你就可以简单的使用下面这条命令,而不用回到句子开头来申明删除整行。

修改整条句子

cis

这条命令杀伤力还是挺大的,它会删除整条句子并进入插入模式等待你输入新的内容。

更多对象类型

还有许多其他的对象类型,我只简单的来介绍一下。

  • 段落 : ip 和ap
  • 单引号 : i’和a’
  • 双引号: i”和 a”

当我编辑代码或者HTML文件时,时常会用到这些命令。关键是如果这些对象文本存在问题,你甚至并不需要处于这些对象内部就可以操作它们;如果光标当前位置在双引号外面(经测试,跟双引号处于同一行并在双引号前面),只需要输入ci”就可以删除双引号内部的所有内容,并将你挪到双引号中间进入插入模式等待你的输入。简直不要太酷。

对其他类型的某些对象同样适用,比如说各种括号(圆括号、尖括号、大括号)和标签(html)。

比如说编辑HTML链接时,URL地址一般在双引号之间,链接文本在标签中定义;通过vim这两条命令可以很方便地修改:ci”(修改URL)和cit(修改链接文本)。

文本对象总结

下面列出常见的文本对象:

  • 单词: iw 和 aw
  • 句子: is 和 as
  • 段落: ip 和 ap
  • 单引号: i’ 和 a’
  • 双引号: i” 和 a”
  • 反勾号 : i· 和 a
  • 圆括号: i( 和 a(
  • 方括号: i[ 和a[
  • 大括号: i{ 和a{
  • 标签: it 和at

总的来说,我使用最多的是单词,双引号和标签。

进入可视化模式

Vim的有许多迷人之处吸引着我们,不过它们相比较可视化模式的技能来说,还是太弱了。

也许可视化模式的最强大之处在于:我们前面学到的所有的一切命令,通过结合可视化模式使用,能力都会大有提升。这是由于在可视化模式下选中的高亮文本,可以全部作为命令的操作对象。

首先,学会进入可视化模式并选择文本。通过“v”进入可视化模式,有三种选择。

  • 基于字符选中: v
  • 基于行选中: V
  • 段落选中: Ctrl-v

在容器内部选中文本

有时候你会处于一段文本中,两头被,.({[之类的符号所包围。你可以使用下面的命令来选中这部分的内容:

选中圆括号中的内容

vi(

选中中括号之间的内容

vi[

你还可以加数字限定选择的层数(比如说你处于嵌套层)

选中两层大括号之间的所有内容

v2i{

【注意: 你也可以使用va来代替vi。请不要被这些迷惑。它们与我们所知的名词和动词没有什么不同】

基于字符的可视化选择

从基于字符的可视化开始介绍(从普通模式输入v进入可视化模式),在这种模式下你可以选中单个字符、多个字符、单词等等。相比较行选择模式来说,我比较少使用这种模式,不过只是相对而言,其实用的也还是挺多的。

这里最主要的是要理解这一点: 处于可视化模式中,你的移动动作会修改被高亮选择的文本。这就意味着,w或)之类的动作也会起作用,它们可以扩展当前高亮选择的范围。之后,这部分高亮文本就可以作为命令执行的目标。

基于行选择的可视化模式

普通模式下输入V进入这种模式,之后我们会讨论在这种模式下的操作。

基于列选择的可视化模式

可视化模式中还可以垂直地选择文本,这对操作列数据来说很给力。

可视化模式下对选中文本的执行动作

对这些文本做什么操作都是你的自由,不过最普遍的操作是删除、拷贝和粘贴。想一下你平常用鼠标选中一片文本会做什么操作?

进入可视化模式,选中两个以上单词并拷贝

vwwy

拷贝之后你可以在任何你希望的地方输入p来粘贴。

或者你可以做一些基于行的操作。

进入行选择模式,删除几行文本

Vjjd

你也可以使用文本对象,不过这种操作不太安全,最好别做。

可视化模式下选中一整个段落

vip

可视化模式下选中一整段文本并粘贴到当前段落之下。

vipyjjp

不要因为命令很长而感到崩溃。要记住,这些命令跟我们平常说话是一样的。你可以没有任何困难地、很随意地念叨:

我想去商店。

这条命令也是一样的:

拷贝这一段,下移两行,再粘贴。

将可视化模式与重复结合

可视化模式还有一个很神奇的地方,对于选中的文本,你可以通过点命令来执行之前缓存的命令。请看下面这个例子:

如果想在每行前面都加个冒号,可以现在第一行foo之前添加,然后可视化模式下选择之下所有行,执行点命令就可以了。

【注意:要想使用这个功能,必须要在配置文件中添加映射 vnoremap . :norm.

BAM!

觉得这功能没有那么酷炫? 那就想象一下: 你有个六万行的文件,每行都与例子一样,需要追加个冒号。你会怎么做?

对整个文件添加分号

0i:j0vG.

wut

大大简化了我们的操作,大杀器一枚。步骤如下:

  1. 走到第一行行首并插入一个冒号(0i:)
  2. 走到下一行定位到行首位置(j0,注意之前是插入模式,需要先回到普通模式)
  3. 选中当前行到文件末尾的内容(vG)
  4. 给选中的内容添加冒号(.)

这样对六万行文件的操作就全部完成了。你甚至不需要去记一些乱七八糟的口诀——就像学骑单车会摔倒一样,摔着摔着你就会了。相信我。

使用宏

有些人会觉得宏很可怕,其实不然。宏只用来完成这样一件事: 记录你做的一切,在你需要的时候帮你再做一次。下面是一个简单总结:

  • qa: 开始记录接下来要执行的动作,并命名为a
  • q: 停止记录
  • @a :将宏代表的动作再执行一次

很简单啊是不是?你可以保存多个宏,比如说宏a、宏b、宏c等等。当你需要的时候执行@a或者@c之类的就可以了。

什么场景下使用宏?

你可能会问:

既然可视化模式与点命令的结合已经这么强大了,我们为什么还要使用宏呢?

问得好,不过答案挺复杂。宏可以做你所做的一切,来看看这样一个流程

  1. 在当前行搜索“widget”
  2. 找到最后一个匹配到的单词并添加–maker
  3. 走到行首并添加一个分号
  4. 走到行末再添加一个点号
  5. 如果行末有空格则删除

这么一堆工作,咱们还是假设文件有六万行,试着在Microsoft word之类的工具里完成这些操作,照样会让人感到万分痛苦。

不过在vim中,这都不算事儿,你只需要把这些动作都做一遍(recording模式下),然后对每一行重新执行宏就可以了。

【注意:如果想要对可视化选中文本执行宏的话,可以执行:normal @a(或者其他你所定义的宏的名字),这条命令会临时将你切换到普通模式,针对每一行执行宏命令】

技巧

下面我们来看一些大家经常问的一些问题,总结出来节约大家时间。

删除行末空格

根据打开文件的类型不同,可能需要你对回车符和换行符做一些处理。这里介绍如何删除行末那些烦人的Ctrl-M字符。

删除行末的Ctrl-M字符

:%s/s+$//

修改文件类型

set ft=unix

set ft=html

set ft=dos

【注意:如果要显示当前文件类型,在配置文件中添加:set filetype,或者在vim中直接执行这条命令就可以了】

包装文本

借用Surround插件,我们可以很

  • cs”‘: 将光标所在单词两边的双引号修改成单引号
  • cs’ :做同样的事,不同的是将单引号修改成
  • ds” : 删除两边的的配对双引号
  • ysiw[ :在当前单词两边加上中括号
  • ysiw: 给当前单词(其他文本对象也可)添加强调标签!!!知道我为什么这么激动吗?因为这个操作可以使用点命令来重复!
  • Visual Mode: 可视化模式下,选中任意文本,输入S。vim会带你跳转到窗口最底端。这时候你可以输入任何你想给这段文本打上的标签,比如说 ,回车试试?

总结

通过本教程我希望大家能明白以下两点:

  1. vim 是可以学会的
  2. vim 很强大

上面介绍的那些vim基础用法,即便你只喜欢其中一部分,我想可能也会加深你对编辑文本的热情——这可不是小事。你对操作的编辑器越熟练,思维也就越发散,我想这是堪比史诗级的大事了。

更为重要的是,这也是为什么要求你得精通你选择的编辑器。只有精通编辑器,不被编辑器的操作所阻滞,当你脑内思绪奔腾时,你才能激扬文字跟紧思绪跳跃的步伐。

或者,你也可以将这一切都抛开,成为那些被人所取笑的人——不管怎样,我希望本教程能对你有用。

【如果你喜欢这篇文章,可以去我的地盘看看其他技术指导文章】

引用

  1. 推荐大家阅读Drew Neil的这本书《Practical Vim: Edit Text at the Speed of Thought》这是vim爱好者必备书籍。
  2. 强烈推荐《Your Problem with vim is that you don’t grok vi》,它不光对vim一般用法做了非凡概述,还介绍了一些漂亮的vim技巧。
  3. 如果你还没看过Steve Losh的《Coming Home to Vim》,强烈推荐。
  4. 一定要去看看 Kana 大神的《True Power of Vim》。
  5. 另外还有Drew的 Vimcasts.org. 这两者都从实际操作角度让你认识到vim的强大。
  6. 如果想要vim简明命令资源,下载 Vim Quick Reference
  7. 当然不要忘了 Vim Wiki,这也是学习vim的重要资源。
  8. 如果你对vimscript感兴趣, 一定去看看Steve Losh的《Learn Vimscript the Hard Way》。这是迄今为止关于vimscript的最好学习资料。
  9. 还有Openvim的《This is a really well done interactive tutorial》。
  10. The help 非常棒,不过内容有点多,如果你真的非常想要学好Vim的话,那么一定要从头到尾通读。
  11. Vim from zero to hero – Vim 从入门到精通
  12. Vim的Window窗口

from:http://blog.jobbole.com/86132/

分布式服务化系统一致性的“最佳实干”

1 背景

一致性是一个抽象的、具有多重含义的计算机术语,在不同应用场景下,有不同的定义和含义。在传统的IT时代,一致性通常指强一致性,强一致性通常体现在你中有我、我中有你、浑然一体;而在互联网时代,一致性的含义远远超出了它原有的含义,在我们讨论互联网时代的一致性之前,我们先了解一下互联网时代的特点,互联网时代信息量巨大、需要计算能力巨大,不但对用户响应速度要求快,而且吞吐量指标也要向外扩展(既:水平伸缩),于是单节点的服务器无法满足需求,服务节点开始池化,想想那个经典的故事,一只筷子一折就断,一把筷子怎么都折不断,可见人多力量大的思想是多么的重要,但是人多也不一定能解决所有事情,还得进行有序、合理的分配任务,进行有效的管理,于是互联网时代谈论最多的话题就是拆分,拆分一般分为“水平拆分”和“垂直拆分”(大家不要对应到数据库或者缓存拆分,这里主要表达一种逻辑)。这里,“水平拆分”指的是同一个功能由于单机节点无法满足性能需求,需要扩展成为多节点,多个节点具有一致的功能,组成一个服务池,一个节点服务一部分的请求量,团结起来共同处理大规模高并发的请求量。“垂直拆分”指的是按照功能拆分,秉着“专业的人干专业的事儿”的原则,把一个复杂的功能拆分到多个单一的简单的元功能,不同的元功能组合在一起,和未拆分前完成的功能是一致的,由于每个元功能职责单一、功能简单,让维护和变更都变得更简单、安全,更易于产品版本的迭代,在这样的一个互联网的时代和环境,一致性指分布式服务化系统之间的弱一致性,包括应用系统一致性和数据一致性。

无论是水平拆分还是垂直拆分,都解决了特定场景下的特定问题,凡事有好的一面,都会有坏的一面,拆分后的系统或者服务化的系统最大的问题就是一致性问题,这么多个具有元功能的模块,或者同一个功能池中的多个节点之间,如何保证他们的信息是一致的、工作步伐是一致的、状态是一致的、互相协调有序的工作呢?

本文根据作者在互联网企业的实际项目经验,对服务化系统中最难解决的一致性问题进行研究和探讨,试图从实践经验中找到规律,抽象出模式,分享给大家,希望对大家的项目实施有所帮助,在对实践的总结中也会对相关的一致性术语做最朴实的解释,希望能帮助大家彻底理解一致性的本质,并能将其应用到实践,解决读者现实中遇到的服务化系统的一致性问题,本文使用理论与实践相结合的方法,突出在实践中解决问题的模式,因此叫做《分布式服务化系统一致性的“最佳实干”》。

2 问题

本节列举不一致会导致的种种问题,这也包括一例生活中的问题。

案例1:买房

假如你想要享受生活的随意,只想买个两居,不想让房贷有太大压力,而你媳妇却想要买个三居,还得带花园的,那么你们就不一致了,不一致导致生活不愉快、不协调,严重情况下还会吵架,可见生活中的不一致问题影响很大。

案例2:转账

转账是经典的不一致案例,设想一下银行为你处理一笔转账,扣减你账户上的余额,然后增加别人账户的余额;如果扣减你的账户余额成功,增加别人账户余额失败,那么你就会损失这笔资金。反过来,如果扣减你的账户余额失败,增加别人账户余额成功,那么银行就会损失这笔资金,银行需要赔付。对于资金处理系统来说,上面任何一种场景都是不允许发生的,一旦发生就会有资金损失,后果是不堪设想的,严重情况会让一个公司瞬间倒闭,可参考案例

案例3:下订单和扣库存

电商系统中也有一个经典的案例,下订单和扣库存如何保持一致,如果先下订单,扣库存失败,那么将会导致超卖;如果下订单没有成功,扣库存成功,那么会导致少卖。两种情况都会导致运营成本的增加,严重情况下需要赔付。

案例4:同步超时

服务化的系统间调用常常因为网络问题导致系统间调用超时,即使是网络很好的机房,在亿次流量的基数下,同步调用超时也是家常便饭。系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了预定的功能或者没有完成预定的功能。于是,系统A就迷茫了,不知道应该继续做什么,如何反馈给使用方。(曾经的一个B2B产品的客户要求接口超时重新通知他们,这个在技术上是难以实现的,因为服务器本身可能并不知道自己超时,可能会继续正常的返回数据,只是客户端并没有接受到结果罢了,因此这不是一个合理的解决方案)。

案例5:异步回调超时

此案例和上一个同步超时案例类似,不过这个场景使用了异步回调,系统A同步调用系统B发起指令,系统B采用受理模式,受理后则返回受理成功,然后系统B异步通知系统A。在这个过程中,如果系统A由于某种原因迟迟没有收到回调结果,那么两个系统间的状态就不一致,互相认知不同会导致系统间发生错误,严重情况下会影响核心事务,甚至会导致资金损失。

案例6:掉单

分布式系统中,两个系统协作处理一个流程,分别为对方的上下游,如果一个系统中存在一个请求,通常指订单,另外一个系统不存在,则导致掉单,掉单的后果很严重,有时候也会导致资金损失。

案例7:系统间状态不一致

这个案例与上面掉单案例类似,不同的是两个系统间都存在请求,但是请求的状态不一致。

案例8:缓存和数据库不一致

交易相关系统基本离不开关系型数据库,依赖关系型数据库提供的ACID特性(后面介绍),但是在大规模高并发的互联网系统里,一些特殊的场景对读的性能要求极高,服务于交易的数据库难以抗住大规模的读流量,通常需要在数据库前垫缓存,那么缓存和数据库之间的数据如何保持一致性?是要保持强一致呢还是弱一致性呢?

案例9:本地缓存节点间不一致

一个服务池上的多个节点为了满足较高的性能需求,需要使用本地缓存,使用了本地缓存,每个节点都会有一份缓存数据的拷贝,如果这些数据是静态的、不变的,那永远都不会有问题,但是如果这些数据是半静态的或者常被更新的,当被更新的时候,各个节点更新是有先后顺序的,在更新的瞬间,各个节点的数据是不一致的,如果这些数据是为某一个开关服务的,想象一下重复的请求走进了不同的节点(在failover或者补偿导致的场景下,重复请求是一定会发生的,也是服务化系统必须处理的),一个请求走了开关打开的逻辑,同时另外一个请求走了开关关闭的逻辑,这导致请求被处理两次,最坏的情况下会导致灾难性的后果,就是资金损失。

案例10:缓存数据结构不一致

这个案例会时有发生,某系统需要种某一数据结构的缓存,这一数据结构有多个数据元素组成,其中,某个数据元素都需要从数据库中或者服务中获取,如果一部分数据元素获取失败,由于程序处理不正确,仍然将不完全的数据结构存入缓存,那么缓存的消费者消费的时候很有可能因为没有合理处理异常情况而出错。

3 模式

3.1 生活中不一致问题的解决

大家回顾一下上一节列举的生活中的案例1-买房,如果置身事外来看,解决这种不一致的办法有两个,一个是避免不一致的发生,如果已经是媳妇了就不好办了:),还有一种方法就是慢慢的补偿,先买个两居,然后慢慢的等资金充裕了再换三居,买比特币赚了再换带花园的房子,于是问题最终被解决了,最终大家处于一致的状态,都开心了。这样可以解决案例1的问题,很自然由于有了过渡的方法,问题在不经意间就消失了,可见“过渡”也是解决一致性问题的一个模式。

从案例1的解决方案来看,我们要解决一致性问题,一个最直接最简单的方法就是保持强一致性,对于案例1的情况,尽量避免在结婚前两个人能够互相了解达成一致,避免不一致问题的发生;不过有些事情事已至此,发生了就是发生了,出现了不一致的问题,我们应该考虑去补偿,尽最大的努力从不一致状态修复到一致状态,避免损失全部或者一部分,也不失为一个好方法。

因此,避免不一致是上策,出现了不一致及时发现及时修复是中策,有问题不积极解决留给他人解决是下策。

3.2 酸碱平衡理论

ACID在英文中的意思是“酸”,BASE的意识是“碱”,这一段讲的是“酸碱平衡”的故事。

1. ACID(酸)

如何保证强一致性呢?计算机专业的童鞋在学习关系型数据库的时候都学习了ACID原理,这里对ACID做个简单的介绍。如果想全面的学习ACID原理,请参考ACID

关系型数据库天生就是解决具有复杂事务场景的问题,关系型数据库完全满足ACID的特性。

ACID指的是:

  • A: Atomicity,原子性
  • C: Consistency,一致性
  • I: Isolation,隔离性
  • D: Durability,持久性

具有ACID的特性的数据库支持强一致性,强一致性代表数据库本身不会出现不一致,每个事务是原子的,或者成功或者失败,事物间是隔离的,互相完全不影响,而且最终状态是持久落盘的,因此,数据库会从一个明确的状态到另外一个明确的状态,中间的临时状态是不会出现的,如果出现也会及时的自动的修复,因此是强一致的。

3个典型的关系型数据库Oracle、Mysql、Db2都能保证强一致性,Oracle和Mysql使用多版本控制协议实现,而DB2使用改进的两阶段提交协议来实现。

如果你在为交易相关系统做技术选型,交易的存储应该只考虑关系型数据库,对于核心系统,如果需要较好的性能,可以考虑使用更强悍的硬件,这种向上扩展(升级硬件)虽然成本较高,但是是最简单粗暴有效的方式,另外,Nosql完全不适合交易场景,Nosql主要用来做数据分析、ETL、报表、数据挖掘、推荐、日志处理等非交易场景。

前面提到的案例2-转账案例3-下订单和扣库存都可以利用关系型数据库的强一致性解决。

然而,前面提到,互联网项目多数具有大规模高并发的特性,必须应用拆分的理念,对高并发的压力采取“大而化小、小而化了”的方法,否则难以满足动辄亿级流量的需求,即使使用关系型数据库,单机也难以满足存储和TPS上的需求。为了保证案例2-转账可以利用关系型数据库的强一致性,在拆分的时候尽量的把转账相关的账户放入一个数据库分片,对于案例3,尽量的保证把订单和库存放入同一个数据库分片,这样通过关系型数据库自然就解决了不一致的问题。

然而,有些时候事与愿违,由于业务规则的限制,无法将相关的数据分到同一个数据库分片,这个时候我们就需要实现最终一致性。

对于案例2-转账场景,假设账户数量巨大,对账户存储进行了拆分,关系型数据库一共分了8个实例,每个实例8个库,每个库8个表,共512张表,假如要转账的两个账户正好落在了一个库里,那么可以依赖关系型数据库的事务保持强一致性。

如果要转账的两个账户正好落在了不同的库里,转账操作是无法封装在同一个数据库事务中的,这个时候会发生一个库的账户扣减余额成功,另外一个库的账户增加余额失败的情况。

对于这种情况,我们需要继续探讨解决之道,CAP原理和BASE原理,BASE原理通过记录事务的中间的临时状态,实现最终一致性。

2. CAP(帽子理论)

如果想深入的学习CAP理论,请参考CAP

由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系的帽子理论包含三个元素:

  • C:Consistency,一致性, 数据一致更新,所有数据变动都是同步的
  • A:Availability,可用性, 好的响应性能,完全的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应
  • P:Partition tolerance,分区容错性,可靠性

帽子理论证明,任何分布式系统只可同时满足二点,没法三者兼顾。关系型数据库由于关系型数据库是单节点的,因此,不具有分区容错性,但是具有一致性和可用性,而分布式的服务化系统都需要满足分区容错性,那么我们必须在一致性和可用性中进行权衡,具体表现在服务化系统处理的异常请求在某一个时间段内可能是不完全的,但是经过自动的或者手工的补偿后,达到了最终的一致性。

3. BASE(碱)

BASE理论解决CAP理论提出了分布式系统的一致性和可用性不能兼得的问题,如果想全面的学习BASE原理,请参考Eventual consistency

BASE在英文中有“碱”的意思,对应本节开头的ACID在英文中“酸”的意思,基于这两个名词提出了酸碱平衡的结论,简单来说是在不同的场景下,可以分别利用ACID和BASE来解决分布式服务化系统的一致性问题。

BASE模型与ACID模型截然不同,满足CAP理论,通过牺牲强一致性,获得可用性,一般应用在服务化系统的应用层或者大数据处理系统,通过达到最终一致性来尽量满足业务的绝大部分需求。

BASE模型包含个三个元素:

  • BA:Basically Available,基本可用
  • S:Soft State,软状态,状态可以有一段时间不同步
  • E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致

BASE模型的软状态是实现BASE理论的方法,基本可用和最终一致是目标。按照BASE模型实现的系统,由于不保证强一致性,系统在处理请求的过程中,可以存在短暂的不一致,在短暂的不一致窗口请求处理处在临时状态中,系统在做每步操作的时候,通过记录每一个临时状态,在系统出现故障的时候,可以从这些中间状态继续未完成的请求处理或者退回到原始状态,最后达到一致的状态。

案例1-转账为例,我们把用户A给用户B转账分成四个阶段,第一个阶段用户A准备转账,第二个阶段从用户A账户扣减余额,第三个阶段对用户B增加余额,第四个阶段完成转账。系统需要记录操作过程中每一步骤的状态,一旦系统出现故障,系统能够自动发现没有完成的任务,然后,根据任务所处的状态,继续执行任务,最终完成任务,达到一致的最终状态。

在实际应用中,上面这个过程通常是通过持久化执行任务的状态和环境信息,一旦出现问题,定时任务会捞取未执行完的任务,继续未执行完的任务,直到执行完成为止,或者取消已经完成的部分操作回到原始状态。这种方法在任务完成每个阶段的时候,都要更新数据库中任务的状态,这在大规模高并发系统中不会有太好的性能,一个更好的办法是用Write-Ahead Log(写前日志),这和数据库的Bin Log(操作日志)相似,在做每一个操作步骤,都先写入日志,如果操作遇到问题而停止的时候,可以读取日志按照步骤进行恢复,并且继续执行未完成的工作,最后达到一致。写前日志可以利用机械硬盘的追加写而达到较好性能,因此,这是一种专业化的实现方式,多数业务系系统还是使用数据库记录的字段来记录任务的执行状态,也就是记录中间的“软状态”,一个任务的状态流转一般可以通过数据库的行级锁来实现,这比使用Write-Ahead Log实现更简单、更快速。

有了BASE理论作为基础,我们对复杂的分布式事务进行拆解,对其中的每一步骤都记录其状态,有问题的时候可以根据记录的状态来继续执行任务,达到最终的一致,通过这个方法我们可以解决案例2-转账案例3-下订单和扣库存中遇到的问题。

4. 酸碱平衡的总结

  1. 使用向上扩展(强悍的硬件)运行专业的关系型数据库(例如:Oracle或者DB2)能够保证强一致性,钱能解决的问题就不是问题
  2. 如果钱是问题,可以对廉价硬件运行的开源关系型数据库(例如:Mysql)进行分片,将相关的数据分到数据库的同一个片,仍然能够使用关系型数据库保证事务
  3. 如果业务规则限制,无法将相关的数据分到同一个片,就需要实现最终一致性,通过记录事务的软状态(中间状态、临时状态),一旦处于不一致,可以通过系统自动化或者人工干预来修复不一致的情况

3.3 分布式一致性协议

国际开放标准组织Open Group定义了DTS(分布式事务处理模型),模型中包含4个角色:应用程序、事务管理器、资源管理器、通信资源管理器四部分。事务处理器是统管全局的管理者,资源处理器和通信资源处理器是事务的参与者。

J2EE规范也包含此分布式事务处理模型的规范,并在所有的AppServer中进行实现,J2EE规范中定义了TX协议和XA协议,TX协议定义应用程序与事务管理器之间的接口,而XA协议定义了事务管理器与资源处理器之间的接口,在过去,大家使用AppServer,例如:Websphere、Weblogic、Jboss等配置数据源的时候会看见类似XADatasource的数据源,这就是实现了DTS的关系型数据库的数据源。企业级开发JEE中,关系型数据库、JMS服务扮演资源管理器的角色,而EJB容器则扮演事务管理器的角色。

下面我们就介绍两阶段提交协议三阶段提交协议以及阿里巴巴提出的TCC,它们都是根据DTS这一思想演变出来的。

1. 两阶段提交协议

上面描述的JEE的XA协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。

两阶段提交协议把分布式事务分成两个过程,一个是准备阶段,一个是提交阶段,准备阶段和提交阶段都是由事务管理器发起的,为了接下来讲解方便,我们把事务管理器称为协调者,把资管管理器称为参与者。

两阶段如下:

  1. 准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写redo或者undo日志(这也是前面提起的Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交
  2. 提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源

两阶段提交协议成功场景示意图如下:

两阶段提交协议

我们看到两阶段提交协议在准备阶段锁定资源,是一个重量级的操作,并能保证强一致性,但是实现起来复杂、成本较高,不够灵活,更重要的是它有如下致命的问题:

  1. 阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续做下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放
  2. 单点故障:如果协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果之前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,并且参与者接收后也宕机,新上任的协调者无法处理这种情况
  3. 脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的

上面所有的这些问题,都是需要人工干预处理,没有自动化的解决方案,因此两阶段提交协议在正常情况下能保证系统的强一致性,但是在出现异常情况下,当前处理的操作处于错误状态,需要管理员人工干预解决,因此可用性不够好,这也符合CAP协议的一致性和可用性不能兼得的原理。

2. 三阶段提交协议

三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:

  1. 询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段超时导致中止
  2. 准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段超时导致成功
  3. 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致

三阶段提交协议成功场景示意图如下:

三阶段提交协议

然而,这里与两阶段提交协议有两个主要的不同:

  1. 增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生
  2. 在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大

三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。

3. TCC

上面两节讲解了两阶段提交协议和三阶段提交协议,实际上他们能解决案例2-转账案例3-下订单和扣库存中的分布式事务的问题,但是遇到极端情况,系统会发生阻塞或者不一致的问题,需要运营或者技术人工解决。无论两阶段还是三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。

阿里巴巴提出了新的TCC协议,TCC协议将一个任务拆分成Try、Confirm、Cancel,正常的流程会先执行Try,如果执行没有问题,再执行Confirm,如果执行过程中出了问题,则执行操作的逆操Cancel,从正常的流程上讲,这仍然是一个两阶段的提交协议,但是,在执行出现问题的时候,有一定的自我修复能力,如果任何一个参与者出现了问题,协调者通过执行操作的逆操作来取消之前的操作,达到最终的一致状态。

可以看出,从时序上,如果遇到极端情况下TCC会有很多问题的,例如,如果在Cancel的时候一些参与者收到指令,而一些参与者没有收到指令,整个系统仍然是不一致的,这种复杂的情况,系统首先会通过补偿的方式,尝试自动修复的,如果系统无法修复,必须由人工参与解决。

从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而,TCC通过自动化补偿手段,会把需要人工处理的不一致情况降到到最少,也是一种非常有用的解决方案,根据线人,阿里在内部的一些中间件上实现了TCC模式。

我们给出一个使用TCC的实际案例,在秒杀的场景,用户发起下单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,然后指引用户去支付,由于某种原因用户支付失败,或者支付超时,系统会自动将锁定的库存解锁供其他用户秒杀。

TCC协议使用场景示意图如下:

TCC

总结一下,两阶段提交协议、三阶段提交协议、TCC协议都能保证分布式事务的一致性,他们保证的分布式系统的一致性从强到弱,TCC达到的目标是最终一致性,其中任何一种方法都可以不同程度的解决案例2:转账、案例3:下订单和扣库存的问题,只是实现的一致性的级别不一样而已,对于案例4:同步超时可以通过TCC的理念解决,如果同步调用超时,调用方可以使用fastfail策略,返回调用方的使用方失败的结果,同时调用服务的逆向cancel操作,保证服务的最终一致性。

3.4 保证最终一致性的模式

在大规模高并发服务化系统中,一个功能被拆分成多个具有单一功能的元功能,一个流程会有多个系统的多个元功能组合实现,如果使用两阶段提交协议和三阶段提交协议,确实能解决系统间一致性问题,除了这两个协议带来的自身的问题,这些协议的实现比较复杂、成本比较高,最重要的是性能并不好,相比来看,TCC协议更简单、容易实现,但是TCC协议由于每个事务都需要执行Try,再执行Confirm,略微显得臃肿,因此,在现实的系统中,底线要求仅仅需要能达到最终一致性,而不需要实现专业的、复杂的一致性协议,实现最终一致性有一些非常有效的、简单粗暴的模式,下面就介绍这些模式及其应用场景。

1. 查询模式

任何一个服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。服务操作的使用方可以通过查询接口,得知服务操作执行的状态,然后根据不同状态来做不同的处理操作。

为了能够实现查询,每个服务操作都需要有唯一的流水号标识,也可使用此次服务操作对应的资源ID来标志,例如:请求流水号、订单号等。

首先,单笔查询操作是必须提供的,我们也鼓励使用单笔订单查询,这是因为每次调用需要占用的负载是可控的,批量查询则根据需要来提供,如果使用了批量查询,需要有合理的分页机制,并且必须限制分页的大小,以及对批量查询的QPS需要有容量评估和流控等。

查询模式的示意图如下:

查询模式

对于案例4:同步超时、案例5:异步回调超时、案例6:掉单、案例7:系统间状态不一致,我们都需要使用查询模式来了解被调用服务的处理情况,来决定下一步做什么:补偿未完成的操作还是回滚已经完成的操作。

2. 补偿模式

有了上面的查询模式,在任何情况下,我们都能得知具体的操作所处的状态,如果整个操作处于不正常的状态,我们需要修正操作中有问题的子操作,这可能需要重新执行未完成的子操作,后者取消已经完成的子操作,通过修复使整个分布式系统达到一致,为了让系统最终一致而做的努力都叫做补偿。

对于服务化系统中同步调用的操作,业务操作发起的主动方在还没有得到业务操作执行方的明确返回或者调用超时,场景可参考案例4:同步超时,这个时候业务发起的主动方需要及时的调用业务执行方获得操作执行的状态,这里使用查询模式,获得业务操作的执行方的状态后,如果业务执行方已经完预设的工作,则业务发起方给业务的使用方返回成功,如果业务操作的执行方的状态为失败或者未知,则会立即告诉业务的使用方失败,然后调用业务操作的逆向操作,保证操作不被执行或者回滚已经执行的操作,让业务的使用方、业务发起的主动方、业务的操作方最终达成一致的状态。

补偿模式的示意图如下:

补偿模式

补偿操作根据发起形式分为:

  1. 自动恢复:程序根据发生不一致的环境,通过继续未完成的操作,或者回滚已经完成的操作,自动来达到一致
  2. 通知运营:如果程序无法自动恢复,并且设计时考虑到了不一致的场景,可以提供运营功能,通过运营手工进行补偿
  3. 通知技术:如果很不巧,系统无法自动回复,又没有运营功能,那必须通过技术手段来解决,技术手段包括走数据库变更或者代码变更来解决,这是最糟的一种场景

3. 异步确保模式

异步确保模式是补偿模式的一个典型案例,经常应用到使用方对响应时间要求并不太高,我们通常把这类操作从主流程中摘除,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方,这个方案最大的好处能够对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入账等。

实践中,将要执行的异步操作封装后持久入库,然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统足够健壮,任何一个任务最终会被成功执行。

异步确保模式的示意图如下:

异步确保模式

对于案例5:异步回调超时,使用的就是异步确保模式,这种情况下对于某个操作,如果迟迟没有收到响应,我们通过查询模式和补偿模式来继续未完成的操作。

4. 定期校对模式

既然我们在系统中实现最终一致性,系统在没有达到一致之前,系统间的状态是不一致的,甚至是混乱的,需要补偿操作来达到一致的目的,但是我们如何来发现需要补偿的操作呢?

在操作的主流程中的系统间执行校对操作,我们可以事后异步的批量校对操作的状态,如果发现不一致的操作,则进行补偿,补偿操作与补偿模式中的补偿操作是一致的。

另外,实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID,ID的生成请参考SnowFlake

在分布式系统中,全局唯一ID的示意图如下:

唯一ID

一般情况下,生成全局唯一ID有两种方法:

  1. 持久型:使用数据库表自增字段或者Sequence生成,为了提高效率,每个应用节点可以缓存一批次的ID,如果机器重启可能会损失一部分ID,但是这并不会产生任何问题
  2. 时间型:一般由机器号、业务号、时间、单节点内自增ID组成,由于时间一般精确到秒或者毫秒,因此不需要持久就能保证在分布式系统中全局唯一、粗略递增能特点

实践中,为了能在分布式系统中迅速的定位问题,一般的分布式系统都有技术支持系统,它能够跟踪一个请求的调用链,调用链是在二维的维度跟踪一个调用请求,最后形成一个调用树,原理可参考谷歌的论文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,一个开源的参考实现为pinpoint

在分布式系统中,调用链的示意图如下:

调用链

全局的唯一流水ID可以把一个请求在分布式系统中的流转的路径聚合,而调用链中的spanid可以把聚合的请求路径通过树形结构进行展示,让技术支持人员轻松的发现系统出现的问题,能够快速定位出现问题的服务节点,提高应急效率。

关于订单跟踪、调用链跟踪、业务链跟踪,我们会在后续文章中详细介绍。

在分布式系统中构建了唯一ID,调用链等基础设施,我们很容易对系统间的不一致进行核对,通常我们需要构建第三方的定期核对系统,以第三方的角度来监控服务执行的健康程度。

定期核对系统示意图如下:

定期核对模式

对于案例6:掉单、案例7:系统间状态不一致通常通过定期校对模式发现问题,并通过补偿模式来修复,最后完成系统间的最终一致性。

定期校对模式多应用在金融系统,金融系统由于涉及到资金安全,需要保证百分之百的准确性,所以,需要多重的一致性保证机制,包括:系统间的一致性对账、现金对账、账务对账、手续费对账等等,这些都属于定期校对模式,顺便说一下,金融系统与社交应用在技术上本质的区别在于社交应用在于量大,而金融系统在于数据的准确性。

到现在为止,我们看到通过查询模式、补偿模式、定期核对模式可以解决案例4到案例7的所有问题,对于案例4:同步超时,如果同步超时,我们需要查询状态进行补偿,对于案例5:异步回调超时,如果迟迟没有收到回调响应,我们也会通过查询状态进行补偿,对于案例6:掉单、案例7:系统间状态不一致,我们通过定期核对模式可以保证系统间操作的一致性,避免掉单和状态不一致导致问题。

5. 可靠消息模式

在分布式系统中,对于主流程中优先级比较低的操作,大多采用异步的方式执行,也就是前面提到的异步确保型,为了让异步操作的调用方和被调用方充分的解耦,也由于专业的消息队列本身具有可伸缩、可分片、可持久等功能,我们通常通过消息队列实现异步化,对于消息队列,我们需要建立特殊的设施保证可靠的消息发送以及处理机的幂等等。

消息的可靠发送

消息的可靠发送可以认为是尽最大努力发送消息通知,有两种实现方法:

第一种,发送消息之前,把消息持久到数据库,状态标记为待发送,然后发送消息,如果发送成功,将消息改为发送成功。定时任务定时从数据库捞取一定时间内未发送的消息,将消息发送。

消息发送模式1

第二种,实现方式与第一种类似,不同的是持久消息的数据库是独立的,并不耦合在业务系统中。发送消息之前,先发送一个预消息给某一个第三方的消息管理器,消息管理器将其持久到数据库,并标记状态为待发送,发送成功后,标记消息为发送成功。定时任务定时从数据库捞取一定时间内未发送的消息,回查业务系统是否要继续发送,根据查询结果来确定消息的状态。

消息发送模式2

一些公司把消息的可靠发送实现在了中间件里,通过Spring的注入,在消息发送的时候自动持久消息记录,如果有消息记录没有发送成功,定时会补偿发送。

消息处理器的幂等性

如果我们要保证消息可靠的发送,简单来说,要保证消息一定要发送出去,那么就需要有重试机制,有了重试机制,消息一定会重复,那么我们需要对重复做处理。

处理重复的最佳方式为保证操作的幂等性,幂等性的数学公式为:

f(f(x)) = f(x)

保证操作的幂等性常用的几个方法:

  1. 使用数据库表的唯一键进行滤重,拒绝重复的请求
  2. 使用分布式表对请求进行滤重
  3. 使用状态流转的方向性来滤重,通常使用行级锁来实现(后续在锁相关的文章中详细说明)
  4. 根据业务的特点,操作本身就是幂等的,例如:删除一个资源、增加一个资源、获得一个资源等

6. 缓存一致性模型

大规模高并发系统中一个常见的核心需求就是亿级的读需求,显然,关系型数据库并不是解决高并发读需求的最佳方案,互联网的经典做法就是使用缓存抗读需求,下面有一些使用缓存的保证一致性的最佳实践:

  1. 如果性能要求不是非常的高,尽量使用分布式缓存,而不要使用本地缓存
  2. 种缓存的时候一定种完全,如果缓存数据的一部分有效,一部分无效,宁可放弃种缓存,也不要把部分数据种入缓存
  3. 数据库与缓存只需要保持弱一致性,而不需要强一致性,读的顺序要先缓存,后数据库,写的顺序要先数据库,后缓存

这里的最佳实践能够解决案例8:缓存和数据库不一致、案例9:本地缓存节点间不一致、案例10:缓存数据结构不一致的问题,对于数据存储层、缓存与数据库、Nosql等的一致性是更深入的存储一致性技术,将会在后续文章单独介绍,这里的数据一致性主要是处理应用层与缓存、应用层与数据库、一部分的缓存与数据库的一致性。

3.5 专题模式

这一节介绍特殊场景下的一致性问题和解决方案。

迁移开关的设计

在大多数企业里,新项目和老项目一般会共存,大家都在努力的下掉老项目,但是由于种种原因总是下不掉,如果要彻底的下掉老项目,就必须要有非常完善的迁移方案,迁移是一项非常复杂而艰巨的任务,我会在将来的文章中详细探讨迁移方案、流程和技术,这里我们只对迁移中使用的开关进行描述。

迁移过程必须使用开关,开关一般都会基于多个维度来设计,例如:全局的、用户的、角色的、商户的、产品的等等,如果迁移过程中遇到问题,我们需要关闭开关,迁移回老的系统,这需要我们的新系统兼容老的数据,老的系统也兼容新的数据,从某种意义上来讲,迁移比实现新系统更加困难。

曾经看过很多简单的开关设计,有的开关设计在应用层次,通过一个curl语句调用,没有权限控制,这样的开关在服务池的每个节点都是不同步的、不一致的;还有的系统把开关配置放在中心化的配置系统、数据库或者缓存等,处理的每个请求都通过统一的开关来判断是否迁移等等,这样的开关有一个致命的缺点,服务请求在处理过程中,开关可能会变化,各个节点之间开关可能不同步、不一致,导致重复的请求可能走到新的逻辑又走了老的逻辑,如果新的逻辑和老的逻辑没有保证幂等性,这个请求就被重复处理了,如果是金融行业的应用,可能会导致资金损失,电商系统可能会导致发货并退款等问题。

这里面我们推荐使用订单开关,不管我们在什么维度上设计了开关,接收到服务请求后,我们在请求创建的关联实体(例如:订单)上标记开关,以后的任何处理流程,包括同步的和异步的处理流程,都通过订单上的开关来判断,而不是通过全局的或者基于配置的开关,这样在订单创建的时候,开关已经确定,不再变更,一旦一份数据不再发生变化,那么它永远是线程安全的,并且不会有不一致的问题。

这个模式在生产中使用比较频繁,建议每个企业都把这个模式作为设计评审的一项,如果不检查这一项,很多开发童鞋都会偷懒,直接在配置中或者数据库中做个开关就上线了。

4 总结

本文从一致性问题的实践出发,从大规模高并发服务化系统的实践经验中进行总结,列举导致不一致的具体问题,围绕着具体问题,总结出解决不一致的方法,并且抽象成模式,供大家在开发服务化系统的过程中参考。

另外,由于篇幅有限,还有一些关于分布式一致性的技术无法在一篇文章中与大家分享,包括:paxos算法、raft算法、zab算法、nwr算法、一致性哈希等,我会在后续文章中详细介绍。

5 反馈与建议

from:http://www.jianshu.com/p/1156151e20c8