首页 文章 精选 留言 我的

精选列表

搜索[面试],共4915篇文章
优秀的个人博客,低调大师

面试被问Redis和zk两种分布式锁的对比

一、基于数据库实现分布式锁 1. 悲观锁 利用select … where … for update排他锁 注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。 2. 乐观锁 所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。 我们的抢购、秒杀就是用了这种实现以防止超卖。 通过增加递增的版本号字段实现乐观锁 二、基于缓存(Redis等)实现分布式锁 1、官方叫做 RedLock 算法,是 redis 官方支持的分布式锁算法。 这个分布式锁有 3 个重要的考量点: 1.互斥(只能有一个客户端获取锁) 2.不能死锁 3.容错(只要大部分 redis 节点创建了这把锁就可以) 2、下面是redis分布式锁的各种实现方式和缺点,按照时间的发展排序 1、直接setnx 直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴 缺点:如果setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到 2、setnx设置一个过期时间 为了改正第一个方法的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,过期时间一到自动释放 缺点:setnx和expire是两个方法,不能保证原子性,如果在setnx之后,还没来得及expire,服务挂了,还是会出现锁不释放的问题 3、set nx px redis官方为了解决第二种方式存在的缺点,在2.8版本为set指令添加了扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:set key value ex 5 nx 缺点: 如果在过期时间内,事务还没有执行完,锁提前被自动释放,其他的线程还是可以拿到锁 上面所说的那个缺点还会导致当前的线程释放其他线程占有的锁 4、加一个事务id 上面所说的第一个缺点,没有特别好的解决方法,只能把过期时间尽量设置的长一点,并且最好不要执行耗时任务 第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁。 即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,否则抛异常回滚,其实也是变相地解决了第一个问题 缺点:get key和将value与id比较是两个步骤,不能保证原子性 5、set nx px + 事务id + lua 我们可以用lua来写一个getkey并比较的脚本,jedis/luttce/redisson对lua脚本都有很好的支持 缺点:集群环境下,对master节点申请了分布式锁,由于redis的主从同步是异步进行的,master在内存中写入了nx之后直接返回,客户端获取锁成功。 此时master节点挂了,并且数据还没来得及同步,另一个节点被升级为master,这样其他的线程依然可以获取锁。 6、redlock 为了解决上面提到的redis集群中的分布式锁问题,redis的作者antirez的提出了red lock的概念,假设集群中所有的n个master节点完全独立,并且没有主从同步。 此时对所有的节点都去setnx,并且设置一个请求过期时间re和锁的过期时间le,同时re必须小于le(可以理解,不然请求3秒才拿到锁,而锁的过期时间只有1秒也太蠢了)。 此时如果有n / 2 + 1个节点成功拿到锁,此次分布式锁就算申请成功。 缺点:可靠性还没有被广泛验证,并且严重依赖时间,好的分布式系统应该是异步的,并不能以时间为担保,程序暂停、系统延迟等都可能会导致时间错误。 三、基于zookeeper实现的分布式锁 1. 实现方式 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: 创建一个目录mylock; 线程A想获取锁就在mylock目录下创建临时顺序节点; 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。 优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。 2. 两种利用特性实现原理: 1、利用临时节点特性 zookeeper的临时节点有两个特性,一是节点名称不能重复,二是会随着客户端退出而销毁,因此直接将key作为节点名称,能够成功创建的客户端则获取成功,失败的客户端监听成功的节点的删除事件 缺点:所有客户端监听同一个节点,但是同时只有一个节点的事件触发是有效的,造成资源的无效调度 2、利用顺序临时节点特性 zookeeper的顺序临时节点拥有临时节点的特性,同时,在一个父节点下创建创建的子临时顺序节点,会根据节点创建的先后顺序,用一个32位的数字作为后缀。 我们可以用key创建一个根节点,然后每次申请锁的时候在其下创建顺序节点,接着获取根节点下所有的顺序节点并排序,获取顺序最小的节点,如果该节点的名称与当前添加的名称相同。 则表示能够获取锁,否则监听根节点下面的处于当前节点之前的节点的删除事件,如果监听生效,则回到上一步重新判断顺序,直到获取锁。 总结 基于数据库分布式锁实现 优点:直接使用数据库,实现方式简单。 缺点: db操作性能较差,并且有锁表的风险 非阻塞操作失败后,需要轮询,占用cpu资源; 长时间不commit或者长时间轮询,可能会占用较多连接资源 基于redis缓存 redis set px nx + 唯一id + lua脚本 优点:redis本身的读写性能很高,因此基于redis的分布式锁效率比较高 缺点:依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有一定的影响,如果发生则需要人工介入 基于redis的redlock 优点:可以解决redis集群的同步可用性问题 缺点: 依赖中间件,并没有被广泛验证,维护成本高,需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率 锁删除失败 过期时间不好控制 非阻塞,操作失败后,需要轮询,占用cpu资源; 基于zookeeper的分布式锁 优点:不存在redis的超时、数据同步(zookeeper是同步完以后才返回)、主从切换(zookeeper主从切换的过程中服务是不可用的)的问题,可靠性很高 缺点:依赖中间件,保证了可靠性的同时牺牲了一部分效率(但是依然很高)。性能不如redis。 jdk的方式不太推荐。 从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper 从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库 从性能角度(从高到低)缓存 > Zookeeper >= 数据库 从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库 没有绝对完美的实现方式,具体要选择哪一种分布式锁,需要结合每一种锁的优缺点和业务特点而定。

优秀的个人博客,低调大师

从前端性能优化引申出来的5道经典面试题(值得收藏)

作者:一阵风,一枚只想安静写代码的程序员,来自程序员成长指北 https://juejin.im/post/6888848660591968264 前端优化是一个大的课题,需要花好多时间才能理解,之前对前端优化陆陆续续有一些了解。所以这次从渲染优化,打包优化,代码优化做了一个系统的总结,并且引申出了几个需要关注的问题,文章可能有点长,大家一定要看到最后。最后写作不易,希望觉得还可以的话,帮忙点赞一波,提前感谢了。当然如果有写不好的地方,也请指出来,我会积极改进,共同成长。 渲染优化 渲染优化是前端优化中一个很重要的部分,一个好的首屏时间能给用户带来很好的体验,这里要说的一点是关于首屏时间的定义,不同的团队对首屏时间定义不一样,有的团队认为首屏时间就是白屏时间,是从页面加载到第一个画面出现的时间。但是当我们说到用户体验的时候,仅仅是这样还达不到效果,所以有的前端团队认为,首屏时间应该是从页面加载到用户可以进行正常的页面操作时间,那么我们就依照后者来进行说明 js css 加载顺序 说渲染优化之前,我们还需要说一个小插曲,就是比较经典的一道问题"浏览器地址栏输入url发生了什么",理解了这个我们才可以更清楚js,css加载顺序对渲染的影响 问题 1:地址栏输入url 发生了什么 这个问题经常被人提起,有人回答比较简洁点,有人可能回答的比较详细,下面就说一下主要流程 首先会进行 url 解析,根据 dns 系统进行 ip 查找 根据 ip 就可以找到服务器,然后浏览器和服务器会进行 TCP 三次握手建立连接,如果此时是 https 的话,还会建立 TLS 连接以及协商加密算法,这里就会出现另一个需要注意的问题"https 和 http 的区别"(下文会讲到) 连接建立之后浏览器开始发送请求获取文件,此时这里还会出现一种情况就是缓存,建立连接后是走缓存还是直接重新获取,需要看后台设置,所以这里会有一个关注的问题"浏览器缓存机制",缓存我们等会在讲,现在我们就当没有缓存,直接去获取文件 首先获取 html 文件,构建 DOM 树,这个过程是边下载边解析,并不是等 html 文件全部下载完了,再去解析 html,这样比较浪费时间,而是下载一点解析一点 好了解析到 html 头部时候,又会出现一种问题,css,js 放到哪里了?不同的位置会造成渲染的不同,此时就会出现另一个需要关注的问题"css,js 位置应该放哪里?为什么",我们先按照正确的位置来说明(css 放头部,js 放尾部) 解析到了 html 头部发现有 css 文件,此时下载 css 文件,css 文件也是一边下载一边解析的,构建的是 CSSOM 树,当 DOM 树和 CSSOM 树全部构建完之后,浏览器会把 DOM 树和 CSSOM 树构建成渲染树。 样式计算, 上面最后一句"DOM 树和 CSSOM 树会一起构建成渲染树"说的有点笼统,其实还有更细一点的操作,但是一般回答到上面应该就可以了,我们现在接上面说一下构造渲染树的时候还做了哪些事情。第一个就是样式计算,DOM树 和 CSSOM树有了之后,浏览器开始样式计算,主要是为 DOM 树上的节点找到对应的样式 构建布局树,样式计算完之后就开始构建布局树。主要是为 DOM 树上的节点找到页面上对应位置以及一些"display:none"元素的隐藏。 构建分层树,布局树完成后浏览器还需要建立分层树,主要是为了满足滚动条,z-index,position 这些复杂的分层操作 将分层树图块化,利用光栅找到视图窗口下的对应的位图。主要是因为一个页面可能有几屏那么长,一下渲染出来比较浪费,所以浏览器会找到视图窗口对应的图块,将这部分的图块进行渲染 最终渲染进程将整个页面渲染出来,在渲染的过程中会还出现重排和重绘,这也是比较爱问的问题"重排重绘为什么会影响渲染,如何避免?" 以上过程大概讲解了一下从 url 到页面渲染的整个过程,其实涉及到了几个需要关注的问题,下面来具体讲讲 问题 2:js css 顺序对前端优化影响 上面我们说到了整个渲染流程,但是没有说到 css 和 js 对渲染的影响。渲染树的构成必须要 DOM 树和 CSSOM 树的,所以尽快的构建 CSSOM 树是一个重要的优化手段,如果 css 文件放在尾部,那么整个过程就是一个串行的过程先解析了 dom,再去解析 css。所以 css 我们一般都是放在头部,这样 DOM 树和 CSSOM 树的构建是同步进行的。 再来看 js,因为 js 的运行会阻止 DOM 树的渲染的,所以一旦我们的 js 放在了头部,而且也没有异步加载这些操作的话,js 一旦一直在运行,DOM 树就一直构建不出来,那么页面就会一直出现白屏界面,所以一般我们会把 js 文件放在尾部。当然放到尾部也不是就没有问题了,只是问题相对较小,放到尾部的 js 文件如果过大,运行时间长,代码加载时,就会有大量耗时的操作造成页面不可点击,这就是另一个问题,但这肯定比白屏要好,白屏是什么页面都没有,这种是页面有了只是操作不流畅。 js 脚本放在尾部还有一个原因,有时候 js 代码会有操作 dom 节点的情况,如果放在头部执行,DOM树还没有构建,拿不到 DOM 节点但是你又去使用就会出现报错情况,错误没处理好的话页面会直接崩掉 问题 3:重排重绘为什么会影响渲染,如何避免? 重排和重绘为什么会影响渲染,哪个影响更大,如何避免是经常被问到的一道题目,我们先来说一下重绘 重绘 重绘指的是不影响界面布局的操作,比如更改颜色,那么根据上面的渲染讲解我们知道,重绘之后我们只需要在重复进行一下样式计算,就可以直接渲染了,对浏览器渲染的影响相对较小 重排 重排指的是影响界面布局的操作,比如改变宽高,隐藏节点等。对于重排就不是一个重新计算样式那么简单了,因为改变了布局,根据上面的渲染流程来看涉及到的阶段有样式计算,布局树重新生成,分层树重新生成,所以重排对浏览器的渲染影响是比较高的 避免方法 js 尽量减少对样式的操作,能用 css 完成的就用 css 对 dom 操作尽量少,能用 createDocumentFragment 的地方尽量用 如果必须要用 js 操作样式,能合并尽量合并不要分多次操作 resize 事件 最好加上防抖,能尽量少触发就少触发 加载图片的时候,提前写好宽高 问题 4:浏览器缓存机制 浏览器缓存是比较常见的问题,我会从浏览器缓存的方式,缓存的实现, 缓存在哪里这几个点来说明 缓存方式 我们经常说的浏览器缓存有两种,一种是强制缓存,一种是协商缓存,因为下面有具体实现讲解,所以这里就说一下概念 协商缓存 协商缓存意思是文件已经被缓存了,但是否从缓存中读取是需要和服务器进行协商,具体如何协商要看请求头/响应头的字段设置,下面会说到。需要注意的是协商缓存还是发了请求的 强制缓存 强制缓存就是文件直接从缓存中获取,不需要发送请求 缓存实现 强制缓存 强制缓存在 http1.0 的时候用的是 Expires,是响应头里面的一个字段表示的是文件过期时间。是一个绝对时间,正因为是绝对时间所以在某些情况下,服务器的时区和浏览器时区不一致的时候就会导致缓存失效。为了解决这个问题,HTPP1.1 引入了一个新的响应头 cache-control 它的可选值如下 cache-control max-age: 缓存过期时间,是一个相对时间 public: 表示客户端和代理服务器都会缓存 private: 表示只在客户端缓存 no-cache: 协商缓存标识符,表示文件会被缓存但是需要和服务器协商 no-store: 表示文件不会被缓存 HTTP1.1 利用的就是 max-age:600 来强制缓存,因为是相对时间,所以不会出现 Expires 问题 协商缓存 协商缓存是利用 Last-Modified/if-Modified-Since,Etag/if-None-Match 这两对请求、响应头。 Last-Modified/if-Modified-Since Etag/If-None-Match 由于 Last-Modified 的时间粒度是秒,有的文件在 1s 内可能被改动多次。这种方式在这种特殊情况下还是会失效,所以HTTP1.1又引入了 Etag 字段。这个字段是根据文件内容生成一个标记符比如"W/"5f9583bd-10a8"",然后再和 If-None-Match 进行对比就能更准确的知道文件有没有被改动过 浏览器第一次发送请求获取文件缓存下来,服务器响应头返回一个 if-Modified-Since,记录被改动的时间 浏览器第二次发送请求的时候会带上一个 Last-Modified 请求头,时间就是 if-Modified-Since 返回的值。然后服务器拿到这个字段和自己内部设置的时间进行对比,时间相同表示没有修改,就直接返回 304 从缓存里面获取文件 缓存在哪里 知道了缓存方式和实现,再来说一下缓存存在哪个地方,我们打开掘金可以看到如下的信息 。缓存的来源有两个地方 from dist cache,from memeory cache form memory cache 这个是缓存在内存里面,优点是快速,但是具有时效性,当关闭 tab 时候缓存就会失效。 from dist cache 这个是缓存在磁盘里面,虽然慢但是还是比请求快,优点是缓存可以一直被保留,即使关闭 tab 页,也会一直存在 何时缓存在memory,合适缓存在dist? 这个问题网上很少找的到标准答案,大家一致的说法是js,图片文件浏览器会自动保存在memory中,css文件因为不常修改保存在dist里面,我们可以打开掘金网站,很大一部分文件都是按照这个规则来的,但是也有少数js文件也是缓存在dist里面。所以他的存放机制到底是什么样了?我带着这个疑问查了好多文章,虽然最后没有确切找到答案,但是一个知乎的回答可以给我们提供思路,下面引用一个知乎回答者一段话 第一个现象(以图片为例):访问-> 200 -> 退出浏览器再进来-> 200(from disk cache) -> 刷新 -> 200(from memory cache)。总结: 会不会是chrome很聪明的判断既然已经从disk拿来了, 第二次就内存拿吧 快。(笑哭) 第二个现象(以图片为例):只要图片是base64 我看都是from memroy cache。总结: 解析渲染图片这么费劲的事情,还是做一次然后放到内存吧。用的时候直接拿 第三个现象(以js css为例):个人在做静态测试的发现,大型的js css文件都是直接disk cache。结: chrome会不会说 我去 你这么大太占地方了。你就去硬盘里呆着吧。慢就慢点吧。 第四个现象:隐私模式下,几乎都是 from memroy cache。总结: 隐私模式 是吧。我不能暴露你东西,还是放到内存吧。你关,我死。 上面几点是虽然很幽默,但是却可以从中找到一部分答案,但是我觉得另一个知乎回答我更赞同 浏览器运行的时候也是由几个进程协作的,所以操作系统为了节省内存,会把一部分内存里的资源交换回磁盘的交换区,当然交换是有策略的,比如最常用的就是LRU。 什么时候存dist,什么时候存memoey都是在浏览器控制下的,memory不够了可能就会考虑去存dist了,所以经过上面所说我自己总结结果如下 大一点的文件会缓存在dist里面,因为内存也是有限的,磁盘的空间更大 小一点文件js,图片存的是memory css文件一般存在dist 特殊情况memory大小是有限制的,浏览器也会根据自己的内置算法,把一部分js文件存到dist里面 问题 5:https 和 http 的区别 说到https和http的区别,可以说一下https服务器和客户端连接的差异,以及https特定的加密算法协商,甚至可能还要说到对称加密,非对称加密和证书等,篇幅很长,请看我之前单独写的一篇https详解,里面讲的非常详细。 请求优化 讲请求优化的之前先来总结下上面说到的js, css文件顺序优化,为了让渲染更快,我们需要把js放到尾部,css放到头部,然后还要注意在书写js的时候尽量减少重排,重绘。书写html,css的时候尽量简洁,不要冗余,目的是为了更快的构建DOM树和CSSOM树。好了下面我们在来说说请求优化,请求优化可以从请求数量和请求时间两方面入手 减少请求数量 将小图片打包成base64 利用雪碧图融合多个小图片 利用缓存上面已经说到过 减少请求时间 将js,css,html等文件能压缩的尽量压缩,减少文件大小,加快下载速度 利用webpack打包根据路由进行懒加载,不要初始就加载全部,那样文件会很大 能升级到高版本的http就升级到高版本(这个回答是套话),为什么高版本能提高速度具体看上面我说的那篇https文章 建立内部CDN能更快速的获取文件 webpack优化 介绍了渲染优化,现在来看看webpack优化,自己平常写demo给团队做培训的时候都是自己手写webpack配置,虽然也就几十行,但每次都能让我巩固webpack的基本配置,下面直接说一下webpack优化手段有哪些 基础配置优化 extensions 这个配置是属于resolve里面的,经常用来对文件后缀进行扩展,写法如下 resolve:{extensions:['.ts','.tsx','.js']} 这个配置表示webpack会根据extensions去寻找文件后缀名,所以如果我们的项目主要用ts写的话,那我们就可以.tsx和.ts写前面,目的是为了让webpack能够快速解析 alias 这个配置也是属于resolve里面的,是用来映射路劲,能减少打包时间的主要原因是能够让webpack快速的解析文件路径,找到对应的文件,配置如下 resolve:{alias:{Components:path.resolve(__dirname,'./src/components')}} noParse noParse表示不需要解析的文件,有的文件可能是来自第三方的文件,被 providePlugin引入作为windows上的变量来使用,这样的文件相对比较大,并且已经是被打包过的,所以把这种文件排除在外是很有必要的,配置如下 module:{noParse:[/proj4\.js/]} exclude 某些loader会有这样一个属性,目的是指定loader作用的范围,exclude表示排除某些文件不需要babel-loader处理,loader的作用范围小了,打包速度自然就快了,用babel-loader举一个简单例子 {test:/\.js$/,loader:"babel-loader",exclude:path.resolve(__dirname,'node_modules')} devtool 这个配置是一个调试项,不同的配置展示效果不一样,打包大小和打包速度也不一样,比如开发环境下cheap-source-map肯定比source-map快,至于为什么,强烈推荐自己之前写的这一篇讲解devtool的文章:webpack devtools篇讲的非常详细。 {devtool:'cheap-source-map'} .eslintignore 这个虽不是webpack配置但是对打包速度优化还是很有用的,在我的实践中eslint检查对打包的速度影响很大,但是很多情况我们不能没有这个eslint检查,eslint检查如果仅仅在vs里面开启的话,可能不怎么保险。 因为有可能你vs中的eslint插件突然关闭了或者某些原因vs不能检查了,只能靠webpack构建去帮你拦住错误代码的提交,即使这样还不能确保万无一失,因为你可能某一次提交代码很急没有启动服务,直接盲改提交上去了。这个时候只能通过最后一道屏障给你保护,就是在CI的时候。比如我们也会是在jenkins构建的时候帮你进行eslint检查,三道屏障确保了我们最终出的镜像是不会有问题的。 所以eslint是很重要的,不能删掉,在不能删掉的情况下怎么让检查的时间更少了,我们就可以通过忽略文件,让不必要的文件禁止eslint,只对需要的文件eslint可以很大程度提高打包速度 loader,plugins优化 上述说了几个基础配置优化,应该还有其他的基础配置,今后遇到了再继续添加,现在在来讲讲利用某些loader,plugins来提高打包速度的例子 cache-loader 这个loader就是在第一次打包的时候会缓存打包的结果,在第二次打包的时候就会直接读取缓存的内容,从而提高打包效率。但是也需要合理利用,我们要记住一点你加的每一个loader,plugins都会带来额外的打包时间。这个额外时间比他带来的减少时间多,那么一味的增加这个loader就没意义,所以cache-loader最好用在耗时比较大的loader上,配置如下 {rules:[{test:/\.vue$/,use:['cache-loader','vue-loader'],include:path.resolve(__dirname,'./src')}]} webpack-parallel-uglify-plugin, uglifyjs-webpack-plugin, terser-webpack-plugin 在上面的渲染优化中我们已经知道,文件越小渲染的速度是越快的。所以我们在配置webpack时候经常会用到压缩,但是压缩也是需要消耗时间的,所以我们我们经常会用到上面三个插件之一来开启并行压缩,减少压缩时间,我们用webpack4推荐使用的terse-webpack-plugin做例子来说明 optimization:{minimizer:[newTerserPlugin({parallel:true,cache:true})],} happypack, parallel-webpack, thread-loader 这几个loader/plugin和上面一样也是开启并行的,只不过是开启并行构建。由于happypack的作者说自己的兴趣已经不再js上了,所以已经没有维护了,并推荐如果使用的是webpack4的话,就去使用thread-loader。基本配置如下 {test:/\.js$/,use:[{loader:"thread-loader",options:threadLoaderOptions},"babel-loader",],exclude:/node_modules/,} DllPlugin,webpack.DllReferencePlugin 上面说的几个并行插件理论上是可以增加构建速度,网上很多文章都是这么说的,但是我在实际的过程中使用,发现有时候不仅没提升反而还降低了打包速度,网速查阅给的理由是可能你的电脑核数本来就低,或者当时你CPU运行已经很高了,再去开启多进程导致构建速度降低。 上面说的几个并行插件可能在某些情况下达不到你想要的效果,然而在我们团队优化webpack性能经验来看,这次所说的两个插件是很明显并且每次都能提高打包速度的。原理就是先把第三方依赖先打包一次生成一个js文件,然后真正打包项目代码时候,会根据映射文件直接从打包出来的js文件获取所需要的对象,而不用再去打包第三方文件。只不过这种情况打包配置稍微麻烦点,需要写一个webpack.dll.js。大致如下 webpack.dll.js constpath=require('path');constwebpack=require('webpack');module.exports={entry:{library:["vue","moment"]},output:{filename:'[name].dll.js',path:path.resolve(__dirname,'json-dll'),library:'[name]'},plugins:[newwebpack.DllPlugin({path:'./json-dll/library.json',name:'[name].json'})]} webpack.dev.js newAddAssetHtmlWebpack({filepath:path.resolve(__dirname,'./json-dll/library.dll.js')}),newwebpack.DllReferencePlugin({manifest:require("./json-dll/library.json")}) 其他优化配置 这些插件就简单的介绍下,在我的个人项目中已经使用过,自我感觉还可以,具体使用可以查阅npm或者github webpack-bundle-analyzer 这个插件可以用可视化帮我们分析打包体积,从而来采用合适的优化方式去改进我们的webpack配置 speed-measure-webpack-plugin 这个插件可以告诉我们打包时候每一个loader或者plugin花费了多少时间,从而对耗时比较长的plugin和loader做优化 friendly-errors-webpack-plugin 这个插件可以帮我们优化打包日志,我们打包时候经常看到很长一个日志信息,有的时候是不需要的,也不会去看所以可以用这个插件来简化 代码优化 这是最后一部分代码优化了,这里的代码性能优化我只说我在工作中感受到的,至于其他的比较小的优化点比如createDocumentFragment使用可以查查其他文章 能不操作dom不要操作dom,哪怕有时候需要改设计 很多情况下我们都能用css还原设计稿,但是有些时候单单从css没法还原,尤其组件还不是你写的时候,比如我们团队用的就是antd,有时候的产品设计单从css上没法实现,只能动用js,删除,增加节点在配合样式才能完成。 由于我们又是一个做大数据的公司,这个时候就会出现性能问题,最开始写代码时候,产品说什么就是什么,说什么我都会想办法搞出来,不管用什么方法。后来到客户现场大数据请况下,性能缺点立马暴露的出来。 所以代码优化的原则之一我认为是能不写的代码就不写,当然这是要从性能角度出发,通过性能分析给产品说出理由,并且最好还能提供更好的解决方案,这个才是我们需要考虑的。 如果用的是react 一定用写shouldComponentUpdate这个生命周期函数,不然打印的时候你会发现,你自己都迷糊为什么执行了这么多遍 将复杂的比对,变成简单比对 这句话是什么意思了?我们就拿shouldComponentUpdate举例子,用这个函数没问题,但是可以做的更好,我们在工作中经常这么写 shouldComponentUpdate(nextPrpops){returnJSON.stringify(nextPrpops.data)!==JSON.stringify(this.props.data)} 如果这是一个分页表格,data是每一页数据,数据改变了重新渲染,在小数据场景下这本身是没有问题。但是如果在大数据的场景下可能会有问题,可能有人有疑问,既然做了分页怎么还会有大数据了,因为我们的产品是做大数据分析日志的,一页十条日志,有的日志可能非常的长,也就是说就算是10条数据比对起来也是很耗时,所以当时想法能不能找到其他的替代变量来表示数据变了?比如下面这样 shouldComponentUpdate(nextPrpops){returnnextPrpops.data[0].id!==this.props.data[0].id} 第一条的id不一样就表示数据变化了行不行,显然在某种情况下是存在的,也有人会说可能会出现id一样,那如果换成下面这种了? shouldComponentUpdate(nextPrpops){returnnextPrpops.current!==this.props.current} 将data的比对转换成了current的比对,因为页数变了,数据基本都是变了,对于我们自己日志的展示来说基本不存在两页数据是一模一样的,如果有那可能是后台问题。然后在好好思考这个问题,即使存在了两页数据一摸一样,顶多就是这个表格不重新渲染,但是两页数据一摸一样不重新渲染是不是也没有问题,因为数据是一样的。或者如果你还是不放心,那下面这种会不会好点 this.setState({data,requestId:guid()})shouldComponentUpdate(nextPrpops){returnnextPrpops.requestId!==this.props.requestId} 给一个requestId跟宗data,后面就只比对requestId。上面的写法可能都有问题,但是主要是想说的是我们在写代码时候可以想想是不是可以"将复杂的比对,变成简单比对" 学习数据结构和算法,一定会在你的工作中派上用场 我们经常会听到学习数据结构和算法没有什么大的用处,因为工作基本用不上。这句话我之前觉得没错,现在看来错的很严重。我们所学的每一样技能,都会在将来的人生中派上用场。之前写完代码就丢了不去优化所以我觉得算法没意义,又难又容易忘记。但现在要求自己做完需求,开启mock,打开perfermance进行大数据量的测试,看着那些标红的火焰图和肉眼可见的卡顿,就明白了算法和数据结构的重要性,因为此时你只能从它身上获取优化,平时你很排斥它,到优化的时候你是那么想拥有它。我拿自己之前写的代码举例,由于公司代码是保密的我就把变量换一下,伪代码如下 data.filter(({id})=>{returnselectedIds.includes(id);}) 就是这样几行代码,逻辑就是筛选出data里面已经被勾选的数据。基本上很多人都可能这么写,因为我看我们团队里面都是这么写的。产品当时已经限制data最多200数据,所以写完完全没压力,性能没影响。但是秉着对性能优化的原则(主要是被现场环境搞怕了~~~),我开启了mock服务,将数据调到了2万条再去测试,代码弊端就暴露出来了,界面进入卡顿,重新选择的时候也会卡顿。然后就开始了优化,当时具体的思路如下 按照现在的代码来看,这是一个两层循环的暴力搜索时间复杂度为O(n^2)。所以想着能不能降一下复杂度至少是O(nlogn),看了一下代码只能从selectedIds.includes(id)这句入手,于是想着可不可以用二分,但是立马被否定因为二分是需要有序的,我这数组都是字符串怎么二分。 安静了一下之后,回想起看过的算法课程和书籍以及做的算法题,改变暴力搜索的方法基本都是 1:上指针 2:数组升维 3:利用hash表 前两者被我否定了因为我觉得还没那么复杂,于是利用hash表思想解决这个问题,因为js里面有一个天然的hash表结构就是对象。我们知道hash表的查询是O(1)的,所以我将代码改写如下 constids={};selectedIds.forEach(id=>ids[id]=1);data.filter(({id})=>{return!!ids[id];}) 将从selectedIds查询变成从ids查询,这样时间复杂度就从O(n^2)变成了O(n)了,这段代码增加了 constids={};selectedIds.forEach(id=>ids[id]=1); 其实增加了一个selectedIds遍历也是一个O(n)的复杂度,总来说复杂度是O(2n),但是从时间复杂度长期期望来看还是一个O(n)的时间复杂度,只不过额外增加了一个对象,所以这也是一个典型的空间换时间的例子,但是也不要担心,ids用完之后垃圾回收机制会把他回收的。 最后 其实这篇文章写出来还是对自己帮助很大,让自己系统的梳理了一下自己理解的前端优化,希望对你们也有帮助。 最后 关注公众号【前端宇宙】,每日获取好文推荐 添加微信,入群交流 本文分享自微信公众号 - 前端宇宙(gh_8184da923ced)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

阿里面试官:说说操作系统微内核和Dubbo微内核?

微信搜 「yes的练级攻略」干货满满,不然来掐我,回复【123】一份20W字的算法刷题笔记等你来领。 个人文章汇总:[https://github.com/yessimida/yes](https://github.com/yessimida/yes) 欢迎 star ! 你好,我是 yes。 在之前的文章已经提到了 RPC 的核心,想必一个 RPC 通信大致的流程和基本原理已经清晰了。 这篇文章借着 Dubbo 来说说微内核这种设计思想,不会扯到 Dubbo 某个具体细节实现上,和 Dubbo 强相关的内容会在之后的文章写到。 所以今天的重点在微内核,而这个概念我最早是从操作系统那里得知,不过操作系统的微内核和 Dubbo 相关的微内核又不太一样。 Dubbo 的微内核广义上的微内核,而操作系统只是针对内核实现。 这么说你肯定不清楚,别急,听我慢慢道来。 我们先看看操作系统的微内核。 操作系统中的微内核 在维基百科上搜索微内核出现的就是: 在计算机科学中,微内核(英语:Microkernel,μ-kernel),是一种内核的设计架构,由尽可能精简的程序所组成,以实现一个操作系统所需要的最基本功能,包括了底层的寻址空间管理、线程管理、与进程间通信。 这个词条归类在操作系统技术下,所以这里的微内核指的就是操作系统的内核设计,与之对应的是宏内核架构。 Linux 就是宏内核架构。 操作系统我们都知道它是一个中间层,为我们管理底层的硬件资源,为上层服务提供接口。 提供进程管理、内存管理、文件系统、进程通信等功能。 像 Linux 这样的宏内核设计是把这些功能都作为内核来实现,而微内核则仅保留最基础的功能。 比如就留下进程的管理、内存管理等,把文件管理等功能剥离出去,变成用户空间的独立进程来提供服务。 来看下这个维基百科上的这个图应该就很清晰了。 宏内核中的一些功能在微内核架构上都被独立到用户态中,这样内核代码量就少了。 代码少了潜在的 bug 就少,出了问题也更容易排查。 系统也就更加稳定,不易奔溃,因为那些服务从内核中移除,在用户空间运行着,如果出了故障,内核重启这个服务就好了,不会像之前那样整个内核 GG。 拿显卡驱动来说,出问题就蓝屏,这要是微内核设计就可以重启显卡驱动。 听起来好像微内核很好啊?并不是,接下来就说说微内核的缺点。 首先是性能问题。 因为很多功能作为独立进程放到用户空间运行了,所以宏内核时的函数调用就变成了进程间调用,涉及进程间的通信,还会伴随着内核态和用户态的来回切换,我们知道这种上下文切换时比较耗时的。 这性能的问题就有点大了。 然后微内核设计没那么简单,想要灵巧、减少耦合、提高可移植性就需要好好的设计,按照林纳斯的话来说:“如果 GNU 内核(微内核架构)早在去年春天完成了,我压根不会开始我的项目(Lniux)。” GNU Hurd 采用微内核架构,设计过于精巧,研发速度缓慢,性能长期无法提升。 当年林纳斯还和 Minix 的作者安德鲁,对操作系统的宏内核和微内核的好坏进行了一波网络口水战。 我们来回顾一下那段历史,挺有意思的。 因为 AT&T 把 Unix 商业化了,大学不能免费使用 Unix,身为大学教授的安德鲁为了教学自己搞了个操作系统,即 Minix。 安德鲁 当时的学术风潮是微内核架构,把核心功能模块化,划分为几个独立的进程,运行在不同的地址空间提高了代码的可移植和系统的安全性。 所以 Minix 就是按微内核架构编写的,当然还有上述提到的 GNU Hurd。 而林纳斯那时候读大学,他祖父送了他一台 Intel 80386,林纳斯也看到了安德鲁的教科书,根据书上的内容写出了 Linux。 林纳斯 不过没有按照微内核的设计,而是跟 Unix 一样采用了宏内核架构。 安德鲁教授看到了 Linux ,然后在 comp.os.Minix 上批评道:宏内核的设计是有害的。 Linux 内核耦合度太高,完全是为了 Intel 80386 而设计的,处理器架构进化很快的,操作系统应该都具备可移植性。 安德鲁还提到:都1991年了还用宏内核来设计操作系统,这是一种巨大的退步。 林纳斯在一天之后进行了反击,他说 Minix 设计上有缺陷,从哲学和美学角度来看微内核确实好,但是你看 GUN Hurd 到现在还没开发出来。 然后操作系统本来就依靠硬件的特性,所以内核本身不需要过度具备可移植性,应用程序的可移植性才重要,Linux 比 Minix 好移植多了。 而且 Linux 本来就是为我自己做的,所以契合 80386,如果要移植到别的平台,代码都是开源的(Minix 源码当时得买),想要的人自己做咯。 安德鲁也做了一波回应:Minix 有局限性是因为我是教授,因为大部分学生都只能在低配的机器上使用,所以系统的硬件需求得足够低,虽然你 Linux 是免费的,但是需要的硬件贵呀。 其实可以看到,两者并没有对宏内核和微内核的技术细节的进行深入探讨,而是抓住对方的:你这 Minix 代码还要收费,你这 Linux 需要的硬件这么贵来进行“攻击”,所以称之为口水战。 反正口水战之后双方都没有改变各自的设计,不过林纳斯有引进微内核的思想来改进代码,也改善了可移植性。 微内核市面上设计成功的有 QNX,黑莓手机就是用这个操作系统,车用市场也几乎都用 QNX 系统。 黑莓手机 这手机很多年前我用过,当时觉得有点东西的。 宏内核的话就提个 Linux ,足够了。 两个架构都有成功的产品,所以还是取舍的问题,也没有谁完全压着谁。 再具体的就不深入了,今天的主角其实是广义上的微内核。 Dubbo 中的微内核 Dubbo 的微内核是广义上的,它的思想是:核心系统+插件。 这个微内核说白了就是把不变的功能抽象出来称为核心,把变动的功能作为插件来扩展,符合开闭原则,更容易扩展、维护。 小霸王游戏机大家都应该玩过,就长这样的,它的设计就可以认为是个微内核设计。 机体本身作为核心系统,游戏片就是插件。 我们想玩哪个游戏就插哪个游戏片,简单便捷,不影响机体本身。 假设不把游戏片抽象成插件式,那是不是就难搞了?换个游戏就成为难题了。 所以微内核架构的本质就是将变化的部分抽象成插件,使得可以快速简便地满足各种需求又不影响整体的稳定性。 这就是微内核架构的精髓。 这里再扣个细节,较个真(就是我个人的一点想法)。 Mark Richards 在 《Software Architecture Patterns》的微内核章节里面提到 The core system of the microkernel architecture pattern traditionally contains only the minimal functionality required to make the system operational. 从字面意义来看,Mark 认为核心系统指的是可以独立运行且提供基本功能的最小模块。 例如 vscode、idea、chrome 等设计就符合 Mark 认为的核心设计,核心系统提供基础必备的功能,可以独立运行。 像 vscode 核心就是编辑器,没有插件也可以独立运行,然后又有丰富的插件,来满足一些特殊需求,扩展核心系统的功能。 这里可能会让人产生误导,认为核心必须是能让系统运行的最小功能模块。 我认为核心不一定需要独立运行且提供基础功能,能让系统运行的最小组织模块也是核心。 两个说法的差别在于:只有核心的话系统能否正常的运行。 vscode 没有插件照样能运行,能提供基本功能,而小霸王游戏机没有游戏片那运行个寂寞,玩个球。 但是小霸王这种难道就不算微内核了嘛? 就我个人而言,把变与不变区分出来,把变化封装成插件就称为微内核设计。 像 Dubbo 能支持很多协议、各种负载均衡的扩展、集群的扩展等等,它自身的一些功能也是通过扩展点实现的,没有插件也跑不了。 这样的内核就像胶水,把各个插件结合起来最终提供服务,没有插件这个系统就没什么意义。 所以我认为核心系统不一定需要能独立运行,能让系统运行的最小组织模块也是核心。。 因此我觉得 Mark 说的不太准确,容易产生误导。 当然这是文字上面的抠细节,核心概念都是一致的:抽象出核心,剥离变化为插件。 微内核设计的好处 这里的微内核指的是广义的微内核。 作为一个框架或者软件,扩展性非常的重要。 因为一个框架、一个软件的使用者千千万,不同的人有不同的需求。 身为框架的维护者、软件的开发者你有精力和能力满足所有用户的需求? 做梦,不存在的。 有些 idea 你想都想不到。 比如我之前看到的 vscode 里面有个「坤坤鼓励师」插件,默认你代码写一小时之后蔡徐坤来给你跳鸡你太美,让你休息休息...... 来感受一下? 所以做一个框架或者软件,想要让更多人使用,不仅自身提供的功能要全、性能要好、使用要简单,让用户 DIY,做各种定制化也非常的关键! Dubbo 的成功其实就离不开它的微内核设计,定制化开发在很多场景都要用到,毕竟都得稍加改造一番来满足自己公司的一些特殊需求。 当然也不是什么都要微内核 微内核看起来是很方便,但是设计起来就不方便了。 需要精心的设计,抽象出各种扩展点。 除了本身的功能还需要管理插件的生命周期、插件如何连接、如何通信等。 有些还需要做沙箱隔离,防止插件污染整个系统等等,本来的内部函数调用变成了插件间的通信,反正设计起来是复杂了。 一般微内核适合用在框架的设计上,或者一些规则引擎的设计,只有复杂的会有很多变化的需求场景才需要用到微内核。 像一般简单的项目,本来就一条直道走到底的那种就算了,不要瞎操作,等下秀折了腿。 最后 微内核其实就是一种架构思想,可以是框架层面的,也可以细化到某个模块的设计。 归根结底就是把变化封装成插件,可插拔,拥抱变化。 当然今天也提到了操作系统的微内核,这个和广义上的微内核还是不太一样的。 至于 Dubbo 的微内核就离不开它的 SPI,之后文章会深入写一波,等我哈。 欢迎关注我的公众号【yes的练级攻略】,更多硬核文章等你来读。 更多文章可看我的文章汇总:https://github.com/yessimida/yes 欢迎 star ! 我是 yes,从一点点到亿点点,欢迎在看、转发、留言,我们下篇见。 巨人的肩膀 https://en.wikipedia.org/wiki/Microkernel https://en.wikipedia.org/wiki/Tanenbaum%E2%80%93Torvalds_debate 《Software Architecture Patterns》 推荐阅读 RPC 核心,万变不离其宗 今年我读了四个开源项目的源码,来分享下心得 本文分享自微信公众号 - yes的练级攻略(yes_java)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

面试官问我什么是「栈」,我随手画了10张图来解释

关注、星标公众号 ,直达精彩内容 ID:技术让梦想更伟大 作者:李肖遥 栈的概念 栈(stack)是限定仅在表的一端进行操作的数据结构,且栈是一种先进后出的数据结构,允许操作的一端称为栈顶,不允许操作的称为栈底,如下图所示: 之前我们讲到了链表,我们只能够对其链表的表尾结点进行操作,并且只能进行插入一个新的结点与删除最末尾的这个结点两个操作,而这样强限制性的‘链表’,就是我们所说的栈。 就像是一个死胡同一样,只有一个出口,如图所示,有个概念: 栈的结点设计 栈分为数组栈和链表栈,其区别如下: 数组栈使用数组进行功能的模拟,实现较为快速和便利; 链表栈使用链表的思路去设计,实现相比较说较为麻烦,但是其稳定,且不易出错; 在链表栈中又分为静态链表栈和动态链表栈,其区别如下: 静态链表栈给定栈的空间大小,不允许超过存储超过给定数据大小的元素; 动态栈使用的是自动创建空间的方法进行创建,只要符合机器的硬件要求以及编译器的控制,其理论上是极大的。 数组栈 其实际就是用一段连续的存储空间来存储栈中的数据元素,有以下特点: 元素所占的存储空间必须连续,这里的连续是指的逻辑连续,而不是物理连续。 元素在存储空间的位置是按逻辑顺序存放的 我们来举例说明,鉴于C语言数组下标都是0开始,并且栈的使用需要的空间大小难以估计,所以初始化空栈的时候,不应该设定栈的最大容量。 我们先为栈设定一个基本容量,在应用过STACK_程当中,当栈的空间不够用时,再逐渐扩大。 设定2个常量,STACK_INIT_SIZE(存储空间初始化分配量)和STACK_INCREMENT(存储空间分配增量),宏定义如下 #defineSTACK_INIT_SIZE1000//数值可以根据实际情况确定#defineSTACK_INCREMENT10//数值可以根据实际情况确定 栈的定义如下 typedefstruct{void*base;void*top;intstackSize;}SqSTACK; base 表示栈底指针 top 表示栈顶指针 stackSize 表示栈当前可以使用的最大容量 若base的值是NULL,表示栈结构不存在;top初始值指向栈底,即top = base; 每当插入新的元素时,指针top就增1,反之删除就减1,非空栈中的栈顶指针始终在栈顶元素的下一个指针上面。 数据元素和栈顶指针的关系如下图所示: 链表栈 我们以链表栈的动态链表栈为例子,进行栈的设计。 首先是栈的结点,设计出两个结构体,一个结构体Node表示结点,其中包含有一个data域和next指针,如图所示: Node 其中data表示数据,next指针表示下一个的指针,其指向下一个结点,通过next指针将各个结点链接。 接下来是我们设计的重点,为这个进行限制性的设计,我们需要额外添加一个结构体,其包括了一个永远指向栈头的指针top和一个计数器count记录元素个数。 其主要功效就是设定允许操作元素的指针以及确定栈何时为空,如图所示: Stack 这里我采用的是top和count组合的方法。其代码可以表示为: //栈的结点设计//单个结点设计,数据和下一个指针typedefstructnode{intdata;structnode*next;}Node; 利用上面的结点创建栈,分为指向头结点的top指针和计数用的count typedefstructstack{Node*top;intcount;}Link_Stack; 栈的基本操作—入栈(压栈) 入栈的基本顺序可以用以下图所示: 入栈(push)操作时,我们只需要找到top所指向的空间,创建一个新的结点,将新的结点的next指针指向top指针指向的空间,再将top指针转移,并且指向新的结点,这就是是入栈操作。 其代码可以表示为: //入栈pushLink_Stack*Push_stack(Link_Stack*p,intelem){if(p==NULL)returnNULL;Node*temp;temp=(Node*)malloc(sizeof(Node));//temp=newNode;temp->data=elem;temp->next=p->top;p->top=temp;p->count++;returnp;} 栈的基本操作—出栈 出栈(pop)操作,是在栈不为空的情况下,重复说一次,一定要进行判空操作,将栈顶的元素删除,同时top指针,next向下进行移动即可的操作。 其代码可以表示为: //出栈popLink_Stack*Pop_stack(Link_Stack*p){Node*temp;temp=p->top;if(p->top==NULL){printf("错误:栈为空");returnp;}else{p->top=p->top->next;free(temp);//deletetemp;p->count--;returnp;}} 栈的基本操作—遍历 这个就很常见了,也是我们调试必须的手段。 栈的遍历相对而言比较复杂,由于栈的特殊性质,其只允许在一端进行操作,所以遍历操作操作永远都是逆序的。 简单一点描述,其过程为,在栈不为空的情况下,一次从栈顶元素向下访问,直到指针指向空(即到栈尾)为结束。 其代码可以表示为: //遍历栈:输出栈中所有元素intshow_stack(Link_Stack*p){Node*temp;temp=p->top;if(p->top==NULL){printf("");printf("错误:栈为空");return0;}while(temp!=NULL){printf("%d\t",temp->data);temp=temp->next;}printf("\n");return0;} 栈数组与栈链表的代码实现 最后呢,我们使用代码来帮助我们了解一下: 栈数组 数组栈是一种更为快速的模拟实现栈的方法,这里我们不多说。 模拟,就是不采用真实的链表设计,转而采用数组的方式进行模拟操作。 也就是说这是一种仿真类型的操作,其可以快速的帮助我们构建代码,分析过程,相应的实现起来也更加的便捷。 其代码如下: #include<stdio.h>#include<stdlib.h>#include<string.h>#definemaxn10000//结点设计typedefstructstack{intdata[maxn];inttop;}stack;//创建stack*init(){stack*s=(stack*)malloc(sizeof(stack));if(s==NULL){printf("分配内存空间失败");exit(0);}memset(s->data,0,sizeof(s->data));//memset操作来自于库文件string.h,其表示将整个空间进行初始化//不理解可以查阅百度百科https://baike.baidu.com/item/memset/4747579?fr=aladdins->top=0;//栈的top和bottom均为0(表示为空)returns;}//入栈pushvoidpush(stack*s,intdata){s->data[s->top]=data;s->top++;}//出栈popvoidpop(stack*s){if(s->top!=0){s->data[s->top]=0;//让其回归0模拟表示未初始化即可s->top--;}}//模拟打印栈中元素voidprint_stack(stack*s){for(intn=s->top-1;n>=0;n--){printf("%d\t",s->data[n]);}printf("\n");//习惯性换行}intmain(){stack*s=init();intinput[5]={11,22,33,44,55};//模拟五个输入数据for(inti=0;i<5;i++){push(s,input[i]);}print_stack(s);/////////////pop(s);print_stack(s);return0;} 其编译结果如下: 栈链表 #include<stdio.h>#include<stdlib.h>//栈的结点设计//单个结点设计,数据和下一个指针typedefstructnode{intdata;structnode*next;}Node;//利用上面的结点创建栈,分为指向头结点的top指针和计数用的counttypedefstructstack{Node*top;intcount;}Link_Stack;//创建栈Link_Stack*Creat_stack(){Link_Stack*p;//p=newLink_Stack;p=(Link_Stack*)malloc(sizeof(Link_Stack));if(p==NULL){printf("创建失败,即将退出程序");exit(0);}else{printf("创建成功\n");}p->count=0;p->top=NULL;returnp;}//入栈pushLink_Stack*Push_stack(Link_Stack*p,intelem){if(p==NULL)returnNULL;Node*temp;temp=(Node*)malloc(sizeof(Node));//temp=newNode;temp->data=elem;temp->next=p->top;p->top=temp;p->count++;returnp;}//出栈popLink_Stack*Pop_stack(Link_Stack*p){Node*temp;temp=p->top;if(p->top==NULL){printf("错误:栈为空");returnp;}else{printf("\npopsuccess");p->top=p->top->next;free(temp);//deletetemp;p->count--;returnp;}}//遍历栈:输出栈中所有元素intshow_stack(Link_Stack*p){Node*temp;temp=p->top;if(p->top==NULL){printf("");printf("错误:栈为空");return0;}while(temp!=NULL){printf("%d\t",temp->data);temp=temp->next;}printf("\n");return0;}intmain(){//用主函数测试一下功能inti;Link_Stack*p;p=Creat_stack();intn=5;intinput[6]={10,20,30,40,50,60};/////////////以依次入栈的方式创建整个栈//////////////for(i=0;i<n;i++){Push_stack(p,input[i]);}show_stack(p);////////////////////出栈///////////////////////Pop_stack(p);show_stack(p);return0;} 编译结果如下: 关于栈的总结 栈-它是一种运算受限的线性表,在数制转换,括号匹配的检验,表达式求值等方面都可以使用,并且较为简便的解决问题。 今天栈基础就讲到这里,下一期,我们再见! 推荐阅读: 嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 关注 微信公众号『技术让梦想更伟大』,后台回复“ m ”查看更多内容,回复“ 加群 ”加入技术交流群。 长按前往图中包含的公众号关注 本文分享自微信公众号 - 技术让梦想更伟大(gh_f7effb2fbc1c)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

【高并发】ThreadLocal学会了这些,你也能和面试官扯皮了!

点击上方蓝色“冰河技术”,关注并选择“设为星标” 持之以恒,贵在坚持,每天进步一点点! 作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址: https://github.com/sunshinelyz/mykit-delay PS: 欢迎各位Star源码,也可以pr你牛逼哄哄的代码。 前言 我们都知道,在多线程环境下访问同一个共享变量,可能会出现线程安全的问题,为了保证线程安全,我们往往会在访问这个共享变量的时候加锁,以达到同步的效果,如下图所示。 对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而ThreadLocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量。 什么是ThreadLocal? ThreadLocal是JDK提供的,支持线程本地变量。也就是说,如果我们创建了一个ThreadLocal变量,则访问这个变量的每个线程都会有这个变量的一个本地副本。如果多个线程同时对这个变量进行读写操作时,实际上操作的是线程自己本地内存中的变量,从而避免了线程安全的问题。 ThreadLocal使用示例 例如,我们使用ThreadLocal保存并打印相关的变量信息,程序如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newThreadLocal<String>();publicstaticvoidmain(String[]args){//创建第一个线程ThreadthreadA=newThread(()->{threadLocal.set("ThreadA:"+Thread.currentThread().getName());System.out.println("线程A本地变量中的值为:"+threadLocal.get());});//创建第二个线程ThreadthreadB=newThread(()->{threadLocal.set("ThreadB:"+Thread.currentThread().getName());System.out.println("线程B本地变量中的值为:"+threadLocal.get());});//启动线程A和线程BthreadA.start();threadB.start();}} 运行程序,打印的结果信息如下所示。 线程A本地变量中的值为:ThreadA:Thread-0线程B本地变量中的值为:ThreadB:Thread-1 此时,我们为线程A增加删除ThreadLocal中的变量的操作,如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newThreadLocal<String>();publicstaticvoidmain(String[]args){//创建第一个线程ThreadthreadA=newThread(()->{threadLocal.set("ThreadA:"+Thread.currentThread().getName());System.out.println("线程A本地变量中的值为:"+threadLocal.get());threadLocal.remove();System.out.println("线程A删除本地变量后ThreadLocal中的值为:"+threadLocal.get());});//创建第二个线程ThreadthreadB=newThread(()->{threadLocal.set("ThreadB:"+Thread.currentThread().getName());System.out.println("线程B本地变量中的值为:"+threadLocal.get());System.out.println("线程B没有删除本地变量:"+threadLocal.get());});//启动线程A和线程BthreadA.start();threadB.start();}} 此时的运行结果如下所示。 线程A本地变量中的值为:ThreadA:Thread-0线程B本地变量中的值为:ThreadB:Thread-1线程B没有删除本地变量:ThreadB:Thread-1线程A删除本地变量后ThreadLocal中的值为:null 通过上述程序我们可以看出,线程A和线程B存储在ThreadLocal中的变量互不干扰,线程A存储的变量只能由线程A访问,线程B存储的变量只能由线程B访问。 ThreadLocal原理 首先,我们看下Thread类的源码,如下所示。 publicclassThreadimplementsRunnable{/***********省略N行代码*************/ThreadLocal.ThreadLocalMapthreadLocals=null;ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;/***********省略N行代码*************/} 由Thread类的源码可以看出,在ThreadLocal类中存在成员变量threadLocals和inheritableThreadLocals,这两个成员变量都是ThreadLocalMap类型的变量,而且二者的初始值都为null。只有当前线程第一次调用ThreadLocal的set()方法或者get()方法时才会实例化变量。 这里需要注意的是:每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程的threadLocals变量里面的。也就是说,调用ThreadLocal的set()方法存储的本地变量是存放在具体线程的内存空间中的,而ThreadLocal类只是提供了set()和get()方法来存储和读取本地变量的值,当调用ThreadLocal类的set()方法时,把要存储的值放入调用线程的threadLocals中存储起来,当调用ThreadLocal类的get()方法时,从当前线程的threadLocals变量中将存储的值取出来。 接下来,我们分析下ThreadLocal类的set()、get()和remove()方法的实现逻辑。 set()方法 set()方法的源代码如下所示。 publicvoidset(Tvalue){//获取当前线程Threadt=Thread.currentThread();//以当前线程为Key,获取ThreadLocalMap对象ThreadLocalMapmap=getMap(t);//获取的ThreadLocalMap对象不为空if(map!=null)//设置value的值map.set(this,value);else//获取的ThreadLocalMap对象为空,创建Thread类中的threadLocals变量createMap(t,value);} 在set()方法中,首先获取调用set()方法的线程,接下来,使用当前线程作为Key调用getMap(t)方法来获取ThreadLocalMap对象,getMap(Thread t)的方法源码如下所示。 ThreadLocalMapgetMap(Threadt){returnt.threadLocals;} 可以看到,getMap(Thread t)方法获取的是线程变量自身的threadLocals成员变量。 在set()方法中,如果调用getMap(t)方法返回的对象不为空,则把value值设置到Thread类的threadLocals成员变量中,而传递的key为当前ThreadLocal的this对象,value就是通过set()方法传递的值。 如果调用getMap(t)方法返回的对象为空,则程序调用createMap(t, value)方法来实例化Thread类的threadLocals成员变量。 voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);} 也就是创建当前线程的threadLocals变量。 get()方法 get()方法的源代码如下所示。 publicTget(){//获取当前线程Threadt=Thread.currentThread();//获取当前线程的threadLocals成员变量ThreadLocalMapmap=getMap(t);//获取的threadLocals变量不为空if(map!=null){//返回本地变量对应的值ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}//初始化threadLocals成员变量的值returnsetInitialValue();} 通过当前线程来获取threadLocals成员变量,如果threadLocals成员变量不为空,则直接返回当前线程绑定的本地变量,否则调用setInitialValue()方法初始化threadLocals成员变量的值。 privateTsetInitialValue(){//调用初始化Value的方法Tvalue=initialValue();Threadt=Thread.currentThread();//根据当前线程获取threadLocals成员变量ThreadLocalMapmap=getMap(t);if(map!=null)//threadLocals不为空,则设置value值map.set(this,value);else//threadLocals为空,创建threadLocals变量createMap(t,value);returnvalue;} 其中,initialValue()方法的源码如下所示。 protectedTinitialValue(){returnnull;} 通过initialValue()方法的源码可以看出,这个方法可以由子类覆写,在ThreadLocal类中,这个方法直接返回null。 remove()方法 remove()方法的源代码如下所示。 publicvoidremove(){//根据当前线程获取threadLocals成员变量ThreadLocalMapm=getMap(Thread.currentThread());if(m!=null)//threadLocals成员变量不为空,则移除value值m.remove(this);} remove()方法的实现比较简单,首先根据当前线程获取threadLocals成员变量,不为空,则直接移除value的值。 注意:如果调用线程一致不终止,则本地变量会一直存放在调用线程的threadLocals成员变量中,所以,如果不需要使用本地变量时,可以通过调用ThreadLocal的remove()方法,将本地变量从当前线程的threadLocals成员变量中删除,以免出现内存溢出的问题。 ThreadLocal变量不具有传递性 使用ThreadLocal存储本地变量不具有传递性,也就是说,同一个ThreadLocal在父线程中设置值后,在子线程中是无法获取到这个值的,这个现象说明ThreadLocal中存储的本地变量不具有传递性。 接下来,我们来看一段代码,如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newThreadLocal<String>();publicstaticvoidmain(String[]args){//在主线程中设置值threadLocal.set("ThreadLocalTest");//在子线程中获取值Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("子线程获取值:"+threadLocal.get());}});//启动子线程thread.start();//在主线程中获取值System.out.println("主线程获取值:"+threadLocal.get());}} 运行这段代码输出的结果信息如下所示。 主线程获取值:ThreadLocalTest子线程获取值:null 通过上述程序,我们可以看出在主线程中向ThreadLocal设置值后,在子线程中是无法获取到这个值的。那有没有办法在子线程中获取到主线程设置的值呢?此时,我们可以使用InheritableThreadLocal来解决这个问题。 InheritableThreadLocal使用示例 InheritableThreadLocal类继承自ThreadLocal类,它能够让子线程访问到在父线程中设置的本地变量的值,例如,我们将ThreadLocalTest类中的threadLocal静态变量改写成InheritableThreadLocal类的实例,如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newInheritableThreadLocal<String>();publicstaticvoidmain(String[]args){//在主线程中设置值threadLocal.set("ThreadLocalTest");//在子线程中获取值Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("子线程获取值:"+threadLocal.get());}});//启动子线程thread.start();//在主线程中获取值System.out.println("主线程获取值:"+threadLocal.get());}} 此时,运行程序输出的结果信息如下所示。 主线程获取值:ThreadLocalTest子线程获取值:ThreadLocalTest 可以看到,使用InheritableThreadLocal类存储本地变量时,子线程能够获取到父线程中设置的本地变量。 InheritableThreadLocal原理 首先,我们来看下InheritableThreadLocal类的源码,如下所示。 publicclassInheritableThreadLocal<T>extendsThreadLocal<T>{protectedTchildValue(TparentValue){returnparentValue;}ThreadLocalMapgetMap(Threadt){returnt.inheritableThreadLocals;}voidcreateMap(Threadt,TfirstValue){t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);}} 由InheritableThreadLocal类的源代码可知,InheritableThreadLocal类继承自ThreadLocal类,并且重写了ThreadLocal类的childValue()方法、getMap()方法和createMap()方法。也就是说,当调用ThreadLocal的set()方法时,创建的是当前Thread线程的inheritableThreadLocals成员变量而不再是threadLocals成员变量。 这里,我们需要思考一个问题:InheritableThreadLocal类的childValue()方法是何时被调用的呢?这就需要我们来看下Thread类的构造方法了,如下所示。 publicThread(){init(null,null,"Thread-"+nextThreadNum(),0);}publicThread(Runnabletarget){init(null,target,"Thread-"+nextThreadNum(),0);}Thread(Runnabletarget,AccessControlContextacc){init(null,target,"Thread-"+nextThreadNum(),0,acc,false);}publicThread(ThreadGroupgroup,Runnabletarget){init(group,target,"Thread-"+nextThreadNum(),0);}publicThread(Stringname){init(null,null,name,0);}publicThread(ThreadGroupgroup,Stringname){init(group,null,name,0);}publicThread(Runnabletarget,Stringname){init(null,target,name,0);}publicThread(ThreadGroupgroup,Runnabletarget,Stringname){init(group,target,name,0);}publicThread(ThreadGroupgroup,Runnabletarget,Stringname,longstackSize){init(group,target,name,stackSize);} 可以看到,Thread类的构造方法最终调用的是init()方法,那我们就来看下init()方法,如下所示。 privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){/************省略部分源码************/if(inheritThreadLocals&&parent.inheritableThreadLocals!=null)this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/*StashthespecifiedstacksizeincasetheVMcares*/this.stackSize=stackSize;/*SetthreadID*/tid=nextThreadID();} 可以看到,在init()方法中会判断传递的inheritThreadLocals变量是否为true,同时父线程中的inheritableThreadLocals是否为null,如果传递的inheritThreadLocals变量为true,同时,父线程中的inheritableThreadLocals不为null,则调用ThreadLocal类的createInheritedMap()方法。 staticThreadLocalMapcreateInheritedMap(ThreadLocalMapparentMap){returnnewThreadLocalMap(parentMap);} 在createInheritedMap()中,使用父线程的inheritableThreadLocals变量作为参数创建新的ThreadLocalMap对象。然后在Thread类的init()方法中会将这个ThreadLocalMap对象赋值给子线程的inheritableThreadLocals成员变量。 接下来,我们来看看ThreadLocalMap的构造函数都干了啥,如下所示。 privateThreadLocalMap(ThreadLocalMapparentMap){Entry[]parentTable=parentMap.table;intlen=parentTable.length;setThreshold(len);table=newEntry[len];for(intj=0;j<len;j++){Entrye=parentTable[j];if(e!=null){@SuppressWarnings("unchecked")ThreadLocal<Object>key=(ThreadLocal<Object>)e.get();if(key!=null){//调用重写的childValue方法Objectvalue=key.childValue(e.value);Entryc=newEntry(key,value);inth=key.threadLocalHashCode&(len-1);while(table[h]!=null)h=nextIndex(h,len);table[h]=c;size++;}}}} 在ThreadLocalMap的构造函数中,调用了InheritableThreadLocal类重写的childValue()方法。而InheritableThreadLocal类通过重写getMap()方法和createMap()方法,让本地变量保存到了Thread线程的inheritableThreadLocals变量中,线程通过InheritableThreadLocal类的set()方法和get()方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。此时,如果父线程创建子线程,在Thread类的构造函数中会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量中。 如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。 最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。 后记: 记住:你比别人强的地方,不是你做过多少年的CRUD工作,而是你比别人掌握了更多深入的技能。不要总停留在CRUD的表面工作,理解并掌握底层原理并熟悉源码实现,并形成自己的抽象思维能力,做到灵活运用,才是你突破瓶颈,脱颖而出的重要方向! 你在刷抖音,玩游戏的时候,别人都在这里学习,成长,提升,人与人最大的差距其实就是思维。你可能不信,优秀的人,总是在一起。。 扫一扫或长按二维码 关注冰河技术微信公众号 如果你喜欢这篇文章,欢迎点赞和转发。 生活很美好,明天见(。・ω・。)ノ♡ 本文分享自微信公众号 - 冰河技术(hacker-binghe)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

数据源面试三连杀:是啥?为什么要用?怎么用?

云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 一、概述 在日常项目中肯定需要用到数据源,那么数据源是什么,当遇到分布式事务的场景时数据源与非分布式事务场景的数据源又有什么不同呢,在J2EE中分布式事务又是如何实现的呢,希望本文可以解答您的疑惑。 二、 数据源 2.1 数据源是什么 通俗来讲,数据源是存储数据的地方。例如,数据库是数据源,其他系统也可以是数据源。在J2EE里,数据源是代表物理数据存储系统的实际Java对象。通过这些对象,J2EE应用可以获取到数据库的JDBC连接。 2.2 数据源的设计 从UML图上可以看出,CommonDaraSource是对数据源概念的顶层抽象,约束了数据源必须实现的方法。数据源有三种类型的实现,分别是: DataSource,基本实现,用于生成标准Connection对象 ConnectionPoolDataSource,连接池实现,这个数据源并不会直接创建数据库物理连接,而是一个逻辑实现,它的作用在于池化数据库物理连接。由于数据库物理连接是一个重量级的对象,频繁的创建销毁很影响性能,将物理连接池化后可降低创建和销毁的频率,复用连接以充分利用连接资源。 XAConnection,分布式事务实现,为支持分布式事务而诞生,这个数据源直接生产出的不是数据库物理连接Connection,而是一个支持XA的XAConnection对象,XAConnection对象可以直接生产数据库物理连接,同时生产XAResource用于支持XA事务,通常XAConnection对象生产出的数据库物理连接Connection需要和该XAConnection生产出的XAResource对象配合使用以完成XA事务处理。并且XAConnection继承PooledConnection,那就也具备连接池的实现。 三、为什么需要XA数据源 3.1 XA数据源是什么 XA数据源指的是支持XA规范的数据源,支持分布式事务。 3.2 XA规范是什么 XA规范是一种分布式事务解决方案。X/OPEN组织定义的分布式事务处理模型(DTP),其包含3种角色和两个协议: AP(Application,应用程序) RM(Resources manager,资源管理器),通常指数据库 TM(Transaction manager,事务管理器),通常指事务协调者,负责协调和管理事务,提供给AP接口以及管理资源 XA协议是事务管理器与资源管理器之间的通信接口 TX协议是应用程序与事务管理器之间的通信接口 该模型中应用程序将一个全局事务传送到事务管理器,事务管理器将每个全局事务分解为多个分支(分支事务),并将分支事务分配给单独的资源管理器进行服务,事务管理器通过XA接口将每个分支事务与适当的资源管理器进行协调。 3.3 分布式事务具备有什么样的作用? 如果仅在同一个事务上下文中需要协调多种资源(即数据库以及消息主题或队列等等),这个事务中的所有操作都必须成功,否则所有操作都将在失败的情况下回滚。这就是XA数据源的作用。 3.4 那什么样的场景需要使用XA? 您的JavaEE应用程序必须使用单个事务将数据存储在两个数据库中 您的应用程序需要通过单个事务发送JMS消息并将信息存储在数据库中 您希望使用PVP将您自己项目的域信息存储在一个不同的数据库中,而这个数据库是被jBPM用来存储它自己的数据。 四、 那怎么使用分布式事务呢? 4.1 J2EE的分布式事务 Java事务编程接口(Java Transaction API,JTA)和Java事务服务(Java Transaction Service,JTS)为J2EE平台提供了分布式事务服务。 JTA事务是XA规范的Java实现,JTA事务有效的屏蔽了底层事务资源,使应用可以以透明的方式参与到事务处理中。分布式事务包括事务管理器和一个或多个支持XA协议的资源管理器。 JTA是面向应用或应用服务器与资源管理器的高层事务接口。 JTS是一组约定JTA中角色之间交互细节的规范。 JTA提供了以下四个接口 javax.transaction.UserTransaction,是面向开发人员的接口,能够编程地控制事务处理。UserTransaction.begin方法开启一个全局事务,并且将该事务与调用线程关联起来。 javax.transaction.TransactionManager,允许应用程序服务器来控制代表正在管理的应用程序的事务。它本身并不承担实际的事务处理功能,它更多的是充当UserTransaction接口和Transaction接口之间的桥梁。 javax.transaction.Transaction,代表了一个物理意义上的事务,在开发人员调用UserTransaction.begin()方法时TransactionManager会创建一个Transaction事务对象, javax.transaction.xa.XAResource,面向提供商的实现接口,是一个基于X/Open CAE Specification的行业标准XA接口的Java映射。提供商在提供访问自己资源的驱动时,必须实现这样的接口。 开发者调用UserTransaction.begin方法时,因为UserTransaction的实现类持有TransactionManager,TransactionManager充当UserTransaction和Transaction之间的桥梁,所以在调用UserTransaction的begin方法时,TransactionManager会创建Transaction事务对象,并把此对象通过ThreadLocal关联到当前线程。当调用UserTransaction其他方法时,会从当前线程取出事务对象Transaction对象,并通过Transaction对象找到与其关联的XAResource对象,然后进行commit、rollback等操作。其基本流程如以下代码: // 创建一个Transaction,挂到当前线程上 UserTransaction userTx = null; Connection connA = null; Connection connB = null; try{ userTx.begin(); // 将Connection对应的XAResource挂到当前线程对应的Transaction connA.exec("xxx") connB.exec("xxx") // 找到Transaction关联的XAResource,让它们都提交 userTx.commit(); }catch(){ // 找到Transaction关联的XAResource,让它们都回滚 userTx.rollback(); } 4.2 如何使用J2EE的分布式事务 WebLogic、Websphere、JBoss等主流的应用服务器提供了JTA的实现和支持。 Tomcat中没有提供JTA的实现的,这就需要借助第三方的框架Jotm、Automikos等来实现。 五、总结 本文主要介绍了数据源和XA数据源,以及分布式事务基本原理、作用和场景以及如何使用J2EE分布式事务,但这种是属于基于资源层的底层分布式事务解决方案,在业内,用来解决分布式事务的方案还有柔性事务,柔性事务包括几种类型:两阶段型、补偿型、异步确保型和最大努力通知型,有兴趣可以更深入的了解一下。 【云栖号在线课堂】每天都有产品技术专家分享!课程地址:https://yqh.aliyun.com/live 立即加入社群,与专家面对面,及时了解课程最新动态!【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK 原文发布时间:2020-07-24本文作者:程序猿DD_本文来自:“掘金”,了解相关信息可以关注“掘金”

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。