注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

一文扫清DDD核心概念理解障碍

引言 在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些...
继续阅读 »

引言


在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些看似复杂的概念形象化、简单化。


领域、子域、核心域等这么多域到底怎么理解?


在DDD的众多概念中,首先需要搞清楚的就是到底什么是领域。因为DDD是领域驱动设计,所以领域是DDD的核心基础概念。那么到底什么是领域呢?领域是指某个业务范围以及在范围内进行的活动。根据这个定义,我们知道领域最重要的一个核心点就是范围,只有限定了问题研究的范围,才能针对具体范围内的问题进行研究分析,在后期 进行微服务拆分的的时候也是根据范围来进行的。


我们开发的软件平台是为了解决用户问题,既然我们需要研究问题并解决问题,那就得先确定问题的范围到底是什么。如果我们做的是电商平台,那么我们研究的是电商这个领域或者说电商这个范围的问题,实体店内的销售情况就不是我们研究的问题范围了。因此我们可以把领域理解为我们需要解决指定业务范围内的问题域。再举个生活中的例子,派出所实际都是有自己的片区的也就是业务范围,户籍管理、治安等都是归片区的派出所负责的。这里的片区实际就是领域,派出所专注解决自己片区内的各种事项。


既然我们研究的领域确定了,或者说研究的问题域以及范围确定了,那么接下来就需要对领域进行进一步的划分和切割。实际上这和我们研究事物的一般方法手段是一致的,一旦某个问题太大无从下手的时候,都会将问题进行一步步的拆解,再逐个进行分析和解决。那么放到DDD中,我们在进行分析领域的时候,如果领域对应的业务范围过大,那么就需要对领域进行拆解划分,形成对应的子域或者说更小的问题域,所以说子域对应的是相对于领域来说,更小的业务范围以及问题域。


回到我们刚才所说的电商领域,它就是一个非常大的领域,因为电商实际还包含了商品、用户、营销、支付、订单、物流等等各种复杂的业务。因此支付域、物流域等就是相对于电商来说更小的业务范围或者更小的问题域,那么这部分领域就是对于电商这个领域的子域,相当于对电商这个业务范围的进一步的划分。


image.png


搞清楚了领域和子域的区别之后,那么怎么理解核心域、通用域以及支撑域这么多其他的域呢(域太多了,我脑袋开始嗡嗡响了)?领域和子域是按照范围大小进行区分的,那么核心域、通用域等实际就是按照功能属性进行划分的。


核心域:平台的核心竞争力、最重要的业务,比如对于阿里来说,电商就是核心域。


通用域:其他子域沉淀的通用能力,没有定制化的能力,比如公司的数据中台。


支撑域:不包含核心业务能力也不是各个子域的通用能力沉淀。


那么为什么划分了子域之后,还要分什么核心域、通用域呢?实际上这样划分的目的是根据子域的属性,确定公司对于不同域的资源投入。将优势的资源投入到具备核心竞争力的域上,也是为了让产品更加具备竞争力,就是所谓的钱要花到刀刃上。


限界上下文?限界是什么?上下文又是什么?


限界上下文我觉得是DDD中一个不太好理解的概念,光看这个不明觉厉的名字,甚至有点不知道它到底想表达什么样的意思。我们先来看下限界上下文的原文---Bounded Context,通过原文我们可以看得出来,实际上限界上下文这个翻译增加了我们的理解成本。而反观Bounded Context这个原文实际更好理解一点,即为有边界的上下文。这里给大家一个小建议,如果技术上某个概念不好理解,那么不妨去看看它的原文是什么,大部分情况下原文会比翻译过来的更好理解,更能反映设计者想要表达的真实含义。


image.png


大家都知道我们的语言是有上下文环境的,有的时候同样一句话在不同的语言环境或者说语言上下文中,所代表的意思是不一样的。打个比方假如你有一个女朋友,你们约好晚上一起去吃饭,你在去晚上吃饭地方的路上,这个时候你收到一条来自女朋友的语音:“我已经到龙翔桥了,你出来后往苹果店走。如果你到了,我还没到,你就等着吧。如果我到了,你还没到,你就等着吧。”这里的你就等着吧,在不同的语境下包含的意思是不同的,一个是陈述事实,一个让你瑟瑟发抖。


因此,既然语言本身就有上下文,那么用通用语言描述的业务肯定也是有边界的。DDD中的限界上下文就是用来圈定业务范围的,目的是为了确保业务语言在限界上下文内的表达唯一,不会产生额外的歧义。这个时候大家会不会有另外一个问题,那么这个限界上下文到底是一个逻辑概念还是代码层面会有一个实实在在的边界呢?


按照我自己的理解,限界上下文既是概念上的业务边界,也是代码层面的逻辑逻辑边界。为什么这么说呢?我们在进行业务划分的时候,领域划分为子域集合,子域再划分为子子域集合,那么子子域的业务边界有时候就会和限界上下文的边界重合,也就是说子子域本身就是限界上下文,那么此时限界上下文就是业务边界。在代码落地的过程中,用户服务涉及到用户的创建、用户信息的修改等操作。肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的边界上下文中进行。


域和边界上下文的关系是一对一或者一对多的关系,实际上我认为域和限界上下文本质上一致的,应该是为什么这么说呢,比如我们做的微服务当中用户服务,比如,肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的限界上下文中进行。限界上下文最主要的作用就是限定哪些业务面描述以及业务动作在这个限界当中。


image.png


总结
DDD在实际落地实践过程中会遇到各种各样的问题,首当其冲的就是一些核心概念晦涩难懂,阻碍了技术人员对DDD的理解和掌握。本文对DDD比较难理解的核心概念进行了详细的描述,相信通过本文大家对于这些核心概念的理解能够更加深入。


创作不易,如果各位同学觉得文章还不错的话,麻烦点赞+收藏+评论交流哦。老样子,文末和大家分享一首诗词。


西江月▪中秋和子由


世事一场大梦,人生几度秋凉?夜来风叶已鸣廊,看取眉头鬓上。


酒贱常愁客少,月明多被云妨。中秋谁与共孤光,把盏凄然北望。


作者:慕枫技术笔记
来源:https://juejin.cn/post/7084074668147081230 收起阅读 »

Redis 缓存穿透与缓存击穿

一、🐕缓存穿透(查不到数据) 比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持...
继续阅读 »

一、🐕缓存穿透(查不到数据)


比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透



解决方案




布隆过滤器


布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
在这里插入图片描述




缓存空对象


当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了持久层数据库
在这里插入图片描述


但是这种方法会存在两个问题


1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,并且当中还有还能多空值的键


2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响



二、🐂缓存击穿(量太大,缓存过期)


这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的杠大并发,大并发集中对这个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一堵墙上戳穿了一个洞
当某个key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库瞬间压力过大



解决方案



设置热点数据永不过期



从缓存层面上看,没有设置过期时间,所以不会出现热点key过期后产生的问题



加互斥锁



分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获取分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大



三、🐅缓存雪崩


缓存雪崩,是指在某一个时间段,缓存集中过期失效,Redis宕机


产生雪崩的原因之一,比如在双十一零点,有一波商品比较热门,所以这波商品会放在缓存,假设缓存一个小时,那么到了1点钟时,缓存即将过期,此时如果再对此波商品进行访问查询,压力最终就会落到数据库上,对于数据库而言,就会产生周期性的压力波峰,于是所有的请求都会到达存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。


在这里插入图片描述


其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,此时,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。



解决方案


1、Redis 高可用


既然 redis 有可能挂掉,那我就设多几台 redis ,其实就是搭建集群


2、限流降级


在缓存失效后,通过加锁或者 队列来控制读数据库写缓存的线程数量,比如对某个key 只允许一个线程查询数据和写缓存,其他线程等待


3、数据预热


数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀



作者:CSDNCoder
来源:https://juejin.cn/post/7084820115505545247
收起阅读 »

系统模块划分设计的思考

系统模块划分设计的思考前言首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。直接原因公司内部某技术团队,在引用我们系统的client包时,启动失败...
继续阅读 »

系统模块划分设计的思考

前言

首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。

直接原因

公司内部某技术团队,在引用我们系统的client包时,启动失败。
失败原因是由于client下有一个cache相关的依赖,其注入失败导致的。

然后,就发出了这样一个疑问:我只是希望使用一个hsf接口,为什么还要引入诸如缓存,web处理工具等不相关的东西。

这也就自然地引出了前辈对我的一句教导:对外的client需要尽可能地轻便。

很明显,我们原有的client太重了,包含了对外的RPC接口,相关模型(如xxxDTO),工具包等等。

可能有人就要提出,直接RPC+模型一个包,其它内容一个包不就OK了嘛?

问题真的就这么简单嘛?

根本原因

其实出现上述问题,是因为在系统设计之初,并没有深入思考client包的定位,以及日后可能遇到的情况。

这也就导致了今天这样的局面,所幸目前的外部引用并不多,更多是内部引用。及时调整,推广新的依赖,与相关规范为时不晚。

常见模块拆分

先说说我以前的模块拆分。最早的拆分是每个业务模块主要拆分为:

  • xxx-service:具体业务实现模块。

  • xxx-client:对外提供的RPC接口模块。

  • xxx-common:对外的工具,以及模型。

这种拆分方式,是我早期从一份微服务教程中看到的。优点是简单明了,调用方可选择性地选择需要的模块引入。

至于一些通用的组件,如统一返回格式(如ServerResponse,RtObject),则放在了最早的核心(功能核心,但内容很少)模块上。

后来,认为这样并不合适,不应该将通用组件放在一个业务模块上。所以建立了一个base模块,用来将通用的组件,如工具,统一返回格式等都放入其中。

另外,将每个服务都有的xxx-common模块给取消了。将其中的模型,放入了xxx-client,毕竟是外部调用需要的。将其中的工具,根据需要进行拆分:

  • base:多个服务都会使用的。

  • xxx-service:只有这个服务本身使用

  • xxx-client:有限的服务使用,并且往往是服务提供方和服务调用方都要使用。但是往往这种情况,大多是由于接口设计存在问题导致的。所以多为过渡方案。

上述这个方案,也就是我在负责某物联网项目时采用的最终模块划分方式。

在当时的业务下,该方案的优点是模块清晰,较为简洁,并且尽可能满足了迪米特原则(可以参考《阿里Java开发手册相关实践》)。缺点则是需要一定的技术水平,对组件的功能域认识清晰。并且需要有一定的设计思考与能力(如上述工具拆分的第三点-xxx-client,明白为什么这是设计缺陷导致,并能够解决)。

新的问题

那么,既然上述的方案挺不错的,为什么不复用到现在的项目呢?

因为业务变了,导致应用场景变了。而这也带来了新的问题,新的考虑角度。

原先的物联网业务规模并不大,所以依赖也较为简单,也并不需要进行依赖的封装等,所以针对主要是client的内/外这一维度考虑的。

但是现有的业务场景,由于规模较大,模块依赖层级较多,导致上层模块引入过多的依赖。如有一个缓存模块,依赖tair-starter(一个封装的key-value的存储),然后日志模块依赖该缓存模块(进行性能优化),紧接着日志模块作为一个通用模块,被放入了common模块中。依赖链路如下:

调用方 -> common模块 -> 日志模块 -> 缓存模块 -> tair-starter依赖

但是有的调用方表示,根本就不需要日志模块,却引入了tair-starter这一重依赖(starter作为封装,都比较重),甚至由于tair-starter的内部依赖与自身原有依赖冲突,得去排查依赖,进行exclude。
但是同时,也有的调用方,系统通过rich客户端,达到性能优化等目标。

所以,现有的业务场景除了需要考虑client的内/外这一维度,还需要考虑client的pool/rich这一维度。

可能有的小伙伴,看到这里有点晕乎乎的,这两个维度考量的核心在哪里?

内/外,考虑的是按照内外这条线,尽量将client设计得简洁,避免给调用方引入无用依赖。

而pool/rich,考虑的是性能,用户的使用成本(是否开箱即用)等。

最终解决方案

最终的解决方案是对外提供3+n

  • xxx-client(1个):所有外部系统引用都需要的内容,如统一返回格式等。

  • xxx-yyy-client(n个):对具体业务依赖的引用,进行了二次拆分。如xxx-order-client(这里是用订单提花那你一下,大家理解意思就OK)。

  • xxx-pool-client(1个):系统引用所需要的基本依赖,如Lindorm的依赖等。

  • xxx-rich-client(1个):系统引用所需要的依赖与对应starter,如一些自定义的自动装载starter(不需要用户进行配置)。

这个方案,换个思路,理解也简单。
我们提供相关的能力,具体如何选择,交给调用方决定。

其实,讨论中还提到了BOM方案(通过DependentManagement进行jar包版本管理)。不过分析后,我们认为BOM方案更适合那些依赖集比较稳定的client,如一些中间件。而我们目前的业务系统,还在快速发展,所以并不适用。

总结

简单来说,直接从用户需求考虑(这里的用户就是调用方):

  • 外部依赖:

    • 额外引入的依赖尽可能地少,最好只引入二方依赖(我们提供的jar),不引入第三方依赖。

    • 引入的二方依赖不“夹带”私货(如二方jar引入了一堆大第三方依赖)。

  • 自动配置:

    • 可以傻瓜式使用。如引入对应的starter依赖,就可以自动装配对应默认配置。

    • 也可以自定义配置。用户可以在自定义配置,并不用引入无效的配置(因为starter经常引入不需要的依赖)。

  • 性能:

    • 可以通过starter,提供一定的封装,保证一定的性能(如接口缓存,请求合并等)。

    • 可以自定义实现基础功能。因为有些人并不放心功能封装(虽然只是少数,但是稳定性前辈提出的)。

补充

这里补充一点,我对讨论中一个问题的回答,这里提一下。

有人提到工具类,应该如何划分。因为有的工具类,是不依赖于服务状态的,如CookieUtil进行Cookie处理。有的工具类,是依赖于服务状态的,如RedisUtil包含RedisPool状态,直连Redis,处理Redis请求与响应。

其实这里有两个细节:

  • 工具应该按照上面的方式进行划分为两种。单一模块系统,不依赖服务状态的工具往往置于util包,依赖服务状态的工具往往置于common包中。这里还有一个明显的区分点:前者的方法可以设为static,而后者并不能(因为依赖于new出来的状态)。

  • 依赖于状态的工具类,其实是一种拆分不完全的体现。如RedisUtil,可以拆分为连接状态管理的RedisPool与请求响应处理的RedisUitl。两者的组合方式有很多,取决于使用者的需要,以后有机会写一篇相关的博客。不过,我希望大家记住 面向接口编程的原则。

愿与诸君共进步。

原文链接:https://blog.csdn.net/cureking/article/details/105663951

收起阅读 »

不幸言中,“核酸码”打不开.....那就聊聊为什么我觉得要挂的原因吧!

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码。当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。我也顺带瞎扯了一句...
继续阅读 »

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码

当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:

但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。

我也顺带瞎扯了一句:可能会出性能问题(因为我一直觉得国内擅长Hibernate的开发者比较少)


谁想到,今天在获取核酸码的时候真的碰到各种困难,在获取核酸码的时候,就一直刷不出来,有时候显示人多,有时候504错误:

上面我是12点尝试的,后来16、17点还看到很多朋友圈反应各种卡住,刷不出来。


可能这个系统确实太赶了,所以没做好?不过这个谁知道呢?作为一名技术博主就不瞎猜了。

顺手分享一下为什么我觉得用spring data jpa,很可能会挂?

先说说常规国内用的比较多的技术MyBatis,因为大家都是用直接写SQL的方式来实现数据读写的,这个时候团队里DBA、数据库专家、或者实力强点的开发,往往自己已经能够把SQL执行优化到比较好的地步了。这个是否能做好,与我们对SQL、Java这些知识的掌握程度有关

而当我们用Spring Data JPA这样的框架时候,开发者在框架的帮助下,好多SQL都被隐藏了,喜欢些Java代码来替代SQL的开发过程是挺爽的,但也因为这个原因,他可能并不知道最终自己写的代码真正会执行的SQL具体是怎么样的。

这的时候对于优化就带了很大的难度,对于专业DBA来说,他一般都是不具备Spring Data JPA代码到SQL转化的认识,他是很难帮你做静态分析的。而开发者一侧也有这个问题,如果不是很熟悉Hibernate的话,就很容易写出低性能的代码(不代表框架实现的低性能,核心还是使用姿势的问题)。

所以,我一直建议在高并发系统中对数据访问框架的选型一定要慎重,不是说Spring Data JPA不行,而是需要有熟悉的人来把握(特别提这点的原因是国好多是半调子)。不然就比较容易出现性能问题,但是MyBatis的话,对于国内开发者来说,因为直接写SQL,所以还是相对还是更容易理解和把控一些。

好了,借今天核酸码的现象,跟大家聊聊这两个框架的想法,不知道你是否认同?欢迎留言区说说你的观点。

来源:https://mp.weixin.qq.com/s/43bE8juIRQbQLO3vBTUKWA

收起阅读 »

你会写注释吗?

前言有一本书叫《代码整洁之道》,不知你看过没?初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗...
继续阅读 »

前言

有一本书叫《代码整洁之道》,不知你看过没?

初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。

我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗被代码撞乱的心,于是咬咬牙读了下去。

这本书里面讲了很多代码整洁之道,关于有意义的命名、函数、注释、格式、错误处理、边界等共十七大篇章。如果你感兴趣,可以去看看。我只是粗略地看了一下,因为有些我也看不大明白。特别是当某些代码脱离了计算机而存在的时候,我好像不认识它们了,它们变得异常陌生。恕我我孤陋寡闻了,哎。

尽管如此,此书第四章中,关于“注释”的代码整洁之道,却给我留下了异常深刻的印象。Why? 因为里面关于注释的观点刷新了我的认知,与我的思想产生了一点点灵魂的碰撞,并且说服了我,还驱动我写下了这篇文章。

一、被注释吸引

下面是“注释”篇章的开头两段,特意贴了上来,因为我就是被这样的开头吸引了。希望它能带给你一点点启发。


不知你读完以上两段,作何感想?

我的感想是:如果你的代码写得足够优秀,是不需要过多注释的。注释最多可以证明糟糕的代码。

额,此刻我很想找一个捂脸的表情。与此同时,我在脑海里迅速地回忆了一遍注释之于我的心历路程:从最初知道“注释”这么个神奇玩意儿时的欣喜,到步步沦陷“注释”的魔爪,以致如今看着满屏的代码,不写点儿注释都感觉空落落的......

收回来,继续品。 作者开篇的观点约莫如下:

  • 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败

  • 如果你发现自己需要写注释,再想想是否有办法翻盘,用代码来表达

  • 注释会撒谎,代码在变动演化,但注释不能总是跟着走

  • 只有代码是唯一准确的信息来源

注意,作者用来了“失败”一词。你无法找到表达自我的恰当方法,所以就要用注释,这并不值得庆祝。当然,这并不意味作者就完全否定了注释的价值,程序员应当负责将代码保持在可维护、有关联、精确的高度。只不过作者更倾向于把力气用在写清楚代码上,直接保证无须编写注释,或者花心思减少注释量。

二、好的注释

有些注释是必须的,作者列举了一些值得写的注释。

  • 公司代码规范要求编写与法律有关的注释

  • 提供基本信息的注释

  • 对意图的解释

  • 阐释:把晦涩难明的参数或返回值的意义翻译为某种可读形式

  • 警示:用于警告其他程序员会出现某种后果的注释

  • TODO 注释:是一种程序员认为应该做,但由于某些原因目前还没做的工作

  • 放大:放大某种看起来不合理之物的重要性的注释

  • 公共 API 中的 Javadoc

尽管如此,作者一再强调:唯一真正好的注释是你想办法不去写的注释。足见作者对注释之深恶痛疾,对糟糕代码之嫌弃,对代码整洁要求之高。你可以细品。

三、坏的注释

果然是有代码洁癖的人,作者用了更多的篇幅来描述坏的注释。

  • 喃喃自语:因为过程需要就添加注释,就是无谓之举

  • 多余的注释:并不比代码本身提供更多的信息,甚至比读代码所花时间长

  • 误导性注释:写出不够精确的注释误导读者

  • 循规式注释:每个函数都要有 Javadoc 或每个变量都要有注释的规则愚不可及

  • 日志式注释:每次编辑代码时,在模块开始处添加一条注释,应当全部删除

  • 废话注释:喋喋不休,废话连篇的注释,一旦代码修改,将变成一堆谎言

  • 能用函数或变量时就别用注释:建议重构代码,删掉注释

  • 位置标记:在源代码中标记某个特别位置,多数实属无理又鸡零狗碎

  • 括号后面的注释:如果你发现自己想标记右括号,其实应该做的是缩短函数

  • 归属与署名:源代码控制系统是这类信息最好的归属地

  • 注释掉的代码:注释掉的代码堆积在一起,就像酒瓶底的渣滓一般

  • HTML 注释:源代码注释中的 HTML 标记是一种厌物

  • 非本地信息:假如你一定要写注释,请确保它描述了离它最近的代码

  • 信息过多:别在注释中添加有趣的历史性话题或无关的细节描述

  • 不明显的关系:注释及其描述的代码之间的联系应该显而易见

  • 函数头:短函数不需要太多的描述,选个好的函数名胜于写函数头注释

一言以蔽之:用整理代码的决心替代创造废话的冲动吧。 你会发现自己成为更优秀、更快乐的程序员。

小结

作者把“注释”拎出来,说了这么多,最终还是回归到了代码本身。

那如何才能写出整洁的代码呢?如果你不明白整洁对代码的意义,尝试去写整洁代码就毫无意义。如果你明白糟糕的代码给你带来的代价,你就会明白,花时间保持代码整洁不但有关效率,还有关生存。争取让营地比你来时更干净吧!

最后,贴上书中震撼我的一隅,希望它能指引你逐渐走向代码整洁之道,与君共勉!



作者:linwanxia
来源:https://juejin.cn/post/7083029096615116837

收起阅读 »

作为一名前端,该如何理解Nginx?

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~Nginx是什么?Ng...
继续阅读 »

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~


Nginx是什么?

Nginx (engine x) 是一个轻量级、高性能的HTTP反向代理服务器,同时也是一个通用代理服务器(TCP/UDP/IMAP/POP3/SMTP),最初由俄罗斯人Igor Sysoev编写。

简单的说:

  • Nginx是一个拥有高性能HTTP和反向代理服务器,其特点是占用内存少并发能力强,并且在现实中,nginx的并发能力要比在同类型的网页服务器中表现要好

  • Nginx专为性能优化而开发,最重要的要求便是性能,且十分注重效率,有报告nginx能支持高达50000个并发连接数

正向代理和反向代理

Nginx 是一个反向代理服务器,那么反向代理是什么呢?我们先看看什么叫做正向代理

正向代理:局域网中的电脑用户想要直接访问网络是不可行的,只能通过代理服务器(Server)来访问,这种代理服务就被称为正向代理。

就好比我们俩在一块,直接对话即可,但如果我和你分隔两地,我们要想对话,必须借助一个通讯设备(如:电话)来沟通,那么这个通讯设备就是"代理服务器",这种行为称为“正向代理”

那么反向代理是什么呢?

反向代理:客户端无法感知代理,因为客户端访问网络不需要配置,只要把请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据,然后再返回到客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

在正向代理中,我向你打电话,你能看到向你打电话的电话号码,由电话号码知道是我给你打的,那么此时我用虚拟电话给你打过去,你看到的不再是我的手机号,而是虚拟号码,你便不知道是我给你打的,这种行为变叫做"反向代理"。

在以上述的例子简单的说下:

  • 正向代理:我通过我的手机(proxy Server)去给你打电话,相当于我和我的手机是一个整体,与你的手机(Server)是分开的

  • 反向代理:我通过我的手机(proxy Server)通过软件转化为虚拟号码去给你打电话,此时相当于我的手机和你的手机是一个整体,和我是分开的

负载均衡

负载均衡:是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

如果没有负载均衡,客户端与服务端的操作通常是:客户端请求服务端,然后服务端去数据库查询数据,将返回的数据带给客户端


但随着客户端越来越多,数据,访问量飞速增长,这种情况显然无法满足,我们从上图发现,客户端的请求和相应都是通过服务端的,那么我们加大服务端的量,让多个服务端分担,是不是就能解决这个问题了呢?

但此时对于客户端而言,他去访问这个地址就是固定的,才不会去管那个服务端有时间,你只要给我返回出数据就OK了,所以我们就需要一个“管理者“,将这些服务端找个老大过来,客户端直接找老大,再由老大分配谁处理谁的数据,从而减轻服务端的压力,而这个”老大“就是反向代理服务器,而端口号就是这些服务端的工号。


向这样,当有15个请求时,反向代理服务器会平均分配给服务端,也就是各处理5个,这个过程就称之为:负载均衡

动静分离

当客户端发起请求时,正常的情况是这样的:


就好比你去找客服,一般先是先说一大堆官方的话,你问什么,他都会这么说,那么这个就叫静态资源(可以理解为是html,css)

而回答具体的问题时,每个回答都是不同的,而这些不同的就叫做动态资源(会改变,可以理解为是变量)

在未分离的时候,可以理解为每个客服都要先说出官方的话,在打出具体的回答,这无异加大了客服的工作量,所以为了更好的有效利用客服的时间,我们把这些官方的话分离出来,找个机器人,让他代替客服去说,这样就减轻了客服的工作量。

也就是说,我们将动态资源和静态资源分离出来,交给不同的服务器去解析,这样就加快了解析的速度,从而降低由单个服务器的压力


安装 Nginx

关于 nginx 如何安装,这里就不做过多的介绍了,感兴趣的小伙伴看看这篇文章:【Linux】中如何安装nginx

这里让我们看看一些常用的命令:

  • 查看版本:./nginx -v

  • 启动:./nginx

  • 关闭:./nginx -s stop(推荐) 或 ./nginx -s quit

  • 重新加载nginx配置:./nginx -s reload

Nginx 的配置文件

配置文件分为三个模块:

  • 全局块:从配置文件开始到events块之间,主要是设置一些影响nginx服务器整体运行的配置指令。(按道理说:并发处理服务的配置时,值越大,可支持的并发处理量越多,但此时会受到硬件、软件等设备等的制约)

  • events块:影响nginx服务器与用户的网络连接,常用的设置包括是否开启对多workprocess下的网络连接进行序列化,是否允许同时接收多个网络连接等等

  • http块:如反向代理和负载均衡都在此配置

location 的匹配规则

共有四种方式:

    location[ = | ~ | ~* | ^~ ] url {
   
  }
复制代码
  • =精确匹配,用于不含正则表达式的url前,要求字符串与url严格匹配,完全相等时,才能停止向下搜索并处理请求

  • ^~:用于不含正则表达式的url前,要求ngin服务器找到表示url和字符串匹配度最高的location后,立即使用此location处理请求,而不再匹配

  • ~最佳匹配,用于表示url包含正则表达式,并且区分大小写。

  • ~*:与~一样,只是不区分大小写

注意:

  • 如果 url 包含正则表达式,则不需要~ 作为开头表示

  • nginx的匹配具有优先顺序,一旦匹配上就会立马退出,不再进行向下匹配

End

关于具体的配置可以参考:写给前端的nginx教程

致此,有关Nginx相关的知识就已经完成了,相信对于前段而言已经足够了,喜欢的点个赞👍🏻支持下吧(● ̄(エ) ̄●)


作者:小杜杜
来源:https://juejin.cn/post/7082655545491980301

收起阅读 »

高并发之伪共享和缓存行填充(缓存行对齐)(@Contended)

1.使用缓存行(Cache Line)填充前后对比伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果...
继续阅读 »

1.使用缓存行(Cache Line)填充前后对比

伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果,可以看到耗时是3579毫秒:

而在其变量x的前后加上7个long类型到变量(在变量x前56Byte,后面也是56Byte,这就是缓存行填充,下面章节会详细介绍),当然这个14个变量是不会在代码中被用到的,但是为什么速度会提升将近2倍呢,如下图所示,可以看到耗时为1280毫秒:


ps:上面两个截图中的完整代码见
章节5,大家也可以直接跳转到章节去看下完整的代码。

为什么会这么神奇,这里为先提前说下结论,具体的大家可以往后看。

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,缓存行的内容一发生变化,就需要进行缓存同步,这个同步是需要时间的。

2.内存、缓存与寄存器之间如何传输数据

为什么会这样呢?前面我们提到过缓存一致性的问题,见笔者该篇博文:“了解高并发底层原理”,面试官:讲一下MESI(缓存一致性协议)吧,点击文字即可跳转。 其中内存、缓存与寄存器之间的关系图大致如下:


硬盘中的可执行文件加载到寄存器中进行运算的过程如下:

  1. 硬盘中的可执行文件(底层存储还是二进制的)加载到内存中,操作系统为其分配资源,变成了一个进程A,此时还没有跑起来;

  2. 过了一段时间之后,CPU0的时间片分配给了进程A,此时CPU0进行线程的装载,然后把需要用到的数据先从内存中读取到缓存中,读取的单元为一个缓存行,其大小现在通常为64字节(记住这个缓存行大小为64字节,这个非常重要,在后面会多次用到这个数值)。

  3. 然后数据再从缓存中读取到寄存器中,目前缓存一般为三级缓存,这里不具体画出。

  4. 寄存器得到了数据之后送去ALU(arithmetic and logic unit)做计算。

这里说一下为什么要设计三级缓存:

  • 电脑通过使用时钟来同步指令的执行。时钟脉冲在一个固定的频率(称为时钟频率)。当你买了一台1.5GHz的电脑,1.5GHz就是时钟频率,即每秒15亿次的时钟脉冲,一次完整的时钟脉冲称为一个周期(cycle),时钟并不记录分和秒。它以不变的速率简单跳动。

  • 其主要原因还是因为CPU方法内存消耗的时间太长了,CPU从各级缓存和内存中读取数据所需时间如下:

CPU访问大约需要的周期(cycle)大约需要的时间
寄存器1 cycle0ns
L1 Cache3—4 cycle1ns
L2 Cache10—20 cycle3ns
L3 Cache40—45 cycle15ns
内存60—90ns

3.缓存中数据共享问题(真实共享和伪共享)

3.1 真实共享(不同CPU的寄存器中都到了同一个变量X)

首先我们先说数据的真实共享,如下图,我们在CPU0和CPU1中都用到了数据X,现在不考虑数据Y。


如果不考虑缓存一致性,会出现如下问题: 在多线程情况下,此时由两个cpu同时开始读取了long X =0,然后同时执行如下语句,会出现如下情况:

int X = 0;
X++;

刚开始,X初始化为0,假设有两个线程A,B,

  1. A线程在CPU0上进行执行,从主存加载X变量的数值到缓存,然后从缓存中加载到寄存器中,在寄存器中执行X+1操作,得到X的值为1,此时得到X等于1的值还存放在CPU0的缓存中;

  2. 由于线程A计算X等于1的值还存放在缓存中,还没有刷新会内存,此时线程B执行在CPU1上,从内存中加载i的值,此时X的值还是0,然后进行X+1操作,得到X的值为1,存到CPU1的缓存中,

  3. A,B线程得到的值都是1,在一定的时间周期之后刷新回内存

  4. 写回内存后,两次X++操作之后,其值还是1;

可以看到虽然我们做了两次++X操作,但是只进行了一次加1操作,这就是缓存不一致带来的后果。

如何解决该问题:

  • 具体的我们可以通过MESI协议(详情见笔者该篇博文:blog.csdn.net/MrYushiwen/…)来保证缓存的一致性,如上图最中间的红字所示,在不同寄存器的缓存中,需要考虑数据的一致性问题,这个需要花费一定的时间来同步数据,从而达到缓存一致性的作用。

3.2伪共享(不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,并且XY在同一个缓存行中)

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 在3.1中,我们在寄存器用到的数据是同一个X,他们肯定是在同一个缓存行中的,这个是真实的共享数据的,共享的数据为X。

  • 而在3.2中,不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,但是变量X、Y在同一个缓存行中(一次读取64Byte,见3.1中的图),缓存一致性是根据缓存行为单元来进行同步的,所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,他们的缓存同步也需要时间。


4.伪共享解决办法(缓存行填充或者使用@Contended注解)

4.1.缓存行填充

如章节一所示,我们可以在x变量前后进行缓存行的填充,:

public volatile long A,B,C,D,E,F,G;
public volatile long x = 1L;
public volatile long a,b,c,d,e,f,g;

添加后,3.2章节中的截图将会变成如下样子:


不论如何进行缓存行的划分,包括x在内的连续64Byte,也就是一个缓存行不可能存在变量Y,同样变量Y所在的缓存行不可能存在x,这样就不存在伪共享的情况,他们之间就不需要考虑缓存一致性问题了,也就节省了这一部分时间。

4.2.Contended注解

在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。我们目前的缓存行大小一般为64Byte,这里Contended注解为我们前后加上了128字节绰绰有余。 注意:如果想要@Contended注解起作用,需要在启动时添加JVM参数-XX:-RestrictContended 参数后 @sun.misc.Contended 注解才有。

然而在java11中@Contended注解被归类到模块java.base中的包jdk.internal.vm.annotation中,其中定义了Contended注解类型。笔者用的是java12,其注解如下:


加上该注解,如下,也能达到缓存行填充的效果


5.完整代码(利用缓存行填充和没用缓存行填充)

大家自己也可以跑一下如下代码,看利用缓存行填充后的神奇效果。

5.1没用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 2:52 PM
* @Version: 1.0
*/

public class NoCacheLineFill {

   public volatile long x = 1L;
}

class MainDemo {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       NoCacheLineFill[] arr = new NoCacheLineFill[2];
       arr[0] = new NoCacheLineFill();
       arr[1] = new NoCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 100_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

5.2利用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 3:45 PM
* @Version: 1.0
*/

public class UseCacheLineFill {

   public volatile long A, B, C, D, E, F, G;
   public volatile long x = 1L;
   public volatile long a, b, c, d, e, f, g;
}

class MainDemo01 {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       UseCacheLineFill[] arr = new UseCacheLineFill[2];
       arr[0] = new UseCacheLineFill();
       arr[1] = new UseCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

作者:YuShiwen
来源:https://juejin.cn/post/7083030159304949767

收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间...... 都什么年代了你还在用 Date

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

纯后端如何写前端?我用了低代码平台

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。1、我的前端技术老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一...
继续阅读 »

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手

花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。


1、我的前端技术

老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一无所知,甚至报考了计算机专业之后也未曾了解过它是做什么的。

在大一的第一个学期,我印象中只开了一门C++的编程课(其他的全是数学)。嗯,理所当然,我是听不懂的,也不知道用来干什么。


刚进大学的时候,我对一切充满了未知,在那时候顺其自然地就想要进几个社团玩玩。但在众多社团里都找不到我擅长的领域,等快到截止时间了。我又不想大学期间什么社团都没有参加,最后报了两个:乒乓球社团和计算机协会

这个计算机协会绝大多数的人员都来自于计算机专业,再后来才发现这个协会的主要工作就是给人「重装系统」,不过这是后话啦。

当时加入计算机协会还需要满足一定的条件:师兄给了一个「网站」我们这群人,让我们上去学习,等到国庆回来后看下我们的学习进度再来决定是否有资格加入。

那个网站其实就是对HTML/CSS/JavaScript入门教程,是一个国外的网站,具体的地址我肯定是忘了。不过那时候,我国庆闲着也没事干,于是就开始学起来了。我当时的进度应该是学到CSS,能简单的页面布局和展示图片啥的

刚开始的时候,觉得蛮有趣的:我改下这个代码,字体的颜色就变了,图片就能展示出来了。原来我平时上网的网站是这样弄出来的啊! (比什么C++有趣多了)

国庆后回来发现:考核啥的并不重要,只要报名了就都通过了。


有了基本的认知后,我对这个也并不太上心,没有持续地学下去。再后来,我实在是太无聊,就开始想以后毕业找工作的事了,自己也得在大学充实下自己,于是我开始在知乎搜各种答案「如何入门编程」。

在知乎搜了各种路线并浪费了大量时间以后,我终于开始看视频入门。我熬完了JavaSE基础之后,我记得我是看方立勋老师入门的JavaWeb,到前端的课程以后,我觉得前端HTML/CSS/JavaScript啥的都要补补,于是又去找资源学习(那时候信奉着技多不压身)。

印象中是看韩顺平老师的HTML/CSS/JavaScript,那时候还手打代码的阶段,把我看得一愣一愣的(IDE都不需要的)。随着学习,发现好像还得学AJAX/jQuery,于是我又去找资源了,不过我已经忘了看哪个老师的AJAXjQuery课程。

在这个学习的过程中,我曾经用纯HTML/CSS/JavaScript跟着视频仿照过某某网站,在jQuery的学习时候做过各种的轮播图动画。还理解了marginpadding的区别。临近毕业的时候,也会点BootStrap来写个简单的页面(丑就完事了)


等我进公司了以后,技术架构前后端是分离的,虽然我拉了前端的代码,但我看不懂,期间我也没学。以至于我两年多是没碰过前端的,我对前端充满着敬畏(刚毕业那段时间,前端在飞速发展

2、AUSTIN前端选型

从我筹划要写austin项目的时候,我就知道我肯定要写一个「后台管理页面」,但我迟迟没下手。一方面是我认为「后端」才是我的赛道,另一方面我「前端」确实菜,不想动手。

我有想过要不找个小伙伴帮我写,但是很快就被我自己否定了:还得给小伙伴提需求,算了


当我要面临前端的时,我第一时间就想到:肯定是有什么框架能够快速搭建出一个管理页面的。我自己不知道,但是,我的朋友圈肯定是有人知道的啊。于是,我果断求助:


我被安利了很多框架,简单列举下出场率比较高的。

:大多数我只是粗略看了下,没有仔细研究。若有错误可以在评论区留言,轻喷

2.1 renren-fast

官网文档:http://www.renren.io/guide#getdo…


它这个框架是前后端分离的,后端还可以生成对应的CRUD代码,前端基于vueelement-ui开发。

当时其实我有点想选它的,但考虑到我要再部署个后端,还得学点vue,我就搁置了

2.2 RuoYi

官方文档:doc.ruoyi.vip/ruoyi/


RuoYi给我安利的也很多,这个貌似最近非常火?感觉我被推荐了以后,到处都能看到它的身影。

我简单刷了下文档,感觉他做的事比renren-fast要多,文档也很齐全,但是没找到我想要的东西:我打开一个文档,我希望能看到它的系统架构,系统之间的交互或者架构层面上的东西,但我没快速找到。

项目齐全和复杂对我来说或许并不是一件好事,很可能意味着我的学习成本可能会更大。于是,我也搁置着。

2.3 Vue相关

vue-element-admin

官方文档:panjiachen.github.io/vue-element…


Vue Antd Admin

官方文档:iczer.gitee.io/vue-antd-ad…


Ant Design Pro

官方文档:pro.antdv.com/docs/gettin…


这几个项目被推荐率也是极高的,从第一行介绍我基本就知道需要去学Vue的语法,奈何我太懒了,搁置着。

2.4 layui

有好几小伙伴们听说我会jQuery,于是给我推荐了layui。我以前印象中好像听过这个框架,但一直没了解过他。但是,当我搜到它的时候,它已经不维护了


GitHub地址:github.com/sentsin/lay…

我简单浏览下文档,其实它也有对应的一套”语法“,需要一定的学习成本,但不高。


第一感觉有点类似我以前写过的BootStrap,我对这不太感冒,感觉如果要接入可能还是需要自己写比较多的代码。

2.5 其他

还有些小伙伴推荐或者我看到的文章推荐:x-admin/D2admin/smartchart/JEECG-BOOT/Dcat-admin/iview-admin等等等,在这里面还有些依赖着PHP/Python

总的来说,我还是觉得这些框架有一定的学习成本(我真的是懒出天际了)。可能需要我去部署后端,也可能需要我学习前端的框架语法,也可能让我学Vue

看到这里,可能你们很好奇我最后选了什么作为austin的前端,都已经被我筛了这么多了。在公布之前,我想说的是:如果想要页面好看灵活性高还是得学习Vue。从上面我被推荐的框架中,好多都是在Vue的基础上改动的,并且我敢肯定:还有很多基于Vue且好用的后台是我不知道的。

:我这里指代跟我一样不懂前端的(如果本身就已经懂前端,你说啥都对)


3、AMIS框架

我最后选择了amis作为austin的前端。这个框架在我朋友圈只有一个小伙伴推荐,我第一次打开文档的时候,确实惊艳到我了


文档地址:baidu.gitee.io/amis/zh-CN/…

它是一个低代码前端框架:amis 的渲染过程是将 json 转成对应的 React 组件

我花了半天粗略地刷了下文档,大概知道了JSON的结构(说实话,他这个文档写得挺可以的),然后我去GitHub找了一份模板,就直接开始动手了,readme十分简短。


GitHub:github.com/aisuda/amis…

这个前端低代码工具还有个好处就是可以通过可视化编辑器拖拉生成JSON代码,将生成好的代码直接往自己本地一贴,就完事了,确实挺方便的。


可视化编辑器的地址:aisuda.github.io/amis-editor…

4、使用感受

其实没什么好讲的,无非就是在页面上拖拉得到一个页面,然后调用API的时候看下文档的姿势。

在这个过程中我也去看了下这个框架的评价,发现百度内部很多系统就用的这个框架来搭建页面的,也看到Bigo也有在线上使用这个框架来搭建后台。有一线/二线公司都在线上使用该框架了,我就认为问题不大了。

总的来说,我这次搭建austin后台实际编码时间没多少,都在改JSON配置和查文档。我周六下午2点到的图书馆,新建了GitHub仓库,在6点闭馆前就已经搭出个大概页面了,然后在周日空闲时间里再完善了几下,感觉可以用了

austin-amis仓库地址:github.com/ZhongFuChen…

在搭建的过程中,amis低代码框架还是有地方可吐槽的,就是它的灵活性太低。我们的接口返回值需要迎合它的主体结构,当我们如果有嵌套JSON这种就变得异常难处理,表单无法用表达式进行回显等等。

它并不完美,很可能需要我用些奇怪的姿势妥协,不要问我接口返回的时候为啥转了一层Map


不管怎么说,这不妨碍我花了极短的时间就能搭出一个能看的后台管理页面(CRUD已齐全)


5、总结

目前搭好的前端能用,也只能用一点点,后面会逐渐完善它的配置和功能的。我后面有链路追踪的功能,肯定要在后台这把清洗后的数据提供给后台进行查询,但也不会花比较长的篇幅再来聊前端这事了。

我一直定位是在后端的代码上,至于前端我能学,但我又不想学。怎么说呢,利益最大化吧。我把学前端的时间花在学后端上,或许可能对我有更大的受益。现在基本前后端分离了,在公司我也没什么机会写前端。

下一篇很有可能是聊分布式定时任务框架上,我发现我的进度可以的,这个季度拿个4.0应该问题不大了。

都看到这里了,点个赞一点都不过分吧?我是3y,下期见。


austin项目源码Gitee链接:gitee.com/austin

austin项目源码GitHub链接:github.com/austin


作者:Java3y
来源:https://juejin.cn/post/7076231399669235725

收起阅读 »

基于JDK的动态代理原理分析

基于JDK的动态代理原理分析 这篇文章解决三个问题: What 动态代理是什么 How 动态代理怎么用 Why 动态代理的原理 动态代理是什么? 动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件...
继续阅读 »

基于JDK的动态代理原理分析


这篇文章解决三个问题:



  1. What 动态代理是什么

  2. How 动态代理怎么用

  3. Why 动态代理的原理


动态代理是什么?


动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件),实现对目标对象所有方法的增强。通过这种方式,我们可以在不改变(或无法改变)目标对象源码的情况下,对目标对象的方法执行前后进行干预。


动态代理怎么用?


首先,准备好我们需要代理的类和接口,因为JDK的动态代理是基于接口实现的,所以被代理的对象必须要有接口


/**
* SaySomething接口
*/

public interface SaySomething {

   public void sayHello();

   public void sayBye();
}

/**
* SaySomething的实现类
*/

public class SaySomethingImpl implements SaySomething {
   @Override
   public void sayHello() {
       System.out.println("Hello World");
  }

   @Override
   public void sayBye() {
       System.out.println("Bye Bye");
  }
}

按照动态代理的用法,需要自定义一个处理器,用来编写自定义逻辑,实现对被代理对象的增强。


自定义的处理器需要满足以下要求:



  • 需要实现InvocationHandler,重写invoke方法,在invoke方法中通过加入自定义逻辑,实现对目标对象的增强。

  • 需要持有一个成员变量,成员变量的是被代理对象的实例,通过构造参数传入。(用来支持反射调用被代理对象的方法)

  • 需要提供一个参数为被代理对象接口类的有参构造。(用来支持反射调用被代理对象的方法)


/**
* 自定义的处理器,用来编写自定义逻辑,实现对被代理对象的增强
*/

public class CustomHandler implements InvocationHandler {

   //需要有一个成员变量,成员变量为被代理对象,通过构造参数传入,用来支持方法的反射调用。
   private SaySomething obj;
   
   //需要有一个有参构造,通过构造函数将被代理对象的实例传入,用来支持方法的反射调用
   public CustomHandler(SaySomething obj) {
       this.obj = obj;
  }

   /**
    * proxy:动态生成的代理类对象com.sun.proxy.$Proxy0
    * method:被代理对象的真实的方法的Method对象
    * args:调用方法时的入参
    */

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       //目标方法执行前的自定义逻辑处理
       System.out.println("-----before------");

       //执行目标对象的方法,使用反射来执行方法,反射需要传入目标对象,此时用到了成员变量obj。
       Object result = method.invoke(obj, args);

       //目标方法执行后的自定义逻辑处理
       System.out.println("-----after------");
       return result;
  }
}

这样我们就完成了自定义处理器的编写,同时在invoke方法中实现对了代理对象方法的增强,被代理类的所有方法的执行都会执行我们自定义的逻辑。


接下来,需要通过Proxy,newProxyInstance()方法来生成代理对象的实例,并进行方法调用测试。


public class JdkProxyTest {
   public static void main(String[] args) {
       //将生成的代理对象的字节码文件 保存到硬盘
       System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

       //被代理对象的实例
       SaySomething obj = new SaySomethingImpl();
       //通过构造函数,传入被代理对象的实例,生成处理器的实例
       InvocationHandler handler = new CustomHandler(obj);
       //通过Proxy.newProxyInstance方法,传入被代理对象Class对象、处理器实例,生成代理对象实例
       SaySomething proxyInstance = (SaySomething) Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                                                                          new Class[]{SaySomething.class}, handler);
       //调用生成的代理对象的sayHello方法
       proxyInstance.sayHello();
       System.out.println("===================分割线==================");
       //调用生成的代理对象的sayBye方法
       proxyInstance.sayBye();
  }
}

image.png
运行main方法,查看控制台,大功告成。至此,我们已经完整的完成了一次动态代理的使用。


动态代理的原理


生成的proxyInstance对象到底是什么,为什么调用它的sayHello方法会执行CustomerHandler的invoke方法呢?


直接贴上proxyInstance的字节码文件,我们就会恍然大悟了...


//$Proxy0是SaySomething的实现类,重写了sayHello和sayBye方法
public final class $Proxy0 extends Proxy implements SaySomething {
   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m4;
   private static Method m0;

   public $Proxy0(InvocationHandler var1) throws {
       super(var1);
  }

   static {
       try {
           m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
           m3 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayHello");
           m2 = Class.forName("java.lang.Object").getMethod("toString");
           m4 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayBye");
           m0 = Class.forName("java.lang.Object").getMethod("hashCode");
      } catch (NoSuchMethodException var2) {
           throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
           throw new NoClassDefFoundError(var3.getMessage());
      }
  }
 
   //实现了接口的sayHello方法,在方法内部调用了CustomerHandler的invoke方法,同时传入了Method对象,
   //所以在CustomerHandler对象中可以通过mathod.invovke方法调用SyaSomthing的sayHello方法
   public final void sayHello() throws {
       try {
           //h是父类Proxy中的InvocationHandler对象,其实就是我们自定义的CustomHandler对象
           super.h.invoke(this, m3, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }

   public final void sayBye() throws {
       try {
           super.h.invoke(this, m4, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }
   public final int hashCode() throws {
      //忽略内容
  }
   public final boolean equals(Object var1) throws {
      //忽略内容
  }
   public final String toString() throws {
      //忽略内容
  }
}

看到了生成的代理对象的字节码文件,是不是一切都明白你了,原理竟然如此简单^_^


作者:82年咖啡
来源:https://juejin.cn/post/7079720742899843080
收起阅读 »

画一手好的架构图是码农进阶的开始

1.前言 你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方...
继续阅读 »

1.前言

你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方法论,让技术图纸更加清晰。

2. 架构的定义

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

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

  • 架构是结构和愿景.

在TOGAF企业架构理论中, 架构是从公司战略层面,自顶向下的细化的一部分,从战略=> 业务架构=>应用/数据/技术架构,当然老板层关注的是战略与业务架构,我们搬砖的需要聚焦到应用/数据/技术架构这一层。


  • 业务架构: 由业务架构师负责,也可以称为业务领域专家、行业专家,业务架构属于顶层设计,其对业务的定义和划分会影响组织架构和技术架构;

  • 应用架构: 由应用架构师负责,需要根据业务场景需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性的要求如性能、安全、稳定性等。

  • 技术架构: 描述了需要哪些服务;选择哪些技术组件来实现技术服务;技术服务以及组件之间的交互关系;

  • 数据架构: 描述了数据模型、分布、数据的流向、数据的生命周期、数据的管理等关系;

3.架构图的分类

系统架构图是为了抽象的表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。好的架构图可以让干系人理解、遵循架构决策,就需要把架构信息传递出去。那么,画架构图是为了:解决沟通障碍/达成共识/减少歧义。比较流行的是4+1视图和C4视图。

3.1 4+1视图

3.1.1 场景视图

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


3.1.2 逻辑视图

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


3.1.3 物理视图

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


3.1.4 处理流程视图

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


3.1.5 开发视图

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


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

3.2 C4视图

下面的案例来自C4官网,然后加上了一些笔者的理解。


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

3.2.1 语境图(System Context Diagram)

用于描述要我们要构建的系统是什么,用户是谁,需要如何融入已有的IT环境。这个图的受众可以是开发团队的内部人员、外部的技术或非技术人员。


3.2.2 容器图(Container Diagram)

容器图是把语境图里待建设的系统做了一个展开描述,主要受众是团队内部或外部的开发人员或运维人员,主要用来描述软件系统的整体形态,体现了高层次的技术决策与选型,系统中的职责是如何分布的,容器间是如何交互的。


3.2.3 组件图(Component Diagram)

组件图是把某个容器进行展开,描述其内部的模块,主要是给内部开发人员看的,怎么去做代码的组织和构建,描述了系统由哪些组件/服务组成,了组件之间的关系和依赖,为软件开发如何分解交付提供了框架。


4.怎么画好架构图

上面的分类是前人的经验总结,图也是从网上摘来的,那么这些图画的好不好呢?是不是我们要依葫芦画瓢去画这样一些图?先不去管这些图好不好,我们通过对这些图的分类以及作用,思考了一下,总结下来,我们认为,明确这两点之后,从受众角度来说,一个好的架构图是不需要解释的,它应该是自描述的,并且要具备一致性和足够的准确性,能够与代码相呼应。

4.1 视图的受众

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

4.2 视图的元素区分

可以看到架构视图是由方框和线条等元素构成,要利用形状、颜色、线条变化等区分元素的含义,避免混淆。架构是一项复杂的工作,只使用单个图表来表示架构很容易造成莫名其妙的语义混乱。

让我们一起画出好的架构图!

参考资料


作者:代码的色彩
来源:https://juejin.cn/post/7062662600437268493

收起阅读 »

前端人员不要只知道KFC,你应该了解 BFC、IFC、GFC 和 FFC

前言说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而...
继续阅读 »

前言

说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而且必须得做到像肯德基这样印象深刻。下面我将会带大家一起揭开这些FC的真面目,如果你已经了解的请奖励自己一顿肯德基~(注意文明用语,这里别用语气词😂)

FC的全称是:Formatting Contexts,译作格式化上下文,是W3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

CSS2.1中只有BFC和IFC,CSS3中才有GFC和FFC。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

前置概念

在学习各种FC之前,我们先来了解几个基本概念:

Box(CSS布局基本单位)

简单来讲,我们看到的所有页面都是由一个个Box组合而成的,元素的类型和display属性决定了Box的类型。

  • block-level Box: 当元素的 CSS 属性 displayblock, list-itemtable 时,它是块级元素 block-level。块级元素(比如<p>)视觉上呈现为块,竖直排列。 每个块级元素至少生成一个块级盒(block-level Box)参与 BFC ,称为主要块级盒(principal block-level box)。一些元素,比如<li>,生成额外的盒来放置项目符号,不过多数元素只生成一个主要块级盒。

  • Inline-level Box: 当元素的 CSS 属性 display 的计算值为inline,inline-blockinline-table 时,称它为行内级元素。视觉上它将内容与其它行内级元素排列为多行。典型的如段落内容,有文本或图片,都是行内级元素。行内级元素生成行内级盒(inline-level boxes),参与行内格式化上下文 IFC 。

  • flex container: 当元素的 CSS 属性 display 的计算值为 flexinline-flex ,称它为弹性容器display:flex这个值会导致一个元素生成一个块级(block-level)弹性容器框。display:inline-flex这个值会导致一个元素生成一个行内级(inline-level)弹性容器框。

  • grid container:*当元素的 CSS 属性 display 的计算值为 gridinline-grid,称它为*栅格容器

块容器盒(block container box)

只包含其它块级盒,或生成一个行内格式化上下文(inline formatting context),只包含行内盒的叫做块容器盒子

也就是说,块容器盒要么只包含行内级盒,要么只包含块级盒。

块级盒(block-level Box)是描述元素跟它的父元素与兄弟元素之间的表现。

块容器盒(block container box)描述元素跟它的后代之间的影响。

块盒(BLock Boxes)

同时是块容器盒的块级盒称为块盒(block boxes)

行盒(Line boxes)

行盒由行内格式化上下文(inline formatting context)产生的盒,用于表示一行。在块盒里面,行盒从块盒一边排版到另一边。 当有浮动时, 行盒从左浮动的最右边排版到右浮动的最左边。

OK,了解完上面这些概念,我们再来看我们本篇文章的重点内容(终于要揭开各种FC的庐山真面目了,期待~)

BFC(Block Formatting Contexts)块级格式化上下文

什么是BFC?

BFC 全称:Block Formatting Context, 名为 块级格式化上下文

W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。

如何触发BFC?

  • 根元素或其它包含它的元素

  • 浮动 float: left/right/inherit

  • 绝对定位元素 position: absolute/fixed

  • 行内块display: inline-block

  • 表格单元格 display: table-cell

  • 表格标题 display: table-caption

  • 溢出元素 overflow: hidden/scroll/auto/inherit

  • 弹性盒子 display: flex/inline-flex

BFC布局规则

  • 内部的Box会在垂直方向,一个接一个地放置。

  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。

  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。

  • BFC的区域不会与float box重叠。

  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

  • 计算BFC的高度时,浮动元素也参与计算

BFC应用场景

解决块级元素垂直方向margin重叠

我们来看下面这种情况:

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
</style>
<body>
   <div class="box">nanjiu</div>
   <div class="box">南玖</div>
</body>

按我们习惯性思维,上面这个box的margin-bottom60px,下面这个box的margin-top也是60px,那他们垂直的间距按道理来说应该是120px才对。(可事实并非如此,我们可以来具体看一下)

bfc1.png 从图中我们可以看到,两个box的垂直间距只有60px,并不是120px!

这种情况下的margin边距为两者的最大值,而不是两者相加,那么我们可以使用BFC来解决这种margin塌陷的问题。

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
 .outer_box{
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
   </div>
   <div class="box">南玖</div>
</body>

bfc2.png 由上面可以看到,我们通过给第一个box外面再包裹一层容器,并触发它形成BFC,此时的两个box就不属于同一个BFC了,它们的布局互不干扰,所以这时候他们的垂直间距就是两者间距相加了。

解决高度塌陷问题

我们再来看这种情况,内部box使用float脱离了普通文档流,导致外层容器没办法撑起高度,使得背景颜色没有显示出来。

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
 .outer_box{
   background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc3.png 从这张图,我们可以看到此时的外层容器的高度为0,导致背景颜色没有渲染出来,这种情况我们同样可以使用BFC来解决,可以直接为外层容器触发BFC,我们来看看效果:

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
.outer_box{
 display:inline-block;
 background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc4.png

清除浮动

在早期前端页面大多喜欢用浮动来布局,但浮动元素脱离普通文档流,会覆盖旁边内容:

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc5.png 我们可以通过触发后面这个元素形成BFC,从而来清楚浮动元素对其布局造成的影响

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc6.png

IFC(Inline Formatting Contexts)行内级格式化上下文

什么是IFC?

IFC全称:Inline Formatting Context,名为行级格式化上下文

如何触发IFC?

  • 块级元素中仅包含内联级别元素

形成条件非常简单,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个IFC。

IFC布局规则

  • 在一个IFC内,子元素是水平方向横向排列的,并且垂直方向起点为元素顶部。

  • 子元素只会计算横向样式空间,【padding、border、margin】,垂直方向样式空间不会被计算,【padding、border、margin】。

  • 在垂直方向上,子元素会以不同形式来对齐(vertical-align)

  • 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和与其中的浮动来决定。

  • IFC中的line box一般左右边贴紧其包含块,但float元素会优先排列。

  • IFC中的line box高度由 CSS 行高计算规则来确定,同个IFC下的多个line box高度可能会不同。

  • inline boxes的总宽度少于包含它们的line box时,其水平渲染规则由 text-align 属性值来决定。

  • 当一个inline box超过父元素的宽度时,它会被分割成多个boxes,这些boxes分布在多个line box中。如果子元素未设置强制换行的情况下,inline box将不可被分割,将会溢出父元素。

IFC应用场景

元素水平居中

当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。

<style>
/* IFC */
 .text_container{
   width: 650px;
   border: 3px solid salmon;
   margin-top:60px;
   text-align: center;
}
 strong,span{
   /* border:1px solid cornflowerblue; */
   margin: 20px;
   background-color: cornflowerblue;
   color:#fff;
}
</style>
<body>
   <div class="text_container">
       <strong>众里寻他千百度,南玖需要你关注</strong>
       <span>蓦然回首,那人却在,南玖前端交流群</span>
   </div>
</body>

ifc1.png

多行文本水平垂直居中

创建一个IFC,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。

<style>
.text_container{
 text-align: center;
 line-height: 300px;
 width: 100%;
 height: 300px;
 background-color: turquoise;
 font-size: 0;
}
 p{
   line-height: normal;
   display: inline-block;
   vertical-align: middle;
   background-color: coral;
   font-size: 18px;
   padding: 10px;
   width: 360px;
   color: #fff;
}
</style>
<body>
 <div class="text_container">
   <p>
    东风夜放花千树,更吹落,星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。蛾儿雪柳黄金缕,笑语盈盈暗香去。
     <strong>众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。</strong>
   </p>
 </div>
</body>

ifc2.png

GFC(Grid Formatting Contexts)栅格格式化上下文

什么是GFC?

GFC全称:Grids Formatting Contexts,名为网格格式上下文

简介: CSS3引入的一种新的布局模型——Grids网格布局,目前暂未推广使用,使用频率较低,简单了解即可。 Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置。但是,它们也存在重大区别。 Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

如何触发GFC?

当为一个元素设置display值为grid或者inline-grid的时候,此元素将会获得一个独立的渲染区域。

GFC布局规则

通过在网格容器(grid container)上定义网格定义行(grid definition rows)网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间(具体可以在MDN上查看)

GFC应用场景

任意魔方布局

这个布局使用用GFC可以轻松实现自由拼接效果,换成其他方法,一般会使用相对/绝对定位,或者flex来实现自由拼接效果,复杂程度将会提升好几个等级。

<style>
.magic{
display: grid;
grid-gap: 2px;
width:300px;
height:300px;
}
.magic div{
border: 1px solid coral;
}
.m_1{
grid-column-start: 1;
grid-column-end: 3;
}
.m_3{
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: 3;
}
</style>
<body>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
</div>
</body>

gfc1.png

FFC(Flex Formatting Contexts)弹性格式化上下文

什么是FFC?

FFC全称:Flex Formatting Contexts,名为弹性格式上下文

简介: CSS3引入了一种新的布局模型——flex布局。 flex是flexible box的缩写,一般称之为弹性盒模型。和CSS3其他属性不一样,flexbox并不是一个属性,而是一个模块,包括多个CSS3属性。flex布局提供一种更加有效的方式来进行容器内的项目布局,以适应各种类型的显示设备和各种尺寸的屏幕,使用Flex box布局实际上就是声明创建了FFC(自适应格式上下文)

如何触发FFC?

display 的值为 flexinline-flex 时,将生成弹性容器(Flex Containers), 一个弹性容器为其内容建立了一个新的弹性格式化上下文环境(FFC)

FFC布局规则

  • 设置为 flex 的容器被渲染为一个块级元素

  • 设置为 inline-flex 的容器被渲染为一个行内元素

  • 弹性容器中的每一个子元素都是一个弹性项目。弹性项目可以是任意数量的。弹性容器外和弹性项目内的一切元素都不受影响。简单地说,Flexbox 定义了弹性容器内弹性项目该如何布局

⚠️注意:FFC布局中,float、clear、vertical-align属性不会生效。

Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

FFC应用场景

这里只介绍它对于其它布局所相对来说更方便的特点,其实flex布局现在是非常普遍的,很多前端人员都喜欢用flex来写页面布局,操作方便且灵活,兼容性好。

自动撑开剩余高度/宽度

看一个经典两栏布局:左边为侧边导航栏,右边为内容区域,用我们之前的常规布局,可能就需要使用到csscalc方法来动态计算剩余填充宽度了,但如果使用flex布局的话,只需要一个属性就能解决这个问题:

calc动态计算方法:

<style>
.outer_box {
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 width:calc(100% - 180px);
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc.gif 使用FFC:

<style>
.outer_box {
 display:flex;
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 flex: 1;
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc2.gif

总结

一般来说,FFC能做的事情,通过GFC都能搞定,反过来GFC能做的事通过FFC也能实现。 通常弹性布局使用FFC,二维网格布局使用GFC,所有的FFC与GFC也是一个BFC,在遵循自己的规范的情况下,向下兼容BFC规范。

现在所有的FC都介绍完了,了解清楚的去奖励自己一顿KFC吧😄~

推荐阅读

作者:南玖
来源:https://juejin.cn/post/7072174649735381029

收起阅读 »

MySQL模糊查询再也用不着 like+% 了

前言 我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不...
继续阅读 »

前言


我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不是B+树索引能很好完成的工作。


通过数值比较,范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较,全文索引就是为这种场景设计的。


全文索引(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。


在早期的 MySQL 中,InnoDB 并不支持全文检索技术,从 MySQL 5.6 开始,InnoDB 开始支持全文检索。


倒排索引


全文检索通常使用倒排索引(inverted index)来实现,倒排索引同 B+Tree 一样,也是一种索引结构。它在辅助表中存储了单词与单词自身在一个或多个文档中所在位置之间的映射,这通常利用关联数组实现,拥有两种表现形式:



  • inverted file index:{单词,单词所在文档的id}

  • full inverted index:{单词,(单词所在文档的id,再具体文档中的位置)}


MarkerHub


上图为 inverted file index 关联数组,可以看到其中单词"code"存在于文档1,4中,这样存储再进行全文查询就简单了,可以直接根据 Documents 得到包含查询关键字的文档;而 full inverted index 存储的是对,即(DocumentId,Position),因此其存储的倒排索引如下图,如关键字"code"存在于文档1的第6个单词和文档4的第8个单词。



相比之下,full inverted index 占用了更多的空间,但是能更好的定位数据,并扩充一些其他搜索特性。



MarkerHub


全文检索


创建全文索引


1、创建表时创建全文索引语法如下:

CREATE TABLE table_name ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, author VARCHAR(200), 
title VARCHAR(200), content TEXT(500), FULLTEXT full_index_name (col_name) ) ENGINE=InnoDB;
复制代码

输入查询语句:


SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_TABLES
WHERE name LIKE 'test/%';
复制代码

MarkerHub


上述六个索引表构成倒排索引,称为辅助索引表。当传入的文档被标记化时,单个词与位置信息和关联的DOC_ID,根据单词的第一个字符的字符集排序权重,在六个索引表中对单词进行完全排序和分区。


2、在已创建的表上创建全文索引语法如下:

CREATE FULLTEXT INDEX full_index_name ON table_name(col_name);
复制代码

使用全文索引


MySQL 数据库支持全文检索的查询,全文索引只能在 InnoDB 或 MyISAM 的表上使用,并且只能用于创建 char,varchar,text 类型的列。


其语法如下:


MATCH(col1,col2,...) AGAINST(expr[search_modifier])
search_modifier:
{
    IN NATURAL LANGUAGE MODE
    | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
    | IN BOOLEAN MODE
    | WITH QUERY EXPANSION
}
复制代码

全文搜索使用 MATCH() AGAINST()语法进行,其中,MATCH()采用逗号分隔的列表,命名要搜索的列。AGAINST()接收一个要搜索的字符串,以及一个要执行的搜索类型的可选修饰符。全文检索分为三种类型:自然语言搜索、布尔搜索、查询扩展搜索,下面将对各种查询模式进行介绍。


Natural Language


自然语言搜索将搜索字符串解释为自然人类语言中的短语,MATCH()默认采用 Natural Language 模式,其表示查询带有指定关键字的文档。


接下来结合demo来更好的理解Natural Language


SELECT
    count(*) AS count 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL' );
复制代码

MarkerHub


上述语句,查询 title,body 列中包含 'MySQL' 关键字的行数量。上述语句还可以这样写:


SELECT
    count(IF(MATCH ( title, body ) 
    against ( 'MySQL' ), 1, NULL )) AS count 
FROM
    `fts_articles`;
复制代码

上述两种语句虽然得到的结果是一样的,但从内部运行来看,第二句SQL的执行速度更快些,因为第一句SQL(基于where索引查询的方式)还需要进行相关性的排序统计,而第二种方式是不需要的。


还可以通过SQL语句查询相关性:


SELECT
    *,
    MATCH ( title, body ) against ( 'MySQL' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


相关性的计算依据以下四个条件:



  • word 是否在文档中出现

  • word 在文档中出现的次数

  • word 在索引列中的数量

  • 多少个文档包含该 word


对于 InnoDB 存储引擎的全文检索,还需要考虑以下的因素:



  • 查询的 word 在 stopword 列中,忽略该字符串的查询

  • 查询的 word 的字符长度是否在区间 [ innodb_ft_min_token_size,innodb_ft_max_token_size] 内


如果词在 stopword 中,则不对该词进行查询,如对 'for' 这个词进行查询,结果如下所示:


SELECT
    *,
    MATCH ( title, body ) against ( 'for' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


可以看到,'for'虽然在文档 2,4中出现,但由于其是 stopword ,故其相关性为0


参数 innodb_ft_min_token_sizeinnodb_ft_max_token_size 控制 InnoDB 引擎查询字符的长度,当长度小于 innodb_ft_min_token_size 或者长度大于 innodb_ft_max_token_size 时,会忽略该词的搜索。在 InnoDB 引擎中,参数 innodb_ft_min_token_size 的默认值是3,innodb_ft_max_token_size的默认值是84


Boolean


布尔搜索使用特殊查询语言的规则来解释搜索字符串,该字符串包含要搜索的词,它还可以包含指定要求的运算符,例如匹配行中必须存在或不存在某个词,或者它的权重应高于或低于通常情况。


例如,下面的语句要求查询有字符串"Pease"但没有"hot"的文档,其中+和-分别表示单词必须存在,或者一定不存在。


select * from fts_test where MATCH(content) AGAINST('+Pease -hot' IN BOOLEAN MODE);
复制代码

Boolean 全文检索支持的类型包括:



  • +:表示该 word 必须存在

  • -:表示该 word 必须不存在

  • (no operator)表示该 word 是可选的,但是如果出现,其相关性会更高

  • @distance表示查询的多个单词之间的距离是否在 distance 之内,distance 的单位是字节,这种全文检索的查询也称为 Proximity Search,如 MATCH(context) AGAINST('"Pease hot"@30' IN BOOLEAN MODE)语句表示字符串 Pease 和 hot 之间的距离需在30字节内

  • >:表示出现该单词时增加相关性

  • <:表示出现该单词时降低相关性

  • ~:表示允许出现该单词,但出现时相关性为负

  • * :表示以该单词开头的单词,如 lik*,表示可以是 lik,like,likes

  • " :表示短语


下面是一些demo,看看 Boolean Mode 是如何使用的。


demo1:+ -


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL -YourSQL' IN BOOLEAN MODE );
复制代码

上述语句,查询的是包含 'MySQL' 但不包含 'YourSQL' 的信息


MarkerHub


demo2: no operator


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL IBM' IN BOOLEAN MODE );
复制代码

上述语句,查询的 'MySQL IBM' 没有 '+','-'的标识,代表 word 是可选的,如果出现,其相关性会更高


MarkerHub


demo3:@


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"DB2 IBM"@3' IN BOOLEAN MODE );
复制代码

上述语句,代表 "DB2" ,"IBM"两个词之间的距离在3字节之内


MarkerHub


demo4:> <


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL +(>database <DBMS)' IN BOOLEAN MODE );
复制代码

上述语句,查询同时包含 'MySQL','database','DBMS' 的行信息,但不包含'DBMS'的行的相关性高于包含'DBMS'的行。


MarkerHub


demo5: ~


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL ~database' IN BOOLEAN MODE );
复制代码

上述语句,查询包含 'MySQL' 的行,但如果该行同时包含 'database',则降低相关性。


MarkerHub


demo6:*


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'My*' IN BOOLEAN MODE );
复制代码

上述语句,查询关键字中包含'My'的行信息。


MarkerHub


demo7:"


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"MySQL Security"' IN BOOLEAN MODE );
复制代码

上述语句,查询包含确切短语 'MySQL Security' 的行信息。


MarkerHub


Query Expansion


查询扩展搜索是对自然语言搜索的修改,这种查询通常在查询的关键词太短,用户需要 implied knowledge(隐含知识)时进行,例如,对于单词 database 的查询,用户可能希望查询的不仅仅是包含 database 的文档,可能还指那些包含 MySQL、Oracle、RDBMS 的单词,而这时可以使用 Query Expansion 模式来开启全文检索的 implied knowledge通过在查询语句中添加 WITH QUERY EXPANSION / IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 可以开启 blind query expansion(又称为 automatic relevance feedback),该查询分为两个阶段。



  • 第一阶段:根据搜索的单词进行全文索引查询

  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询


接着来看一个例子,看看 Query Expansion 是如何使用的。


-- 创建索引
create FULLTEXT INDEX title_body_index on fts_articles(title,body);
复制代码

-- 使用 Natural Language 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database');
复制代码

使用 Query Expansion 前查询结果如下:


MarkerHub


-- 当使用 Query Expansion 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database' WITH QUERY expansion);
复制代码

使用 Query Expansion 后查询结果如下:


MarkerHub


由于 Query Expansion 的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。


删除全文索引


1、直接删除全文索引语法如下:

DROP INDEX full_idx_name ON db_name.table_name;
复制代码

2、使用 alter table 删除全文索引语法如下:

ALTER TABLE db_name.table_name DROP INDEX full_idx_name;
复制代码


来源:juejin.cn/post/6989871497040887845





推荐3个原创springboot+Vue项目,有完整视频讲解与文档和源码:


【dailyhub】【实战】带你从0搭建一个Springboot+elasticsearch+canal的完整项目



【VueAdmin】手把手教你开发SpringBoot+Jwt+Vue的前后端分离后台管理系统



【VueBlog】基于SpringBoot+Vue开发的前后端分离博客项目完整教学



作者:MarkerHub
来源:https://juejin.cn/post/7072257652784365599
收起阅读 »

最完整的Explain总结,SQL优化不再困难!

两个变种会在 explain 的基础上额外提供一些查询优化的信息。一般是使用了覆盖索引(索引包含了所有查询的字段)。对于innodb来说,如果是辅助索引性能会有不少提高mysql> explain select film_id from film_act...
继续阅读 »

在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询时,会返回执行计划的信息,而不是执行这条SQL(如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中)

CREATE TABLE `film` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

两个变种

explain extended

会在 explain 的基础上额外提供一些查询优化的信息。

紧随其后通过 show warnings 命令可以 得到优化后的查询语句,从而看出优化器优化了什么。

额外还有 filtered 列,是一个半分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)

mysql> explain extended select * from film where id = 1;


mysql> show warnings;


explain partitions

相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。

id列

id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。


一般是使用了覆盖索引(索引包含了所有查询的字段)。对于innodb来说,如果是辅助索引性能会有不少提高

mysql> explain select film_id from film_actor where film_id = 1;

Using where

查询的列未被索引覆盖,where筛选条件非索引的前导列

mysql> explain select * from actor where name = 'a';

Using where Using index

查询的列被索引覆盖,并且where筛选条件是索引列之一但是不是索引的前导列,意味着无法直接通过索引查找来查询到符合条件的数据

mysql> explain select film_id from film_actor where actor_id = 1;

NULL

查询的列未被索引覆盖,并且where筛选条件是索引的前导列,意味着用到了索引,但是部分字段未被索引覆盖,必须通过“回表”来实现,不是纯粹地用到了索引,也不是完全没用到索引

mysql>explain select * from film_actor where film_id = 1;

Using index condition

与Using where类似,查询的列不完全被索引覆盖,where条件中是一个前导列的范围;

mysql> explain select * from film_actor where film_id > 1;

Using temporary

mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。

  1. actor.name没有索引,此时创建了张临时表来distinct

mysql> explain select distinct name from actor;
  1. film.name建立了idx_name索引,此时查询时extra是using index,没有用临时表

mysql> explain select distinct name from film;

Using filesort

mysql 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。

此时mysql会根据联接类型浏览所有符合条件的记录,并保存排序关键字和行指针,然后排序关键字并按顺序检索行信息。

这种情况下一般也是要考虑使用索引来优化的。

  1. actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录。

mysql> explain select * from actor order by name;
  1. film.name建立了idx_name索引,此时查询时extra是using index

mysql> explain select * from film order by name;

作者:程序员段飞
来源:https://juejin.cn/post/7074030240904773645

收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间......

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

看看别人后端API接口写得,那叫一个优雅!

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。一般系统...
继续阅读 »

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。

一般系统的大致整体架构图如下:

需要说明的是,有些小伙伴会回复说,这个架构太简单了吧,太low了,什么网关啊,缓存啊,消息中间件啊,都没有。因为老顾这篇主要介绍的是API接口,所以我们聚焦点,其他的模块小伙伴们自行去补充。

接口交互

前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。

针对URL路径的restful风格,以及传入参数的公共请求头的要求(如:app_version,api_version,device等),老顾这里就不介绍了,小伙伴们可以自行去了解,也比较简单。

着重介绍一下后端服务器如何实现把数据返回给前端?

返回格式

后端返回给前端我们一般用JSON体方式,定义如下:

{
  #返回状态码
  code:integer,      
  #返回信息描述
  message:string,
  #返回值
  data:object
}

CODE状态码

code返回状态码,一般小伙伴们是在开发的时候需要什么,就添加什么。
如接口要返回用户权限异常,我们加一个状态码为101吧,下一次又要加一个数据参数异常,就加一个102的状态码。这样虽然能够照常满足业务,但状态码太凌乱了

我们应该可以参考HTTP请求返回的状态码,下面是常见的HTTP状态码:

200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误


我们可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4位数。

#1000~1999 区间表示参数错误
#2000~2999 区间表示用户错误
#3000~3999 区间表示接口异常

这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位。

Message

这个字段相对理解比较简单,就是发生错误时,如何友好的进行提示。一般的设计是和code状态码一起设计,如

再在枚举中定义,状态码

状态码和信息就会一一对应,比较好维护。

Data

返回数据体,JSON格式,根据不同的业务又不同的JSON体。
我们要设计一个返回体类Result

控制层Controller

我们会在controller层处理业务请求,并返回给前端,以order订单为例

我们看到在获得order对象之后,我们是用的Result构造方法进行包装赋值,然后进行返回。小伙伴们有没有发现,构造方法这样的包装是不是很麻烦,我们可以优化一下。

美观优化

我们可以在Result类中,加入静态方法,一看就懂

那我们来改造一下Controller

代码是不是比较简洁了,也美观了。

优雅优化

上面我们看到在Result类中增加了静态方法,使得业务处理代码简洁了。但小伙伴们有没有发现这样有几个问题:

1、每个方法的返回都是Result封装对象,没有业务含义

2、在业务代码中,成功的时候我们调用Result.success,异常错误调用Result.failure。是不是很多余

3、上面的代码,判断id是否为null,其实我们可以使用hibernate validate做校验,没有必要在方法体中做判断。

我们最好的方式直接返回真实业务对象,最好不要改变之前的业务方式,如下图

这个和我们平时的代码是一样的,非常直观,直接返回order对象,这样是不是很完美。那实现方案是什么呢?

实现方案

小伙伴们怎么去实现是不是有点思路,在这个过程中,我们需要做几个事情

1、定义一个注解@ResponseResult,表示这个接口返回的值需要包装一下

2、拦截请求,判断此请求是否需要被@ResponseResult注解

3、核心步骤就是实现接口ResponseBodyAdvice和@ControllerAdvice,判断是否需要包装返回值,如果需要,就把Controller接口的返回值进行重写。

注解类

用来标记方法的返回值,是否需要包装

拦截器

拦截请求,是否此请求返回的值需要包装,其实就是运行的时候,解析@ResponseResult注解

此代码核心思想,就是获取此请求,是否需要返回值包装,设置一个属性标记。

重写返回体

上面代码就是判断是否需要返回值包装,如果需要就直接包装。这里我们只处理了正常成功的包装,如果方法体报异常怎么办?处理异常也比较简单,只要判断body是否为异常类。

怎么做全局的异常处理,篇幅原因,老顾这里就不做介绍了,只要思路理清楚了,自行改造就行。

重写Controller

在控制器类上或者方法体上加上@ResponseResult注解,这样就ok了,简单吧。到此返回的设计思路完成,是不是又简洁,又优雅。

总结

这个方案还有没有别的优化空间,当然是有的。如:每次请求都要反射一下,获取请求的方法是否需要包装,其实可以做个缓存,不需要每次都需要解析。

作者:码农出击HK
来源:https://juejin.cn/post/7068884412674342926

收起阅读 »

当我们谈部署时,我们在谈什么?

计算机网络把各地的计算机连接了起来,只要有一台可以上网的终端,比如手机、电脑,就可以访问互联网上任何一台服务器的资源(包括静态资源和动态的服务)。作为开发者的我们,就是这些资源、服务的提供者,把资源上传到服务器,并把服务跑起来的过程就叫做部署。代码部分的部署,...
继续阅读 »


计算机网络把各地的计算机连接了起来,只要有一台可以上网的终端,比如手机、电脑,就可以访问互联网上任何一台服务器的资源(包括静态资源和动态的服务)。

作为开发者的我们,就是这些资源、服务的提供者,把资源上传到服务器,并把服务跑起来的过程就叫做部署


代码部分的部署,需要先经过构建,也就是编译打包的过程,把产物传到服务器。

最原始的部署方式就是在本地进行 build,然后把产物通过 FTP 或者 scp(基于 SSH 的远程拷贝文件拷贝) 传到服务器上,如果是后端代码还需要重启下服务。


每个人单独构建上传,这样不好管理,也容易冲突,所以现在都会用专门的平台来做这件事构建和部署,比如 jenkins。

我们代码会提交到 gitlab 等代码库,然后 jenkins 从这些代码库里把代码下载下来进行 build,再把产物上传到服务器上。

流程和直接在本地构建上传差不多,只不过这样方便管理冲突、历史等,还可以跨项目复用一些东西。


构建、部署的过程最开始是通过 shell 来写,但写那个的要求还是很高的。后来就支持了可视化的编排,可以被编排的这个构建、部署的流程叫做流水线 pipeline。


比如这是 jenkins 的 pipeline 的界面:


除了构建、部署外,也可以加入一些自动化测试、静态代码检查等任务。

这种自动化了的构建、部署流程就叫做 CI(持续集成)、CD(持续部署)。

我们现在还是通过 scp / FTP 来上传代码做的部署,但是不同代码的运行环境是不同的,比如 Node.js 服务需要安装 node,Java 服务需要安装 JRE 等,只把代码传上去并不一定能跑起来。

那怎么办呢?怎么保证部署的代码运行在正确的环境?

把环境也给管理起来,作为部署信息的一部分不就行了?

现在流行的容器技术就是做这个的,比如 docker,可以把环境信息和服务启动方式放到 dockerfile 里,build 产生一个镜像 image,之后直接部署这个 docker image 就行。

比如我们用 nginx 作为静态服务器的时候,dockerfile 可能是这样的:

FROM nginx:alpine

COPY /nginx/ /etc/nginx/

COPY /dist/ /usr/share/nginx/html/

EXPOSE 80

这样就把运行环境给管理了起来。

所以,现在的构建产物不再是直接上传服务器,而是生成一个 docker image,上传到 docker registry,然后把这个 docker image 部署到服务器。


还有一个问题,现在前端代码、后端代码都部署在了我们的服务器上,共享服务器的网络带宽,其中前端代码是不会变动的、流量却很大,这样使得后端服务的可用带宽变小、支持的并发量下降。

能不能把这部分静态资源的请求分离出去呢?最好能部署到离用户近一点的服务器,这样访问更快。

确实可以,这就是 CDN 做的事情。

网上有专门的 CDN 服务提供商,它们有很多分散在各地的服务器,可以提供静态资源的托管。这些静态资源最终还是从我们的静态资源服务器来拿资源的,所以我们的静态资源服务器叫做源站。但是请求一次之后就会缓存下来,下次就不用再请求源站了,这样就减轻了我们服务器的压力,还能加速用户请求静态资源的速度。


这样就解决了静态资源分去了太多网络带宽的问题,而且还加速了资源的访问速度。

此外,静态资源的部署还要考虑顺序问题,要先部署页面用到的资源,再部署页面,还有,需要在文件名加 hash 来触发缓存更新等,这些都是更细节的问题。

这里说的都是网页的部署方式,对于 APP/小程序它们是有自己的服务器和分发渠道的,我们构建完之后不是部署,而是在它们的平台提交审核,审核通过之后由它们负责部署和分发。

总结

互联网让我们能够用手机、PC 等终端访问任何一台服务器的资源、服务。而提供这些资源、服务就是我们开发者做的事情。把资源上传到服务器上,并把服务跑起来,就叫做部署。

对于代码,我们可以本地构建,然后把构建产物通过 FTP/scp 等方式上传到服务器。

但是这样的方式不好管理,所以我们会有专门的 CI/CD 平台来做这个,比如 jenkins。

jenkins 支持 pipeline 的可视化编排,比写 shell 脚本的方式易用很多,可以在构建过程中加入自动化测试、静态代码检查等步骤。

不同代码运行环境不同,为了把环境也管理起来,我们会使用容器技术,比如 docker。把环境信息写入 dockerfile,然后构建生成 docker image,上传到 registry,之后部署这个 docker image 就行。

静态资源和动态资源共享服务器的网络带宽,为了减轻服务器压力、也为了加速静态资源的访问,我们会使用 CDN 来对静态资源做加速,把我们的静态服务器作为源站。第一个静态资源的请求会请求源站并缓存下来,之后的请求就不再需要请求源站,这样就减轻了源站的压力。此外,静态资源的部署还要考虑顺序、缓存更新等问题。

对于网页来说是这样,APP/小程序等不需要我们负责部署,只要在它们的平台提交审核,然后由它们负责部署和分发。

当我们在谈部署的时候,主要就是在谈这些。

作者:zxg_神说要有光
来源:https://juejin.cn/post/7073826878858985479

收起阅读 »

关于项目版本号命名的规范与原则

软件版本阶段说明Alpha版此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改Beta版版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消...
继续阅读 »

软件版本阶段说明

  • Alpha版

    此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改

  • Beta版

    版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消除,此版本主要的修改对像是软件的UI

  • RC版

    该版本已经相当成熟了,基本上不存在导致错误的BUG,与即将发行的正式版相差无几

  • Release版:

    该版本意味“最终版本”,在前面版本的一系列测试版之后,终归会有一个正式版本,是最终交付用户使用的一个版本。该版本有时也称为标准版。一般情况下,Release不会以单词形式出现在软件封面上,取而代之的是符号(R)

在上面我们大致了解了软件从开发到交付会经历4个版本阶段,而当我们开始搭建一个新项目时,不可避免地要配置 package.json 文件,这个 version 怎么命名呢?

// package.json
{
   "name": "项目名称",
   "version": "0.1.0",
   "description": "项目描述",
   "author": "项目作者",
   "license": "MIT",
}

版本命名规范

在说版本命名规范之前,先说说比较流行的版本命名格式

  • GNU 风格

  • Windows 风格

  • .Net Framework 风格

最常见的版本命名格式就是 GNU 风格,下面以 GNU 风格展开说明

主版本号.子版本号.修正(或阶段)版本号.日期版本号_希腊字母版本号

希腊字母版本号共有5种,分别为:base、alpha、beta、RC、release


版本命名修改规则

项目初版本时,版本号可以为 0.1.0

  • 希腊字母版本号(beta)

    此版本号用于标注当前版本的软件处于哪个开发阶段,当软件进入到另一个阶段时需要修改此版本号

  • 日期版本号

    用于记录修改项目的当前日期,每天对项目的修改都需要更改日期版本号(只有此版本号才可由开发人员决定是否修改)

  • 修正(或阶段)版本号改动

    当项目在进行了局部修改或bug修正时,主版本号和子版本号都不变,修正版本号加1

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.0",
       "version": "0.1.1",
    }
  • 子版本号改动

    当项目在原有基础上增加了某些功能模块时,比如增加了对权限控制、增加自定义视图等功能,主版本号不变,子版本号加1,修正版本号复位为0

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.8",
       "version": "0.2.0",
    }
  • 主版本号改动

    当项目在进行了重大修改或局部修正累积较多,而导致项目整体发生全局变化时,比如增加多个模块或者重构,主版本号加 1

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.0",  // 一期项目
       "version": "1.1.0",  // 二期项目
       "version": "2.1.0",  // 三期项目
    }

文件命名规范

文件名称由四部分组成:第一部分为项目名称,第二部分为文件的描述,第三部分为当前软件的版本号,第四部分为文件阶段标识加文件后缀

例如:xx后台管理系统测试报告1.1.1.031222_beta_d.xls,此文件为xx后台管理系统的测试报告文档,版本号为:1.1.1.031222_beta


  • 如果是同一版本同一阶段的文件修改过两次以上,则在阶段标识后面加以数字标识,每次修改数字加1,xx后台管理系统测试报告1.1.1.031222_beta_d1.xls

  • 当有多人同时提交同一份文件时,可以在阶段标识的后面加入人名或缩写来区别,例如:xx后台管理系统测试报告 1.1.1.031222_beta_d_spp.xls。当此文件再次提交时也可以在人名或人名缩写的后面加入序号来区别,例如:xx后台管理系统测试报告1.1.1.031222_beta_d_spp2.xls

阶段标识

软件的每个版本中包括11个阶段,详细阶段描述如下:

阶段名称阶段标识
需求控制a
设计阶段b
编码阶段c
单元测试d
单元测试修改e
集成测试f
集成测试修改g
系统测试h
系统测试修改i
验收测试j
验收测试修改k

作者:Jesse90s
来源:https://juejin.cn/post/7073902470585384990

收起阅读 »

PAT 乙级 1029 旧键盘:找出键盘上的坏键

题目描述旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。输入格式输入在 2 行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过 80 个字符的串,由字...
继续阅读 »


题目描述

旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。

输入格式

输入在 2 行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过 80 个字符的串,由字母 A-Z(包括大、小写)、数字 0-9、以及下划线 _(代表空格)组成。题目保证 2 个字符串均非空。

输出格式:

按照发现顺序,在一行中输出坏掉的键。其中英文字母只输出大写,每个坏键只输出一次。题目保证至少有 1 个坏键。

输入样例:

7_This_is_a_test
_hs_s_a_es

输出样例:

7TI

思路分析

这道题用哈希表标记输入状态的话会很简单,我就是把字符串中所有字符的ASCII码值映射到数组下标中去,0为未输入,1为已输入。
先将第二个字符串所有字符标记(因为是第二个字符串少了字符),为了方便进行比较和后面的输出,我在标记之前将所有字符统一全部转换成了大写字母
然后再遍历第一个字符串,把所有第一个字符串中存在而第二个字符串里不存在的字符输出,也就是第二个字符串里缺少的字符,不要忘了输出完标记为-1,不然会重复输出哦
PS:其实我刚开始的思路也不是用哈希表,是想用两层循环把两个字符串一个一个字符比较输出,但是还没能实现只输出一遍

AC代码

版本一

#include<bits/stdc++.h>
using namespace std;
int main()
{
   int keyboard[128]={0};//下标映射为ASCII码值,0:未输入,1:已输入
   string s1,s2;
   cin>>s1>>s2;
   for(char c2:s2)
       keyboard[toupper(c2)]=1;//已输入标记为1
   for(char c1:s1)
       if(keyboard[toupper(c1)]==0)
      {
           putchar(toupper(c1));
           keyboard[toupper(c1)]=-1;//该字符已输出,标记为-1,避免重复输出
      }
   return 0;
}

版本2

#include <ctype.h>
#include<iostream>
#include <string>
using namespace std;
int main()
{
   int keyboard[128] = {0};
   string s1;
   char c2;
   getline(cin, s1);//因为后面换行之后还要输入字符所以不能直接用cin
   while((c2 = getchar()) != '\n')//输入第二行字符串
       keyboard[toupper(c2)] = 1;

   for(char c1:s1)
       if(keyboard[toupper(c1)] == 0)
{
           putchar(toupper(c1));
           keyboard[toupper(c1)] = -1;
      }
   return 0;
}

总结

用了一次Hash表,感觉也没有想象的那么困难嘛
看书上写的哈希散列可真是头疼,自己用一了遍反而好像也不难😂
真是纸上得来终觉浅,觉知此事要躬行。

作者:梦境无限
来源:https://juejin.cn/post/7074090679198023688

收起阅读 »

奇怪的电梯广搜做法~

一、题目描述:一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例...
继续阅读 »

一、题目描述:

一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 Ki(K1=3, K2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 -2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?

来源:洛谷 http://www.luogu.com.cn/problem/P11…

输入格式

共二行。 第一行为三个用空格隔开的正整数,表示 N, A, B(1≤N≤200,1≤A,B≤N )。 第二行为 N 个用空格隔开的非负整数,表示 Ki

5 1 5
3 3 1 2 5

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

3

二、思路分析:

首先看一下输入数据是什么意思,首先输入一个N, A, B,也就是分别输入楼层数(N)、开始楼层(A)、 终点楼层(B)。 在例子中,我们的 楼层数N是5,也就是说有5层楼,第二行就是这5层楼的每层楼的数字k。

1、题目中说到只有四个按钮:开,关,上,下,上下的层数等于当前楼层上的那个数字,众所周知,电梯的楼层到了的时候啊,它是会自动打开的,没有人进来的时候,也会自动关上,这里求的是最少按几个按钮,所以我们在这里不用看开关,也就是可以看作该楼层只有两个按钮 +k 、 -k

2、题目中提到最少按几次,很明显,这是一个搜索题。当出现最少的时候,我们就可以考虑用广搜了(也可以用深搜做的啦)

3、这里注意一下,就是我们在不同的按钮次数时遇到停在同一楼层,这时候就会出现一个重复的且没有必要的搜索,所以我们需要在搜索的时候加个条件。

三、AC 代码:

import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sr = new Scanner (System.in);
int N = sr.nextInt();              
int A = sr.nextInt();            
int B = sr.nextInt();              

// 广搜必备队列
Queue<Fset> Q = new LinkedList<Fset>();
// 一个记忆判断,看看这层楼是不是来过
boolean[] visit = new boolean[N+1];
// 来存楼梯按钮的,假设第3层的k是2, 那么 k[3][0]=2 (向上的按钮)、 k[3][1]=-2 (向下的按钮)
int[][] k = new int [N+1][2];
for(int i = 1 ; i <= N ; i++){
k[i][0] = sr.nextInt();
k[i][1] = -k[i][0];
}

// 存一个起始楼层和按钮次数到队列
Q.add(new Fset(A,0));
// 当队列为空也就是所以能走的路线都走过了,没有找到就可以返回-1了
while(!Q.isEmpty()){
Fset t = Q.poll();
// 找到终点楼层,不用找了直接输出并退出搜索
if(t.floor == B){
System.out.println(t.count);
return;
}
//
for(int j = 0 ; j < 2 ; j++){
// 按键后到的楼层
int f = t.floor + k[t.floor][j];
// 判断按键后到的楼层是否有效和是否走过
if(f >= 1 && f <= N && visit[f]!=true) {
Q.add(new Fset(f,t.count+1));
// 做标记
visit[f]=true;
}  
      }
      }
       // 没找到
       System.out.println(-1);
}
}

class Fset{
int floor; // 当前楼层
int count; // 当前按键次数
public Fset(int floor, int count) {
this.floor = floor;
this.count = count;
}
}


四、总结:

为什么用的队列呢? 因为队列的排队取出的,首先判断的一定是按钮次数最少的,感觉这道题用广搜或者深搜效果其实差不多,我写的深搜多一个判断,就是当当前次数超过我找到的最少按钮次数,我就丢弃这个。 广搜像晕染吧,往四周分散搜索,

嗯,就酱~


作者:d粥
来源:https://juejin.cn/post/7073817170618089479

收起阅读 »

Google 大佬们为什么要开发 Go 这门新语言?

Go
大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?难不成是造轮子,其他语言不香吗?背景Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施...
继续阅读 »

大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?

难不成是造轮子,其他语言不香吗?

背景

Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施时遇到的一些问题。


图上三位是 Go 语言最初的设计者,功力都非常的深厚,按序从左起分别是:

  • Robert Griesemer:参与过 Google V8 JavaScript 引擎和 Java HotSpot 虚拟机的研发。

  • Rob Pike:Unix 操作系统早期开发者之一,UTF-8 创始人之一,Go 语言吉祥物设计者是 Rob Pike 的媳妇。

  • Ken Thompson:图灵奖得主,Unix 操作系统早期开发者之一,UTF-8 创始人之一,C 语言(前身 B 语言)的设计者。

遇到的问题

曾经在早期的采访中,Google 大佬们反馈感觉 "编程" 太麻烦了,他们很不喜欢 C++,对于现在工作所用的语言和环境感觉比较沮丧,充满着许多不怎么好用的特性。

具体遭遇到的问题。如下:

  • 软件复杂:多核处理器、网络系统、大规模计算集群和网络编程模型所带来的问题只能暂时绕开,没法正面解决。

  • 软件规模:软件规模也发生了变化,今天的服务器程序由数千万行代码组成,由数百甚至数千名程序员进行工作,而且每天都在更新(据闻 Go 就是在等编译的 45 分钟中想出来的)。

  • 编译耗时:在大型编译集群中,构建时间也延长到了几分钟,甚至几小时。

设计目的

为了实现上述目标,在既有语言上改造的话,需要解决许多根本性的问题,因此需要一种新的语言。

这门新语言需要符合以下需求:

  • 目的:设计和开发 Go 是为了使在这种环境下能够提高工作效率

  • 设计:在 Go 的设计上,除了比较知名的方面:如内置并发和垃圾收集。还考虑到:严格的依赖性管理,随着系统的发展,软件架构的适应性,以及跨越组件之间边界的健壮性。

这门新语言就是现在的 Go。

Go 在 Google

Go 是 Google 设计的一种编程语言,用于帮助解决谷歌的问题,而 Google 的问题很大。

Google 整体的应用软件很庞大,硬件也很庞大,有数百万行的软件,服务器主要是 C++ 语言,其他部分则是大量的 Java 和 Python。

数以千计的工程师在代码上工作,在一个由所有软件组成的单一树的 "头 " 上工作,所以每天都会对该树的所有层次进行重大改变。

一个大型的定制设计的分布式构建系统使得这种规模的开发是可行的,但它仍然很大。

当然,所有这些软件都在几十亿台机器上运行,这些机器被视为数量不多的独立、联网的计算集群。


简而言之,Google 的开发规模很大,速度可能是缓慢的,而且往往是笨拙的。但它是有效的。

Go 项目的目标是:消除 Google 软件开发的缓慢和笨拙,从而使这个过程更富有成效和可扩展。这门语言是由编写、阅读、调试和维护大型软件系统的人设计的,也是为他们设计的

因此 Go 的目的不是为了研究编程语言的设计,而是为了改善其设计者及其同事的工作环境。

Go 更多的是关于软件工程而不是编程语言研究。或者换个说法,它是为软件工程服务的语言设计。

痛点

当 Go 发布时,有些人声称它缺少被认为是现代语言的必要条件的特定功能或方法。在缺乏这些设施的情况下,Go怎么可能有价值?

我们的答案是:Go 所拥有的特性可以解决那些使大规模软件开发变得困难的问题。

这些问题包括:

  • 构建速度缓慢。

  • 不受控制的依赖关系。

  • 每个程序员使用不同的语言子集。

  • 对程序的理解不透彻(代码可读性差,文档不全等)。

  • 工作的重复性。

  • 更新的成本。

  • 版本偏移(version skew)。

  • 编写自动工具的难度。

  • 跨语言的构建。

纯粹一门语言的单个功能并不能解决这些问题,我们需要对软件工程有一个更大的看法。因此在 Go 的设计中,我们试图把重点放在这些问题的解决方案上。

总结

软件工程指导了 Go 的设计。

与大多数通用编程语言相比,Go 的设计是为了解决我们在构建大型服务器软件时接触到的一系列软件工程问题。这可能会使 Go 听起来相当沉闷和工业化。

但事实上,整个设计过程中对清晰、简单和可组合性的关注反而导致了一种高效、有趣的语言,许多程序员发现它的表现力和力量。

为此产生的 Go 特性包括:

  • 清晰的依赖关系。

  • 清晰的语法。

  • 清晰的语义。

  • 相对于继承的组合。

  • 编程模型提供的简单性(垃圾收集、并发)。

  • 简单的工具(Go工具、gofmt、godoc、gofix)。

这就是为什么要开发 Go 的由来,以及为什么会产生如此的设计和特性的原因。

你学会了吗?:)

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

参考


作者:煎鱼eddycjy
来源:https://juejin.cn/post/7054028466060001288

收起阅读 »

本着什么原则,才能写出优秀的代码?

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。在这个...
继续阅读 »

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。

有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。

风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。

在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如直接重写。

但有的时候,我们看一些著名的开源项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。

那么,这篇文章就试图分析一下好代码都有哪些特点,以及本着什么原则,才能写出优秀的代码。

初级阶段

先说说比较基本的原则,只要是程序员,不管是高级还是初级,都会考虑到的。

img

这只是列举了一部分,还有很多,我挑选四项简单举例说明一下。

  1. 格式统一

  2. 命名规范

  3. 注释清晰

  4. 避免重复代码

以下用 Python 代码分别举例说明:

格式统一

格式统一包括很多方面,比如 import 语句,需要按照如下顺序编写:

  1. Python 标准库模块

  2. Python 第三方模块

  3. 应用程序自定义模块

然后每部分间用空行分隔。

import os
import sys

import msgpack
import zmq

import foo
复制代码

再比如,要添加适当的空格,像下面这段代码;

i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
复制代码

代码都紧凑在一起了,很影响阅读。

i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
复制代码

添加空格之后,立刻感觉清晰了很多。

还有就是像 Python 的缩进,其他语言的大括号位置,是放在行尾,还是另起新行,都需要保证统一的风格。

有了统一的风格,会让代码看起来更加整洁。

命名规范

好的命名是不需要注释的,只要看一眼命名,就能知道变量或者函数的作用。

比如下面这段代码:

a = 'zhangsan'
b = 0
复制代码

a 可能还能猜到,但当代码量大的时候,如果满屏都是 abcd,那还不得原地爆炸。

把变量名稍微改一下,就会使语义更加清晰:

username = 'zhangsan'
count = 0
复制代码

还有就是命名要风格统一。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来非常分裂。

注释清晰

看别人代码的时候,最大的愿望就是注释清晰,但在自己写代码时,却从来不写。

但注释也不是越多越好,我总结了以下几点:

  1. 注释不限于中文或英文,但最好不要中英文混用

  2. 注释要言简意赅,一两句话把功能说清楚

  3. 能写文档注释应该尽量写文档注释

  4. 比较重要的代码段,可以用双等号分隔开,突出其重要性

举个例子:

# =====================================
# 非常重要的函数,一定谨慎使用 !!!
# =====================================

def func(arg1, arg2):
   """在这里写函数的一句话总结(如: 计算平均值).

  这里是具体描述.

  参数
  ----------
  arg1 : int
      arg1的具体描述
  arg2 : int
      arg2的具体描述

  返回值
  -------
  int
      返回值的具体描述

  参看
  --------
  otherfunc : 其它关联函数等...

  示例
  --------
  示例使用doctest格式, 在`>>>`后的代码可以被文档测试工具作为测试用例自动运行

  >>> a=[1,2,3]
  >>> print [x + 3 for x in a]
  [4, 5, 6]
  """
复制代码

避免重复代码

随着项目规模变大,开发人员增多,代码量肯定也会增加,避免不了的会出现很多重复代码,这些代码实现的功能是相同的。

虽然不影响项目运行,但重复代码的危害是很大的。最直接的影响就是,出现一个问题,要改很多处代码,一旦漏掉一处,就会引发 BUG。

比如下面这段代码:

import time


def funA():
   start = time.time()
   for i in range(1000000):
       pass
   end = time.time()

   print("funA cost time = %f s" % (end-start))


def funB():
   start = time.time()
   for i in range(2000000):
       pass
   end = time.time()

   print("funB cost time = %f s" % (end-start))


if __name__ == '__main__':
   funA()
   funB()
复制代码

funA()funB() 中都有输出函数运行时间的代码,那么就适合将这些重复代码抽象出来。

比如写一个装饰器:

def warps():
   def warp(func):
       def _warp(*args, **kwargs):
           start = time.time()
           func(*args, **kwargs)
           end = time.time()
           print("{} cost time = {}".format(getattr(func, '__name__'), (end-start)))
       return _warp
   return warp
复制代码

这样,通过装饰器方法,实现了同样的功能。以后如果需要修改的话,直接改装饰器就好了,一劳永逸。

进阶阶段

当代码写时间长了之后,肯定会对自己有更高的要求,而不只是格式注释这些基本规范。

但在这个过程中,也是有一些问题需要注意的,下面就来详细说说。

炫技

第一个要说的就是「炫技」,当对代码越来越熟悉之后,总想写一些高级用法。但现实造成的结果就是,往往会使代码过度设计。

这不得不说说我的亲身经历了,曾经有一段时间,我特别迷恋各种高级用法。

有一次写过一段很长的 SQL,而且很复杂,里面甚至还包含了一个递归调用。有「炫技」嫌疑的 Python 代码就更多了,往往就是一行代码包含了 N 多魔术方法。

然后在写完之后漏出满意的笑容,感慨自己技术真牛。

结果就是各种被骂,更重要的是,一个星期之后,自己都看不懂了。

img

其实,代码并不是高级方法用的越多就越牛,而是要找到最适合的。

越简单的代码,越清晰的逻辑,就越不容易出错。而且在一个团队中,你的代码并不是你一个人维护,降低别人阅读,理解代码的成本也是很重要的。

脆弱

第二点需要关注的是代码的脆弱性,是否细微的改变就可能引起重大的故障。

代码里是不是充满了硬编码?如果是的话,则不是优雅的实现。很可能导致每次性能优化,或者配置变更就需要修改源代码。甚至还要重新打包,部署上线,非常麻烦。

而把这些硬编码提取出来,设计成可配置的,当需要变更时,直接改一下配置就可以了。

再来,对参数是不是有校验?或者容错处理?假如有一个 API 被第三方调用,如果第三方没按要求传参,会不会导致程序崩溃?

举个例子:

page = data['page']
size = data['size']
复制代码

这样的写法就没有下面的写法好:

page = data.get('page', 1)
size = data.get('size', 10)
复制代码

继续,项目中依赖的库是不是及时升级更新了?

积极,及时的升级可以避免跨大版本升级,因为跨大版本升级往往会带来很多问题。

还有就是在遇到一些安全漏洞时,升级是一个很好的解决办法。

最后一点,单元测试完善吗?覆盖率高吗?

说实话,程序员喜欢写代码,但往往不喜欢写单元测试,这是很不好的习惯。

有了完善,覆盖率高的单元测试,才能提高项目整体的健壮性,才能把因为修改代码带来的 BUG 的可能性降到最低。

重构

随着代码规模越来越大,重构是每一个开发人员都要面对的功课,Martin Fowler 将其定义为:在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改。

重构的收益是明显的,可以提高代码质量和性能,并提高未来的开发效率。

但重构的风险也很大,如果没有理清代码逻辑,不能做好回归测试,那么重构势必会引发很多问题。

这就要求在开发过程中要特别注重代码质量。除了上文提到的一些规范之外,还要注意是不是滥用了面向对象编程原则,接口之间设计是不是过度耦合等一系列问题。

那么,在开发过程中,有没有一个指导性原则,可以用来规避这些问题呢?

当然是有的,接着往下看。

高级阶段

最近刚读完一本书,Bob 大叔的《架构整洁之道》,感觉还是不错的,收获很多。

img

全书基本上是在描述软件设计的一些理论知识。大体分成三个部分:编程范式(结构化编程、面向对象编程和函数式编程),设计原则(主要是 SOLID),以及软件架构(其中讲了很多高屋建翎的内容)。

总体来说,这本书中的内容可以让你从微观(代码层面)和宏观(架构层面)两个层面对整个软件设计有一个全面的了解。

其中 SOLID 就是指面向对象编程和面向对象设计的五个基本原则,在开发过程中适当应用这五个原则,可以使软件维护和系统扩展都变得更容易。

五个基本原则分别是:

  1. 单一职责原则(SRP)

  2. 开放封闭原则(OCP)

  3. 里氏替换原则(LSP)

  4. 接口隔离原则(ISP)

  5. 依赖倒置原则(DIP)

单一职责原则(SRP)

A class should have one, and only one, reason to change. – Robert C Martin

一个软件系统的最佳结构高度依赖于这个系统的组织的内部结构,因此每个软件模块都有且只有一个需要被改变的理由。

这个原则非常容易被误解,很多程序员会认为是每个模块只能做一件事,其实不是这样。

举个例子:

假如有一个类 T,包含两个函数,分别是 A()B(),当有需求需要修改 A() 的时候,但却可能会影响 B() 的功能。

这就不是一个好的设计,说明 A()B() 耦合在一起了。

开放封闭原则(OCP)

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

通俗点解释就是设计的类对扩展是开放的,对修改是封闭的,即可扩展,不可修改。

看下面的代码示例,可以简单清晰地解释这个原则。

void DrawAllShape(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawSquare((struct Circle*)s);
break;
default:
break;
}
}
}
复制代码

上面这段代码就没有遵守 OCP 原则。

假如我们想要增加一个三角形,那么就必须在 switch 下面新增一个 case。这样就修改了源代码,违反了 OCP 的封闭原则。

缺点也很明显,每次新增一种形状都需要修改源代码,如果代码逻辑复杂的话,发生问题的概率是相当高的。

class Shape
{
public:
virtual void Draw() const = 0;
}

class Square: public Shape
{
public:
virtual void Draw() const;
}

class Circle: public Shape
{
public:
virtual void Draw() const;
}

void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator I;
for (i = list.begin(): i != list.end(); i++)
{
(*i)->Draw();
}
}
复制代码

通过这样修改,代码就优雅了很多。这个时候如果需要新增一种类型,只需要增加一个继承 Shape 的新类就可以了。完全不需要修改源代码,可以放心扩展。

里氏替换原则(LSP)

Require no more, promise no less.– Jim Weirich

这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。

里氏替换原则可以从两方面来理解:

第一个是继承。如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。

子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

第二个是多态,而多态的前提就是子类覆盖并重新定义父类的方法。

为了符合 LSP,应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法。当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

举个例子:

看下面这段代码:

class A{
public int func1(int a, int b){
return a - b;
}
}

public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50=" + a.func1(100, 50));
System.out.println("100-80=" + a.func1(100, 80));
}
}
复制代码

输出;

100-50=50
100-80=20
复制代码

现在,我们新增一个功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:

  1. 两数相减

  2. 两数相加,然后再加 100

现在代码变成了这样:

class B extends A{
public int func1(int a, int b){
return a + b;
}

public int func2(int a, int b){
return func1(a,b) + 100;
}
}

public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50=" + b.func1(100, 50));
System.out.println("100-80=" + b.func1(100, 80));
System.out.println("100+20+100=" + b.func2(100, 20));
}
}
复制代码

输出;

100-50=150
100-80=180
100+20+100=220
复制代码

可以看到,原本正常的减法运算发生了错误。原因就是类 B 在给方法起名时重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。

这样做就违反了 LSP,使程序不够健壮。更通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

接口隔离原则(ISP)

Clients should not be forced to depend on methods they do not use. –Robert C. Martin

软件设计师应该在设计中避免不必要的依赖。

ISP 的原则是建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法要尽量少。

也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。

单一职责与接口隔离的区别:

  1. 单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。

  2. 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节; 而接口隔离原则主要约束接口。

举个例子:

img

首先解释一下这个图的意思:

「犬科」类依赖「接口 I」中的方法:「捕食」,「行走」,「奔跑」; 「鸟类」类依赖「接口 I」中的方法「捕食」,「滑翔」,「飞翔」。

「宠物狗」类与「鸽子」类分别是对「犬科」类与「鸟类」类依赖的实现。

对于具体的类:「宠物狗」与「鸽子」来说,虽然他们都存在用不到的方法,但由于实现了「接口 I」,所以也 必须要实现这些用不到的方法,这显然是不好的设计。

如果将这个设计修改为符合接口隔离原则的话,就必须对「接口 I」进拆分。

img

在这里,我们将原有的「接口 I」拆分为三个接口,拆分之后,每个类只需实现自己需要的接口即可。

依赖倒置原则(DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.– Robert C. Martin

高层策略性的代码不应该依赖实现底层细节的代码。

这话听起来就让人听不明白,我来翻译一下。大概就是说在写代码的时候,应该多使用稳定的抽象接口,少依赖多变的具体实现。

举个例子:

看下面这段代码:

public class Test {

public void studyJavaCourse() {
System.out.println("张三正在学习 Java 课程");
}

public void studyDesignPatternCourse() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

上层直接调用:

public static void main(String[] args) {
Test test = new Test();
test.studyJavaCourse();
test.studyDesignPatternCourse();
}
复制代码

这样写乍一看并没有什么问题,功能也实现的好好的,但仔细分析,却并不简单。

第一个问题:

如果张三又新学习了一门课程,那么就需要在 Test() 类中增加新的方法。随着需求增多,Test() 类会变得非常庞大,不好维护。

而且,最理想的情况是,新增代码并不会影响原有的代码,这样才能保证系统的稳定性,降低风险。

第二个问题:

Test() 类中方法实现的功能本质上都是一样的,但是却定义了三个不同名字的方法。那么有没有可能把这三个方法抽象出来,如果可以的话,代码的可读性和可维护性都会增加。

第三个问题:

业务层代码直接调用了底层类的实现细节,造成了严重的耦合,要改全改,牵一发而动全身。

基于 DIP 来解决这个问题,势必就要把底层抽象出来,避免上层直接调用底层。

img

抽象接口:

public interface ICourse {
void study();
}
复制代码

然后分别为 JavaCourseDesignPatternCourse 编写一个类:

public class JavaCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习 Java 课程");
}
}

public class DesignPatternCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

最后修改 Test() 类:

public class Test {

   public void study(ICourse course) {
       course.study();
  }
}
复制代码

现在,调用方式就变成了这样:

public static void main(String[] args) {
   Test test = new Test();
   test.study(new JavaCourse());
   test.study(new DesignPatternCourse());
}
复制代码

通过这样开发,上面提到的三个问题得到了完美解决。

其实,写代码并不难,通过什么设计模式来设计架构才是最难的,也是最重要的。

所以,下次有需求的时候,不要着急写代码,先想清楚了再动手也不迟。

这篇文章写的特别辛苦,主要是后半部分理解起来有些困难。而且有一些原则也确实没有使用经验,单靠文字理解还是差点意思,体会不到精髓。

其实,文章中的很多要求我都做不到,总结出来也相当于是对自己的一个激励。以后对代码要更加敬畏,而不是为了实现功能草草了事。写出健壮,优雅的代码应该是每个程序员的目标,与大家共勉。


作者:yongxinz
链接:https://mp.weixin.qq.com/s/xWZmP4qBI8cm68UZH6AXOg

收起阅读 »

十分钟搞懂手机号码一键登录

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只...
继续阅读 »

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只要确认登录用的手机号码是在绑定了此号码的手机上发起的即可认证成功,从这一点来看,它和短信验证码登录并无本质区别,都是一种设备认证登录方式。这篇文章就来捋一下其中的技术门道。

这几年为了保护用户的隐私安全,Android和iOS系统都限制了应用获取本机号码的能力,即使通过某些技术手段获取到了本机号码,这个号码还可能是被篡改的,所以应用直接读取本机号码用于登录是不可行的。那么这些应用是怎么获取到真实的本机号码的呢?答案是电信运营商,手机要打电话、要上网、要计费,运营商肯定能对应到正确的手机号码。国内的运营商就是移动、联通、电信这三家,它们都开放了这种能力。对于在互联网大潮中被管道化的运营商来说,不失为一种十分有意义的积极进取。

手机流量上网的原理

手机号码一键登录是借助手机流量上网来实现,所以先要搞清楚流量上网的原理。

目前网上已有很多关于一键登录的技术文章,但是内容基本雷同,关于获取手机号码的部分,所述都是通过运营商的数据网关能力,语焉不详,对于有追求的技术人来说,难以忍受。这个章节就来介绍下这种从数据网关获取手机号码的能力是如何实现的,因为通信专业知识十分繁杂,我也没有经过专业的学习,大家也不想接触到很多的专业名词,所以这里只保留一些关键的专业名词,尽量以通俗易懂的方式来理清这个机制。

五层网络模型

对网络比较熟悉的同学,应该了解五层协议,那么手机流量上网时的五层网络模型有何不同呢?


从上图可以看出,手机流量上网的主要区别在数据链路层和物理层。在数据链路层,流量上网没有MAC地址的概念,它采用一种点对点协议(PPP),手机端通过拨号方式建立这种PPP连接,然后发送数据。在物理层,流量上网通过手机内置的基带模块进行无线信号的调制、解调工作,从而实现与移动基站之间的电磁波通信。

流量上网的机制

点对点协议支持身 验证功能,手机端发起连接时会携带自己的身 粉证明,一般就是手机卡内置的IMSI,这个IMSI也会保存在运营商的数据库中,因此基站就可以验证连接用户的身 ,当然这个验证过程不是简单的对比IMSI,会有更多安全机制。为了更清楚的了解流量上网机制,下面再来一张4G流量上网时手机与运营商的交互示意图:


核心组件

手机:这其中对流量上网起到关键作用的就是手机卡和基带模块。手机卡中保存了IMSI,全称International Mobile Subscriber Identification Number,国际移动用户识别码。IMSI是手机卡的身 标识。

基站:就是外边常见的铁架子信号塔,是一种能覆盖一定范围的无线电收发信息电台,手机会连接到它,然后它再通过光纤连接到运营商网络,从而实现移动通信。

MME:Mobility Management Entity,移动控制单元。手机建立连接时会先访问到这里,负责:手机与基站的接入控制,手机卡的鉴权、会话管理、安全传输,漫游控制、跨运营商通信等。

HSS:Home Subscriber Server,归属签约用户服务器。保存本地签约的手机卡信息,包括手机卡IMSI与手机号的对应关系,手机号的套餐信息、手机号的归属地信息等。

S-GW:Service Gateway,服务网关。4G环境下,用户侧与运营商核心网之间的业务网关。访问能不能进入,能做什么业务,去哪里做业务,是在这里控制的。跨运营商计费、漫游计费等也在这里完成。

P-GW:PDN Gateway,PDN网关。运营商核心网与互联网之间的网关,手机真正上网就是通过它了。它会给手机分配一个IP地址,控制上网的速度,对流量进行计费等。

PCRF:Policy and Charging Rules Function,策略与计费控制单元,保存每个用户的网络访问策略和计费规则。

上网过程

为了方便理解,这里将上网的过程大致分为两个部分(和上图的1、2对应):

  • 1 接入:建立连接时,手机携带IMSI信息,通过基站访问到MME,MME通过HSS验证IMSI信息,然后MME进行一些初始化工作,返回一些鉴权参数给手机,手机再进行一些计算,然后把计算结果返回给MME,MME验证手机的计算结果,验证通过则允许接入。这个过程保证了接入的安全,MME还为后续的数据传输提供了加密传输支持,保护数据不被窃听和篡改,有兴趣的同学可以去详细了解下。

    如果手机卡销售的时候没有写入手机号,手机卡首次注册登记的时候,运营商会从HSS中取出手机号,然后再写入手机卡中。

    实际应用中,为了防止跟踪和攻击,不是每次通信时都要携带IMSI,MME会生成一个临时的GUTI对应到IMSI,就像Web程序中的SessionId。MME还有一定的机制控制GUIT的重新分配。

  • 2 传输:手机网络流量的传输,还是先要通过基站,然后下一步进入S-GW,S-GW会检查用户的授权,就像Web程序中检查前端提交过来的SessionId,再看看用户有没有权限进行其提交的业务,这里就是看看用户有没有开通流量上网,这是S-GW通过连接MME实现的。S-GW处理完毕后,数据包会进入P-GW,P-GW在手机使用流量上网时会给用户分配一个IP地址,然后数据包通过网关进入互联网,访问到相关的资源。P-GW还会对上网行为进行速率控制、流量计费等操作,这些策略来源于PCRF,PCRF中的规则是根据HSS中的用户套餐、用户等级等计算出来的。

    对P-GW来说S-GW屏蔽了用户的移动性,手机在多个基站切换时,S-GW不变。

以上就是手机流量上网的基本原理了,可以看到,运营商通过IMSI或者GUTI完全有能力获取到当前上网用户的手机号码。对于运营商的一键登录具体是怎么实现的,我并没有找到相关的介绍,但是可以设想下:手机应用通过运营商的SDK发起获取手机号码的业务请求,此时会携带IMSI或者GUTI,业务请求到达S-GW,S-GW鉴权通过,然后将这个业务请求路由到运营商核心网中获取手机号码的服务,服务根据业务规则从HSS中取出手机号码并进行若干处理。

一键登录的原理

理解了手机流量上网的原理,再来看下一键登录业务是如何实现的,这个部分属于上层应用程序开发,大家应该相对熟悉一些。

如果你接入过微信的第三方应用登录,或者其他类似的第三方应用登录,过程是差不多的。还是先来看图:


这里对一些关键步骤进行说明:

  • 2预取手机号掩码:这个手机号掩码需要在请求用户授权的页面展示给用户看,因为获取这个信息要通过电信运营商的网络,所以可能会比较慢,为了提升用户体验,可以在应用启动的时候就去获取,然后缓存一段时间。

  • 8授权请求:因为应用获取用户手机号这个事比较敏感,必须让用户清楚的了解并授权之后才能进行,为了确保这件事,运营商的认证SDK提供了这个授权请求页面,用户确认授权后,SDK直接向运营商认证服务发起请求认证,认证服务会返回一个认证Token给应用。应用再通过自己的服务端拿着这个Token找运营商获取手机号码。

  • 17生成应用授权Token:应用要维护自己用户的登录状态,这里可以采用传统的Session机制,也可以使用JWT机制。

  • 3预取手机号掩码 和 11请求认证,都需要通过手机蜂窝网络通信,也就说需要通过手机流量上网。如果手机同时开启了流量和WIFI,认证SDK会将手机短暂切换到流量上网模式。如果手机没有开启流量,有些SDK还会在上次成功取号之后多缓存一个临时Token,这样也能成功实现一次一键登录,不过这个限制性很大。

这里其实还有一个安全问题

14登录请求:用户如果随便造一个认证Token,然后就向应用服务提交请求,应用服务再向认证服务提交请求,这属于一种跨站攻击。虽然这个Token可以被阻止,但是不免浪费资源,给服务端带来压力。

这一点微信第三方应用登录做的比较好,用户登录前,应用服务端先生成一个随机数,然后应用前端向应用服务端提交时,带着这个随机数,应用服务端可以验证这个随机数。

号码验证场景

除了用于登录,运营商网关的这种取号能力,还可以用在验证手机号上,在某些关键业务上,比如支付过程中,要求用户输入本机手机号码或者其中的某几位,然后通过运营商认证服务验证手机号是否本机号码。

隐私保护问题

设备唯一标识问题

现在大家对隐私问题关注的越来越多了,经常会出现这种情况:你在某电商网站搜索了某个商品,然后访问其它网站时,都向你推荐这类商品的广告。还有一种感觉很恐怖的情况,你刚和某个人谈论了某件事,然后就在某个App上看到了关于这件事的推荐,有人猜测是App在偷听,不过基于目前的舆论和监督,偷听风险太大,这其中的原因可能真的只是算法太厉害了。

最近几年Android和iOS系统都对App获取手机唯一标识进行了限制,比如IMEI、Mac地址、序列号、广告Id等,目的就是防止用户的信息在多个App之间进行关联,导致泄漏用户的隐私,产生一些安全问题和法律风险,前述跨App的广告行为也自然受到了抑制。

在了解一键登录的技术原理时,看到某运营商提供了一种和SIM卡绑定的设备唯一Id服务,宣传语就是为了应对移动操作系统限制访问手机唯一标识的问题,在现今越来越重视隐私保护的前提下,如果这种能力开放给了广告平台,就是开历史的倒车了。

手机号作为身 份标识的问题

对于国内普遍使用手机号登录的方式,从技术上很难限制App之间进行手机号关联,然后综合分析用户的行为。比如某家大厂运营了多款不同种类的热门App,它就有能力更全面的了解某个用户,如果要限制可能就得通过法律层面来解决了。至于不同厂商之间的手机号关联行为,基于商业利益的保护,不太可能会出现。

在国内这种商业环境下,如果你真的对自己的隐私很关注,最好只使用账号密码的方式登录,否则经常更换手机号可能是一种没办法的办法。

手机号重新销售问题

手机号的总量是有限的,为了有效利用手机号资源,手机号注销以后,经过一段时间就会被运营商重新销售。如果新的手机号拥有者拿着这个手机号登录某个APP,而这个手机号之前已经在这个App上注册过,产生了大量的使用记录,那么此手机号前拥有者的隐私就会被泄漏。所以大家现在都不太敢随便更换手机号,因为注册过的地方太多了,留下了数不清的使用痕迹。

在了解一键登录的技术原理时,还看到某运营商提供了一种“手机号更换绑定SIM卡通知”的服务,应用可以据此解绑重新销售的手机号与应用账号之间的关系,从而保护用户的隐私。在上文中已经提过手机卡使用IMSI进行标识,如果手机号被重新销售,就会绑定新的IMSI,运营商可以据此产生通知。当然运营商还需要排除手机卡更换和携号转网的情况,这些情况下手机号也会绑定新的IMSI。

不得不说运营商的这个服务还是挺赞的👍。


作者:萤火架构
来源:https://juejin.cn/post/7059182505101885471

收起阅读 »

一些著名的软件都用什么语言编写?

1、操作系统Microsoft Windows :汇编 -> C -> C++备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。相...
继续阅读 »

1、操作系统

Microsoft Windows :汇编 -> C -> C++


备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。

相信很多朋友都知道Windows Vista,这个系统开发早期比尔盖茨想全部用C#写,但最终因为执行慢而放弃,结果之前无数软件工程师日夜劳作成果一夜之间被宣告作废。

Linux :C


Apple MacOS : 主要为C,部分为C++。

备注:之前用的语言比较杂,最早是汇编和Pascal。


Sun Solaris : C

HP-UX : C

Symbian OS : 汇编,主要为C++(诺基亚手机)

Google Android :2008 年推出:C语言(有传言说是用Java开发的操作系统,但最近刚推出原生的C语言SDK)

RIM BlackBerry OS 4.x :黑莓 C++

2、图形界面层

Microsoft Windows UI :C++

Apple MacOS UI (Aqua) : C++

Gnome (Linux图形界面之一,大脚): C和C++, 但主要是C

KDE (Linux图形界面): C++

3、桌面搜索工具

Google Desktop Search : C++


Microsoft Windows Desktop Search : C++

Beagle (Linux/Windows/UNIX 下): C# (基于开源的 .net : Mono)

4、办公软件

Microsoft Office :在 汇编 -> C -> 稳定在C++


Sun Open Office : 部分JAVA(对外接口),主要为C++ (开源,可下载其源代码)

Corel Office/WordPerfect Office : 1996年尝试过Java,次年被抛弃,重新回到C/C++

Adobe Systems Acrobat Reader/Distiller : C++

5、关系型数据库

Oracle : 汇编、C、C++、Java。主要为C++


MySQL : C++


IBM DB2 :汇编、C、C++,但主要为C


Microsoft SQL Server : 汇编 -> C->C++

IBM Informix : 汇编、C、C++,但主要为C

SAP DB/MaxDB : C++

6、Web Browsers/浏览器

Microsoft Internet Explorer : C++


Mozilla Firefox : C++


Netscape Navigator :The code of Netscape browser was written in C, and Netscape engineers, all bought to Java (see M. Cusumano book and article) redeveloped the browser using Java. It was too slow and abandoned. Mozilla, the next version, was later developed using C++.

Safari : (2003年1月发布)C++

Google Chrome : (2008的发布)C++


Sun HotJava : Java (死于1999年)

Opera : C++ (手机上占用率比较大)

Opera Mini : Opera Mini (2007) has a very funny architecture, and is indeed using both C++ and Java. The browser is split in two parts, an ultra thin (less than 100Kb) “viewer” client part and a server side responsible of rendering. The first uses Java and receives the page under the OBML format, the latter reuses classical Opera (C++) rendering engine plus Opera’s Small Screen Rendering, on the server. This allows Opera to penetrate various J2ME-enabled portable devices, such as phones, while preserving excellent response time. This comes obviously with a few sacrifices, for instance on JavaScript execution.

Mosaic : 鼻祖(已死) C 语言

7、邮件客户端

Microsoft Outlook : C++


IBM Lotus Notes : Java


Foxmail : Delphi


8、软件开发集成环境/IDE

Microsoft Visual Studio :C++


Eclipse : Java (其图形界面SWT基于C/C++)


Code::Blocks :C++


易语言:C++


火山中文:C++

火山移动:C++

9、虚拟机

Microsoft .Net CLR (.NET的虚拟机): C++


Java Virtual Machine (JVM) : Java 虚拟机:C++


10、ERP软件 (企业应用)

SAP mySAP ERP : C,后主要为“ABAP/4”语言

Oracle Peoplesoft : C++ -> Java


Oracle E-Business Suite : Java

11、商业智能(Business Intelligence )

Business Objects : C++

12、图形处理

Adobe Photoshop : C++


The GIMP : C

13、搜索引擎

Google : 汇编 与 C++,但主要为C++

14、著名网站

eBay : 2002年为C++,后主要迁至Java

facebook : C++ 和 PHP

This line is only about facebook, not its plugins. Plugins can be developed in many different technologies, thanks to facebook’s ORB/application server, Thrift. Thrift contains a compiler coded in C++. facebook people write about Thrift: “The multi-language code generation is well suited for search because it allows for application development in an efficient server side language (C++) and allows the Facebook PHP-based web application to make calls to the search service using Thrift PHP libraries.” Aside the use of C++, facebook has adopted a LAMP architecture.


阿里巴巴和淘宝:php->C++/Java(主要用)


15、游戏

汇编、C、C++

星际争霸、魔兽争霸、CS、帝国时代、跑跑卡丁车、传奇、魔兽世界… 数不胜数了,自己数吧


都是用C开发的,C语言靠近系统地称,执行速度最快。比如你的两个朋友与你分别玩用VB、Java、与C编写的“跑跑卡丁车”,你玩C编写的游戏已经跑玩结束了,发现你的两个朋友还没开始跑呢,那是相当的卡啊。

16、编译器

Microsoft Visual C++ 编译器: C++

Microsoft Visual Basic 解释、编译器:C++

Microsoft Visual C# :编译器: C++

gcc (GNU C compiler) : C

javac (Sun Java compiler) : Java

Perl : C++

PHP : C

17、3D引擎

Microsoft DirectX : C++


OpenGL : C


OGRE 3D : C++


18、Web Servers (网页服务)

Apache : C和C++,但主要为C


Microsoft IIS : C++

Tomcat : Java


Jboss : Java


19、邮件服务

Microsoft Exchange Server : C->C++

Postfix : C

hMailServer : C++

Apache James : Java

20、CD/DVD刻录

Nero Burning ROM : C++


K3B : C++

21、媒体播放器

Nullsoft Winamp : C++


Microsoft Windows Media Player : C++


22、Peer to Peer (P2P软件)

eMule : C++

μtorrent : C++

Azureus : Java (图形界面使用基于C/C++的SWT,类Eclipse)

23、全球定位系统(GPS)

TomTom : C++


Hertz NeverLost : C++

Garmin : C++

Motorola VIAMOTO : 2007年6月,停止服务,Java

24、3D引擎

Microsoft DirectX : C++(相信玩游戏的同学都知道这个,现在最高版本是DX11)

OpenGL : C

OGRE 3D : C++

25、服务器软件

Apache:C

Nginx:C


IIS:C

26、其它

OpenStack:Python


作者:土豆居士
来源:一口Linux

收起阅读 »

代码对比工具,我就用这6个

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。 支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通...
继续阅读 »

WinMerge

pic_2c55c38b.png

WinMerge是一款运行于Windows系统下的文件比较和合并工具,使用它可以非常方便地比较多个文档内容,适合程序员或者经常需要撰写文稿的朋友使用。

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。

Diffuse

pic_73d4bebc.png

Diffuse在命令行中的速度是相当快的,支持像 C++、Python、Java、XML 等语言的语法高亮显示。可视化比较,非常直观,支持两相比较和三相比较。这就是说,使用 Diffuse 你可以同时比较两个或三个文本文件。

支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通过 Diffuse 直接从版本控制系统获取源代码,以便对其进行比较和合并。

Beyond Compare

pic_45e693a5.png

Beyond Compare可以很方便地对比出两份源代码文件之间的不同之处,相差的每一个字节用颜色加以表示,查看方便,支持多种规则对比。

Beyond Compare选择最好的方法来突出不同之处,文本文件可以用语法高亮和设置比较规则的方法进行查看和编辑,适用于用于文档、源代码和HTML。

Altova DiffDog

pic_4afb57c3.png

pic_b3fd72aa.png

是一款用于文件、目录、数据库模式与表格对比与合并的使用工具。

这个强大易用的对比/合并工具可以让你通过其直观的可视化界面快速比较和合并文本或源代码文件,同步目录以及比较数据库模式与表格。DiffDog还提供了先进XML的差分和编辑功能。

AptDiff

pic_5bfb9a16.png

AptDiff是一个文件比较工具,可以对文本和二进制文件进行比较和合并,适用于软件开发、网络设计和其它的专业领域。

它使用方便,支持键盘快捷键,可以同步进行横向和纵向卷动,支持Unicode格式和大于4GB的文件,可以生成HTML格式的比较报告。

Code Compare

pic_1ff7b983.png

Code Compare是一款用于程序代码文件的比较工具,目前Code Compare支持的对比语言有:C#、C++、CSS、HTML、Java、JavaScrip等代码语言。

Code Compare的运行环境为Visual Studio,而Visual Studio可以方便所有的程序开发设计。

作者:小白学视觉
来源:https://mp.weixin.qq.com/s/I5__jnuDAIJlWbCHJsPDRQ

收起阅读 »

Java之父独家专访:我可太想简化一下 Java了

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Ja...
继续阅读 »

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Java 语言将如何发展?在亚马逊云科技 re:Invent 十周年之际,InfoQ 有幸对 Java 父 James Gosling 博士进行了一次独家专访。James Gosling 于 2017 年作为“杰出工程师”加入亚马逊云科技,负责为产品规划和产品发布之类的工作提供咨询支持,并开发了不少原型设计方案。在本次采访中,James Gosling 谈到了 Java 的诞生与发展、他对众多编程语言的看法、编程语言的未来发展趋势以及云计算带来的改变等问题。


Java 的诞生与发展

InfoQ:Java 语言是如何诞生的?是什么激发您创建一门全新的语言?

James Gosling:Java 的诞生其实源于物联网的兴起。当时,我在 Sun 公司工作,同事们都觉得嵌入式设备很有发展前景,而且随着设备数量的激增,整个世界正逐渐向智能化的方向发展。我们投入大量时间与不同行业的从业者进行交流,也拜访了众多东南亚、欧洲的从业者,结合交流心得和行业面临的问题,决定构建一套设计原型。正是在这套原型的构建过程中,我们深刻地意识到当时主流的语言 C++ 存在问题。

最初,我们只打算对 C++ 做出一点小调整,但随着工作的推进、一切很快“失控”了。我们构建出不少非常有趣的设备原型,也从中得到了重要启示。因此,我们及时对方向进行调整,希望设计出某种适用于主流业务和企业计算的解决方案,这正是一切故事的开端。

InfoQ:Java 作为一门盛行不衰的语言,直到现在依旧稳居编程语言的前列,其生命力何在?

James Gosling:Java 得以拥有顽强的生命力背后有诸多原因。

首先,采用 Java 能够非常便捷地进行多线程编程,能大大提升开发者的工作效率。

其次,Java 提供多种内置安全功能,能够帮助开发者及时发现错误、更加易于调试,此外,各种审查机制能够帮助开发者有效识别问题。

第三,热修复补丁功能也非常重要,亚马逊开发者开发出的热补丁修复程序,能够在无须停机的前提下修复正在运行的程序,这是 Java 中非常独特的功能。

第四,Java 拥有很好的内存管理机制,自动垃圾收集大大降低了内存泄露或者双重使用问题的几率。总之,Java 的设计特性确实提升了应用程序的健壮性,特别是极为强大的现代垃圾收集器方案。如果大家用过最新的长期支持版本 JDK17,应该对其出色的垃圾收集器印象深刻。新版本提供多种强大的垃圾收集器,适配多种不同负载使用。另外,现代垃圾收集器停顿时间很短、运行时的资源消耗也非常低。如今,很多用户会使用体量极为庞大的数据结构,而只要内存能容得下这种 TB 级别的数据,Java 就能以极快的速度完成庞大数据结构的构建。

InfoQ:Java 的版本一直以来更新得比较快,几个月前发布了最新的 Java17 版本,但 Java8 仍然是开发人员使用的主要版本,新版本并未“得宠”,您认为主要的原因是什么?

James Gosling:对继续坚守 Java8 的朋友,我想说“是时候作出改变了”。新系统全方位性更强、速度更快、错误也更少、扩展效率更高。无论从哪个角度看,大家都有理由接纳 JDK17。确实,大家在从 JDK8 升级到 JDK9 时会遇到一个小问题,这也是 Java 发展史中几乎唯一一次真正重大的版本更替。大多数情况下,Java 新旧版本更替都非常简单。只需要直接安装新版本,一切就能照常运作。长久以来,稳定、非破坏性的升级一直是 Java 的招牌特性之一,我们也不希望破坏这种良好的印象。

InfoQ:回顾当初,你觉得 Java 设计最成功的点是什么?相对不太满意的地方是什么?

James Gosling:这其实是一种博弈。真正重要的是 Java 能不能以更便利的方式完成任务。我们没办法设想,如果放弃某些问题域,Java 会不会变得更好?或者说,如果我现在重做 Java,在取舍上会有不同吗?区别肯定会有,但我估计我的取舍可能跟大多数人都不一样,毕竟我的编程风格也跟多数人不一样。不过总的来讲,Java 确实还有改进空间。

InfoQ:有没有考虑简化一下 Java?

James Gosling:我可太想简化一下 Java 了。毕竟简化的意义就是放下包袱、轻装上阵。所以 JavaScript 刚出现时,宣传的就是精简版 Java。但后来人们觉得 JavaScript 速度太慢了。在 JavaScript 的早期版本中,大家只是用来执行外部验证之类的简单事务,所以速度还不太重要。但在人们打算用 JavaScript 开发高性能应用时,得出的解决方案就成了 TypeScript。其实我一直觉得 TypeScript 的定位有点搞笑——JavaScript 就是去掉了 Type 的 Java,而 TypeScript 在 JavaScript 的基础上又把 type 加了回来。Type 系统有很多优势,特别是能让系统运行得更快,但也确实拉高了软件开发者的学习门槛。但如果你想成为一名专业的软件开发者,那最好能克服对于学习的恐惧心理。

Java 之父的编程语言之见

InfoQ:一款优秀的现代化编程语言应该是怎样的?当下最欣赏哪一种编程语言的设计理念?

James Gosling:我个人还是会用最简单的评判标准即这种语言能不能改善开发者的日常工作和生活。我尝试过很多语言,哪种更好主要取决于我想干什么。如果我正要编写低级设备驱动程序,那我可能倾向于选择 Rust。但如果需要编写的是用来为自动驾驶汽车建立复杂数据结构的大型导航系统,那我几乎肯定会选择 Java。

InfoQ:数据科学近两年非常热门,众所周知,R 语言和 Python 是数据科学领域最受欢迎的两门编程语言,那么,这两门语言的发展前景怎么样?因具体的应用领域产生专用的编程语言,会是接下来编程语言领域的趋势之一吗?

James Gosling:我是领域特定语言的铁粉,也深切认同这些语言在特定领域中的出色表现。大多数领域特定语言的问题是,它们只能在与世隔绝的某一领域中发挥作用,而无法跨越多个领域。这时候大家更愿意选择 Java 这类语言,它虽然没有针对任何特定领域作出优化,但却能在跨领域时表现良好。所以,如果大家要做的是任何形式的跨领域编程,肯定希望单一语言就能满足所有需求。有时候,大家也会尝试其他一些手段,希望在两种不同的领域特定语言之间架起一道桥梁,但一旦涉及两种以上的语言,我们的头脑通常就很难兼顾了。

InfoQ:Rust 一直致力于解决高并发和高安全性系统问题,这也确实符合当下绝大部分应用场景的需求,对于 Rust 语言的现在和未来您怎么看?

James Gosling:在我看来,Rust 太过关注安全了,这让它出了名的难学。Rust 解决问题的过程就像是证明定理,一步也不能出错。如果我们只需要编写一小段代码,用于某种固定不变的设备,那 Rust 的效果非常好。但如果大家需要构建一套具有高复杂度动态数据结构的大规模系统,那么 Rust 的使用难度就太高了。

编程语言的学习和发展

InfoQ:编程语言倾向于往更加低门槛的方向发展,开发者也更愿意选择学习门槛低的开发语言,一旦一门语言的学习成本过高,开发者可能就不愿意去选择了。对于这样的现象,您怎么看?

James Gosling:要具体问题具体分析。我到底需要 Rust 中的哪些功能特性?我又需要 Java 中的哪些功能特性?很多人更喜欢 Python,因为它的学习门槛真的很低。但跑跑基准测试,我们就会发现跟 Rust 和 Java 相比,Python 的性能实在太差了。如果关注性能,那 Rust 或 Java 才是正确答案。另外,如果你需要的是只有 Rust 能够提供的那种致密、安全、严谨的特性,代码的编写体量不大,而且一旦出问题会造成严重后果,那 Rust 就是比较合适的选择。只能说某些场景下某些语言更合适。Java 就属于比较折衷的语言,虽然不像 Python 那么好学,但也肯定不算难学。

InfoQ:当前,软件项目越来越倾向采用多语言开发,对程序员的要求也越来越高。一名开发人员,应该至少掌握几种语言?最应该熟悉和理解哪些编程语言?

James Gosling:我刚刚入行时,市面上已经有很多语言了。我学了不少语言,大概有几十种吧。但很多语言的诞生本身就很荒谬、很没必要。很多语言就是同一种语言的不同方言,因为它们只是在用不同的方式实现基本相同的语言定义。最让我振奋的是我生活在一个能够致力于解决问题的世界当中。Java 最大的吸引力也正在于此,它能帮助我们解决几乎任何问题。具有普适性的语言地位高些、只适用于特定场景的语言则地位低些,对吧?所以到底该学什么语言,取决于你想解决什么问题、完成哪些任务。明确想要解决什么样的问题才是关键。

InfoQ:2021 年,技术圈最热门的概念非元宇宙莫属,您认为随着元宇宙时代的到来,新的应用场景是否会对编程语言有新的需求?可否谈谈您对未来编程语言的理解?

James Gosling:其实人们从很早开始就在构建这类虚拟世界系统了,所以我觉得元宇宙概念对编程不会有什么影响。唯一的区别是未来我们可以漫步在这些 3D 环境当中,类似于大型多人游戏那种形式。其实《我的世界》就是用户构建型元宇宙的雏形嘛,所以这里并没有什么真正新鲜的东西,仍然是游戏粉加上社交互动机制的组合。我还想强调一点,虚拟现实其实没什么意思。我更重视与真实人类的面对面互动,真的很难想象自己有一天会跟独角兽之类的虚拟形象聊天。

写在最后:云计算带来的改变

InfoQ:您最初是从什么时候或者什么具体事件开始感受到云计算时代的到来的?

James Gosling:云计算概念的出现要远早出云计算的真正实现。因为人们一直把计算机摆在大机房里,再通过网络连接来访问,这其实就是传统的 IT 服务器机房,但这类方案维护成本高、建造成本高、扩展成本也高,而且对于人员技能等等都有着很高的要求。如果非要说,我觉得多租户云的出现正是云计算迎来飞跃的关键,这时候所有的人力与资本支出都由云服务商负责处理,企业客户再也不用为此烦心了。他们可以单纯关注自己的业务重心,告别那些没完没了又没有任何差异性可言的繁重工作。

InfoQ:云计算如今已经形成巨大的行业和生态,背后的根本驱动力是什么?

James Gosling:云计算的驱动力实际上与客户当前任务的实际规模有很大关系。过去几年以来,数字化转型已经全面掀起浪潮,而这波转型浪潮也凸显出新的事实,即我们还有更多的探索空间和机遇,例如,现在人们才刚刚开始探索真正的机器学习能做些什么,能够以越来越有趣且多样的方法处理大规模数据,开展数据分析,获取洞见并据此做出决策,而这一切既是客户需求,也为我们指明了接下来的前进方向。亚马逊云科技做为云科技领导者,引领着云科技的发展,改变着 IT 世界,切实解决了企业客户的诸多痛点。

作者:张雅文
来源:https://mp.weixin.qq.com/s/B4_YaVrnltm54aV4cW1XpA

收起阅读 »

这才是Yaml的语法精髓, 不要再只有字符串了

文章目录什么是YAML基本语法数据类型标量对象数组文本块显示指定类型引用单文件多配置什么是YAMLYAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Ma...
继续阅读 »

文章目录

  • 什么是YAML

  • 基本语法

  • 数据类型

    • 标量

    • 对象

    • 数组

  • 文本块

  • 显示指定类型

  • 引用

  • 单文件多配置

什么是YAML

YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。主要强度这种语音是以数据为中心,而不是以标记语音为重心,例如像xml语言就会使用大量的标记。

YAML是一个可读性高,易于理解,用来表达数据序列化的格式。它的语法和其他高级语言类似,并且可以简单表达清单(数组)、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件等。

YAML的配置文件后缀为 .yml,例如Springboot项目中使用到的配置文件 application.yml

基本语法

  • YAML使用可打印的Unicode字符,可使用UTF-8或UTF-16。

  • 数据结构采用键值对的形式,即 键名称: 值,注意冒号后面要有空格。

  • 每个清单(数组)成员以单行表示,并用短杠+空白(- )起始。或使用方括号([]),并用逗号+空白(, )分开成员。

  • 每个散列表的成员用冒号+空白(: )分开键值和内容。或使用大括号({ }),并用逗号+空白(, )分开。

  • 字符串值一般不使用引号,必要时可使用,使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。

  • 大小写敏感

  • 使用缩进表示层级关系,缩进不允许使用tab,只允许空格,因为有可能在不同系统下tab长度不一样

  • 缩进的空格数可以任意,只要相同层级的元素左对齐即可

  • 在单一文件中,可用连续三个连字号(—)区分多个文件。还有选择性的连续三个点号(…)用来表示文件结尾。

  • '#'表示注释,可以出现在一行中的任何位置,单行注释

  • 在使用逗号及冒号时,后面都必须接一个空白字符,所以可以在字符串或数值中自由加入分隔符号(例如:5,280或http://www.wikipedia.org)而不需要使用引号。

数据类型

  • 纯量(scalars):单个的、不可再分的值

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)

标量

标量是最基础的数据类型,不可再分的值,他们一般用于表示单个的变量,有以下七种:

  1. 字符串

  2. 布尔值

  3. 整数

  4. 浮点数

  5. Null

  6. 时间

  7. 日期

# 字符串
string.value: Hello!我是陈皮!
# 布尔值,true或false
boolean.value: true
boolean.value1: false
# 整数
int.value: 10
int.value1: 0b1010_0111_0100_1010_1110 # 二进制
# 浮点数
float.value: 3.14159
float.value1: 314159e-5 # 科学计数法
# Null,~代表null
null.value: ~
# 时间,时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
datetime.value: !!timestamp 2021-04-13T10:31:00+08:00
# 日期,日期必须使用ISO 8601格式,即yyyy-MM-dd
date.value: !!timestamp 2021-04-13

这样,我们就可以在程序中引入了,如下:

@RestController
@RequestMapping("demo")
public class PropConfig {
   
   @Value("${string.value}")
   private String stringValue;

   @Value("${boolean.value}")
   private boolean booleanValue;

   @Value("${boolean.value1}")
   private boolean booleanValue1;

   @Value("${int.value}")
   private int intValue;

   @Value("${int.value1}")
   private int intValue1;

   @Value("${float.value}")
   private float floatValue;

   @Value("${float.value1}")
   private float floatValue1;

   @Value("${null.value}")
   private String nullValue;

   @Value("${datetime.value}")
   private Date datetimeValue;

   @Value("${date.value}")
   private Date datevalue;
}

对象

我们知道单个变量可以用键值对,使用冒号结构表示 key: value,注意冒号后面要加一个空格。可以使用缩进层级的键值对表示一个对象,如下所示:

person:
 name: 陈皮
 age: 18
 man: true

然后在程序对这几个属性进行赋值到Person对象中,注意Person类要加get/set方法,不然属性会无法正确取到配置文件的值。使用@ConfigurationProperties注入对象,@value不能很好的解析复杂对象。

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "my.person")
@Getter
@Setter
public class Person {
   private String name;
   private int age;
   private boolean man;
}

当然也可以使用 key:{key1: value1, key2: value2, ...}的形式,如下:

person: {name: 陈皮, age: 18, man: true}

数组

可以用短横杆加空格 -开头的行组成数组的每一个元素,如下的address字段:

person:
 name: 陈皮
 age: 18
 man: true
 address:
   - 深圳
   - 北京
   - 广州

也可以使用中括号进行行内显示形式,如下:

person:
 name: 陈皮
 age: 18
 man: true
 address: [深圳, 北京, 广州]

在代码中引入方式如下:

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {
 
   
   
   private String name;
   private int age;
   private boolean man;
   private List<String> address;
}

如果数组字段的成员也是一个数组,可以使用嵌套的形式,如下:

person:
name: 陈皮
age: 18
man: true
address: [深圳, 北京, 广州]
twoArr:
-
- 2
- 3
- 1
-
- 10
- 12
- 30
package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {



private String name;
private int age;
private boolean man;
private List<String> address;
private List<List<Integer>> twoArr;
}

如果数组成员是一个对象,则用如下两种形式形式:

childs:
-
name: 小红
age: 10
-
name: 小王
age: 15
childs: [{name: 小红, age: 10}, {name: 小王, age: 15}]

文本块

如果你想引入多行的文本块,可以使用|符号,注意在冒号:|符号之间要有空格。

person:
name: |
Hello Java!!
I am fine!
Thanks! GoodBye!

它和加双引号的效果一样,双引号能转义特殊字符:

person:
name: "Hello Java!!\nI am fine!\nThanks! GoodBye!"

显示指定类型

有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。!单叹号通常是自定义类型,!!双叹号是内置类型,例如:

# 指定为字符串
string.value: !!str HelloWorld!
# !!timestamp指定为日期时间类型
datetime.value: !!timestamp 2021-04-13T02:31:00+08:00

内置的类型如下:

  • !!int:整数类型

  • !!float:浮点类型

  • !!bool:布尔类型

  • !!str:字符串类型

  • !!binary:二进制类型

  • !!timestamp:日期时间类型

  • !!null:空值

  • !!set:集合类型

  • !!omap,!!pairs:键值列表或对象列表

  • !!seq:序列

  • !!map:散列表类型

引用

引用会用到 &锚点符合和 *星号符号,&用来建立锚点,<< 表示合并到当前数据,* 用来引用锚点。

xiaohong: &xiaohong
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
<<: *xiaohong

上面最终相当于如下:

xiaohong:
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
name: 小红
age: 20

还有一种文件内引用,引用已经定义好的变量,如下:

base.host: https://chenpi.com
add.person.url: ${base.host}/person/add

单文件多配置

可以在同一个文件中,实现多文档分区,即多配置。在一个yml文件中,通过 — 分隔多个不同配置,根据spring.profiles.active 的值来决定启用哪个配置

#公共配置
spring:
profiles:
active: pro # 指定使用哪个文档块
---
#开发环境配置
spring:
profiles: dev # profiles属性代表配置的名称

server:
port: 8080
---
#生产环境配置
spring:
profiles: pro

server:
port: 8081

作者:陈皮的JavaLib
来源:https://blog.csdn.net/chenlixiao007/article/details/115654824

收起阅读 »

掉了两根头发,可算是把volatile整明白了

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底...
继续阅读 »

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解

相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点

先说它的两个作用:

  • 保证变量在内存中对线程的可见性

  • 禁用指令重排

每个字都认识,凑在一起就麻了

这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile

关于可见性

不多bb,码来

public class VolatileTest {
   private static volatile int count = 0;
   
   private static void increase() {
   count++;
  }

   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           new Thread(() -> {
               for (int j = 0; j < 10000; j++) {
                   increase();
              }
          }).start();
      }
       // 所有线程累加完成后输出
       while (Thread.activeCount() > 2) Thread.yield();
       System.out.println(count);
  }
}

代码很好理解,开了十个线程对同一个共享变量count做累加,每个线程累加1w次

count我们已经用volatile修饰,已经保证了count对十个线程在内存中的可见性,按理说十个线程执行完毕count的值应该10w

然鹅,运行多次,结果都远小于期望值


是哪个环节出了问题?


你肯定听过一句话:volatile只保证可见性,不保证原子性

这句话就是答案,但是依旧很多人没搞懂其中的奥秘

说来话长我长话短说,简单来讲就是 count++这个操作不是原子的,它是分三步进行

  1. 从内存读取 count 的值

  2. 执行 count + 1

  3. 将 count 的新值写回

要彻底搞懂这个问题,我们得从字节码入手

下面是increase方法编译后的字节码


看不懂没关系,我们一行一行来看:

  1. GETSTATIC:读取 count 的当前值

  2. ICONST_1:将常量 1 加载到栈顶

  3. IADD:执行+1

  4. PUTSTATIC:写入count最新值

ICONST_1和IADD其实就是真正的++操作

关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖

懂我意思吗

所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全

就如上面的demo,稍加修改就能实现真正的线程安全

最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理)

private synchronized static void increase() {
   ++count;
}

run几下


这不就妥了嘛

到现在,对于以下两点你应该有了新的认知

  1. volatile保证变量在内存中对线程的可见性

  2. volatile只保证可见性,不保证原子性

关于指令重排

并发编程中,cpu自身和虚拟机为了提高执行效率,都会采用指令重排(在保证不影响结果的前提下,将某些代码乱序执行)

  1. 关于cpu:为了从分利用cpu,实际执行指令时会做优化;

  2. 关于虚拟机:在HotSpot vm中,为了提升执行效率,JIT(即时编译)模式也会做指令优化

指令重排在大部分场景下确实能提升执行效率,但有些场景对代码执行顺序是强依赖的,此时我们需要禁用指令重排,如下面这个场景


伪代码取自《深入理解Java虚拟机》:

其描述的场景是开发中常见配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。
试想一下,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile通过禁止指令重排则可以避免此类情况发生

禁用指令重排只需要将变量声明为volatile,是不是很神奇

我们来看看volatile是如何实现禁用指令重排的

也借用《深入理解Java虚拟机》的一个例子吧,比较好理解


这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值


可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话

最后,留一个能加深大家对volatile理解的问题,兄弟们好好思考下:

Java代码明明是从上往下依次执行,为什么会出现指令重排这个问题?

ok我话说完
————————————————
作者:负债程序猿
来源:https://blog.csdn.net/qq_33709582/article/details/122415754

收起阅读 »

什么样的问题应该使用动态规划

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:你对动态规划相关问题的套路和思想还没有完全...
继续阅读 »

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:

  • 你对动态规划相关问题的套路和思想还没有完全掌握;

  • 你没有系统地总结过究竟有哪些问题可以用动态规划解决。

知己知彼,你想把动态规划作为你的面试武器之一,就得足够了解它;而应对面试,总结、归类问题其实是个不错的选择,这在我们刷题的时候其实也能感觉得到。那么,我们就针对以上两点,系统地谈一谈究竟什么样的问题可以用动态规划来解。

一、动态规划是一种思想

动态规划算法,这种叫法我想你应该经常听说。嗯,从道理上讲这么叫我觉得也没错,首先动态规划它不是数据结构,这一点毋庸置疑,并且严格意义上来说它就是一种算法。但更加准确或者更加贴切的提法应该是说动态规划是一种思想。那算法和思想又有什么区别呢?

一般来说,我们都会把算法和数据结构放一起来讲,这是因为它们之间密切相关,而算法也往往是在特定数据结构的基础之上对解题方案的一种严谨的总结。

比如说,在一个乱序数组的基础上进行排序,这里的数据结构指的是什么呢?很显然是数组,而算法则是所谓的排序。至于排序算法,你可以考虑使用简单的冒泡排序或效率更高的快速排序方法等等来解决问题。

没错,你应该也感觉到了,算法是一种简单的经验总结和套路。那什么是思想呢?相较于算法,思想更多的是指导你我来解决问题。

比如说,在解决一个复杂问题的时候,我们可以先将问题简化,先解决简单的问题,再解决难的问题,那么这就是一种指导解决问题的思想。另外,我们常说的分治也是一种简单的思想,当然它在诸如归并排序或递归算法当中会常常被提及。

而动态规划就是这样一个指导我们解决问题的思想:你需要利用已经计算好的结果来推导你的计算,即大规模问题的结果是由小规模问题的结果运算得来的

总结一下:算法是一种经验总结,而思想则是用来指导我们解决问题的。既然动态规划是一种思想,那它实际上就是一个比较抽象的概念了,也很难和实际的问题关联起来。所以说,弄清楚什么样的问题可以使用动态规划来解就显得十分重要了。

二、动态规划问题的特点

动态规划作为运筹学上的一种最优化解题方法,在算法问题上已经得到广泛应用。接下来我们就来看一下动归问题所具备的一些特点。

2.1 最优解问题

除非你碰到的问题是简单到找出一个数组中最大的值这样,对这种问题来说,你可以对数组进行排序,然后取数组头或尾部的元素,如果觉得麻烦,你也可以直接遍历得到最值。不然的话,你就得考虑使用动态规划来解决这个问题了。这样的问题一般都会让你求最大子数组、求最长递增子数组、求最长递增子序列或求最长公共子串、子序列等等。

如果碰到求最值问题,我们可以使用下面的套路来解决问题:

  • 优先考虑使用贪心算法的可能性;

  • 然后是暴力递归进行穷举,针对数据规模不大的情况;

  • 如果上面两种都不适合,那么再选择动态规划。

可以看到,求解动态规划的核心问题其实就是穷举。当然了,动态规划问题也不会这么简单了事,我们还需要考虑待解决的问题是否存在重叠子问题、最优子结构等特性。

清楚了动态规划算法的特点,接下来我们就来看一下哪些问题适合用动态规划思想来解题。

1. 乘积最大子数组

给你一个整数数组 numbers,找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回该子数组的乘积。

示例1:
输入: [2,7,-2,4]
输出: 14
解释: 子数组 [2,7] 有最大乘积 14。


示例2:
输入: [-5,0,3,-1]
输出: 3
解释: 结果不能为 15, 因为 [-5,3,-1] 不是子数组,是子序列。

首先,很明显这个题目当中包含一个“最”字,使用动态规划求解的概率就很大。这个问题的目的就是从数组中寻找一个最大的连续区间,确保这个区间的乘积最大。由于每个连续区间可以划分成两个更小的连续区间,而且大的连续区间的结果是两个小连续区间的乘积,因此这个问题还是求解满足条件的最大值,同样可以进行问题分解,而且属于求最值问题。同时,这个问题与求最大连续子序列和比较相似,唯一的区别就是你需要在这个问题里考虑正负号的问题,其它就相同了。

对应实现代码:

class Solution {
public:
   int maxProduct(vector<int>& nums) {
       if(nums.empty()) return 0;

       int curMax = nums[0];
       int curMin = nums[0];
       int maxPro = nums[0];
       for(int i=1; i<nums.size(); i++){
           int temp = curMax;    // 因为curMax在下一行可能会被更新,所以保存下来
           curMax = max(max(curMax*nums[i], nums[i]), curMin*nums[i]);
           curMin = min(min(curMin*nums[i], nums[i]), temp*nums[i]);
           maxPro = max(curMax, maxPro);
       }
       return maxPro;
   }
};

2. 最长回文子串

问题:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例1:
输入: "babad"
输出: "bab"


示例2:
输入: "cbbd"
输出: "bb"

【回文串】是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。这个问题依然包含一个“最”字,同样由于求解的最长回文子串肯定包含一个更短的回文子串,因此我们依然可以使用动态规划来求解这个问题。

对应实现代码:

class Solution {
      public boolean isPalindrome(String s, int b, int e){//判断s[b...e]是否为回文字符串
      int i = b, j = e;
      while(i <= j){
          if(s.charAt(i) != s.charAt(j)) return false;
          ++i;
          --j;
      }
      return true;
  }
  public String longestPalindrome(String s) {
      if(s.length() <=1){
          return s;
      }
      int l = 1, j = 0, ll = 1;
      for(int i = 1; i < s.length(); ++i){
            //下面这个if语句就是用来维持循环不变式,即ll恒表示:以第i个字符为尾的最长回文子串的长度
            if(i - 1 - ll >= 0 && s.charAt(i) == s.charAt(i-1-ll)) ll += 2;
            else{
                while(true){//重新确定以i为边界,最长的回文字串长度。确认范围为从ll+1到1
                    if(ll == 0||isPalindrome(s,i-ll,i)){
                        ++ll;
                        break;
                    }
                    --ll;
                }
            }
            if(ll > l){//更新最长回文子串信息
              l = ll;
              j = i;
          }
      }
      return s.substring(j-l+1, j+1);//返回从j-l+1到j长度为l的子串
  }
}

3. 最长上升子序列

问题:给定一个无序的整数数组,找到其中最长上升子序列的长度。可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

示例:
输入: [10,9,2,5,3,7,66,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,66],它的长度是 4。

这个问题依然是一个最优解问题,假设我们要求一个长度为 5 的字符串中的上升自序列,我们只需要知道长度为 4 的字符串最长上升子序列是多长,就可以根据剩下的数字确定最后的结果。
对应实现代码:

class Solution {
   public int lengthOfLIS(int[] nums) {
       if(nums.length == 0) return 0;
       int[] dp = new int[nums.length];
       int res = 0;
       Arrays.fill(dp, 1);
       for(int i = 0; i < nums.length; i++) {
           for(int j = 0; j < i; j++) {
               if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
           }
           res = Math.max(res, dp[i]);
       }
       return res;
   }
}

2.2 求可行性

如果有这样一个问题,让你判断是否存在一条总和为 x 的路径(如果找到了,就是 True;如果找不到,自然就是 False),或者让你判断能否找到一条符合某种条件的路径,那么这类问题都可以归纳为求可行性问题,并且可以使用动态规划来解。

1. 凑零兑换问题

问题:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

示例1:
输入: c1=1, c2=2, c3=5, c4=7, amount = 15
输出: 3
解释: 11 = 7 + 7 + 1。


示例2:
输入: c1=3, amount =7
输出: -1
解释: 3怎么也凑不到7这个值。

这个问题显而易见,如果不可能凑出我们需要的金额(即 amount),最后算法需要返回 -1,否则输出可能的硬币数量。这是一个典型的求可行性的动态规划问题。

对于示例代码:

class Solution {
   public int coinChange(int[] coins, int amount) {
       if(coins.length == 0)
           return -1;
       //声明一个amount+1长度的数组dp,代表各个价值的钱包,第0个钱包可以容纳的总价值为0,其它全部初始化为无穷大
       //dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数
       int[] dp = new int[amount+1];
       Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
       for (int coin : coins) {
           for (int j = coin; j <= amount; j++) {
               if(dp[j-coin] != Integer.MAX_VALUE) {
                   dp[j] = Math.min(dp[j], dp[j-coin]+1);
              }
          }
      }
       if(dp[amount] != Integer.MAX_VALUE)
           return dp[amount];
       return -1;
  }
}

2. 字符串交错组成问题

问题:给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

示例1:
输入: s1="aabcc",s2 ="dbbca",s3="aadbbcbcac"
输出: true
解释: 可以交错组成。


示例2:
输入: s1="aabcc",s2="dbbca",s3="aadbbbaccc"
输出: false
解释:无法交错组成。

这个问题稍微有点复杂,但是我们依然可以通过子问题的视角,首先求解 s1 中某个长度的子字符串是否由 s2 和 s3 的子字符串交错组成,直到求解整个 s1 的长度为止,也可以看成一个包含子问题的最值问题。
对应示例代码:

class Solution {
  public boolean isInterleave(String s1, String s2, String s3) {
      int length = s3.length();
      // 特殊情况处理
      if(s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return true;
      if(s1.isEmpty()) return s2.equals(s3);
      if(s2.isEmpty()) return s1.equals(s3);
      if(s1.length() + s2.length() != length) return false;

      int[][] dp = new int[s2.length()+1][s1.length()+1];
      // 边界赋值
      for(int i = 1;i < s1.length()+1;i++){
          if(s1.substring(0,i).equals(s3.substring(0,i))){
              dp[0][i] = 1;
          }
      }
      for(int i = 1;i < s2.length()+1;i++){
          if(s2.substring(0,i).equals(s3.substring(0,i))){
              dp[i][0] = 1;
          }
      }
       
      for(int i = 2;i <= length;i++){
          // 遍历 i 的所有组成(边界除外)
          for(int j = 1;j < i;j++){
              // 防止越界
              if(s1.length() >= j && i-j <= s2.length()){
                  if(s1.charAt(j-1) == s3.charAt(i-1) && dp[i-j][j-1] == 1){
                      dp[i-j][j] = 1;
                  }
              }
              // 防止越界
              if(s2.length() >= j && i-j <= s1.length()){
                  if(s2.charAt(j-1) == s3.charAt(i-1) && dp[j-1][i-j] == 1){
                      dp[j][i-j] = 1;
                  }
              }
          }
      }
      return dp[s2.length()][s1.length()]==1;
  }
}

2.3 求总数

除了求最值与可行性之外,求方案总数也是比较常见的一类动态规划问题。比如说给定一个数据结构和限定条件,让你计算出一个方案的所有可能的路径,那么这种问题就属于求方案总数的问题。

1. 硬币组合问题

问题:英国的英镑硬币有 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), 和 £2 (200p)。比如我们可以用以下方式来组成 2 英镑:1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p。问题是一共有多少种方式可以组成 n 英镑? 注意不能有重复,比如 1 英镑 +2 个 50P 和 50P+50P+1 英镑是一样的。

示例1:
输入: 2
输出: 73682

这个问题本质还是求满足条件的组合,只不过这里不需要求出具体的值或者说组合,只需要计算出组合的数量即可。

public class Main {
  public static void main(String[] args) throws Exception {
       
      Scanner sc = new Scanner(System.in);
      while (sc.hasNext()) {
           
          int n = sc.nextInt();
          int coin[] = { 1, 5, 10, 20, 50, 100 };
           
          // dp[i][j]表示用前i种硬币凑成j元的组合数
          long[][] dp = new long[7][n + 1];
           
          for (int i = 1; i <= n; i++) {
              dp[0][i] = 0; // 用0种硬币凑成i元的组合数为0
          }
           
          for (int i = 0; i <= 6; i++) {
              dp[i][0] = 1; // 用i种硬币凑成0元的组合数为1,所有硬币均为0个即可
          }
           
          for (int i = 1; i <= 6; i++) {
               
              for (int j = 1; j <= n; j++) {
                   
                  dp[i][j] = 0;
                  for (int k = 0; k <= j / coin[i - 1]; k++) {
                       
                      dp[i][j] += dp[i - 1][j - k * coin[i - 1]];
                  }
              }
          }
           
          System.out.print(dp[6][n]);
      }
      sc.close();
  }
}

2. 路径规划问题

问题:一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,共有多少路径?

示例1:
输入: 2 2
输出: 2


示例1:
输入: 3 3
输出: 6

这个问题还是一个求满足条件的组合数量的问题,只不过这里的组合变成了路径的组合。我们可以先求出长宽更小的网格中的所有路径,然后再在一个更大的网格内求解更多的组合。这和硬币组合的问题相比没有什么本质区别。

这里有一个规律或者说现象需要强调,那就是求方案总数的动态规划问题一般都指的是求“一个”方案的所有具体形式。如果是求“所有”方案的具体形式,那这种肯定不是动态规划问题,而是使用传统递归来遍历出所有方案的具体形式。

为什么这么说呢?因为你需要把所有情况枚举出来,大多情况下根本就没有重叠子问题给你优化。即便有,你也只能使用备忘录对遍历进行一个简单加速。但本质上,这类问题不是动态规划问题。

对应示例代码:

package com.qst.Tesst;

import java.util.Scanner;

public class Test12 {
  public static void main(String[] args) {
      Scanner scanner = new Scanner(System.in);
      while (scanner.hasNext()) {
          int x = scanner.nextInt();
          int y = scanner.nextInt();

          //设置路径
          long[][] path = new long[x + 1][y + 1];
          //设置领导数量
          int n = scanner.nextInt();

          //领导位置
          for (int i = 0; i < n; i++) {
              int a = scanner.nextInt();
              int b = scanner.nextInt();
              path[a][b] = -1;
          }

          for (int i = 0; i <= x; i++) {
              path[i][0] = 1;
          }
          for (int j = 0; j <= y; j++) {
              path[0][j] = 1;
          }

          for (int i = 1; i <= x; i++) {
              for (int j = 1; j <= y; j++) {
                  if (path[i][j] == -1) {
                      path[i][j] = 0;
                  } else {
                      path[i][j] = path[i - 1][j] + path[i][j - 1];
                  }

              }

          }
          System.out.println(path[x][y]);
      }
  }
}

三、 如何确认动态规划问题

从前面我所说来看,如果你碰到了求最值、求可行性或者是求方案总数的问题的话,那么这个问题就八九不离十了,你基本可以确定它就需要使用动态规划来解。但是,也有一些个别情况需要注意:

3.1 数据不可排序

假设我们有一个无序数列,希望求出这个数列中最大的两个数字之和。很多初学者刚刚学完动态规划会走火入魔到看到最优化问题就想用动态规划来求解,事实上,这个问题不是简单做一个排序或者做一个遍历就可以求解出来的。对于这种问题,我们应该先考虑一下能不能通过排序来简化问题,如果不能,才极有可能是动态规划问题。

最小的 k 个数

问题:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。

示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

我们发现虽然这个问题也是求“最”值,但其实只要通过排序就能解决,所以我们应该用排序、堆等算法或者数据结构就可以解决,而不应该用动态规划。

对应的示例代码:

public class Solution {
  public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
              int t;
      boolean flag;
      ArrayList result = new ArrayList();
      if(k>input.length){
          return result;
      }
      for(int i =0;i<input.length;i++){
          flag = true;
          for(int j = 0; j < input.length-i;j++)
              if(j<input.length-i-1){
                  if(input[j] > input[j+1]) {
                      t = input[j];
                      input[j] = input[j+1];
                      input[j+1] = t;
                      flag = false;
                  }
              }
          if(flag)break;
      }
      for(int i = 0; i < k;i++){
          result.add(input[i]);
      }
      return result;
  }
}

3.2 数据不可交换

还有一类问题,可以归类到我们总结的几类问题里去,但是不存在动态规划要求的重叠子问题(比如经典的八皇后问题),那么这类问题就无法通过动态规划求解。

全排列

问题:给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

这个问题虽然是求组合,但没有重叠子问题,更不存在最优化的要求,因此可以使用回溯方法处理。

对应的示例代码:

public class Main {
   public static void main(String[] args) {
       perm(new int[]{1,2,3},new Stack<>());
  }
   public static void perm(int[] array, Stack<Integer> stack) {
       if(array.length <= 0) {
           //进入了叶子节点,输出栈中内容
           System.out.println(stack);
      } else {
           for (int i = 0; i < array.length; i++) {
               //tmepArray是一个临时数组,用于就是Ri
               //eg:1,2,3的全排列,先取出1,那么这时tempArray中就是2,3
               int[] tempArray = new int[array.length-1];
               System.arraycopy(array,0,tempArray,0,i);
               System.arraycopy(array,i+1,tempArray,i,array.length-i-1);
               stack.push(array[i]);
               perm(tempArray,stack);
               stack.pop();
          }
      }
  }
}

总结一下,哪些问题可以使用动态规划呢,通常含有下面情况的一般都可以使用动态规划来解决:

  • 求最优解问题(最大值和最小值);

  • 求可行性(True 或 False);

  • 求方案总数;

  • 数据结构不可排序(Unsortable);

  • 算法不可使用交换(Non-swappable)。

如果面试题目出现这些特征,那么在 90% 的情况下你都能断言它就是一个动归问题。除此之外,还需要考虑这个问题是否包含重叠子问题与最优子结构,在这个基础之上你就可以 99% 断言它是否为动归问题,并且也顺势找到了大致的解题思路。

作者:xiangzhihong

来源:https://segmentfault.com/a/1190000041300090

收起阅读 »

10 个让人头疼的 bug

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界...
继续阅读 »

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。

作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界异常等。

如果你不小心看到同事的代码出现了我所描述的这些 bug 后,那你就把我这篇文章甩给他!!!你甩给他一篇文章,并让他关注了一波 cxuan,你会收获他在后面像是如获至宝并满眼崇拜大神的目光。

废话不多说,下面进入正题。

错误一:Array 转换成 ArrayList

Array 转换成 ArrayList 还能出错?这是哪个笨。。。。。。

等等,你先别着急说,先来看看是怎么回事。

如果要将数组转换为 ArrayList,我们一般的做法会是这样

List<String> list = Arrays.asList(arr);

Arrays.asList() 将返回一个 ArrayList,它是 Arrays 中的私有静态类,它不是 java.util.ArrayList 类。如下图所示


Arrays 内部的 ArrayList 只有 set、get、contains 等方法,但是没有能够像是 add 这种能够使其内部结构进行改变的方法,所以 Arrays 内部的 ArrayList 的大小是固定的。


如果要创建一个能够添加元素的 ArrayList ,你可以使用下面这种创建方式:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

因为 ArrayList 的构造方法是可以接收一个 Collection 集合的,所以这种创建方式是可行的。


错误二:检查数组是否包含某个值

检查数组中是否包含某个值,部分程序员经常会这么做:

Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);

这段代码虽然没错,但是有额外的性能损耗,正常情况下,不用将其再转换为 set,直接这么做就好了:

return Arrays.asList(arr).contains(targetValue);

或者使用下面这种方式(穷举法,循环判断)

for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;

上面第一段代码比第二段更具有可读性。

错误三:在 List 中循环删除元素

这个错误我相信很多小伙伴都知道了,在循环中删除元素是个禁忌,有段时间内我在审查代码的时候就喜欢看团队的其他小伙伴有没有犯这个错误。


说到底,为什么不能这么做(集合内删除元素)呢?且看下面代码

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);

这个输出结果你能想到么?是不是蠢蠢欲动想试一波了?

答案其实是 [b,d]

为什么只有两个值?我这不是循环输出的么?

其实,在列表内部,当你使用外部 remove 的时候,一旦 remove 一个元素后,其列表的内部结构会发生改变,一开始集合总容量是 4,remove 一个元素之后就会变为 3,然后再和 i 进行比较判断。。。。。。所以只能输出两个元素。

你可能知道使用迭代器是正确的 remove 元素的方式,你还可能知道 for-each 和 iterator 这种工作方式类似,所以你写下了如下代码

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));

for (String s : list) {
if (s.equals("a"))
list.remove(s);
}

然后你充满自信的 run xxx.main() 方法,结果。。。。。。ConcurrentModificationException

为啥呢?

那是因为使用 ArrayList 中外部 remove 元素,会造成其内部结构和游标的改变。

在阿里开发规范上,也有不要在 for-each 循环内对元素进行 remove/add 操作的说明。


所以大家要使用 List 进行元素的添加或者删除操作,一定要使用迭代器进行删除。也就是

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String s = iter.next();

if (s.equals("a")) {
iter.remove();
}
}

.next() 必须在 .remove() 之前调用。在 foreach 循环中,编译器会在删除元素的操作后调用 .next(),导致ConcurrentModificationException。

错误四:Hashtable 和 HashMap

这是一条算法方面的规约:按照算法的约定,Hashtable 是数据结构的名称,但是在 Java 中,数据结构的名称是 HashMap,Hashtable 和 HashMap 的主要区别之一就是 Hashtable 是同步的,所以很多时候你不需要 Hashtable ,而是使用 HashMap。

错误五:使用原始类型的集合

这是一条泛型方面的约束:

在 Java 中,原始类型和无界通配符类型很容易混合在一起。以 Set 为例,Set 是原始类型,而 Set<?> 是无界通配符类型。

比如下面使用原始类型 List 作为参数的代码:

public static void add(List list, Object o){
list.add(o);
}
public static void main(String[] args){
List<String> list = new ArrayList<String>();
add(list, 10);
String s = list.get(0);
}

这段代码会抛出 java.lang.ClassCastException 异常,为啥呢?


使用原始类型集合是比较危险的,因为原始类型会跳过泛型检查而且不安全,Set、Set<?> 和 Set<Object> 存在巨大的差异,而且泛型在使用中很容易造成类型擦除。

大家都知道,Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除

如在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM 看到的只是List,而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是 Java 的泛型与 C++ 模板机制实现方式之间的重要区别。

比如下面这段示例

public class Test {

  public static void main(String[] args) {

      ArrayList<String> list1 = new ArrayList<String>();
      list1.add("abc");

      ArrayList<Integer> list2 = new ArrayList<Integer>();
      list2.add(123);

      System.out.println(list1.getClass() == list2.getClass());
  }

}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型StringInteger都被擦除掉了,只剩下原始类型。

所以,最上面那段代码,把 10 添加到 Object 类型中是完全可以的,然而将 Object 类型的 "10" 转换为 String 类型就会抛出类型转换异常。

错误六:访问级别问题

我相信大部分开发在设计 class 或者成员变量的时候,都会简单粗暴的直接声明 public xxx,这是一种糟糕的设计,声明为 public 就很容易赤身裸体,这样对于类或者成员变量来说,都存在一定危险性。

错误七:ArrayList 和 LinkedList

哈哈哈,ArrayList 是我见过程序员使用频次最高的工具类,没有之一。


当开发人员不知道 ArrayList 和 LinkedList 的区别时,他们经常使用 ArrayList(其实实际上,就算知道他们的区别,他们也不用 LinkedList,因为这点性能不值一提),因为看起来 ArrayList 更熟悉。。。。。。

但是实际上,ArrayList 和 LinkedList 存在巨大的性能差异,简而言之,如果添加/删除操作大量且随机访问操作不是很多,则应首选 LinkedList。如果存在大量的访问操作,那么首选 ArrayList,但是 ArrayList 不适合进行大量的添加/删除操作。

错误八:可变和不可变

不可变对象有很多优点,比如简单、安全等。但是不可变对象需要为每个不同的值分配一个单独的对象,对象不具备复用性,如果这类对象过多可能会导致垃圾回收的成本很高。在可变和不可变之间进行选择时需要有一个平衡。

一般来说,可变对象用于避免产生过多的中间对象。比如你要连接大量字符串。如果你使用一个不可变的字符串,你会产生很多可以立即进行垃圾回收的对象。这会浪费 CPU 的时间和精力,使用可变对象是正确的解决方案(例如 StringBuilder)。如下代码所示:

String result="";
for(String s: arr){
result = result + s;
}

所以,正确选择可变对象还是不可变对象需要慎重抉择。

错误九:构造函数

首先看一段代码,分析为什么会编译不通过?


发生此编译错误是因为未定义默认 Super 的构造函数。在 Java 中,如果一个类没有定义构造函数,编译器会默认为该类插入一个默认的无参数构造函数。如果在 Super 类中定义了构造函数,在这种情况下 Super(String s),编译器将不会插入默认的无参数构造函数。这就是上面 Super 类的情况。

要想解决这个问题,只需要在 Super 中添加一个无参数的构造函数即可。

public Super(){
  System.out.println("Super");
}

错误十:到底是使用 "" 还是构造函数

考虑下面代码:

String x = "abc";
String y = new String("abc");

上面这两段代码有什么区别吗?

可能下面这段代码会给出你回答

String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True

String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True

这就是一个典型的内存分配问题。

后记

今天我给你汇总了一下 Java 开发中常见的 10 个错误,虽然比较简单,但是很容易忽视的问题,细节成就完美,看看你还会不会再犯了,如果再犯,嘿嘿嘿。


作者:cxuan
来源:https://mp.weixin.qq.com/s/uF0p8MGDhfvke4gdRv44iA

收起阅读 »

Ngnix之父突然离职,程序员巅峰一代落幕

当地时间 1 月 18 日,Nginx 公司副总裁兼总经理 Rob Whiteley 在 Nginx 官网发布了一篇「告别信」,正式宣告 Nginx 的作者和 Nginx Inc. 的联合创始人 Igor Sysoev 退出 Nginx 和 F5 Networ...
继续阅读 »

当地时间 1 月 18 日,Nginx 公司副总裁兼总经理 Rob Whiteley 在 Nginx 官网发布了一篇「告别信」,正式宣告 Nginx 的作者和 Nginx Inc. 的联合创始人 Igor Sysoev 退出 Nginx 和 F5 Networks。

此事很快登上 Hacker News 的热搜榜,有网友留言道:

我看过 Igor 参加某个会议的视频,他一说:“你好,我是 Nginx 的创建者 Igor Sysoev ”,观众席就会‘爆发’绵延不绝的掌声。他甚至不得不告诉他们“Come on guys, 你们还没听我的讲演呢。”。

不少开发者对 Igor 所做出的贡献表达了崇敬和感谢,也有网友感慨“巅峰一代落幕”。从 2002 年发展至今日,Nginx 已经成为全球最受欢迎的 Web 服务器。据 W3Techs 统计,截至 2022 年 1 月上旬,Nginx 占据了全球 Web 服务器市场 33% 的份额。排在第二位的是 Apache,份额为 31%。

pic_d710133a.png

一直以来,Nginx 常被拿来跟 Apache 对比,也有观点认为,Nginx 和 Apache 不算真正意义上的竞争者,很多地方会同时使用两者。但无论如何,Igor 和 Nginx 的成功确实鼓舞了不少开源人。

作为一名开源开发者和商业 OSS 初创公司创始人,Nginx 给了我很大的挑战现状的信心。Apache 是如此受人尊敬,以至于你会认为可以改进它的想法是很疯狂的,但他( Igor )做到了,这对我产生了真正的影响。——yesimahuman

Igor 早期曾在采访中分享对于开源和商业产品找平衡的观点,他表示不想创建单独的商业产品,而是希望对 Nginx 的主要开源产品进行商业扩展,社区想要的新功能将出现在其中。商业扩展更多的是有助于处理数千个实例、添加扩展性能监控、托管、云和 CDN 基础设施的附加功能等。

很多客户会说愿意付钱让 Igor 增加他们所需要的新功能,而 Igor 等人收集此类请求后会将其与从用户社区收到的需求进行比较,并寻找交叉点——“如果我们意识到每个人都需要某些功能,而不仅仅是某些公司,我们会将这些功能包含在开源版本中。我们从中了解我们可以销售什么,而不会惹恼开源产品的支持者,也不会损害整个项目的信誉。”

Nginx 如今归属于 F5 Networks。2019 年 3 月,F5 Networks 宣布将以 6.7 亿美元收购 Nginx,根据交易条款,Nginx 品牌被保留,而 Igor 和 合伙人 Konovalov 作为 F5 的一部分继续致力于该项目。但这笔交易很快就触发了利益纷争,同年 12 月,Igor 陷入版权纠纷,前东家 Rambler 集团对 NGINX Inc. 提出了侵犯版权的诉讼,声称拥有 Nginx Web 服务器代码的全部所有权,但 Igor 辩称是在业余时间开发了 Nginx。

此事随即引发热议,业余项目究竟属于开发者个人、还是属于开发者所在的企业,目前没有明确的统一的法律来判定。2020 年 4 月,Rambler 驳回针对 Nginx 的刑事诉讼。但 Rambler 并未就此停下,只是不再是以刑事诉讼的方式,而是通过民事法院,并于 2020 年 6 月初宣布授权旗下 Lynwood Investments 在美国对 F5 Networks、Igor 本人发起民事诉讼,要求索赔 7.5 亿美元。6 月末,俄罗斯内政部因缺乏犯罪记录证据,结案了有关 Nginx 版权的案件。

告别信有提到 Igor 从 Nginx 离职后将从事个人项目,目前我们尚不清楚他具体会涉及哪些项目。

以下是「告别信」全文:

挥别 Igor:
感谢你为 Nginx 付出的一切

怀着深深的感激之情,我们今天宣布,Nginx 的作者和 Nginx 公司联合创始人 Igor Sysoev 选择退出 Nginx 和 F5,以便花更多的时间与他的朋友和家人在一起,并追求个人项目。

2002 年的春天,Igor Sysoev 开始开发 Nginx。互联网的早期飞速发展让他萌生出一个念头:用一套全新架构改进网络流量的处理方式,帮助高流量网站从容应对数万个并发连接,并将照片、视频等各类可能严重拖慢页面加载速度的内容统统塞进缓存。

二十年过去,Igor 写下的代码已经在为世界上大部分网站提供支持。除了直接使用外,也被作为 Cloudflare、OpenResty、Tengine 等流行服务器的底层软件。很多人认为,Igor 最初的梦想就是把 Web 塑造成如今的样貌。Igor 所秉持的意志与价值观则汇聚成 Nginx 公司,结合开源与技术社区之力成就高透明度、质量卓越的代码,最终转化为客户喜闻乐见的商业产品。

但其中的平衡往往很难把握。Igor 之所以受到开发者、企业客户以及 Nginx 工程师们的高度赞扬,依靠的正是他谦逊的内心、不断探索的激情以及在开发工作中勇攀高峰的意志。

Igor 的成长与 Nginx 的诞生

Igor 的人生起点不高。他出生于苏联时期的一个哈萨克斯坦小镇,父亲是一名军官。一岁时,他们全家迁往首都阿拉木图。Igor 从小痴迷计算机,1980 年代中期就在 Yamaha MSX 上写下了人生第一行代码。而伴随着早期互联网产业的快速发展,Igor 也从著名的鲍曼莫斯科国立技术大学计算机科学系顺利毕业。

Igor 毕业后先找了份系统管理员工作,但写代码的好习惯一直没有丢下。1999 年,他用汇编语言开发出自己的第一个程序,这款反病毒软件能抵御当时最常见的十种计算机病毒。Igor 免费开放了程序的二进制文件,这款工具也在俄罗斯国内风靡一时。之后,敏感的他发觉 Apache HTTP 服务器的连接处理方式过于原始,根本无法满足不断发展的万维网需求。于是他决定开展相关研究,这也正是后来 Nginx 项目的雏形。

彼时,Igor 将目光投向了 C10k 问题,即如何在单一服务器上处理 10000 个并发连接。此外,他还希望让自己的 Web 服务器更快、更高效地处理照片或者音乐文件等极占传输带宽的元素。在获得俄罗斯国内外多家公司的肯定和采用之后,Igor 于 2004 年 10 月 4 日(即苏联发射全球首颗人造卫星「斯普特尼克」号的四十七周年纪念日)对这个名为 Nginx 的项目进行了许可开源。

七年来,Igor 一直是唯一的开发者。他独力写下数十万行代码,并把 Nginx 从简单的 Web 服务器加反向代理工具,扩展成一把能满足各类 Web 应用与服务需求的“瑞士军刀”。随着项目发展,负载均衡、缓存、安全和内容加速等关键功能也在他的指尖一一成形。

没有队伍的 Igor 当时自然没精力宣传项目,甚至连说明文档也不够完备。但 Nginx 仍然凭借着出色的表现迅速占领了市场。更神奇的是,新用户发现就算没有全面的使用指南、自己仍然能轻松玩转 Nginx,于是项目就在口口相传之下普及开来。越来越多的开发者和系统管理员利用 Nginx 解决自己面对的现实问题,提升网站响应速度。对于 Igor 的贡献,我们已经不需要刻意赞美或者宣扬,他的代码已经说明了一切。

Nginx 开启商业化之路,但开源定位永不动摇

2011 年,Igor 与 Maxim Konovalov、Andrew Alexeev 两位联合创始人共同成立了 Nginx 公司,希望借众人之力加快项目开发速度。但 Igor 也很清楚,从这一刻起他和团队得想办法赚钱了。不过他们坚持发布 Nginx 完整开源版本、恪守开源许可的承诺不会动摇。君子一诺值千金,自公司成立以来,Igor 引领 Nginx 通过 140 多个版本不断完善自我,始终以开源姿态为全球数亿网站提供支持。

pic_93439c65.png

奔波在为 Nginx 公司筹集风险投资的路上——(右起)Igor、公司 CEO Gus Robertson、联合创始人 Andrew Alexeev 以及 Maxim Konovalov

2011 年的时候,以专有模块的形式向商业版本中添加新功能的想法还属于开时代之先河。但如今,很多开源后起之秀已经可以站在巨人的肩膀上享受这种商业模式。在商业版 Nginx Plus 于 2013 年首次推出时,市场立刻抱以热烈欢迎。四年之后,Nginx 已经拥有超过 1000 家付费客户和数千万收入,Nginx 开源项目与技术社区的规模也在同步发展壮大。截至 2019 年底,Nginx 已经在为全球超过 4.75 亿个网站提供支持;到 2021 年,Nginx 正式成为世界上应用范围最广的 Web 服务器方案。

着眼于未来需求,Igor 还一路打造出多个 Nginx 相关项目,包括 Nginx JavaScript(njs)与 Nginx Unit。他还为 sendfile(2)系统调用设计了全新实现,将其整合到开源 FreeBSD 操作系统当中。随着 Nginx 工程师队伍的壮大和 Nginx 公司正式加入 F5,Igor 一直是团队背后稳健的领导者,保证 Nginx 始终方向明确、斗志坚定。

接过 Igor 手中的旗帜

今天,Igor 希望退居幕后享受生活,独余我们继续前行。但 Igor 的精神和他一路塑造的文化不会消失。伟大的企业、产品和项目中,创始人的 DNA 是永恒不变的。我们对于产品、社区、透明度、开源和创新的态度皆继承自 Igor,我们也将继续在 Maxim 和 Nginx 领导团队的指引下接过这面旗帜、发挥这份传统。

Igor 在 Nginx 与 F5 时代的奋斗与付出凝结成了我们今天所看到的项目代码,多年以来一直默默支撑起整个互联网世界。时间会考验我们、鞭策我们,证明我们能否像 Igor 那样创造出历久弥新、影响深远的产品。这当然是一条极高的标准,但 Igor 也用实际行动为我们指明了达成目标的方法。感恩多年来的指引与教导,Igor,祝你在人生的新阶段写下新的传奇故事。

来源:https://mp.weixin.qq.com/s/GANdlnXt1_vuUm3j97Njg

收起阅读 »

城市数字孪生标准化白皮书(2022版)

当 前,城市数字孪生已经发展成为支撑智慧城市的重要技术手段。全文共计3026字,预计阅读时间8分钟来源| 全国信标委智慧城市标准工作组编辑 | 蒲蒲城市数字孪生通过在数字空间对城市物理空间和社会空间进行全要素表达、全过程呈现、全周期可溯,实现城市全面感知、虚实...
继续阅读 »

当 前,城市数字孪生已经发展成为支撑智慧城市的重要技术手段。

pic_02b697b8.png
全文共计3026字,预计阅读时间8分钟

来源| 全国信标委智慧城市标准工作组

编辑 | 蒲蒲

城市数字孪生通过在数字空间对城市物理空间和社会空间进行全要素表达、全过程呈现、全周期可溯,实现城市全面感知、虚实交互、智能决策、精准控制,推动城市智能化、智慧化发展。

当前,城市数字孪生已经发 展成为支撑智慧城市的重要技术手段。 为做好城市数字孪生标准化工作整体规划,有序推动相关标准制定与应用实施工作,全国信标 委智慧城市标准工作组组建了城市数字孪生专题组,并联合相关单位编制了《城市数字孪生标准化白皮书(2022版)》。

白皮书在系统研究城市数字孪生内涵、典型特征、相关方等基础上,构建了城市数字孪生技术参考架构,梳理了城市数字孪生关键技术和典型应用场景,总结了城市数字孪生发展现状、发展趋势、面临的问题与挑战及国际国内标准化现状。在此基础上,白皮书探索形成了“城市数字孪生标准体系总体框架(1.0版)”,并提出了拟研制标准建议和标准化工作建议。白皮书构建了城市数字孪生标准化路线图,为后续相关标准研制、应用实施指明了方向。

城市数字孪生典型特征

全面感知:城市数字孪生以全面感知为前提。城市是一个复杂巨系统,时刻处于发展变化中,必须时刻掌握物理城市的全局发展与精细变化,实现孪生环境下的数字城市与物理城市同步运行。

精准映射是构建数字世界并建立数字世界与物理世界紧密关系的过程。

智能推演是城市数字孪生具备智慧能力的体现,是实现对物理城市进行科学预测、指导与优化的关键。

动态可视:指通过将感知的多源数据进行数字化建模和可视化渲染,城市数字孪生提供了全要素、全范围、全精度真实的渲染效果,实现全空间信息和城市实时运行

虚实互动:指物理空间与数字空间的互操作和双向互动,借助物联网、图形/图像、AR/VR、人机交互等领域技术的协同和融合,实现城市级虚实空间融合、控制与反馈等能力。态势的动态展示。

协同演进是城市数字孪生具有高阶智慧能力的体现。城市数字孪生过程中,物理城市与数字城市在城市运行、数据、技术、机制等方面存在长期协同关系,长期相互反馈、相互影响。

pic_1f49ab65.png

多维度构建参考架构,立体刻画城市数字孪生内涵

白皮书对“城市数字孪生”概念进行了系统地梳理和分析,创新性地从概念、技术、相关方等不同视角构建了城市数字孪生参考架构。

白皮书认为,城市数字孪生是利用数字孪生技术,以数字化方式创建城市物理实体的虚拟映射,借助历史数据、实时数据、空间数据以及算法模型等,仿真、预测、交互、控制城市物理实体全生命周期过程的技术手段,可以实现城市物理空间和社会空间中物理实体对象以及关系、活动等在数字空间的多维映射和连接。

从概念视角,城市数字孪生以城市物理空间、社会空间以及数字空间在时间维度和空间维度更加精准的映射、更加紧密的联接和更加多维的联动,实现三元空间的协同演进和共生共智,满足“人”在城市生活、生产、生态的各类需求,服务“以人为本”的智慧城市建设初心。

pic_49a1d280.png

图1 城市数字孪生概念模型

从技术视角,需对物理空间以及社会空间中的物理实体对象、事件对象以及关系对象进行数字空间的虚拟表达以及映射,通过信息基础设施的转化传输以及处理形成数据资源,在通用服务能力的支撑下进一步融合数字孪生技术形成能够对外提供的数字孪生服务,并通过交互服务实现与上层应用场景的融合。同时,需提供立体化安全管理以及全生命周期的运营管理,保障数字空间各类资产以及服务的安全高效运行。

pic_5800ae7f.png图2 城市数字孪生技术参考架构

从相关方视角,城市数字孪生由城市数字孪生咨询服务提供方、建设技术提供方、运营服务方三方多类主体联动构建。

pic_6b1cf3f0.png图3 城市数字孪生相关方

pic_6240dbd6.png

梳理发展现状,总结城市数字孪生趋势与问题

白皮书梳理了城市数字孪生国家及地方相关政策,分析了产业生态发展现状。同时,提出了城市数字孪生发展的总体发展趋势及面临的主要问题和挑战。

pic_17724c61.png图4 城市数字孪生产业生态

随着各地城市数字孪生探索与落地,智慧城市建设也将进入新的发展阶段。随供需双发力,城市数字孪生的技术创新、产业发展、标准规范等将快速发展,呈现物理城市和数字城市并行共生的新发展格局。

  • 城市数字孪生将成为智慧城市建设技术底座。城市数字孪生将成为智慧城市发展新阶段的核心底座,为城市构建虚实共生的数字基础设施能力。
  • 城市数字孪生将在智慧城市中迎来深度应用。城市数字孪生相关产业快速发展,市场规模不断扩大,以城市大脑、城运中心、城市信息模型、城市数字孪生运营管理为主的相关领域市场迅速升温。
  • 城市数字孪生将形成跨行业协作生态共融。数据融合、技术融合和业务融合推动城市数字孪生产业链上下游的多元主体在竞争中发展出共生关系,生态共融正成为行业共识。

城市数字孪生从概念培育逐步走向建设实施,各项支撑技术日渐成熟,但仍面临着供应链安全性不足、数据支撑不足、应用深度不足、产业联动不足和标准支撑不足等问题与挑战。

pic_bb1e559f.png

构建标准体系,以标准化助力高质量发展

当前,城市数字孪生标准化处于起步阶段,亟需开展标准体系顶层设计。白皮书探索构建了“城市数字孪生标准体系总体框架(1.0版)”,将城市数字孪生标准划分为“01总体”“02数据”“03技术与平台”“04安全”“05运维/运营”“06应用”六大类。

pic_852cf2b0.png图5 城市数字孪生标准体系总体框架(1.0版)

pic_99774322.png图6 城市数字孪生标准体系结构

为充分发挥标准基础性、引领性作用,助力城市数字孪生相关产业高质量发展,白皮书建议城市数字孪生标准化工作主要从四方面开展。

  • 完善工作机制,强化统筹与协同。城市数字孪生涉及技术、应用、相关方众多,是复杂的系统工程,其标准化工作开展需统筹布局、协同各方。
  • 研制重点标准,完善标准体系建设。以“规划引领、需求牵引”为原则,推动重点标准研制工作,不断完善城市数字孪生标准体系。
  • 挖掘优秀案例,发挥示范引领作用。建立城市数字孪生典型案例与标准的良性互动机制,充分发挥先进性、代表性案例的引领与示范作用。
  • 强化国际交流,推动国际标准制定。城市数字孪生承载了一系列关键技术和核心产业,要借助国际标准带动我国产品和方案走出去。

下一步,全国信标委智慧城市标准工作组将以此白皮书为基础,与各界共同推动城市数字孪生技术、理论、标准研究与制定工作,凝共识、聚合力,助力城市数字孪生产业生态培育,推动“以人为本”的智慧城市向智能化、智慧化迈进。

具体内容如下

pic_26d37640.png

pic_5e18ea32.png

pic_819d1a52.png

pic_41f0df74.png

pic_83d7532c.png

pic_0e4ecd8e.png

pic_3f946dd0.png

pic_6b04c7bc.png

pic_dcc0daaf.png

pic_0f29152f.png

pic_fdd8dedd.png

pic_66735768.png

pic_b20e947b.png

pic_b3b47ca6.png

pic_228d6a38.png

pic_7e0ae0aa.png

pic_13e8029d.png

pic_198b3a43.png

pic_38d1988e.png

pic_822ed03b.png

pic_4013dc97.png

pic_1a552d51.png

pic_18768e2d.png

pic_a42a5335.png

pic_92e9e847.png

pic_3cbad5fe.png

pic_b349d1bc.png

pic_ac929dd1.png

pic_9c257eca.png

pic_a126168b.png

pic_6e02a89e.png

pic_36202e9e.png

pic_7d0afa1c.png

pic_0988e5d8.png

pic_2bf91f18.png

pic_59d7d9ac.png

pic_dc59256b.png

pic_6402fd89.png

pic_9f8fb217.png

pic_69a13f05.png

pic_0b680970.png

pic_35a51d84.png

pic_590b48db.png

pic_d425aa26.png

pic_5a62839c.png

pic_3a23fd3e.png

pic_ecf8dfa3.png

pic_1bf22b4c.png

pic_ddb0d741.png

pic_a156ccc9.png

pic_1af6169c.png

pic_1b160c3a.png

pic_97466946.png

pic_36139833.png

pic_dbaae499.png

pic_133108f1.png

pic_e4108299.png

pic_e64220e2.png

pic_c4cb36d3.png

pic_0a2c9b5b.png来源:全国信标委智慧城市标准工作组    

收起阅读 »

还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!

本文介绍策略模式的具体应用以及Map+函数式接口如何 “更完美” 的解决 if-else的问题。 需求 最近写了一个服务:根据优惠券的类型resourceType和编码resourceId来 查询 发放方式grantType和领取规则实现方式:根据优惠券类型...
继续阅读 »

本文介绍策略模式的具体应用以及Map+函数式接口如何 “更完美” 的解决 if-else的问题。

需求

最近写了一个服务:根据优惠券的类型resourceType和编码resourceId来 查询 发放方式grantType和领取规则

实现方式:

  1. 根据优惠券类型resourceType -> 确定查询哪个数据表

  2. 根据编码resourceId -> 到对应的数据表里边查询优惠券的派发方式grantType和领取规则

优惠券有多种类型,分别对应了不同的数据库表:

  • 红包 —— 红包发放规则表

  • 购物券 —— 购物券表

  • QQ会员

  • 外卖会员

实际的优惠券远不止这些,这个需求是要我们写一个业务分派的逻辑

第一个能想到的思路就是if-else或者switch case:

switch(resourceType){

case "红包"
查询红包的派发方式 
break;
case "购物券"
查询购物券的派发方式
break;
case "QQ会员" :
break;
case "外卖会员" :
break;
......
default : logger.info("查找不到该优惠券类型resourceType以及对应的派发方式");
break;
}

如果要这么写的话, 一个方法的代码可就太长了,影响了可读性。(别看着上面case里面只有一句话,但实际情况是有很多行的)

而且由于 整个 if-else的代码有很多行,也不方便修改,可维护性低。

策略模式

策略模式是把 if语句里面的逻辑抽出来写成一个类,如果要修改某个逻辑的话,仅修改一个具体的实现类的逻辑即可,可维护性会好不少。

以下是策略模式的具体结构(详细可看这篇博客: 策略模式.):


策略模式在业务逻辑分派的时候还是if-else,只是说比第一种思路的if-else 更好维护一点。。。

switch(resourceType){

case "红包"
String grantType=new Context(new RedPaper()).ContextInterface();
break;
case "购物券"
String grantType=new Context(new Shopping()).ContextInterface();
break;

......
default : logger.info("查找不到该优惠券类型resourceType以及对应的派发方式");
break;

但缺点也明显:

  • 如果 if-else的判断情况很多,那么对应的具体策略实现类也会很多,上边的具体的策略实现类还只是2个,查询红包发放方式写在类RedPaper里边,购物券写在另一个类Shopping里边;那资源类型多个QQ会员和外卖会员,不就得再多写两个类?有点麻烦了

  • 没法俯视整个分派的业务逻辑

Map+函数式接口

用上了Java8的新特性lambda表达式

  • 判断条件放在key中

  • 对应的业务逻辑放在value中

这样子写的好处是非常直观,能直接看到判断条件对应的业务逻辑

需求: 根据优惠券(资源)类型resourceType和编码resourceId查询派发方式grantType

上代码:

@Service
public class QueryGrantTypeService {

   @Autowired
   private GrantTypeSerive grantTypeSerive;
   private Map<StringFunction<String,String>> grantTypeMap=new HashMap<>();

   /**
    * 初始化业务分派逻辑,代替了if-else部分
    * key: 优惠券类型
    * value: lambda表达式,最终会获得该优惠券的发放方式
    */
   @PostConstruct
   public void dispatcherInit(){

       grantTypeMap.put("红包",resourceId->grantTypeSerive.redPaper(resourceId));
       grantTypeMap.put("购物券",resourceId->grantTypeSerive.shopping(resourceId));
       grantTypeMap.put("qq会员",resourceId->grantTypeSerive.QQVip(resourceId));
  }

   public String getResult(String resourceType){
  
    
    
       //Controller根据 优惠券类型resourceType、编码resourceId 去查询 发放方式grantType
       Function<String,String> result=getGrantTypeMap.get(resourceType);
       if(result!=null){

      //传入resourceId 执行这段表达式获得String型的grantType
           return result.apply(resourceId);
      }
       return "查询不到该优惠券的发放方式";
  }
}

如果单个 if 语句块的业务逻辑有很多行的话,我们可以把这些 业务操作抽出来,写成一个单独的Service,即:

//具体的逻辑操作

@Service
public class GrantTypeSerive {

   public String redPaper(String resourceId){

       //红包的发放方式
       return "每周末9点发放";
  }
   public String shopping(String resourceId){

       //购物券的发放方式
       return "每周三9点发放";
  }
   public String QQVip(String resourceId){

       //qq会员的发放方式
       return "每周一0点开始秒杀";
  }
}

入参String resourceId是用来查数据库的,这里简化了,传参之后不做处理。

用http调用的结果:

@RestController
public class GrantTypeController {

   @Autowired
   private QueryGrantTypeService queryGrantTypeService;

   @PostMapping("/grantType")
   public String test(String resourceName){

       return queryGrantTypeService.getResult(resourceName);
  }
}


用Map+函数式接口也有弊端:
你的队友得会lambda表达式才行啊,他不会让他自己百度去

最后捋一捋本文讲了什么:

  1. 策略模式通过接口、实现类、逻辑分派来完成,把 if语句块的逻辑抽出来写成一个类,更好维护。

  2. Map+函数式接口通过Map.get(key)来代替 if-else的业务分派,能够避免策略模式带来的类增多、难以俯视整个业务逻辑的问题。

    ————————————————
    作者:zhongh Jim
    来源:https://blog.csdn.net/qq_44384533/article/details/109197926

收起阅读 »

DDD划分领域、子域、核心域、支撑域的目的

名词解释在DDD兴起的原因以及与微服务的关系中曾举了一个研究桃树的例子,如果要研究桃树,将桃树根据器官分成根、茎、叶、花、果实、种子,这每一种器官都可以认为是一个研究领域,而领域又有更加具体的细分,分成子域、核心域、通用域、支撑域等,下面回顾桃树这个例子看上面...
继续阅读 »

名词解释

DDD兴起的原因以及与微服务的关系中曾举了一个研究桃树的例子,如果要研究桃树,将桃树根据器官分成根、茎、叶、花、果实、种子,这每一种器官都可以认为是一个研究领域,而领域又有更加具体的细分,分成子域、核心域、通用域、支撑域等,下面回顾桃树这个例子


看上面这张图 ,如果研究桃树是我们的业务,那么如何更加快速有效的研究桃树呢? 根据回忆,初中课本是这样研究的:

第一步: 确定研究的对象,即研究领域 ,这里是一棵桃树。

第二步: 根据研究对象的某些维度,对其进行进一步的拆分,例如拆分成器官,而器官又可以分成营养器官,生殖器官,其中营养器官包括根、茎、叶,生殖器官包括花、果实、种子,那么这些就是我们要研究的子域。

第三步: 现在就可以最子域进行划分了,找出核心域,通用域,支撑域,至于为什么要这么划分,后面再解释,当我们找到核心域之后,再各个子域进行深一步的划分,划分成组织,例如分成保护组织,营养组织,疏导组织,这就儿也可以理解成将领域继续划分为子域的过程。

第四步:对组织进行进一步的划分,可以分成细胞,例如根毛细胞、导管细胞等等

我们有没有必要继续拆分细胞呢?这个取决于我们研究的业务,例如在之前光学显微镜时,研究到细胞也就截止了,具体到其他业务,也是研究到某一步就不需要继续拆分,而这最小层次的领域,通常就是我们所说的实体,聚合、聚合根、实体以及值对象等内容会在后面深入了解。


下面归纳一下上面提到的几个名词的概念 :

领域: 往往就是业务的某一个部分 , 例如电商的销售部分、物流部分、供应链部分等, 这些对于电商来说就是各个领域(模块),领域主要作用就是用来驱动范围, DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

子域:相对的一个概念, 我们可以将领域进行进一步的划分 , 这时候就是子域, 甚至可以对子域继续划分形成 子子域(依旧叫子域),就好比当我们研究植物时,如果研究的对象是桃树,那么果实根茎叶是领域,可是如果不仅仅要研究果实,还要研究组织甚至细胞,那么研究的就是果实的子域、组织的子域。

核心域:所有领域中最关键的部分 , 什么意思呢, 就是最核心的部分, 对于业务来说, 核心域是企业根本竞争力, 也是创造利润里最关键的部分 , 例如电商里面那么多领域, 最重要的是什么? 就是销售系统, 无论你是2B还是2C, 还是PDD ,这些核心模块就是核心域。

通用域:除了核心域之外, 还需要自己做的一些领域, 例如鉴权、日志等, 特点是可能被多个领域公用的部分。

支撑域:系统中业务分析阶段最不重点关注的领域, 也就是非核心域非通用域的领域, 例如电商里面的支付、物流,仅仅是为了支撑业务的运转而存在, 甚至可以去购买别人的服务, 这类的领域就是支撑域。

需要注意的是,这些名词在实际的微服务设计和开发过程中不一定用得上,但是可以帮助理解DDD的核心设计思想以及理念,而这些思想和理念在实际的IT战略设计业务建模和微服务设计上都是可以借鉴的。

为什么要划分核心域、通用域、支撑域 ?

通过上面可以知道,决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。

这三类子域相较之下,核心域是最重要的,我们下面讲目的的时候还会以核心域为例详细介绍。通用域和支撑域如果对应到企业系统,举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。

那么为什么要划分出这些新的名词呢? 先想一个问题,对于桃树而言,根、茎、叶、花、果实、种子六个领域哪一个是核心域?

是不是有不同的理解? 有人说是种子,有人说是根,有人说是叶子,也有人说是茎等等,为什么会有这种情况呢?

因为每个人站的角度不一样,你如果是果农,那么果实就是核心域,你的大部分操作应该都是围绕提高果实产量进行,如果你是景区管理员,那么芳菲四月桃花盛开才是你重点关注,如果比是林场工作人员,那么树干才应该是你重点关注的领域,看到没,对于同一个领域划分的子域,每个人都有不同的理解,那么要通过讨论确定核心域,确保大家认同一致,对于实际业务开发来说,参与的人员众多,有业务方面的,有架构师,有后端开发人员,营销市场等等,势必要最开始就确定我们的核心域,除了统一大家的认识之外还有什么好处呢?

对于一个企业来说,预算以及时间是有限的,也就意味着时间以及精力甚至金钱要尽可能多的花在核心的的地方。就好比电商,电商企业那么多,每一家核心域都有所差别,造成的市场结果也千差万别,那么公司战略重点和商业模式应该找到核心域,且重点关注核心域。

总的来说,核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能
属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

作者:等不到的口琴
来源:https://www.cnblogs.com/Courage129/p/14853600.html


收起阅读 »

API安全接口安全设计

如何保证外网开放接口的安全性。使用加签名方式,防止数据篡改信息加密与密钥管理搭建OAuth2.0认证授权使用令牌方式搭建网关实现黑名单和白名单一、令牌方式搭建搭建API开放平台方案设计:1.第三方机构申请一个appId,通过appId去获取accessToke...
继续阅读 »

如何保证外网开放接口的安全性。

  • 使用加签名方式,防止数据篡改

  • 信息加密与密钥管理

  • 搭建OAuth2.0认证授权

  • 使用令牌方式

  • 搭建网关实现黑名单和白名单

一、令牌方式搭建搭建API开放平台


方案设计:

1.第三方机构申请一个appId,通过appId去获取accessToken,每次请求获取accessToken都要把老的accessToken删掉

2.第三方机构请求数据需要加上accessToken参数,每次业务处理中心执行业务前,先去dba持久层查看accessToken是否存在(可以把accessToken放到redis中,这样有个过期时间的效果),存在就说明这个机构是合法,无需要登录就可以请求业务数据。不存在说明这个机构是非法的,不返回业务数据。

3.好处:无状态设计,每次请求保证都是在我们持久层保存的机构的请求,如果有人盗用我们accessToken,可以重新申请一个新的taken.

二、基于OAuth2.0协议方式

原理

第三方授权,原理和1的令牌方式一样

1.假设我是服务提供者A,我有开发接口,外部机构B请求A的接口必须申请自己的appid(B机构id)

2.当B要调用A接口查某个用户信息的时候,需要对应用户授权,告诉A,我愿同意把我的信息告诉B,A生产一个授权token给B。

3.B使用token获取某个用户的信息。

联合微信登录总体处理流程

  1. 用户同意授权,获取code

  2. 通过code换取网页授权access_token

  3. 通过access_token获取用户openId

  4. 通过openId获取用户信息

三、信息加密与密钥管理

  • 单向散列加密

  • 对称加密

  • 非对称加密

  • 安全密钥管理

1.单向散列加密

散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。

散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。

单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:

  • MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;

SHA-1与MD5的比较

因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:

  • 对强行供给的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2128数量级的操作,而对SHA-1则是2160数量级的操作。这样,SHA-1对强行攻击有更大的强度。

  • 对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。

  • 速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

1、特征:雪崩效应、定长输出和不可逆。

2、作用是:确保数据的完整性。

3、加密算法:md5(标准密钥长度128位)、sha1(标准密钥长度160位)、md4、CRC-32

4、加密工具:md5sum、sha1sum、openssl dgst。

5、计算某个文件的hash值,例如:md5sum/shalsum FileName,openssl dgst –md5/-sha

2.对称加密

秘钥:加密解密使用同一个密钥、数据的机密性双向保证、加密效率高、适合加密于大数据大文件、加密强度不高(相对于非对称加密)

对称加密优缺点

  • 优点:与公钥加密相比运算速度快。

  • 缺点:不能作为身份验证,密钥发放困难


DES是一种对称加密算法,加密和解密过程中,密钥长度都必须是8的倍数

public class DES {
public DES() {
}

// 测试
public static void main(String args[]) throws Exception {
// 待加密内容
String str = "123456";
// 密码,长度要是8的倍数 密钥随意定
String password = "12345678";
byte[] encrypt = encrypt(str.getBytes(), password);
System.out.println("加密前:" +str);
System.out.println("加密后:" + new String(encrypt));
// 解密
byte[] decrypt = decrypt(encrypt, password);
System.out.println("解密后:" + new String(decrypt));
}

/**
* 加密
*
* @param datasource
*           byte[]
* @param password
*           String
* @return byte[]
*/
public static byte[] encrypt(byte[] datasource, String password) {
try {
  SecureRandom random = new SecureRandom();
  DESKeySpec desKey = new DESKeySpec(password.getBytes());
  // 创建一个密匙工厂,然后用它把DESKeySpec转换成
  SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
  SecretKey securekey = keyFactory.generateSecret(desKey);
  // Cipher对象实际完成加密操作
  Cipher cipher = Cipher.getInstance("DES");
  // 用密匙初始化Cipher对象,ENCRYPT_MODE用于将 Cipher 初始化为加密模式的常量
  cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
  // 现在,获取数据并加密
  // 正式执行加密操作
  return cipher.doFinal(datasource); // 按单部分操作加密或解密数据,或者结束一个多部分操作
} catch (Throwable e) {
  e.printStackTrace();
}
return null;
}

/**
* 解密
*
* @param src
*           byte[]
* @param password
*           String
* @return byte[]
* @throws Exception
*/
public static byte[] decrypt(byte[] src, String password) throws Exception {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(password.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");// 返回实现指定转换的
                  // Cipher
                  // 对象
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 真正开始解密操作
return cipher.doFinal(src);
}
}

输出

加密前:123456
加密后:>p.72|
解密后:123456

3.非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。

公钥与私钥是一对

  • 公钥对数据进行加密,只有用对应的私钥才能解密

  • 私钥对数据进行加密,只有用对应的公钥才能解密

过程:

  • 甲方生成一对密钥,并将公钥公开,乙方使用该甲方的公钥对机密信息进行加密后再发送给甲方;

  • 甲方用自己私钥对加密后的信息进行解密。

  • 甲方想要回复乙方时,使用乙方的公钥对数据进行加密

  • 乙方使用自己的私钥来进行解密。

  • 甲方只能用其私钥解密由其公钥加密后的任何信息。

特点:

  • 算法强度复杂

  • 保密性比较好

  • 加密解密速度没有对称加密解密的速度快。

  • 对称密码体制中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥。所以保证其安全性就是保证密钥的安全,而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多

  • 适用于:金融,支付领域

RSA加密是一种非对称加密

import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import org.apache.commons.codec.binary.Base64;


/**
* RSA加解密工具类
*
*
*/
public class RSAUtil {

public static String publicKey; // 公钥
public static String privateKey; // 私钥

/**
* 生成公钥和私钥
*/
public static void generateKey() {
// 1.初始化秘钥
KeyPairGenerator keyPairGenerator;
try {
  keyPairGenerator = KeyPairGenerator.getInstance("RSA");
  SecureRandom sr = new SecureRandom(); // 随机数生成器
  keyPairGenerator.initialize(512, sr); // 设置512位长的秘钥
  KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 开始创建
  RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
  RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
  // 进行转码
  publicKey = Base64.encodeBase64String(rsaPublicKey.getEncoded());
  // 进行转码
  privateKey = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
}

/**
* 私钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByprivateKey(String content, String privateKeyStr, int opmode) {
// 私钥要用PKCS8进行处理
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyStr));
KeyFactory keyFactory;
PrivateKey privateKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, privateKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }

} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

/**
* 公钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByPublicKey(String content, String publicKeyStr, int opmode) {
// 公钥要用X509进行处理
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyStr));
KeyFactory keyFactory;
PublicKey publicKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, publicKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }
} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

// 测试方法
public static void main(String[] args) {
/**
  * 注意: 私钥加密必须公钥解密 公钥加密必须私钥解密
  * // 正常在开发中的时候,后端开发人员生成好密钥对,服务器端保存私钥 客户端保存公钥
  */
System.out.println("-------------生成两对秘钥,分别发送方和接收方保管-------------");
RSAUtil.generateKey();
System.out.println("公钥:" + RSAUtil.publicKey);
System.out.println("私钥:" + RSAUtil.privateKey);

System.out.println("-------------私钥加密公钥解密-------------");
  String textsr = "11111111";
  // 私钥加密
  String cipherText = RSAUtil.encryptByprivateKey(textsr,
  RSAUtil.privateKey, Cipher.ENCRYPT_MODE);
  System.out.println("私钥加密后:" + cipherText);
  // 公钥解密
  String text = RSAUtil.encryptByPublicKey(cipherText,
  RSAUtil.publicKey, Cipher.DECRYPT_MODE);
  System.out.println("公钥解密后:" + text);

System.out.println("-------------公钥加密私钥解密-------------");
// 公钥加密
String textsr2 = "222222";

String cipherText2 = RSAUtil.encryptByPublicKey(textsr2, RSAUtil.publicKey, Cipher.ENCRYPT_MODE);
System.out.println("公钥加密后:" + cipherText2);
// 私钥解密
String text2 = RSAUtil.encryptByprivateKey(cipherText2, RSAUtil.privateKey, Cipher.DECRYPT_MODE);
System.out.print("私钥解密后:" + text2 );
}

}


四、使用加签名方式,防止数据篡改

客户端:请求的数据分为2部分(业务参数,签名参数),签名参数=md5(业务参数)

服务端:验证md5(业务参数)是否与签名参数相同
————————————————
作者:单身贵族男
来源:https://blog.csdn.net/zhou920786312/article/details/95536556

收起阅读 »

一个向上帝买了挂的男人!

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。 在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不...
继续阅读 »

pic_8ae2a872.png

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。

在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不知道他是谁。 约翰·冯·诺依曼是20世纪最有影响力的人物之一。他可能比过去150年中任何一位伟大的思想更直接地影响了你的生活,他的研究涉及从量子力学到气候科学的一切。 pic_1f8dc88d.png 冯·诺依曼最大的贡献是现代计算机,他采用了图灵奠定的卓越理论框架,并实际提出了为几乎所有数字计算机提供动力的架构:冯·诺依曼架构。 更有争议的是,冯·诺依曼在二战期间对曼哈顿计划做出了重大贡献,包括完善原子弹本身的设计和对其功能至关重要的机制。 pic_7ec06076.png 与曼哈顿计划的其他一些老兵不同,冯·诺依曼从未对自己在该计划中的角色表示遗憾,甚至在冷战期间推动了「同归于尽」的政策。 至少可以说,约翰·冯·诺依曼是一个复杂的人物,但他在20世纪几乎无人能及,可以说他对现代世界的责任比他同时代的任何人都大。

神童降世

约翰·冯·诺依曼于1903年12月28日出生在匈牙利首都布达佩斯。 冯·诺依曼的父亲是银行家,母亲是奥匈贵族的女儿,他的父母富有且受人尊敬。
pic_34745df7.png
1913年,奥匈帝国皇帝弗朗茨·约瑟夫授予冯·诺依曼的父亲贵族地位,并给了这个家族一个世袭的头衔「马吉塔」,即现在的罗马尼亚马吉塔。 这个头衔纯粹是尊称,因为这个家庭与这个地方没有任何联系,但这是冯·诺依曼一生都会坚持的。 年轻的冯·诺依曼在同龄人中,被认为是真正的神童,尤其是在数学方面。人们认为他有摄影一般的记忆力,这帮助他从很小的时候就吸收了大量的知识。 pic_9118426a.png 冯·诺伊曼11岁时与表妹 Katalin Alcsuti在一起 六岁时,他就开始在头脑中进行两个八位数的除法,八岁时,他已经掌握了微积分。 pic_1875c648.png 他的父亲认为,他所有的孩子都需要说除母语匈牙利语以外的欧洲主要语言,所以冯·诺依曼学习了英语、法语、意大利语和德语。 小时候,他对历史也有很深的兴趣,并阅读了德国历史学家威廉·昂肯的整部46卷专著《通史》。
pic_b93227b9.png
德国历史学家威廉·昂肯 在老师的鼓励下,冯·诺依曼在学习上取得了优异的成绩,但他的父亲不相信数学家的职业会带来经济上的利益。 相反,冯·诺依曼和他的父亲达成共识,同意冯·诺依曼从事化学工程,他17岁去柏林学习,后来在苏黎世学习。 化学似乎是冯·诺依曼少数几个不感兴趣的领域之一,尽管他确实获得了苏黎世化学工程文凭,同时还获得了数学博士学位。
pic_c0043b57.png

职业生涯早期

约翰·冯·诺依曼很早就发表了论文,从20岁开始,他写了一篇定义序数的论文,这仍然是我们今天使用的定义。 他写了关于集合论的博士论文,并在一生中对该领域做出了若干贡献。
到1927年,冯·诺依曼已经发表了12篇著名的数学论文。到1929年,他已经出版了32部作品,以大约一个月一篇学术论文的速度写出了很多重要的工作。 pic_1cb6f516.png 1928年,他成为柏林大学的一名私 人教师,也是柏林大学历史上获得该职位最年轻的人。 这个职位使他能够在大学里讲课,直到1929年他成为汉堡大学的一名私人教师。 冯·诺依曼在父亲于1929年去世后,皈依了天主教。1930年元旦,约翰·冯·诺依曼与布达佩斯大学经济学学生玛丽埃塔·科维西结婚,并于1935年与她生下了他唯一的孩子玛丽娜。 虽然冯·诺依曼似乎注定要在德国科学院从事一个有前途的职业,但在1929年10月,他获得了新泽西州普林斯顿大学的一个职位,他最终接受了这个职位,并于1930年与妻子一起前往美国。

移民美国

到1933年,约翰·冯·诺依曼成为新成立的普林斯顿高等研究院最初的六名数学教授之一,他也将在这个岗位上度过余生。 当他搬到新泽西时,像他之前的许多美国移民一样,冯·诺依曼将他的匈牙利名字英语化了(由玛吉塔伊·诺依曼·亚诺斯变成约翰·冯·诺依曼,使用德国式的世袭尊称)。 1937年,他和妻子离婚,第二年冯·诺依曼再婚,这次是和克拉拉·丹,他在第二次世界大战前最后一次访问匈牙利时,在布达佩斯第一次见到了克拉拉·丹。 pic_75b34244.png 1937年,冯·诺依曼成为美国公民,1939年,他的母亲、兄弟姐妹和姻亲也都移民到了美国(他的父亲早些时候去世了)。

战争年代与「曼哈顿计划」

约翰·冯·诺依曼对历史最重要的贡献之一是他在第二次世界大战期间对曼哈顿计划的研究。 一如既往,冯·诺依曼不能让数学挑战得不到解决,更困难的问题之一是如何模拟爆炸的影响。 冯·诺依曼在20世纪30年代投身于这些问题的研究,并成为该领域的专家。如果他有特长的话,那应该是研究聚能装药(Shaped Charges,用于爆破)领域的数学问题,聚能装药是用来控制和引导爆炸能量的。 pic_434c04b8.png 这使他与美国军方,特别是美国海军进行了相当多的定期磋商。当曼哈顿计划在20世纪40年代初开始工作时,冯·诺依曼因其专业知识而被招募。 1943年,冯·诺依曼对曼哈顿计划产生了最重大、最持久的影响。 pic_dd9691a8.png 曼哈顿计划成员 当时,设计原子弹的洛斯阿拉莫斯实验室发现,钚-239(该项目使用的裂变材料之一)与实验室的工作炸弹设计不兼容。 实验室的物理学家塞斯·尼德迈尔一直在研究一种独立的内爆型炸弹设计,这种设计很有希望,但许多人认为它不可行。
pic_bbad35a1.png
核弹内爆机制的动画 要引爆核爆炸,需要在炸弹的反应物中引发失控的裂变链式反应。链式反应的速度是指数级的,因此控制爆炸足够长的时间,使足够的裂变材料进行所需的反应,是一项重大挑战。 内爆型炸弹需要更复杂的控制来产生反应,但它也不需要像洛斯阿拉莫斯开发的枪型炸弹设计那样耗费那么多的材料。 内爆型装置使用一系列受控的常规爆炸来压缩其核心中的裂变反应物。 在这种压力下,裂变材料迅速开始核裂变链式反应,通过内爆的力量保持在原位,并允许更多的裂变材料释放其能量。
pic_32a52999.png
控制这些爆炸以产生精确的内爆力来产生期望的反应是一项很大的挑战,而冯·诺依曼以极大的热情接受了它。 他认为,使用较少的球形材料并通过内爆力适当压缩,可以产生更有效的爆炸,尽可能多地使用现有的裂变材料。 他经常是少数几个主张内爆方法的人之一,并最终计算出了一个数学公式,表明如果内爆能以至少95%的精度保持球形的几何形状,该方法就是可以实现的。 冯·诺依曼还计算出,如果爆炸在目标上方一定距离引爆,而不是在击中地面时引爆,爆炸的有效性将会提高。 这大大增加了原子弹的杀伤力,也减少了爆炸产生的尘埃量。 之后,冯·诺依曼被选为科学顾问团队的一员,他们就炸弹的可能目标咨询军方。 冯·诺依曼建议将目标定为日本京都,因为京都作为文化之都,其毁灭可能足以迫使战争迅速结束。 提出这一建议的不止他一个人,但战争部长亨利·史汀生否决了将目标对准京都这个提议,因为那里有许多历史建筑和重要的宗教圣地,所以最后选择了广岛和长崎。 1945年7月16日的三位一体试验中,冯·诺依曼在场,当时第一枚原子武器被引爆。广岛和长崎被炸后,日本投降,第二次世界大战结束。 pic_87622a6c.png 三位一体试验 与曼哈顿计划中同时代的一些人不同,冯·诺依曼似乎没有任何反思的痛苦、遗憾,甚至对他在原子弹方面的工作也没有一丝怀疑。 事实上,冯·诺依曼是核武器发展和相互保证毁灭(MAD)理论的最有力支持者之一,认为这是防止另一场灾难性世界大战的唯一方法。

核武器的「轻量化」思想

像战后初期的许多美国人一样,冯·诺依曼担心美国在核武器发展方面落后于苏联。 到上世纪40 年代末到50 年代初,用战略轰炸机向敌人投掷更多原子弹的理念,逐渐被新的火箭技术所取代。
冯·诺依曼认为,导弹是核武器的未来,由于他与参与苏联武器研制的德国科学家有过接触,他知道苏联对此问题的看法与他是一样的。 军备竞赛开始了,美苏两国把氢弹越做越小,可以装入洲际弹道导弹的弹头,冯·诺依曼积极为美国效力,努力缩小与苏联的「导弹差距」。 战后,冯·诺依曼在原子能委员会任职,为政府和军方提供核技术开发和战略方面的建议,并被广泛认为是「确保相互毁灭」理论(MAD)的设计者,而 MAD 在冷战期间确实被政府采纳,成为事实上的美国国策。

建造第一台真正的计算机

pic_75fe701f.png 冯·诺依曼在上世纪30年代初遇到了艾伦·图灵,当时图灵正在普林斯顿攻读博士学位。1937年,图灵发表了具有里程碑意义的论文《论可计算数》,奠定了现代计算的理论基础。
pic_3623b10a.png 冯·诺依曼很快认识到了图灵这一发现的重要性,并在30年代推动了计算机科学的发展。在普林斯顿大学,他和图灵围绕人工智能的思想曾进行了长时间的讨论。 作为一名数学家,冯·诺依曼从更抽象的角度研究计算机科学,另一个原因也是因为在30年代时,并没有真正可以工作的计算机。 在二战结束后,这种情况很快就改变了。 pic_bf84e043.png 冯·诺依曼深入参与了第一台可编程电子计算机「ENIAC」的开发,这台计算机能够识别和决定其他数据操作规则集,而不是最初使用的规则集。是冯诺依曼将 ENIAC 修改为作为存储程序机器运行。 后者让使我们今天理解的现代程序成为可能。冯·诺依曼本人编写了几个在 ENIAC 上运行的首批程序,并用这些程序模拟原子能委员会的部分核武器研究。 毫无疑问,冯·诺依曼对计算机科学领域最持久的贡献是在当今运行的每台计算机中使用的两个基本概念:冯·诺依曼体系架构和存储程序概念。 pic_e701e538.png 冯·诺依曼架构涉及构成计算机的物理电子电路的组织方式。按照这种方式构建的计算机被称为「冯·诺依曼机」。该架构由算术和逻辑单元 (ALU)、控制单元和临时存储器寄存器组成,它们共同构成了中央处理器 (CPU)。 CPU 连接到内存单元,该内存单元包含将要由CPU处理和操作的所有数据。CPU还连接到输入和输出设备,以根据需要更改数据,并检索运行程序的结果。 自 1945 年冯诺依曼提出这一架构以来,直到今天,它基本上仍是当今大多数通用计算机的运行方式,几乎没有改变。 另一项重大创新与冯·诺依曼架构有关,即存储程序概念,也就是说,被操作或处理的数据,以及描述如何操纵和处理该数据的程序,都存储在计算机的内存中。 这两项相互交织的创新实现了图灵机的理论框架,实际上将它们变成了可以用来计算工资、火炮轨迹、游戏、互联网等几乎所有一切数据的机器。

对其他领域的杰出贡献

除了数学和计算机科学之外,冯·诺依曼一生都对其他几个领域也做出了重大贡献。
在早期的职业生涯中,冯·诺依曼为新兴的量子力学领域做出了重大贡献。 pic_e52acca3.png 1932年,他和保罗·狄拉克在《量子力学的数学基础》一书中发表了狄拉克-冯·诺依曼公理,这是该领域的第一个完整的数学框架。在这本书中,他还提出了量子逻辑的形式系统,也是同类体系中的首创。 冯·诺依曼还将博弈论确立为一门严谨的数学学科,这无疑影响了他后来关于MAD理论的地缘政治战略工作。 pic_a9fc558f.png 冯·诺依曼的博弈论中包含这样一个观点,即在广泛的博弈类别中,总是有可能找到一个平衡,任何参与者都不应单方面偏离这个平衡。 在生命科学领域,冯·诺依曼对元胞自动机的自我复制进行了彻底的数学分析,主要是构造函数、正在构建的事物以及构造函数构建所讨论事物所遵循的蓝图之间的关系。该分析描述了一种自我复制的机器,它是在40年代设计的,没有使用计算机。 冯·诺依曼的数学造诣也惠及气候科学。1950年,他编写了第一个气候建模程序,并使用 ENIAC 使用数值数据进行了世界上第一个气象预测。 冯·诺依曼预计,全球变暖是人类活动的结果,他在1955年写道: 「工业燃烧煤和石油释放到大气中的二氧化碳,可能已经充分改变了大气的成分,导致全球普遍变暖约1华氏度。」 冯·诺依曼也被认为是第一个描述「技术奇点」的人。冯·诺依曼的朋友斯坦·乌拉姆 (Stan Ulam) 后来描述了与他的一次对话,这次对话在今天听起来非常有先见之明。 pic_ccf72d4b.png 斯坦·乌拉姆、理查德·费曼和冯·诺依曼在一起 乌拉姆回忆说:「有一次谈话集中在不断加速的技术进步和人类生活方式的变化上,这让我们看到了人类历史上一些本质上的奇点。一旦超越了这些奇点,我们所熟知的人类事务就将无法继续下去了。」

冯·诺依曼的去世,和他的光辉遗产

1955 年,冯·诺依曼在看医生时发现他的锁骨上长了一块肉,他被诊断患有癌症,但他并没有充分接受这个事实。 众所周知,冯·诺依曼对即将到来的结局感到恐惧。他的一生好友尤金·维格纳 (Eugene Wigner) 写道:
pic_4c140e80.png 「当冯·诺依曼意识到自己病入膏肓时,他的逻辑迫使他意识到,自己即将不复存在,因此也不再有思想……亲眼目睹这一过程是令人心碎的,所有的希望都消失了,即将到来的命运尽管难以接受,但已经不可避免。」 冯·诺依曼的病情在1956年持续恶化,最终被送进了华盛顿特区的沃尔特里德陆军医疗中心。为防止泄密,军方对他实施了特殊的安全措施。 冯·诺依曼邀请一位天主教神父在他临终前商量,并接受了他的临终仪式安排。不过这位神父本人表示,冯诺依曼看上去似乎并没有从仪式中得到安慰。 1957年2月8日,冯·诺依曼因癌症逝世,享年53岁,他被安葬在新泽西州的普林斯顿公墓。 关于冯·诺依曼的癌症是否与他在「曼哈顿计划」期间遭受辐射有关,人们一直存在争议,但毫无争议的是,人类过早地失去了当代最伟大的科学巨人之一。 pic_1628bda4.png 冯·诺依曼的助手 P.R. Halmos 在1973年写道: 「人类的英雄有两种:一种和我们所有人一样,但更加相似,另一种显然具有一些「超人」的特质。 我们都可以跑步,我们中的一些人可以在不到4分钟的时间内跑完一英里。但有些事,我们大多数人一辈子都无法做到。冯·诺依曼的伟大贡献是惠及全人类的。在某些时候,我们或多或少都能清晰地思考,但冯·诺依曼的清晰思维始终比我们大多数人高出好几个数量级。」 冯·诺依曼的才华是毋庸置疑的,尽管他留下的遗产,尤其是核武器方面的贡献,比他的朋友和崇拜者愿意承认的要复杂得多。 无论我们最终如何看待冯·诺依曼和他的成就,我们都可以肯定地说,在未来一代人甚至几代人的时间里,都不太可能出现像他一样,对人类历史产生如此重大影响的人了。

转自:新智元 | David 小咸鱼
原文链接:https://interestingengineering.com/john-von-neumann-human-the-computer-behind-project-manhattan

收起阅读 »

使用 Nginx 构建前端日志统计服务

之前的几篇文章都是关于低代码平台的。这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。先放一下整体流程图吧:日志收集在常见的埋点方案中,...
继续阅读 »

背景

之前的几篇文章都是关于低代码平台的。


这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。

今天就来说一下其中的统计服务:目的主要是为了实现 H5 页面的分渠道统计(其实不仅仅是分渠道统计,核心是想做一个自定义事件统计服务,只是目前有分渠道统计的需求),查看每个渠道具体的 PV 情况。(具体会在 url 上面体现,会带上页面名称、id、渠道类型等)

先放一下整体流程图吧:


日志收集

常见的日志收集方式有手动埋点和自动埋点,这里我们不关注于如何收集日志,而是如何将收集的日志的发送到服务器。

在常见的埋点方案中,通过图片来发送埋点请求是一种经常被采纳的,它有很多优势:

  • 没有跨域

  • 体积小

  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)

  • 执行过程无阻塞

这里的方案就是在 nginx 上放一张 1px * 1px 的静态图片,然后通过访问该图片(http://xxxx.png?env=xx&event=xxx),并将埋点数据放在query参数上,以此将埋点数据落到nginx日志中。

iOS 上会限制 get 请求的 url 长度,但我们这里真实场景发送的数据不会太多,所以目前暂时采用这种方案

这里简单阐述一下为什么图片地址的query key 要这么设计,如果单纯是为了统计渠道和作品,很有可能会把key设计为channelworkId这种,但上面也说到了,我们是想做一个自定义事件统计服务,那么就要考虑字段的可扩展性,字段应更有通用语义。所以参考了很多统计服务的设计,这里采用的字段为:

  • env

  • event

  • key

  • value

之后每次访问页面,nginx就会自动记录日志到access_log中。

有了日志,下面我们来看下如何来对其进行拆分。

日志拆分

为何要拆分日志

access.log日志默认不会拆分,会越积累越多,系统磁盘的空间会被消耗得越来越多,将来可能面临着日志写入失败、服务异常的问题。

日志文件内容过多,对于后续的问题排查和分析也会变得很困难。

所以日志的拆分是有必要也是必须的。

如何拆分日志

我们这里拆分日志的核心思路是:将当前的access.log复制一份重命名为新的日志文件,之后清空老的日志文件。

视流量情况(流量越大日志文件积累的越快),按天、小时、分钟来拆分。可以把access.log按天拆分到某个文件夹中。

log_by_day/2021-12-19.log
log_by_day/2021-12-20.log
log_by_day/2021-12-21.log

但上面的复制 -> 清空操作肯定是要自动处理的,这里就需要启动定时任务,在每天固定的时间(我这里是在每天凌晨 00:00)来处理。

定时任务

其实定时任务不仅在日志拆分的时候会用到,在后面的日志分析和日志清除都会用到,这里先简单介绍一下,最终会整合拆分、分析和清除。

linux中内置的cron进程就是来处理定时任务的。在node中我们一般会用node-schedulecron来处理定时任务。

这里使用的是cron

/**
   cron 定时规则 https://www.npmjs.com/package/cron
   *    *    *    *    *    *
           
           
            day of week (0 - 6) (Sun-Sat)
          └───── month (1 - 12)
        └────────── day of month (1 - 31)
      └─────────────── hour (0 - 23)
    └──────────────────── minute (0 - 59)
  └───────────────────────── second (0 - 59)
*/

具体使用方式就不展开说明了。

编码

有了上面这些储备,下面我就来写一下这块代码,首先梳理下逻辑:

1️⃣ 读取源文件 access.log

2️⃣ 创建拆分后的文件夹(不存在时需自动创建)

3️⃣ 创建日志文件(天维度,不存在时需自动创建)

4️⃣ 拷贝源日志至新文件

5️⃣ 清空 access.log

/**
* 拆分日志文件
*
* @param {*} accessLogPath
*/
function splitLogFile(accessLogPath) {
 const accessLogFile = path.join(accessLogPath, "access.log");

 const distFolder = path.join(accessLogPath, DIST_FOLDER_NAME);
 fse.ensureDirSync(distFolder);

 const distFile = path.join(distFolder, genYesterdayLogFileName());
 fse.ensureFileSync(distFile);
 fse.outputFileSync(distFile, ""); // 防止重复,先清空

 fse.copySync(accessLogFile, distFile);

 fse.outputFileSync(accessLogFile, "");
}

日志分析

日志分析就是读取上一步拆分好的文件,然后按照一定规则去处理、落库。这里有一个很重要的点要提一下:node在处理大文件或者未知内存文件大小的时候千万不要使用readFile,会突破 V8 内存限制。正是考虑到这种情况,所以这里读取日志文件的方式应该是:createReadStream创建一个可读流交给 readline 逐行读取处理

readline

readline 模块提供了用于从可读流每次一行地读取数据的接口。 可以使用以下方式访问它:

const readline = require("readline");

readline 的使用也非常简单:创建一个接口实例,传入对应的参数:

const readStream = fs.createReadStream(logFile);
const rl = readline.createInterface({
 input: readStream,
});

然后监听对应事件即可:

rl.on("line", (line) => {
 if (!line) return;

 // 获取 url query
 const query = getQueryFromLogLine(line);
 if (_.isEmpty(query)) return;

 // 累加逻辑
 // ...
});
rl.on("close", () => {
 // 逐行读取结束,存入数据库
 const result = eventData.getResult();
 resolve(result);
});

这里用到了lineclose事件:

  • line事件:每当 input 流接收到行尾输入(\n、\r 或 \r\n)时,则会触发 'line' 事件

  • close事件:一般在传输结束时会触发该事件

逐行分析日志结果

了解了readline 的使用,下面让我们来逐行对日志结果进行分析吧。

首先来看下access.log中日志的格式:

我们取其中一行来分析:

127.0.0.1 - - [19/Feb/2021:15:22:06 +0800] "GET /event.png?env=h5&event=pv&key=24&value=2 HTTP/1.1" 200 5233 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" "-"

我们要拿到的就是urlquery部分,也就是我们在h5中自定义的数据。

通过正则匹配即可:

const reg = /GET\s\/event.png\?(.+?)\s/;
const matchResult = line.match(reg);
console.log("matchResult", matchResult);

const queryStr = matchResult[1];
console.log("queryStr", queryStr);

打印结果为:

queryStr可通过node中的querystring.parse()来处理:

const query = querystring.parse(queryStr);

console.log('query', query)
{
env: 'h5',
event: 'pv',
key: '24',
value: '2'
}

剩下的就是对数据做累加处理了。

但如何去做累加,我们要想一下,最开始也说了是要去做分渠道统计,那么最终的结果应该可以清晰的看到两个数据:

  • 所有渠道的数据

  • 每个渠道单独的数据

只有这样的数据对于运营才是有价值的,数据的好坏也直接决定了后面在每个渠道投放的力度。

这里我参考了 Google Analytics中的多渠道漏斗的概念,由上到下分维度记录每个维度的数据,这样就可以清晰的知道每个渠道的情况了。

具体实现也不麻烦,我们先来看下刚刚从一条链接中得到的有用数据:

{
 env: 'h5',
 event: 'pv',
 key: '24',
 value: '2'
}

这里的env代表环境,这里统计的都是来源于h5页面,所以envh5,但是为了扩展,所以设置了这个字段。

event表示事件名称,这里主要是统计访问量,所以为pv

key是作品 id。

value是渠道 code,目前主要有:1-微信、2-小红书、3-抖音。

再来看下最终统计得到的结果吧:

{
 date: '2021-12-21',
 key: 'h5',
 value: { num: 1276}
}
{
 date: '2021-12-21',
 key: 'h5.pv',
 value: { num: 1000}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12',
 value: { num: 200}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.1',
 value: { num: 56}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.2',
 value: { num: 84}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.3',
 value: { num: 60}
}

这是截取了2021-12-21当天的数据,我给大家分析一波:

1️⃣ h5:当天 h5 页面的自定义事件上报总数为 1276

2️⃣ h5.pv:其中 所有 pv(也就是 h5.pv)为 1000

3️⃣ h5.pv.12:作品 id 为 12 的 pv 一共有 200

4️⃣ h5.pv.12.1:作品 id 为 12 的在微信渠道的 pv 为 56

5️⃣ h5.pv.12.2:作品 id 为 12 的在小红书渠道的 pv 为 84

6️⃣ h5.pv.12.2:作品 id 为 12 的在抖音渠道的 pv 为 60

这样就能清楚的得到某一天某个作品在某条渠道的访问情况了,后续再以这些数据为支撑做成可视化报表,效果就一目了然了。

统计结果入库

目前这部分数据是放在了mongoDB中,关于node中使用mongoDB就不展开说了,不熟悉的可以参考我另外一篇文章Koa2+MongoDB+JWT 实战--Restful API 最佳实践

这里贴下model吧:

/**
* @description event 数据模型
*/
const mongoose = require("../db/mongoose");

const schema = mongoose.Schema(
{
   date: Date,
   key: String,
   value: {
     num: Number,
  },
},
{
   timestamps: true,
}
);

const EventModel = mongoose.model("event_analytics_data", schema);

module.exports = EventModel;

日志删除

随着页面的持续访问,日志文件会快速增加,超过一定时间的日志文件存在的价值也不是很大,所以我们要定期清除日志文件。

这个其实比较简单,遍历文件,因为文件名都是以日期命名的(格式:2021-12-14.log),所以只要判断时间间隔大于 90 天就删除日志文件。

贴一下核心实现:

// 读取日志文件
const fileNames = fse.readdirSync(distFolder);
fileNames.forEach((fileName) => {
 try {
   // fileName 格式 '2021-09-14.log'
   const dateStr = fileName.split(".")[0];
   const d = new Date(dateStr);
   const t = Date.now() - d.getTime();
   if (t / 1000 / 60 / 60 / 24 > 90) {
     // 时间间隔,大于 90 天,则删除日志文件
     const filePath = path.join(distFolder, fileName);
     fse.removeSync(filePath);
  }
} catch (error) {
   console.error(`日志文件格式错误 ${fileName}`, error);
}
});

定时任务整合

到这里,日志的拆分、分析和清除都说完了,现在要用cron来对他们做整合了。

首先来创建定时任务:

function schedule(cronTime, onTick) {
 if (!cronTime) return;
 if (typeof onTick !== "function") return;

 // 创建定时任务
 const c = new CronJob(
   cronTime,
   onTick,
   null, // onComplete 何时停止任务
   true, // 初始化之后立刻执行
   "Asia/Shanghai" // 时区
);

 // 进程结束时,停止定时任务
 process.on("exit", () => c.stop());
}

然后每一阶段都在不同的时间阶段去处理(定时拆分 -> 定时分析 -> 定时删除)

定时拆分

function splitLogFileTiming() {
 const cronTime = "0 0 0 * * *"; // 每天的 00:00:00
 schedule(cronTime, () => splitLogFile(accessLogPath));
 console.log("定时拆分日志文件", cronTime);
}

定时分析并入库

function analysisLogsTiming() {
 const cronTime = "0 0 3 * * *"; // 每天的 3:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => analysisLogsAndWriteDB(accessLogPath));
 console.log("定时分析日志并入库", cronTime);
}

定时删除

function rmLogsTiming() {
 const cronTime = "0 0 4 * * *"; // 每天的 4:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => rmLogs(accessLogPath));
 console.log("定时删除过期日志文件", cronTime);
}

然后在应用入口按序调用即可:

// 定时拆分日志文件
splitLogFileTiming();
// 定时分析日志并入库
analysisLogsTiming();
// 定时删除过期日志文件
rmLogsTiming();

总结

ok,到这里,一个简易的统计服务就完成了。

作者:前端森林
来源:https://segmentfault.com/a/1190000041184489

收起阅读 »

偷偷看了同事的代码找到了优雅代码的秘密

真正的大师永远怀着一颗学徒的心引言对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程...
继续阅读 »

真正的大师永远怀着一颗学徒的心

引言

对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程序猿在这个工程中写好代码的初始热情都没了。相反,如果clone下的代码结构清晰,代码优雅易懂,那么你在写代码的时候都不好意思写烂代码。这其中的差别相信工作过的同学都深有体会,那么我们看了那么多代码之后,到底什么样的代码才是好代码呢?它们有没有一些共同的特征或者原则?本文通过阐述优雅代码的设计原则来和大家聊聊怎么写好代码。

代码设计原则

好代码是设计出来的,也是重构出来的,更是不断迭代出来的。在我们接到需求,经过概要设计过后就要着手进行编码了。但是在实际编码之前,我们还需要进行领域分层设计以及代码结构设计。那么怎么样才能设计出来比较优雅的代码结构呢?有一些大神们总结出来的优雅代码的设计原则,我们分别来看下。

SRP

所谓SRP(Single Responsibility Principle)原则就是职责单一原则,从字面意思上面好像很好理解,一看就知道什么意思。但是看的会不一定就代表我们就会用,有的时候我们以为我们自己会了,但是在实际应用的时候又会遇到这样或者那样的问题。原因就是实际我们没有把问题想透,没有进行深度思考,知识还只是知识,并没有转化为我们的能力。就比如这里所说的职责单一原则指的是谁的单一职责,是类还是模块还是域呢?域可能包含多个模块,模块也可以包含多个类,这些都是问题。

为了方便进行说明,这里以类来进行职责单一设计原则的说明。对于一个类来说,如果它只负责完成一个职责或者功能,那么我们可以说这个类时符合单一职责原则。请大家回想一下,其实我们在实际的编码过程中,已经有意无意的在使用单一职责设计原则了。因为实际它是符合我们人思考问题的方式的。为什么这么说呢?想想我们在整理衣柜的时候,为了方便拿衣服我们会把夏天的衣服放在一个柜子中,冬天的衣服放在一个柜子。这样季节变化的时候,我们只要到对应的柜子直接拿衣服就可以了。否则如果冬天和夏天的衣服都放在一个柜子中,我们找衣服的时候可就费劲了。放到软件代码设计中,我们也需要采用这样的分类思维。在进行类设计的时候,要设计粒度小、功能单一的类,而不是大而全的类。

举个栗子,在学生管理系统中,如果一个类中既有学生信息的操作比如创建或者删除动作,又有关于课程的创建以及修改动作,那么我们可以认为这个类时不满足单一职责的设计原则的,因为它将两个不同业务域的业务混杂在了一起,所以我们需要进行拆分,将这个大而全的类拆分为学生以及课程两个业务域,这样粒度更细,更加内聚。

笔者根据自身的经验,总结了需要考虑进行单一职责拆分的几个场,希望对大家判断是否需要进行拆分有个简单的判断的标准: 1、不同的业务域需要进行拆分,就像上面的例子,另外如果与其他类的依赖过多,也需要考虑是不是应该进行拆分; 2、如果我们在类中编写代码的时候发现私有方法具有一定的通用性,比如判断ip是不是合法,解析xml等,那我们可以考虑将这些方法抽出来形成公共的工具类,这样其他类也可以方便的进行使用。 另外单一职责的设计思想不止在代码设计中使用,我们在进行微服务拆分的时候也会一定程度的遵循这个原则。

OCP

OCP(Open Closed Principle)即对修改关闭,对扩展开放原则,个人觉得这是设计原则中最难的原则。不仅理解起来有一定的门槛,在实际编码过程中也是不容易做到的。 首先我们得先搞清楚这里的所说的修改以及扩展的区别在什么地方,说实话一开始看到这个原则的时候,我总觉得修改和开放说的不是一个意思嘛?想来想去都觉得有点迷糊。后来在不断的项目实践中,对这个设计原则的理解逐渐加深了。

设计原则中所说的修改指的是对原有代码的修改,而扩展指的是在原有代码基础上的能力的扩展,并不修改原先已有的代码。这是修改与扩展的最大的区别,一个需要修改原来的代码逻辑,另一个不修改。因此才叫对修改关闭但是对扩展开放。弄清楚修改和扩展的区别之后,我们再想一想为什么要对修改关闭,而要对扩展开放呢? 我们都知道软件平台都是不断进行更新迭代的,因此我们需要不断在原先的代码中进行开发。那么就会涉及到一个问题如果我们的代码设计的不好,扩展性不强,那么每次进行功能迭代的时候都会修改原先已有的代码,有修改就有可能引入bug,造成系统平台的不稳定。因此我们为了平台的稳定性,需要对修改关闭。但是我们要添加新的功能怎么办呢?那就是通过扩展的方式来进行,因此需要实现对扩展开放。

这里我们以一个例子来进行说明,否则可能还是有点抽象。在一个监控平台中,我们需要对服务所占用CPU、内存等运行信息进行监控,第一版代码如下。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

代码逻辑很简单,就是根据对应的告警规则中的阈值判断是否达到触发告警通知的条件。如果此时来了个需求,需要增加判断的条件,就是根据服务对应的状态,判断需不需要进行告警通知。我们来先看下比较low的修改方法。我们在checkServiceStatus方法中增加了服务状态的参数,同事在方法中增加了判断状态的逻辑。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

很显然这种修改方法非常的不友好,为什么这么说呢?首先修改了方法参数,那么调用该方法的地方可能也需要修改,另外如果改方法有单元测试方法的话,单元测试用例必定也需要修改,在原有测试过的代码中添加新的逻辑,也增加了bug引入的风险。因此这种修改的方式我们需要进行避免。那么怎么修改才能够体现对修改关闭以及对扩展开放呢? 首先我们可以先将关于服务状态的属性抽象为一个ServiceStatus 实体,在对应的检查方法中以ServiceStatus 为入参,这样以后如果还有服务状态的属性增加的话,只需要在ServiceStatus 中添加即可,并不需要修改方法中的参数以及调用方法的地方,同样单元测试的方法也不用修改。

@Data
public class ServiceStatus {
   String serviecName;
   int cpu;
   int memory;
   int status;

}

另外在检测方法中,我们怎么修改才能体现可扩展呢?而不是在检测方法中添加处理逻辑。一个比较好的实现方式就是通过抽象检测方法,具体的实现在各个实现类中。这样即使新增检测逻辑,只需要扩展检测实现方法就可,不需要在修改原先代码的逻辑,实现代码的可扩展。

LSP

LSP(Liskov Substitution Principle)里氏替换原则,这个设计原则我觉得相较于前面的两个设计原则来说要简单些。它的内容为子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

我们怎么判断有没有违背LSP呢?我觉得有两个关键点可以作为判断的依据,一个是子类有没有改变父类申明需要实现的业务功能,另一个是否违反父类关于输入、输出以及异常抛出的规定。

ISP

ISP(Interface Segregation Principle)接口隔离原则,简单理解就是只给调用方需要的接口,它不需要的就不要硬塞给他了。这里我们举个栗子,以下是关于产品的接口,其中包含了创建产品、删除产品、根据ID获取产品以及更新产品的接口。如果此时我们需要对外提供一个根据产品的类别获取产品的接口,我们应该怎么办?很多同学会说,这还不简单,我们直接在这个接口里面添加根据类别查询产品的接口就OK了啊。大家想想这个方案有没有什么问题。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public class UserServiceImpl implements UserService { //...}

这个方案看上去没什么问题,但是再往深处想一想,外部系统只需要一个根据产品类别查询商品的功能,,但是实际我们提供的接口中还包含了删除、更新商品的接口。如果这些接口被其他系统误调了可能会导致产品信息的删除或者误更新。因此我们可以将这些第三方调用的接口都隔离出来,这样就不存在误调用以及接口能力被无序扩散的情况了。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public interface ThirdSystemProductService{
   List<Product> getProductByType(int type);
}

public class UserServiceImpl implements UserService { //...}

LOD

LOD(Law of Demeter)即迪米特法则,这是我们要介绍的最后一个代码设计法则了,光从名字上面上看,有点不明觉厉的感觉,看不出来到底到底表达个什么意思。我们可以来看下原文是怎么描述这个设计原则的。 Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers. 按照我自己的理解,这迪米特设计原则的最核心思想或者说最想达到的目的就是尽最大能力减小代码修改带来的对原有的系统的影响。所以需要实现类、模块或者服务能够实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。打个比方这就像抗战时期的的地下组织一样,相关联的聚合到一起,但是与外部保持尽可能少的联系,也就是低耦合。

总结

本文总结了软件代码设计中的五大原则,按照我自己的理解,这五大原则就是程序猿代码设计的内功,而二十三种设计模式实际就是内功催生出来的编程招式,因此深入理解五大设计原则是我们用好设计模式的基础,也是我们在平时设计代码结构的时候需要遵循的一些常见规范。只有不断的在设计代码-》遵循规范-》编写代码-》重构这个循环中磨砺,我们才能编写出优雅的代码。

作者:慕枫技术笔记
来源:https://juejin.cn/post/7046404022143549447

收起阅读 »

二维码扫码登录是什么原理

前几天看了极客时间一个二维码的视频,写的不错,这里总结下在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码...
继续阅读 »

前几天看了极客时间一个二维码的视频,写的不错,这里总结下

在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码,防止上当受骗。

二维码,大家再熟悉不过了

购物扫个码,吃饭扫个码,坐公交也扫个码

在扫码的过程中,大家可能会有疑问:这二维码安全吗?会不会泄漏我的个人信息?更深度的用户还会考虑:我的系统是不是也可以搞一个二维码来推广呢?

这时候就需要了解一下二维码背后的技术和逻辑了!

二维码最常用的场景之一就是通过手机端应用扫描PC或者WEB端的二维码,来登录同一个系统。 比如手机微信扫码登录PC端微信,手机淘宝扫码登录PC端淘宝。 那么就让我们来看一下,二维码登录是怎么操作的!

二维码登录的本质

二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!

  1. 告诉系统我是谁

  2. 向系统证明我是谁

比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;

那么扫码登录是怎么做到这两件事情的呢?我们一起来考虑一下

手机端应用扫PC端二维码,手机端确认后,账号就在PC端登录成功了!这里,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。

所以,第一件事情,告诉系统我是谁,是比较清楚的!

通过扫描二维码,把手机端的账号信息传递到PC端,至于是怎么传的,我们后面再说

第二件事情,向系统证明我是谁。扫码登录过程中,用户并没有去输入密码,也没有输入验证码,或者其他什么码。那是怎么证明的呢?

有些同学会想到,是不是扫码过程中,把密码传到了PC端呢? 但这是不可能的。因为那样太不安全的,客户端也根本不会去存储密码。我们仔细想一下,其实手机端APP它是已经登录过的,就是说手机端是已经通过登录认证。所说只要扫码确认是这个手机且是这个账号操作的,其实就能间接证明我谁。

认识二维码

那么如何做确认呢?我们后面会详细说明,在这之前我们需要先认识一下二维码! 在认识二维码之前我们先看一下一维码!

所谓一维码,也就是条形码,超市里的条形码--这个相信大家都非常熟悉,条形码实际上就是一串数字,它上面存储了商品的序列号。

二维码其实与条形码类似,只不过它存储的不一定是数字,还可以是任何的字符串,你可以认为,它就是字符串的另外一种表现形式,

在搜索引擎中搜索二维码,你可以找到很多在线生成二维码的工具网站,这些网站可以提供字符串与二维码之间相互转换的功能,比如 草料二维码网站

在左边的输入框就可以输入你的内容,它可以是文本、网址,文件........。然后就可以生成代表它们的二维码

你也可以把二维码上传,进行”解码“,然后就可以解析出二维码代表的含义

系统认证机制

认识了二维码,我们了解一下移动互联网下的系统认证机制。

前面我们说过,为了安全,手机端它是不会存储你的登录密码的。 但是在日常使用过程中,我们应该会注意到,只有在你的应用下载下来后,第一次登录的时候,才需要进行一个账号密码的登录, 那之后呢 即使这个应用进程被杀掉,或者手机重启,都是不需要再次输入账号密码的,它可以自动登录。

其实这背后就是一套基于token的认证机制,我们来看一下这套机制是怎么运行的,

  1. 账号密码登录时,客户端会将设备信息一起传递给服务端,

  2. 如果账号密码校验通过,服务端会把账号与设备进行一个绑定,存在一个数据结构中,这个数据结构中包含了账号ID,设备ID,设备类型等等

const token = {
 acountid:'账号ID',
 deviceid:'登录的设备ID',
 deviceType:'设备类型,如 iso,android,pc......',
}

然后服务端会生成一个token,用它来映射数据结构,这个token其实就是一串有着特殊意义的字符串,它的意义就在于,通过它可以找到对应的账号与设备信息,

  1. 客户端得到这个token后,需要进行一个本地保存,每次访问系统API都携带上token与设备信息。

  2. 服务端就可以通过token找到与它绑定的账号与设备信息,然后把绑定的设备信息与客户端每次传来的设备信息进行比较, 如果相同,那么校验通过,返回AP接口响应数据, 如果不同,那就是校验不通过拒绝访问

从前面这个流程,我们可以看到,客户端不会也没必要保存你的密码,相反,它是保存了token。可能有些同学会想,这个token这么重要,万一被别人知道了怎么办。实际上,知道了也没有影响, 因为设备信息是唯一的,只要你的设备信息别人不知道, 别人拿其他设备来访问,验证也是不通过的。

可以说,客户端登录的目的,就是获得属于自己的token。

那么在扫码登录过程中,PC端是怎么获得属于自己的token呢?不可能手机端直接把自己的token给PC端用!token只能属于某个客户端私有,其他人或者是其他客户端是用不了的。在分析这个问题之前,我们有必要先梳理一下,扫描二维码登录的一般步骤是什么样的。这可以帮助我们梳理清楚整个过程,

扫描二维码登录的一般步骤

大概流程

  1. 扫码前,手机端应用是已登录状态,PC端显示一个二维码,等待扫描

  2. 手机端打开应用,扫描PC端的二维码,扫描后,会提示"已扫描,请在手机端点击确认"

  3. 用户在手机端点击确认,确认后PC端登录就成功了

可以看到,二维码在中间有三个状态, 待扫描,已扫描待确认,已确认。 那么可以想象

  1. 二维码的背后它一定存在一个唯一性的ID,当二维码生成时,这个ID也一起生成,并且绑定了PC端的设备信息

  2. 手机去扫描这个二维码

  3. 二维码切换为 已扫描待确认状态, 此时就会将账号信息与这个ID绑定

  4. 当手机端确认登录时,它就会生成PC端用于登录的token,并返回给PC端

好了,到这里,基本思路就已经清晰了,接下来我们把整个过程再具体化一下

二维码准备

按二维码不同状态来看, 首先是等待扫描状态,用户打开PC端,切换到二维码登录界面时。

  1. PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端

  2. 服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定

  3. 然后把二维码ID返回给PC端

  4. PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)

  5. 为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息

二维码已经准好了,接下来就是扫描状态

扫描状态切换

  1. 用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID

  2. 再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端

  3. 服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端

  4. 因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描

那么为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。

在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的,

状态确认

最后就是状态的确认了。

  1. 手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认

  2. 服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户PC端登录的token

  3. 这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到用户登录的token

  4. 到这里,登录就成功了,后端PC端就可以用token去访问服务端的资源了

扫码动作的基础流程都讲完了,有些细节还没有深入介绍,

比如二维码的内容是什么?

  • 可以是二维码ID

  • 可以是包含二维码ID的一个url地址

在扫码确认这一步,用户取消了怎么处理? 这些细节都留给大家思考

总结

我们从登陆的本质触发,探索二维码扫码登录是如何做到的

  1. 告诉系统我是谁

  2. 向系统证明我谁

在这个过程中,我们先简单讲了两个前提知识,

  • 一个是二维码原理,

  • 一个是基于token的认证机制。

然后我们以二维码状态为轴,分析了这背后的逻辑: 通过token认证机制与二维码状态变化来实现扫码登录.

需要指出的是,前面的讲的登录流程,它适用于同一个系统的PC端,WEB端,移动端。

平时我们还有另外一种场景也比较常见,那就是通过第三方应用来扫码登录,比如极客时间/掘金 都可以选择微信/QQ等扫码登录,那么这种通过第三方应用扫码登录又是什么原理呢?

感兴趣的同学可以思考研究一下,欢迎在评论区留下你的见解。


作者:望道同学
来源:https://juejin.cn/post/6940976355097985032

收起阅读 »

Nginx 配置在线一键生成“神器”

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。网址:https://nginxconfig.io/操...
继续阅读 »

Nginx作为一个轻量级的HTTP服务器,相比Apache优势也是比较明显的,在性能上它占用资源少,能支持更高更多的并发连接,从而达到提高访问效率;在功能上它是一款非常优秀的代理服务器与负载均衡服务器;在安装配置上它安装,配置都比较简单。

但在实际的生产配置环境中,肯定会经常遇到需要修改、或者重新增加Nginx配置的问题,有的时候需求更是多种多样,修修改改经常会出现这样、那样的一些错误,特别的烦索。

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。

网址:https://nginxconfig.io/

NGINX Config 支持 HTTP、HTTPS、PHP、Python、Node.js、WordPress、Drupal、缓存、逆向代理、日志等各种配置选项。在线生成 Web 服务器 Nginx 配置文件。

操作配置也非常简单,你需要做的只需要2步:

  • 打开官方网站

  • 按需求配置相关参数

系统就会自动生成特定的配置文件。虽然界面是英文的,但是功能的页面做的非常直观,生成的Nginx格式规范。

案例展示

配置域名:mingongge.com 实现用户访问*.mingongge.com 域名时会自动跳转到 mingongge.com 此配置,并且开启http强制跳转到https的配置。

这时,Nginx的配置就会实时自动生成在下面,我把生成的配置复制过来,如下:

/etc/nginx/sites-available/mingongge.com.conf#文件名都给你按规则配置好了
server {
listen 443 ssl http2;

server_name mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

# security
include nginxconfig.io/security.conf;

# additional config
include nginxconfig.io/general.conf;
}

# subdomains redirect
server {
listen 443 ssl http2;

server_name *.mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

return 301 https://mingongge.com$request_uri;
}

# HTTP redirect
server {
listen 80;

server_name .mingongge.com;

include nginxconfig.io/letsencrypt.conf;

location / {
return 301 https://mingongge.com$request_uri;
}
}

非常的方便与快速。

官方还提供一些Nginx的基础优化配置,如下:

/etc/nginx/nginx.conf
# Generated by nginxconfig.io

user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
multi_accept on;
worker_connections 65535;
}

http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
client_max_body_size 16M;

# MIME
include mime.types;
default_type application/octet-stream;

# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

# load configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

还有基于安全的配置,如下:

/etc/nginx/nginxconfig.io/security.conf
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;

# . files
location ~ /\.(?!well-known) {
deny all;
}

都相当于是提供一些基础的模板配置,可以根据自己的实际需求去修改。

有了这个神器在手,再也不用为配置Nginx的各类配置而烦恼了!!

作者:民工哥

来源:https://www.bbsmax.com/A/kjdw09OwzN/

收起阅读 »

求解“微信群覆盖”的三种方法:暴力,染色,链表,并查集

(1) 题目简介;(3) 思路二:染色法;(5) 思路四:并查集法;(1) 每个微信群由一个唯一的gid标识;(3) 一个用户可以加入多个群;g1{u1, u2, u3}可以看到,用户u1加入了g1与g2两个群。gid和uid都是uint64;(1) ...
继续阅读 »



这是一篇聊算法的文章,从一个小面试题开始,扩展到一系列基础算法,包含几个部分:

(1) 题目简介;

(2) 思路一:暴力法;

(3) 思路二:染色法;

(4) 思路三:链表法;

(5) 思路四:并查集法;

除了聊方案,重点分享思考过程。文章较长,可提前收藏。


第一部分:题目简介


问题提出:求微信群覆盖


微信有很多群,现进行如下抽象:

(1) 每个微信群由一个唯一的gid标识;

(2) 微信群内每个用户由一个唯一的uid标识;

(3) 一个用户可以加入多个群;

(4) 群可以抽象成一个由不重复uid组成的集合,例如:

g1{u1, u2, u3}

g2{u1, u4, u5}

可以看到,用户u1加入了g1与g2两个群。

画外音:

gid和uid都是uint64;

集合内没有重复元素;


假设微信有M个群(M为亿级别),每个群内平均有N个用户(N为十级别).


现在要进行如下操作:

(1) 如果两个微信群中有相同的用户则将两个微信群合并,并生成一个新微信群;

例如,上面的g1和g2就会合并成新的群:

g3{u1, u2, u3, u4, u5};

画外音:集合g1中包含u1,集合g2中包含u1,合并后的微信群g3也只包含一个u1。

(2) 不断的进行上述操作,直到剩下所有的微信群都不含相同的用户为止

将上述操作称:求群的覆盖。


设计算法,求群的覆盖,并说明算法时间与空间复杂度。

画外音:你遇到过类似的面试题吗?


对于一个复杂的问题,思路肯定是“先解决,再优化”,大部分人不是神,很难一步到位。先用一种比较“笨”的方法解决,再看“笨方法”有什么痛点,优化各个痛点,不断升级方案。


第二部分:暴力法


拿到这个问题,很容易想到的思路是:

(1) 先初始化M个集合,用集合来表示微信群gid与用户uid的关系;

(2) 找到哪两个(哪些)集合需要合并;

(3) 接着,进行集合的合并;

(4) 迭代步骤二和步骤三,直至所有集合都没有相同元素,算法结束;


第一步,如何初始化集合?

set这种数据结构,大家用得很多,来表示集合:

(1) 新建M个set来表示M个微信群gid;

(2) 每个set插入N个元素来表示微信群中的用户uid;


set有两种最常见的实现方式,一种是树型set,一种是哈希型set


假设有集合:

s={7, 2, 0, 14, 4, 12}


树型set的实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(lg(n));

(2) 能实现有序查找;

(3) 省空间;


哈希型set实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(1);

(2) 不能实现有序查找;

画外音:求群覆盖,哈希型实现的初始化更快,复杂度是O(M*N)。


第二步,如何判断两个(多个)集合要不要合并?

集合对set(i)和set(j),判断里面有没有重复元素,如果有,就需要合并,判重的伪代码是:

// 对set(i)和set(j)进行元素判断并合并

(1)    foreach (element in set(i))

(2)    if (element in set(j))

         merge(set(i), set(j));


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)判断element是否在第二个集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第三步,如何合并集合?

集合对set(i)和set(j)如果需要合并,只要把一个集合中的元素插入到另一个集合中即可:

// 对set(i)和set(j)进行集合合并

merge(set(i), set(j)){

(1)    foreach (element in set(i))

(2)    set(j).insert(element);

}


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)把element插入到集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第四步:迭代第二步与第三步,直至结束

对于M个集合,暴力针对所有集合对,进行重复元素判断并合并,用两个for循环可以暴力解决:

(1)for(i = 1 to M)

(2)    for(j= i+1 to M)

         //对set(i)和set(j)进行元素判断并合并

         foreach (element in set(i))

         if (element in set(j))

         merge(set(i), set(j));


递归调用,两个for循环,复杂度是O(M*M)。


综上,如果这么解决群覆盖的问题,时间复杂度至少是:

O(M*N) // 集合初始化的过程

+

O(M*M) // 两重for循环递归

*

O(N) // 判重

*

O(N) // 合并

画外音:实际复杂度要高于这个,随着集合的合并,集合元素会越来越多,判重和合并的成本会越来越高。


第三部分:染色法


总的来说,暴力法效率非常低,集合都是一个一个合并的,同一个元素在合并的过程中要遍历很多次。很容易想到一个优化点,能不能一次合并多个集合?


暴力法中,判断两个集合set和set是否需要合并,思路是:遍历set中的所有element,看在set中是否存在,如果存在,说明存在交集,则需要合并。


哪些集合能够一次性合并?

当某些集合中包含同一个元素时,可以一次性合并。


怎么一次性发现,哪些集合包含同一个元素,并合并去重呢?


回顾一下工作中的类似需求:

M个文件,每个文件包含N个用户名,或者N个手机号,如何合并去重?

最常见的玩法是:

cat file_1 file_2 … file_M | sort | uniq > result


这里的思路是什么?

(1) 把M*N个用户名/手机号输出;

(2) sort排序,排序之后相同的元素会相邻

(3) uniq去重,相邻元素如果相同只保留一个;


排序之后相同的元素会相邻”,就是一次性找出所有可合并集合的关键,这是染色法的核心。


举一个栗子

假设有6个微信群,每个微信群有若干个用户:

s1={1,0,5} s2={3,1} s3={2,9}

s4={4,6} s5={4,7} s6={1,8}

假设使用树形set来表示集合。

首先,给同一个集合中的所有元素染上相同的颜色,表示来自同一个集合。

然后,对所有的元素进行排序,会发现:

(1) 相同的元素一定相邻,并且一定来自不同的集合;

(2) 同一个颜色的元素被打散了;

这些相邻且相同的元素,来自哪一个集合,这些集合就是需要合并的,如上图:

(1) 粉色的1来自集合s1,紫色的1来自集合s2,黄色的1来自集合s6,所以s1s2s6需要合并;

(2) 蓝色的4来自集合s4,青色的4来自集合s5,所以s4s5需要合并;


不用像暴力法遍历所有的集合对,而是一个排序动作,就能找到所有需要合并的集合。

画外音:暴力法一次处理2个集合,染色法一次可以合并N个集合。

集合合并的过程,可以想象为,相同相邻元素所在集合,染成第一个元素的颜色

(1) 紫色和黄色,染成粉色;

(2) 青色,染成蓝色;


最终,剩余三种颜色,也就是三个集合:

s1={0,1,3,5,8}

s3={2,9}

s4={4,6,7}


神奇不神奇!!!


染色法有意思么?但仍有两个遗留问题

(1) 粉色1,紫色1,黄色1,三个元素如何找到这三个元素所在的集合s1s2s6呢?

(2) s1s2s6三个集合如何快速合并

画外音:假设总元素个数n=M*N,如果使用树形set,合并的复杂度为O(n*lg(n)),即O(M*N*lg(M*N))。


我们继续往下看。


第四部分:链表法


染色法遗留了两个问题:

步骤(2)中,如何通过元素快速定位集合

步骤(3)中,如何快速合并集合

我们继续聊聊这两个问题的优化思路。


问题一:如何由元素快速定位集合?

普通的集合,只能由集合根(root)定位元素,不能由元素逆向定位root,如何支持元素逆向定位root呢?

很容易想到,每个节点增加一个父指针即可。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* parent;    // 指向父节点

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向它的上级,而只有root->parent=NULL。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         element* temp=x;

         while(temp->parent != NULL){

                   temp= temp->parent;

         }

         return temp;

}


很容易发现,由元素找集合根的时间复杂度是树的高度,即O(lg(n))


有没有更快的方法呢?

进一步思考,为什么每个节点要指向父节点,直接指向根节点是不是也可以。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* root;         // 指向集合根

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向集合的根。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         return x->root;

}


很容易发现,升级后,由元素找集合根的时间复杂度是O(1)

画外音:不能更快了吧。


另外,这种方式,能在O(1)的时间内,判断两个元素是否在同一个集合内

bool in_the_same_set(element* a, element* b){

         return (a->root == b->root);

}

甚为方便。

画外音:两个元素的根相同,就在同一个集合内。


问题二:如何快速进行集合合并? 

暴力法中提到过,集合合并的伪代码为:

merge(set(i), set(j)){

         foreach(element in set(i))

                   set(j).insert(element);

}

把一个集合中的元素插入到另一个集合中即可。


假设set(i)的元素个数为n1,set(j)的元素个数为n2,其时间复杂度为O(n1*lg(n2))。


在“微信群覆盖”这个业务场景下,随着集合的不断合并,集合高度越来越高,合并会越来越慢,有没有更快的集合合并方式呢?


仔细回顾一下:

(1) 树形set的优点是,支持有序查找,省空间;

(2) 哈希型set的优点是,快速插入与查找;


而“微信群覆盖”场景对集合的频繁操作是:

(1) 由元素找集合根;

(2) 集合合并;


那么,为什么要用树形结构或者哈希型结构来表示集合呢?

画外音:优点完全没有利用上嘛。


让我们来看看,这个场景中,如果用链表来表示集合会怎么样,合并会不会更快?

s1={7,3,1,4}

s2={1,6}

如上图,分别用链表来表示这两个集合。可以看到,为了满足“快速由元素定位集合根”的需求,每个元素仍然会指向根。


s1和s2如果要合并,需要做两件事:

(1) 集合1的尾巴,链向集合2的头(蓝线1);

(2) 集合2的所有元素,指向集合1的根(蓝线2,3);


合并完的效果是:

变成了一个更大的集合。


假设set(1)的元素个数为n1,set(2)的元素个数为n2,整个合并的过程的时间复杂度是O(n2)。

画外音:时间耗在set(2)中的元素变化。


咦,我们发现:

(1) 将短的链表,接到长的链表上;

(2) 将长的链表,接到短的链表上;

所使用的时间是不一样的。


为了让时间更快,一律使用更快的方式:“元素少的链表”主动接入到“元素多的链表”的尾巴后面。这样,改变的元素个数能更少一些,这个优化被称作“加权合并”。


对于M个微信群,平均每个微信群N个用户的场景,用链表的方式表示集合,按照“加权合并”的方式合并集合,最坏的情况下,时间复杂度是O(M*N)。

画外音:假设所有的集合都要合并,共M次,每次都要改变N个元素的根指向,故为O(M*N)。


于是,对于“M个群,每个群N个用户,微信群求覆盖”问题,使用“染色法”加上“链表法”,核心思路三步骤:

(1) 全部元素全局排序

(2) 全局排序后,不同集合中的相同元素,一定是相邻的,通过相同相邻的元素,一次性找到所有需要合并的集合

(3) 合并这些集合,算法完成;


其中:

步骤(1),全局排序,时间复杂度O(M*N);

步骤(2),染色思路,能够迅猛定位哪些集合需要合并,每个元素增加一个属性指向集合根,实现O(1)级别的元素定位集合;

步骤(3),使用链表表示集合,使用加权合并的方式来合并集合,合并的时间复杂度也是O(M*N);


总时间复杂度是:

O(M*N)    //排序

+

O(1)        //由元素找到需要合并的集合

*

O(M*N)    //集合合并


神奇不神奇!


神奇不止一种,还有其他方法吗?我们接着往下看。


第五部分:并查集法


分离集合(disjoint set)是一种经典的数据结构,它有三类操作:

Make-set(a):生成一个只有一个元素a的集合;

Union(X, Y):合并两个集合X和Y;

Find-set(a):查找元素a所在集合,即通过元素找集合;


这种数据结构特别适合用来解决这类集合合并与查找的问题,又称为并查集


能不能利用并查集来解决求“微信群覆盖”问题呢?


一、并查集的链表实现


链表法里基本聊过,为了保证知识的系统性,这里再稍微回顾一下。

如上图,并查集可以用链表来实现。


链表实现的并查集,Find-set(a)的时间复杂度是多少?

集合里的每个元素,都指向“集合的句柄”,这样可以使得“查找元素a所在集合S”,即Find-set(a)操作在O(1)的时间内完成


链表实现的并查集,Union(X, Y)的时间复杂度是多少?

假设有集合:

S1={7,3,1,4}

S2={1,6}


合并S1和S2两个集合,需要做两件事情:

(1) 第一个集合的尾元素,链向第二个集合的头元素(蓝线1);

(2) 第二个集合的所有元素,指向第一个集合的句柄(蓝线2,3);


合并完的效果是:

变成了一个更大的集合S1。


集合合并时,将短的链表,往长的链表上接,这样变动的元素更少,这个优化叫做“加权合并”。

画外音:实现的过程中,集合句柄要存储元素个数,头元素,尾元素等属性,以方便上述操作进行。


假设每个集合的平均元素个数是nUnion(X, Y)操作的时间复杂度是O(n)


能不能Find-set(a)与Union(X, Y)都在O(1)的时间内完成呢?

可以,这就引发了并查集的第二种实现方法。


二、并查集的有根树实现


什么是有根树,和普通的树有什么不同?

常用的set,就是用普通的二叉树实现的,其元素的数据结构是:

element{

         int data;

         element* left;

         element* right;

}

通过左指针与右指针,父亲节点指向儿子节点。


而有根树,其元素的数据结构是:

element{

         int data;

         element* parent;

}

通过儿子节点,指向父亲节点。


假设有集合:

S1={7,3,1,4}

S2={1,6}


通过如果通过有根树表示,可能是这样的:

所有的元素,都通过parent指针指向集合句柄,所有元素的Find-set(a)的时间复杂度也是O(1)。

画外音:假设集合的首个元素,代表集合句柄。


有根树实现的并查集,Union(X, Y)的过程如何?时间复杂度是多少?

通过有根树实现并查集,集合合并时,直接将一个集合句柄,指向另一个集合即可。

如上图所示,S2的句柄,指向S1的句柄,集合合并完成:S2消亡,S1变为了更大的集合。


容易知道,集合合并的时间复杂度为O(1)


会发现,集合合并之后,有根树的高度变高了,与“加权合并”的优化思路类似,总是把节点数少的有根树,指向节点数多的有根树(更确切的说,是高度矮的树,指向高度高的树),这个优化叫做“按秩合并”。


新的问题来了,集合合并之后,不是所有元素的Find-set(a)操作都是O(1)了,怎么办?

如图S1与S2合并后的新S1,首次“通过元素6来找新S1的句柄”,不能在O(1)的时间内完成了,需要两次操作。


但为了让未来“通过元素6来找新S1的句柄”的操作能够在O(1)的时间内完成,在首次进行Find-set(“6”)时,就要将元素6“寻根”路径上的所有元素,都指向集合句柄,如下图。

某个元素如果不直接指向集合句柄,首次Find-set(a)操作的过程中,会将该路径上的所有元素都直接指向句柄,这个优化叫做“路径压缩”。

画外音:路径上的元素第二次执行Find-set(a)时,时间复杂度就是O(1)了。


实施“路径压缩”优化之后,Find-set的平均时间复杂度仍是O(1)


稍微总结一下。


通过链表实现并查集:

(1) Find-set的时间复杂度,是O(1)常数时间;

(2) Union的时间复杂度,是集合平均元素个数,即线性时间;

画外音:别忘了“加权合并”优化。


通过有根树实现并查集:

(1) Union的时间复杂度,是O(1)常数时间;

(2) Find-set的时间复杂度,通过“按秩合并”与“路径压缩”优化后,平均时间复杂度也是O(1);


即,使用并查集,非常适合解决“微信群覆盖”问题。


知其然,知其所以然,思路往往比结果更重要

算法,其实还是挺有意思的。

作者:58沈剑
来源:https://mp.weixin.qq.com/s/2MNL4vDpXQR94KGts4JUhA

收起阅读 »

一张图看懂开源许可协议,开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别 以下是上述协议的简单介绍:BSD开源协议BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由...
继续阅读 »

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别



以下是上述协议的简单介绍:
BSD开源协议
BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。

但”为所欲为”的前提当你发布使用了BSD协议的代码,或则以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件:

    如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。
    如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。
    不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD 代码鼓励代码共享,但需要尊重代码作者的著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。

Apache Licence 2.0
Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似:

    需要给代码的用户一份Apache Licence
    如果你修改了代码,需要再被修改的文件中说明。
    在延伸的代码中(修改和有源代码衍生的代码中)需要带有原来代码中的协议,商标,专利声明和其他原来作者规定需要包含的说明。
    如果再发布的产品中包含一个Notice文件,则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可,但不可以表现为对Apache Licence构成更改。

Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。
GPL

我们很熟悉的Linux就是采用了GPL。GPL协议和BSD, Apache Licence等鼓励代码重用的许可很不一样。GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。这也就是为什么我们能用免费的各种linux,包括商业公司的linux和linux上各种各样的由个人,组织,以及商业软件公司开发的免费软件了。

GPL协议的主要内容是只要在一个软件中使用(”使用”指类库引用,修改后的代码或者衍生代码)GPL 协议的产品,则该软件产品必须也采用GPL协议,既必须也是开源和免费。这就是所谓的”传染性”。GPL协议的产品作为一个单独的产品使用没有任何问题,还可以享受免费的优势。

由于GPL严格要求使用了GPL类库的软件产品必须使用GPL协议,对于使用GPL协议的开源代码,商业软件或者对代码有保密要求的部门就不适合集成/采用作为类库和二次开发的基础。

其它细节如再发布的时候需要伴随GPL协议等和BSD/Apache等类似。

LGPL
LGPL是GPL的一个为主要为类库使用设计的开源协议。和GPL要求任何使用/修改/衍生之GPL类库的的软件必须采用GPL协议不同。LGPL 允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。

但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。

GPL/LGPL都保障原作者的知识产权,避免有人利用开源代码复制并开发类似的产品

MIT
MIT是和BSD一样宽范的许可协议,作者只想保留版权,而无任何其他了限制.也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的.

MPL
MPL是The Mozilla Public License的简写,是1998年初Netscape的 Mozilla小组为其开源软件项目设计的软件许可证。MPL许可证出现的最重要原因就是,Netscape公司认为GPL许可证没有很好地平衡开发者对源代码的需求和他们利用源代码获得的利益。同著名的GPL许可证和BSD许可证相比,MPL在许多权利与义务的约定方面与它们相同(因为都是符合OSIA 认定的开源软件许可证)。但是,相比而言MPL还有以下几个显著的不同之处:

◆ MPL虽然要求对于经MPL许可证发布的源代码的修改也要以MPL许可证的方式再许可出来,以保证其他人可以在MPL的条款下共享源代码。但是,在MPL 许可证中对“发布”的定义是“以源代码方式发布的文件”,这就意味着MPL允许一个企业在自己已有的源代码库上加一个接口,除了接口程序的源代码以MPL 许可证的形式对外许可外,源代码库中的源代码就可以不用MPL许可证的方式强制对外许可。这些,就为借鉴别人的源代码用做自己商业软件开发的行为留了一个豁口。
◆ MPL许可证第三条第7款中允许被许可人将经过MPL许可证获得的源代码同自己其他类型的代码混合得到自己的软件程序。
◆ 对软件专利的态度,MPL许可证不像GPL许可证那样明确表示反对软件专利,但是却明确要求源代码的提供者不能提供已经受专利保护的源代码(除非他本人是专利权人,并书面向公众免费许可这些源代码),也不能在将这些源代码以开放源代码许可证形式许可后再去申请与这些源代码有关的专利。
◆ 对源代码的定义
而在MPL(1.1版本)许可证中,对源代码的定义是:“源代码指的是对作品进行修改最优先择取的形式,它包括:所有模块的所有源程序,加上有关的接口的定义,加上控制可执行作品的安装和编译的‘原本’(原文为‘Script’),或者不是与初始源代码显著不同的源代码就是被源代码贡献者选择的从公共领域可以得到的程序代码。”
◆ MPL许可证第3条有专门的一款是关于对源代码修改进行描述的规定,就是要求所有再发布者都得有一个专门的文件就对源代码程序修改的时间和修改的方式有描述。

作者:微wx笑
翻译:https://blog.csdn.net/testcs_dn/article/details/38496107
原文:http://www.mozilla.org/MPL/MPL-1.1.html

收起阅读 »

拒绝白嫖,开源项目作者删库跑路,数千个应用程序无限输出乱码

「我删我自己的开源项目代码,需要经过别人允许吗?」几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「co...
继续阅读 »



「我删我自己的开源项目代码,需要经过别人允许吗?」

几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。

更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「colors.js」的作者 Marak Squires 本人。

一夜之间,Marak Squires 主动删除了「faker.js」和「colors.js」项目仓库的所有代码,让正在使用这两个开源项目的数千位开发者直接崩溃。

「faker.js」和「colors.js」

faker.js 在 npm 上的每周下载量接近 250 万,color.js 每周的下载量约为 2240 万,本次删库的影响是极其严重的,使用这两个项目开发的工具包括 AWS CDK 等。

如果在构建和测试应用时,真实的数据量远远不够,那么 Faker 类工具将帮助开发者生成伪数据。faker.js 就是可为多个领域生成伪数据的 Node.js 库,包括地址、商业、公司、日期、财务、图像、随机数、名称等。

faker.js 支持生成英文、中文等多语种信息,包含丰富的 API,此前版本通常一个月迭代更新一次。faker.js 不仅可以使用在服务器端的 JavaScript,还可以应用在浏览器端的 JavaScript。

现在,faker.js 项目的所有 commit 信息都被改为「endgame」,在 README 中,作者写下这样一句话:「What really happened with Aaron Swartz?」

Swartz 是一位杰出的开发人员,帮助建立了 Creative Commons、RSS 和 Reddit。2011 年,Swartz 被指控从学术数据库 JSTOR 中窃取文件,目的是免费访问这些文件。Swartz 在 2013 年自杀,Squires 提到 Swartz 可能意指围绕这一死亡疑云。

Marak Squires 向 colors.js 提交了恶意代码,添加了一个「a new American flag module」,然后将其发布到了 GitHub 和 npm。

随后他在 GitHub 和 npm 发布了 faker.js 6.6.6,这两个动作引发了同样的破坏性事件。破坏后的版本导致应用程序无限输出奇怪的字母和符号,从三行写着「LIBERTY LIBERTY LIBERTY」的文本开始,后面跟着一系列非 ASCII 字符:

目前,color.js 已经更新了一个可以使用的版本。faker.js 项目尚未恢复,开发者只能通过降级到此前的 5.5.3 版本来解决问题。

为了解决问题,Squires 在 GitHub 上还发布了更新以解决「zalgo 问题」,该问题是指损坏文件产生的故障文本。

「我们注意到在 v1.4.44-liberty-2 版本的 colors 中有一个 zalgo 错误,」Squires 以一种讽刺的语气写道。「我们现在正在努力解决这个问题,很快就会有解决方案。」

在将更新推送到 faker.js 两天后,Squires 发了一条推文,表示自己存储了数百个项目的 GitHub 账户已经被封。Squires 在 1 月 4 日发布了 faker.js 的最新 commit,在 1 月 6 日被封,直到 1 月 7 日推送了 colors.js 的「liberty」版本。然而,从 faker.js 和 colors.js 的更新日志来看,他的账户似乎被解封过。目前尚不清楚 Squires 的帐户是否再次被封。

至此,故事并没有就此结束。Squires 2020 年 11 月发在 GitHub 上的一篇帖子被挖出来,在帖子中他写道自己不再想做免费的工作了。「恕我直言,我不想再用我的免费工作来支持财富 500 强(和其他小型公司),以此为契机,向我发送一份六位数的年度合同,或者 fork 项目并让其他人参与其中。」

Squires 的大胆举动引起了人们对开源开发者的道德和财务困境的关注,这可能是 Marak Squires 行动的目标。大量网站、软件和应用程序依赖开源开发人员来创建基本工具和组件,而所有这些都是免费的,无偿开发人员经常不知疲倦地工作,努力修复其开源软件中的安全问题。

开发者们怎么看

软件工程师 Sergio Gómez 表示:「从 GitHub 删除自己的代码违反了他们的服务条款?WTF?这是绑架。我们需要开始分散托管免费软件源代码。」

「不知道发生了什么,但我将我所有的项目都托管在 GitLab 私有 instance 上,永远不要相信任何互联网服务提供商。」

有网友认为 faker.js 团队的反应有些夸张了,并说道:「没有人会用一个只生成一些虚假数据的包赚大钱。faker.js 的确为开发者生成伪数据节省了一些时间,但我们也可以让实习生编写类似程序来生成数据。这对企业来说并没有那么重要。」

甚至有人认为 Marak 这么做是一种冲动行为,不够理性,并和他之前「卖掉房子购买 NFT」的传闻联系起来,认为 Marak 需要学会控制自己的情绪:

这种说法很快带偏部分网友的看法,有人原本同情开源项目被「白嫖」,但现在已转向认为 Marak 是恶意删库,并指出:「停止维护他的项目或完全删除都是他的权利,但故意提交有害代码是不对的。」

当然,也有人为开源软件(FOSS)开发者的待遇鸣不平:「希望有相关的基金会位 FOSS 开发人员提供资金支持」,而软件的可靠性和稳定性也是至关重要的

有人表示:一些大公司确实不尊重开源项目的版权,滥用开源项目对于 FOSS 开发者来说是绝对不公平的。但 Marak 对 faker.js 的做法并不可取,不是正面例子,存在 Marak 的个人负面原因。

对此,你有什么看法?

作者:机器之心Pro

来源:https://www.163.com/dy/article/GTC8PE5M0511AQHO.html

收起阅读 »

崩溃的一天,西安一码通崩溃背后的技术问题

12月20号,算得上西安崩溃的一天。西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。足足瘫痪超过 15 个多小时!到了下午,新闻甚至提示:这是解决问题的方法吗?今天,我们就试着分析一下这个业务、以及对应的技术问题。西安一码通其它业务我们暂且不分析...
继续阅读 »

1.崩溃的一天

12月20号,算得上西安崩溃的一天。

12月19号新增病例21个,20号新增病例42个,并且有部分病例已经在社区内传播.

西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。

在这样严峻的情况下,作为防控最核心的系统:西安一码通竟然崩溃了,并且崩溃得是那么的彻底。

足足瘫痪超过 15 个多小时!

整整一天的时间呀,多少上班族被堵在地铁口,多少旅客被冻在半路上,进退不能...

到了下午,新闻甚至提示:

为了减轻系统压力,建议广大市民非必要不展码、亮码,在出现系统卡顿时,请耐心等待,尽量避免反复刷新,也感谢广大市民朋友们的理解配合。

这是解决问题的方法吗?

如果真的需要限流来防止系统崩溃,用技术手段来限流是不是会更简单一些,甚至前面加一个 nginx 就能解决的问题。

今天,我们就试着分析一下这个业务、以及对应的技术问题。

2.产品分析

西安一码通其它业务我们暂且不分析,那并不是重点,并且当天也没有完全崩溃,崩溃的仅有扫码功能。

其实这是一个非常典型的大量查询、少数更新的业务,闭着眼睛分析一下,可以说, 90% 以上的流量都是查询。

我们先来看看第一版的产品形态,扫码之后展示个人部分姓名和身份政信息,同时下面展示绿、黄、红码。

这是西安一码通最开始的样子,业务流程仅仅只需要一个请求,甚至一个查询的 SQL 就可以搞定。

到了后来,这个界面做了2次比较大的改版。

第一次改版新增了疫苗接种信息,加了一个边框;第二次改版新增了核酸检测信息,在最下方展示核酸检测时间、结果。

整个页面增加了2个查询业务,如果系统背后使用的是关系数据库,可能会多增加至少2个查询SQL。

基本上就是这样的一个需求,据统计西安有1300万人口,按照最大10%的市民同时扫码(我怀疑不会有这么多),也就是百万的并发量。

这样一个并发量的业务,在互联网公司很常见,甚至比这个复杂的场景也多了去了。

那怎么就崩了呢?

3.技术分析

在当天晚上的官方回复中,我们看到有这样一句话:

12月20日早7:40分左右,西安“一码通”用户访问量激增,每秒访问量达到以往峰值的10倍以上,造成网络拥塞,致使包括“一码通”在内的部分应用系统无法正常使用。“

一码通”后台监控第一时间报警,各24小时驻场通信、网络、政务云、安全和运维团队立即开展排查,平台应用系统和数据库运行正常,判断问题出现在网络接口侧。

根据上面的信息,数据库和平台系统都正常,是网络出现了问题。

我之前在文章《一次dns缓存引发的惨案》画过一张访问示意图,用这个图来和大家分析一下,网络出现问题的情况。

一般用户的请求,会先从域名开始,经过DNS服务器解析后拿到外网IP地址,经过外网IP访问防火墙和负载之后打到服务器,最后服务器响应后将结果返回到浏览器。

如果真的是网络出现问题,一般最常见的问题就是 DNS 解析错误,或者外网的宽带被打满了。

DNS解析错误一定不是本次的问题,不然可能不只是这一个功能出错了;外网的宽带被打满,直接增加带宽就行,不至于一天都没搞定。

如果真的是网络侧出现问题,一般也不需要改动业务,但实际上系统恢复的时候,大家都发现界面回到文章开头提到了第一个版本了。

也就是说系统“回滚”了。

界面少了接种信息和核酸检测信息的内容,并且在一码通的首页位置,新增加了一个核酸查询的页面。

所以,仅仅是网络接口侧出现问题吗?我这里有一点点的疑问。

4.个人分析

根据我以往的经验,这是一个很典型的系统过载现象,也就是说短期内请求量超过服务器响应。

说人话就是,外部请求量超过了系统的最大处理能力。

当然了,系统最大处理能力和系统架构息息相关,同样的服务器不同的架构,系统负载量差异极大。

应对这样的问题,解决起来无非有两个方案,一个是限流,另外一个就是扩容了。

限流就是把用户挡在外面,先处理能处理的请求;扩容就是加服务器、增加数据库承载能力。

上面提到官方让大家没事别刷一码通,也算是人工限流的一种方式;不过在技术体系上基本上不会这样做。

技术上的限流方案有很多,但最简单的就是前面挂一个 Nginx 配置一下就能用;复杂一点就是接入层自己写算法。

当然了限流不能真正的解决问题,只是负责把一部分请求挡在外面;真正解决问题还是需要扩容,满足所有用户。

但实际上,根据解决问题的处理和产品回滚的情况来看,一码通并没有第一时间做扩容,而是选择了回滚。

这说明,在系统架构设计上,没有充分考虑扩容的情况,所以并不能支持第一时间选择这个方案。

5.理想的方案?

上面说那么多也仅仅是个人推测,实际上可能他们会面临更多现实问题,比如工期紧张、老板控制预算等等...

话说回来,如果你是负责一码通公司的架构师,你会怎么设计整个技术方案呢?欢迎大家留言,这里说说我的想法。

第一步,读写分离、缓存。

至少把系统分为2大块,满足日常使用的读业务单独抽取出来,用于承接外部的最大流量。

单独抽出一个子系统负责业务的更新,比如接种信息的更新、核酸信息的变化、或者根据业务定时变更码的颜色。

同时针对用户大量的单查询,上缓存系统,优先读取缓存系统的信息,防止压垮后面的数据库。

第二步,分库分表、服务拆分。

其实用户和用户之间的单个查询是没有关系的,完全可以根据用户的属性做分库分表。

比如就用用户ID取模分64个表,甚至可以分成64个子系统来查询,在接口最前端将流量分发掉,减轻单个表或者服务压力。

上面分析没有及时扩容,可能就是没有做服务拆分,如果都是单个的业务子服务的话,遇到过载的问题很容易做扩容。

当然,如果条件合适的话,上微服务架构就更好了,有一套解决方案来处理类似的问题。

第三步,大数据系统、容灾。

如果在一个页面中展示很多信息,还有一个技术方案,就是通过异步的数据清洗,整合到 nosql 的一张大表中。

用户扫描查询等相关业务,直接走 nosql 数据库即可。

这样处理的好处是,哪怕更新业务完全挂了,也不会影响用户扫码查询,因为两套系统、数据库都是完全分开的。

使用异地双机房等形式部署服务,同时做好整体的容灾、备灾方案,避免出现极端情况,比如机房光缆挖断等。

还有很多细节上的优化,这里就不一一说明了,这里也只是我的一些想法,欢迎大家留言补充。

6.最后

不管怎么分析,这肯定是人祸而不是天灾。

系统在没有经过严格测试之下,就直接投入到生产,在强度稍微大一点的环境中就崩溃了。

比西安大的城市很多,比西安现在疫情还要严重的情况,其它城市也遇到过,怎么没有出现类似的问题?

西安做为一个科技大省,出现这样的问题真的不应该,特别是我看了这个小程序背后使用的域名地址之后。

有一种无力吐槽的感觉,虽然说这和程序使用没有关系,但是从细节真的可以看出一个技术团队的实力。

希望这次能够吸取教训,避免再次出现类似的问题!

作者:纯洁的微笑
出处:https://www.cnblogs.com/ityouknow/p/15719395.html

收起阅读 »

利用好 git bisect 这把利器,帮助你快速定位疑难 bug

Git
使用git bisect二分法定位问题的基本步骤:git bisect start [最近的出错的commitid] [较远的正确的commitid]测试相应的功能git bisect good 标记正确直到出现问题则 标记错误 git bisect bad提...
继续阅读 »

使用git bisect二分法定位问题的基本步骤:

  1. git bisect start [最近的出错的commitid] [较远的正确的commitid]

  2. 测试相应的功能

  3. git bisect good 标记正确

  4. 直到出现问题则 标记错误 git bisect bad

  5. 提示的commitid就是导致问题的那次提交

问题描述

我们以Vue DevUI组件库的一个bug举例子🌰

5d14c34b这一次commit,执行yarn build报错,报错信息如下:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

我可以确定的是上一次发版本(d577ce4)是可以build成功的。

git bisect 简介

git bisect命令使用二分搜索算法来查找提交历史中的哪一次提交引入了错误。它几乎能让你闭着眼睛快速定位任何源码导致的问题,非常实用。

你只需要告诉这个命令一个包含该bug的坏commit ID和一个引入该bug之前的好commit ID,这个命令会用二分法在这两个提交之间选择一个中间的commit ID,切换到那个commit ID的代码,然后询问你这是好的commit ID还是坏的commit ID,你告诉它是好还是坏,然后它会不断缩小范围,直到找到那次引入bug的凶手commit ID

这样我们就只需要分析那一次提交的代码,就能快速定位和解决这个bug(具体定位的时间取决于该次提交的代码量和你的经验),所以我们提交代码时一定要养成小批量提交的习惯,每次只提交一个小的独立功能,这样出问题了,定位起来会非常快。

接下来我就以Vue DevUI之前出现过的一个bug为例,详细介绍下如何使用git bisect这把利器。

定位过程

git bisect start 5d14c34b d577ce4
or
git bisect start HEAD d577ce4

其中5d14c34b这次是最近出现的有bug的提交,d577ce4这个是上一次发版本没问题的提交。

执行完启动bisect之后,马上就切到中间的一次提交啦,以下是打印结果:

kagol:vue-devui kagol$ git bisect start 5d14c34b d577ce4
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[1cfafaaa58e03850e0c9ddc4246ae40d18b03d71] fix: read-tip icon样式泄露 (#54)

可以看到已经切到以下提交:

[1cfafaaa] fix: read-tip icon样式泄露 (#54)

执行命令:

yarn build

构建成功,所以标记下good

git bisect good
kagol:vue-devui kagol$ git bisect good
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0] feat(drawer): add service model (#27)

标记万good,马上又通过二分法,切到了一次新的提交:

[c0c4cc1a] feat(drawer): add service model (#27)

再次执行build命令:

yarn build

build失败了,出现了我们最早遇到的报错:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

标记下bad,再一次切到中间的提交:

kagol:vue-devui kagol$ git bisect bad
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[86634fd8efd2b808811835e7cb7ca80bc2904795] feat: add scss preprocessor in docs && fix:(Toast) single lifeMode bug in Toast

以此类推,不断地验证、标记、验证、标记...最终会提示我们那一次提交导致了这次的bug,提交者、提交时间、提交message等信息。

kagol:vue-devui kagol$ git bisect good
c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0 is the first bad commit
commit c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0
Author: nif <lnzhangsong@163.com>
Date: Sun Dec 26 21:37:05 2021 +0800

feat(drawer): add service model (#27)

* feat(drawer): add service model

* docs(drawer): add service model demo

* fix(drawer): remove 'console.log()'

packages/devui-vue/devui/drawer/index.ts | 7 +++--
.../devui-vue/devui/drawer/src/drawer-service.ts | 33 ++++++++++++++++++++++
packages/devui-vue/devui/drawer/src/drawer.tsx | 3 ++
packages/devui-vue/docs/components/drawer/index.md | 29 +++++++++++++++++++
4 files changed, 69 insertions(+), 3 deletions(-)
create mode 100644 packages/devui-vue/devui/drawer/src/drawer-service.ts

最终定位到出问题的commit:

c0c4cc1a is the first bad commit

github.com/DevCloudFE/…

整个定位过程几乎是机械的操作,不需要了解项目源码,不需要了解最近谁提交了什么内容,只需要无脑地:验证、标记、验证、标记,最后git会告诉我们那一次提交出错。

这么香的工具,赶紧来试试吧!

问题分析

直到哪个commit出问题了,定位起来范围就小了很多。

如果平时提交代码又能很好地遵循小颗粒提交的话,bug呼之欲出。

这里必须表扬下我们DevUI的田主(Contributor)们,他们都养成了小颗粒提交的习惯,这次导致bug的提交c0c4cc1a,只提交了4个文件,涉及70多行代码。

我们在其中搜索下document关键字,发现了两处,都在drawer-service.ts整个文件中:

一处是12行的:

static $body: HTMLElement | null = document.body

另一处是17行的:

this.$div = document.createElement('div')

最终发现罪魁祸首就是12行的代码!

破案!

作者:DevUI团队
来源:https://juejin.cn/post/7046409685561245733

收起阅读 »

手把手带你配置MySQL主备环境

为了保障生产数据的安全性,我们往往需要对数据库做备份,而通过MySQL主备配置,则是一种MySQL数据库备份的好的实现方案。本文将一步步带你搭建MySQL主备环境。MySQL主备搭建现有两台虚拟机,192.168.56.11(主)和192.168.56.12(...
继续阅读 »



为了保障生产数据的安全性,我们往往需要对数据库做备份,而通过MySQL主备配置,则是一种MySQL数据库备份的好的实现方案。本文将一步步带你搭建MySQL主备环境。

MySQL主备搭建

现有两台虚拟机,192.168.56.11(主)和192.168.56.12(备)

1 MySQL安装

离线安装解压版MySQL

1 上传压缩包
  • 以root用户上传mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz到/root/Downloads/下

2 解压
  • cd /root/Downloads

  • tar -zxvf mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz

  • mv mysql-5.7.29-linux-glibc2.12-x86_64 mysql5.7

  • mkdir /usr/database

  • mv mysql5.7 /usr/database

3 创建系统用户组和用户
  • groupadd mysql

  • useradd -r -g mysql mysql

  • id mysql

4 创建mysql data目录
  • cd /usr/database/mysql5.7

  • mkdir data

4 设置data目录权限
  • chown -R mysql:mysql /usr/database/mysql5.7/

  • ll /usr/database

5 修改my.cnf文件

删除并重新创建/etc/my.cnf:

rm -rf /etc/my.cnf
vim /etc/my.cnf
[client]
port = 3306
socket = /tmp/mysql.sock

[mysqld]
init-connect='SET NAMES utf8'
basedir=/usr/database/mysql5.7     #根据自己的安装目录填写
datadir=/usr/database/mysql5.7/data #根据自己的mysql数据目录填写
socket=/tmp/mysql.sock
max_connections=200                 # 允许最大连接数
character-set-server=utf8           # 服务端使用的字符集默认为8比特编码的latin1字符集
default-storage-engine=INNODB       # 创建新表时将使用的默认存储引擎

最大连接数

  • max_connections<=16384

  • 管理员(SUPER)登录的连接,不计其中

  • mysql会为每个连接提供连接缓冲区,连接越多内存开销越大

  • 查询:show variables like '%_connections';show status like '%_connections';

  • max_used_connections / max_connections * 100% (理想值≈ 85%)

  • 数值过小会经常出现ERROR 1040: Too many connections错误

  • 修改方法:

    • 永久:在配置文件my.cnf中设置max_connections的值

    • 临时:以root登录mysql:set GLOBAL max_connections=xxx;->flush privileges;

mysql存储引擎

  • InnoDB:5.5及之后版本的默认引擎

    • 支持事务

    • 聚集索引,文件存放在主键索引的叶子节点上,必须要有主键。通过主键索引效率很高,但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。

    • 不支持全文类型索引

    • 支持外键

    • 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。

    • 最小锁粒度是行锁

    • 适用场景:支持事物、较多写操作、系统崩溃后相对易恢复

  • MyISAM:5.5版本之前的默认引擎

    • 不支持事务

    • 非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。

    • 保存了整个表的行数,执行select count(*) 时只需要读出该变量即可,速度很快;

    • 支持全文类型索引

    • 不支持外键

    • 最小锁粒度是表锁,一个更新语句会锁住整张表,导致其他查询和更新都被阻塞,因此并发访问受限

    • 表以文件形式保存,跨平台使用较方便

    • 适用场景:非事物型、读操作、小型应用

6 mysql初始化
  • /usr/database/mysql5.7/bin/mysqld --initialize-insecure --user=mysql --basedir=/usr/database/mysql5.7 --datadir=/usr/database/mysql5.7/data

#注意:mysqld --initialize-insecure初始化后的mysql是没有密码的

  • chown -R root:root /usr/database/mysql5.7/ #把安装目录的目录的权限所有者改为root

  • chown -R mysql:mysql /usr/database/mysql5.7/data/ #把data目录的权限所有者改为mysql

7 启动mysql
/usr/database/mysql5.7/bin/mysqld_safe --user=mysql &
8 修改root密码
  • cd /usr/database/mysql5.7/bin

  • ./mysql -u root -p # 默认没有密码,直接回车就行

  • use mysql;

  • update user set authentication_string=password('``rootpasswd``') where user='root';

  • flush privileges;

  • exit;

9 登录测试
  • /usr/database/mysql5.7/bin/mysql mysql -u root -p

  • (输入密码)

  • show databases;

  • exit;

10 启动设置
  • cp /usr/database/mysql5.7/support-files/mysql.server /etc/init.d/mysql

  • chkconfig --add mysql # 添加服务

  • chkconfig --list # 查看服务列表

  • chkconfig --level 345 mysql on # 设置开机启动

11 测试服务命令是否可用
  • systemctl status mysql

  • systemctl start mysql

  • systemctl stop mysql

12 设置远程访问
  • 登录数据库:mysql -uroot -p[password]

  • use mysql;

  • select host,user from user;

  • update user set host='%' where user='root';

  • flush privileges;

  • 如果还是无法访问,检查防火墙

13 创建新用户
  • 创建用户app:create user 'app'@'%' identified by 'password';

  • 用户赋权(具有数据库appdb的所有权限,并可远程不限ip访问):grant all on appdb.* to 'app'@'%';

  • flush privilegesl;

14 其他问题
  • -bash: mysql: command not found

    • 临时方案,重新登录后失效:alias mysql=/usr/database/mysql5.7/bin/mysql

    • 永久方案,将命令路径添加到PATH中:

      • vim /etc/profile

      • PATH="$PATH:/usr/database/mysql5.7/bin"

      • source /etc/profile

2 主备配置

1 生产环境为什么需要MySQL集群
  • 高可用性,在主节点失效时自动切换,不需要技术人员紧急处理

  • 高吞吐,可以多个节点同时提供读取数据服务,降低主节点负载,实现高吞吐

  • 可扩展性强,支持在线扩容

  • 无影响备份,在备节点进行备份操作不会对业务产生影响

MySQL集群的缺点:

  • 架构复杂,在部署、管理方面对技术人员要求高

  • 备节点拉取主节点日志时会对主节点服务器性能有一定影响

  • 如果配置了半同步复制,会对事物提交有一定影响

2 修改my.cnf
  • 主备服务器均创建日志文件路径:mkdir /data/mysql_log

  • 修改master下的/etc/my.cnf:

[client]
port = 3306
default-character-set=utf8mb4
socket = /usr/database/mysql5.7/mysql.sock

[mysqld]
basedir = /usr/database/mysql5.7
datadir = /usr/database/mysql5.7/data
tmpdir = /tmp
socket = /tmp/mysql.sock
# pid-file = /data/mysql_db/mysql_seg_3306/mysql.pid
skip-external-locking = 1
skip-name-resolve = 1
port = 3306
server-id = 113306

default-storage-engine = InnoDB
character-set-server = utf8mb4
default_password_lifetime=0

#### log ####
log_timestamps=system
log_bin = /data/mysql_log/mysql-bin
log_bin_index = /data/mysql_log/mysql-bin.index
binlog_format = row
relay_log_recovery=ON
relay_log=/data/mysql_log/mysql-relay-bin
relay_log_index=/data/mysql_log/mysql-relay-bin.index
log_error = /data/mysql_log/mysql-error.log

#### replication ####
replicate_wild_ignore_table = information_schema.%,performance_schema.%,sys.%

#### semi sync replication settings #####
plugin_dir=/usr/database/mysql5.7/lib/plugin
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
loose_rpl_semi_sync_master_enabled = 1
loose_rpl_semi_sync_slave_enabled = 1
loose_rpl_semi_sync_master_timeout = 5000
  • 修改slave下的/etc/my.cnf:

[client]
port = 3306
default-character-set=utf8mb4
socket = /usr/database/mysql5.7/mysql.sock

[mysqld]
basedir = /usr/database/mysql5.7
datadir = /usr/database/mysql5.7/data
tmpdir = /tmp
socket = /tmp/mysql.sock
# pid-file = /data/mysql_db/mysql_seg_3306/mysql.pid
skip-external-locking = 1
skip-name-resolve = 1
port = 3306
server-id = 123306
read-only=1

default-storage-engine = InnoDB
character-set-server = utf8mb4
default_password_lifetime=0

#### log ####
log_timestamps=system
log_bin = /data/mysql_log/mysql-bin
log_bin_index = /data/mysql_log/mysql-bin.index
binlog_format = row
relay_log_recovery=ON
relay_log=/data/mysql_log/mysql-relay-bin
relay_log_index=/data/mysql_log/mysql-relay-bin.index
log_error = /data/mysql_log/mysql-error.log

#### replication ####
replicate_wild_ignore_table = information_schema.%,performance_schema.%,sys.%

#### semi sync replication settings #####
plugin_dir=/usr/database/mysql5.7/lib/plugin
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
loose_rpl_semi_sync_master_enabled = 1
loose_rpl_semi_sync_slave_enabled = 1
loose_rpl_semi_sync_master_timeout = 5000
  • utf8:最大unicode字符是0xffff,仅支持Unicode 中的基本多文种平面(BMP),任何不在基本多文本平面的 Unicode字符,都无法使用 Mysql 的 utf8 字符集存储。

  • utf8mb4(most bytes 4)专门用来兼容四字节的unicode,是utf8的超集,包含很多不常用的汉字、Emoji表情,以及任何新增的 Unicode 字符等等

3 master上创建同步用户
  • mysql -uroot -proot

  • use mysql;

  • create user 'repl'@'%' identified by 'repl';

  • grant replication slave on . to 'repl'@'%';

  • flush privileges;

4 备份master数据库
/usr/database/mysql5.7/bin/mysqldump -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -A > mysql_backup_full.sql

上述命令报错,1045,增加登录信息:

/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -A > mysql_backup_full.sql

导出指定数据库(仅表结构):

/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -d testdb1 testdb2 > mysql_backup_test.sql

导出指定数据库(表结构+数据):

**/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 testdb1 testdb2 > mysql_backup_testdatas.sql**
5 slave上恢复master数据库

master推送sql:rsync -avzP mysql_backup_test.sql 192.168.56.12:/root/Downloads/

slave导入sql:/usr/database/mysql5.7/bin/mysqldump -S /tmp/mysql.sock < mysql_backup_test.sql

上述命令不成功,需先创建数据库,改为下述操作:

**mysql -uroot -proot**
**use test;**
**source /root/Downloads/**``**mysql_backup_test.sql**
6 开启同步
  • mysql命令行中查看master status中的File和Position参数:show master status;

    • 查看进程:show processlist\G

  • slave的mysql命令行执行:

mysql> CHANGE MASTER TO
   -> MASTER_HOST='192.168.41.83',
   -> MASTER_PORT=3306,
   -> MASTER_USER='repl',
   -> MASTER_PASSWORD='repl',
   -> MASTER_LOG_FILE='mysql-bin.000004',
   -> MASTER_LOG_POS=154;
mysql> start slave;
mysql> show slave status\G

状态中注意这几项:

Slave_IO_Running:取 Master 日志的线程, Yes 为正在运行

Slave_SQL_Running:从日志恢复数据的线程, Yes 为正在运行

Seconds_Behind_Master:当前数据库相对于主库的数据延迟, 这个值是根据二进制日志的时间戳计算得到的(秒)

7 同步测试

master数据库插入一条数据,可用看到slave同步更新了

主备有了,要是能够故障自动切换就完美了,这正是下一篇内容。

引用请注明出处!

收起阅读 »

动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?

90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。今天的面试题是:如何实现动态代理?JDK Proxy 和 CGLib 有什...
继续阅读 »



90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。

今天的面试题是:如何实现动态代理?JDK Proxy 和 CGLib 有什么区别?

典型回答

动态代理的常用实现方式是反射。反射机制 是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。

但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。

JDK Proxy 和 CGLib 的区别主要体现在以下几个方面:

  • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;

  • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;

  • JDK Proxy 是通过拦截器加反射的方式实现的;

  • JDK Proxy 只能代理继承接口的类;

  • JDK Proxy 实现和调用起来比较简单;

  • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;

  • CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。

考点分析

此面试题考察的是你对反射、动态代理及 CGLib 的了解,很多人经常会把反射和动态代理划为等号,但从严格意义上来说,这种想法是不正确的,真正能搞懂它们之间的关系,也体现了你扎实 Java 的基本功。和这个问题相关的知识点,还有以下几个:

  • 你对 JDK Proxy 和 CGLib 的掌握程度。

  • Lombok 是通过反射实现的吗?

  • 动态代理和静态代理有什么区别?

  • 动态代理的使用场景有哪些?

  • Spring 中的动态代理是通过什么方式实现的?

知识扩展

1.JDK Proxy 和 CGLib 的使用及代码分析

JDK Proxy 动态代理实现

JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* JDK Proxy 相关示例
*/
public class ProxyExample {
  static interface Car {
      void running();
  }

  static class Bus implements Car {
      @Override
      public void running() {
          System.out.println("The bus is running.");
      }
  }

  static class Taxi implements Car {
      @Override
      public void running() {
          System.out.println("The taxi is running.");
      }
  }

  /**
    * JDK Proxy
    */
  static class JDKProxy implements InvocationHandler {
      private Object target; // 代理对象

      // 获取到代理对象
      public Object getInstance(Object target) {
          this.target = target;
          // 取得代理对象
          return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                  target.getClass().getInterfaces(), this);
      }

      /**
        * 执行代理方法
        * @param proxy 代理对象
        * @param method 代理方法
        * @param args   方法的参数
        * @return
        * @throws InvocationTargetException
        * @throws IllegalAccessException
        */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
              throws InvocationTargetException, IllegalAccessException {
          System.out.println("动态代理之前的业务处理.");
          Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理)
          return result;
      }
  }

  public static void main(String[] args) {
      // 执行 JDK Proxy
      JDKProxy jdkProxy = new JDKProxy();
      Car carInstance = (Car) jdkProxy.getInstance(new Taxi());
      carInstance.running();
}

以上程序的执行结果是:

动态代理之前的业务处理.
The taxi is running.

可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下:

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。

CGLib 的实现

在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置:

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

CGLib 实现代码如下:

package com.lagou.interview;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLibExample {

  static class Car {
      public void running() {
          System.out.println("The car is running.");
      }
  }

  /**
    * CGLib 代理类
    */
  static class CGLibProxy implements MethodInterceptor {
      private Object target; // 代理对象

      public Object getInstance(Object target) {
          this.target = target;
          Enhancer enhancer = new Enhancer();
          // 设置父类为实例类
          enhancer.setSuperclass(this.target.getClass());
          // 回调方法
          enhancer.setCallback(this);
          // 创建代理对象
          return enhancer.create();
      }

      @Override
      public Object intercept(Object o, Method method,
                              Object[] objects, MethodProxy methodProxy) throws Throwable {
          System.out.println("方法调用前业务处理.");
          Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
          return result;
      }
  }

  // 执行 CGLib 的方法调用
  public static void main(String[] args) {
      // 创建 CGLib 代理类
      CGLibProxy proxy = new CGLibProxy();
      // 初始化代理对象
      Car car = (Car) proxy.getInstance(new Car());
      // 执行方法
      car.running();
}

以上程序的执行结果是:

方法调用前业务处理.
The car is running.

可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。

2.Lombok 原理分析

在开始讲 Lombok 的原理之前,我们先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效的解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,向这种方法都可以使用 Lombok 注解来完成。

例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的:

public class Person {
  private Integer id;
  private String name;
  public Integer getId() {
      return id;
  }
  public void setId(Integer id) {
      this.id = id;
  }
  public String getName() {
      return name;
  }
  public void setName(String name) {
      this.name = name;
  }
}

在使用 Lombok 之后,代码是这样的:

@Data
public class Person {
  private Integer id;
  private String name;
}

可以看出 Lombok 让代码简单和优雅了很多。

小贴士:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,我们需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示:

接下来讲讲 Lombok 的原理。

Lombok 的实现和反射没有任何关系,前面我们说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢?

回到我们刚才 Setter/Getter 的方法,当我们打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码竟然是这样的:

可以看出 Lombok 是在编译期就为我们生成了对应的字节码。

其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下:

从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。

3.动态代理知识点扩充

当面试官问动态代理的时候,经常会问到它和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。

动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。

Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-/> 即可。

小结

今天我们介绍了 JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。

除了 JDK Proxy 和 CGLib 之外,我们还讲了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式,希望本文可以帮助到你。

作者:Java面试真题解析
来源:https://mp.weixin.qq.com/s/UIMeWRerV9FhtEotV8CF9w

收起阅读 »

Java内存区域异常

一、内存区域划分Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。 JVM内存自动管理是Java...
继续阅读 »



一、内存区域划分

Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。

JVM内存自动管理是Java语言的一大优良特性,也是Java语言在软件市场长青的一大优势,有了JVM的自动内存管理,程序员不再为令人抓狂的内存泄漏担忧,但深入理解Java虚拟机内存区域依然至关重要,JVM内存自动管理并非永远万无一失,不当的使用也会造成OOM等内存异常,本文尝试列举Java内存溢出相关的常见异常并提供使用建议。

二、栈溢出

  • 方法调用深度过大

我们知道Java虚拟机栈及本地方法栈中存放的是方法调用所需的栈帧,栈帧是一种包括局部变量表、操作数栈、动态链接和方法返回地址的数据结构,每一次方法的调用和返回对应着压栈及出栈操作。Java虚拟机栈及本地方法栈由每个Java执行线程独享,当方法嵌套过深直至超过栈的最大空间时,当再次执行压栈操作,将抛出StackOverflowError异常。为演示栈溢出,假设存在如下代码:

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
  * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
  */
  public static void increment() {
      count++;
      increment();
  }

  public static void main(String[] args) {
      try {
          increment();
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

对于示例中的increment()方法,每次方法调用将深度+1,通过不断的栈帧压栈操作,当栈中空间无法再进行扩展时,程序将抛出StackOverflowError异常。Java虚拟机栈的空间大小可以通过JVM参数调整,我们先设置栈空间大小为512K(-Xss512k),程序执行结果如下:(方法调用5355次后触发栈溢出) 我们再将栈空间缩小至256k(-Xss256k),程序执行结果如下:(方法调用2079次时将触发栈溢出): 由此可见,Java虚拟机栈的空间是有限的,当我们进行程序设计时,应尽量避免使用递归调用。生产环境抛出StackOverflowError异常时,可以排查系统中是否存在调用深度过大的方法。方法的不断嵌套调用,不但会占用更多的内存空间,也会影响程序的执行效率。当我们进行程序设计时需要充分考虑并设置合理的栈空间大小,一般情况下虚拟机默认配置即满足大部分应用场景。

  • 局部变量表过大

局部变量表用于存放Java方法中的局部变量,我们需要合理的设置局部变量,避免过多的冗余变量产生,否则可能会导致栈溢出,沿用刚刚的实例代码,我们定义一个创建大量局部变量的重载方法,则在栈空间不变的情况下(-Xss512k),创建大量局部变量的方法将降低栈调用深度,更容易触发StackOverflowError异常,通过分别执行increment及其重载方法,其中代码及执行结果如下:

package org.learn.jvm.basic;

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
    * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
    */
  public static void increment() {
      count++;
      increment();
  }

  /**
    * 创建大量局部变量,导致局部变量表过大,影响栈调用深度
    *
    * @param a
    * @param b
    * @param c
    */
  public static void increment(long a, long b, long c) {
      long d = 1, e = 2, f = 3, g = 4, h = 5, i = 6,
              j = 7, k = 8, l = 9, m = 10;
      count++;
      increment(a, b, c);
  }

  public static void main(String[] args) {
      try {
          //increment();
          increment(1, 2, 3);
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

执行increment()时,最大栈深度为5355(栈空间大小-Xss512k)

执行increment(1,2,3)时,最大栈深度为1487(栈空间大小-Xss512k)

通过对比我们知道,创建大量的局部变量将使得局部变量表膨胀从而引发StackOverflowError异常,程序设计时应当尽量避免同一个方法中包含大量局部变量,实在无法避免可以考虑方法的重构及拆分。

三、OOM

对于Java程序员而言,OOM算是比较常见的生产环境异常,OOM往往容易引发线上事故,因此有必要梳理常见可能导致OOM的场景,并尽量规避保障服务的稳定性。

  • 创建大量的类

方法区主要的职责是用于存放类型相关信息,如类名、接口名、父类、访问修饰符、字段描述、方法描述等,Java8以后方法区从永久区移到了Metaspace,若系统中创建过多的类时,可能会引发OOM。Java开发中经常会利用字节码增强技术,通过创建代理类从而实现系统功能,以我们熟知的Spring框架为例,经常通过Cglib运行时创建代理类实现类的动态性,通过设置虚拟机参数,-XX:MaxMetaspaceSize=10M,则示例程序运行一段时间时间后,将触发Full GC并最终抛出OOM异常,因此日常开发过程中要特别留意,系统中创建的类的数量是否合理,以免产生OOM。实例代码如下:

/**
* 大量创建类导致方法区OOM
*/
public class JavaMethodAreaOOM {
  /**
    * VM Args: -XX:MaxMetaspaceSize=10M(设置Metaspace空间为10MB)
    *
    * @param args
    */
  public static void main(String[] args) {
      while (true) {
          Enhancer enhancer = new Enhancer();
          enhancer.setSuperclass(OOMObject.class);
          enhancer.setUseCache(false);
          enhancer.setCallback(new MethodInterceptor() {
              @Override
              public Object intercept(Object obj, Method method,
                                      Object[] args, MethodProxy proxy) throws Throwable {
                  return proxy.invokeSuper(obj, args);
              }
          });
          Object object = enhancer.create();
          System.out.println("hashcode:" + object.hashCode());
      }
  }

  /**
    * 对象
    */
  static class OOMObject {

  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定元数据区为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下:

  • 创建大对象

Java堆内存空间非常宝贵,若系统中存在数组、复杂嵌套对象等大对象,将会引发FullGc并最终引发OOM异常,因此程序设计时需要合理设置类结构,避免产生过大的对象,示例代码如下:

/**
* 堆内存溢出
*/
public class HeapOOM {
  /**
    *
    */
  static class InnerClass {
      private int value;
      private byte[] bytes = new byte[1024];
  }

  /**
    * VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
    *
    * @param args
    */
  public static void main(String[] args) {
      List<InnerClass> innerClassList = new ArrayList<>();
      while (true) {
          innerClassList.add(new InnerClass());
      }
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 常量池溢出

Java8开始,常量池从永久区移至堆区,当系统中存在大量常量并超过常量池最大容量时,将引发OOM异常。String类的intern()方法内部逻辑是:若常量池中存在字符串常量,则直接返回字符串引用,否则创建常量将其加入常量池中并返回常量池引用。通过每次创建不同的字符串,常量池将因为无法容纳新创建的字符串常量而最终引发OOM,示例代码如下:

/**
* VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
*
* @param args
*/
public static void main(String[] args) {
  Set<String> set = new HashSet<>();
  int i = 0;
  while (true) {
      set.add(String.valueOf(i++).intern());
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 直接内存溢出

直接内存是因NIO技术而引入的独立于堆区的一块内存空间,对于读写频繁的场景,通过直接操作直接内存可以获得更高的执行效率,但其内存空间受制于操作系统本地内存大小,超过最大限制后,也将抛出OOM 异常,以下代码通过UnSafe类,直接操作内存,程序执行一段时间后,将引发OOM异常。

public class DirectMemoryOOM {
  private static final int _1MB = 1024 * 1024;

  /**
    * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
    * @param args
    * @throws IllegalAccessException
    */
  public static void main(String[] args) throws IllegalAccessException {
      Field field = Unsafe.class.getDeclaredFields()[0];
      field.setAccessible(true);
      Unsafe unsafe = (Unsafe) field.get(null);
      while (true) {
          unsafe.allocateMemory(_1MB*1024);
      }
  }
}

设置虚拟机参数:-Xmx20M -XX:MaxDirectMemorySize=10M 限定最大直接内存空间为10M,程序最终执行结果如下:

四、总结

本文通过实际的案例列举了常见的OOM异常,由此我们可以得知,程序设计过程中应当合理的设置栈区、堆区、直接内存的大小,从实际情况出发,合理设计数据结构,从而避免引发OOM故障,此外通过分析引发OOM的原因也有利于我们针对深入理解JVM并对现有系统进行系统调优。

作者:洞幺幺洞
来源:https://juejin.cn/post/7043442191619850277

收起阅读 »

消息队列的使用场景是什么样的?

本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。什么是消息中间件作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、...
继续阅读 »



本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。

什么是消息中间件

作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、异步和事务等特性的消息通信服务。应用消息代理组件可以降低系统间耦合度,提高系统的吞吐量、可扩展性和高可用性。

分布式消息服务主要涉及五个核心角色,消息发布者(Publisher)、可靠消息组件(MsgBroker)、消息订阅者(Subscriber)、消息类型(Message Type)和订阅关系(Binding),具体描述如下:

  1. 消息发布者,指发送消息的应用系统,一个应用系统可以发送一种或者多种消息类型,发布者发送消息到可靠消息组件 (MsgBroker)。

  2. 可靠消息组件,即 MsgBroker,负责接收发布者发送的消息,根据消息类型和订阅关系将消息分发投递到一个或多个消息订阅者。整个过程涉及消息类型校验、消息持久化存储、订阅关系匹配、消息投递和消息恢复等核心功能。

  3. 消息订阅者,指订阅消息的应用系统,一个应用系统可以订阅一种或者多种消息类型,消息订阅者收到的消息来自可靠消息组件 (MsgBroker)。

  4. 消息类型:一种消息类型由 TOPIC 和 EVENTCODE 唯一标识。

  5. 订阅关系,用来描述一种消息类型被订阅者订阅,订阅关系也被称为 Binding。

核心功能特色

可为不同应用系统间提供可靠的消息通信,降低系统间耦合度并提高整体架构的可扩展性和可用性。

可为不同应用系统间提供异步消息通信,提高系统吞吐量和性能。

发布者系统、消息代理组件以及订阅者系统均支持集群水平扩展,可依据业务消息量动态部署计算节点。

支持事务型消息,保证消息与本地数据库事务的一致性。

远程调用RPC和消息MQ区别

谈到消息队列,有必要看下RPC和MQ的本质区别,从两者的定义和定位来看,RPC(Remote Procedure Call)远程过程调用,主要解决远程通信间的问题,不需要了解底层网络的通信机制;消息队列(MQ)是一种能实现生产者到消费者单向通信的通信模型。核心区别在于RPC是双向直接网络通讯,MQ是单向引入中间载体的网络通讯。单纯去看队列,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。在队列前面增加限定词“消息”,意味着通过消息驱动来进行整体的架构实现。RPC和MQ本质上是网络通讯的两种不同的实现机制,RPC同步等待结果对比于MQ在异步、解耦、削峰填谷等上的特征显著差异主要有以下几点差异:

  1. 在架构上,RPC和MQ的差异点是,Message有一个中间结点Message Queue,可以把消息存储起来。

  2. 同步调用:对于要立即等待返回处理结果的场景,RPC是首选。

  3. MQ的使用,一方面是基于性能的考虑,比如服务端不能快速的响应客户端(或客户端也不要求实时响应),需要在队列里缓存;另外一方面,它更侧重数据的传输,因此方式更加多样化,除了点对点外,还有订阅发布等功能。

  4. 随着业务增长,有的处理端调用下游服务太多或者处理量会成为瓶颈,会进行同步调用改造为异步调用,这个时候可以考虑使用MQ。

核心应用场景

针对MQ的核心场景,我们从异步、解耦、削峰填谷等特性进行分析,区别于传统的RPC调用。尤其在引入中间节点的情况下,通过空间(拥有存储能力)换时间(RPC同步等待响应)的思想,增加更多的可能性和能力。

异步通信

针对不需要立即处理消息,尤其那种非常耗时的操作,通过消息队列提供了异步处理机制,通过额外的消费线程接管这部分进行异步操作处理。

解耦

在应用和应用之间,提供了异构系统之间的消息通讯的机制,通过消息中间件解决多个系统或异构系统之间除了RPC之外另一种单向通讯的机制。

扩展性

因为消息队列解耦了主流程的处理过程,只要另外增加处理过程即可,不需要改变代码、不需要调整参数,便于分布式扩容。

分布式事务一致性

在2个应用系统之间的数据状态同步,需要考虑数据状态的最终一致性的场景下,利用消息队列所提供的事务消息来实现系统间的数据状态一致性。

削峰填谷

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提前预知;如果为了能处理这类瞬间峰值访问提前准备应用资源无疑是比较大的浪费。使用消息队列在突发事件下的防脉冲能力提供了一种保障,能够接管前台的大脉冲请求,然后异步慢速消费。

可恢复性

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了应用间的耦合度,所以即使一个处理消息的应用挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

顺序保证

在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来进行处理。

大量堆积

通过消息堆积能力处理数据迁移场景,针对旧数据进行全量迁移的同时开启增量消息堆积,待全量迁移完毕,再开启增量,保证数据最终一致性且不丢失。

数据流处理

分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后导入到大数据实时计算引擎,通过消息队列解决异构系统的数据对接能力。

业界消息中间件对比

详细的对比可以参考:blog.csdn.net/wangzhipeng…

消息中间件常用协议

AMQP协议

AMQP即Advanced Message Queuing Protocol,提供统一消息服务的高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。

优点:可靠、通用

MQTT协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。

优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统

STOMP协议

STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。

优点:命令模式(非topic/queue模式)

XMPP协议

XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。

优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大

基于TCP/IP自定义的协议

有些特殊框架(如:redis、kafka、rocketMQ等)根据自身需要未严格遵循MQ规范,而是基于TCP/IP自行封装了一套二进制编解码协议,通过网络socket接口进行传输,实现了MQ的标准规范相关功能。

消息中间件推和拉模式对比

Push推模式:服务端除了负责消息存储、处理请求,还需要保存推送状态、保存订阅关系、消费者负载均衡;推模式的实时性更好;如果push能力大于消费能力,可能导致消费者崩溃或大量消息丢失

Push模式的主要优点是:

  1. 对用户要求低,方便用户获取需要的信息

  2. 及时性好,服务器端即时地向客户端推送更行的动态信息

Push模式的主要缺点是:

  1. 推送的信息可能并不能满足客户端的个性化需求

  2. Push消息大于消费者消费速率额,需要有协调QoS机制做到消费端反馈

Pull拉模式:客户端除了消费消息,还要保存消息偏移量offset,以及异常情况下的消息暂存和recover;不能及时获取消息,数据量大时容易引起broker消息堆积。

Pull拉模式的主要优点是:

  1. 针对性强,能满足客户端的个性化需求

  2. 客户端按需获取,服务器端只是被动接收查询,对客户端的查询请求做出响应

Pull拉模式主要的缺点是:

  1. 实时较差,针对于服务器端实时更新的信息,客户端难以获取实时信息

  2. 对于客户端用户的要求较高,需要维护位点

相关资料

建议学习以下的技术文档,了解更多详细的技术细节和实现原理,加深对消息中间件的理解和应用,同时可以下载开源的源代码,本地调试相应的代码,加深对技术原理的理解和概念的掌握,以及在实际生产中更多的掌握不同的消息队列应用的场景下,高效和正确地使用消息中间件。

RocketMQ资料: github.com/apache/rock…

Kafka资料: kafka.apache.org/documentati…

阿里云RocketMQ文档: help.aliyun.com/document_de…


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025955365812437028

收起阅读 »