阿里 memcached 客户端源码分析

本贴最后更新于 2802 天前,其中的信息可能已经物是人非

由于需要对GAP平台的缓存解决方案进行扩充,我们选择了memcached作为分布式缓存的一种可选方案,它是基于内存的一种分布式缓存系统,主要面向较小的对象,比如数据库返回值,api调用返回值以及页面渲染结果等,同时在选择客户端的时候,使用了阿里文初改造后的客户端memchaced-client-forjava,该客户端经过阿里内部大量实际项目的线上运行,表现都很稳定。下面首先会分析该客户端的源码结构,然后说说我们对其改造的部分。

源码分析

memcached本身是一个集中式的内存缓存系统,对于分布式的支持服务端并没有实现,只有通过客户端实现;再者,memcached是基于TCP/UDP进行通信,只要客户端语言支持TCP/UDP即可实现客户端,并且可以根据需要进行功能扩展。memchaced-client-forjava 既是使用java语言实现的客户端,并且实现了自己的功能扩展,下面这张类图描述了其主要类之间的关系。

memcached

几个重要类的说明:

  • MemcachedCacheManager: 管理类,负责缓存服务端,客户端,以及相关资源池的初始化工作,获取客户端等等
  • MemcachedCache:memcached缓存实体类,实现了所有的缓存API,实际上也会调用MemcachedClient进行操作
  • MemcachedClient:memcached缓存客户端,一个逻辑概念,负责与服务端实例的实际交互,通过调用sockiopool中的socket
  • SockIOPool:socket连接资源池,负责与memcached服务端进行交互
  • ClusterProcessor:集群内数据异步操作工具类

客户端可配置化

MemcachedCacheManager是入口,其start方法读取配置文件memcached.xml,初始化各个组建,包括memcached客户端,socket连接池以及集群节点。
memcached客户端是个逻辑概念,并不是和memcached服务端实例一一对应的,可以认为其是一个逻辑环上的某个节点(后面会讲到hash一致性算法时涉及),该配置文件中,可配置一个或多个客户端,每个客户端可配置一个socketPool连接池,如下:

<client name="mclient0" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool0”>
    <errorHandler>com.alisoft.xplatform.asf.cache.memcached.MemcachedErrorHandler</errorHandler>
</client>

扩容

socketpool连接池配置的才是真正连接的memcached服务实例,当然,你可以连接多个memcached服务实例,多个实例可以分布在一台或者多台物理机器上。这样,随着实际业务数据量的增加,可以对现有缓存容量进行扩容,只需在servers中增加memcached实例即可,或者增加多个socketpool配置项,配置如下:

<socketpool name="pool0" failover="true" initConn="5" minConn="5" maxConn="250" maintSleep="5000" nagle="false" socketTO="3000" aliveCheck="true">
    <servers>192.168.1.66:11211,192.168.1.68:11211</servers>
</socketpool>

初始化过程

上文提及的MemcachedCacheManager,该类功能包括有初始化各种资源池,获取所有客户端,重新加载配置文件以及集群复制等。我们重点分析方法start,该方法首先加载配置文件,然后初始化资源池,即方法initMemCacheClientPool,该方法中定义了三个资源池,即socket连接资源池socketpool,memcachedcache资源池cachepool,以及由客户端组成的集群资源池clusterpool,这些资源池的数据结构都是线程安全的ConcurrentHashMap,保证了并发效率。将配置信息分别实例化后,再分别放入对应的资源池容器中,socket连接放入socketpool中,memcached客户端放入cachepool中,定义的集群节点放入clusterpool中。
注意,在实例化socket连接池资源socketpool时,会调用每个pool的初始化方法pool.initialize(),来映射memcached实例到HASH环上,以及初始化socket连接。

单点问题

memcached的分布式,解决了容量水平扩容的问题,但是当某个节点失效时,还是会丢失一部分数据,单点故障依然存在,分布式只是解决了数据整体失效问题,而在实际项目中,特别是GAP平台适应的企业级项目中,是不允许数据不一致的,所以对每一份保存的数据都需要进行容灾处理,那么对于定义的每个memcached客户端,都至少增加一个新客户端与其组成一个cluster集群,当更新或者查找数据时,会先定位到该集群中某个节点,如果该节点失效,就去另外一个节点进行操作。在实际项目中,通过合理规划配置cluster和client(memcached客户端),可以最大限度的避免单点故障(当所有client都失效时还会丢失数据)。在配置文件中,集群配置如下:

<cluster name="cluster1" mode="active">
    <memCachedClients>mclient1,mclient2</memCachedClients>
</cluster>

下图展现了扩容和单点故障解决方案:

scalout

HASH一致性算法

在memcached支持分布式部署场景下,如何获取一个memcached实例?如何平均分配memcached实例的存储?这些需要一个算法来实现,我们选择的是HASH一致性算法,具体就体现在客户端如何获取一个连接memcached服务端的socket上,也就是如何定位memcached实例的问题?算法要求能够根据每次提供的同一个key获得同一个实例。

HASH闭环的初始化

本质上,hash一致性算法是需要实现一个逻辑环,如图所示,环上所有的节点即为一个memcached实例,如何实现?其实是根据每个memcached实例所在的ip地址,将所有的实例映射到hash数值空间中,构成一个闭合的圆环。

hash

HASH环映射的初始化的代码位于SocketIOPool.populateConsistentBuckets方法中,主要代码如下:

private void populateConsistentBuckets()
     {
         ……...
          for (int i = 0; i < servers.length; i++)
          {
               int thisWeight = 1;
               if (this.weights != null && this.weights[i] != null)
                    thisWeight = this.weights[i];
                    double factor = Math .floor(((double) (40 * this.servers.length * thisWeight)) / (double) this.totalWeight);
               for (long j = 0; j < factor; j++)
               {
                    byte[] d = md5.digest((servers[i] + "-" + j).getBytes());
                    for (int h = 0; h < 4; h++)
                    {
                         // k 的值使用MD5hash算法计算获得
                         Long k = ((long) (d[3 + h * 4] & 0xFF) << 24)
                                   | ((long) (d[2 + h * 4] & 0xFF) << 16)
                                   | ((long) (d[1 + h * 4] & 0xFF) << 8)
                                   | ((long) (d[0 + h * 4] & 0xFF));
                         // 用treemap来存储memcached实例所在的ip地址,
                         // 也就是将每个缓存实例所在的ip地址映射到由k组成的hash环上
                        consistentBuckets.put(k, servers[i]);
                         if (log.isDebugEnabled())
                              log.debug("++++ added " + servers[i]
                                        + " to server bucket");
                    }
               }
           ……...
          }
     }

获取socket连接

在实际获取memcahced实例所在服务器的soket时,只要使用基于同一个存储对象的key的MD5Hash算法,就可以获得相同的memcached实例所在的ip地址,也就是可以准确定位到hash环上相同的节点,代码位于SocketIOPool.getSock方法中,主要代码如下:

public SockIO getSock(String key, Integer hashCode){
          ………….
      // from here on, we are working w/ multiple servers
      // keep trying different servers until we find one
      // making sure we only try each server one time
      Set&lt;String&gt; tryServers = new HashSet&lt;String&gt;(Arrays.asList(servers));
      // get initial bucket
      // 通过key值计算hash值,使用的是基于MD5的算法
      long bucket = getBucket(key, hashCode);
      String server = (this.hashingAlg == CONSISTENT_HASH) ? consistentBuckets .get(bucket)  : buckets.g                          et((int) bucket);
       &hellip;&hellip;&hellip;&hellip;...  
 }
 private long getBucket(String key, Integer hashCode)
 {
      / / 通过key值计算hash值,使用的是基于MD5的算法
      long hc = getHash(key, hashCode);
      if (this.hashingAlg == CONSISTENT_HASH)
      {
           return findPointFor(hc);
      } else
      {
           long bucket = hc % buckets.size();
           if (bucket &lt; 0)
                bucket *= -1;
           return bucket;
      }
 }
 /**
  * Gets the first available key equal or above the given one, if none found,
  * returns the first k in the bucket
  *
  * @param k
  *            key
  * @return
  */
 private Long findPointFor(Long hv)
 {
      // this works in java 6, but still want to release support for java5
      // Long k = this.consistentBuckets.ceilingKey( hv );
      // return ( k == null ) ? this.consistentBuckets.firstKey() : k;
      // 该consistentBuckets中存储的是HASH结构初始化时,存入的所有memcahced实例节点,也就是整个hash环
      // tailMap方法是取出大于等于hv的所有节点,并且是递增有序的
      SortedMap&lt;Long, String&gt; tmap = this.consistentBuckets.tailMap(hv);
      // 如果tmap为空,就默认返回hash环上的第一个值,否则就返回最接近hv值的那个节点
      return (tmap.isEmpty()) ? this.consistentBuckets.firstKey() : tmap .firstKey();
 }
/**
  * Returns a bucket to check for a given key.
  *
  * @param key
  *            String key cache is stored under
  * @return int bucket
  */
 private long getHash(String key, Integer hashCode)
 {
      if (hashCode != null)
      {
           if (hashingAlg == CONSISTENT_HASH)
                return hashCode.longValue() &amp; 0xffffffffL;
           else
                return hashCode.longValue();
      } else
      {
           switch (hashingAlg)
           {
           case NATIVE_HASH:
                return (long) key.hashCode();
           case OLD_COMPAT_HASH:
                return origCompatHashingAlg(key);
           case NEW_COMPAT_HASH:
                return newCompatHashingAlg(key);
           case CONSISTENT_HASH:
                return md5HashingAlg(key);
           default:
                // use the native hash as a default
                hashingAlg = NATIVE_HASH;
                return (long) key.hashCode();
           }
      }
 }
/**
  * Internal private hashing method.
  *
  * MD5 based hash algorithm for use in the consistent hashing approach.
  *
  * @param key
  * @return
  */
 private static long md5HashingAlg(String key)
 {
      / /通过key值计算hash值,使用的是基于MD5的算法
      MessageDigest md5 = MD5.get();
      md5.reset();
      md5.update(key.getBytes());
      byte[] bKey = md5.digest();
      long res = ((long) (bKey[3] &amp; 0xFF) &lt;&lt; 24)
                | ((long) (bKey[2] &amp; 0xFF) &lt;&lt; 16)
                | ((long) (bKey[1] &amp; 0xFF) &lt;&lt; 8) | (long) (bKey[0] &amp; 0xFF);
      return res;
 }</pre>

通过以上代码的分析,整个memcahced服务端实例HASH环的初始化,以及数据更新和查找使用的算法都是基于同一种算法,这就保证了通过同一个key获得的memcahced实例为同一个。

socket连接池

这部分单独介绍,请猛烈地戳这里

容灾、故障转移以及性能

衡量系统的稳定性,很大程度上是对各种异常情况的处理,充分考虑异常情况,以及合理处理异常是对系统设计人员的要求,下面看看在故障处理和容灾方面系统都做了那些工作。

  • 定位memcached实例时,当第一次定位失败,会对所有其他的属于同一个socketpool中的memcahced实例进行定位,找到一个可用的,代码如下:
// log that we tried
 // 先删除定位失败的实例
 tryServers.remove(server);
 if (tryServers.isEmpty())
     break;
 // if we failed to get a socket from this server
 // then we try again by adding an incrementer to the
 // current key and then rehashing
 int rehashTries = 0;
 while (!tryServers.contains(server))
 {
   // 重新计算key值
   String newKey = new StringBuilder().append(rehashTries).append(key).toString();
   // String.format( "%s%s", rehashTries, key );
   if (log.isDebugEnabled())
       log.debug("rehashing with: " + newKey);
   // 去HASH环上定位实例节点
   bucket = getBucket(newKey, null);
   server=(this.hashingAlg == CONSISTENT_HASH) ? consistentBuckets.get(bucket) : buckets.get((int) bucket);
   rehashTries++;
  }
  • 查找数据时,当前节点获取不到,会尝试到所在集群中其他的节点查找,成功后,会将数据复制到原先失效的节点中,代码如下:
public Object get(String key)
     {
          Object result = null;
          boolean isError = false;  
       …….......
          if (result == null && helper.hasCluster())
           if (isError || helper.getClusterMode().equals(MemcachedClientClusterConfig.CLUSTER_MODE_ACTIVE))
          {
               List<MemCachedClient> caches = helper.getClusterCache();
               for(MemCachedClient cache : caches)
               {
                    if (getCacheClient(key).equals(cache))
                         continue;
                    try{ try
                         {
                              result = cache.get(key);
                         }
                         catch(MemcachedException ex)
                         {
                              Logger.error(new StringBuilder(helper.getCacheName())
                                   .append(" cluster get error"),ex);
                              continue;
                         }
                         //仅仅判断另一台备份机器,不多次判断,防止效率低下
                         if (helper.getClusterMode().equals(MemcachedClientClusterConfig.CLUSTER_MODE_ACTIVE) && result != null)
                         {
                              Object[] commands = new Object[]{CacheCommand.RECOVER,key,result};
                             // 加入队列,异步执行复制数据
                              addCommandToQueue(commands);
                         }
                         break;    
                    }
                    catch(Exception e)
                    {
                     Logger.error(new StringBuilder(helper.getCacheName()) .append(" cluster get error"),e);
                    }
               }
          }
          return result;
     }
  • 更新数据时,异步更新到集群内其他节点,示例代码如下:
public boolean add(String key, Object value)
     {
          boolean result = getCacheClient(key).add(key,value);
          if (helper.hasCluster())
          {
               Object[] commands = new Object[]{CacheCommand.ADD,key,value};
               // 加入队列,异步执行
               addCommandToQueue(commands);
          }
          return result;
     }
  • 删除数据时,需要同步执行,如果异步的话,会产生脏数据,代码如下:
public Object remove(String key)
     {
          Object result = getCacheClient(key).delete(key);
          //异步删除由于集群会导致无法被删除,因此需要一次性全部清除
          if (helper.hasCluster())
          {
               List<MemCachedClient> caches = helper.getClusterCache();
           for(MemCachedClient cache : caches)
           {
                if (getCacheClient(key).equals(cache))
                     continue;
                try
                {
                     cache.delete(key);
                }
                catch(Exception ex)
                {
                     Logger.error(new StringBuilder(helper.getCacheName())
                                    .append(" cluster remove error"),ex);
                }
           }
      }
      return result;
 }</pre>
  • 异步执行集群内数据同步,因为不可能每次数据都要同步执行到集群内每个节点,这样会降低系统性能;所以在构造MemcachedCache对象时,会建立一个队列,线程安全的linked阻塞队列LinkedBlockingQueue,将所有需要异步执行的命令放入队列中,异步执行,具体异步执行由ClusterProcessor类负责。
public MemcachedCache(MemCachedClientHelper helper,int statisticsInterval)
     {
          this.helper = helper; 
          dataQueue = new LinkedBlockingQueue<Object[]>();
         ………
          processor = new ClusterProcessor(dataQueue,helper);
          processor.setDaemon(true);
          processor.start();
     }
  • 本地缓存的使用是为了降低连接服务端的IO开销,当有些数据变化频率很低时,完全可以放在应用服务器本地,同时可以设置有效时间,直接获取。DefaultCacheImpl类为本地缓存的实现类,在构造MemcachedCache对象时,即初始化。

每次查找数据时,会先查找本地缓存,如果没有再去查缓存,结束后将数据让如本地缓存中,代码如下:

public Object get(String key, int localTTL)
     {
          Object result = null;
         // 本地缓存中查找
          result = localCache.get(key);
          if (result == null)
          {
               result = get(key);
               if (result != null)
               {
                    Calendar calendar = Calendar.getInstance();
                    calendar.add(Calendar.SECOND, localTTL);
                    // 放入本地缓存
                    localCache.put(key, result,calendar.getTime());
               }
          }
          return result;
     }

增加缓存数据时,会删除本地缓存中对应的数据,代码如下:

public Object put(String key, Object value, Date expiry)
     {
          boolean result = getCacheClient(key).set(key,value,expiry);
          //移除本地缓存的内容
          if (result) localCache.remove(key);
        ……..
          return value;
     }

改造部分

据以上分析,我们通过封装,做到了客户端的可配置化,memcached实例的水平扩展,通过集群解决了单点故障问题,并且保证了应用程序只要每次使用相同的数据对象的key值即可获取相同的memcached实例进行操作。但是,为了使缓存的使用对于应用程序来说完全透明,我们对cluster部分进行了再次封装,即把cluster看做一个node,根据cluster名称属性,进行HASH数值空间计算(同样基于MD5算法),映射到一个HASH环上,如下图:

cluster

这部分逻辑放在初始化资源池clusterpool时进行(即放在MemcahedCacheManager.initMemCacheClientPool方法中),与上文中所描述的memcached实例HASH环映射的逻辑一致,部分代码如下:

//populate cluster node to hash consistent Buckets
 MessageDigest md5 = MD5.get();
 // 使用cluster的名称计算HASH数值空间
 byte[] d = md5.digest((node.getName()).getBytes());
 for (int h = 0; h < 4; h++)
 {
    Long k = ((long) (d[3 + h * 4] & 0xFF) << 24)
                        | ((long) (d[2 + h * 4] & 0xFF) << 16)
                        | ((long) (d[1 + h * 4] & 0xFF) << 8)
                        | ((long) (d[0 + h * 4] & 0xFF));
   consistentClusterBuckets.put(k, node.getName());
   if (log.isDebugEnabled())
        log.debug("++++ added " + node.getName() + " to cluster bucket");
 } 

在进行缓存操作时,仍然使用数据对象的key值获取到某个cluster节点,然后再使用取余算法(这种算法也是经常用到的分布式定位算法,但是有局限性,即随着节点数的增减,定位越来越不准确),拿到cluster中的某个节点,在进行缓存的操作;定位hash环上cluster节点的逻辑也与上文一样,这里不在赘述。部分定位cluster中节点的取余算法代码如下:

public IMemcachedCache getCacheClient(String key){
       ………….
       String clusterNode = getClusterNode(key);
        MemcachedClientCluster mcc = clusterpool.get(clusterNode);
        List<IMemcachedCache> memcachedCachesClients = mcc.getCaches();
        //根据取余算法获取集群中的某一个缓存节点
        if (!memcachedCachesClients.isEmpty())
        {
            long keyhash = key.hashCode();
            int index = (int)keyhash % memcachedCachesClients.size();
            if (index < 0 )
                index *= -1;
            return memcachedCachesClients.get(index);
        }
        return null;
    }

这样,对于应用来说,配置好资源池以后,无需关心那个集群或者客户端节点,直接通过MemcachedCacheManager获取到某个memcachedcache,然后进行缓存操作即可。

最后,使用GAP平台分布式缓存组件,需要提前做好容量规划,集群和客户端事先配置好;另外,缓存组件没有提供数据持久化功能。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    915 引用 • 931 回帖
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    180 引用 • 400 回帖
  • OAuth

    OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。

    36 引用 • 103 回帖 • 10 关注
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    116 引用 • 99 回帖 • 269 关注
  • CSDN

    CSDN (Chinese Software Developer Network) 创立于 1999 年,是中国的 IT 社区和服务平台,为中国的软件开发者和 IT 从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。

    14 引用 • 155 回帖 • 1 关注
  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    164 引用 • 407 回帖 • 527 关注
  • RIP

    愿逝者安息!

    8 引用 • 92 回帖 • 291 关注
  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    4 引用 • 55 回帖 • 8 关注
  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖 • 2 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    284 引用 • 4481 回帖 • 654 关注
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    5 引用 • 15 回帖 • 222 关注
  • 音乐

    你听到信仰的声音了么?

    59 引用 • 509 回帖
  • 以太坊

    以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。

    34 引用 • 367 回帖 • 2 关注
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    173 引用 • 990 回帖
  • Flutter

    Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。 Flutter 可以与现有的代码一起工作,它正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。

    39 引用 • 92 回帖 • 8 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    5 引用 • 13 回帖
  • JSON

    JSON (JavaScript Object Notation)是一种轻量级的数据交换格式。易于人类阅读和编写。同时也易于机器解析和生成。

    51 引用 • 190 回帖 • 1 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    129 引用 • 793 回帖 • 2 关注
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 246 关注
  • BookxNote

    BookxNote 是一款全新的电子书学习工具,助力您的学习与思考,让您的大脑更高效的记忆。

    笔记整理交给我,一心只读圣贤书。

    1 引用 • 1 回帖
  • JetBrains

    JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA

    18 引用 • 54 回帖 • 1 关注
  • PWA

    PWA(Progressive Web App)是 Google 在 2015 年提出、2016 年 6 月开始推广的项目。它结合了一系列现代 Web 技术,在网页应用中实现和原生应用相近的用户体验。

    14 引用 • 69 回帖 • 132 关注
  • 区块链

    区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。

    91 引用 • 751 回帖 • 1 关注
  • Eclipse

    Eclipse 是一个开放源代码的、基于 Java 的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。

    75 引用 • 258 回帖 • 627 关注
  • Git

    Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

    205 引用 • 357 回帖 • 1 关注
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    57 引用 • 22 回帖 • 5 关注
  • TensorFlow

    TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。

    20 引用 • 19 回帖