分布式事务的 N 种实现

目录

需求缘起

在微服务架构中,随着服务的逐步拆分,数据库私有已经成为共识,这也导致所面临的分布式事务问题成为微服务落地过程中一个非常难以逾越的障碍,但是目前尚没有一个完整通用的解决方案。

其实不仅仅是在微服务架构中,随着用户访问量的逐渐上涨,数据库甚至是服务的分片、分区、水平拆分、垂直拆分已经逐渐成为较为常用的提升瓶颈的解决方案,因此越来越多的原子操作变成了跨库甚至是跨服务的事务操作。最终结果是在对高性能、高扩展性,高可用性的追求的道路上,我们开始逐渐放松对一致性的追求,但是在很多场景下,尤其是账务,电商等业务中,不可避免的存在着一致性问题,使得我们不得不去探寻一种机制,用以在分布式环境中保证事务的一致性

引用自 https://www.infoq.cn/article/2018/08/rocketmq-4.3-release

理论基石

ACID 和 BASE

https://www.infoq.cn/article/2018/08/rocketmq-4.3-release
https://www.txlcn.org/zh-cn/docs/preface.html

2PC

谈到分布式事务,首先要说的就是 2PC(two phase commit)方案,如下图所示:

2PC 把事务的执行分为两个阶段,第一个阶段即 prepare 阶段,这个阶段实际上就是投票阶段,协调者向参与者确认是否可以共同提交,再得到全部参与者的所有回答后,协调者向所有的参与者发布共同提交或者共同回滚的指令,用以保证事务达到一致性。
2PC 是几乎所有分布式事务算法的基础,后续的分布式事务算法几乎都由此改进而来。

需求样例

这里我们定义一个充值需求,后续我们在各个实现中看看如何为该需求实现分布式事务。

Order 和 Account 分别是独立的一个服务,充值完成后,要分别将订单Order 设置为成功以及增加用户余额。

实现1 Seata

介绍 & 框架

Seata(Fescar) is a distributed transaction solution with high performance and ease of use for microservices architecture.
阿里开源,其特点是用一个事务管理器,来管理每个服务的事务,本质上是 2PC(后文会解释) 的一种实现。
Seata 提供了全局的事务管理器

原理

Fescar官方介绍

Fescar全局锁的理解

代理 SQL 查询,实现事务管理,类似中间件

实现充值需求

用该方案实现需求的话,就是这样的:

Order 和 Account 都接入 Seata 来代理事务

代码示例

比起自己去实现 2PC,Seata 提供了简化方案,代码实例见 :

Seata Samples

实现2 TCC

介绍

TCC(Try-Confirm-Concel) 模型是一种补偿性事务,主要分为 Try:检查、保留资源,Confirm:执行事务,Concel:释放资源三个阶段,如下图所示:

其中,活动管理器记录了全局事务的推进状态以及各子事务的执行状态,负责推进各个子事务共同进行提交或者回滚。同时负责在子事务处理超时后不停重试,重试不成功后转手工处理,用以保证事务的最终一致性。

原理

每个子节点,要实现 TCC 接口,才能被管理。
优点:不依赖 local transaction,可以管理非关系数据库库的服务
缺点:TCC 模式多增加了一个状态,导致在业务开发过程中,复杂度上升,而且协调器与子事务的通信过程增加,状态轮转处理也更为复杂。而且,很多业务是无法补偿的,例如银行卡充值。

实现框架

tx-lcn LCN distributed transaction framework, compatible with dubbo, spring cloud and Motan framework, supports various relational databases https://www.txlcn.org

或者 Seata MT 模式

代码示例

txlcn-demo

实现充值需求

需要把 Oder.done 和 Account 的余额+ 操作都实现 tcc 接口
可以看出,这样真的很麻烦,能用本地事务的还是尽量用本地事务吧

实现3 事务消息

介绍

以购物场景为例,张三购买物品,账户扣款 100 元的同时,需要保证在下游的会员服务中给该账户增加 100 积分。由于数据库私有,所以导致在实际的操作过程中会出现很多问题,比如先发送消息,可能会因为扣款失败导致账户积分无故增加,如果先执行扣款,则有可能因服务宕机,导致积分不能增加,无论是先发消息还是先执行本地事务,都有可能导致出现数据不一致的结果。

事务消息的本质就是为了解决此类问题,解决本地事务执行与消息发送的原子性问题。

实现框架

Apache RocketMQ™ is an open source distributed messaging and streaming data platform.

原理

  1. 事务发起方首先发送 prepare 消息到 MQ。
  2. 在发送 prepare 消息成功后执行本地事务。
  3. 根据本地事务执行结果返回 commit 或者是 rollback。
  4. 如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发,如果是 commit 消息,MQ 将会把这个消息发送给 consumer 端。
  5. 如果执行本地事务过程中,执行端挂掉,或者超时,MQ 将会不停的询问其同组的其它 producer 来获取状态。
  6. Consumer 端的消费成功机制有 MQ 保证。

优点:对异步操作支持友好
缺点:Producer 端要为 RMQ 实现事务查询接口,导致在业务开发过程中,复杂度上升。

代码示例

// TODO

实现充值需求

通过 MQ,来保障 Order 和 Acount 的两个操作要么一起成功,要么一起失败。
注意一个点,假设 Account 的余额+失败了,这里是无法回滚 Order 的操作的,Account 要保证自己能正确处理消息。

实现4 本地消息表

介绍 & 原理

分布式事务=A系统本地事务 + B系统本地事务 + 消息通知;
准备:
A系统维护一张消息表log1,状态为未执行,
B系统维护2张表,
未完成表log2,
已完成表log3,
消息中间件用两个topic,
topic1是A系统通知B要执行任务了,
topic2是B系统通知A已经完成任务了,

  1. 用户在A系统里领取优惠券,并往log1插入一条记录
  2. 由定时任务轮询log1,发消息给B系统
  3. B系统收到消息后,先检查是否在log3中执行过这条消息,没有的话插入log2表,并进行发短信,发送成功后删除log2的记录,插入log3
  4. B系统发消息给A系统
  5. A系统根据id删除这个消息

假设出现网络中断和系统 Crash 等问题时,为了继续执行事务,需要进行重试。重试方式有:

  1. 定时任务恢复事务的执行,
  2. 使用 MQ 来传递消息,MQ可以保证消息被正确消费。

优点:简单
缺点:程序会出现执行到一半的状态,重试则要求每个操作需要实现幂等性

注意:分布式系统实现幂等性的时候,记得使用分布式锁,分布式锁详细介绍见文末参考文章

实现充值需求

通过消息表,把断开的事务继续执行下去。

实现5 考拉的方案

介绍 & 原理

考拉的方案,就是使用本地消息表,但是少了两个重要组件(MQ 和 关系型数据库),写起来还是比较辛苦的。

考拉方案有如下特点:

  1. Order 表承担了消息表功能
  2. 服务之间使用 http 通信,所以碰到问题要依赖定时任务发布补单重试
  3. 没有使用关系型数据库,幂等性的实现比较困难。

实现充值需求

难点:

  • 实现幂等性的要求太高,基本要求所有操作都需要实现幂等性,例如更新余额操作,要高效更新,简单的办法是使用乐观锁,但是要同时兼顾幂等性的话,乐观锁就不够用了。

  • 程序在任一一步断开,都需要重新运行起来,补单程序会很难写(简单的业务还好,复杂业务就会混乱了)

改进建议:

  • 服务直接使用 mq 通信,服务异常需要重试消费。
  • 使用关系型数据库,通过本地事务,可以只程序开始处判断重复,简化幂等性的实现逻辑

实际上就是往上一个实现4上走

总结

我们先对这些实现方案进行一个总结:

基础原理 实现 优势 必要前提
2PC Seata 简单 关系型数据库
2PC TCC 不依赖关系系数据库 实现 TCC 接口
2PC 事务消息 高性能 实现事务检查接口
最终一致性 本地消息表 去中心化 侵入业务,接口需要幂等性

各个方案有自己的优劣,实际使用过程中,我们还是需要根据情况来选择不同事务方案来灵活组合。

例如存在服务模块A 、B、 C。A模块是mysql作为数据源的服务,B模块是基于redis作为数据源的服务,C模块是基于mongo作为数据源的服务。若需要解决他们的事务一致性就需要针对不同的节点采用不同的方案,并且统一协调完成分布式事务的处理。

方案:将A模块采用 Seata 模式、B/C采用TCC模式就能完美解决。

参考文章

RocketMQ 4.3 正式发布,支持分布式事务

Seata

txlcn

分布式事务 CAP 理解论证 解决方案

再有人问你分布式锁,这篇文章扔给他

如何 parse 一个 typescript interface

blog 编程相关 Node posts 编程相关 typescript ast

目标

有如下一个接口定义,我们想把它的结构 parse 出来,知道每个字段的定义和注释,方便我们生成文档

import {IBanner} from './base/IBanner'
import {IProductGroup} from './base/IProductGroup'
/**
* 投资列表页面
*/
export interface IProductList {
/**
* 产品分组
*/
groupList: Array<IProductGroup>
/**
* 产品列表
*/
list: Array<{
a: string,
b: number
}>
/**
* 投资列表页面的banner
*/
banners: Array<IBanner>
}

typescript 提供的 ast

import * as ts from 'typescript'
const {options} = ts.convertCompilerOptionsFromJson({}, '.', 'tsconfig.json')
options.skipLibCheck = true
options.skipDefaultLibCheck = true
let program = ts.createProgram([
'./src/dashboard/interface/IProductService'
], options)
for (let sourceFile of program.getSourceFiles()) {
if (sourceFile.fileName.includes('IProductService.ts')) {
console.log('fileName', sourceFile.fileName)
console.log('statements', sourceFile.statements[1].members[])
}
}

节选一段输出:

{
pos: 38,
end: 276,
flags: 0,
transformFlags: undefined,
parent: undefined,
kind: 235,
jsDoc:
[ NodeObject {
.....
tags: undefined,
comment: '投资列表页面' } ],
decorators: undefined,
modifiers:
[ TokenObject { pos: 38, end: 64, flags: 0, parent: undefined, kind: 84 },
pos: 38,
end: 64 ],
name:
IdentifierObject {
.....
escapedText: 'IProductList' },
typeParameters: undefined,
heritageClauses: undefined,
members:
[ NodeObject {
.....
jsDoc: [Array],
modifiers: undefined,
name: [Object],
questionToken: undefined,
type: [Object] },
....

原始的 ast ,需要自己写程序遍历完整结构。

ts-simple-ast

ts-simple-ast 提供了一些元素获取的方法,例如
getInterface
getProperties

可以快速拿到你要的对象。
不足是,无法识别 import 进来的内容,例如

interfaceFile.getInterface('IBanner')

是无法获取的,因为 IBanner 是引用其他文件的

import {Project} from 'ts-simple-ast'
// initialize
const project = new Project()
project.addSourceFilesFromTsConfig('./tsconfig.json')
const interfaceFile = project.getSourceFile('src/dashboard/interface/IProductService.ts')
console.log('getProperties', interfaceFile.getInterface('IProductList').getProperties())

readts

readts 这个开源包虽然很冷门,但它却更符合我们的需求,它可以 parse 出项目里所有的 class 和 interface 等,而且连 import 进来的对象也帮你 ref 好了。

遗憾的是,匿名的对象定义还是无法 parse。

{
a: string,
b: number
}

用法:

const readts = require('readts');
const lodash = require('lodash');
var parser = new readts.Parser();
// Read configuration used in the project we want to analyze.
var config = parser.parseConfig('tsconfig.json');
// Modify configuration as needed, for example to avoid writing compiler output to disk.
config.options.noEmit = true;
// Parse the project.
var tree = parser.parse(config);
var interfaceList = lodash(tree)
.filter(item => item.interfaceList.length > 0)
.concat()
.map('interfaceList')
.map(item => item[0])
// .filter(item => item.name && item.name.match(/^I.*/))
// .value()
.find(item => item.name === 'IProductList');
console.log('interfaceList', interfaceList);

输出示例:

ClassSpec {
name: 'IProductList',
pos:
{ sourcePath: '/Users/nick/nodePro/klg-app/src/dashboard/interface/IProductService.ts',
firstLine: 6,
lastLine: 22 },
symbol:
SymbolObject {
flags: 64,
escapedName: 'IProductList',
declarations: [ [Object] ],
members:
Map {
'groupList' => [Object],
'list' => [Object],
'banners' => [Object] },
parent:
SymbolObject {
flags: 512,
escapedName: '"/Users/nick/nodePro/klg-app/src/dashboard/interface/IProductService"',
declarations: [Array],
exports: [Object],
valueDeclaration: [Object],
id: 8410 },
documentationComment: [ [Object] ],
id: 8359 },
doc: '投资列表页面',
propertyList:
[ IdentifierSpec {
name: 'groupList',
type: [Object],
optional: false,
pos: [Object],
doc: '产品分组' },
IdentifierSpec {
name: 'list',
type: [Object],
optional: false,
pos: [Object],
doc: '产品列表' },
IdentifierSpec {
name: 'banners',
type: [Object],
optional: false,
pos: [Object],
doc: '投资列表页面的banner' } ] }

AI 考拉 2018 开源汇总(Node 基础架构)

blog 编程相关 Node posts 编程相关

前言

2018 年,考拉开始对现有项目的常用的工具库进行整理,包含日期处理,数字处理,logger 等常用工具,并打包成 npm module,方便各个项目使用。

代码风格

在开发工具库之前,我们统一了编码标准

JavaScript 代码规范

前端 browser 通用
统一使用 eslint-config-klg ,基于 eslint-config-standard 封装
安装说明见文档

Typescript 代码规范

统一使用 tslint-config-klg ,基于 tslint-config-standard 封装

安装说明见文档

脚手架

我们也提供了脚手架 klg-init,来方便大家启动一个新项目。
安装好这个工具后,一键生成项目模板

$ klg-init dest
[klg-init] fetching npm info of klg-init-config
? Please select a boilerplate type (Use arrow keys)
──────────────
❯ module - npm 库项目模板
model - mongoose model 模板 todo
project - JavaScript 后端项目模板 todo
project-ts - Typescript 后端项目模板 todo
admin - 管理后台项目模板 todo

目前支持的模板有:

工具库列表

目前已经发布的工具库有:

  • klg-logger : logger 工具,基于 tracer
  • klg-number : 数字处理,主要解决 node 小数精度问题
  • klg-request-log :http 请求 log 记录,方便排查问题
  • klg-mq : rabbitmq 连接工具
  • klg-mq-koa : 将 mq 和 koa router 无缝结合
  • klg-redlock : 基于 redis 的分布式锁
  • klg-date : 日期处理,基于 moment
  • klg-request : 后端使用的 http 请求工具,基于 superagent
  • klg-retry : 重试工具
  • klg-tracer : 链路追踪工具,该项目实际不能使用,原因见项目内文档

上述项目都可以在我们公司的开源账号找到 https://github.com/kaolalicai?utf8=%E2%9C%93&q=&type=&language=typescript

API 接口与 Typescript Interface

blog 编程相关 Node posts 编程相关

前言

Interface 就是接口,在 typescript 用于类型限定。
在 Node 项目中,我们常用 apidoc 来定义接口文档。
那么,他们俩是否能结合起来呢?

apidoc 的痛点

apidoc Inline Documentation for RESTful web APIs,使用起来非常方便,直接在 api 上添加注释,就可以生成可视化的文档。

但是,在实际业务中,我们有些接口比较复杂,定义返回值是一件比较痛苦的事情,如图:

而且,过了一段时间之后,出于惰性,开发人员会放弃更新 API 文档,导致文档落后于实际。

API Interface

API 未定义的问题

在 js 项目中,api 返回结构一般是无法清晰地看出来的,你得跑一下接口,才知道会返回什么,或者仔细看一遍代码。这种情况下,开发人员维护 API 会面临以下问题:

  • 文档缺失:接口文档缺少对某个字段的定义,不知道该字段作何用处
  • 字段混乱:明明是一个字段,可能在 接口 A 是叫 amount, 在接口 B 却叫 number
  • 重复定义:多个接口中都用到一个叫 Banner 的东西,需要在每个接口文档中定义一遍,稍有改动,需要更改多处

使用 interface 的好处

所以可以尝试使用 typescript 的 interface 来定义 API 的返回值结构,一个接口对应一个 interface。

假设一个 api 是 product info ,定义结构如下:

/**
* 产品明细
*/
export interface IProductInfo {
/**
* 基础利息
*/
baseRate: number
/**
* 加息
*/
bonusRate: number
/**
* 加息提示
*/
bonusRateStr: string
}

另一个 api 是 product list ,可以很方便地进行复用:

/**
* 产品列表
*/
export interface IProductList {
list: Array<IProductInfo>
}

这些接口定义也可以共享给前端(前端也用 typescript 的话)

apidoc + interface

很多时候,你的痛点其实也是其他人的痛点,所以就有了开源项目,通过搜寻,我们可以找到一个 apidoc 的插件 apidoc-plugin-ts,这个插件可以根据 interface 的定义帮你生成 apidoc 需要的 @apiSuccess 内容。
定义一个接口:

filename: ./employers.ts
export interface Employer {
/**
* Employer job title
*/
jobTitle: string;
/**
* Employer personal details
*/
personalDetails: {
name: string;
age: number;
}
}

引入 interface:

@apiInterface (./employers.ts) {Person}

翻译的效果:

@apiSuccess {String} jobTitle Job title
@apiSuccess {Object} personalDetails Empoyer personal details
@apiSuccess {String} personalDetails.name
@apiSuccess {Number} personalDetails.age

还支持数组结构定义呢~
有了这个插件,就可以通过维护 interface 来维护 apidoc 了。

注意:

该插件不支持匿名接口定义数组结构, 如下:

export interface IProductList {
list: Array<{
a: string,
b: string
}>
}

AST 解析代码的时候会拿不到某些属性,会报错

Node.js APM 软件调研报告

blog 编程相关 Node posts 编程相关

前言

最近做 Node 服务的性能优化工作,在 2016年3月,我写过 Node 性能优化 这篇文章, 讲解了常见的 Node 服务缓慢原因,还介绍了 内存泄露 和 cpu profile 的知识。

这一次,是侧重中应用运行信息收集这一块,根据同事 Leo 的介绍,我对一些支持 Node.js 的 APM 软件做了一次调研。

在分别介绍每个软件之前,我要先阐述我使用这些软件的目的:它可以发现慢的 http 请求,并且可以查看当时的 Node 服务状态(包括内存 CPU 占用状态),database 状态(这里是 mongodb),帮助 dev 准确分析出请求处理缓慢的原因。

下文的体验报告将以此目标为标准进行评价。

软件列表

本次体验的 APM 软件有:

名称 介绍 开源/收费情况
atatus 支持多种语言,包括前端 不开源,收费
newrelic 支持多种语言,包括前端 开源,收费
keymetrics pm2, Node 应用管理器 开源,限额免费
Pandora.js 阿里出品,Node 应用管理器 开源,免费(自建服务)
alinode Node 底层的监控 开源,免费(阿里云上配置实例)
statsd + graphite + grafana 监控三件套,开发自由度高 开源,自建服务

atatus

atatus 提供了 npm 工具包 atatus-node 用于监控数据收集,具体配置见官方文档

使用报告:不是 100 % 的采样率,没有收集到官网宣传的各种维度的信息,使用 Koa 框架,基本上只能看到总的 http response time。

newrelic

网址 https://newrelic.com/ ,和 atatus 基本相同,也提供 npm 工具包 newrelic。

keymetrics

偏向应用生命周期管理,可以看到 Node 服务的 cpu 内存等占用情况,有错误(crash)收集功能,国内访问很卡。

Pandora.js

Pandora.js 是一个 Node.js 应用监控管理器。它集成了多种类型的能力诸如:监控、链路追踪、调试、进程管理等等。

它是个很有趣的东西,总结来说是它很强大但是还不够成熟,包括 Pandora 自身和整个 Node 生态都不成熟。

应用管理

可以对标 PM2,不赘述。

应用度量

有操作系统指标,包含 Load、CPU、内存、磁盘、网络、TCP 等各种指标;
有 Node.js 指标(内存占用);
还可以自定义指标,类似 statsd 做的事情,但是后续的存储和展示嘛,非常简单,没有 statsd + graphite + grafana 好用。

链路追踪

这个是 Pandora 提供的比较“先进”的功能,链路追踪理论上可以分析出一个 http 哪里耗时多,例如一个 get 请求,做的事情有,查询 db ,调用 第三方服务等。链路追踪会记录每一个环节的耗时:

链路追踪的实现依赖了 Node 的一个实验性特性 async_hooks,因为是新特性,目前还有很多问题没有解决,例如 mongoose 和 superagent 两个工具包的 Promise 实现方式会导致 async_hooks,具体可以见我在 Pandora 提的 issue ,也就是说,如果你刚好用了这两个框架,Pandora 链路追踪就失效了。

alinode

Node.js 性能平台 https://cn.aliyun.com/product/nodejs ,alinode 是在 node runtime 层面做的应用信息收集,而不是上述各个框架在应用层进行信息收集,所以 alinode 可以监控到 进程数据,堆快照、堆时间线、CPU Profile、GC Trace 等非常底层的信息,如果你的应用性能瓶颈在 Node 服务本身,使用该工具会有很大的帮助,如果你的应用性能瓶颈在 DB,那应该是用 DB 监控工具。

statsd + graphite + grafana

这个三件套有意思的是,监控指标是开发自己定义的,考拉用这三件套监控请求处理时间,记录每个请求的处理时间
这样在 grafana 上可以快速看出当前应用的请求量与趋势,已经快速分析哪些接口缓慢。

接口访问量的趋势变化:

处理最慢的接口:

这套工具的优势在于 grafana, 提供了非常直观的图表。
这套工具的搭建方法我之前也写过文章,可以看看 快速搭建一个监控服务

总结

回到我们的目标“帮助 dev 准确分析出请求处理缓慢的原因”,
理论上最贴合这个目标的是 Pandora.js ,但是我们还需要再等等,等它成熟。
最简单而强大的是 statsd + graphite + grafana 三件套,不用考虑 atatus newrelic 等。
如果你使用 Node 做高并发服务,alinode 将会很有用。

class extends function

我写的东西 编程相关 Node posts

注意:本文示例代码是 typescript

klg-logger 的封装

最近在做一个共用的 logger 包 klg-logger ,在调研了一圈后,决定基于 tracer 这个包来做封装,tracer 足够简单,也具有拓展性。因为公司使用的工具包都是使用 class 风格的, 所以第一版的 klg-logger 长这样:

import {console, Tracer} from 'tracer'
export class Logger {
private logger: any
constructor (config?: Tracer.LoggerConfig) {
this.logger = console(config)
}
log (msg: any, ...params): void {
this.logger.log.apply(this, arguments)
}
error (msg: any, ...params): void {
this.logger.error.apply(this, arguments)
}
// ...
}

使用方式:

const logger = new Logger({})
logger.log('hello world')

就是使用一个 Logger class 把 tracer 封装了一次。

this 域问题

过了一段时间,klg-logger 出现了一个错误:

TypeError: Cannot read property 'logger' of undefined

经排查,是 this 作用域的问题,调用 logger 的代码长这样:

this.connect().then(function () {
// code
}).catch(logger.error)

logger.error 这个 function 是作为 参数直接传递个 catch, 那么 logger.error 内部的 this 将会指向 catch 对象,而不再是 logger 对象。
我们可以改变写法来避免这个错误:

this.connect().then(function () {
// empty
}).catch(function (err) {
logger.error(err.message)
})

但如果要更彻底地解决这个问题,还是要改变一下 logger.error 的实现才行。

class extends function

考虑到这个包 klg-logger 已经在多个系统中使用,为了保持兼容,我们要继续保持 class 风格,所以我们要把上文提到的“封装”的实现改为继承实现。

import {console} from 'tracer'
export class Logger extends console {
// Error:(3, 29) TS2507: Type '(config?: LoggerConfig) => Logger' is not a constructor function type.
}

直接继承 tracer 的 console 是会报错的,因为 console function 没有 construction。
在 stackoverflow 上可以找到解决方案:https://stackoverflow.com/questions/36871299/how-to-extend-function-with-es6-classes

通过一个 Function 把 console 包装成一个具有 construction 的对象,然后在 construction 里面 return console 实例即可

class Logger extends Function implements Tracer.Logger {
constructor (config?: LoggerConfig) {
super()
Object.setPrototypeOf(console, Logger.prototype)
const instance = console(config as any) as Logger
instance.err = instance.error
return instance
}
// 下列方法都会被覆盖,只做声明用, 无需实现
debug (...args: any[]): Tracer.LogOutput {
return undefined
}
error (...args: any[]): Tracer.LogOutput {
return undefined
}
/// ......
}

经测试,最后的写法和功能都是原版保持了一致。

知识库

blog 编程相关 Node 编程相关 posts

方法论

编程的精进之法 任务列表法+PDCA
像机器一样思考 input -> process -> output
然而培训并没有什么用 最后一块吃饱的饼+练习
软件开发工作的第一现场 软件开发工作的第一现场,在语言里。使用 uml 这类的工具统一语言可以极大地提升沟通效率
聊聊clean code
clean-code-javascript
由屎色自行车棚引发的思考 避免讨论无关紧要但会引起大量争论的问题
X-Y PROBLEM 问问题的时候没有去问怎么解决问题X,而是去问解决方案Y应该怎么去实现和操作。

编程知识与概念

浅谈命令查询职责分离(CQRS)模式
Unit of Work
Thinking In Design Pattern——Unit Of Work(工作单元)模式探索 – 木宛城主 – 博客园

微服务与RPC
谈谈后端业务系统的微服务化改造
如何选择开源许可证?

深度学习

深度 | 机器学习敲门砖:任何人都能看懂的TensorFlow介绍
面向普通开发者的机器学习入门
深度学习系列视频 bilibili
深度学习系列视频 中文 YouTube
深度学习系列视频 YouTube

Web 框架

Spring Boot
Koa
Express
Egg.js

数据库

用Redis构建分布式锁

算法与数据结构

基于用户投票的排名算法
跳跃表Skip List的原理和实现 多层链表

操作系统

怎样理解阻塞非阻塞与同步异步的区别

网络 Http

一个Option请求引发的深度解析
总结 XSS 与 CSRF 两种跨站攻击 XSS: 注入脚本,收集用户信息, CSRF:冒充用户之手
一文读懂 HTTP/2 特性
什么是JS跨域访问?

Node 常用库

Building a typescript library
Pandora.js 应用管理器
PM2 应用管理器
lodash 集合库
moment 日期库
mocha 测试库
autod Auto generate dependencies and devDependencies by parse the project file.
玩转npm

运维

shadowsocks server 搭建

js 基础知识

Node.js 8 Node 8 新特性介绍
JavaScript中的稀疏数组与密集数组
js 异常处理最佳实践 详细列出了 js 异常处理的发展历史,也补充了非常多的基础知识
Babel是如何读懂JS代码的 三个阶段:

  1. 解析:将代码字符串解析成抽象语法树
  2. 变换:对抽象语法树进行变换操作
  3. 再建:根据变换后的抽象语法树再生成代码字符串

前端高手必备:详解 JavaScript 柯里化

做个 SSO 系统

我写的东西 编程相关 Node posts 编程相关

应用场景

最近公司要统一内部管理系统的用户与权限控制,于是考虑开发一个 SSO 单点登陆系统,考虑到内部系统都是 web 服务和开发时间有限,而且内部系统都是 Node.js 写的, 使用 Koa 框架,最终决定使用最简单的解决方案。

实现方案

原理

kwt + cookie
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,这里将作为用户的登陆标识。
用户的登陆流程是这样的:

  1. 打开接入方系统,假设域名是 aaa.abc.com
  2. 校验登陆状态,查看 cookie 里是否存在 jwt 并解密校验,如果未登录,跳转到 SSO 登陆页面,SSO 域名是 sso.abc.com
  3. 在 SSO 登陆页面填写用户名和密码(未注册的先注册),登陆后 SSO 系统把 jwt 写入 cookie,cookie 的有效域名为 abc.com,然后跳跳转回原系统(跳过来的时候在 url 里带上原系统地址)
  4. 返回原系统,校验登陆状态,登陆完成

sso 系统实现

基本的用户管理功能,有:

  • 部门管理
  • 权限管理
  • 注册功能
  • 登陆功能
  • 找回密码

重点功能是登陆,需要在登陆时将 jwt 写入 cookie,上文原理中有要求各个系统要有个共同的主域名 abc.com,各个系统不同域的话,那就只能把 jwt 在 url 中返回给原系统了,原系统自己把 jwt 写入 cookie,来看看这部分的代码:

const jwtdata = {
username: user.username,
email: user.email,
role: user.role
}
// 生成 jwt
const token = jwt.sign(jwtdata, Const.TOKEN_SECRET, {expiresIn: '365d'})
// 写入 cookie
ctx.cookies.set(
'token',
token,
{
domain: '.abc.com', // 写cookie所在的域名
path: '/', // 写cookie所在的路径
maxAge: 30 * 60 * 1000, // cookie有效时长
httpOnly: false, // 是否只用于http请求中获取
overwrite: true // 是否允许重写
}
)

注意用于加密 jwt 的密匙,内部系统的话可以直接用一个密匙加密,然后接入方都用这个密匙解密就可以了。
如果安全要求高一点,那就是 sso 系统用公钥(保密)加密 jwt,然后接入方使用私钥(公开)解密。

接入方实现

考虑到接入方有多个,所以考虑直接写个 Koa 中间件给各个接入方调用是最简单的,将此中间件作为 npm 包发布,接入方使用该中间件监听所有请求,完成对 jwt 的解析和校验。

监控请求

const Koa = require('koa')
const app = new Koa()
const auth = require('sso-auth')
app.use(auth.validate({
unless: [//register/, //login/, //message/],
errHandle: async function (ctx) {
console.log('授权错误')
// 跳转到SSO的登陆页面或者 返回url给前端跳转
ctx.body = {
code: 3,
message: '授权错误',
url: 'https://sso.abc.com'
}
ctx.status = 200
}
}))

中间件 sso-auth 的内部实现

// 从 cookie 取出 jwt
// 校验 jwt 是否符合

假设接入方是其他编程语言的系统,那就对该语言实现对应的上述校验逻辑即可。

总结

jwt 作为 token 可以附带额外的信息
使用 cookie 存储 token ,同域名的系统接入非常方便
使用中间件方便了接入方校验 token

Node.js 并发模型

我写的东西 编程相关 Node posts 编程相关

前言

首先,什么是并发?

并发是指程序可以同时处理多个任务,是一个web 服务必备的能力。

自从 Nodejs 出现后,js 开始涉及后端领域,因为其出色的并发模型,被很多企业用来处理高并发请求,例如淘宝已经大量使用 node 处理中间层业务。

接下来本文就分析一下 js 的并发模型,来理解 node 相对于其他语言的优势以及其最合适的应用场景

tips:并发和并行区别

异步IO

什么是异步IO?
异步 IO 具体是如何实现的呢?
异步和同步有什么区别呢?
异步就不阻塞吗?IO 阻塞又是什么概念呢?
带着这些问题,我们慢慢分析。

IO 模型

《UNIX网络编程:卷一》第六章——I/O复用。书中向我们提及了5种类UNIX下可用的I/O模型:

  • 阻塞式I/O;
  • 非阻塞式I/O;
  • I/O复用(select,poll,epoll…);
  • 信号驱动式I/O(SIGIO);
  • 异步I/O(POSIX的aio_系列函数);

总结一下阻塞,非阻塞,同步,异步

  • 阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待;
  • 同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。

说人话

上面的解释太复杂我看不懂怎么办?我们把上文说到 IO 代入到生活的场景中,考虑到我们公司很多人喜欢买一点点饮料,就以买饮料为例,将四种常见 IO 模型转换为对应的买饮料流程。下面是一下设定:

  • 把买一杯一点点的流程简化为两步:下单制作和拿一点点回公司
  • 公司员工 === 线程
  • 下单制作 === 发起IO请求
  • 拿一点点回公司 === 读取数据

异步 IO 的特点就是把 IO 处理的事情都交给了操作系统(美团外卖),这样线程就不会被 IO 阻塞,可以继续处理其他请求

Node 的异步IO

Node.js 的异步 IO 由 Libuv 这个库提供实现,Libuv 是 Node.js 关键的一个组成部分,它为上层的 Node.js 提供了统一的 API 调用,使其不用考虑平台差距,隐藏了底层实现。

可以看出,它提供了非阻塞的网络 I/O,异步文件系统访问等功能,而且右下角居然还有个线程池,实际上 Libuv 收到的 IO 请求是同个多线程来实现的, 看来 Node 只是在程序层面单线程而已

事件循环

任务队列

先看看 Node.js 结构

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎(callback 处理结果)。

(4)V8引擎再将结果返回给用户。

异步操作都会被压入任务队列,当任务队列为空的时候,程序退出。

示例代码

总结

Libuv 使用异步IO + 线程池实现的事件循环处理机制提供的高效的 IO 处理,是 Node 能承担高并发请求的主要原因

参考文章与书籍

《深入浅出Node.js》
《Unix 网络编程》
《七周七并发模型》
并发与并行的区别
JavaScript 运行机制详解:再谈Event Loop
怎样理解阻塞非阻塞与同步异步的区别? – 大姚的回答 – 知乎
Node.js 探秘:初识单线程的 Node.js
Linux IO模式及 select、poll、epoll详解

UML

我有哪些提升编程工作效率的方法

1. 列出每日TODO

最简单也是最有效的方法,每天早上整理好今天待办事项,按照紧急和重要两个指标划分四象限,优先处理重要的事情,紧急但不重要的可次之。
这个做法的好处是做事会有计划有条理。

2. 做好工作笔记

使用云笔记记录每个 task 的相关信息,整理素材。云笔记的好处是不怕丢,手机也能方便查询,也方便日后回顾,特别是年终的时候 看着一个个 task 记录,就知道今年做了哪些事情。

3. 坚决地自动化

如果是重复简单的工作,就尽量使用程序脚本解决,可能第一次做自动化,写脚步的时间比直接做还慢,但是基础的脚本都差不多,需要用到自动化的一般就是那几类,有了积累之后,下次使用就方便很多了。珍惜自己的时间,重视自己的价值,工程师不应该把时间浪费在简单重复的事情上。

4. 熟练使用工具,节省时间和精力

人一天的精力有限,而且人能连续集中精力时间也不长,番茄工作法认为人平均能集中精力的时长为 25 分钟,并以此为周期,每个周期之间需要休息 5 分钟。所以在编码时,需要熟悉 IDE 的各种快捷功能,提高工作。

举个例子:使用 IDE 提供的 debug 功能,可以在断点处之前查看上下文变量,还可以修改上下文,也可以给短点设置变量,debug 效率非常高。

使用 console.log 来 debug 同样可以找出问题,但需要浪费时间在输入 console.log 和重试上面。即使只有几秒钟,毕竟一个周期也只有 25 分钟,而且我自己感觉一天能高效工作的时间也就 3 小时左右 LOL。

5. 先思考,再编码,多画图

不要把实现方案想个大概就动工,不然写到一半才发现某些地方没考虑完整,又得推翻部分设计,返工浪费的时间其更多。
处理简单的问题,可以脑子里过一遍流程,把可能影响的地方都一一列出来,再整理好 TODO ,编码的时候其实就只需要关注把 TODO 翻译成代码即可。
如果问题比较复杂,那就把画出流程图或者时序图,可以帮你有效地整理流程。画图还有另一个好处:当做问题,日后接收的人也轻松。这里推荐使用 plantUML 来画各种流程图,类似 markdown 只需要写好文本,渲染后就是标准的 UML 图了。