GithubHelp home page GithubHelp logo

cdtf's Introduction

复杂分布式事务处理框架

CDTF(Complex Distributed Transaction Framework)

背景/业务痛点

在日常CRUD开发过程中,我们往往很容易遇到复杂分布式长事务,以某一个运营处罚业务为例,涉及到的下游业务接口调用众多(封号接口内部涉及到的下游RPC调用多达39次),逻辑复杂且耗时较长,执行过程中很容易遇到失败,这对于我们接口的一致性及可靠性都造成了极大的挑战。我们目前通常的做法都是直接选择忽略错误,这就导致我们目前很多的运营相关逻辑实现都是不够可靠的,从而经常性的会发生运营接口中的逻辑部分成功部分失败,不时就会有外部的投诉和反馈,最终只能依靠人工处理解决。

相信大家在日常开发过程中都会不时地遇到如下几个很纠结的点:

  1. 一段非常长的业务逻辑,涉及很多下游业务接口的调用,如何保证整个业务逻辑中的每一部分都尽最大努力的可靠执行,以及如何在想要的特定逻辑范围内保证业务逻辑的一致性和原子性?
  2. 中间某个接口有失败,事情做了一半,是直接忽略还是退出重试?
  3. 有失败了要不要回退?失败后回退又失败了怎么办?
  4. 业务并发情况下导致的事务隔离性问题。比如封号和解封的请求并发执行。
  5. 业务逻辑里有非幂等接口,丢MQ里无脑重试会不会导致重入安全的问题?
  6. 业务中的主要逻辑和次要逻辑以及次要逻辑间如何做到互不影响的可靠完成?(A B C D四个在MQ里面异步执行,如果A执行失败,那就一直会卡在A这里重试,导致B C D都没办法执行)
  7. 逻辑没办法纯异步执行,需要同步返回结果给上游,如何兼顾逻辑异步可靠执行和同步调用,并尽最大努力保证事务性。

解决上述问题往往需要额外的付出很多的开发成本,因此很多时候当业务没那么重要或对可靠性要求不高时,我们往往会选择忽视上面的这些问题。因此我们亟需要一个通用的框架/开发模式来低成本地解决上面的问题。

分布式事务

这部分内容属于老生常谈了,网上的内容很多,但大都千篇一律,接下来会讲一下个人对这块的一些理解。

分布式事务模型

2PC

分布式事务最基本的**,是面向数据库层面的分布式事务模型,更偏向于理论化,对于各种异常处理不足,且仅针对于资源方为数据库的场景(RM主要管理数据库的单机事务),不适用于实际微服务业务场景。MySql单机及跨机事务支持2PC/XA协议的实现。标准的2PC模型协调器TC有状态,事务执行状态不落盘,宕机后无法恢复,存在单点问题)

TCC

基本**和2PC一样,但更偏向微服务业务层面,可以理解为是2PC在微服务实际业务层面的一个应用,与传统2PC不同,TCC一阶段由业务方APP自身发起,在事务执行前会向TC发起请求开启全局事务,TC负责将事务执行状态落盘。而2PC整个流程都由TC发起执行,业务方只是被动的参与。TCC的TC本身无状态,背后依赖分布式存储落地事务执行状态,不存在2PC的TC单点问题,同时得益于无状态TC可以在failover后保证CC阶段的Trybest可靠执行,支持宕机后事务故障恢复。

SAGA

中文译为长篇记事,简单概括就是一个一个执行,如果有问题就一个一个回退,用这种方式来保证原子性和一致性,缺点就是串行化的执行性能较差,事务中间态暴露时间较长。

TryBest

尽最大努力执行,适用于逻辑本身幂等且不涉及回滚的场景,不断重试到成功就好。

AT

AT 是 Auto Transaction 的首字母缩写,主要用于解决跨多个 SQL 数据库的数据一致性问题。它是古老的 2PC/XA 事务在近代演化的结果。AT 将 SQL 语句或事务看作一个分支,并自动对其进行解析,求出反 SQL。在一阶段,执行原 SQL 并保存反 SQL;在二阶段,如果成功,则删除反 SQL,失败,则执行反 SQL。该模式对业务有一定侵入,且仅适用于资源为MySql的场景。需要业务替换SQL Driver。同时它对 SQL 语法的支持有限,只能对一些简单的 SQL 求其反 SQL。

事务消息

其实也是两阶段提交的**,和TCC非常像,或者可以说就是一回事。流程上都是先请求TC Prepare(Begin)开启事务,再执行本地事务逻辑(Try),再决定Commit(Confirm)还是Rollback(Cancel) 。可以看到流程基本一毛一样,个人认为,其实事务消息就是TCC在MQ场景下的一个应用。(RocketMq内部实现基于Half Message**)。

这么多事务模型眼花缭乱,但其实2PC、TCC、AT、事务消息本质上都是以2PC作为基础**。传统的2PC主要用于数据库多机事务层面,AT在其方案上进行了改造,但适用范围仍有限。TCC和事务消息分别是该**在微服务以及MQ场景下的实际应用,RM的类型不再局限于DB单机事务而是可以拓展开来。

TCC和SAGA的对比

TCC比较使用于业务本身能够执行预留相关动作的场景,适用范围相对较小且业务改造和侵入成本都比较高。而SAGA适用范围则很广,同时也更好理解,下游业务资源方几乎无需任何改造就可以嵌入SAGA框架执行,契合的业务场景更多,业务改造成本和侵入度都比较低。相对来说,TCC更适合短事务场景,且通常子事务间没有时序要求。SAGA则更适合长事务且子事务对时序有要求的场景。

简单分布式事务&复杂分布式事务

简单分布式事务:一组需要保证原子性的分布式资源操作的组合,是一个最细粒度的分布式事务单元,同时RM必须能够直接接触管理到底层存储资源,比如RM必须是直接管理对于MySql的操作。例如创建订单,扣款,扣库存这三个动作组成了一个简单分布式事务,其中每个动作都是直接对于DB的操作。再例如,向DB1写入扣分记录,在DB2中写入扣分后的分数,这两个动作就构成了一个简单分布式事务。

复杂分布式事务:RM通常无法直接接触到资源本身(DB)而是隔着多次RPC,又或是资源方根本不涉及传统类型的存储等。同时复杂还体现在复杂分布式事务是由多个分支事务单元的级联组合构成,分支事务间相互独立,整体构成一个多叉树结构。根节点事务与叶子节点事务的执行保证一致性及原子性,叶节点内部各自保证事务属性,叶节点之间并不保证ACID,相互独立执行。举个例子,调接口设置封禁状态位,调接口通知搜索禁搜,调接口添加封禁记录,调接口发通知(无法直接接触到底层存储)。这种就是复杂分布式事务,与底层存储间隔着若干次RPC,无法直接接触到。

比较来看,简单分布式事务是一个单一的分布式事务单元,同时对RM资源方类型要求比较严苛。而复杂分布式事务是由多个分布式事务单元级联组成,同时没有对RM资源方的任何限制。由于资源方类型要求的不同。简单分布式事务场景的适用场景很很有限,使用条件较为严苛,但是能够在大部分DB类型下可靠的保证事务ACID特性。复杂分布式事务没有任何适用场景的限制,但是在极端网络环境下只能够尽最大努力保证事务性。

业内分布式事务框架分析

业内现有的一些框架Seata,DTM,以及部门内的OddEyed,主要针对的都是简单分布式事务场景,对复杂分布式事务的场景适用度不高,且业务侵入极大,RM的抽象较为单一且通常需要能够直接接触到底层存储资源,并且需要侵入到业务侧DB单机事务的层面(RM管理的对象多为关系型数据库单机事务)。

  1. 阿里Seata,主要针对简单分布式事务场景,Java生态,业务侵入高,RM需要部署到下游业务资源侧,且需要直接接管业务DB单机事务,对业务侵入度极高。支持复杂分布式事务场景(混合模式),支持TCC,SAGA,AT等多种模式。
  2. DTM,Go开源项目,大致情况与Seata类似,主要针对简单分布式事务场景。其中很多基础特性需要RM侵入到DB单机事务这个层面才能够实现(譬如需要在对应业务DB内部建一张用于去重和记录本地事务状态的表,同时需要对业务的每一个DB操作附加框架层的sql逻辑,以单机事务包裹),对业务侵入极大,适用范围也不够广(并不是所有业务场景都允许你侵入和改造),尤其更是很难匹配我们的业务使用场景。

上面两种的特点主要是,对业务侵入大,改造成本高,对于资源方依赖的DB类型有要求(必须得是支持单机事务的DB类型Leveldb,RocksDB或者自研NoSql这种不就行了),适用范围有限。但能够在资源方满足一定条件的情况下很好的解决NPC问题带来的诸如事务乱序,空回滚,事务悬挂等问题。整体来看对于我们的业务场景适配度极低,甚至可以说完全没办法用,很多框架要求的改造我们都无法做到,同时对于业务的侵入会使得开发成本极大增加。此外两者都无隔离性保证,业务自行保证。

CDTF

概况

CDTF对复杂分布式事务进行了结构化拆分,拆分后整体分为两个阶段,一阶段由一个主分支事务(根节点)构成,二阶段由多个子分支事务(叶节点)级联组成,分支事务内部由多个子事务构成。一阶段分支事务和二阶段分支事务间基于事务消息模型保证一致性,分支事务内部基于SAGA事务模型保证其事务属性。因此,整体来看CDTF=事务消息+变种SAGA + Try-Best。

CDTF框架中主要涉及以下几个角色:

  • TC(Transaction Coordinator) :负责分布式事务的调度,独立部署。(基于一个支持延时消息的MQ就能实现一个TC,比较成熟的实现可以基于RocketMQueue,RocketMQ)
  • TM(Transaction Manager):部署在业务侧本地,类似于sdk,是分布式事务的实际执行者,负责响应TC的调度并与RM,状态机和分布式锁交互保证事务的ACID属性
  • RM(Resource Manager):部署在业务侧本地,负责管理事务资源(资源在CDTF中被抽象成了一个一个的函数)
  • State Machine:分布式状态机,负责落地事务的执行状态及上下文。其实就是个Mysql,当然也可以有其他的实现比如(Redis,Mongodb,KV)
  • Distributed Lock:分布式锁,负责保证事务的隔离性。可以基于Redis,etcd,ttlkv等实现。实现需要保证该分布式锁支持XID维度可重入,且锁只能被相同XID的持有者释放。

框架特性:

  1. 多分支事务结构,由一个主分支事务和若干个从分支事务组成全局事务。
  2. 分支事务节点按多叉树形式编排,根节点为主分支事务,叶节点为从分支事务。
  3. 分支事务内部基于变种的SAGA事务模型(多了个Check),子事务间串行执行,通过状态机保存事务执行上下文信息及子事务执行状态。
  4. 一阶段主分支可嵌入同步调用流程,可以同步返回结果给到上游,二阶段从分支为纯异步执行。
  5. 主从分支之间通过事务消息模型保证最终一致性。
  6. 分支事务内部基于SAGA尽最大努力保证事务ACID属性。
  7. 分支事务相互之间并不保证一致性/原子性,及允许部分分支事务成功,部分分支事务失败。
  8. 框架主要针对于复杂分布式事务场景,但对于简单分布式事务场景仍然适用。框架虽然基于SAGA及事务消息模型,但对于TCC的业务场景也同样能够work。
  9. 框架提供有限的重入安全保证(尽最大努力防止重入),还是建议资源能够自行保证幂等,但如果由于一些情况没办法保证幂等,可以依赖于框架的幂等保证。简单说就是框架的幂等性能够屏蔽大部分异常场景对 RM 幂等的依赖,只用 RM 幂待兜底一些极端的场景。
  10. 在RM层面业务方需要提供Do, Undo, Check三个操作逻辑,Do用于执行正向逻辑,Undo用于回滚,Check用于事后对帐及事中有限的重入保护检查。
  11. 支持Retry Backoff。
  12. 框架没有提供事务乱序,空回滚和悬挂的通用解决方案,需要回归业务本身根据情况进行解决。(后面解释)

整体架构

整体的架构图如下:

descript

事务模型抽象如下:

descript

业务案例

上面的内容还是有点抽象,举个具体的业务案例吧。考虑一个虚构的复杂分布式事务demo,这个也是我们平时业务中经常会遇到的长事务类型。

某运营管控接口,需对账号进行扣分,能力限制及相关账号属性的修改。并需要同步返回扣分记录ID给到上游。整体业务逻辑如下所示:

  1. 扣分。先生成扣分记录ID,再扣除账号运营分
  2. 生成一条默认文本违规记录,并使用该记录作为证据关联处罚账号的相关能力
  3. 通知搜索进行账号屏蔽
  4. 添加一条操作记录
  5. 修改账号的昵称到"Invalid Account",同时在修改前需要保存旧的昵称,便于解除管控时进行昵称的恢复。(获取当前名称,保存当前名称,修改名称到"Invalid Account")
  6. 扣除账号相关权益的quota值
  7. 发站内信通知用户
  • 针对上面这个业务,我们现在通常的做法是直接同步顺序去写:

伪代码:

int AcctControl(req, resp) {
    // 1. 扣分
    // 生成扣分记录ID
    int ret = AddScoreDeductionRecord(req, record_id);
    if (ret) return ret;
    resp.set_record_id(record_id);
    // 计算下扣除后的目标分数
    ret = GetOperScore(bizuin, score);
    // 这里失败直接退出,但是扣分记录已经生成,造成扣分记录和实际扣分不一致
    if (ret) return ret; 
    score -= 10;
    // 将分数写入存储
    ret = SetOperScore(score);
    if (ret) return ret;
    
    // 2. 生成一条文本违规记录
    // 并使用该记录作为证据关联处罚账号的相关能力
    ret = AddIllegalRecord(req, illegal_record_id);
    if (ret == 0) {
        ret = LockAcctFunction(bizuin);
        if (ret) {
            // 这里失败了咋办。。
            // 处罚可能因为反误判等原因失败,
            // 就这样直接退出会导致平白无故生成了一条违规记录。
        }
    } else {
        // 这里失败了咋办。。先简单搞,不算关键逻辑先放过吧。。后面再来优化
    }
        
    // 3. 通知搜索进行账号屏蔽
    ret = NotifyMMSearch(bizuin); //失败就算了
    // 4. 添加一条操作记录
    ret = AddLog(); //失败就算了
    // 5. 修改账号的昵称
    ret = GetNickName(bizuin, old_nickname); // 失败就算了
    if(ret == 0) {
        ret = SaveNickName(bizuin, old_nickname); // 失败就算了
        if (ret == 0) {
            ret = SetNickname(bizuin, "Invalid Account"); // 失败就算了
        }
    }
    // 6. 扣除账号相关权益的quota值
    ret = DecreaseQuota(bizuin); // 失败就算了 放他一马
    // 7. 发站内信通知用户 
    ret = SendNotification(bizuin); // 失败就算了
}

**存在的问题:**这个应该不用多说,这种实现方式基本上哪哪都是问题,执行过程中的失败全部选择忽略,执行了一半的无法进行回滚,上游重试后又会有重入安全的问题。内部接口就算了,重要业务极不推荐这么写。

  • 接下来我们升级一下,区分主次逻辑,把所以次要逻辑都丢进MQ/RocketMQ异步执行,依赖MQ/RocketMQ的重试保证可靠,同时该回滚的部分加上失败后的回滚逻辑。

伪代码:

int AcctControl(req, resp) {
    // 1. 扣分
    // 生成扣分记录ID
    int ret = AddScoreDeductionRecord(req, record_id);
    if (ret) return ret;
    resp.set_record_id(record_id);
    // 计算下扣除后的目标分数
    ret = GetOperScore(bizuin, score);
    // 这里失败直接退出,但是扣分记录已经生成,造成扣分记录和实际扣分不一致
    if (ret) {
        // 这里回滚又失败了咋办
        ret = DelScoreDeductionRecord(req, record_id);
        return ret;
    } 
    score -= 10;
    // 将分数写入存储
    ret = SetOperScore(score);
    if (ret) {
        // 这里回滚又失败了咋办
        ret = DelScoreDeductionRecord(req, record_id);
        return ret;
    }
    // 次要逻辑直接全丢MQ,失败了靠MQ重试
    return AddToMQ();    // 这里失败了或者crash了怎么办?次要逻辑全都没了。。
    // 再试想 如果上面的逻辑实际都执行成功,但由于网络原因,调用端感知到超时,进行重试,就会造成重复扣分。
}

int ProcessMQMessage(req){
    // 2. 生成一条默认文本违规记录,并使用该记录作为证据关联处罚账号的相关能力
    ret = AddIllegalRecord(req, illegal_record_id);
    if (ret == 0) {
        ret = LockAcctFunction(bizuin);
        if (ret) {
            DeleteIllegalRecord(illegal_record_id);// 这里回滚又失败了咋办,MQ重试又会产生一条记录。。。
            return RES_ERROR;
        }
    } else {
        return RES_ERROR;// 如果是超时,可能记录已经生成了,这里重试又会产生一条记录。
    }
    
    // 3. 通知搜索进行账号屏蔽
    ret = NotifyMMSearch(bizuin);
    if (ret) {
        return RES_ERROR; // 假如这里一致都没重试成功,剩下的逻辑都没办法执行了
    }
    // 4. 添加一条操作记录
    ret = AddOpLog();
    if (ret)
    // 5. 修改账号的昵称
    ret = GetNickName(bizuin, old_nickname);
    if(ret == 0) {
        ret = SaveNickName(bizuin, old_nickname);
        if (ret) return RES_ERROR;
        if (ret == 0) {
            ret = SetNickname(bizuin, "Invalid Account");
            if (ret) return RES_ERROR;
            // 如果Nickname Set完成后任务Crash,重新拉起后,会导致保存的就nickname变成修改后的值,导致原始昵称丢失。
    }
    // 6 . 扣除账号相关权益的quota值
    ret = DecreaseQuota(bizuin);
    if (ret) return ret;
    // 7. 发站内信通知用户 
    ret = SendNotification(bizuin);
    if (ret) return ret;
}

存在的问题:

这种实现相较第一种已经可靠了许多但仍然存在以下几个问题:

  1. 提交MQ/RocketMQ消息可能失败,导致主从不一致。
  2. 事务中存在非幂等接口,重试仍然是不安全的
  3. 重试逻辑相互影响,导致很多非幂等接口已经执行完了但又被重入了
  4. 回滚又失败了无法处理,没办法可靠的进行回滚,无法保证一致性和原子性
  5. 没有进行子事务/逻辑拆分导致在无非幂等接口的情况下,重试仍然会导致逻辑BUG(例如改名接口)。
  6. 无法从某个断点或者中间态进行重试。
  7. 在中间位置重试最终失败会导致接下来的逻辑都无法执行。
  8. 业务没有隔离性,如果管控的同时来了解除管控,则该账号会处于半封半解的状态。
  9. 如果逻辑执行超时,MQ重试,导致同时两个任务在跑。
  10. 实际执行成功,但上游感知到超时,重试导致逻辑重复执行。
  • 使用RocketMQ事务消息

伪代码:

int AcctControl(req, resp) {
    int ret = RocketMQueue_api.PublishTxMsg(xid);
    // 1. 扣分
    // 生成扣分记录ID
    ret = AddScoreDeductionRecord(req, record_id);
    if (ret) return ret;
    resp.set_record_id(record_id);
    // 计算下扣除后的目标分数
    ret = GetOperScore(bizuin, score);
    // 这里失败直接退出,但是扣分记录已经生成,造成扣分记录和实际扣分不一致
    if (ret) {
        // 这里回滚又失败了,可以依靠RocketMQ反查兜底
        ret = DelScoreDeductionRecord(req, record_id);
        ret = RocketMQueue_api.Rollback(xid);
        return ret;
    } 
    score -= 10;
    // 将分数写入存储
    ret = SetOperScore(score);
    if (ret) {
        // 这里回滚又失败了,可以依靠RocketMQ反查兜底
        ret = DelScoreDeductionRecord(req, record_id);
        ret = RocketMQueue_api.Rollback(xid);
        return ret;
    }
    // 次要逻辑直接全丢RocketMQ
    return RocketMQueue_api.Commit(xid);
}
int TxTimeoutCallback(req){
    // 走到这里说明AcctControl里的逻辑执行失败或者中途crash,需要回滚
    int ret = GetScoreDeductionRecord(req, record_id)
    if (exsist) {
        // 如果查到扣分记录,则回滚删除记录
        ret = DelScoreDeductionRecord(record_id);
        if (ret) return ret;
    } else {
        return RocketMQueue_api.Rollback(xid);
    }
    // 由于一阶段没有状态记录,导致我不知道扣分到底有没有执行,也就无法决定分数应不应该进行回滚,这里直接默认有扣分记录就回滚分数是会有问题的。
    ret = GetOperScore(bizuin, score);
    // 这里失败直接退出,但是扣分记录已经生成,造成扣分记录和实际扣分不一致
    if (ret) {
        // 如果确实是需要回分,但这里失败了,RocketMQ重试再进来会在上一步检查是否有扣分记录那里就直接退出了,根本无法进行这里的重试
        return ret;
    } 
    score -= 10;
    ret = SetOperScore(score);
    if (ret) {
        // 如果确实是需要回分,但这里失败了,RocketMQ重试再进来会在上一步检查是否有扣分记录那里就直接退出了,根本无法进行这里的重试
        return ret;
    }
    return RocketMQueue_api.Rollback(xid);
}
int ProcessMQCommitMessage(req){
    // 2. 生成一条默认文本违规记录,并使用该记录作为证据关联处罚账号的相关能力
    ret = AddIllegalRecord(req, illegal_record_id);
    if (ret == 0) {
        ret = LockAcctFunction(bizuin);
        if (ret) {
            DelIllegalRecord(illegal_record_id);// 这里回滚又失败了咋办,RocketMQ重试又会产生一条记录。。。
            return ret;
        }
    } else {
        return ret;// 如果是超时,可能记录已经生成了,这里重试又会产生一条记录。
    }
    
    // 3. 通知搜索进行账号屏蔽
    ret = NotifyMMSearch(bizuin);
    if (ret) {
        return ret; // 假如这里一致都没重试成功,剩下的逻辑都没办法执行了
    }
    // 4. 添加一条操作记录
    ret = AddOpLog();
    if (ret)
    // 5. 修改账号的昵称
    ret = GetNickName(bizuin, old_nickname);
    if(ret == 0) {
        ret = SaveNickName(bizuin, old_nickname);
        if (ret) return ret;
        if (ret == 0) {
            ret = SetNickname(bizuin, "Invalid Account");
            if (ret) return ret;
            // 如果Nickname Set完成后任务Crash,重新拉起后,会导致保存的就nickname变成修改后的值,导致原始昵称丢失。
    }
    // 6. 扣除账号相关权益的quota值
    ret = DecreaseQuota(bizuin);
    if (ret) return ret;
    // 7. 发站内信通知用户 
    ret = SendNotification(bizuin);
    if (ret) return ret;
}

存在的问题:

可以看到使用一个支持事务消息的消息队列改造后的代码,只能够解决上面1的问题即主从提交不一致,其他问题依然没办法得到有效的解决。提交和回滚虽然能够被RocketMQ异步调度不断重试,但正确性、一致性、可靠性、隔离性及重入安全性依然无法得到保障。

  • 使用CDTF进行开发之后可以很好的解决上述的所有问题。上述逻辑嵌入到框架后的效果如下图所示:

descript

伪代码:

int AcctControl(req, resp) {
    return cdtf.Begin("AcctControl", GetMd5(req)/*business_uuid*/, StrFormat("acctcontrol_%u", req.uin())/*locked_identifier*/,req, resp);
}
// 1. 扣分
// 1.1 生成扣分记录ID 
cdtf::comm::TransactionDoErrorCode AddScoreDeductionRecordDo(req, resp, context) {
    int ret = AddScoreDeductionRecord(req, record_id);
    if (ret) return ret;
    resp.set_record_id(record_id);
    context.set_record_id(record_id);
}
cdtf::comm::TransactionUndoErrorCode AddScoreDeductionRecordUndo(req, resp, context) {
    int ret = GetScoreDeductionRecord(req, record_id);
    if (ret) return comm::TransactionDoErrorCode::kRetry;
    if (has_record) {
        ret = DelScoreDeductionRecord(record_id);
        if (ret) return comm::TransactionDoErrorCode::kRetry;
    }
    return comm::TransactionDoErrorCode::kOk;
}
// 1.2 计算好扣除后的分数(不需要Undo和Check)
cdtf::comm::TransactionDoErrorCode CalcScoreAfterDeductionDo(req, resp, context) {
    int ret = GetOperScore(bizuin, score);
    // 这里失败直接退出,但是扣分记录已经生成,造成扣分记录和实际扣分不一致
    if (ret) {
        // 失败会不断重试
        return comm::TransactionDoErrorCode::kRetry;
    } 
    context.set_original_score(score);
    score -= 10;
    context.set_score_after_deduction(score);
    return comm::TransactionDoErrorCode::kOk;
}
// 1.3 将扣除后的分数落地存储(接口语义幂等,不需要提供Check)
cdtf::comm::TransactionDoErrorCode SetScoreAfterDeductionDo(req, resp, context) {
    int ret = SetOperScore(score);
    if (ret) {
        return comm::TransactionDoErrorCode::kRetry;
    }
    return comm::TransactionDoErrorCode::kOk;
}
cdtf::comm::TransactionUndoErrorCode SetScoreAfterDeductionUndo(req, resp, context) {
    int ret = SetOperScore(context.original_score());
    if (ret) {
        return comm::TransactionDoErrorCode::kRetry;
    }
    return comm::TransactionDoErrorCode::kOk;
}
// 2. 生成一条默认文本违规记录,并使用该记录作为证据关联处罚账号的相关能力
// 2.1 先生成一条默认文本违规记录
cdtf::comm::TransactionDoErrorCode AddIllegalRecordDo(req, resp, context) {
    int ret = AddIllegalRecord(req, illegal_record_id);
    if (ret) {
        return comm::TransactionDoErrorCode::kRetry;
    }
    context.set_illegal_record_id(illegal_record_id);
    return comm::TransactionDoErrorCode::kOk;
}
cdtf::comm::TransactionUndoErrorCode AddIllegalRecordUndo(req, resp, context) {
    int ret = GetIllegalRecord(req, illegal_record_id);
    if (ret) return comm::TransactionDoErrorCode::kRetry;
    if (has_illegal_record) {
        ret = DelIllegalRecord(illegal_record_id);
        if (ret) return comm::TransactionDoErrorCode::kRetry;
    }
    return comm::TransactionDoErrorCode::kOk;
}
// 2.2 使用该记录作为证据关联处罚账号的相关能力(能力封禁接口非幂等)
cdtf::comm::TransactionDoErrorCode LockAcctFunctionDo(req, resp, context) {
    int ret = LockAcctFunction(req.bizuin(), context.illegal_record_id(), lock_func_record_id);
    if (ret == -202) {
        return comm::TransactionDoErrorCode::kTimeout;
    } else if (ret < 0) {
        return comm::TransactionDoErrorCode::kRetry;
    } else if (ret > 0) {
        return comm::TransactionDoErrorCode::kCancel;
    }
    context.set_lock_func_record_id(lock_func_record_id);
    return comm::TransactionDoErrorCode::kOk;
}
cdtf::comm::TransactionUndoErrorCode LockAcctFunctionUndo(req, resp, context) {
// 利用证据捞一下有没有已经处罚过的记录
    int ret = GetLockAcctFunctionRecord(req.bizuin(), context.illegal_record_id());
    if (ret) return comm::TransactionUndoErrorCode::kRetry;
    if (has_lock_function_record) {
        ret = UnsetLockAcctFunction(lock_function_record_id);
        if (ret) return comm::TransactionUndoErrorCode::kRetry;
    }
    return comm::TransactionUndoErrorCode::kOk;
}
cdtf::comm::TransactionCheckErrorCode LockAcctFunctionCheck(req, resp, context) {
// 利用证据捞一下有没有已经处罚过的记录
    int ret = GetLockAcctFunctionRecord(req.bizuin(), context.illegal_record_id());
    if (ret) return comm::TransactionCheckErrorCode::kRetry;
    if (has_lock_function_record) {
        return comm::TransactionCheckErrorCode::kDone;
    }
    return comm::TransactionCheckErrorCode::kUndone;
}
// 3. 通知搜索进行账号屏蔽
cdtf::comm::TransactionDoErrorCode NotifyMMSearchDo(req, resp, context) {
    int ret = NotifyMMSearch();
    if (ret) return comm::TransactionCheckErrorCode::kRetry;
    return comm::TransactionDoErrorCode::kOk;
}
// 4. 添加一条操作记录
cdtf::comm::TransactionDoErrorCode  AddOpLogDo(req, resp, context) {
    int ret = AddOpLog();
    if (ret) return comm::TransactionCheckErrorCode::kRetry; 
    return comm::TransactionDoErrorCode::kOk;
}
// 5. 修改账号的昵称
// 5.1 先保存一下旧的昵称
cdtf::comm::TransactionDoErrorCode  SaveOldNicknameDo(req, resp, context) {
    int ret = GetNickname(bizuin, old_nickname);
    if (ret) return comm::TransactionDoErrorCode::kRetry;
    ret = SaveNickName(bizuin, old_nickname);
    if (ret) return comm::TransactionDoErrorCode::kRetry;
    return comm::TransactionDoErrorCode::kOk;
}
// 5.2 设置新昵称
cdtf::comm::TransactionDoErrorCode  ModifyNicknameDo(req, resp, context) {
    ret = SetNickName(bizuin, "Invalid Account");
    if (ret) return comm::TransactionDoErrorCode::kRetry;
    return comm::TransactionDoErrorCode::kOk;
}
// 6. 扣除账号相关权益的quota值
// 本来非幂等的接口通过对拆分子事务,状态机上下文的暂存以及事务反查逻辑可以有效地保证重入安全,安全的进行重试
// 6.1 先获取当前quota并计算出扣除后的quota值
cdtf::comm::TransactionDoErrorCode  CalcQuotaAfterDecreaseDo(req, resp, context) {
    int ret = GetQuota(bizuin, quota);
    if (ret) comm::TransactionDoErrorCode::kRetry;
    context.set_quota_after_decrease(quota - 1);
    return comm::TransactionDoErrorCode::kOk;
}
// 6.2 扣减quota值
cdtf::comm::TransactionDoErrorCode  DecreaseQuotaDo(req, resp, context) {
    int ret = DecreaseQuota(bizuin);
    if (ret == -202) 
        return cdtf::comm::TransactionDoErrorCode::kTimeout;
    else if (ret < 0)
        return cdtf::comm::TransactionDoErrorCode::kRetry;
    return comm::TransactionDoErrorCode::kOk;
}
cdtf::comm::TransactionCheckErrorCode  DecreaseQuotaCheck(req, resp, context) {
    int ret = GetQuota(bizuin, quota);
    if (ret) 
        return cdtf::comm::TransactionCheckErrorCode::kRetry;
    if (quota == context.quota_after_decrease()) 
        return cdtf::comm::TransactionCheckErrorCode::kDone;        
    return comm::TransactionCheckErrorCode::kUndone;
}
// 7. 发站内信通知用户
cdtf::comm::TransactionDoErrorCode  SendNotificationDo(req, resp, context) {
    int ret = SendNotification(bizuin);
    if (ret == -202) 
        return cdtf::comm::TransactionDoErrorCode::kTimeout;
    else if (ret)
        return cdtf::comm::TransactionDoErrorCode::kRetry;
    return comm::TransactionDoErrorCode::kOk;
}
cdtf::comm::TransactionCheckErrorCode  SendNotificationCheck(req, resp, context) {
    int ret = GetNotifyMsgList(bizuin);
    if (ret) 
        return cdtf::comm::TransactionCheckErrorCode::kRetry;
    for (content : content_list) {
        if (content == "xxx")
            return comm::TransactionCheckErrorCode::kDone;
    }        
    return comm::TransactionCheckErrorCode::kUndone;
}

如何解决上述的问题:

使用CDTF后能够很好的解决上述全部的问题。长事务一大段的逻辑被拆分成了一个一个的分支事务及子事务(分支事务下包含多个子事务),分支事务间相互独立并行执行互不影响,分支事务内部通过SAGA事务模型保证了一致性及原子性,有效的解耦了全局事务下无时序相关性的业务逻辑,通过对复杂事务的结构化拆分,缩小了事务的粒度和重试范围,通过分布式状态机保证了全局事务执行过程中事务执行状态及上下文的持久化,保证了failover后全局事务仍可以恢复到故障时的上下文并继续执行或回退,从而有效地避免了”空回滚“及重复提交。非幂等子事务通过提供的Check逻辑也能够在一定程度上保证一致性和幂等性。整个全局事务执行全程通过分布式锁进行保护,从而保证了事务的隔离性。框架的RM对事务资源进行了标准化的抽象及管理,业务仅需填入Do,Undo,Check逻辑,无需考虑资源的具体使用逻辑,框架层通过TM及RM接管了事务具体执行的调度流程,使得本来复杂的分布式事务执行及调度流程对业务方完全透明。 框架还通过状态机和分布式锁在业务能够提供Business UUID的前提下提供有效的全局事务幂等保证。对于分布式场景下一些可能出现的竞态及异常情况,框架也都有相关的措施应对,业务侧完全无需考虑分布式事务底层的实现细节及各种异常的处理。此外CDTF还提供了事务挂起及断点重入等能力,通过状态机持久化的事务执行全程状态信息对于后续的问题回溯等都会很有帮助。

复杂长事务场景下,CDTF能够提供一个通用化的解决方案,业务使用后可以用很低的成本实现一个更加可靠且一致性更好的业务接口。

详细流程&时序

1 开启全局事务

descript

**注:**Business Lock业务锁用于做全局事务的隔离,在全局事务开启时获得,在全局事务结束时被释放,期间可以被具有相同XID的请求重入,释放也必须被其XID的所有者释放。考虑到极端异常情况下,业务锁可能无法被主动释放,因此业务锁会有一个租约,该租约时间需要尽可能多的大于TC的重试间隔,以此来保证分支事务在Crash或失败后等待TC重试期间仍然持有该锁,TC重试到来后则会重新对锁进行续租。

2 一阶段提交流程

descript

3 一阶段回滚流程

descript

4 一阶段完成后TC回调

descript

5 一阶段超时TC反查回调

descript

6 二阶段分支事务TC回调

descript

7 二阶段提交流程

descript

8 二阶段回滚流程

descript

9 子分支事务发布流程

descript

10 分支事务流程结束

descript

部分异常考虑及处理

  1. **一阶段【1.4】Begin前挂掉:**事务并未开启,等上游重试就好。
  2. **一阶段Begin和拿锁哪个应该放前面?**先Begin后拿锁会更好一点,因为如果先拿锁之后在Begin前挂掉,则会导致整个资源被"死锁",需要等待锁租约到期才能被释放而业务锁的超时时间是较长的(通常是重试间隔+30s,原因是crash后服务重启最坏情况可能会花费30s的时间)因此会导致业务存在较长时间的不可用。如果先Begin后拿锁,则可以依赖于TC的反查及时将锁释放掉。
  3. **一阶段Begin后拿锁失败,请求TC回滚时Crash:**由于此时已经开启了事务,因此在事务超时后TC会触发【5 一阶段超时反查】,此时如果正好业务锁被其他事务持有,则本次请求会直接在【5.2】拿锁失败后回滚全局事务并退出,但如果此时恰好没有其他事务持有该业务锁,则本次请求会在获得锁之后走到【5.5】处由于获取不到事务状态而被认为是无效请求,随后则会释放刚刚获得的业务锁并回滚全局事务退出。
  4. **一阶段Begin后初始化事务状态前Crash:**情况与上面类似,不同点在于该情况下,业务锁已经被获得,此时上游的各种重试都会因为无法获取到业务锁而失败。在TC超时反查到来后,由于没有事务状态并未初始化,因此业务锁会在超时反查中得到释放,全局事务也会被回滚,做好准备等待上游再次过来重试,重新开启事务。
  5. 一阶段Commit或Rollback执行过程中Crash:TC会负责重新拉起事务一阶段,此时会从状态机中恢复事务一阶段执行的上下文,随后根据事务执行状态进入分支【4.8】,根据事务的状态及全局事务配置决定是继续提交还是进行回滚。
  6. 如果一阶段初始化事务状态记录失败(DB挂了),二阶段能否依赖TC帮他重试到成功呢?
  7. **一阶段执行时间过长,导致TC进行了超时反查,但实际事务逻辑还在执行:**通过Branch Lock分支锁进行保护,如果当前事务一阶段仍在执行则分支锁会被一直持有,后续的TC一阶段超时反查请求会在【5.3】处因为获取不到分支锁而无法继续,此时TC会不断的进行重试反查直到一阶段执行完成。重新获得锁并从状态机中恢复事务执行状态后,进入分支【5.6】根据当前事务的状态决定后续的流程。
  8. **一阶段提交【2.7】kCommitted状态落盘后Crash:**此时事务一阶段实际已经执行完成,会等待TC重新拉起,重新拉起并恢复事务状态后TM判断到当前事务状态已经为kCommitted,则全局事务仍会继续提交并进入二阶段分支事务执行流程,最终全局事务会在TC的驱动下执行完成。但实际上,这种情况上游感知到的是超时失败(实际却执行成功了),上游重试后会导致事务被重复执行。在业务能够提供Business UUID的前提下,通过一阶段重入保护机制可以有效地防止全局事务被重复执行保证全局事务幂等。
  9. 二阶段Do或Undo已经完成,但在【7.13】或【8.12】处Update状态机失败:等待【6. 二阶段分支事务TC回调】重新拉起事务,从状态机中恢复事务状态,此时状态应为kCommitting/kRollbacking,根据子事务的配置,如果分支有开启重入保护(逻辑如果本身幂等则不需要开启)则会先进行Check检查当前子事务的完成状态(图中【7.5】或【8.4】),如果是完成/未完成则会直接将状态机转移到下/上一个子事务。
  10. **二阶段Do或Undo过程中Crash:**处理流程同上。
  11. 二阶段Commit或Rollback执行耗时过长,导致TC进行了超时重试,但逻辑实际还在执行:处理流程和一阶段相同,都是通过分支锁保证逻辑串行化。
  12. **锁获取失败的处理:**分支锁获取失败,需要不断重试直到拿到锁,因为此时不确定持有锁的请求是否能够最终执行成功。而业务锁获取失败,则可以直接退出,因为此时正有互斥的请求在执行。
  13. **二阶段【9.7】子分支事务发布失败:**重复提交两个子分支事务是否有问题?子分支事务发布失败只需等待TC拉起后重试就好,但可能会存在部分发布成功,部分发布失败,然后重试导致重复发布的情况出现,其实分支重复发布到TC并不会造成太大影响,因为本身TC可能就支持消息去重,就算TC不支持,相同的两个分支也会由于分支锁的原因仅有一个能够成功执行。同时注意这里分支事务发布结束后仍需要进行全局事务结束检查,因为有可能在发布的过程中全部的子分支事务就已经执行完毕了。
  14. 二阶段【10. 分支事务流程结束】多个分支事务同时完成的竞态条件处理:这里的竞态条件在于所有的分支事务在同一时刻完成,多个分支事务都检查到全局事务进入终态,并同时去释放业务锁。这个竞态条件看起来不会有什么问题,无非就是重复释放锁,并不会造成其他问题。但其实不然,在下面这种极端情况下,问题就会暴露出来。假设分支事务结束流程中,多个分支事务同时到达终态,同时检查对方也都为终态,并先后多次释放了业务锁,但如果在前几次释放业务锁后,又有另外一个新的事务获取到了该锁,后续老事务释放该锁的请求又继续执行,这种情况下就会错误的把别人的业务锁释放掉,因此CDTF的分布式锁在释放时一定会判断锁的归属XID,只有XID匹配锁才能够被释放。

descript

  1. **极端情况下TC队列积压导致业务锁过期:**业务锁过期后,新的全局事务又获得了该锁并很快执行完成,此时之前出现挤压的RocketMQ又重新恢复,之前老的全局事务的分支事务又重新被拉起,这种情况如果放其继续执行,则可能会造成一致性问题。试想如果老事务是封号,新事务是解封,如果出现下面这种情况则会造成半封半解的脏状态。因此框架层做了相关的全局事务屏障检查来杜绝这种情况的发生。

descript

3.6 Q&A

  1. **一阶段全局事务重入检查的作用?**保证全局事务幂等,由于可能存在超时及网络抖动,上游感知到的调用结果和实际调用结果有时很难保持一致,比如可能全局事务实际执行成功,但由于执行时间过长或网络原因,上游感知到的其实是超时,于是上游则会发起重试。为了防止全局事务在这种情况下被重入,业务侧可以提供一个标识本次请求的业务唯一ID,通过这个UUID,框架会在每次开启全局事务前及开启全局事务后对该唯一ID进行重入检查,如果发现已经执行完成,则会直接返回结果给到上游,不会重复执行。
  2. **为什么一阶段要有重入预检查和检查两步?只有重入预检查够不够?**我们先来看只有【1.2】重入预检查的情况,此时两个相同的请求同时到来,此时【1.2】预检查未查到有相同的已经完成的全局事务记录,因此两个请求都会被放过,但是在【1.6】处只有一个请求能够获得业务锁继续往下走,而另一个请求则会因为拿锁失败退出。此时退出的这个请求在经过不断地重试后,会在【1.2】处直接获取到另外一个请求一阶段执行完成后的结果。这样看,看似好像【1.8】处的重入检查时多余的。实则不然,考虑这样一种极端情况,A B两个请求同时到来,还是一样,此时在【1.2】处未查到已完成记录,继续往后执行,但此时A请求由于线程调度等原因迟迟没有分配到时间片执行很慢,而B请求却执行的较快,在A请求执行【1.2】至【1.6】的过程中,B请求已经做完了整个的全局事务,并释放了业务锁,此时A请求会在【1.6】处重新获得锁,造成重复执行。因此重入检查应放在拿到锁之后再进行。之所以保留【1.2】处的预检查是由于业务锁在全局事务结束后才被释放(一阶段执行完不一定会释放),而重入检查只需要之前请求的一阶段完成就可以直接返回结果给到上游,而此时一阶段完成锁并未释放。因此需要有一个重入预检查的动作在获取业务锁之前执行。
  3. 一阶段异常被拉起后应该继续Commit还是Rollback? 都可以,这是一个可选项,框架会根据配置来执行,继续提交和回滚没有本质上的区别,两者都无法保证上游感知到的调用结果和实际调用结果一致,同时也都不可避免的会存在重复调用的情况(比如实际成功但上游感知超时,或者kCommitted状态落盘后Crash),需要自行去评估是要Commit还是要Rollback。这里类比下MySql单机事务在事务执行过程中异常Crash恢复后的处理流程,MySql的做法是,对于已经标记为Committed的事务会利用Redo Log继续提交,而未被标记为Committed的事务则会利用Undo Log进行回滚。
  4. 为啥要业务既需要提供locked_identifer又需要提供business_uuid,都是用来标识业务的有什么不一样? locked_identifer用来指定事务的隔离范围,business_uuid用来标识请求唯一性。举个例子,账号封号和账号解封的locked_identifer一定要相同如:3897866312_ban_unban_uin,但business_uuid两者则是不同的。
  5. **为啥要进行逻辑拆分,将一个长事务拆分成多个子事务执行?写成一坨行不行?**3.3中的业务案例已经阐述了在很多业务场景下逻辑拆分的必要性,此外将整块的长业务逻辑进行拆分还能够提升代码质量及可阅读性对于后续的维护有一定的帮助。
  6. **为啥需要两把锁,分支锁和业务锁?**分支锁用来保证TC在分支调度层面的逻辑串行化(每个事务分支同时只能有一个实例被TC调度执行),业务锁用来保证业务层面的隔离性,比如对同一个账号的封号和对同一个账号的解封应该相互隔离。
  7. **用这个框架和直接用RocketMQueue事务消息有啥不同?**首先RocketMQueue本质上是一个支持事务消息的TC,只是框架的其中一个组件,CDTF是基于RocketMQueue的事务消息调度能力去实现分布式事务的上层,资源应用及执行层面的调度及管理逻辑。在处理复杂及长事务流程方面,简单的使用RocketMQueue肯定是不够的,一个完整的分布式事务处理方案需要考虑到方方面面。自己在实际业务代码中实现一套相对完善和可靠的分布式事务处理逻辑还是一件较为复杂且成本较高的事情,往往业务对于这些成本都是不可接受的,所以基本会选择简单搞,除非是一个非常重要的业务。所以框架的意义就在于可以很低成本的去把每一个复杂长事务的接口都做得更加可靠及完善,使得每一个复杂的业务逻辑都能够低成本的获得事务性及可靠性保障,同时对于业务层面屏蔽分布式事务底层的细节,使得我们的开发能够专注于业务逻辑本身。

局限性&业内难题

目前的框架在极端网络情况下仍然有诸多的局限性。

分布式系统最大的挑战就是NPC:

  • Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
  • Process Pause,进程暂停。有很多种原因可以导致进程暂停:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们无法确定性预测进程暂停的时长,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
  • Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用NTP协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是设备的本地时间可能会突然向前或向后跳跃。

我们这里主要考虑NP问题。分布式事务至今仍有一些几乎“无解”的难题,无解指的是没有通用的完美解决方案,这些问题主要是事务乱序,空回滚以及悬挂。

descript

descript

简单说就是由于网络的拥塞或进程暂停,Do(Try)一直都“在路上”迟迟没有执行,并且在Undo(Cancel)或者Check(反查)之后才执行。这种情况就是事务乱序,造成的结果就是事务最终会处于”悬挂“状态。以RocketMQ事务消息举例,Prepare之后调用扣款接口,但扣款接口由于网络拥塞或者进程暂停迟迟没有执行,直到超时反查到来,这时反查发现并没有进行扣款,于是Rollback了。但之后扣款接口又开始执行了并扣款成功,但此时整体事务已经被Rollback,这就造成了主从不一致。

这个问题其实普遍存在于现有的所有分布式事务框架,各个框架也都是仅针对有限的使用场景提出了解决方案,但至今都没有通用的完美解决方案,只能是根据实际业务资源存储类型来定。MySql和MongoDB这类支持单机强事务类型的DB目前的解决方案是比较成熟的,而Redis这类仅支持弱事务或者是LevelDB RocksDB这类根本不支持单机事务类型的存储相对来讲没有特别稳的方案解决这个问题,同时如果资源方是个黑盒接口距离底层存储很远没办法改造的时候,这个问题就变得无解了。因此对于这个问题框架层能做的很有限,需要业务根据实际情况自行解决。

可能大家会想到用分布式锁是不是可以解决上面的问题。其实是不行的,解决这个问题的关键在于需要框架的锁和业务资源的实际落地是原子的,简单的加分布式锁无法保证原子性,在加锁和资源实际落地的过程中仍然会出现NP问题。

CDTF的方案:

  1. 参考DTM子事务屏障的实现,框架层提供特别的Sql Driver供业务进行使用和替换,该Driver将业务本身的Sql逻辑和框架层的特殊事务状态Sql逻辑绑定在一个MySql单机事务中(原理其实就是依赖于MySql单机事务的原子性以及insert unique key的并发写事务状态冲突),经过这种方式对业务改造后可以彻底解决刚才说的问题。但是缺点也很明显,对业务侵入极大,需要我们侵入到下游资源的实际执行层面,直接对业务操作DB的方式进行改造。而通常我们的业务场景距离底层存储都较远,外部门的接口如果推他们去做改造得难度极大,而且底层存储类型各异,因此这种方案的适用范围非常小。
  2. 尽量将接口实现成幂等,不过度依赖于Check防止重入。
  3. 重试间隔尽可能拉长。结合TCP MSL,rpc Timeout以及rpc logic worker最大处理时间,制定合适的重试间隔。
  4. 框架层有定时事务状态对帐脚本,会通过Check接口对事务状态于资源状态不对齐的情况进行异步修复,已达到最终一致。
  5. 如果业务允许的情况下,尽量不要涉及回滚操作,一阶段做必要的检查,二阶段每个分支事务Trybest执行,接口保证幂等,如果是按这种执行模式,实际是不涉及上面的问题的。

cdtf's People

Contributors

ljr318 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.