文章目录

  • 需求背景
  • UUID方法
  • 取当前毫秒数
  • SnowFlake 算法
  • 改进的美团Leaf-snowflake优化方案
  • Mongdb objectID
  • 数据库生成
  • Leaf-segment数据库方案
  • 使用数据库的 auto_increment 来生成全局唯一递增ID

需求背景

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。
如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求。
特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。
此时一个能够生成全局唯一ID的系统是非常必要的。
概括下来,那业务系统对ID号的要求有哪些呢?
1.全局唯一性
不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
2.趋势递增
在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS(关系数据库管理系统)使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
3. 单调递增
保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
4. 信息安全
如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则不规则
这也是本文要讨论的核心问题:如何高效生成趋势有序的全局唯一ID。

上述123对应三类不同的场景,3和4需求还是互斥的,无法使用同一个方案满足。

UUID方法

不管是通过数据库,还是通过服务来生成ID,业务方Application都需要进行一次远程调用,比较耗时。
有没有一种本地生成ID的方法,即高性能,又时延低呢?
UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前为止业界一共有5种方式生成UUID,详情见IETF发布的UUID规范 A Universally Unique IDentifier (UUID) URN Namespace。
string ID =GenUUID();

  • 优点

    1. 本地生成ID,不需要进行远程调用,时延低,性能非常高
    2. 扩展性好,基本可以认为没有性能上限
  • 缺点
    1. 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用(“转化为两个uint64整数存储”或者“折半存储”(折半后不能保证唯一性))。
    2. 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
    3. ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
      ① MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。
      ② 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。
    4. 无法保证趋势递增

取当前毫秒数

uuid是一个本地算法,生成性能高,但无法保证趋势递增,且作为字符串ID检索效率低,有没有一种能保证递增的本地算法呢?
取当前毫秒数是一种常见方案:
uint64 ID = GenTimeMS();
优点:
本地生成ID,不需要进行远程调用,时延低
生成的ID趋势递增
生成的ID是整数,建立索引后查询效率高
缺点:
如果并发量超过1000,会生成重复的ID
这个缺点要了命了,不能保证ID的唯一性。当然,使用微秒可以降低冲突概率,但每秒最多只能生成1000000个ID,再多的话就一定会冲突了,所以使用微秒并不从根本上解决问题。

SnowFlake 算法

  • 优点

    1. 保证分布式场景下生成的ID是唯一
    2. 生成的全局ID整体上是呈自增趋势的,也就是说整体是有序
    3. 高性能,能快速产生ID
    4. 只占64bit位空间,可以根据业务需求扩展在前缀或后缀拼接业务标志位转换为字符串
  • 缺点
    1. 由于“没有一个全局时钟”,每台服务器分配的ID是绝对递增的,但从全局看,生成的ID只是趋势递增的(有些服务器的时间早,有些服务器的时间晚)
    2. 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

生成规则算法:

  1. 1位,不用。二进制中,最高位为1的都是负数,但是我们生成的ID一般都使用整数,所以这个最高位固定为0
  2. 41位,用来纪录时间戳(毫秒),41位可以表示2的41次方-1个毫秒的值,转化成单位年则是69年
  3. 10位 用来纪录工作机器ID。可以部署在2的10次方 1024个节点。包括5位的数据中心ID,和5位的机器ID
  4. 12位 序列号,用来纪录同毫秒内产生的不同id.12位bit 可以表示的最大正整数是2的12次方减1 是4095 来表示同一个机器同一个时间戳毫秒内产生4096个序号

核心在于如何分配64的位数
举例:假设某公司ID生成器服务的需求如下:

  1. 单机高峰并发量小于1W,预计未来5年单机高峰小于10w
  2. 有两个机房,预计未来5年机房数量小于4个
  3. 每个机房机器小于100台
  4. 目前有5个业务线有ID生成需求,未来业务线小于10个

分析过程如下,

  1. 高位取 2018年9月22日(系统ID生成器在这个时间之后上线)到现在的毫秒数,假设系统要运行10年,需要103652436001000等于 320 的10的9次方,2的39次方大概刚好大于这个数,则预留39位给毫秒数
  2. 每秒的单机高峰并发小于10W,即平均每毫秒的单机并发量小于100,差不多预留7个bit给每秒内序列号
  3. 5年内机房数小雨4个,预留2个bit
  4. 每个机房小于100个机器,留7个bit给每个机房的服务器标识
  5. 业务线小于10个,预留四个bit给业务标识

这样设计的64bit标识,可以保证:

  • 每个业务线、每个机房、每个机器生成的ID都是不同的
  • 同一个机器,每个毫秒内生成的ID都是不同的
  • 同一个机器,同一个毫秒内,以序列号区区分保证生成的ID是不同的
  • 将毫秒数放在最高位,保证生成的ID是趋势递增的

改进的美团Leaf-snowflake优化方案

Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。面对这一问题,我们提供了 Leaf-snowflake方案

Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是是“1+41+10+12”的方式组装ID号。

对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。
所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。
Leaf-snowflake是按照下面几个步骤启动的:

  1. 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。

  2. 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。

  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

    弱依赖ZooKeeper
    除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动,这样做到了对三方组件的弱依赖,一定程度上提高了SLA(对互联网公司来说就是网站服务可用性的一个保证,9越多代表全年服务可用时间越长服务更可靠,停机时间越短,反之亦然)
    解决时钟问题
    因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

    参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

  4. 若写过,则用自身系统时间与leaf_forever/self节点记录时间做比较,若小于leafforever/{self}节点记录时间做比较,若小于leaf_forever/self节点记录时间做比较,若小于leaff​orever/{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。

  5. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。

  6. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。

  7. 否则认为本机系统时间发生大步长偏移,启动失败并报警。

  8. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,如下:

//发生了回拨,此刻时间小于上次发号时间

 if (timestamp < lastTimestamp) {          long offset = lastTimestamp - timestamp;          if (offset <= 5) {             try {                  //时间偏差大小小于5ms,则等待两倍时间wait(offset << 1);//waittimestamp = timeGen();                if (timestamp < lastTimestamp) {                     //还是小于,抛异常并上报throwClockBackwardsEx(timestamp);}    } catch (InterruptedException e) {  throw  e;}} else {                //throwthrowClockBackwardsEx(timestamp);}} //分配ID

从上线情况来看,在2017年闰秒出现那一次出现过部分机器回拨,由于Leaf-snowflake的策略保证,成功避免了对业务造成的影响。

Mongdb objectID

MongoDB官方文档 ObjectID可以算作是和snowflake类似方法,通过“时间+机器码+pid+inc”共12个字节,通过4+3+2+3的方式最终标识成一个24长度的十六进制字符。

数据库生成

以MySQL举例,利用给字段设置auto_increment_incrementauto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;


这种方案的优缺点如下:
优点:

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。
    缺点:
  • 强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
  • ID发号性能瓶颈限制在单台MySQL的读写性能。
    对于MySQL性能问题,可用如下方案解决:在分布式系统中我们可以多部署几台机器,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器。设置步长step为2,TicketServer1的初始值为1(1,3,5,7,9,11…)、TicketServer2的初始值为2(2,4,6,8,10…)。这是Flickr团队在2010年撰文介绍的一种主键生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap )。如下所示,为了实现上述方案分别设置两台机器对应的参数,TicketServer1从1开始发号,TicketServer2从2开始发号,两台机器每次发号之后都递增2。
TicketServer1:
auto-increment-increment = 1
auto-increment-offset = 2TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

假设我们要部署N台机器,步长需设置为N,每台的初始值依次为0,1,2…N-1那么整个架构就变成了如下图所示:

  • 系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,比如14(假设在扩容时间之内第一台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数。然后摘掉第一台,把ID值保留为奇数,比如7,然后修改第一台的步长为2。让它符合我们定义的号段标准,对于这个例子来说就是让第一台以后只能产生奇数。扩容方案看起来复杂吗?貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。
  • ID没有了单调递增的特性,只能趋势递增,这个缺点对于一般业务需求不是很重要,可以容忍。
  • 数据库压力还是很大,每次获取ID都得读写一次数据库,只能靠堆机器来提高性能。

综合对比上述几种方案,每种方案都不完全符合我们的要求。所以Leaf分别在上述第二种和第三种方案上做了相应的优化,实现了Leaf-segment和Leaf-snowflake方案。

Leaf-segment数据库方案

第一种Leaf-segment方案,在使用数据库的方案上,做了如下改变:

  • 原方案每次获取ID都得读写一次数据库,造成数据库压力大。
  • 改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。
  • 用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
  • 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响
  • 如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

数据库表设计如下:

Field Type Null Key Default Extra
biz_tag varchar(128) NO PRI
max_id bigint(20) NO 1
step int(11) NO NULL
desc varchar(256) YES NULL
update_time timestamp NO CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step,大致架构如下图所示:
  • Leaf Server 1:从DB加载号段[1,1000]。
  • Leaf Server 2:从DB加载号段[1001,2000]。
  • Leaf Server 3:从DB加载号段[2001,3000]。
    用户通过Round-robin的方式调用Leaf Server的各个服务,所以某一个Client获取到的ID序列可能是:1,1001,2001,2,1002,2002……也可能是:1,2,1001,2001,2002,2003,3,4……当某个Leaf Server号段用完之后,下一次请求就会从DB中加载新的号段,这样保证了每次加载的号段是递增的。

test_tag在第一台Leaf机器上是1~ 1000的号段,当这个号段用完时,会去加载另一个长度为step=1000的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是3001~4000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000,更新号段的SQL语句如下:

Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit

这种模式有以下优缺点:
优点:

  • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
  • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。

缺点:

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
  • DB宕机会造成整个系统不可用。
    双buffer优化
    对于第二个缺点,Leaf-segment做了一些优化,简单的说就是:

Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。

为此,我们希望DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。详细实现如下图所示:

采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

Leaf高可用容灾
对于第三点“DB可用性”问题,我们目前采用一主两从的方式,同时分机房部署,Master和Slave之间采用半同步方式[5]同步数据。同时使用公司Atlas数据库中间件(已开源,改名为DBProxy)做主从切换。当然这种方案在一些情况会退化成异步模式,甚至在非常极端情况下仍然会造成数据不一致的情况,但是出现的概率非常小。如果你的系统要保证100%的数据强一致,可以选择使用“类Paxos算法”实现的强一致MySQL方案,如MySQL 5.7前段时间刚刚GA的MySQL Group Replication。但是运维成本和精力都会相应的增加,根据实际情况选型即可。


同时Leaf服务分IDC部署,内部的服务化框架是“MTthrift RPC”。服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施。

Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。面对这一问题,我们提供了 Leaf-snowflake方案。

使用数据库的 auto_increment 来生成全局唯一递增ID

优点

  • 简单,使用数据库已有的功能
  • 能够保证唯一性
  • 能够保证递增性
  • 步长固定

缺点

  • 可用性难以保证:数据库常见架构是一主多从+读写分离,生成自增ID是写请求,主库挂了就玩不转了
  • 扩展性差,性能有上限:因为写入是单点,数据库主库的写性能决定ID的生成性能上限,并且难以扩展
    改进方法:
    冗余主库,避免写入单点
    数据水平切分,保证各主库生成的ID不重复
    如上图所述,由1个写库变成3个写库,每个写库设置不同的auto_increment初始值,以及相同的增长步长,以保证每个数据库生成的ID是不同的(上图中库0生成0,3,6,9…,库1生成1,4,7,10,库2生成2,5,8,11…)
    改进后的架构保证了可用性,但缺点是:
    丧失了ID生成的“绝对递增性”:先访问库0生成0,3,再访问库1生成1,可能导致在非常短的时间内,ID生成不是绝对递增的(这个问题不大,目标是趋势递增,不是绝对递增)
    数据库的写压力依然很大,每次生成ID都要访问数据库
    为了解决上述两个问题,引出了第二个常见的方案

分布式ID生成器方案相关推荐

  1. 分布式id生成器:彻底解决雪花算法时间回拨问题

    Butterfly 简介 雪花算法是twitter提出的分布式id生成器方案,但是有三个问题,其中前两个问题在业内很常见: 时间回拨问题 机器id的分配和回收问题 机器id的上限问题 Butterfl ...

  2. 分布式ID生成器(来源:架构师之路,2017-06-25 58沈剑 架构师之路)

    一.需求缘起 几乎所有的业务系统,都有生成一个唯一记录标识的需求,例如: 消息标识:message-id 订单标识:order-id 帖子标识:tiezi-id 这个记录标识往往就是数据库中的主键,数 ...

  3. 分布式ID生成器的解决方案总结

    转载自 分布式ID生成器的解决方案总结 在互联网的业务系统中,涉及到各种各样的ID,如在支付系统中就会有支付ID.退款ID等.那一般生成ID都有哪些解决方案呢?特别是在复杂的分布式系统业务场景中,我们 ...

  4. 融云发送图片消息_IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现...

    1.引言 很多人一想到IM应用开发,第一印象就是"长连接"."socket"."保活"."协议"这些关键词,没错,这些确 ...

  5. 来吧,自己动手撸一个分布式ID生成器组件

    在经过了众多轮的面试之后,小林终于进入到了一家互联网公司的基础架构组,小林目前在公司有使用到架构组研究到分布式id生成器,前一阵子大概看了下其内部的实现,发现还是存在一些架构设计不合理之处.但是又由于 ...

  6. 推特雪花算法,分布式id生成器

    推特雪花算法 分布式id生成器 package util;import java.lang.management.ManagementFactory; import java.net.InetAddr ...

  7. 你不得不知的几个互联网ID生成器方案

    服务化.分布式已成为当下系统开发的首选,高并发操作在数据存储时,需要一套id生成器服务,来保证分布式情况下全局唯一性,以确保系统的订单创建.交易支付等场景下数据的唯一性,否则将造成不可估量的损失. 基 ...

  8. IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现

    1.引言 很多人一想到IM应用开发,第一印象就是"长连接"."socket"."保活"."协议"这些关键词,没错,这些确 ...

  9. 分布式ID生成器 | 架构师之路

    一.需求缘起 几乎所有的业务系统,都有生成一个唯一记录标识的需求,例如: 消息标识:message-id 订单标识:order-id 帖子标识:tiezi-id 这个记录标识往往就是数据库中的主键,数 ...

  10. 面试官:你会几种分布式 ID 生成方案???

    1. 为什么需要分布式 ID 对于单体系统来说,主键 ID 常用主键自动的方式进行设置.这种 ID 生成方法在单体项目是可行的,但是对于分布式系统,分库分表之后就不适应了.比如订单表数据量太大了,分成 ...

最新文章

  1. 我的puppet web 管理,彻底告别终端模式
  2. 深入探究 Win32 PE 文件格式
  3. 从0到1,马蜂窝大交通团队如何构建高效研发流程体系?
  4. 10个舍不得删的高质量公号
  5. 您必须学习Java 8的函数式编程吗?
  6. ireport 循环_ireport5.6.0分组显示
  7. mm1排队系统仿真matlab实验报告,MM1排队系统仿真matlab实验报告.doc
  8. 单例模式访问mysql设计类图_利用单例模式设计数据库连接Model类
  9. 记录几种敏捷开发应用的工具
  10. Adobe Illustrator CS6 已停止工作 的问题
  11. linux基础教程之在Linux上安装Go语言开发包
  12. linux怎么保存7天内文件,Linux七天系列(第七天)—文件系统管理
  13. vue项目hash模式改history模式 mand-mobile组件库样式不生效
  14. 店铺没有自然流量,系统平台如何引流。
  15. 贪心--CF645E
  16. XELA ROBOTICS 发布Uskin 皮肤触觉传感器
  17. 翻译:seq2seq model in Machine Learning
  18. windows的C盘根目錄下中文文件夾重命名
  19. 淘宝神话-one piece 般的十年
  20. Tailwind 初识

热门文章

  1. 易语言linux时间戳转换,易语言unix时间戳转易语言格式源码
  2. 模式识别—判别函数分类法(几何分类法)
  3. layui开发使用文档(镜像网址)
  4. GEE:快速下载数字高程DEM数据
  5. python科学计算库-Scipy,主要讲解优化,方程求解,积分,蒙特卡洛,插值计算
  6. 指南|七步搞定研控E总线驱动器与欧姆龙PLC主站连接
  7. 计算机学不学工程制图,如何学好工程制图
  8. 无线领夹式麦克风方案,专门为USB-C接口手机打造的直播方案
  9. Airplay vs DLNA vs Miracast
  10. PRML Chapter01 练习题Exercise