欢迎光临
我们一直在努力

Redis 概念以及底层数据结构

mumupudding阅读(22)


Redis 简介

REmote DIctionary Server(Redis) 是一个由SalvatoreSanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Map), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型。

Redis特点

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

Redis 与其他 key – value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis 优势

性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。

丰富的数据类型 – Redis支持 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。

原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。

丰富的特性 – Redis 还支持 publish/subscribe, 队列,key 过期等等特性。

Redis对象类型简介

  • Redis是一种key/value型数据库,其中,每个key和value都是使用对象表示的。比如,我们执行以下代码:
redis> SET message "hello redis"  

其中的key是message,是一个包含了字符串"message"的对象。而value是一个包含了"hello redis"的对象。Redis共有五种对象的类型,分别是:

类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

Redis中的一个对象的结构体表示如下:

typedef struct redisObject {        // 类型      unsigned type:4;              // 编码方式      unsigned encoding: 4;        // 引用计数      int refcount;        // 指向对象的值      void *ptr;    } robj;  

type表示了该对象的对象类型,即上面五个中的一个。但为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种。encoding就表示了对象底层所使用的编码。

  • Redis对象底层数据结构
编码常量 编码所对应的底层数据结构
REDIS_ENCODING_INT long 类型的整数
REDIS_ENCODING_EMBSTR embstr 编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典
  • 字符串对象

字符串对象的编码可以是int、raw或者embstr如果一个字符串的内容可以转换为long,那么该字符串就会被转换成为long类型,对象的ptr就会指向该long,并且对象类型也用int类型表示。普通的字符串有两种,embstr和raw。embstr应该是Redis 3.0新增的数据结构,在2.8中是没有的。如果字符串对象的长度小于39字节,就用embstr对象。否则用传统的raw对象。

#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 44  robj *createStringObject(char *ptr, size_t len) {      if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)          return createEmbeddedStringObject(ptr,len);      else          return createRawStringObject(ptr,len);  }  

embstr的好处有如下几点:

  1. embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为objet分配对象,embstr省去了第一次)。
  2. 相对地,释放内存的次数也由两次变为一次。
  3. embstr的objet和sds放在一起,更好地利用缓存带来的优势。

raw和embstr的区别可以用下面两幅图所示:

图-1.png

图-2.png

  • 列表对象列表对象的编码可以是ziplist或者linkedlist
  1. ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。如下图所示,对象结构中ptr所指向的就是一个ziplist整个ziplist只需要malloc一次,它们在内存中是一块连续的区域。

图-3.png

  1. linkedlist是一种双向链表。它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增加一个node的时候,就需要重新malloc一块内存。

图-4.png

  • 哈希对象哈希对象的底层实现可以是ziplist或者hashtable。ziplist中的哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。当对象数目不多且内容不大时,这种方式效率是很高的。

hashtable的是由dict这个结构来实现的, dict是一个字典,其中的指针dicht ht[2] 指向了两个哈希表

typedef struct dict {      dictType *type;      void *privdata;      dictht ht[2];      long rehashidx; /* rehashing not in progress if rehashidx == -1 */      int iterators; /* number of iterators currently running */  } dict;  typedef struct dictht {      dictEntry **table;      unsigned long size;      unsigned long sizemask;      unsigned long used;  } dictht;  

dicht[0] 是用于真正存放数据,dicht[1]一般在哈希表元素过多进行rehash的时候用于中转数据。dictht中的table用语真正存放元素了,每个key/value对用一个dictEntry表示,放在dictEntry数组中。

图-5.png

  • 集合对象集合对象的编码可以是intset或者hashtableintset是一个整数集合,里面存的为某种同一类型的整数,支持如下三种长度的整数:
#define INTSET_ENC_INT16 (sizeof(int16_t))  #define INTSET_ENC_INT32 (sizeof(int32_t))  #define INTSET_ENC_INT64 (sizeof(int64_t))  

intset是一个有序集合,查找元素的复杂度为O(logN),但插入时不一定为O(logN),因为有可能涉及到升级操作。比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。intset不支持降级操作。

  • 有序集合对象有序集合的编码可能两种,一种是ziplist,另一种是skiplist与dict的结合。ziplist作为集合和作为哈希对象是一样的,member和score顺序存放。按照score从小到大顺序排列skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。它的结构比较特殊。下面分别是跳跃表skiplist和它内部的节点skiplistNode的结构体:
/*  * 跳跃表  */  typedef struct zskiplist {      // 头节点,尾节点      struct zskiplistNode *header, *tail;      // 节点数量      unsigned long length;      // 目前表内节点的最大层数      int level;  } zskiplist;  /* ZSETs use a specialized version of Skiplists */  /*  * 跳跃表节点  */  typedef struct zskiplistNode {      // member 对象      robj *obj;      // 分值      double score;      // 后退指针      struct zskiplistNode *backward;      // 层      struct zskiplistLevel {          // 前进指针          struct zskiplistNode *forward;          // 这个层跨越的节点数量          unsigned int span;      } level[];  } zskiplistNode;  

head和tail分别指向头节点和尾节点,然后每个skiplistNode里面的结构又是分层的(即level数组)用图表示,大概是下面这个样子:

图-6.png

总结

以上简单介绍了Redis的简介,特性以及五种对象类型和五种对象类型的底层实现。事实上,Redis的高效性和灵活性正是得益于同一个对象类型采用不同的底层结构,并且在必要的时候对二者进行转换,还有就是各种底层结构对内存的合理利用。

本文作者:Worktile高级工程师 龚林杰

文章来源:Worktile技术博客

欢迎访问交流更多关于技术及协作的问题。

文章转载请注明出处。

Spring Cloud OAuth 实现微服务内部Token传递的源码解析

mumupudding阅读(15)


背景分析

本文主要来探讨第三部 A –> B ,token 自定维护的源码实现

如何实现token 传递

配置OAuth2FeignRequestInterceptor 即可

  • 此类是Feign 的拦截器实现

@Bean@ConditionalOnProperty("security.oauth2.client.client-id")public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,              OAuth2ProtectedResourceDetails resource,) { return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource);}

源码解析

  • 获取上下文中的token ,组装到请求头
public class OAuth2FeignRequestInterceptor implements RequestInterceptor { // 给请求增加 token @Override public void apply(RequestTemplate template) {  template.header(header, extract(tokenType)); }  protected String extract(String tokenType) {  OAuth2AccessToken accessToken = getToken();  return String.format("%s %s", tokenType, accessToken.getValue()); } // 从spring security 上下文中获取token public OAuth2AccessToken getToken() {  OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken();  if (accessToken == null || accessToken.isExpired()) {   try {    accessToken = acquireAccessToken();   }  }  return accessToken; }}
  • 再来看AccessTokenContextRelay, 上下文token 中转器.非常简单从上下文获取认证信息得到把 token 放到上下文
public class AccessTokenContextRelay { private OAuth2ClientContext context; public AccessTokenContextRelay(OAuth2ClientContext context) {  this.context = context; }     public boolean copyToken() {  if (context.getAccessToken() == null) {   Authentication authentication = SecurityContextHolder.getContext()     .getAuthentication();   if (authentication != null) {    Object details = authentication.getDetails();    if (details instanceof OAuth2AuthenticationDetails) {     OAuth2AuthenticationDetails holder = (OAuth2AuthenticationDetails) details;     String token = holder.getTokenValue();     DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(       token);     String tokenType = holder.getTokenType();     if (tokenType != null) {      accessToken.setTokenType(tokenType);     }     context.setAccessToken(accessToken);     return true;    }   }  }  return false; }}
  • 什么时候执行中转,oauth2 资源服务器非常简单暴力,加了个拦截器给转发。

源码非常简单

谈谈spring security oauth 实现的问题

  1. 当请求上下文没有Token,如果调用feign 会直接,这个OAuth2FeignRequestInterceptor 肯定会报错,因为上下文copy 失败
  2. 如果设置线程隔离,这里也会报错。导致安全上下问题传递不到子线程中。
  3. 强制使用拦截器去处理 token 转发到这里上下文,使用的业务场景只有这里,影响性能高

这三个问题,大家在使用的过程中一定会遇到

自定义OAuth2FeignRequestInterceptor

  • 通过外部条件是否执行token中转
public void apply(RequestTemplate template) { Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM); if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {  return; } accessTokenContextRelay.copyToken(); if (oAuth2ClientContext != null  && oAuth2ClientContext.getAccessToken() != null) {  super.apply(template); }}
  • 手动调用accessTokenContextRelay的copy,当然需要覆盖原生oauth 客户端的配置

总结

欢迎关注我们获得更多的好玩JavaEE 实践

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

mumupudding阅读(21)

最近开始使用Spring开发新项目了,开发新项目必定少不了折腾增删查改。 其中模糊查询,精确查询,分页查询也算是不好对付的功能,需要手写大量重复的代码来实现相关的功能,如何优雅的实现查询功能呢? 

首先上两张截图。

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

 

第一张截图是分页列出活动,可以根据活动名称和活动内容进行模糊查询。 举办机构,举办年份,活动分类为可选项。如指定了年份就会检索相应年份的数据。

第二张截图是列出分类。 可以根据分类名称进行模糊查询。 

可以发现,两个listData 方法均只有4行代码。 

第一行指定模糊查询的列名s,第二行获取一个 Specification 对象,并且传递需精确匹配的参数。 第三行查询。第四行return 。

以下是获取Specification对象的实现 。   这里使用了静态泛型方法,关键在于熟练的使用 CriteriaBuilder 来构建or  and  like  eq 等操作符号。 

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

 

最终的实现效果如下: 

 

 

这里需要根据fid 隔离不同用户的数据,所以每条sql查询都带有一个fid=? 条件查询,当然这个条件是通过 addCommon 方法来实现的。 如果有其他共同的查询条件,在此方法中实现即可。

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

 

PageRequestUtil 类的实现,简单的实现了参数的校验。 比如页码不能为负数,每页最多100条数据。 如果 numPerPage大于100,很有可能就是有人在采集你网站的数据了,虽然这种限制也根本阻止不了别人的采集行为。但至少可以使得程序更加安全一点,不会一次性查询大量数据导致mysql崩溃。

 

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

 

 

如何应对新的业务, 如果只有根据某些字段进行模糊查询, 只需写4行代码足以实现分页功能。 

如何应对业务的变更, 今天产品说根据活动名称检索足以,但第二天又说需要跟据活动内容进行检索,或者新增检索字段。 那么还是只需要修改第一行代码即可。 

 

 

 

 

 

 

 

 

 

 

开源|ns4_frame分布式服务框架开发指南

mumupudding阅读(14)

导语:宜信于2019年3月29日正式开源nextsystem4(以下简称“NS4”)系列模块。此次开源的NS4系列模块是围绕当前支付系统笨重、代码耦合度高、维护成本高而产生的分布式业务系统解决方案。NS4系列框架允许创建复杂的流程/业务流,对于业务服务节点的实现可串联,可分布式。其精简、轻量,实现了“脱容器”(不依赖tomcat、jetty等容器)独立运行。NS4系列框架的设计理念是将业务和逻辑进行分离,开发人员只需通过简单的配置和业务实现就可以实现逻辑复杂、性能高效、功能稳定的业务系统。【点击查看框架整体介绍】

NS4系列包括4个开源模块,分别是:ns4_frame分布式服务框架、ns4_gear_idgen ID 生成器组件(NS4框架Demo示例)、ns4_gear_watchdog 监控系统组件(服务守护、应用性能监控、数据采集、自动化报警系统)和ns4_chatbot通讯组件。本文将详细介绍ns4_frame分布式服务框架开发指南。

项目开源地址:https://github.com/newsettle/ns4_frame

一、框架介绍

ns4_frame本质上是两个应用加三套开发框架组合起来的分布式业务框架。

1.1 使用范围

ns4_frame分布式框架主要适用于业务类型为消息流或者业务核心模型为流式业务的业务系统。它支持消息分发、传递、追踪,支持分步骤、分批次的消息处理,对于信息流、数据流等消息驱动型的业务尤为契合。

1.2 项目结构

ns4_frame框架是一套MAVEN父子项目,由五个项目组成:

  • NS_MQ :负责和底层消息队列进行通信,提供了对消息队列进行操作的API。
  • NS_TRANSPORTER:通过调用NS_MQ提供的API,对业务消息进行收取、处理、转发。
  • NS_CHAIN :一个可选开发框架,负责对同一个jvm中的业务处理步骤进行链条式的整合,组成当前业务模块的业务处理流程。
  • NS_CONTROLLER:一个业务消息转发应用,负责将接收到的消息转给对应的业务模块进行处理,同时负责将业务模块根据整体业务进行关联。NS_CONTROLLER本质是一个独立的应用系统,构建于 NS_TRANPORTOR和NS_CHAIN之上。
  • NS_DISPATCHER :ns4_frame架构规定的消息入口,通过提供的http服务接受业务系统边界外的http请求,并将请求转化成业务系统内部通信使用的消息协议格式。

二、基础入⻔

2.1 开发环境配置

开发语言:JAVA

JDK版本:JDK1.7

MAVEN版本:3.3以上

REDIS版本:3.0以上

以上是开发环境必备的组件和配置,其中java为开发语言,maven为项目编译打包部署必备, redis作为消息中间件使用。

2.2 运行

ns4_frame运行至少需要启动三个jvm项目才能完整运行。启动整个项目分为如下三步:

  • 第一步: ns4_frame消息入口是一个http接口,http服务是由NS_DISPATCHER项目提供的,所以我们第一件事情就是要把NS_DISPATCHER运行起来。要运行NS_DISPATCHER,直接运行Bootstrap类的main方法即可。默认的NS_DISPATCHER会在本机(127.0.0.1)的8027端口上监听http请求,如果收到http请求,默认会将http请求转换成内部通信消息,并存储到本机(127.0.0.1)的 redis中,默认访问的redis端口号是6379。

  • 第二步: NS_CONTROLLER负责接收NS_DISPATCHER传入的消息,并根据配置进行消息分发。 所以我们随后需要运行NS_CONTROLLER项目(为了方便,以下简称“CONTROLLER”) 。在CONTROLLER项目中我们不能直接运行,需要配置一些东⻄。CONTROLLER要运行至少需要指定一个配置文件位置。这个配置文件需要通过java命令参数来指定。假设我现在指定java运行参数-Dconfigfile=nscontroller.xml这个参数本质上是给CONTROLLER底层的NS_TRANSPORTER使用的,它指明了 NS_TRANSPORTER必须得配置文件位置,使得CONTROLLER能顺利利用 NS_TRANSPORTER进行消息收发。默认情况下,CONTROLLER还会到classpath下去找关于NS_CHAIN需要的配置文件, 默认路径是classpath下的nschainconfig目录,在这个目录下所有的xml文件会被认作是NS_CHAIN需要的配置文件集合。当配置文件配置好后,可以通过调用 com.creditease.ns.transporter.context.XmlAppTransporterContext的 main方法来启动NS_CONTROLLER 。

  • 第三步: 在这个步骤中我们需要启动自己的业务项目,在这个业务项目中,必须有以下三个前置条件:

    • 业务项目需要建立在NS_TRANSPORTER框架之上。
    • 业务项目的消息队列名称必须和CONTROLLER项目中配置的队列名一致。
    • 业务启动必须通过 com.creditease.ns.transporter.context.XmlAppTransporterContext的 main方法来启动。

完成以上的三个步骤,一个基本的ns4_frame系统就搭建好并运行起来了。

三、项目架构

3.1 层次划分

上图展示了ns4_frame每个系统的层次结构。

  • 底层是以redis作为消息中间件,对消息中间件的操作被封装入了NS_MQ项目,它向上层提供了对消息队列的操作API接口。
  • 在NS_MQ的上层是NS_TRANSPORTER,它本质是一套消息收发处理框架,它负责接收消息后反向回调业务代码,并将消息交给业务层处理。当业务层处理完毕后,它负责将处理后的消息返回到redis中。
  • NS_CHAINS是一套开发辅助框架,它负责将一个模块的业务处理步骤解耦成一个个零散的任务,并可以随意以任何顺序做关联。
  • NS_CONTROLLER是一个项目,它本质上是一个独立的应用,它负责将整体业务分解成一个个节点,并通过配置将他们以一定的顺序关联起来,并通过消息机制,将这些节点结合起来 形成一套业务系统。
  • NS_DISPATCHER也是一个项目,它是以NETTY框架作为基础,开发出的一个能提供基本的 http服务的独立应用。同时它也是业务系统和外部通讯的唯一边界。

3.2 运行流程

ns4_frame整套系统本质上其实就是一套消息中间件服务加开发框架,整体的结构图如下:

上图显示了一个ns4_frame整体分布式项目的运行流程,一个消息的运转流程按如下顺序:

  • NS_DISPATCHER收到http请求,并将http请求转化为内部消息协议放入指定的消息队列中(根据配置文件)。
  • NS_CONTORLLER从步骤1指定的队列接收到消息,并根据配置的服务编排开始按照顺序将消息发送到每个服务步骤对应的消息队列中。
  • 业务系统收到步骤2中NS_CONTROLLER指定的消息队列接收到消息并开始处理,处理完毕后,将结果返回。
  • NS_CONTROLLER收到业务系统的响应,开始根据配置好的服务,将返回的消息结果发送到下一个服务对应的消息队列中。

四、NS_MQ框架介绍

4.1 核心类和接口

  • RedisMQTemplate类:封装了所有和消息队列的操作相关的API
  • MQConfig:存储了所有和底层消息中间件相关的配置。

4.2 配置方案

默认的,在没有做任何配置的情况下,NS_MQ会自动访问本机(127.0.0.1)的6379端口的redis,如果没有,则会报异常。通常,NS_MQ会去找classpath下一个名为ns_mq.properties的配置文件,这个配置文件中存储着所有和底层消息中间件相关的属性。

列举一些关键的配置元素:

  • redis.type 1 代表redis单机 2 代表redis集群 默认为1
  • redis.single/cluster.host redis单机或者集群的主机地址(包含端口)
  • redis.single/cluster.maxTotal redis单机或者集群的最大连接数
  • redis.single/cluster.miniIdle redis单机或者集群的最小闲置连接数
  • redis.single/cluster.maxIdle redis单机或者集群的最大闲置连接数
  • redis.single/cluster.connectionTimeout redis单机或者集群的尝试连接的超时时间(尚未连接到服务需要等待的时间)
  • redis.single/cluster.socketTimeout redis单机或者集群连接后socket闲置的超时时间

五、NS_TRANSPORTER框架介绍

5.1 框架架构

上图展示了整个NS_TRANSPORTER的整体架构,整套框架收发处理消息分为如下三个步骤:

  • 首先由接收消息的线程(Fetcher线程)通过NS_MQ从底层消息中间件获取消息并放入到本地消息缓存。
  • 消息处理线程(Handler线程)从本地消息缓存中取出消息,并调用业务层的方法实现对消息进行处理,处理完毕后,将处理后的消息放到本地发送缓存。
  • 发送线程(Sender线程)从本地发送缓存取出消息后,将消息通过NS_MQ将消息放入底层消息中间件。

5.2 核心类和接口

  • ServiceMessage:对各个模块之间传递的消息的java封装,包含了模块间通信需要知道的任何信息;

  • Worker:业务层需要实现此接口的doWork方法,实现此接口的对象会被NS_TRANSPORTER的Handler线程回调用来对ServiceMessage中的信息进行处理。

  • ActionWorker:已经部分封装好的抽象类,实现了Worker接口,业务层可以直接继承这个抽象类,简化开发。

5.3 配置方案

默认的,NS_TRANSPORTER会去找名为configfile的系统变量,这个系统变量的值就是NS_TRANSPOTER需要的配置文件所在的路径,NS_TRANSPORTER会找到这个xml配置文件,并在解析相关的配置后启动。

NS_TRANSPORTER相关的配置文件模板如下:

<queues>     <prefix></prefix>     <launchers>           <launcher><class name="类的全名" method="method方法名" property="" /><class name="类的全名" static-method="method方法名" /> </launcher>     </launchers>       <inqueues>           <queue>               <name></name><fetchernum></fetchernum> <buffersize></buffersize> <handlersize></handlersize> <serviceClass></serviceClass> <sendernum></sendernum>           </queue>     </inqueues></queues>

以上xml模板中有如下几个关键元素需要注意:

  • Launcher:用来定义在整个框架完全运行之前需要执行的方法。
  • queue:在这个元素下 name元素表示需要监听的底层消息中间件的队列名。
  • Fetchernum:表示监听消息队列并获取消息的线程数,默认是1。
  • buffersize:表示本地接收/发送消息队列的大小默认是100。
  • handlersize:表示处理消息的线程数,默认是10。
  • Serviceclass:表示具体的处理消息的业务类,这个类必须实现了Worker接口。
  • Sendernum:表示从本地发送消息队列中获取消息后发送到底层消息中间件的线程数。

六、NS_CHAIN框架介绍

6.1 框架架构

由于NS_CHAIN本质是一个纯开发框架,故暂时忽略此框架的框架架构。

6.2 核心类和接口

暂略

6.3 配置方案

本节将详细介绍NS_CHAINS的配置。

NS_CHAINS启动时会去找系统变量chainconfig,这个变量的值就是NS_CHAINS配置文件所在的路径。NS_CHAINS支持配置目录(目录下的所有xml格式文件都被视作NS_CHAINS框架的配置文件)和配置文件。

对于NS_CHAINS的配置格式我们大致列举出关键要素如下:

  • catalog:这个相当于一个完整的服务或者一个命名空间,是NS_CHAINS对外服务的基本单位,NS_CHAINS外部系统只能看到catalog。
  • Command:这是NS_CHAINS任务执行的最小单位,所有执行任务都可以以command的形式被调用执行。
  • Chain:这是一个command的容器,可以将多个command的任务组合成一个执行链路。
  • Group:这个一个command的组合,它可以将多个command组合成一个整体,并按照配置顺序执行。
  • 同时NS_CHAINS的配置具有完整的逻辑语法,支持if条件判断,while循环结构和顺序结构。

七、NS_DISPATCHER应用介绍

7.1 框架架构

NS_DISPATCHER本质是一个独立的建立在Netty框架上的一个能提供http服务的独立应用,所以框架结构此处从略。

7.2 核心类和接口

NS_DISPATCHER是以NETTY框架为基础的,所以其核心类就是如下的几个协议处理器:

  • HttpDispatcherServerHandler:主要负责解析传入的http请求,并封装成对应的java对象交给 HttpRPCHandler做进一步处理。
  • HttpRPCHandler:主要接收上一步封装好的java对象,并取出对应的请求参数、请求内容等,封装成系统内部传输用的协议对象,并可以以同步请求响应模式/异步发送模式将协议对象放入底层消息中间件。

7.3 配置方案

NS_DISPATCHER启动会去找ns_dispatcher.properties文件,下面介绍配置的关键元素:

  • http.port:指定了http服务的监听端口。
  • dispatcher.pool.num:指定了dispatcher的并发线程数,dispatcher的性能和这个参数有非常大的关系。
  • dispatcher.queuename:封装好的内部协议消息要放入的队列的名字,NS_DISPATCHER也支持https,所以,如果在ns_dispatcher.properties文件中有如下几个选项,那么NS_DISPATCHER也会启动对应的https服务。
  • ca.path:指明了可信任证书的路径。
  • key.path:指明了公钥的路径。
  • https.port:指明了https服务监听的端口。

八、NS_CONTROLLER应用介绍

8.1 框架架构

NS_CONTROLLER本质是建立在NS_TRANSPORTER和NS_CHAINS上的独立应用,核心就是 NS_TRANSPORTER的架构加NS_CHAINS的辅助,故不再重复列举其架构。

8.2 核心类和接口

DefaultPublishCommand:这是NS_CONTROLLER对于NS_CHAINS的一个扩展,它支持同步发送消息,并等待消息的响应,并可以设置等待响应的超时时间。同时,还支持异步发送消息,不需要等待消息的响应。

8.3 配置方案

遵循NS_TRANSPORTER和NS_CHAINS的配置规则,所以不再赘述。注意:在NS_CONTROLLER中对于NS_CHAINS的配置做了一些功能扩展,主要是添加了publish的配置元素,这个随后可以提供配置模板。

九、项目部署

9.1 部署方案

如果要部署整个ns4_frame项目,请按照以下步骤进行:

  • 部署NS_DISPATCHER项目:NS_DISPATCHER项目是一个Maven项目,首先需要通过mvn:package deploy将整个项目打成一个zip包上传到服务器,然后解压成一个目录。在这个目录中,有如下几个子目录:bin、config、lib、logs。其中,bin目录中包含了DISPATCHER的启动脚本;config目录存放了NS_DISPATCHER必须的配置文件;lib目录存放了NS_DISPATCHER所需要的所有jar包;logs目录存放了所有NS_DISPATCHER打印的日志。
  • 部署NS_CONTROLLER项目:NS_CONTROLLER项目也是一个Maven项目,需要通过mvn:package deploy将整个项目打成一个zip包。目录结构同NS_DISPATCHER项目,此处不再赘述。
  • 部署业务代码:业务代码请自行按照各个团队的规则部署。

十、运行日志

10.1 日志分类

ns4_frame项目将日志大致分成了四类:

  • *fram.log:系统日志,属于整个ns4_frame底层系统内部的日志,包括系统的启动,线程的启动关闭等信息。
  • *biz.log:业务日志,所有业务相关的日志统统会被导向到这里。
  • *flow.log:消息流日志,这里记录了系统所有消息的流转信息。
  • *mq.log:这里记录所有对底层消息中间件进行操作的信息。

10.2 如何查看日志

  • 业务报错:如果业务报错,基本所有的报错信息都会在*biz.log中查到。
  • 消息流转: 如果是消息发送响应的问题,基本上在*flow.log中可以查到或者推断出相关的信息。
  • 底层消息中间件交互: 如果消息流转无法推断出问题,或者无法查到对应的消息,就需要转到*mq.log中进行查询。

十一、其他

11.1 常⻅问题

ns4_frame系统本质是一个以消息为通信机制的分布式系统,经常出现的问题分成以下两部分:

  • 业务异常

由于业务本身是由底层NS_TRANSPORTER回调来执行的,当业务出现异常的时候,很可能由于没有合适的被catch到,从而被底层的NS_TRANSPOTER框架捕获。 对于没有在*biz.log和stdoout.log中查找到的问题,可以去查看下*flow.log的日志,看是否出现了异常被底层NS_TRANSPOTER捕获了。

  • 底层异常

有些情况,业务本身并没有出现问题,但是由于消息通信出现了问题,会导致业务没有执行,对于 这种情况我们需要首先从消息入口处即NS_DISPATCHER的*flow.log中查找到对应的 messageId,然后根据消息流转路径,一步步去对应的部署机器上查询。

内容来源:宜信技术学院

Java并发编程之CountDownLatch源码解析

mumupudding阅读(17)


一、导语

最近在学习并发编程原理,所以准备整理一下自己学到的知识,先写一篇CountDownLatch的源码分析,之后希望可以慢慢写完整个并发编程。

二、什么是CountDownLatch

CountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。
比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

三、简单使用

public static void main(String[] args){ System.out.println("主线程和他的两个小兄弟约好去吃火锅"); System.out.println("主线程进入了饭店"); System.out.println("主线程想要开始动筷子吃饭"); //new一个计数器,初始值为2,当计数器为0时,主线程开始执行 CountDownLatch latch = new CountDownLatch(2);   new Thread(){             public void run() {                 try {                    System.out.println("子线程1——小兄弟A 正在到饭店的路上");                    Thread.sleep(3000);                    System.out.println("子线程1——小兄弟A 到饭店了");      //一个小兄弟到了,计数器-1                    latch.countDown();                } catch (InterruptedException e) {                    e.printStackTrace();                }             };         }.start();     new Thread(){             public void run() {                 try {                    System.out.println("子线程2——小兄弟B 正在到饭店的路上");                    Thread.sleep(3000);                    System.out.println("子线程2——小兄弟B 到饭店了");      //另一个小兄弟到了,计数器-1                    latch.countDown();                } catch (InterruptedException e) {                    e.printStackTrace();                }             };         }.start();  //主线程等待,直到其他两个小兄弟也进入饭店(计数器==0),主线程才能吃饭  latch.await();  System.out.println("主线程终于可以开始吃饭了~");}

四、源码分析

核心代码:

CountDownLatch latch = new CountDownLatch(1);        latch.await();        latch.countDown();

其中构造函数的参数是计数器的值;
await()方法是用来阻塞线程,直到计数器的值为0
countDown()方法是执行计数器-1操作

1、首先来看构造函数的代码

public CountDownLatch(int count) {        if (count < 0) throw new IllegalArgumentException("count < 0");        this.sync = new Sync(count);    }

这段代码很简单,首先if判断传入的count是否<0,如果小于0直接抛异常。
然后new一个类Sync,这个Sync是什么呢?我们一起来看下

private static final class Sync extends AbstractQueuedSynchronizer {        private static final long serialVersionUID = 4982264981922014374L;        Sync(int count) {            setState(count);        }        int getCount() {            return getState();        } //尝试获取共享锁        protected int tryAcquireShared(int acquires) {            return (getState() == 0) ? 1 : -1;        } //尝试释放共享锁        protected boolean tryReleaseShared(int releases) {            // Decrement count; signal when transition to zero            for (;;) {                int c = getState();                if (c == 0)                    return false;                int nextc = c-1;                if (compareAndSetState(c, nextc))                    return nextc == 0;            }        }    }

可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。
其中有几个核心点:

  1. 变量 state是父类AQS里面的变量,在这里的语义是计数器的值
  2. getState()方法也是父类AQS里的方法,很简单,就是获取state的值
  3. tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写,先有个印象,之后详讲。

2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。

public void await() throws InterruptedException {        sync.acquireSharedInterruptibly(1);    }

可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。

public final void acquireSharedInterruptibly(int arg)            throws InterruptedException { //判断线程是否是中断状态        if (Thread.interrupted())            throw new InterruptedException(); //尝试获取state的值        if (tryAcquireShared(arg) < 0)//step1            doAcquireSharedInterruptibly(arg);//step2    }

tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。若state!=0,则返回-1,进入if体内step2处。

下面我们来看acquireSharedInterruptibly(int arg)方法:

private void doAcquireSharedInterruptibly(int arg)        throws InterruptedException { //step1、把当前线程封装为共享类型的Node,加入队列尾部        final Node node = addWaiter(Node.SHARED);        boolean failed = true;        try {            for (;;) {  //step2、获取当前node的前一个元素                final Node p = node.predecessor();  //step3、如果前一个元素是队首                if (p == head) {      //step4、再次调用tryAcquireShared()方法,判断state的值是否为0                    int r = tryAcquireShared(arg);      //step5、如果state的值==0                    if (r >= 0) {   //step6、设置当前node为队首,并尝试释放共享锁                        setHeadAndPropagate(node, r);                        p.next = null; // help GC                        failed = false;                        return;                    }                }  //step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    throw new InterruptedException();            }        } finally { //step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消            if (failed)                cancelAcquire(node);        }    }

按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。
1、首先看addWaiter()

//step1private Node addWaiter(Node mode) { //把当前线程封装为node        Node node = new Node(Thread.currentThread(), mode);        // Try the fast path of enq; backup to full enq on failure //获取当前队列的队尾tail,并赋值给pred        Node pred = tail; //如果pred!=null,即当前队尾不为null        if (pred != null) { //把当前队尾tail,变成当前node的前继节点            node.prev = pred;     //cas更新当前node为新的队尾            if (compareAndSetTail(pred, node)) {                pred.next = node;                return node;            }        } //如果队尾为空,走enq方法        enq(node);//step1.1        return node;    }-----------------------------------------------------------------//step1.1private Node enq(final Node node) {        for (;;) {            Node t = tail;     //如果队尾tail为null,初始化队列            if (t == null) { // Must initialize  //cas设置一个新的空node为队首                if (compareAndSetHead(new Node()))                    tail = head;            } else {  //cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点                node.prev = t;                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }

2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现

//step6private void setHeadAndPropagate(Node node, int propagate) { //获取队首head        Node h = head; // Record old head for check below //设置当前node为队首,并取消node所关联的线程        setHead(node); //        if (propagate > 0 || h == null || h.waitStatus < 0 ||            (h = head) == null || h.waitStatus < 0) {            Node s = node.next;     //如果当前node的后继节点为null或者是shared类型的            if (s == null || s.isShared())  //释放锁,唤醒下一个线程                doReleaseShared();//step6.1        }    }--------------------------------------------------------------------//step6.1private void doReleaseShared() {        for (;;) {     //找到头节点            Node h = head;            if (h != null && h != tail) {  //获取头节点状态                int ws = h.waitStatus;                if (ws == Node.SIGNAL) {                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))                        continue;            // loop to recheck cases      //唤醒head节点的next节点                    unparkSuccessor(h);                }                else if (ws == 0 &&                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))                    continue;                // loop on failed CAS            }            if (h == head)                   // loop if head changed                break;        }    }

3、接下来我们来看countDown()方法。

public void countDown() {        sync.releaseShared(1);    }

可以看到调用的是父类AQS的releaseShared 方法

public final boolean releaseShared(int arg) { //state-1        if (tryReleaseShared(arg)) {//step1     //唤醒等待线程,内部调用的是LockSupport.unpark方法            doReleaseShared();//step2            return true;        }        return false;    }------------------------------------------------------------------//step1protected boolean tryReleaseShared(int releases) {            // Decrement count; signal when transition to zero            for (;;) {  //获取当前state的值                int c = getState();                if (c == 0)                    return false;                int nextc = c-1;  //cas操作来进行原子减1                if (compareAndSetState(c, nextc))                    return nextc == 0;            }        }

五、总结

CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。下面我们通过一个简单的图来帮助我们理解一下:juc
PS:本人也是还在学习的路上,理解的也不是特别透彻,如有错误,愿倾听教诲。^_^

ES7之Async/await的使用详解

mumupudding阅读(30)

在 js 异步请求数据时,通常,我们多采用回调函数的方式解决,但是,如果有多个回调函数嵌套时,代码显得很不优雅,维护成本也相应较高。 ES6 提供的 Promise 方法和 ES7 提供的 Async/Await 语法糖可以更好解决多层回调问题。

Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。await 操作符用于等待一个Promise 对象吗。。它只能在异步函数 async function 中使用。await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。一个ajax请求时

通常 使用 ajax 请求数据时,会

$.ajax({ url: 'data1.json', type: 'GET', success: function (res) {  console.log(res) // 请求成功,则得到结果res }, error: function(err) {  console.log(err) }})//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力

上面可以得到我们想要的结果 res —> { "url": "data2.json" }

多个ajax请求时

但是 当得到的数据 res 需要用于另一个 ajax 请求时,则需要如下写法:

$.ajax({ url: 'data1.json', type: 'GET', success: function (res) {  $.ajax({   url: res.url, // 将 第一个ajax请求成功得到的res 用于第二个ajax请求   type: 'GET',   success: function (res) {    $.ajax({     url: res.url, // 将第二个ajax请求成功得到的res 用于第三个ajax请求     type: 'GET',     success: function (res) {      console.log(res) // {url: "this is data3.json"}     },     error: function(err) {      console.log(err)     }    })   },   error: function(err) {    console.log(err)   }  }) },//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力 error: function(err) {  console.log(err) }})

上面出现多个回调函数的嵌套,可读性较差(虽然这种嵌套在平常的开发中少见,但是在node服务端开发时,还是很常见的)

优化方法

使用 promise 链式操作

如下,使用 Promise,进行链式操作,可以使上面的异步代码看起来如同步般易读,从回调地狱中解脱出来。

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url,   type: 'GET',   success: function (res) {    resolve(res);   },   error: function(err) {    reject('请求失败');   }  }) })}; ajaxGet('data1.json').then((d) => { console.log(d);  // {url: "data2.json"} return ajaxGet(d.url);}).then((d) => { console.log(d);  // {url: "data3.json"} return ajaxGet(d.url);}).then((d) => { console.log(d);  // {url: "this is data3.json"}})//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力

Async/await 方法

  • async 表示这是一个async函数,即异步函数,await只能用在这个函数里面。
  • await 表示在这里等待promise返回结果了,再继续执行。
  • await 后面跟着的应该是一个promise对象(当然,其他返回值也没关系,只是会立即执行,不过那样就没有意义了…)
  • await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。
  • await 等待的虽然是promise对象,但不必写.then(..),直接可以得到返回值。

执行一个ajax请求,可以通过如下方法:

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url,   type: 'GET',   success: function (res) {    resolve(res)   },   error: function(err) {    reject('请求失败')   }  }) })}; async function getDate() { console.log('开始') let result1 = await ajaxGet('data1.json'); console.log('result1 ---> ', result1); // result1 ---> {url: "data2.json"}};getDate();  // 需要执行异步函数

执行多个ajax请求时:

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url,   type: 'GET',   success: function (res) {    resolve(res)   },   error: function(err) {    reject('请求失败')   }  }) })}; async function getDate() { console.log('开始') let result1 = await ajaxGet('data1.json'); let result2 = await ajaxGet(result1.url); let result3 = await ajaxGet(result2.url); console.log('result1 ---> ', result1); // result1 ---> {url: "data2.json"} console.log('result2 ---> ', result2); // result2 ---> {url: "data3.json"} console.log('result3 ---> ', result3); // result3 ---> {url: "this is data3.json"}}; getDate(); // 需要执行异步函数

async await捕捉错误:async await中.then(..)不用写了,那么.catch(..)也不用写,可以直接用标准的try catch语法捕捉错误。例如,如果下面的 url 写错了

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url111, // 此处为错误的 url   type: 'GET',   success: function (res) {    resolve(res)   },   error: function(err) {    reject('请求失败')   }  })//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力 })};  async function getDate() { console.log('开始') try {  let result1 = await ajaxGet('data1.json'); // 执行到这里报错,直接跳至下面 catch() 语句  let result2 = await ajaxGet(result1.url);  let result3 = await ajaxGet(result2.url);  console.log('result1 ---> ', result1);  console.log('result2 ---> ', result2);  console.log('result3 ---> ', result3);  } catch(err) {  console.log(err) // ReferenceError: url111 is not defined }}; getDate(); // 需要执行异步函数

Mybatis-Plus 真好用(乡村爱情加持)

mumupudding阅读(14)

乡村爱情


写在前面

MyBatis的增强方案确实有不少,甚至有种感觉是现在如果只用 “裸MyBatis”,不来点增强插件都不好意思了。这不,在上一篇文章《Spring Boot项目利用MyBatis Generator进行数据层代码自动生成》 中尝试了一下 MyBatis Generator。这次来点更加先进的 Mybatis-Plus,SQL语句都不用写了,分页也是自动完成,嗯,真香!


数据库准备

CREATE TABLE tbl_user( user_id BIGINT(20) NOT NULL COMMENT '主键ID', user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', user_age INT(11) NULL DEFAULT NULL COMMENT '年龄', PRIMARY KEY (user_id)) charset = utf8;

MyBatis-Plus加持

  • 工程搭建 (不赘述了)

  • 依赖引入

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.12</version></dependency>

主要是 Mybatis Plus、Lombok(不知道Lombok干嘛的?可以看这里)、Druid连接池 等依赖。

  • MyBatis Plus配置

项目配置

mybatis-plus:  mapper-locations: classpath:/mapper/*Mapper.xml

新增 MyBatis Plus配置类

@Configuration@MapperScan("cn.codesheep.springbtmybatisplus.mapper")public class MyBatisConfig {}

看到没,几乎零配置啊,下面就可以写业务逻辑了


业务编写

  • 实体类
@Data@TableName("tbl_user")public class User {    @TableId(value = "user_id")    private Long userId;    private String userName;    private Integer userAge;}
  • Mapper类
public interface UserMapper extends BaseMapper<User> {}

这里啥接口方法也不用写,就可以实现增删改查了!

  • Service类

Service接口:

public interface UserService extends IService<User> {    int insertUser( User user );    int updateUser( User user );    int deleteUser( User user );    User findUserByName( String userName );    IPage getUserPage( Page page, User user );}

Service实现:

@Service@AllArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {    // 增    @Override    public int insertUser(User user) {        return baseMapper.insert( user );    }    // 改    @Override    public int updateUser(User user) {        return baseMapper.updateById( user );    }    // 删    @Override    public int deleteUser(User user) {        return baseMapper.deleteById( user.getUserId() );    }    // 查    @Override    public User findUserByName( String userName ) {        return baseMapper.getUserByName( userName );    }}
  • Controller类
@RestController@RequestMapping("/user")public class UserContorller {    @Autowired    private UserService userService;    // 增    @PostMapping( value = "/insert")    public Object insert( @RequestBody User user ) {        return userService.insertUser( user );    }    // 改    @PostMapping( value = "/update")    public Object update( @RequestBody User user ) {        return userService.updateUser( user );    }    // 删    @PostMapping( value = "/delete")    public Object delete( @RequestBody User user ) {        return userService.deleteUser( user );    }    // 查    @GetMapping( value = "/getUserByName")    public Object getUserByName( @RequestParam String userName ) {        return userService.findUserByName( userName );    }}

通过以上几个简单的步骤,我们就实现了 tbl_user表的增删改查,传统 MyBatis的 XML文件一个都不需要写!


实际实验【《乡爱》加持】

  • 启动项目

很牛批的 logo就会出现

Mybatis Plus Logo

接下来通过 Postman来发送增删改查的请求

  • 插入记录

通过 Postman随便插入几条记录 POST localhost:8089/user/insert

{"userId":3,"userName":"刘能","userAge":"58"}{"userId":4,"userName":"赵四","userAge":"58"}{"userId":5,"userName":"谢广坤","userAge":"58"}{"userId":6,"userName":"刘大脑袋","userAge":"58"}

发送插入请求

插入结果

  • 修改记录

修改记录时需要带用户ID,比如我们修改 赵四 那条记录的名字为 赵四(Zhao Four)

发送修改请求

修改结果

  • 删除记录

修改记录时同样需要带用户ID,比如删除ID=6 那条 刘大脑袋的记录

image.png

  • 查询记录(普通查询,下文讲分页查询)

比如,按照名字来查询:GET localhost:8089/user/getUserByName?userName=刘能


最关心的分页问题

  • 首先装配分页插件
@Beanpublic PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor();}
  • Mapper类
public interface UserMapper extends BaseMapper<User> {    // 普通查询    User getUserByName( String userName );    // 分页查询    IPage<List<User>> getUsersPage( Page page, @Param("query") User user );}
  • Service类
@Service@AllArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {    // 查:普通查    @Override    public User findUserByName( String userName ) {        return baseMapper.getUserByName( userName );    }    // 分页查    @Override    public IPage getUserPage(Page page, User user) {        return baseMapper.getUsersPage( page, user );    }}
  • Controller类
@GetMapping( value = "/page")public Object getUserPage( Page page, User user ) { return userService.getUserPage( page, user );}

实际实验一下,我们分页查询 年龄 = 58 的多条记录:

分页查询结果

可以看到结果数据中,除了给到当前页数据,还把总记录条数,总页数等一并返回了,很是优雅呢 !


写在最后

由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!



状态机在马蜂窝机票订单交易系统中的应用与优化实践

mumupudding阅读(17)

在设计交易系统时,稳定性、可扩展性、可维护性都是我们需要关注的重点。本文将对如何通过状态机在交易系统中的应用解决上述问题做出一些探讨。

 

关于马蜂窝机票订单交易系统

交易系统往往存在订单维度多、状态多、交易链路长、流程复杂等特点。以马蜂窝大交通业务中的机票交易为例,用户提交的一个订单除了机票信息之外可能还包含很多信息,比如保险或者其他附加产品。其中保险又分为很多类型,如航意险、航延险、组合险等。

从用户的维度看,一个订单是由购买的主产品机票和附加产品共同构成,支付的时候是作为一个整体去支付,而如果想要退票、退保也是可以部分操作的;从供应商的维度看,一个订单中的每个产品背后都有独立的供应商,机票有机票的供应商,保险有保险的供应商,每个供应商的订单都需要分开出票、独立结算。

用户的购买支付流程、供应商的出票出保流程,构成一个有机的整体穿插在机票交易系统中,密不可分。

 

状态机在机票交易系统中的应用与优化

有限状态机的概念

有限状态机(以下简称状态机)是一种用于对事物或者对象行为进行建模的工具。

状态机将复杂的逻辑简化为有限个稳定状态,构建在这些状态之间的转移和动作等行为的数学模型,在稳定状态中判断事件。

对状态机输入一个事件,状态机会根据当前状态和触发的事件唯一确定一个状态迁移。

图1:FSM工作原理

 

业务系统的本质就是描述真实的世界,因此几乎所有的业务系统中都会有状态机的影子。订单交易流程更是天然适合状态机模型的应用。

以用户支付流程为例,如果不使用状态机,在接收到支付成功回调时则需要执行一系列动作:查询支付流水号、记录支付时间、修改主订单状态为已支付、通知供应商去出票、记录通知出票时间、修改机票子订单状态为出票中…… 逻辑非常繁琐,而且代码耦合严重。

为了使交易系统的订单状态按照设计流程正确向下流转,比如当前用户已支付,不允许再支付;当前订单已经关单,不能再通知出票等等,我们通过应用状态机的方式来优化机票交易系统,将所有的状态、事件、动作都抽离出来,对复杂的状态迁移逻辑进行统一管理,来取代冗长的 if else 判断,使机票交易系统中的复杂问题得以解耦,变得直观、方便操作,使系统更加易于维护和管理。

状态机设计

在数据库设计层面,我们将整个订单整体作为一个主订单,把供应商的订单作为子订单。假设一个用户同时购买了机票和保险,因为机票、保险对应的是不同的供应商,也就是 1 个主订单  order  对应 2 个子订单 sub_order。其中主订单 order 记录用户的信息(UID、联系方式、订单总价格等),子订单 sub_order 记录产品类型、供应商订单号、结算价格等。

同时,我们把正向出票、逆向退票改签分开,抽成不同的子系统。这样每个子系统都是完全独立的,有利于系统的维护和拓展。

对于机票正向子系统而言,有两套状态机:主订单状态机负责管理 order 的状态,包括创单成功、支付成功、交易成功、订单关闭等;子订单状态机负责管理 sub_order 的状态,维护预订成功到出票的流程。同样,对于逆向退票和改签子系统,也会有各自的状态机。

图2:机票主订单状态机状态转移示例

 

框架选型

目前业界常用的状态机引擎框架主要有 Spring Statemachine、Stateless4j、Squirrel-Foundation 等。经过结合实际业务进行横向对比后,最终我们决定使用 Squirrel-Foundation,主要是因为:

  1. 代码量适中,扩展和维护相对而言比较容易;

  2. StateMachine 轻量,实例创建开销小;

  3. 切入点丰富,支持状态进入、状态完成、异常等节点的监听,使转换过程留有足够的切入点;

  4. 支持使用注解定义状态转移,使用方便;

  5. 从设计上不支持单例复用,只能随用随 New,因此状态机的本身的生命流管理很清晰,不会因为状态机单例复用的问题造成麻烦。 

MSM 的设计与实现

结合大交通业务逻辑,我们在 Squirrel-Foundation 的基础之上进行了 Action 概念的抽取和二次封装,将状态迁移、异步消息糅合到一起,封装成为 MSM 框架 (MFW State Machine),用来实现业务订单状态定义、事件定义和状态机定义,并用注解的形式来描述状态迁移。

我们认为一次状态迁移必然会伴随着异步消息,因此把一个流程中必须要成功的数据库操作放到一个事务中,把允许失败重试并且对实时度要求不高的操作放到异步消息消费的流程中。

以机票订单支付成功为例,机票订单支付成功时,会涉及修改订单状态为已支付、更新支付流水号等,这些是在一个事务中;而通知供应商出票,则是放在异步消息消费中处理。异步消息的实现使用的是 RocketMQ,主要考虑到 RocketMQ 支持二阶段提交,消息可靠性有保证,支持重试,支持多个 Consumer 组。

以下具体说明:

1. 对每个状态迁移需要执行的动作,都会抽取出一个Action 类,并且继承 AbstractAction,支持多个不同的状态迁移执行相同的动作。这里主要取决于 public List<ActionCondition> matchConditions() 的实现,因此只需要 matchConditions 返回多个初始状态-事件的匹配条件键值对就可以了。每个 Action 都有一个对应的继承 MFWContext 类的上下文类,用于在 process saveDB 等方法中的通信。

2. 注册所有的 Action,添加每个状态迁移执行完成或者执行失败的监听。

3. 由于依赖 RocketMQ 异步消息,所以需要一个 Spring Bean 去继承 BaseMessageSender,这个类会生成异步消息提供者。如果要使用二阶段提交,则需要一个类继承 BaseMsgTransactionListener,这里可以参考机票的 OrderChangeMessageSender 和 OrderChangeMsgTransactionListener。

4. 最后,实现一个事件触发器类。在这个类里面包含一个 Apply 方法,传入订单 PO 对象、事件、对应的上下文,每次执行都实例化出一个状态机实例,并初始化当前状态,并调用 Fire 方法。

5. 实例化一个状态机对象,设置当前状态为数据库对应的状态,调用 Fire 方法之后,最终会执行到 OrderStateMachine 类里面用注解描述的 callMethod 方法。我们配置的是 callMethod = “action”,它就会反射执行当前类的 Action 方法。

Action 方法我们的实现是通过 super.action(from, to, event, context),就会执行 MFWStateMachine 的 Action 方法,先去根据当前状态和事件获取对应的Action,这里使用到了「工厂模式」,然后执行 Process 方法。如果成功,会执行在 MFWStateMachine 类初始化的 TransitionCompleteListener,执行该 Action的 afterProcess 方法来修改数据库记录以及发送消息;如果失败,会执行TransitionExceptionListener,执行该 Action 的onException 方法来进行相应处理。

综上,MSM 可以根据 Action 类的声明和配置,来动态生成出 Squirrel-Foundation 的状态机定义,而不需要由使用方再去定义一次,使 MSM 的使用更方便。

图3: UML

 

趟过的坑

1. 事务不生效

最初我们使用 Spring 注解方式进行事务管理,即在 Action 类的数据库操作方法上加 @Transactional 注解,却发现在实践中不起作用。经过排查后发现, Spring 的事务注解是靠 AOP 切面实现的。在对象内部的方法中调用该对象其他使用 AOP 注解的方法,被调用方法的 AOP 注解会失效。因为同一个类的内部代码调用中,不会走代理类。后来我们通过手动开启事务的方式来解决此问题。

2. 匹配 Action  

最初我们匹配 Action 有两种方式:精准匹配及非精准匹配。精准匹配是指只有当某个状态迁移的初始状态和触发的事件一致时,才能匹配到 Action;非精准匹配是指只要触发的事件一致,就可以匹配到 Action。后来我们发现非精准匹配在某些情形下会出现问题,于是统一改成了多条件精准匹配。即在执行状态机触发时执行的 Action 方法时,去精准匹配 Action,多个状态迁移执行的方法可以匹配到同一个 Action,这样能够复用 Action 代码而不会出问题。 

3. 异步消息一致性 

有一些情况是绝不能出现的,比如修改数据库没成功即发出了消息;或是修改数据库成功了,而发送消息失败;或是在提交数据库事务之前,消息已经发送成功了。解决这个问题我们用到了 RocketMQ 的事务消息功能,它支持二阶段提交,会先发送一条预处理消息,然后回调执行本地事务,最终提交或者回滚,帮助保证修改数据库的信息和发送异步消息的一致。

4. 同一条订单数据并发执行不同事件 

在某些情况下,同一条订单数据可能会在同一时间(毫秒级)同时触发不同的事件。如机票主订单在待支付状态下,可以接收支付中心的回调,触发支付成功事件;也可以由用户点击取消订单,或者超时未支付定时任务来触发关单事件。如果不做任何控制的话,一个订单将可能出现既支付成功又会被取消。

我们用数据库乐观锁来规避这个问题:在执行修改数据库的事务时,update 订单的语句带有原状态的条件判断,通过判断更新行数是否为 1,来决定是否抛出异常,即生成这样的 SQL 语句:update order where order_id = ‘1234′ and order_status = ‘待支付’。

这样的话,如果两个事件同时触发同时执行,谁先把事务提交成功,谁就能执行成功;事务提交较晚的事件会因为更新行数为 0 而执行失败,最终回滚事务,就仿佛无事发生过一样。

使用悲观锁也可以解决这个问题,这种方式是谁先争抢到锁谁就可以成功执行。但考虑到可能会有脚本对数据库批量修改,悲观锁存在死锁的潜在问题,我们最终还是采用了乐观锁的方式。

 

总结

MSM 状态机的定义和声明在 Squirrel-Foundation 的基础之上,抽取出 Action 概念,并对 Action 类配置起始状态、目标状态、触发的事件、上下文定义等,使 MSM 可以根据 Action 类的声明和配置,来动态生成出 Squirrel-Foundation 的状态机定义,而不需要使用方再去定义一次,操作更简单,维护起来也更容易。 

通过使用状态机,机票订单交易系统的流程复杂性问题迎刃而解,系统在稳定性、可扩展性、可维护性等方面也得到了显著的改善和提升。

状态机的使用场景不仅仅局限于订单交易系统,其他一些涉及到状态变更的复杂流程的系统也同样适用。希望通过本文的介绍,能使有状态机了解和使用需求的读者朋友有所收获。

 

本文作者:董天,马蜂窝大交通研发团队机票交易系统研发工程师。

(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)

 

关注马蜂窝技术,找到更多你想要的内容

如何画出一张合格的技术架构图?

mumupudding阅读(18)

阿里妹导读:技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,也体现在优秀工程师在工作效率提升、产品性能优化和用户体验改善等经验方面的分享,以提高我们的专业能力。

接下来,阿里巴巴技术专家三画,将分享自己和团队在画好架构图方面的理念和经验,希望对你有所帮助。

当我们想用一张或几张图来描述我们的系统时,是不是经常遇到以下情况:

  • 对着画布无从下手、删了又来?
  • 如何用一张图描述我的系统,并且让产品、运营、开发都能看明白?
  • 画了一半的图还不清楚受众是谁?
  • 画出来的图到底是产品图功能图还是技术图又或是大杂烩?
  • 图上的框框有点少是不是要找点儿框框加进来?
  • 布局怎么画都不满意……

如果有同样的困惑,本文将介绍一种画图的方法论,来让架构图更清晰。

先厘清一些基础概念

1、什么是架构?

架构就是对系统中的实体以及实体之间的关系所进行的抽象描述,是一系列的决策。

架构是结构和愿景。

系统架构是概念的体现,是对物/信息的功能与形式元素之间的对应情况所做的分配,是对元素之间的关系以及元素同周边环境之间的关系所做的定义。

做好架构是个复杂的任务,也是个很大的话题,本篇就不做深入了。有了架构之后,就需要让干系人理解、遵循相关决策。

2、什么是架构图?

系统架构图是为了抽象地表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。

3、架构图的作用

一图胜千言。要让干系人理解、遵循架构决策,就需要把架构信息传递出去。架构图就是一个很好的载体。那么,画架构图是为了:

  • 解决沟通障碍
  • 达成共识
  • 减少歧义

4、架构图分类

搜集了很多资料,分类有很多,有一种比较流行的是4+1视图,分别为场景视图、逻辑视图、物理视图、处理流程视图和开发视图。

场景视图

场景视图用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示。

逻辑视图

逻辑视图用于描述系统软件功能拆解后的组件关系,组件约束和边界,反映系统整体组成与系统如何构建的过程,通常由UML的组件图和类图来表示。

物理视图

物理视图用于描述系统软件到物理硬件的映射关系,反映出系统的组件是如何部署到一组可计算机器节点上,用于指导软件系统的部署实施过程。

处理流程视图

处理流程视图用于描述系统软件组件之间的通信时序,数据的输入输出,反映系统的功能流程与数据流程,通常由时序图和流程图表示。

开发视图

开发视图用于描述系统的模块划分和组成,以及细化到内部包的组成设计,服务于开发人员,反映系统开发实施过程。

以上 5 种架构视图从不同角度表示一个软件系统的不同特征,组合到一起作为架构蓝图描述系统架构。

怎样的架构图是好的架构图

上面的分类是前人的经验总结,图也是从网上摘来的,那么这些图画的好不好呢?是不是我们要依葫芦画瓢去画这样一些图?

先不去管这些图好不好,我们通过对这些图的分类以及作用,思考了一下,总结下来,我们认为,在画出一个好的架构图之前, 首先应该要明确其受众,再想清楚要给他们传递什么信息 ,所以,不要为了画一个物理视图去画物理视图,为了画一个逻辑视图去画逻辑视图,而应该根据受众的不同,传递的信息的不同,用图准确地表达出来,最后的图可能就是在这样一些分类里。那么,画出的图好不好的一个直接标准就是:受众有没有准确接收到想传递的信息。

明确这两点之后,从受众角度来说,一个好的架构图是不需要解释的,它应该是自描述的,并且要具备一致性和足够的准确性,能够与代码相呼应。

画架构图遇到的常见问题

1、方框代表什么?

为什么适用方框而不是圆形,它有什么特殊的含义吗?随意使用方框或者其它形状可能会引起混淆。

2、虚线、实线什么意思?箭头什么意思?颜色什么意思?

随意使用线条或者箭头可能会引起误会。

3、运行时与编译时冲突?层级冲突?

架构是一项复杂的工作,只使用单个图表来表示架构很容易造成莫名其妙的语义混乱。

本文推荐的画图方法

C4 模型使用容器(应用程序、数据存储、微服务等)、组件和代码来描述一个软件系统的静态结构。这几种图比较容易画,也给出了画图要点,但最关键的是,我们认为,它明确指出了每种图可能的受众以及意义。

下面的案例来自C4官网,然后加上了一些我们的理解,来看看如何更好的表达软件架构

1、语境图(System Context Diagram)

这是一个想象的待建设的互联网银行系统,它使用外部的大型机银行系统存取客户账户、交易信息,通过外部电邮系统给客户发邮件。可以看到,非常简单、清晰,相信不需要解释,都看的明白,里面包含了需要建设的系统本身,系统的客户,和这个系统有交互的周边系统。

用途

这样一个简单的图,可以告诉我们,要构建的系统是什么;它的用户是谁,谁会用它,它要如何融入已有的IT环境。这个图的受众可以是开发团队的内部人员、外部的技术或非技术人员。即:

  • 构建的系统是什么
  • 谁会用它
  • 如何融入已有的IT环境

怎么画

中间是自己的系统,周围是用户和其它与之相互作用的系统。这个图的关键就是梳理清楚待建设系统的用户和高层次的依赖,梳理清楚了画下来只需要几分钟时间。

2、容器图(Container Diagram)

容器图是把语境图里待建设的系统做了一个展开。

上图中,除了用户和外围系统,要建设的系统包括一个基于javaspring mvc的web应用提供系统的功能入口,基于xamarin架构的手机app提供手机端的功能入口,一个基于java的api应用提供服务,一个mysql数据库用于存储,各个应用之间的交互都在箭头线上写明了。

看这张图的时候,不会去关注到图中是直角方框还是圆角方框,不会关注是实线箭头还是虚线箭头,甚至箭头的指向也没有引起太多注意。

我们有许多的画图方式,都对框、线的含义做了定义,这就需要画图的人和看图的人都清晰的理解这些定义,才能读全图里的信息,而现实是,这往往是非常高的一个要求,所以,很多图只能看个大概的含义。

用途

这个图的受众可以是团队内部或外部的开发人员,也可以是运维人员。用途可以罗列为:

  • 展现了软件系统的整体形态
  • 体现了高层次的技术决策
  • 系统中的职责是如何分布的,容器间的是如何交互的
  • 告诉开发者在哪里写代码

怎么画

用一个框图来表示,内部可能包括名称、技术选择、职责,以及这些框图之间的交互,如果涉及外部系统,最好明确边界。

3、组件图(Component Diagram)

组件图是把某个容器进行展开,描述其内部的模块。

用途

这个图主要是给内部开发人员看的,怎么去做代码的组织和构建。其用途有:

  • 描述了系统由哪些组件/服务组成
  • 厘清了组件之间的关系和依赖
  • 为软件开发如何分解交付提供了框架

4、类图(Code/Class Diagram)

这个图很显然是给技术人员看的,比较常见,就不详细介绍了。

案例分享

下面是内部的一个实时数据工具的架构图。作为一个应该自描述的架构图,这里不多做解释了。如果有看不明白的,那肯定是还画的不够好。

画好架构图可能有许多方法论,本篇主要介绍了C4这种方法,C4的理论也是不断进化的。但不论是哪种画图方法论,我们回到画图初衷,更好的交流,我们在画的过程中不必被条条框框所限制。简而言之,画之前想好:画图给谁看,看什么,怎么样不解释就看懂。

作者简介:三画,阿里巴巴技术专家,梓敬、鹏升和余乐对此文亦有贡献。三画曾多年从事工作流引擎研发工作,现专注于高并发移动互联网应用的架构和开发,和本文贡献者均来自阿里巴巴零售通部门。目前零售通大量招Java开发,欢迎有志之士投简历到 lst-wireless@alibaba-inc.com,和我们一起共建智能分销网络,让百万小店拥抱DT时代。

作者:三画

原文链接

本文为云栖社区原创内容,未经允许不得转载。

揭秘京东区块链开源项目——JD Chain

mumupudding阅读(19)

导言

近日,京东区块链底层引擎JD Chain正式对外开源并同步上线开源社区,旨在为企业级用户和开发者提供开源服务,帮助他们提高研发效率,加速技术创新。3月30日,国家互联网信息办公室公布了第一批区块链信息服务名称及备案编号,其中京东区块链BaaS平台、京东区块链防伪追溯通用平台等榜上有名。4月9日,京东发布《京东区块链技术实践白皮书(2019)》,总结了京东区块链在五大类应用场景中的技术实践,介绍了一系列落地案例。同时,白皮书还介绍了京东区块链的技术优势、体系架构与未来规划。

白皮书指出,京东区块链的技术架构分为JD Chain和JD BaaS两部分。其中,JD Chain作为核心引擎,聚焦解决区块链底层的关键技术问题,建立拥有中国自主知识产权的技术生态。JD BaaS是企业级服务平台,提供灵活易用和可伸缩的区块链系统管理能力,支持企业级用户在公有云、私有云及混合云环境快速部署,降低企业使用成本,促进应用落地。

JD Chain简介

01 高性能安全,功能“积木化”

区块链是一种新型分布式架构,以密码学和分布式技术为核心,无需借助“第三方” 就能在多个业务方之间进行安全、可信、直接的信息和价值交换。在这种点对点的信息和价值的交换中,区块链起到了“协议”的作用。

JD Chain团队认为区块链的5大核心技术是:密码算法、共识协议、数据账本模型、数据存储、API。JD Chain在这5个方向上重点突破,从企业的实际需求出发,在设计上推进性能优化、操作简化、安全强化和场景适配通用化,形成如图1中的关键技术特性:

揭秘京东区块链开源项目——JD Chain

   图1  JD Chain关键技术特性

高性能:采用全新的底层架构设计,交易处理达到万级TPS,交易确认缩短至秒级,支持海量存储和高性能密码算法。

积木化定制:共识、账本、合约、存储各自独立,标准接口交互通信,可实现灵活切换不同的密码算法。

强安全和隐私保护:提供多种具有隐私保护能力的算法,支持包括国密算法在内的多套密码体系。

有效数据治理:数据账本采用标准化结构设计,支持业务数据穿透检索、多维分析治理,支持数据的独立备份、归档、监管和审计。

多链协同:支持业务的多链管理,链间数据验证与交易执行,链的拆分与合并,同时可组合轻量公链模式。

低成本易维护:支持轻量网关节点部署,数据可无需开发合约快速上链,合约代码可复用、升级、本地化测试。

02 搭建新环境,重塑主体“关系”

JD Chain为企业提供了一个全新的数据底层,企业可以根据需求配置所需功能组件。万级交易处理速度,秒级交易快速确认,支持多链协同管理等优异性能,能够帮助企业实现更有效的链上数据治理,同时兼容多密码体系,确保数据的安全与隐私。JD Chain为企业业务模式创新提供了一种新的技术支撑,使其能够重塑各参与主体关系,开辟信任经济商业新领地。

具体来说,JD Chain的功能层次分为4个部分:网关服务、共识服务、数据账本和工具包,架构体系如下图2。

揭秘京东区块链开源项目——JD Chain

 图2  JD Chain架构体系

网关服务:JD Chain的网关服务是应用的接入层,提供终端接入、私钥托管、安全隐私和协议转换等功能。

数据账本:数据账本为各参与方提供区块链底层服务功能,包括区块、账户、配置和存储等。

共识服务:共识服务是JD Chain的核心实现层,包括共识网络、身份管理、安全权限、交易处理、智能合约和数据检索等功能,来保证各节点间账本信息的一致性。

工具包:节点可以使用JD Chain中提供的工具包获取上述三个层级的功能服务,并响应相关应用和业务。工具包贯穿整个区块链系统,使用者只需调用特定的接口即可使用对应工具。工具包包括数据管理、开发包(SDK)、安装部署和服务监控等。

03 多模型选择,简部署“量身打造”

各企业的信息化基础设施、技术能力、应用场景往往千差万别,不同的情况下如何选择适合自身的部署方式,往往是每个企业都会面临的实际问题。

JD Chain从易用性方面考虑到实际应用规模提供了面向中小型企业和大型企业两种不同的部署方案。

中小型企业可以直接采用如下图3、4的最简部署模型(只需一个客户端节点、一个网关节点和多个共识节点即可),它是保障JD Chain可正常运行的最低配置,在硬件条件满足的情况下,可以支持亿级交易,通常用于Demo实验或小型应用。另外,JD Chain的数据服务功能作为可选组件,支持链上数据的检索、汇总等功能(数据服务组件与共识节点部署在相同或不同服务器均可)。

揭秘京东区块链开源项目——JD Chain

图3  最简部署模型

揭秘京东区块链开源项目——JD Chain
 图4  加入数据服务的最简部署模型

随着应用级别的提升,数据存储的需求越来越大,每个共识节点可采用数据库集群的方式实现存储的平行化扩展(在这种方式下可支持交易级别达到十亿乃至更多),如图5。在某些中型实际应用中,共识节点会由不同的业务方安装部署,将共识节点集群化提升了系统整体的安全性和可扩展性,如图6。

揭秘京东区块链开源项目——JD Chain
图5  数据库集群部署模型

揭秘京东区块链开源项目——JD Chain

图6  数据库、共识节点集群部署模型

面对大型企业应用中极其复杂的业务关系和应用场景,JD Chain提供了对应的部署解决方案。在整个部署模型中涉及到多种类型的参与方、不同类型的终端,这些终端可以从任意授权的网关节点采用不同的接入方式加入区块链网络,如图7。

揭秘京东区块链开源项目——JD Chain

图7  大型企业应用部署模型

京东区块链技术实践白皮书

01 品质溯源助力食品药品安全和精准扶贫

据不完全统计,全球范围内受假冒伪劣商品影响的市场规模高达3000亿美元,其中有关食品、药品安全事件频发,由此产生的信任危机受到社会高度关注,运用技术手段加以解决,成为了政府和企业关注的重点。

基于区块链技术的去中心化、共识机制、不可篡改、信息可追溯等特点,京东区块链防伪追溯平台推出了消费品解决方案和医药行业解决方案。截至今年2月,平台已经累计有超过700家品牌商和超过5万个SKU入驻,入驻品牌商包括雀巢、惠氏、洋河、伊利等知名企业。平台有逾280万次的售后用户访问,上链数据多达13亿条,产品种类涉及食品、酒类、奶粉、日用品和医药用品,为营造安心可靠的消费体验和医疗服务做出了贡献。

比如,澳大利亚领先肉类产品出口商安格斯通过与京东区块链防伪追溯平台的深度合作,让国内消费者能够通过扫描包装上的二维码,了解到从牛的出生、生长、检疫、屠宰、加工、运输等全部信息,期间每一个环节都有自动记录、每一个环节都不能被人为篡改、每一个环节都能公示给消费者,让每一片牛肉都安全可靠。

与此同时,京东区块链的防伪追溯技术还应用在了精准扶贫领域,京东在国家级贫困县落地的“跑步鸡”、“游水鸭”和“飞翔鸽”等项目,通过计步脚环等物联网设备,结合视频溯源技术,将家禽运动数据、喂食、饮水、除虫等信息进行采集,并记录到区块链网络中,消费者扫码即可了解到所购农产品的养殖过程、生长环境等图文信息,在提升消费体验的同时,也为贫困地区的农民增加了收入。

02 数字存证和信用网络 服务诚信体系建设

在数字存证方面,京东区块链数字存证平台实现了可信存证、自动化取证、一键举证、侵权预警等功能,目前已经应用于电子合同、电子发票、电子证照、电子票据、互联网诉讼、版权保护等场景。

近日,京东集团与广州互联网法院共同签署了可信电子证据平台和司法信用共治平台两方面的合作协议,双方将利用各自的专业经验与技术优势共同确保证据数据过程可溯、记录可查,实现证据数据存储安全、验证便捷,且共同遵循安全、公正、中立、开放原则,妥善保管证据数据。同时,双方还将在依照法律法规和用户授权的前提下,共享司法信用信息,为推动网络空间信用体系建设提供有效支持。

除此之外,“京小租”是业内首家使用区块链技术解决消费租赁市场纠纷取证难问题的信用租赁平台,用户在进行商品租赁时,京小租平台通过自动化流程获取租赁业务中租赁协议、订单数据、租赁流程等数据并完成“上链”操作,保证租赁服务的公开透明。

在信用网络方面,区块链技术的不可篡改性和透明性可以服务于社会信用体系的建设,解决以往信用体系的痛点,辅助监管机构实现对社会主体的信用评价。京东区块链正在运用技术手段在数字身份、企业通用账号、信用租赁、物流征信等方面,在为完善社会信用体系提供助力的同时,也为企业经营和个人生活提供了便利。

揭秘京东区块链开源项目——JD Chain

开发者社区同步上线

JD Chain已在近日对外开源并同步上线了开源社区(http://ledger.jd.com/)。JD Chain开源对于行业和开发者来说都具有重要意义。

开放JD Chain高质量的技术代码、简明清晰的设计文档和代码示例,将帮助开发者快速建立明确、有效的学习路径,快速进入区块链技术领域;

JD Chain开源能够帮助企业提高研发效率;

JD Chain开源将促进区块链技术应用生态的构建、加速助推我国区块链技术的发展。

 

·END·