关于后端开发:每天5分钟行为型模式三

状态模式状态模式的益处是将与特定状态相干的行为部分化,并且将不同状态的行为宰割开来。 将特定相干的行为都放入一个对象中,因为所有与状态相干的代码都存在于某个ConcreteState 中,所以通过定义新的子类能够很容易地减少新的状态和转换。 Context: 上下文,定义了客户程序须要的接口并保护一个状态类。 State: 状态类,定义一个接口以封装上下文环境的一个特定状态相干的行为,与状态相干的操作委托给具体的state对象进行解决。 Concrete State: 具体状态类 状态模式UML 采纳的例子是王者外面的,要么在打团要么在打团的路上,这就波及到了状态的转换。 状态模式State public interface State { void handle(Context context);}Concrete State 回城状态 public class ConcreteStateBack implements State{ @Override public void handle(Context context) { System.out.println("没状态了,回城补个状态先"); context.setState(new ConcreteStateWalk()); }}打团状态 public class ConcreteStateFight implements State{ @Override public void handle(Context context) { System.out.println("大招一按,天穹一开,双手一放,要么黑屏,要么五杀"); context.setState(new ConcreteStateBack()); }}打团的路上状态 public class ConcreteStateWalk implements State{ @Override public void handle(Context context) { System.out.println("状态已满,等我汇合打团"); context.setState(new ConcreteStateFight()); }}失败状态 public class ConcreteStateDefeated implements State{ @Override public void handle(Context context) { System.out.println("Defeated!!!"); }}Context ...

May 1, 2021 · 5 min · jiezi

关于后端开发:Worktile-权限设计与实现

作者:Worktile 后端开发工程师霍世杰 Worktile是国内最优良的企业级我的项目合作与指标管理工具之一,这个我的项目曾经继续了9年之久,书写了研发团队的历史长卷,我作为“后来者”有幸地参加其中。在过来研发的一年里,做的事件大多数是对原有性能的加强和重构,也学习和总结了 一点点Worktile核心技术和常识,本文就是其中之一—— 权限零碎。 Worktile的权限异样简单,在开发中,从纳闷到深刻,再到起初的望之止步,直至最终的克服,这其中屡次与共事的交换,又屡次的总结,逐步地清晰,到当初对它进行了我认为比拟全面的总结,上面就跟大家一起 揭秘(分享) 这块简单而精彩的内容。 一、Worktile 帐户体系 首先总览一下Worktile的帐户体系。在此之前,先介绍一下该零碎:它是多租户零碎的一种,在我接触过的多租户零碎中,有两种类型,一种是像企业微信,飞书,它们的租户类型有用户和团队/组织/企业,那么这类利用的帐户体系 就有两个用户概念,一个是零碎用户,一个是团队成员;而另一种就是 Worktile,租户类型 是 企业/团队/组织的,它是不含集体租户,所有业务的根底条件是 团队(team) ,所以权限这块也是基于 团队成员来管制的。 目前比拟支流的权限设计模型,一种是ACL(Access Control List),是次要是基于用户来管制权限,而另一种是RBAC模型(Role-Based Access Control )基于角色的访问控制,而这两种在Worktile中都有波及,在绝大部分是RBAC模型,大量的权限是基于用户设计和管制的。 上图中,能够看到Worktile权限是由性能权限和数据权限两局部组成,上面对这性能权限和数据权限两局部 具体解说。 二、设计与实现性能权限和数据权限都是基于利用的维度来划分的,Worktile 现有的利用有我的项目、工作、指标(OKR)、音讯、日历、审批、网盘、考勤...... (更多理解点 这里)。 性能权限设计性能权限是齐全依照 RBAC 模型 设计的,关系为: 由两局部组成:操作权限和可见权限,是依照利用模块的维度划分的,每个利用模块下散布多个权限点和可见范畴。 给用户出现的模式是在利用的后盾—>角色治理中(见下图),其中企业角色蕴含两局部: 1. 零碎默认角色(所有者、管理员、成员、部门主管) 2.自定义角色,权限列表(下图右侧)中,打对勾的代表该角色已领有的权限,数据范畴 代表 该角色 在 利用内的可见维度。 可操作权限 可见权限 实现在传统关系型数据库的设计,根本都是三张表:角色表,权限表,角色权限关联表,如果校验一个或一组权限,是须要三表关联查问的。 而在Worktile中则不然,采纳的贮存形式是:非关系型数据库Mongodb + 零碎配置文件, 由一张表来体现,权限列表由零碎配置文件存储。 Mongodb,人造反对数组和JSON类型的数据贮存,在角色和权限的关联配置中更为灵便,这一环在此设计中,不可或缺! 配置文件次要存储的是权限列表,零碎内置,前端也须要一份配置是因为列表展现地位匹配。 为何这么设计? 数据库是因为整个产品的主库就是 Mongodb,这也是最大的起因;权限列表之抉择配置文件贮存,我猜测 是因为权限是零碎内置,并且是固定的,改变频率较低,所以没有必要每次都要再交互一次数据库,不仅是性能权限如此,上面谈到的数据权限亦是如此。 零碎的权限列表 配置的数据结构大抵如下(以下只是阐明构造,并非理论数据): 一级节点的key代表的是利用模块,二级节点的key代表权限点,value为权限的形容,具体如下: 角色与权限的关联配置 ...

April 28, 2021 · 2 min · jiezi

关于后端开发:postgreSQL数据库导入与导出

Windows中PostgreSQL数据库的备份和还原1、备份:         通过cmd命令窗口进入到PostgreSQL装置目录下的bin目录下:而后,输出以下命令: 2、还原 也是通过cmd命令窗口进入到PostgreSQL装置目录下的bin目录下,而后输出以下命令即可。但在还原数据库之前,须要提前建设一个空白的同名数据库。 3、单表备份: 单表复原的办法同上。

December 21, 2020 · 1 min · jiezi

关于后端开发:提高生产力最全-MyBatisPlus-讲解

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦! 本文次要介绍 MybatisPlus的应用如有须要,能够参考 如有帮忙,不忘 点赞 ❥ 微信公众号已开启,小菜良记,没关注的同学们记得关注哦! 如果你每天还在反复写 CRUD 的 SQL,如果你对这些 SQL 曾经不耐烦了,那么你何不破费一些工夫来浏览这篇文章,而后对已有的老我的项目进行革新,必有播种! 一、MP 是什么MP 全称 Mybatis-Plus ,套用官网的解释便是成为 MyBatis 最好的搭档,简称基友。它是在 MyBatis 的根底上只做加强不做扭转,为简化开发、提高效率而生。 1. 三大个性1)润物无声只做加强不做扭转,引入它不会对现有工程产生影响,如丝般顺滑。 2)效率至上只需简略配置,即可疾速进行单表 CRUD 操作,从而节俭大量工夫。 3)丰盛性能代码生成、物理分页、性能剖析等性能一应俱全。 2. 反对数据库mysql 、mariadb 、oracle 、db2 、h2 、hsql 、sqlite 、postgresql 、sqlserver 、presto 、Gauss 、FirebirdPhoenix 、clickhouse 、Sybase ASE 、 OceanBase 、达梦数据库 、虚谷数据库 、人大金仓数据库 、南大通用数据库3. 框架结构 瞎话说,以上这些内容只有你关上官网也能看到,那么咱们接下来就先来实际操作一番! 二、MP实战1. 手摸手式我的项目练习1)数据库及表筹备sql 语句:  use test; CREATE TABLE `student` (  `id` int(0) NOT NULL AUTO_INCREMENT,  `dept_id` int(0) NULL DEFAULT NULL,  `name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,  `remark` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,  PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES (1, 1, '小菜', '关注小菜不迷路!'); INSERT INTO `student` VALUES (2, 2, '小明', '好好学习,天天向上!');2)pom 依赖 <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-test</artifactId> </dependency> <!--lombok--> <dependency>  <groupId>org.projectlombok</groupId>  <artifactId>lombok</artifactId>  <version>1.16.16</version> </dependency> <!--MP插件--> <dependency>  <groupId>com.baomidou</groupId>  <artifactId>mybatis-plus-boot-starter</artifactId>  <version>3.2.0</version> </dependency> <!--Mysql--> <dependency>  <groupId>mysql</groupId>  <artifactId>mysql-connector-java</artifactId>  <version>8.0.21</version> </dependency> <!-- 连接池 --> <dependency>  <groupId>com.alibaba</groupId>  <artifactId>druid</artifactId>  <version>1.2.1</version> </dependency> <!--JUNIT--> <dependency>  <groupId>junit</groupId>  <artifactId>junit</artifactId>  <version>4.13.1</version> </dependency>3)配置文件 spring:  datasource:  url: jdbc:mysql://localhost:3306/test  username: root  password: 123456  driver-class-name: com.mysql.cj.jdbc.Driver4)实体类 @Data @Builder @TableName("student") public class User {   @TableId(type = IdType.AUTO)  private Integer id;   private Integer deptId;   private String name;   private String remark; }5)Mapper public interface UserMapper extends BaseMapper<User> {}6)测试类 @RunWith(SpringRunner.class) @SpringBootTest public class MapperTest {   @Autowired  private UserMapper userMapper;   @Test  public void getAll() {  List<User> users = userMapper.selectList(null);  users.forEach(System.out::println);  } } /** OUTPUT: User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!) User(id=2, deptId=1, name=小明, remark=好好学习,天天向上!) **/小菜结: ...

December 6, 2020 · 8 min · jiezi

关于后端开发:低代码开发平台的敏捷之力

随着互联网的深刻倒退,很多企业逐渐降级了本人的信息系统,然而在对现有IT架构进行降级革新的过程中,通常面临着诸多的压力与挑战。随着先进技术的一直引入,将企业的信息化倒退置于新旧迭代的循环之下,为了更加无效地应答这些挑战,企业须要思考新技术是否为业务发明更多的机会。 值得注意的是,企业本来依靠的开发架构在很多方面曾经不能满足业务倒退的需要,并逐步被当今所风行的麻利了解和低代码开发所取代,并呈现出更多拓展的可能性。 作为生产力进步的延长,麻利开发和低代码开发相辅相成,但具体实施起来又不是那么容易,明天咱们就看看这两者在理论应用过程中是如何进行交融的。 当初很多行业都会提到麻利的概念,比方建设、学习、业务以及制作等,不过它最后的提出却是针对软件行业的,因为其良好的适用性,以至于在其余行业也发光发热。狭义上讲,麻利模式依赖于迭代和增量的倒退,特地是效率晋升和广大的适应性,使其被越来越多的团队所关注。  麻利的个性: 疾速适应外部和内部变动; 疾速响应业务或客户的需要; 在不升高产品交付品质的前提下,以经济高效的形式带动技术改革; 为企业放弃强劲的竞争劣势; 采纳麻利模式须要高度的组织化。麻利团队无论基于传统模式开发还是应用低代码开发,首先要抉择一位经验丰富的技术负责人,他将参加、受权并及时对我的项目进行响应。例如,在团队习惯每周或每次集中突击开发时,则须要其把控节奏,保障我的项目进度合乎预期,并达到质量标准,技术团队的负责人需每天关注并疾速决策需要的优先级,对交付产品进行验收。 在我的项目验收的体系下,所有IT团队成员都应该依据对立的规范来进行产品对接,包含最终交付物的验收规范。在开发过程中,麻利团队成员往往须要合作,并提出如何改良产品的优化倡议。 得益于简化开发、缩短测试周期等能力,低代码将有助于放弃高效的开发进度,并保障产品可能如期甚至提前交付。 另外,谈到麻利,咱们须要介绍一种当今广泛应用的实际办法“Scrum”。 麻利Scrum方法论能够帮忙业务晋升产品质量与价值,并加强团队合作的透明度,在很多跨行业的工作流程中,他们都遵循麻利Scrum的办法来进行,而低代码的呈现将有助于Scrum进一步演变成为更具麻利能力的办法。 规模化麻利规模化麻利或“规模麻利”是促成大型麻利施行的零碎框架。目标是为肯定数量技术团队提供IT结构设计和治理,以便于大型团队从事简单我的项目。大规模的技术团队能力会更加全面,但也会升高麻利的效率劣势。 面对不同的IT环境,存在着多样的麻利框架,它们的范畴从轻到重,且各有优劣。包含Nexus、大型Scrum(LeSS)、Scrum @ Scale(S@S)、SAFe等,这些大都须要装备数集体的Scrum团队。值得关注的是,通过应用低代码开发平台能够进一步扩大规模化麻利,从而实现麻利开发中的多种成果。除了提高效率外,低代码还提供了自动化以及对DevOps的弱小撑持,低代码和规模化麻利一起加强了企业的IT能力。 麻利模式的最佳实际都是从抉择一个经验丰富的团队进行绝对较小范畴的利用开始的。优良的办法要可能解决企业本身的问题,并确保办法的适用性,从而取得企业决策层的反对,随着办法在IT团队中一直利用与优化,麻利开发模式更容易进行复制和延长。此外,在麻利项目管理工具中应建设规范化的规范,在我的项目文档中,这样的操作能够缩小开发阶段因发现缺失规范而延误的开发排期。 最初,对于曾经可能纯熟应用低代码开发平台的麻利团队,应该思考如何缩短集中开发的持续时间,毕竟麻利开发与低代码开发的最终目标都是为了比传统平台更快地向用户交付有价值的软件。 北风.

November 4, 2020 · 1 min · jiezi

关于后端开发:RabbitMQ-基础概念进阶

上一篇 RabbitMQ 入门之根底概念 介绍了 RabbitMQ 的一些根底概念,本文再来介绍其中的一些细节和其它的进阶的概念。 一、音讯生产者发送的音讯不可达时如何解决RabbitMQ 提供了音讯在传递过程中无奈发送到一个队列(比方依据本人的类型和路由键没有找到匹配的队列)时将音讯回传给音讯发送方的性能,应用 RabbitMQ 的客户端提供 channel.basicPublish 办法的两个参数 mandatory 和 immediate (RabbitMQ 3.0 以下版本),除此之外还提供了一个备份交换器能够将无奈发送的音讯存储起来解决,不必从新传回给发送方。 1.1 mandatory 参数mandatory 被定义在 RabbitMQ 提供的客户端的 channel.basicPublish 办法中,如下所示: 当把办法的 mandatory 参数设置为 true 时,那么会在交换器无奈依据本身的类型和路由键找到一个符合要求的队列时,RabbitMQ 会主动调用 Basic.Return 把该音讯回传给发送方也就是咱们的音讯生产者。反之,如果设置为 false 的话,音讯就会被间接抛弃掉。那么问题来了,咱们要如何去获取这些没有被发送进来的音讯呢?RabbitMQ 给咱们提供了事件监听机制来获取这种音讯,能够通过 addReturnListener 办法增加一个 ReturnListener 来获取这种未发送到队列的音讯,如下所示: 通过查看 ReturnListener 接口的源码能够看到,该接口只有一个办法,如果是 JDK8+ 的版本的话能够应用 Lambda 表达式来简化一些代码。 能够看出,当设置了 mandatory 参数时,还必须为生产者同时增加 ReturnListener 监听器的编程逻辑,这样就会使得生产者的代码变得更加简单了,为了解决这种状况,RabbitMQ 提供了 `备份交换器` 来将没有胜利路由进来的音讯存储起来,当咱们须要的时候再去解决即可。 1.2 immediate 参数该的参数同样也是在channel.basicPublish 办法中定义的,其官网形容如下: This flag tells the server how to react if the message cannot be routed to a queue consumer immediately. If this flag is set, the server will return an undeliverable message with a Return method. If this flag is zero, the server will queue the message, but with no guarantee that it will ever be consumed.当把 immediate 参数设置为 true 时,如果交换器依据其类型和路由键找到符合要求的队列时,发现所有队列上没有任何消费者,则该音讯并不会存入到队列中,会通过 Basic.Return 命令把音讯回传给生产者。简而言之也就是说,当设置了 immediate 参数时,该音讯关联的队列上存在消费者时,会立刻发送音讯到该队列中,反之如果匹配的队列上不存在任何消费者,则间接把音讯回传给生产者。这里有一点须要留神的是:从 RabbitMQ 3.0 + 曾经去除了该参数。 ...

August 9, 2020 · 2 min · jiezi

设计模式之备忘录模式

0x01.定义与类型定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。类型:行为型UML类图 基本代码实现/** * 发起人类 */public class Originator { /** * 状态编码 */ private String status; public Originator(String status) { this.status = status; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } /** * 创建备忘录 * @return */ public Memento createMemento() { return new Memento(this); } /** * 回滚 * @param memento */ public void restoreMemento(Memento memento) { this.status = memento.getStatus(); }}/** * 备忘录类 */public class Memento { private String status; public Memento(Originator originator) { this.status = originator.getStatus(); } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; }}/** * 备忘录管理类 */public class Caretaker { /** * 备忘录记录栈 */ private Stack<Memento> MEMENTO_STACK; public Caretaker() { MEMENTO_STACK = new Stack<>(); } /** * 添加一个备忘录 * @param memento */ public void addMemento(Memento memento) { MEMENTO_STACK.push(memento); } /** * 获取一个备忘录 * @return */ public Memento getMemento() { return MEMENTO_STACK.pop(); }}测试与应用/** * 测试与应用 */public class Test { public static void main(String[] args) { //备忘录管理 Caretaker caretaker = new Caretaker(); //发起人 Originator originator = new Originator("1"); //创建备忘录1 Memento memento1 = originator.createMemento(); caretaker.addMemento(memento1); //修改并创建备忘录2 originator.setStatus("2"); Memento memento2 = originator.createMemento(); caretaker.addMemento(memento2); //修改状态3 originator.setStatus("3"); System.out.println(originator.getStatus()); //回滚上一次 originator.restoreMemento(caretaker.getMemento()); System.out.println(originator.getStatus()); //回滚上一次 originator.restoreMemento(caretaker.getMemento()); System.out.println(originator.getStatus()); }}输出结果321备忘录模式角色介绍 ...

November 5, 2019 · 3 min · jiezi

游戏服务器和Web服务器的区别

用Go语言写游戏服务器也有一个多月了,也能够明显的感受到两者的区别。这篇文章就是想具体的聊聊其中的区别。当然,在了解区别之间,我们先简单的了解一下Go语言本身。 1. Go语言的特点Go语言跟其他的语言例如Java比起来,算得上一门很年轻的语言。Go语言是由Robert Griesemer、Rob Pike和Ken Thompson于2007年在Google开发。并于2009年正式发布。 Go语言的设计理念围绕着简洁这两个字,认为少即是多。如果你熟悉Java,用Java那一套语法命名跟Go做对比,可以很明显的体会到这种感觉。 Go的特点可以简单的概括成以下几个点。 1.1 静态类型和编译型首先Go是静态类型,静态类型就是编译时就知道每一个变量的类型,得益于此,在编译的阶段就能够发现很多问题。而如果是动态语言,例如JavaScript,有些问题直到运行时才能发现。 Go是编译型语言,看到编译型大家脑子里可能会想到另外一个词解释型。两者的区别从字面上来理解其实已经可以看出来,我用一个简单的例子来类比一下。 编译型 去餐馆吃饭,点了菜之后,饭店会等所有的菜做好了再上解释型 去餐馆吃饭,点了菜之后,陆陆续续的边吃边上1.2 跨平台顾名思义,你写的Go源码在所有的系统都能够运行。 这点其实很好理解,例如Java的口号是"Write once, run anywhere"。我们都知道Java是编译型的语言,但是Java在编译的时候生成的是字节码,这个字节码与当前的操作系统无关,与CPU也无关。 这种字节码必须依赖Java虚拟机才能运行,而虚拟机会将操作系统和CPU之间的差异与用户屏蔽。对于编程的人来说这个过程其实无感知的。而对Java来说,语言本身的跨平台并不能代表代码可以跨平台。 Go的跨平台从某种方面来说,与Java类型,我们需要安装与当前操作系统相对应版本的Go。编译出来的可执行文件会根据操作系统的不同而有所不同。 1.3 自动垃圾回收与JVM一样,Go在运行时的内存管理(GC)由Go语言本身来管理,不需要程序员的参与,但是我们可以干预。 1.4 原生的并发编程何为原生?我们都知道,在Java中如果要实现并发, 需要外部的类库支持(Thread),而Go不需要从外部再引入任何依赖。支持使用关键字go即可。而且Java中是通过共享内存进行通信的,熟悉Go的应该都看过一句话“不要通过共享内存来通信,而应该通过通信来共享内存” 1.5 完善的构建工具从获取、编译、测试、安装、运行和分析等一系列流程都有自己的内置工具。例如获取可以使用go get命令来下载更新指定的代码包,并且对它们进行编译和安装,可以使用go build 对源码进行编译,用go run命令来运行Go的程序,用go fmt来快速格式化代码,统一代码风格。 1.6 多范式编程目前主流的编程范式有命令式编程、函数式编程和我们最熟悉的面向对象编程。在编写Go的代码的时候,我们可以选择使用面向对象的方法,也可以使用函数式编程的思想,相互结合,相辅相成。 例如,在Go里面也可以用接口来描述行为,也可以使用纯函数来避免出现副作用。因此,多范式编程就是指这个语言支持多种编程范式的。 1.7 代码风格强统一使用Go的内置工具go fmt即可快速的将代码格式化成官方统一的标准,以此来达到代码风格统一的目的。甚至可以用golangci-lint来检测你的语法跟内置的标准语法是否有冲突,完全可以将这个检测工具挂在git的钩子上,以此来达到强制的代码风格统一的目的。 1.8 活跃的社区还有一个很重要的特点是,国内的Go的社区十分的活跃,这对于Go在国内的普及起到了很大的作用。 2. 用Go的优势先说一下我对Go语言的看法,我认为Go在服务器这块是非常有优势的。以后如果有高并发的应用场景,那么大概率这个服务就是用Go写的。不知道大家有没有发现,摩尔定律正在失效。近十年内,硬件的原始处理能力都没有太大的提升。显然,一味的增加晶体管的数量已经不是解决问题最好的方法。 NASA前不久发布到官网然后又迅速删掉的文章透露了,Google可能已经实现了量子霸权,通俗一点说就是拥有超越所有传统计算机的计算能力。而放置更多的晶体管的代价也越来越高,所以现在厂商都在向处理器中添加更多的内核来提升性能。 就像大家熟悉的Java,虽然Java本身支持多线程,但是在Java上使用多线程编程代码算是比较昂贵的。在Java中创建一个新的线程就会消耗接近1M左右的内存。假如你真的需要支持运行上千个线程,那么服务很可能运行着就OOM了。除了内存消耗外,还会存在由于支持多线程带来的并发和死锁等问题。 而Go中,使用协程来代替线程。而且一个协程所消耗的内存比线程少了很多倍。同样的物理设备限制,你可能只能启动最多几千个线程,而协程能够启动上百万个。而且不同的Goroutine可以通过信channel进行安全的通信。 3. 游戏服务器和Web服务器的区别有些对游戏服务器的介绍可能会说,游戏服务器是一个需要长期运行的程序,然后怎么怎么样。我个人认为Web服务器一样的需要长期运行,也需要响应不定点不定时来自用户的请求。两者从宏观上来看其实没有本质的区别。同时Web服务器也会对于稳定性和性能有要求,游戏服一般分为大小服,我们这里都按照小服举例子。 3.1 状态首先要提到的就是状态。可能你会听说过一个概念,游戏服务器是有状态的,而Web服务器是无状态的。什么意思呢?Web服务器的数据流大多直接会到数据库中。而游戏服务器的数据流首先会到内存中,然后定期的写入数据库(落地)。 换句话说,游戏服务器本身的数据与数据库中的数据在运行期间会存在一个数据不一致的窗口。如果此时游戏服务器宕机了,那么就会造成数据首先到的内存数据与数据库存的数据不一致。 而Web服务器则不会有这样的问题,Web所有的数据状态都会落地,而且可以针对操作加上事务,不用担心因为操作失败而引入脏数据。正因为有了状态的约束,游戏服务器就会很慎重的使用内存、CPU。以求在资源有限的情况下,最大化的提高的承载量,并且降低服务延迟。当然,Web服务器会为了降低某个接口的响应时间而去做对应的优化。 3.2 扩容在Web服务器中,如果你不能评估一个服务所面临的压力,又不想因为瞬时的热点访问导致服务直接不可用的话,完全可以设置成自动扩容,因为每个服务只是单纯的接收请求,然后处理请求、返回结果,不会将数据保存在服务器的内存中。要有数据存到内存,那也是在Redis中。而Redis数据丢失对数据的一致性基本没有影响。 但是在游戏服务器这边很难做到像Web那样灵活。首先,数据的流向不是数据库,而是内存。 举个很简单的例子,玩家的主城被攻打着火了,如果有了自动扩容,很有可能在落地的窗口内,玩家再请求一次,请求到了另一个实例。主城又没有着火了。因为数据都会先存在内存中。 再举一个例子,玩家氪金买了一个礼包。然后退出游戏,落地窗口内再次上线没了。这就不是单纯的数据问题了,玩家这是花了真金白银买的道具,突然就没了,一两个还好处理,如果多个玩家都出现这样的问题,那这就属于严重的线上事故了。修复数据的工作量十分的大。 所以,对于一个游戏服务器,所能使用的内存和CPU的资源是非常有限的,不像Web服务器可以不用花很大的代价做到横向扩展。这也就是为什么游戏服务器会十分十分的注重代码的性能以及稳定性。 3.3 稳定就像上面说的例子,如果游戏服务器运行中出了BUG,导致服务直接不可用,或者说通过这个BUG刷到了大量的道具,将是一个非常严重的线上事故。 而对于Web服务器来说,如果是管理系统之类的,有可能会有脏数据值得一提的是,脏数据对于Web来说,排查起来也是一件很头疼的事情。如果没有脏数据,只是服务暂且不可用,而且如果用的是微服务架构,重启服务的代价是相对来说比较小的,只有正在重启的服务的业务是不可用的,其余的部分则可以正常的访问。 而对于游戏服务器来说,服务器重启影响的是全服的玩家。玩家在停服期间,甚至连游戏都进不了,特别的影响玩家体验。而且,如果停服之前服务器的数据落地出现了问题,服务重启之后会将数据从数据库load到内存中,此时同样会造成数据不一致的问题。 3.4 性能从我的经验来看,在做Web服务器的时候,没有为了减少GC的压力,为了少占用内存去做过多的优化。当然这是因为项目本身的体量不大,如果QPS很高的话,Web服务器同样很需要注重性能,只不过游戏服务器需要一直特别注意这个方面。 不过在Web,如果访问量很大的话导致单个服务不能扛住压力,大部分人首先想到的解决方案应该就是搞多个实例,毕竟可以做到很轻松的横向扩展。 在游戏服务器里,会把服务器的资源看的相当的宝贵。例如,能不落地的字段就绝对不要落地,某个字段的值可以通过已知的条件算出来的,就尽量不要定义在代码里。不过这也要看具体情况权衡运算量和调用的频率。因为上线之后,如果遇到了数据不一致,维护的数据越少,修复数据的难度就越小。 ...

October 15, 2019 · 1 min · jiezi

Java程序员必备Linux的面试常见问题及面试题你知道多少

一. 常用命令1. 编辑相关①. awk NF:字段总数NR:第几行数据FS:分隔字符②. sed -n-i 直接修改4a:在第四行后添加4i:在第四行前插入1,5c sting:用sting替换1到5行的内容s/要被替换的字符串/新的字符串/g③. sort -t-nr sort |uniq -c |sort -nr④. tr -d:删除[a-z] [A Z]:替换2. 查看负载相关①. top load average cpu 里面的几个数字代表什么意思,怎么衡量,为什么 load average 50 算高还是低?怎么计算的?系统在1,5,15分钟的平均工作负载,进程队列中的平均进程数量。一般不能大于系统逻辑CPU的个数/proc/loadavg关键参数Task:僵尸进程的数量CPU:%wa IOwaitMem:Swap:要尽可能的少用②. uptime ③. free:读取自文件:/proc/meminfo buffer存放要写回到磁盘的数据cache存放从磁盘上读出的数据-buffers/cache,表示一个应用程序认为系统被用掉多少内存;被程序实实在在占用的内存+buffers/cache,表示一个应用程序认为系统还有多少内存;可用的内存数。④. vmstat:动态的了解系统资源运行 -d:磁盘r:等待运行的进程数,r<5表示状态好b:处于非中断睡眠状态的进程数,b≈0表示状态好id:CPU闲置时间如果r经常大于3或4,且id经常小于50,表示CPU负荷很重⑤. ps aux-l⑥. lsof:列出被进程所打开的文件名 ⑦. pwd首先获取当前目录的i节点编号,但是并不能知道当前目录的名称,我们切换到其的父目录,在里面寻找当前i节点编号对应的文件名即可。终止条件是"."和".."指向同一个i节点,我们可以以此判断是否发到达了根目录 ⑧. pgrep 3. 查找①. grep -n-v-A-B②. find 时间: 4:4天前的那一天+4:大于等于5天之前-4:小于等于4天之内-exec 命令 { } ;4. 磁盘①. du -sh /du -cks * | sort -rn | head -n 10评估目录所占容量,通过将指定文件系统中所有的目录、符号链接和文件使用的块数累加得到该文件系统使用的总块数du命令是用户级的程序,它不考虑Meta Data,而df命令则查看文件系统的磁盘分配图并考虑Meta Data。du以文件名、目录名为依据计算空间使用的,而df是以硬盘块使用情况来计算空间使用的。-sm 以M为单位列出文件容量②. df ...

October 14, 2019 · 2 min · jiezi

必看java后端亮剑诛仙最全知识点

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。你可能有所感悟。零散的资料读了很多,但是很难有提升。到处是干货,但是并没什么用,简单来说就是缺乏系统化。另外,噪音太多,雷同的框架一大把,我不至于全都要去学了吧。 这里,我大体根据基础、Java基础、Java进阶给分了下类,挑的也都是最常用最重要的工具。 这篇文章耗费了我大量的精力,你要是觉得好,请不要吝啬你的赞。如果你认同,可以关注我的微信公众号xjjdog,里面讲的就是这些内容。我会尝试更加系统化。 最新的内容会在github持续更新,添加新的精选相关文章。地址: https://github.com/sayhiai/javaok基础知识数据结构基本的数据结构是非常重要的,无论接触什么编程语言,这些基本数据结构都是首先要掌握的。具体的实现,就体现在java的集合类中。这些数据结构,就是这些复杂工具的具体原始形态,要烂记于心。 培训机构一般没有时间普及基础知识,通过算法和数据结构,“通常”能够一眼看出是否是经过培训。 常用算法算法是某些大厂的门槛。毫无疑问,某些参加过ACM的应届生,能够秒杀大多数工作多年的码农。算法能够培养逻辑思维能力和动手能力,在刚参加工作的前几年,是非常大的加分项。但随着工作年限的增加,它的比重在能力体系中的比重,会慢慢降低。 算法的学习方式就是通过不断的练习与重复。不精此道的同学,永远不要试图解决一个没见过的问题。一些问题的最优解,可能耗费了某个博士毕生的精力,你需要的就是理解记忆以及举一反三。最快的进阶途径就是刷leetcode。 对于普通研发,排序算法和时间复杂度是必须要掌握的,也是工作和面试中最常用的。时间充裕,也可涉猎动态规划、背包等较高阶的算法知识,就是下图的左列。 书籍《算法导论》 《编程之美》 《数学之美》 数据库基础 MySQLMySQL是应用最广的关系型数据库。除了了解基本的使用和建模,一些稍底层的知识也是必要的。 MySQL有存储引擎的区别。InnoDB和MyISAM是最常用的,优缺点应该明晓。ACID是关系型数据库的基本属性,需要了解背后的事务隔离级别。脏读、幻读问题的产生原因也要了解。 为了加快查询速度,索引是数据库中非常重要的一个结构,B+树是最常用的索引结构。因字符集的问题,乱码问题也是经常被提及的。 专业的DBA通常能帮你解决一些规范和性能问题,但并不总是有DBA,很多事情需要后端自己动手。 书籍《MySQL技术内幕——InnoDB存储引擎》 《高性能MySQL》 《高可用MySQL》 网络基础网络通信是互联网时代最有魅力的一个特点,可以说我们的工作和生活,每时每刻都在和它打交道。 连接的三次握手和四次挥手,至今还有很多人非常模糊。造成的后果就是对网络连接处于的状态不慎了解,程序在性能和健壮性上大打折扣。 HTTP是使用最广泛的协议,通常都会要求对其有较深入的了解。对于Java来说,熟悉Netty开发是入门网络开发的捷径。 爬虫是网络开发中另外一个极具魅力的点,但建议使用python而不是java去做。 书籍《HTTP权威指南》 《TCP/IP详解 卷一》 操作系统 Linux科班出身的都学过《计算机组成机构》这门课,这非常重要,但很枯燥。结合Linux理解会直观的多。鉴于目前大多数服务器环境都是Linux,提前接触能够相辅相成。 需要搞清楚CPU、内存、网络、I/O设备之间的交互和速度差别。对于计算密集型应用,就需要关注程序执行的效率;对于I/O密集型,要关注进程(线程)之间的切换以及I/O设备的优化以及调度。这部分知识是开发一些高性能高可靠中间件的前提,无法绕过。 对于Linux,首先应该掌握的就是日常运维,包括常用命令的使用和软件安装配置。正则也是必须要掌握的一个知识点。 脚本编程对后端来说是一个非常大的加分项。它不仅能增加开发效率,也能在一些突发问题上使你游刃有余。 书籍《UNIX环境高级编程(第3版)》 《鸟哥的Linux私房菜》 《Linux内核设计与实现》 《Linux命令行大全》 相关文章《Linux上,最常用的一批命令解析(10年精选)》 Java基础JVMJava程序员的最爱和噩梦。以oracle版本为准,各个jvm版本之间有差别。JVM的知识包含两方面。一个是存储级别的,一个是执行级别的。 以存储为例,又分为堆内的和堆外的两种,各有千秋。垃圾回收器就是针对堆内内存设计的,目前最常用的有CMS和G1。JVM有非常丰富的配置参数来控制这个过程。在字节码层面,会有锁升级以及内存屏障一类的知识,并通过JIT编译来增加执行速度。 JVM还有一个内存模型JMM,用来协调多线程的并发访问。JVM的spec非常庞大,但面试经常提及。 另外,jdk还提供了一系列工具来窥探这些信息。包含jstat,jmap,jstack,jvisualvm等,都是最常用的。 书籍《深入理解Java虚拟机》 JDK现在,终于到了java程序员的核心了:JDK,一套依据jvm规范实现的一套API。我们平常的工作,就是组合这些API,来控制程序的行为。 jdk的代码非常庞大,内容也非常繁杂。最重要的大体包括:集合、多线程、NIO、反射、文件操作、Lambda语法等。这部分内容加上下面的SSM,基本上就是大多数小伙伴玩耍的地方。 假如说数据结构和算法是理论,这里就是支撑理论的实现。Java玩的好不好,就是说这里。 书籍《Effective Java 中文版》 《数据结构与算法分析:Java语言描述》 SSM你可能会用SSM开发项目,觉得编程无非就这些东西。设计模式烂记于心,IOC、AOP手到擒来。这里集中了大部分同行,有些可能到此为止就Ok了,因为有些同学接下来的重点是项目管理,而不是技术。 SSM最擅长的是Web开发。目前的表现形式逐渐多样化,随着前后端分离的盛行,Restful这种有着明确语义的模式逐渐流行。 书籍《Head First 设计模式》 《Spring揭秘》 《SpringBoot揭秘》 《MyBatis技术内幕》 《深入剖析Tomcat》 ...

August 18, 2019 · 1 min · jiezi

设计模式之桥接模式

0x01.定义与类型定义:将抽象部分与它的具体实现部分分离,使它们都可以独立地变化。桥接模式将继承关系转化成关联关系,它降低了类与类之间的耦合度,减少了系统中类的数量,也减少了代码量。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。类型:结构型UML类图 Java实现/** * 主类抽象类 */public abstract class Abstraction { /** * 桥接组合对象 */ protected Implementor implementor; public Abstraction(Implementor implementor) { this.implementor = implementor; } /** * 操作类 */ public abstract void operation();}/** * 抽象接口 */public interface Implementor { void operationImpl();}/** * 实现类 */public class RefinedAbstraction extends Abstraction { public RefinedAbstraction (Implementor implementor) { super(implementor); } @Override public void operation() { System.out.println("操作"); implementor.operationImpl(); }}/** * 接口抽象实现1 */public class ConcreteImplementorA implements Implementor { @Override public void operationImpl() { System.out.println("桥接A"); }}/** * 接口抽象实现2 */public class ConcreteImplementorB implements Implementor { @Override public void operationImpl() { System.out.println("桥接B"); }}测试与应用/** * 测试与应用 */public class Test { public static void main(String[] args) { Abstraction abstraction1 = new RefinedAbstraction(new ConcreteImplementorA()); Abstraction abstraction2 = new RefinedAbstraction(new ConcreteImplementorB()); abstraction1.operation(); abstraction2.operation(); }}输出结果操作桥接A操作桥接B角色介绍 ...

August 18, 2019 · 2 min · jiezi

java中的函数

函数的概述1.函数定义: 能完成特定功能的代码段就是函数,每一个函数都是一个独立的小功能 2.为什么要有: 如果没有函数,在实现一个功能时候,就要写一遍这个逻辑, 如果功能多次使用,就会产生大量重复的代码.有了函数之后,把这个功能封装到函数中,当你在使用这个功能时候不需要再写一遍逻辑了,只需要调用函数名即可. 3.好处: 提高代码的复用性提高了代码封装性,把实现给隐藏起来,调用者只要知道有这样一个功能,根本不需要管里面的逻辑简化了程序设计难度函数的定义1.函数定义的格式 修饰符 返回值数据类型 方法名 (参数类型 形式参数一,参数类型 形式参数二....){ 功能代码(方法体); return 返回的数据;}void:当函数没有一个结果返回,返回值数据类型就用关键字void表示。 总结: 声明一个函数,需要先明确两个东西; 函数的返回数据类型; 函数的参数列表; 函数的调用1.格式: 函数(参数1,参数2,…) 2.函数不调用不调用是不会执行的 3.函数调用的三种格式 如果调用函数没有返回值, 直接调用即可如果调用函数有返回值如果结果要在其他位置使用,使用变量存储方法调用的返回值 如果只想输出函数的返回值, 直接把方法的调用写在输出语句中的小括号中 函数执行内存总结: java中,所有函数都是在栈内存中执行的,都是通过return关键字出栈的;栈内存的特点是先进后出;正在执行的函数一定是位于栈顶的函数,在栈底的函数一定是main函数; 当main函数出栈,整个程序也将结束。 重载概念:在同一个类中,可以定义多个名称相同,参数列表不同的函数,这种情况较做函数的重载; public static int sum(int a,int b){ return a + b;}public static float sum(float a,float b){ return a + b;}调用:通过函数名和参数列表共同确定一个函数; 好处:让开发者需要记忆的函数名大大降低,提高开发效率。 开源地址开源项目地址:https://github.com/371854496/...java学习资料:https://github.com/371854496/...各位觉得还阔以的话,点下Star,分享不易,thank you! 公众号 全战开花获取更多技术干货

August 8, 2019 · 1 min · jiezi

Java语言组成

开源地址开源项目地址:https://github.com/371854496/...java学习资料:https://github.com/371854496/...各位觉得还阔以的话,点下Star,分享不易,thank you! 标识符标识符就是用于给 Java 程序中变量、类、方法等命名的符号。 规则:①可以由字母,数字,下划线(_),美元符($)组成,但不能包含@,%,空格等其他特殊字符,不能以数字开头。 ②不能是java关键字 ③是区分大小写的 例如:下面的标识符是合法的:myName,My_name,Points,$points,_sys_ta,OK,_23b,_3_,Myvoid 下面的标识符是非法的:name,25name,class,&time,if变量:使用小驼峰命名法 常量:使用纯大写的方式每个单词之间使用下划线链接MAX_VALUE 方法:使用小驼峰命名法 类,接口,抽象类,枚举:使用大驼峰命名法 包:使用公司域名反写的方式如:com.ujiuye.www 常见关键字 关键字含义 abstract表明类或者成员方法具有抽象属性assert断言,用来进行程序调试boolean基本数据类型之一,布尔类型break提前跳出一个块byte基本数据类型之一,字节类型case用在switch语句之中,表示其中的一个分支catch用在异常处理中,用来捕捉异常char基本数据类型之一,字符类型class声明一个类const保留关键字,没有具体含义continue回到一个块的开始处default默认,例如,用在switch语句中,表明一个默认的分支do用在do-while循环结构中double基本数据类型之一,双精度浮点数类型else用在条件语句中,表明当条件不成立时的分支enum枚举extends表明一个类型是另一个类型的子类型,这里常见的类型有类和接口final用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量finally用于处理异常情况,用来声明一个基本肯定会被执行到的语句块float基本数据类型之一,单精度浮点数类型for一种循环结构的引导词goto保留关键字,没有具体含义if条件语句的引导词implements表明一个类实现了给定的接口import表明要访问指定的类或包instanceof用来测试一个对象是否是指定类型的实例对象int基本数据类型之一,整数类型interface接口long基本数据类型之一,长整数类型native用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的new用来创建新实例对象package包private一种访问控制方式:私用模式protected一种访问控制方式:保护模式public一种访问控制方式:共用模式return从成员方法中返回数据short基本数据类型之一,短整数类型static表明具有静态属性strictfp用来声明FP_strict(单精度或双精度浮点数)表达式遵循IEEE 754算术规范 [1] super表明当前对象的父类型的引用或者父类型的构造方法switch分支语句结构的引导词synchronized表明一段代码需要同步执行this指向当前实例对象的引用throw抛出一个异常throws声明在当前定义的成员方法中所有需要抛出的异常transient声明不用序列化的成员域try尝试一个可能抛出异常的程序块void声明当前成员方法没有返回值volatile表明两个或者多个变量必须同步地发生变化while用在循环结构中关键字的特点 1.全部都是小写的 2.自己给自己定义东西起名字的时候,不能和关键字重名 3.其中有两个关键字是保留关键字, goto和const,当前java版本中还没有使用,不代表以后不会被使用. 修饰符Java 使用了一定数量的称为修饰符的关键字,这些修饰符指定了数据、方法和类的属性以及它们的使用方法。 例如 public 和 static 都是修饰符,另外还有 private、final、abstract 和 protected。 一个 public 数据、方法或者类可以被其他的类访问,而一个 private 数据或方法不能被其他类访问。 注释在 Java 中,一行注释以双斜杠(//)标识; 多行注释包含在“/”和“/”之间; 文档注释包含在“/*”和“/”之间。 常量//声明常量final int COUNT=10;final float HEIGHT=10.2f;变量//声明变量// 数据类型 变量名 = 初始值;int age = 18;使用变量的注意事项 1.变量是有作用域的 作用于: 起作用的范围 范围:从他定义的哪一行起,到出他所在大括号. 变量定义之前不能使用.出了他所在的大括号就不能访问了. 2.在同一个大括号(同一个作用域)中不能定义同名的变量. 3.使用变量的时候必须先给初始值,才能使用. 4.变量的定义和变量初始化可以分开写. 5.可以在同一行一次定义多个变量 数据类型 变量名1 =初始值1, 变量名2 = 初始值2,….; ...

August 8, 2019 · 3 min · jiezi

java入门

1.什么是Java语言简单地说,Java 是由 Sun Microsystems 公司于 1995 年推出的一门面向对象程序设计语言。2010 年 Oracle 公司收购 Sun Microsystems,之后由 Oracle 公司负责 Java 的维护和版本升级。 其实,Java 还是一个平台。Java 平台由Java 虚拟机(Java Virtual Machine,JVM)和 Java 应用编程接口(Application Programming Interface,API)构成。Java 应用编程接口为此提供了一个独立于操作系统的标准接口,可分为基本部分和扩展部分。在硬件或操作系统平台上安装一个 Java 平台之后,Java 应用程序就可运行。 Java 发展至今,就力图使之无所不能。按应用范围,Java 可分为 3 个体系,即Java SE、Java EE 和 Java ME,下面简单介绍这 3 个体系。 2.什么是JavaSEJavaSE 允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE 提供基础, 如 Java 语言基础、JDBC 操作、I/O 操作、网络通信以及多线程等技术。 3.什么是Java EEJava EE 是在 Java SE 基础上构建的, 它提供 Web 服务、组件模型、管理和通信 API 可以用来实现企业级的面向服务体系结构(Service Oriented Architecture,SOA)和 Web 2.0 应用程序。 ...

August 7, 2019 · 1 min · jiezi

开篇二十三种设计模式的通俗理解

本文为本次系列文章的第一篇,接下来,小编预计用一周的时间,带大家重新解读二十三中设计模式,如果你觉得本文对你有帮助的话,可以帮小编点一下“关注”以及“转发”,支持一下小编,谢谢!一、设计模式的分类总体来说设计模式分为三大类: 创建型模式,共五种: 工厂方法模式抽象工厂模式单例模式建造者模式原型模式。结构型模式,共七种: 适配器模式装饰器模式代理模式外观模式桥接模式组合模式享元模式。行为型模式,共十一种: 策略模式模板方法模式观察者模式迭代子模式责任链模式命令模式备忘录模式状态模式访问者模式中介者模式解释器模式。其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下: 二、设计模式的六大原则总原则:开闭原则(Open Close Principle) 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。 1、单一职责原则 不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。 2、里氏替换原则(Liskov Substitution Principle) 里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科 历史替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。 3、依赖倒转原则(Dependence Inversion Principle) 这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。 4、接口隔离原则(Interface Segregation Principle) 这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。 5、迪米特法则(最少知道原则)(Demeter Principle) 就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。 最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。 6、合成复用原则(Composite Reuse Principle) 原则是尽量首先使用合成/聚合的方式,而不是使用继承。 三、最后本文作为本次系列文章的开篇,暂时讲到这里,从下一篇开始,我将详细介绍Java种23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。 对本系列内容感兴趣的同学可以帮小编点一下“关注”,支持一下小编,谢谢!

July 11, 2019 · 1 min · jiezi

学习nodejs服务开发这一篇就够了持续更新中

开头要说的话接触node.js后端开发也有几年时间了。经过几个项目的实践。不可否认,在后端服务领域。node.js还是有一定的用武之地的。当然在平时的实践中也发现了node体系的一些问题和不足。所以写篇文章分享一下我探索node服务的经历。 目录由于工作繁忙,不定时的会更新的。。。 0. JavaScript语言的学习1. 框架的选择node.js的Web开发框架的选择?Egg.js官网了解一下2. 快速入门3. web综合开发4. 模板引擎Nunjuck的使用5. redis的使用6. 持久层框架的使用---Sequelize7. 持久层框架的使用---Mongoose8. 如何优雅的使用Sequelize9. 如何优雅的开发Router10. egg-security插件的使用11. 定时任务12. 多进程模式开发13. 日志开发的实践14. 全局同一异常处理实践15. 中间件(过滤器)开发实践16. 框架的启动自定义实践17. Service层开发实践18. 一些方便开发的小技巧19. 从Java那边借鉴过来的优秀实践20. 服务的监控21. 容器化部署22. 使用TypeScript开发服务

June 30, 2019 · 1 min · jiezi

PHP初级开发知识体系图

June 25, 2019 · 0 min · jiezi

『-效率工具-』Spring-Boot版的轻量级代码生成器减少70以上的开发任务

一. 前言之前很着迷于代码自动生成,减少写重复代码的工作量。网络上也搜索了很久,有基于插件的,有GUI的。但其配置和学习成本都比较高,都不是很如我意。本想自己用SpringBoot写一个,在收集相关的资料的时候, 偶然找到了人人开源实现的一个renren-generator项目,也正好是Spring Boot搭建的,也就下载下来,自己玩起来了。结合自己项目修改模板使用过后, 还是很方便的。我也不重复造轮子了。那么接下来就说说使用心得。 字段类型与实体类型可以自由配置支持表前缀可以设置是否取消数据库支持Mysql、Sqlserver、Oracle、Postgresql可在线生成entity、xml、dao、service、vue、sql代码2.本地部署我fork了一个renren-generator项目地址:https://gitee.com/rayson517/r... 通过git下载源码修改application.yml,更新MySQL账号和密码、数据库名称Eclipse、IDEA运行RenrenApplication.java,则可启动项目项目访问路径:http://localhost演示效果图: 3. 根据实际项目情况DIY代码生成模板模板路径在resources/template文件夹下面, 然后根据已有项目的代码结构,进行修改就好。 4. 一键生成sql数据库说明文档,解放双手这个功能是我自己DIY增加的功能,目前还没提交上去。需要的可以留言呢。 5. 扩展支持DB2扩展数据库DB2的支持,因为我们公司有在用。所以准备增加一个支持。

June 18, 2019 · 1 min · jiezi

精选掘金后端小册及优惠购买链接

掘金小册推出之后,广受开发者的好评,以其小篇幅、高浓度、成体系的知识结构呈现体系让开发者能在短时间了解或掌握一项精确技术,下面小编精选了一些关于「后端开发」的掘金小册及其「优惠购买链接」分享给大家,点击下方标题或图片即可跳转到掘金进行八折优惠购买。 优惠八折购买:MySQL 是怎样运行的:从根儿上理解 MySQL 优惠八折购买:Kubernetes 从上手到实践 – TaoBeier Kubernetes 已经成为容器编排领域事实上的标准,同时围绕着 Kubernetes 所形成的生态体系也愈发的完善。各个公司都在围绕着 Kubernetes 探索如何使用它,并将它用的更好,加之各云厂商也此方面进行努力。毫不夸张的说,掌握 Kubernetes 相关的技能,对个人发展一定大有裨益。 优惠八折购买:Python 实战:用 Scrapyd 打造个人化的爬虫部署管理控制台 – 韦世东本小册将带你从 Scrap 项目打包部署、Scraped 目录结构分析、功能模块释义、源码剖析来逐步理解相关功能的原理,并且通过自定义API、增加统计数据和界面美化等实践来进一步加深你对 Scraped 的理解。最终达到可以随心所欲的将 Scraped 的功能进行扩展,从而实现自己想要的爬虫部署管理控制台。 优惠八折购买:Netty 入门与实战:仿写微信 IM 即时通讯系统 – 闪电侠 本小册通过一个仿微信 IM 系统,来演示如何使用 Netty 一步一步进行服务端和客户端长连通信的开发,本小册所涉及的代码将会按照小节的顺序放置到 GitHub 上,每小节对应一个分支,方便读者由浅入深地学习。 优惠八折购买:基于 hapi 的 Node.js 小程序后端开发实践指南 – 叶盛飞 小册基于语言一致化的 Node. js,选用善于提供接口化服务的应用框架hapi,以精炼渐进的实战案例,串联起搭建后端AP丨服务所需的核心技术,帮助前端开发者走上小程序的全栈之路。 优惠八折购买:Redis 深度历险:核心原理与应用实践 – 老钱 Redis 是互联网存储系统中使用最为广泛的中间件,它也是大型互联网企业在后端工程师技术面试中最常问到的工程技能之一。开发者不仅要掌握 Redis 基础使用,更应当深层理解 Redis内部实现的细节原理,这将最终决定你的技术道路可以走多快走多远。 优惠八折购买:基于 Go 语言构建企业级的 RESTful API 服务 – 雷克斯 ...

June 6, 2019 · 1 min · jiezi

Kubernetes平台的安装详解

1.概述 Kubernetes是Google开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理。在生产环境中部署一个应用程序时,通常要部署该应用的多个实例以便对应用请求进行负载均衡。 在Kubernetes中,我们可以创建多个容器,每个容器里面运行一个应用实例,然后通过内置的负载均衡策略,实现对这一组应用实例的管理、发现、访问,而这些细节都不需要运维人员去进行复杂的手工配置和处理,本文章将介绍Kubernetes平台的安装过程。1.1. 环境准备Kubernetes安装在Ubuntu、CentOS这两个环境上较为稳定,本次安装将以Ubuntu环境为例(CentOS环境也类似),准备两台Ubuntu虚拟机或云服务器,版本为16.04即可。 2.安装Docker&Kubernetes2.1. 安装Docker2.1.1 添加Docker官方源1)更新包索引: apt-get update 2)下载docker官方源的公钥并添加到apt的公钥库中: curl-fsSL https://download.docker.com/l... | apt-key add - 3)添加docker官方源的仓库: add-apt-repository "deb [arch=amd64] https://download.docker.com/l... $(lsb_release -cs) stable" 4)执行完以上命令之后,在/etc/apt/sources.list文件中将添加如下内容 5)再次更新包索引: apt-get update 2.1.1 安装Docker这里以安装docker-ce-17.03.2版本为例:1)执行: apt-get install docker-ce=17.03.2~ce-0~ubuntu-xenial,安装Docker; 2)验证Docker安装结果: dockerversion 3)查看docker后台服务运行的情况: systemctl status docker 至此,Docker安装完成。2.2. 安装Kubernetes2.2.1 Kubernetes相关组件介绍需要下载kubeadm、kubelet、kubectl等组件1) kubeadm:作为安装工具来引导启动集群,kubeadm将kubernetes核心组件以容器化的方式安装和引导启动运行; 2) kubelet:Node组件中的 kubelet依旧以主机后端服务的形式运行kubernetes集群的所有节点上,是主节点与从节点交互的关键组件;3) kubectl:是安装集群后的命令行工具,至少安装在master上,对集群进行管理。 2.2.2 添加Kubernetes的apt源(google官方源)以下将直接添加google的官方源(如果你的服务器不能访问google官方源,请参考2.2.3)1)添加kubernetes apt源的公钥文件: curl-s https://packages.cloud.google... | apt-key add - 2)将官方源列表加入到本地源列表配置目录中: vi /etc/apt/sources.list.d/kubernetes.list,在该文件中加入如下内容: 3)更新本地包缓存:apt-get update 2.2.3 添加Kubernetes的apt源(国内源)1) apt-get update && apt-get install -y apt-transport-https ...

June 3, 2019 · 1 min · jiezi

python基础教程异步IO-之-概念和历史

编程中,我们经常会遇到“并发”这个概念,目的是让软件能充分利用硬件资源,提高性能。并发的方式有多种,多线程,多进程,异步IO等。多线程和多进程更多应用于CPU密集型的场景,比如科学计算的时间都耗费在CPU上,利用多核CPU来分担计算任务。多线程和多进程之间的场景切换和通讯代价很高,不适合IO密集型的场景(关于多线程和多进程的特点已经超出本文讨论的范畴,有兴趣的同学可以自行搜索深入理解)。而异步IO就是非常适合IO密集型的场景,比如网络爬虫和Web服务。 在计算机程序中,IO就是读写磁盘、读写网络的操作,这种读写速度比读写内存、CPU缓存慢得多,前者的耗时是后者的成千上万倍甚至更多。这就导致,IO密集型的场景99%以上的时间都花费在IO等待的时间上。异步IO就是把CPU从漫长的等待中解放出来的方法。这就可以大大提高我们写的软件系统的并发性。这样的软件,可以是网络爬虫,也可以是Web服务等一切IO密集型的系统。 异步IO的优势显而易见,各种语言都通过实现这个机制来提高自身的效率,Python也不例外。Python经历了2和3两个大版本的跃迁。这其中也有对异步IO支持的变化历程。 Python 2的异步IO库Python 2 时代官方并没有异步IO的支持,但是有几个第三方库通过事件或事件循环(Event Loop)实现了异步IO,它们是: twisted: 是事件驱动的网络库gevent: greenlet + libevent(后来是libev或libuv)。通过协程(greenlet)和事件循环库(libev,libuv)实现的gevent使用很广泛。tornado: 支持异步IO的web框架。自己实现了IOLOOP。Python 3 官方的异步IOPython 3.4 加入了asyncio 库,使得Python有了支持异步IO的官方库。这个库,底层是事件循环(EventLoop),上层是协程和任务。asyncio自从3.4 版本加入到最新的 3.7版一直在改进中。 Python 3.4 刚开始的asyncio的协程还是基于生成器的,通过 yield from 语法实现,可以通过装饰器 @asyncio.coroutine (已过时)装饰一个函数来定义一个协程。比如: Python 3.5 引入了两个新的关键字 await 和 async 用来替换 @asyncio.coroutine 和 yield from ,从语言本身来支持异步IO。从而使得异步编程更加简洁,并和普通的生成器区别开来。 注意: 对基于生成器的协程的支持已弃用,并计划在 Python 3.10 中移除。所以,写异步IO程序时只需使用 async 和 await 即可。 Python 3.7 又进行了优化,把API分组为高层级API和低层级API。 我们先看看下面的代码,发现与上面的有什么不同? 除了用 async 替换 @asyncio.coroutine 和用 await 替换 yield from 外,最大的变化就是关于eventloop的代码不见了,只有一个 async.run()。这就是 3.7 的改进,把eventloop相关的API归入到低层级API,新引进run()作为高层级API让写应用程序的开发者调用,而不用再关心eventloop。除非你要写异步库(比如MySQL异步库)才会和eventloop打交道。 ...

May 18, 2019 · 1 min · jiezi

python基础教程文件读写

在Linux系统中,一切都是文件。但我们通常说的文件是保存在磁盘上的图片、文档、数据、程序等等。而在程序的IO操作中,很多时候就是从磁盘读写文件。本节我们讲解Python中的文件对象如何操作文件。 创建文件对象通过Python内置函数open()可以很容易的创建一个文件对象。open函数有很多参数,最常用的有两个,使用open函数最常用的方法是:open(filename, mode)。 f = open('myfile', 'w') 第一个参数filename是文件名的字符串,比如myfile。第二个参数也是一个字符串,表示文件使用方式。mode的可选项如下: mode含义‘r’以只读模式打开(默认)‘w’以可写模式打开,并清楚文件内容(如果文件存在的话)‘x’创建一个新文件并以可写模式打开‘a’以可写模式打开,从文件末尾开始写入(如果文件存在的话)‘b’二进制模式‘t’文本模式(默认)’+’打开一个已存在文件以便进行更新(读和写)其中的'b'和't'是指定文件内容是文本还是二进制,其它都说是关于读写方式的。 'b'是二进制模式打开文件,读写的数据都是字节对象(bytes),这个模式可以读写一切文件,包括文本文件,但读写文本文件时要注意编码的问题。't'是文本模式下读写文件。读取时,默认会把平台特定的行结束符 (Unix 上的\n, Windows 上的 \r\n)转换为\n。写入是,默认会把出现的\n 转换回平台特定的结束符。这种默认的“幕后修改”对文本文件来说没有问题,但会破坏二进制数据(比如,JPEG或exe)文件中的数据。 在使用open创建文件对象时,最好使用 with 关键字。其好处是,当字句体结束后文件会正确关闭,即使在某个时刻引发了异常。并且with比等效的try-finally代码块更简短: In [102]: with open('myfile') as f: ...: data = f.read() ...:In [103]: f.closed Out[103]: True如果没使用with关键字,就要调用f.close()来关闭文件并立即释放它使用的系统资源。如果没有显示的关闭文件,Python的垃圾回收器最终将销毁该对象并为你关闭打开的文件,但这个文件可能会保持打开状态一段时间。另外一个风险是不同的Python实现会在不同的时间进行清理。 通过 with 语句或者调用 f.close() 关闭文件对象后,尝试使用该文件对象将自动失败。 文件对象的方法通过上面的方法创建文件对象f后,,我们就可以通过其对应的方法读写数据了。 (1)写内容到文件:f.write(string)把内容写入文件的方法是write()方法,传递的对象必须是字符串(文本模式下)或字节对象(二进制模式下)。如果要写入其它类型的对象(比如,字典、列表等等),就要先把它们转换成字符串(文本模式下)或字节对象(二进制模式下)。 In [109]: f = open('myfile', 'w')In [110]: f.write('认真学Python,就是文件的全部内容。\n')Out[110]: 21In [111]: f.close()(2)读取文件内容:f.read(size)它会读取文件里面的数据并将其返回为字符串(在文本模式下),或者字节对象(二进制模式)。参数size是一个可选的整数参数,当size被省略或为负的时候,读取文件的全部内容;如果文件的大小是机器内存的两倍或更大,那么可能出现错误。读取并返回的size大小的内容。如果已经读完全部内容(到达文件末尾),将返回一个空字符串。 In [112]: f = open('myfile')In [113]: f.read()Out[113]: '认真学Python,就是文件的全部内容。\n'In [114]: f.read()Out[114]: ''f.readline() 从文件读取一行,换行符\n留住字符串末尾;f.readlines() 读取文件所有行,返回一个字符串的列表; In [118]: f.readline()Out[118]: '认真学Python,就是文件的全部内容。\n'In [119]: f.seek(0)Out[119]: 0In [120]: f.readlines()Out[120]: ['认真学Python,就是文件的全部内容。\n']遍历文件的所有行,也可以用遍历文件对象的方式,这是内存高效、快速并简单的方式: ...

May 18, 2019 · 1 min · jiezi

程序员画像十年沉浮

十年,转瞬即逝,人生进入下半场。众生皆苦,万相本无。且看风云变幻,慢品苦辣酸甜。小姐姐味道微信公众号首发。小胡,拼搏者小胡很久没有笑过了。他在房价的次高点买了套超出自己承受能力的房子,紧接着老婆失业了,失业以后脾气变得特别的不好。他每天都下班很晚,最近终于鼓足勇气换了份离家近的工作,来省下每天的车费。他的技术很好,别人也是这么评价,但这次跳槽他并没有涨多少薪。心里乱乱的老觉得堵,家里环境太压抑,老母亲最近又和老婆掐了起来。他依然每天很晚下班,心烦了就工作。老板看在眼里,给他颁发了一个拼搏者奖杯。今天团建后他开车回家,天很晚,他把头顶在方向盘上呆了很久。推开车门,他静静伫立一刹,把一个包装袋随手扔进垃圾桶。里面是他刚得的奖杯。小苗,全栈培训讲师小苗很久之前就看透了it行业。别人要淘金,他偏要卖淘金的工具。在工作2年之后,通过被培训,他果断进入培训行业,一直到现在,还出了不少书。他可以算是一个全栈培训讲师,懂的东西多如牛毛,就是不深。“深了也没有用,简单才有市场。有能力自学的谁来培训呀”。小吴咧嘴一笑,他刚给自己镶上了金牙,灯光照耀下显得异常夺目。小包,BAT梦小包的学校不好,所以他很自卑,但还是有两把刷子的,所以业绩很好。他的大部分工作都是小公司经历,所以他觉得缺少点什么。随着年龄的增加,他觉得到了去大公司镀金的最后期限。但天不遂人愿,总有这样或者那样的事牵牵绊绊。准备了很多次面试,都不对口。他觉得在过去的这些公司,技术应该算是上等了,实在不明白这些BAT公司为什么死抠一些冷门而且明显用不着的技术。这些公司也贱,见到他这种小公司摸打滚爬的人,就像猫见了老鼠一样,想要尽情蹂躏一番,感受一下高高在上的感觉。“看来我只适合在小公司混啊”,小包回头按了下车钥匙,几米远的保时捷车灯亮了几下。小殷,佛学院的梦想对于一个不善言语的俗人来说,龙泉寺一直是小殷的一个梦想归宿,但这样的组织比Bat门槛还要高,要能力还要学历。小殷修了硕士,参读经书,在一个低很多档次的寺庙一呆就是五年,希望未来的某刻能结佛缘。“世人笑我太疯癫,我笑他人看不穿”。他昨天刚结婚,才休息2天,今天就要上班了。小刘,公司终结者“我干倒了这么多家公司”,每次小刘说到这里,都惨笑着伸出手,打着手势。这其中的背后意义,是小刘每次工作理想的破灭。哦,这当然是骗人的,因为每次都有赔偿金。他选的很准,每次想要弄点期权实现财务自由,可惜结果都不得愿。他的技术并不差,只是运气衰。面试填表的时候,在离职原因那一栏,他都不好意思下手,只能随意编几个说法。“我要认真准备”,小刘不怀好意的笑笑,“进阿里看看”。小姐姐味道微信公众号首发。小李,理想的路上“无论是干啥,哪怕是卖海鲜,都不会干这一行了”。小李每天都这么想。可工作太忙了,登上公司的电梯,行尸走肉般送到自己的座位,小李就开始了一天忙碌的生活。空闲时候,他喜欢逛知乎,看怎么快速赚钱的话题。回了家累的又什么都不想干,只好刷下抖音什么的。时间匆匆流过,每天他都想着单干,但第二天依然灰溜溜的穿衣上班。小李觉得,干这一行,就要了解自己的优势。他的优势就是技术,所以还得花时间学习一些烂七八糟的技术,那些叫做视野的东西。“我还没准备妥当”,他每次都这么安慰自己,“万无一失的时候我就自己干”。小万,互联网暴发户“风口、风口,都是风口”,小万笑呵呵的说,“我就是那头猪”。刚毕业,小万找工作就遇到不少的障碍。他只会两种语言的hello world。一个是c,一个是java。随着每次碰壁,小万的态度变的无所谓起来,他进了一个刚成立的小作坊,工作内容就是整理excel,只要饱自己就行。公司也实行过一些非人类的制度,但想到出去后还要找工作,就坚持了下来。没想到,公司在短短5年时间里就上市了,作为头部员工,他获得了不菲的回报。他的编程水平也有进步,现在会四门语言的hello world了。小江,坚持是无谓的等待小江终于明白了一个道理。如果你看好一家公司,就一直待下去,和它一直成长。苦也好,酸也罢,不亢不卑,不离不弃。得出这个结论,是因为小江前几家公司都上市了,前同事都分了不少。每次都在他离职后不久,而且是在经济好像要出现问题的时候。他的这家公司,据说也是独角兽。虽然从前年就有离职的打算,但还是咬牙坚持了下来。他要好好的待下去,套现,走人。但也会焦虑,和他一块来的小王,已经被劝退了,那点赔偿都不够塞牙缝的。“坚持就是胜利,我和它死磕到底”,小江用梳子打磨着自己脱发的前额,看着镜中的自己,说。小阮,收租快乐小阮找这份工作,完全是因为无聊。自从村里农改拆迁划入市区之后,他就辞职出去旅游了。最后见识到更加有钱的人的生活,认识到自己的差距后,也实在是玩腻了,就找了份工作打打牙祭。他花了很长时间,写了非常精美的微信小程序,开了个公众号,用来收租。房客想要租房,首先得关注他的公众号。同村的七大姑八大婶有了钱以后,基本上也不着家了,他就都揽过来,替他们打点、收租,仅收取部分管理费。“这群狗日的租客,老想着沾便宜”。他用力拍了下键盘,夹着的香烟烟灰四散,烫的小阮尖声叫骂。小姐姐味道微信公众号首发。小甄,奶爸小甄真是幸福,朋友们都这么说。小甄的老婆特别牛,但孩子没人带,所以他就辞职在家带娃。这一带就带了三年。他其实挺喜欢写代码的,但程序员工资还是低了点。比起天天在家带娃做饭,他还是比较怀念写代码。自从辞职后,他就只用python了,主要是做一些爬虫性的脚本,分析一些问题。他庆幸自己懂互联网,所以抖音的号也做起来了。作为一个男人,他最怕老婆有外遇,那样他就一无所有了,所以变着花样讨好老婆。但他还是有些担心,所以总是偷偷学python。“过几年我就去做人工智能”,小甄嘿嘿的笑起来。小冯,兔子的艺术企业大了,就要讲价值观。就像养狗一样,要有规矩。这时候,一些符合价值观,但没有能力的人,就上台表演了。小冯深谙职场潜规则,老板表演他拍照,老板群里发言他点赞,老板提要求他表决心,就是不干活。工作不就是crud么,他看的是编程之外的东西。兔子本来就是一种悠闲的生物,他只是想要自己的一颗胡萝卜而已。“我是马云眼中的兔子,是他们逼我这样的”。小冯对自己的定位直言不讳。小艾,自由职业者小艾实在是不喜欢天天挤着公交去上班的生活,所以他辞职,做了一个自由职业者。小公司做事,大公司做人,自由职业者做什么呢?他什么都做,哪里有钱,哪里就是他的靶子。股市火的时候,他炒股。比特币火的时候,他炒币。更多的是接一些不痛不痒的活儿,勉强度日。即使这样,也是基本收个首付款,尾款根本就回不来。最近他很焦躁。到处都在割韭菜,收入不增反减,他已经考虑再次打工了。“我是没认识到自己的能力和资源”,小艾不无感慨的说,“其他人做这一行,应该比我做的好”。小吴,工作+旅行有没有想过另外一种生活?每年换一个城市去工作,游览完毕后轻轻的走,不带走一片云彩。小吴就是这样,当他没钱花了,就去工作。大部分时间,他都在旅行。平均每一年,他就到一个美丽的城市落脚,去品味小城的故事。在这过程中,他选了几个比较好的城市,都购置了房产。去年为了还贷款,还变卖了一套。“爸爸”,有一天一个女人带着孩子费尽周折找到他。他头疼的很,而且感觉自己老了。接下来何去何从,他又一次产生了迷茫。哦,那是梦的感觉。小裴,职业经理人一步一步往上爬,最终成为职业经理人,是很多人的梦想。小裴很早就有这样的管理意识,他认识到自己的专业能力很容易遇到瓶颈,也认识到仅靠一己之力,并不能完成高大上的目标,所以他做的准备很早。他的努力的目标可能和大多数程序员的不同,他培养的是影响力。这些还不够,他还缺少经理人生涯中最重要的一环:有点说得过去的成绩。可他现在还没有,这也是他一直在这家中型公司一直待到现在的原因。努力不如借势,等公司名头响了,他就会更加值钱。“没有成绩,我比你们还要焦虑”。他有时掏心窝子和几个亲信说,但看着他们躲闪的眼神,他不认为有人能懂他。小宋,自己的事业公司管理,要向传销看齐。小宋悟道之后,就经常和人分享自己的高见。皇天不负有心人,经过多年的摸打滚爬,小宋终于找到了自己的事业。有了自己的商业模式之后,他就很少写代码了,但经常画图进行公司的架构设计。他现在非常痴迷一种画饼的技术,他的家里供着关公。这些二流子程序员完全不知道架构的最高境界就是架构组织和商业模式。小宋曲高和寡,经常有种高处不胜寒的感觉。”有些事情,你知道就好,千万不要讲出来“。他的好朋友劝他。朋友的身价已经很高了,这样的劝告很有价值,所以他最近收敛了很多。小丁,精彩的生活早晨6点钟,小丁一天的生活开始了。他喜欢跑步、登山、吉他、美食,就是不喜欢读书。小丁的每一天都安排的满满的,他不是科班出身,大学是学兽医的,鬼使神差的做了写代码的工作,主要还是因为工资高点。没办法,人聪明,编程需要的能力起点又低,小丁感觉这样的工作是为他量身定做的。“工作是配角,生活才是主角”。每次看到同事加班,小丁就忍不住劝阻。哦对了,老板马上就结婚了,他马上就可以正式的喊一声姐夫了。小汪,孤独的行者小汪的收入并不低,但他觉得很不幸福。高不成低不就的想法,养成了了目前单身还有点自卑的他。他本来就不喜欢捣鼓代码这份工作,最近项目老出乱子,经常被批,心理也越来越封闭。他下班时间不喜欢有人打扰,女孩子都不行。他养了三只猫,非常温顺。养猫不同于养狗,不用出去溜,他喜欢和它们呆在一起点个外卖醉生梦死的感觉。“这个问题我不知道”,小汪放下工作手机。猫咪脊背上毛发传来的柔软触感,让他舒服的眯起了眼睛。小祝,三线城市的希望小祝对爱情看的很重,所以他现在呆在这个三线城市中。鸟不拉屎说不上,起码还是有些禽类的。修电脑的,就是亲戚对他的认知。虽然在小地方,他可是有理想的,但一身本事只用了3成,就触碰到天花板了。过去几年,干倒了好几家公司,大多数是老板跑了。他现在正在琢磨动用关系,找点外包弄点外快什么的。“互联网红利该下沉一下到三四线城市了,机会是给有准备的人”,盯着大街墙壁上油漆未干的红标语,小祝依然充满希望。小郑,二线城市的焦虑“别飘了。你爸拖关系给你找了个工作“。距离母亲5年前给他打电话,小郑在现在的单位已经呆了4年了。活少不累,每天就是喝茶谈心。代码什么的他已经忘光了,但偶尔,他还会怀念起在帝都通宵调试代码的岁月,那激情澎湃的时光。“在哪混啊,都是为他人做嫁衣裳”。有时候,他凝视着夜空,不经意间还会深深的叹口气。主要还是混的不太好。小吴,国外技术专家幸亏当时走出国门。小吴每次想到这里,都会对自己翘起大拇指。他是典型的技术男,也不想走什么管理路线。随着自己的技术越来越精湛,研究的内容越来越深入,资历也越来越深。相比较起来,国内的同僚就落魄的多了。“现在,我可以干我喜欢的事情。或许等我老了,会回到我的家乡生活的“,小吴说,他在几年前就拿了绿卡。小雷,东南亚彩博小雷应该是走出国门里面最惨的那一批。当年,为了赚点快钱,他兴高采烈的接受了东南亚某公司的offer。结果,一封闭就是3年。在那里,他承受了超过国内三倍的工作强度,并且没有人身自由,心灵也受到了摧残。经过一系列努力,他终于在去年逃了出来,一分钱都没拿到。“现在,我真的能够坦然面对各种不平。世界上有很多灰暗地带,我只关心怎么去躲避“。 他直视着远方,他的眼睛非常有神。小关,赚钱的逻辑小关的父母都在新加坡,他每年都要飞去看望几次。在国内,他也算得上是留过学的高等人才了,当然走的是不同的路线。刚读了某名校的EMBA,然后找枪手出了几本专业的书籍。这么优秀,他还是非常的忙碌,每天只睡6小时。随着在几个大企业镀金完毕,他也顺利的注册了自己的公司。“越脏越臭的地方,机会越大“。父母想要他出国发展,他一句话就能说服。小孟,就喜欢外包比起其他程序员,小孟一直做外包,他喜欢这份工作。对于职场,他有自己的生存法则。他专找能够出差,而且补助高的工作,顺便游山玩水。他还兼职开了个网店,所以不能上网的封闭式环境他是不去的。车补、饭补、住宿,这些会搞的都是有油水的。他最看重的是每年能够连续休息一个月以上,想怎么happy就怎么happy。“赚那么多钱干什么啊,反正房子又买不起”,小孟甚至连房子都不租,他一直在出差。哦,他还喜欢去相亲,目的不是结婚的那种。小邬,心中的天平小邬算是这座城市的成功人士了。通过帮忙打理亲戚的一家公司,他的人生已经开挂,薪资是远高于其他同龄人的。自己的亲戚天天飞来飞去,一年也来不了几次。最近他学精了,招了个职业经理人前来帮忙,自己偷偷躲在办公室抱着电脑,不知道在捣鼓些啥。“我哪点不如他啊,不就是靠运气么”。某天醉酒后,他嘴里小声呢喃着。小顾,我爱996小顾的生活压力是比较大的,几乎是月光。他和妻子都是独子,双方父母多病,都没有短期死亡的迹象,不是贡献者没有退休金。更要命的是没和老婆控制好,生了3个娃。他在这家996的公司已经很长时间了,以至于他非常希望996合法化。他现在已经很少参与开发了,天天是一堆琐事,对外面的世界充满了恐惧。“我虽然已经财务自由了,但我还是不服输,我天天下班这么晚,就是想要为理想拼搏“。小顾每次开会,都要给下属打鸡血。服输俩字,出卖了他。小谢,羊毛党薅羊毛不犯法。在进局子之前,小谢也这么想,但没想到这次的动静这么大。有奖励的地方,就有利可图。小谢入这一行时间较早,从养号到变现,整个流程都颇为熟悉。这些年来,他都比较小心,虽然也有些被反薅的经历。但每个月,还是会为他带来不菲的收益。这次主要是太贪心,金额有点大,竟然也用了自己的支付宝账号,被别人顺藤摸瓜找到了。"我没觉得做错了什么,都是为了生活,我有什么错?“。小谢被拘留后很不服气。他正想着怎么做更隐秘的黑产。小孟,转产品技术转产品是非常容易的,还有不少优势。正因为这个理由,同时基于对代码的厌恶,小孟在三年前就转了产品。可他有硬伤。他不太喜欢和人交流,只会默默的在背后画原型图;被怼的时候,他会特别生气,以至于一整天的心情都不好;他觉得每天都被那么多繁琐的破事给牵绊,开会、交流、项目管理,真的很烦。“我的性格就不适合工作”,他把玩着手中的签字笔,“可我没这种命”。最近他在看javascript,他觉得前端应该会更简单一些。小高,一直走下坡路小高的职业路最近一直不顺,但十年前也曾达到过巅峰。想当年,他是所有同学里最风光无限的。回忆起他的第一次离职,他称为激流勇退。结果离职后,除了公司光环,啥都没有。他接着带着暴富的梦想去创业,接连黄了好几家。重回大公司的他,不得已只能做些普通的研发工作。最近更加凄惨,领导闲他的年龄大,性价比不高,正在慢慢孤立他。他研究孙子兵法,研究毛泽东理论,觉得是因为自己创业的几年把履历给弄坏了。“人生是场持久战,鹿死谁手未可知”。他咬了口手中的煎饼果子,咽下时能看到喉结的耸动。小崔,会跳的猿终于在这家公司干慢两年了。小崔舒了口气,他已经成功的挑战了自己。过去这十年,大多数时候,每隔半年就会跳糟一次,几乎没超过1年的,公司多的自己都不能够一口气说清。刚开始的时候,他傻不拉几全部都写上,结果简历厚厚的长长的吓人。后来,他才学会了合并这些经历,最近的这些工作找的就比较顺利。他从中得到不少好处,最明显的是薪资,翻了几翻。人又聪明,知识技能并没有拉下。“我就奇了怪了,他们看重的到底是能力还是经历”。小崔有时候会很纳闷。小关,专职割韭菜小关的技术是非常棒的,但他的薪资一般。因缘巧合,认识一个做区块链的老板,一拍即合。除了结婚,他从来没穿过西服。老板亲自带人来接,去照相馆照了张非常帅的照片,放在了公司官网上。他的眼睛炯炯有神,自信而有魅力,他是公司的CTO,是精英。但韭菜涨的有点慢,他就开直播,以技术的角度来解析区块链的好处。老板则以投资的角度讲解ICO的魅力。他好想吃一张喷香喷香的韭菜饼啊。“不赌一把,你都不知道自己的底线在哪里”。小关若有所思。小贝,上车容易下车难贝总,是大家对小贝的称呼。他们不知道,这称呼后所带的无奈。小贝是四年前到的这家公司。鉴于看好公司的发展模式,他技术入股,老板承诺了股份,但并没有签署任何协议。2年前,公司因财务紧张,在领导的忽悠下,他先后投入了200w资金,这部分老板倒是给他折合成书面股份了,但占比很小。每次和老板谈他的技术入股股份,老板都闪烁其词,他也不好追问。但看到网上越来越多的毁约,他的心头也有不少焦虑。他的履历并不算好,上市前老板透露有美化一下管理层的需求,他就更加担心。“我要离职,他起码得把200w资金的股份给我吧。况且,我们的交情...”小贝现在开始研究法律书籍了。小钟,猝死小钟兢兢业业的工作,他是最努力的那一个。公司有绩效机制,他想多赚点钱。楼下是地铁,公司楼下也是,直通的那种。他已经好几个月没见过太阳了。有一天晚上公司出了生产事故,他的手机叮叮叮响个不停,但他再也没有起床。死在了家里,据说是猝死。“公司压榨员工,必须给出合理的赔偿”!,小钟的爱人双眼欲泪,旁边小推车里躺着他们一岁的儿子,好奇的看着这个世界。前几天,他还刚给儿子买了《少儿学编程》系列,这是他最后的期望。END人生的目的从来不是享受,也不是承受,而是体验。你的每一次选择,都有自己的理由。你的每一个理由,都会让你热泪盈眶。 分享本人13年Java开发经验及产品研发经验,BAT背景,曾在多家知名企业担任技术总监、企业方案选型首席顾问,先后从事内核开发、大型Java系统架构设计和物联网系统架构设计开发,精通复杂业务技术方案选型、架构、核心难点攻关,对于java语言及项目有非常深入的理解和丰富的实操经验,热爱前沿技术,乐于进行技术分享与技术探讨。本人13年Java开发经验及产品研发经验,BAT背景,曾在多家知名企业担任技术总监、企业方案选型首席顾问,先后从事内核开发、大型Java系统架构设计和物联网系统架构设计开发,精通复杂业务技术方案选型、架构、核心难点攻关,对于java语言及项目有非常深入的理解和丰富的实操经验,热爱前沿技术,乐于进行技术分享与技术探讨。

May 15, 2019 · 1 min · jiezi

NodeJS实现简易区块链

之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。 但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。 想了想,就动手做试试咯。 ????查看全部教程 / 阅读原文???? 技术调研在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。 结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。 区块定义为了方便表示区块,将其封装为一个class,它没有任何方法: /** * 区块信息的结构化定义 */class Block { /** * 构造函数 * @param {Number} index * @param {String} previousHash * @param {Number} timestamp * @param {*} data * @param {String} hash */ constructor(index, previousHash, timestamp, data, hash) { this.index = index // 区块的位置 this.previousHash = previousHash + '' // 前一个区块的hash this.timestamp = timestamp // 生成区块时候的时间戳 this.data = data // 区块本身携带的数据 this.hash = hash + '' // 区块根据自身信息和规则生成的hash }}至于怎么生成hash,这里采用的规则比较简单: ...

May 3, 2019 · 2 min · jiezi

你和阿里员工的技术水平到底差几个等级

根据近年数据,中国现有程序员500万左右,其中P1、P2数量占据了近100万,P8以下程序员约有497万,P8及以上仅有3万。 80后是企业的技术支柱,90后已开始逐步成为企业的中坚力量。BAT的大佬横行,业内的散客也不容小觑。90后有人在P4彻夜敲代码,也有人正迈入P8。 目前并没有对程序员等级进行明确的划分,很多时候是参照BAT的程序员等级进行判定。今天看到一篇对程序员等级划分的文章,分享给大家,看看自己在哪个等级,有哪些不足。 P1、P2 入门1、了解计算机专业的基础知识,懂计算机的基本操作,掌握一门基础的程序语言即可 2、BAT一般空缺,为非常低端岗位预留 3、年薪8w以下,国内约有102w人 P3 助理1、熟练掌握一种语言,掌握一种开发环境 2、了解编译器的原理和实现机制,了解操作系统中的内部机制 3、能独立完成复杂任务,能够发现并解决问题 4、在项目当中可以作为独立的项目组成员 5、年薪9-12w,国内约有116w P4 初级专员1、深入了解一门操作系统,掌握某项领域知识的各种思想原理 2、各种经验、技能、技巧掌握下来,学习一些知名的开源项目 3、对于复杂问题的解决有自己的见解,对于问题的识别、优先级分配有见解,善于寻求资源解决问题 4、可独立领导跨部门的项目;在专业方面能够培训和教导新进员工。 5、年薪13-15w,国内约有55w人 P5 高级工程师1、做基础研究,研究非数值"计算" 2、在专业领域,对自己所从事的职业具备一定的前瞻性的了解 3、对于复杂问题的解决有自己的见解,对于问题的识别、优先级分配见解尤其有影响力,善于寻求资源解决问题 4、可独立领导跨部门的项目;能够培训和教导新进员工; 5、是专业领域的资深人士;行业外或公司内培养周期较长。 6、年薪18-23w,国内约有76w人 P6 资深工程师1、在某一专业领域中,对于业界的相关资源及水平比较了解; 2、参与部门相关策略的制定;对部门管理层的在某个领域的判断力产生影响; 3、对事物和复杂问题的分析更有影响力。 4、进行创新。对任何一种简单的东西,需要考虑各种各样的需求,以需求来驱动研究;对各种最基础性的查找结构和算法都了然于胸。 5、年薪25-35w,国内约有45w人 P7 技术专家1、是某一领域中的资深专家;对某一专业领域的规划和未来走向产生影响 2、有较大的贡献。(首先解决问题必须是比较重要的,其次你要比前辈们在某方面有一个较大的提高,或者你解决的是一个全新的以前没有解决过的问题;最重要的是,主要的思路和方法必须是你自己提供的,不再是在别人的思路基础上进行的优化和改进。) 3、年薪50-70w,国内约有52w人 P8 高级专家1、在公司内部被认为是某一方面的专家或者在国内的业界范围具备知名度和影响力; 2、对公司某一方面的战略规划和未来走向产生影响; 3、在本领域的思想和研究在公司具备较大的影响力; 4、年薪80-100w,国内约有2w人 P9 资深专家1、业内知名,对国内/国际相关领域都较为了解; 2、对公司的发展做出重要贡献或业内有相当的成功记录; 3、所进行的研究或工作对公司有相当程度的影响; 4、年薪120-150w,国内约有0.3W人 P10及以上 研究员1、业内顶尖人才, 对于国际上相关领域的思想/实践都有独到的见解并颇受尊重,比较有名望; 2、对公司的发展做出重要贡献或业内有相当的成功记录; 3、能领导公司相关方面的研究、开创业界一些实践; 4、所倡导或所开创一些做法对公司的未来有深远的影响; 5、年薪160w+,国内约有0.1W人 看完之后,你觉得自己在哪个阶段?还有哪些欠缺? 这是根据以上要求整理的学习视频、面试资料和电子书,希望能帮到你们。 Java架构技术进阶路线图 架构技术进阶资料 最新面试真题 十本技术书籍 后台私信回复“架构” 就可以马上免费获得这套价值一万八的内部教材!

April 24, 2019 · 1 min · jiezi

造个轮子,我学到了什么

阅读原文:造个轮子,我学到了什么听说的最多的是不是“不要重复的造轮子”?不要被这句话蒙骗了,这句话应该还没说完整,在什么情况下不要造轮子?实际项目中由于工期和质量原因,肯定不希望你造轮子,你造轮子花费时间且质量不如现有的轮子。但是!不造轮子怎么去装X!不造轮子怎么去了解其中原理!不造轮子怎么成长!那在造参数校验器轮子的过程中我学到了什么呢?注解的定义与使用反射的应用Spring AOP的使用异常的抛出与处理造之前的规划雄心勃勃的规划,开干!我一定比hibernate validator做的好!我要支持:属性验证方法参数验证方法验证方法内主动验证注解你初见注解时,是不是有种疑惑?为什么在某个类或方法属性上添加一个注解,它就能拥有某种功能呢?那么我将为你慢慢解开这个迷惑。注解就相当于一个标签,它本身并没有任何功能性,只是打个标签说明一下这是什么。那它怎么实现的某些功能呢?这就要说说反射了,只有注解和反射双剑合璧,才能发挥它的功效。我们先说注解,后说反射。如何定义一个注解格式自定义注解的格式为:public @interface 注解名{注解体}@interface用来声明一个注解,并自动继承java.lang.annotation.Annotation接口。注解体中的类似方法定义的,我们称为注解中的元素。@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface NotNull{ String value() default “”;}注解元素格式:权限修饰符 数据类型 元素名() default 默认值权限修饰符:只能public和default(默认)返回值类型:8种基本数据类型和String,Class,enum,Annotaion及他们的数组默认值限制:编译器对元素的默认值有些挑剔,元素的值不能为null并且不能有不确定的默认值(即要么有默认值,要么使用时提供值),所以一般我们在定义注解时便加上默认值。元注解定义注解一定要使用到Java给我们提供的四种元注解,用于注解其他注解的。 @Target @Retention @Documented @Inherited我们重点关注@Target和@Retention.@Target说明定义的注解所作用的范围(可以用于修饰什么)。取值(ElementType)有:值意义CONSTRUCTOR构造器声明FIELD属性声明(包括enum实例)LOCAL_VARIABLE局部变量声明METHOD方法声明PACKAGE包声明PARAMETER参数声明TYPE用于描述类、接口(包括注解类型) 或enum声明@Retention表示需要在什么级别保存该注释信息,用于描述注解的生命周期(被描述的注解在什么范围内有效)取值(RetentionPoicy)有:值意义SOURCE在源文件中有效(即源文件保留)CLASS在class文件中有效(即class保留)RUNTIME在运行时有效(即运行时保留)自定义的注解参数校验定义的常用注解:注解意义NotNull参数不能为空On数值的范围OnMax最大值不能超过OnMin最小值不能低于Email邮箱格式反射反射中牵涉的类有Class,Method,Parameter,Annotation,Field类获取方式ClassClass.forName(""); clazz.getClass(); Type.class;Methodclazz.getMethods();Parametermethod.getParameters(); constructor.getParameters();Annotationclazz.getAnnotations();method.getAnnotations();field.getAnnotations()Fieldclazz.getFields();用好反射的关键在于了解反射的API,之后我会单独一篇讲下我们常用的反射API。Spring AOP的使用我将借助Spring AOP来实现找到这些注解的功能。我这里只讲讲浅显一点的,因为很多人对于Spring AOP的使用还不了解。这个轮子是基于Spring Boot构建,所以我只讲声明式编程,就是注解实现的。使用比较简单,只需三步走:定义切面类指定切入点定义通知类型@Component //声明这是一个组件@Aspect //声明这是一个切面public class ServiceAspect { //定义切入点,没有方法体 @Pointcut("@annotation(定义的注解)") public void pointcut(){ } /* * 前置通知,使用pointcut()上注册的切入点 * * @param joinPoint 接受JoinPoint切入点对象,可以没有该参数 */ @Before(“pointcut()”) public void before(JoinPoint joinPoint){ } //后置通知 @After(“pointcut()”) public void after(JoinPoint joinPoint){ } //环绕通知 @Around(“pointcut()”) public void around(JoinPoint joinPoint){ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //反射就此打开序幕 } //后置返回通知 @AfterReturning(“pointcut()”) public void afterReturn(JoinPoint joinPoint){ } //抛出异常后通知 @AfterThrowing(pointcut=“pointcut()”, throwing=“ex”) public void afterThrow(JoinPoint joinPoint, Exception ex){ } }AOP底层原理是使用动态代理,动态代理有JDK动态代理和cglib动态代理,这里暂不细说了。异常自定义异常类public class FastValidatorException extends RuntimeException { public FastValidatorException(String message) { super(message); }}设计时有两种失败模式:快速失败和安全失败当参数校验,不符合要求时,快速失败将直接抛出此异常,安全失败将收集所有失败返回。如: private void emptyResult(String fieldName) { if (isFailFast) { throw new FastValidatorException(fieldName + “不能为空”); } else { formatResult(fieldName + “不能为空”); } } private void formatResult(String msg) { if (!msg.isEmpty()) { result.getErrors().add(msg); } } 处理异常如果使用快速失败模式,那么使用者将要对异常做全局统一处理。将参数异常类信息,封装成比较友好的信息给前端。@RestControllerAdvicepublic class ErrorHandler { @ExceptionHandler(FastValidatorException.class) public JSONResult handle(FastValidatorException ex) { return new JSONResult(ex.getMessage(), ex.getStatus()); }}造之后的感想还是hibernate validator做的好!(捂脸哭) 我服!but造轮子能迫使我去了解更多的知识点,能迫使我去了解轮子的原理,也能加深我对知识的理解,顺便还能吹吹!做的过程中你会思考如何优化它,一遍遍的推倒重来,会想到用怎么来解耦?用什么提高扩展性,灵活性?造轮子的意义在于能让你不断的思考和学习。无论造的好坏,行动就好。轮子地址:https://github.com/flyhero/fa… 忘不吝指教!发现这个面试视频不错,分享给大家,公众号回复: 面试视频更多精彩技术文章尽在微信公众号:码上实战 ...

April 17, 2019 · 1 min · jiezi

百度社招面试题——如何用Redis实现分布式锁

关于Redis实现分布式锁的问题,网络上很多,但是很多人的讨论基本就是把原来博主的贴过来,甚至很多面试官也是一知半解经不起推敲就来面候选人,最近结合我自己的学习和资料查阅,整理一下用Redis实现分布式锁的方法,欢迎评论、交流、讨论。1.单机Redis实现分布式锁1.1获取锁获取锁的过程很简单,客户端向Redis发送命令:SET resource_name my_random_value NX PX 30000my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。PX 30000表示这个锁有一个30秒的自动过期时间。1.2 释放锁if redis.call(“get”,KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1])else return 0end之前获取锁的时候生成的my_random_value 作为参数传到Lua脚本里面,作为:ARGV[1],而 resource_name作为KEYS[1]。Lua脚本可以保证操作的原子性。1.3 关于单点Redis实现分布式锁的讨论网络上有文章说用如下命令获取锁:SETNX resource_name my_random_valueEXPIRE resource_name 30由于这两个命令不是原子的。如果客户端在执行完SETNX后crash了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁,其他的客户端就永远获取不到这个锁了。为什么my_random_value 要设置成随机值?保证了一个客户端释放的锁是自己持有的那个锁。如若不然,可能出现锁不安全的情况。客户端1获取锁成功。客户端1在某个操作上阻塞了很长时间。过期时间到了,锁自动释放了。客户端2获取到了对应同一个资源的锁。客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。用 SETNX获取锁网上大量文章说用如下命令获取锁:SETNX lock.foo <current Unix time + lock timeout + 1>原文在Redis对SETNX的官网说明,Redis官网文档建议用Set命令来代替,主要原因是SETNX不支持超时时间的设置。https://redis.io/commands/setnx2.Redis集群实现分布式锁上面的讨论中我们有一个非常重要的假设:Redis是单点的。如果Redis是集群模式,我们考虑如下场景:客户端1从Master获取了锁。Master宕机了,存储锁的key还没有来得及同步到Slave上。Slave升级为Master。客户端2从新的Master获取到了对应同一个资源的锁。客户端1和客户端2同时持有了同一个资源的锁,锁不再具有安全性。就此问题,Redis作者antirez写了RedLock算法来解决这种问题。2.1 RedLock获取锁获取当前时间。按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的单机Redis Lua脚本释放锁的方法)。2.2 RedLock释放锁客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。2.3 关于RedLock的问题讨论如果有节点发生崩溃重启假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。节点C重启后,客户端2锁住了C, D, E,获取锁成功。客户端1和客户端2同时获得了锁。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。如果客户端长期阻塞导致锁过期解释一下这个时序图,客户端1在获得锁之后发生了很长时间的GC pause,在此期间,它获得的锁过期了,而客户端2获得了锁。当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源(上图中是一个存储服务)发起了写数据请求,而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。如何解决这个问题呢?引入了fencing token的概念:客户端1先获取到的锁,因此有一个较小的fencing token,等于33,而客户端2后获取到的锁,有一个较大的fencing token,等于34。客户端1从GC pause中恢复过来之后,依然是向存储服务发送访问请求,但是带了fencing token = 33。存储服务发现它之前已经处理过34的请求,所以会拒绝掉这次33的请求。这样就避免了冲突。但是其实这已经超出了Redis实现分布式锁的范围,单纯用Redis没有命令来实现生成Token。时钟跳跃问题假设有5个Redis节点A, B, C, D, E。客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。客户端1和客户端2现在都认为自己持有了锁。这个问题用Redis实现分布式锁暂时无解。而生产环境这种情况是存在的。结论Redis并不能实现严格意义上的分布式锁。但是这并不意味着上面讨论的方案一无是处。如果你的应用场景为了效率(efficiency),协调各个客户端避免做重复的工作,即使锁失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。但是如果你的应用场景是为了正确性(correctness),那么用Redis实现分布式锁并不合适,会存在各种各样的问题,且解决起来就很复杂,为了正确性,需要使用zab、raft共识算法,或者使用带有事务的数据库来实现严格意义上的分布式锁。参考资料Distributed locks with Redis基于Redis的分布式锁到底安全吗(上)? - 铁蕾的个人博客https://martin.kleppmann.com/…热门阅读技术文章汇总【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

April 10, 2019 · 1 min · jiezi

nginx 修改 max open files limits

注意:修改 nginx 的 max open files 有个前提,就是你已经修改好了系统的 max open files.先查看 nginx 的 ulimits: grep ‘open files’ /proc/$( cat /var/run/nginx.pid )/limits修改 nginx.servicesudo vi /lib/systemd/system/nginx.service # (仅适用于 ubuntu)添加:[Service]LimitNOFILE=100000重启服务:sudo systemctl daemon-reload修改 nginx.conf,添加:worker_rlimit_nofile 90000; # (has to be smaller or equal to LimitNOFILE set above)重启 nginx:sudo systemctl restart nginx上面是网上流传的教程,但是还是不够,你这样改了之后,nginx 的并发能力反而会下降,所以还需要改一个关键的参数:修改 nginx.conf添加:events { worker_connections 90000;}重启 nginx:sudo systemctl restart nginx

March 29, 2019 · 1 min · jiezi

我眼中的 Nginx(五):Nginx — 子请求设计之道

张超:又拍云系统开发高级工程师,负责又拍云 CDN 平台相关组件的更新及维护。Github ID: tokers,活跃于 OpenResty 社区和 Nginx 邮件列表等开源社区,专注于服务端技术的研究;曾为 ngx_lua 贡献源码,在 Nginx、ngx_lua、CDN 性能优化、日志优化方面有较为深入的研究。子请求、父请求和主请求Nginx 所处理的大部分请求,都是在接收到客户端发来的 HTTP 请求报文后创建的,这些请求直接与客户端打交道,称之为主请求;与之相对的则是子请求,顾名思义,子请求是由另外的请求创建的,比如主请求(当然子请求本身也可以创建子请求),当一个请求创建一个子请求后,它就成了该子请求的父请求。从源码层面来说,当前请求的主请求通过 r->main 指针获取,父请求则通过 r->parent 指针获取。使用子请求机制的意义在于,它能够分散原本集中在单个请求里的处理逻辑,简化任务,大大降低请求的复杂度。例如当既需要访问一个 MySQL 集群,又需要访问一个 Redis 集群时,我们就可以分别创建一个子请求负责和 MySQL 的交互,另外一个负责和 Redis 的交互,简化主请求的业务复杂度。而且创建子请求的过程不涉及任何的网络 I/O,仅仅是一些内存的分配,其代价非常可控,因此在笔者看来,子请求机制是 Nginx 里最为巧妙的设计之一。子请求创建与驱动通常需要创建子请求时,模块开发者们可以调用函数 ngx_http_subrequest 来实现,默认情况下,子请求会共享父请求的内存池,变量缓存,下游连接和 HTTP 请求头等数据。当子请求创建完毕后,它会被挂到 r->main->posted_requests 链表上,这个链表用以保存需要延迟处理的请求(不局限于子请求)。因此子请求会在父请求本地调度完毕后得到运行的机会,这通常是子请求获得首次运行机会的手段。我们知道 Nginx 针对一个 HTTP 请求,将其处理逻辑分别划分到了 11 个不同的阶段。当一个子请求被创建出来后,它首先运行的是 find config 阶段,即寻找一个合适的 location,然后开始后续的逻辑处理。通常,如果一个子请求不涉及任何的网络 I/O 操作,或者定时器处理,一次调度即可完成当前的子请求;而如果子请求需要处理一些网络、定时器事件,那么后续该子请求的调度,都会由这些事件来驱动,这使得它的调度和普通的主请求变得无差别。既然除第一次外,子请求的驱动可能是由网络事件来驱动的,那么子请求的调度就是乱序的了。假设当前主请求需要向后端请求一个大小 2MB 的资源,我们通过产生两个子请求,分别获取 0-1MB 和 1MB - 2MB 的部分,然后发往下游,因为网络的不确定性,很有可能后者(1MB - 2MB)先获取到并往下游传输。那么此时下游所得到的数据就成了脏数据了。为了解决这个问题,Nginx 为子请求机制引入了另外一个称为 postpone_filter 的模块。该模块的目的在于,判断当前准备发送数据的请求,是否是“活跃的”,如果当前请求不是“活跃”的,则它期望发送的数据会被暂时保存起来,直到某一刻它“活跃”了,才能将这些数据发往下游。怎么判断一个请求是否是“活跃”的?我们需要先了解父、子请求之间的保存形式。对于当前请求,它的子请求以链表的方式被维护起来,而前面提到,子请求也可以创建子请求,因此这些请求间完整的保存形式可以理解成一颗分层树,如下图所示。上图中,每个红圈表示一个请求,每一层的请求分别是上一层请求的子请求。从树遍历的角度讲,在这样一棵树上,哪个节点应该最先被处理?结合子请求机制的实际意义来分析,子请求是为了分摊父请求的处理逻辑,降低业务复杂度。换而言之,父请求是依赖于子请求的。很大程度上父请求可能需要等到当前子请求运行完毕后根据子请求反馈的结果来做一些收尾工作。所以需要采用的是类似后序遍历的规则。即上图最右下角的请求是第一个“活跃”的请求。从源码层面来说,这颗分层树的保存用到了两个数据结构,r->postponed 和 r->parent这两个指针,遍历 r->postponed 来按序访问当前请求的子请求(树中同层的兄弟节点);遍历 r->parent 访问到父请求(树中上一层的父节点)。postpone_filter 模块会判断当前请求是否“活跃”,如果不“活跃”,则把将要发送的数据临时拦截到它自己的 r->postponed链表上(所以这个链表上其实既有数据也有请求);如果是活跃的,则遍历它的 r->postponed 链表,要么把被临时拦截下来的数据发送出去,要么找到第一个子请求,将其标记为 “活跃”,然后返回。等到该子请求处理结束,重新将其父请求标记为“活跃”,这样一来,当父请求再一次运行到 postpone_filter 模块的时候,又可以遍历 r->postponed 链表,循环往复直到所有请求或者数据处理完毕。感兴趣的同学可以自行阅读相关源码(http://hg.nginx.org/nginx/file/tip/src/http/ngx_http_postpone_filter_module.c)。使用了子请求机制的模块目前整个 Nginx 生态圈,有很多使用子请求的例子,最著名的便是 ngx_lua 的子请求和 Nginx 官方的 slice_filter 模块了。ngx_lua 提供给用户的 API (ngx.location.capture)灵活性非常大。 包括针对是否共享变量也可自行选择。特别地,ngx_lua 的子请求运行时,会阻塞父请求(挂起其对应的 Lua 协程)。直到子请求运行完毕,子请求的响应头、响应体(所以如果响应体比较大,则会消耗很多内存)等信息都会返回给父请求。ngx_lua 的子请求是不经过 postpone_filter模块的,它在一个较早的 filter 模块(ngx_http_lua_capture_filter) 里就完成了对子请求响应体的拦截。Nginx 官方提供的 slice_filter模块,可以将一个资源下载,拆分成若干个 HTTP Range 请求,这样做最大的好处是分散热点。这个模块允许我们设置一个指令 slice_size,用以设置后续 Range 请求的区间大小。该模块会陆续创建子请求(在前一个完成后),直到所需资源下载完毕。另外, Nginx/1.13.1 也引入了一个称为 Background subrequests 的机制(用以更新缓存)。基于这个机制,Nginx/1.13.4 引入了一个 mirror 模块,通过创建子请求,可以让用户自定义一些后台任务。比如预热一些资源,直接将它们放入 Nginx 自身的 proxy_cache 缓存中。陷阱与缺陷前文说到,子请求创建出来时,复用了父请求的一些数据,这无形中引入了一些坑点。比如变量缓存,如果在子请求中访问并缓存了某个变量,当后续在父请求中使用时,我们就会得到之前的缓存数据,这可能造成工程师们花费大量的时间和精力去调试这个问题。另外笔者认为一个非常重大的缺陷是,子请求复用了父请求的内存池,以 slice_filter 模块举例,它将一个 HTTP 请求划分成若干个的子请求,每个子请求向后端发起 HTTP Range 请求,在资源非常大 ,而配置的 slice_size 相对比较小的时候,会造成有大量的子请求的创建,整个资源下载过程可能会持续很长一段时间,这导致父请求的内存池在一段时间内没有释放,加之如果并发数比较大,可能会造成进程内存使用率变得很高,严重时可能会 OOM,影响到服务。因此在考虑使用的时候,需要权衡这些问题,有必要的话可能需要自行修改源码,以满足业务上的需要。虽然一些缺点是在所难免的,但是子请求机制很大程度上简化了请求的处理逻辑,它分而治之的处理思想非常值得我们去学习和借鉴,无论如何,子请求机制也将是后续进行系统设计时的一大参考范例。《我眼中的 Nginx》系列:我眼中的 Nginx(一):Nginx 和位运算我眼中的 Nginx(二):HTTP/2 dynamic table size update我眼中的 Nginx(三):Nginx 变量和变量插值我眼中的 Nginx(四):是什么让你的 Nginx 服务退出这么慢? ...

March 27, 2019 · 1 min · jiezi

Hibernate5.2-5.3版本的ManyToOne、OneToOne的延迟加载(fetch=Lazy)失效

项目使用springboot的1.5.19版本进行开发,里面使用到JPA,而springboot这个版本自带的JPA实现是Hibernate的5.0.12版本。这个版本里面的延迟加载是没有问题的,当你设置 fetch = FetchType.LAZY 时,关联的对象在你没有使用的时候,是不会发出sql的。但升级了springboot的2.1.3后,依赖的Hibernate已经去到5.3.7版本了。这时候延迟加载就失效了,就算只是查询一个字段,也会再发多一条关联对象的SQL出来。这个问题,在升级Hibernate5.4.2版本后是解决了的。而对于使用springboot 2.1.3的话,需要先排除掉spring自身加载的Hibernate版本,自己替换成Hibernate5.4.2版本才可以。不知道有没有人遇到这问题,本人测试,Hibernate5.2.x时候就已经出现了,但好像一直没有修复。

March 27, 2019 · 1 min · jiezi

服务的熔断机制 | 从0开始构建SpringCloud微服务(14)

照例附上项目github链接。本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解服务的熔断机制。什么是服务的熔断机制对该服务的调用执行熔断,对于后续请求,不再继续调用该目标服务,而是直接返回,从而可以快速释放资源。有利于保护系统。熔断机制 : 当服务过载了,或者是流量激增,超过了服务的负荷,使用熔断机制,将服务掐断,保护自己不至于崩溃的机制。熔断机制是对系统的防护。当有非常大的请求数目来访问我们的服务的时候,若请求超出了我们的承受范围,或者是超过了我们预先设定的某个阈值,我们就将服务断掉,响应给用户一个默认的值。这个值不是用户所请求的值,到那时这个值可能包含了一些有价值的提示性的信息,如告诉用户我们的服务已经断掉了,请稍后再访问等。基于Hystrix的服务熔断机制原理我们可以把熔断器想象为一个保险丝,在电路系统中,一般在所有的家电系统连接外部供电的线路中间都会加一个保险丝,当外部电压过高,达到保险丝的熔点时候,保险丝就会被熔断,从而可以切断家电系统与外部电路的联通,进而保障家电系统不会因为电压过高而损坏。Hystrix提供的熔断器就有类似功能,当在一定时间段内服务调用方调用服务提供方的服务的次数达到设定的阈值,并且出错的次数也达到设置的出错阈值,就会进行服务降级,让服务调用方之间执行本地设置的降级策略,而不再发起远程调用。但是Hystrix提供的熔断器具有自我反馈,自我恢复的功能,Hystrix会根据调用接口的情况,让熔断器在closed,open,half-open三种状态之间自动切换。open状态说明打开熔断,也就是服务调用方执行本地降级策略,不进行远程调用。closed状态说明关闭了熔断,这时候服务调用方直接发起远程调用。half-open状态,则是一个中间状态,当熔断器处于这种状态时候,直接发起远程调用。三种状态的转换:closed->open:正常情况下熔断器为closed状态,当访问同一个接口次数超过设定阈值并且错误比例超过设置错误阈值时候,就会打开熔断机制,这时候熔断器状态从closed->open。open->half-open:当服务接口对应的熔断器状态为open状态时候,所有服务调用方调用该服务方法时候都是执行本地降级方法,那么什么时候才会恢复到远程调用那?Hystrix提供了一种测试策略,也就是设置了一个时间窗口,从熔断器状态变为open状态开始的一个时间窗口内,调用该服务接口时候都委托服务降级方法进行执行。如果时间超过了时间窗口,则把熔断状态从open->half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用还是失败,则重新设置熔断器状态为open状态,从新记录时间窗口开始时间。half-open->closed: 当熔断器状态为half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用成功,则重新设置熔断器状态为closed状态。如何集成Hystrix这里我们以之前的天气预报微服务系统为例,讲解如何在其中集成Hystrix。由于在天气预报微服务系统中,天气预报微服务将会调用多个其他微服务,所以我们让该服务集成Hystrix,当其他被调用的服务挂掉的时候,将会使熔断状态进入open模式,从而向用户返回一些默认的信息。添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>2.1.1.RELEASE</version> </dependency>@EnableCircuitBreaker注解@SpringBootApplication@EnableDiscoveryClient@EnableFeignClients@EnableCircuitBreakerpublic class Sifoudemo02Application { public static void main(String[] args) { SpringApplication.run(Sifoudemo02Application.class, args); }}书写回调方法在方法前面加上@HystrixCommand注解,启用断路器,由于这个方法我们是依赖城市数据API微服务,与天气数据API微服务的,所以当这两个微服务异常之后,就会触发这个熔断机制。fallbackMethod为我们医用熔断机制后,请求失败时默认调用的方法,该方法的返回类型要与原方法一致。 //传入城市Id得到对应城市的天气数据 @GetMapping("/cityId/{cityId}") @HystrixCommand(fallbackMethod=“defaulGetReprotByCityId”) public Weather getReportByCityId(@PathVariable(“cityId”)String cityId)throws Exception{ //获取城市Id列表 //由城市数据API微服务来提供数据 List<City>cityList=null; try { cityList=dataClient.listCity(); }catch (Exception e) { logger.error(“Exception!",e); } Weather weather=weatherReportService.getDataByCityId(cityId); return weather; } public Weather defaulGetReprotByCityId(String cityId) { Weather weather=new Weather(); return weather; }运行测试在测试时我们将天气数据API微服务,与城市数据API微服务关闭,然后发送请求,得到的为默认返回的天气数据。也可以返回一段信息,提示用户服务不可用。

March 16, 2019 · 1 min · jiezi

微服务的集中化配置 | 从0开始构建SpringCloud微服务(13)

照例附上项目github链接。本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解微服务的集中化配置。微服务为什么需要集中化配置微服务数量多,配置多手工管理配置繁琐使用Config实现Server端的配置中心集成Config Server添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>@EnableConfigServer注解//@ServletComponentScan(basePackages=“com.demo.web.servlet”)@SpringBootApplication@EnableDiscoveryClient@EnableConfigServerpublic class Sifoudemo02Application { public static void main(String[] args) { SpringApplication.run(Sifoudemo02Application.class, args); }}修改配置文件spring.application.name: msa-weather-config-serverserver.port= 8088eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/spring.cloud.config.server.git.uri=https://github.com/ShimmerPig/spring-cloud-config-server-test01spring.cloud.config.server.git.searchPaths=config-repo这里指定的是我github上面的一个仓库将服务端启动后,发送请求进行访问,可以查看在云端配置中心的配置文件,当以后需要使用繁多的配置时,我们可以通过此方法对服务进行集中化的配置。

March 16, 2019 · 1 min · jiezi

API网关 | 从0开始构建SpringCloud微服务(12)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解API网关。项目存在的问题在目前的项目中我们构建了许多的API微服务,当第三方服务想要调用我们的API微服务的时候是通过微服务的名称进行调用的,没有一个统一的入口。使用API网关的意义集合多个API统一API入口API网关就是为了统一服务的入口,可以方便地实现对平台的众多服务接口进行管控,对访问服务的身份进行验证,防止数据的篡改等。我们的一个微服务可能需要依赖多个API微服务,API网关可以在中间做一个api的聚集。那么我们的微服务只需要统一地经过API网关就可以了(只需要依赖于API网关就可以了),不用关心各种API微服务的细节,需要调用的API微服务由API网关来进行转发。使用API网关的利与弊API网关的利避免将内部信息泄露给外部为微服务添加额外的安全层支持混合通信协议降低构建微服务的复杂性微服务模拟与虚拟化API网关的弊在架构上需要额外考虑更多编排与管理路由逻辑配置要进行统一的管理可能引发单点故障API网关的实现Zuul:SpringCloud提供的API网关的组件Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求审查与监控:动态路由:动态将请求路由到不同后端集群压力测试:逐渐增加指向集群的流量,以了解性能负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求静态响应处理:边缘位置进行响应,避免转发到内部集群多区域弹性:跨域AWS Region进行请求路由,旨在实现ELB(ElasticLoad Balancing)使用多样化Spring Cloud对Zuul进行了整合和增强。目前,Zuul使用的默认是Apache的HTTP Client,也可以使用Rest Client,可以设置ribbon.restclient.enabled=true。集成Zuul在原来项目的基础上,创建一个msa-weather-eureka-client-zuul作为API网关。添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId></dependency>添加注解添加@EnableZuulProxy启用Zuul的代理功能。@SpringBootApplication@EnableDiscoveryClient@EnableZuulProxypublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}添加配置这里配置的是设置路由的url。当有人访问 /city/ 路径的时候,就对访问这个路径的请求做一个转发,转发到msa-weather-city-eureka服务中去,同理,当有人访问 /data/ 路径的时候,API网关也会将这个请求转发到msa-weather-data-eureka服务中去。zuul: routes: city: path: /city/** service-id: msa-weather-city-eureka data: path: /data/** service-id: msa-weather-data-eureka微服务依赖API网关原来天气预报微服务依赖了城市数据API微服务,以及天气数据API微服务,这里我们对其进行修改让其依赖API网关,让API网关处理天气预报微服务其他两个微服务的调用。首先删去原来调用其他两个API微服务的Feign客户端——CityClient以及WeatherDataClient,创建一个新的Feign客户端——DataClient。package com.demo.service;import java.util.List;import org.springframework.cloud.netflix.feign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import com.demo.vo.City;import com.demo.vo.WeatherResponse;@FeignClient(“msa-weather-eureka-client-zuul”)public interface DataClient { //获取城市列表 @RequestMapping(value="/city/cities",method=RequestMethod.GET) List<City> listCity()throws Exception; //通过城市Id查询对应城市的天气数据 @RequestMapping(value="/data/weather/cityId",method=RequestMethod.GET) WeatherResponse getDataByCityId(@RequestParam(“cityId”)String cityId);}在service中使用该客户端对API网关进行调用,API网关根据我们请求的路径去调用相应的微服务。@Servicepublic class WeatherReportServiceImpl implements WeatherReportService{ //@Autowired //private WeatherDataClient weatherDataClient; @Autowired private DataClient dataClient; //根据城市Id获取天气预报的数据 @Override public Weather getDataByCityId(String cityId) { //由天气数据API微服务来提供根据城市Id查询对应城市的天气的功能 WeatherResponse resp=dataClient.getDataByCityId(cityId); Weather data=resp.getData(); return data; }}在controller也是同理。 //传入城市Id得到对应城市的天气数据 @GetMapping("/cityId/{cityId}") public Weather getReportByCityId(@PathVariable(“cityId”)String cityId,Model model)throws Exception{ //获取城市Id列表 //由城市数据API微服务来提供数据 List<City>cityList=null; try { cityList=dataClient.listCity(); }catch (Exception e) { logger.error(“Exception!",e); } Weather weather=weatherReportService.getDataByCityId(cityId); return weather; }测试结果 ...

March 14, 2019 · 1 min · jiezi

实现简单的Tomcat | Tomcat原理学习(1)

缘起用了那么久tomcat,突然觉得自己对它或许并没有想象中的那么熟悉,所以趁着放假我研究了一下这只小猫咪,实现了自己的小tomcat,写出这篇文章同大家一起分享!照例附上github链接。项目结构项目结构如下:实现细节创建MyRequest对象首先创建自定义的请求类,其中定义url与method两个属性,表示请求的url以及请求的方式。其构造函数需要传入一个输入流,该输入流通过客户端的套接字对象得到。输入流中的内容为浏览器传入的http请求头,格式如下:GET /student HTTP/1.1Host: localhost:8080Connection: keep-alivePragma: no-cacheCache-Control: no-cacheUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cookie: Hm_lvt_eaa22075ffedfde4dc734cdbc709273d=1549006558; _ga=GA1.1.777543965.1549006558通过对以上内容的分割与截取,我们可以得到该请求的url以及请求的方式。package tomcat.dome;import java.io.IOException;import java.io.InputStream;//实现自己的请求类public class MyRequest { //请求的url private String url; //请求的方法类型 private String method; //构造函数 传入一个输入流 public MyRequest(InputStream inputStream) throws IOException { //用于存放http请求内容的容器 StringBuilder httpRequest=new StringBuilder(); //用于从输入流中读取数据的字节数组 byte[]httpRequestByte=new byte[1024]; int length=0; //将输入流中的内容读到字节数组中,并且对长度进行判断 if((length=inputStream.read(httpRequestByte))>0) { //证明输入流中有内容,则将字节数组添加到容器中 httpRequest.append(new String(httpRequestByte,0,length)); } //将容器中的内容打印出来 System.out.println(“httpRequest = [ “+httpRequest+” ]”); //从httpRequest中获取url,method存储到myRequest中 String httpHead=httpRequest.toString().split("\n")[0]; url=httpHead.split("\s")[1]; method=httpHead.split("\s")[0]; System.out.println(“MyRequests = [ “+this+” ]”); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } }创建MyResponse对象创建自定义的响应类,构造函数需要传入由客户端的套接字获取的输出流。定义其write方法,像浏览器写出一个响应头,并且包含我们要写出的内容content。package tomcat.dome;import java.io.IOException;import java.io.OutputStream;//实现自己的响应类public class MyResponse { //定义输出流 private OutputStream outputStream; //构造函数 传入输出流 public MyResponse(OutputStream outputStream) { this.outputStream=outputStream; } //创建写出方法 public void write(String content)throws IOException{ //用来存放要写出数据的容器 StringBuffer stringBuffer=new StringBuffer(); stringBuffer.append(“HTTP/1.1 200 OK\r\n”) .append(“Content-type:text/html\r\n”) .append("\r\n") .append("<html><head><title>Hello World</title></head><body>") .append(content) .append("</body><html>"); //转换成字节数组 并进行写出 outputStream.write(stringBuffer.toString().getBytes()); //System.out.println(“sss”); outputStream.close(); } }创建MyServlet对象由于我们自己写一个Servlet的时候需要继承HttpServlet,因此在这里首先定义了一个抽象类——MyServlet对象。在其中定义了两个需要子类实现的抽象方法doGet和doSet。并且创建了一个service方法,通过对传入的request对象的请求方式进行判断,确定调用的是doGet方法或是doPost方法。package tomcat.dome;//写一个抽象类作为servlet的父类public abstract class MyServlet { //需要子类实现的抽象方法 protected abstract void doGet(MyRequest request,MyResponse response); protected abstract void doPost(MyRequest request,MyResponse response); //父类自己的方法 //父类的service方法对传入的request以及response //的方法类型进行判断,由此调用doGet或doPost方法 public void service(MyRequest request,MyResponse response) throws NoSuchMethodException { if(request.getMethod().equalsIgnoreCase(“POST”)) { doPost(request, response); }else if(request.getMethod().equalsIgnoreCase(“GET”)) { doGet(request, response); }else { throw new NoSuchMethodException(“not support”); } }}创建业务相关的Servlet这里我创建了两个业务相关类:StudentServlet和TeacherServlet。package tomcat.dome;import java.io.IOException;//实现自己业务相关的Servletpublic class StudentServlet extends MyServlet{ @Override protected void doGet(MyRequest request, MyResponse response) { //利用response中的输出流 写出内容 try { //System.out.println("!!!!!!!!!!!!!!!!!!"); response.write(“I am a student.”); //System.out.println(“9999999999999999”); }catch(IOException e) { e.printStackTrace(); } } @Override protected void doPost(MyRequest request, MyResponse response) { //利用response中的输出流 写出内容 try { response.write(“I am a student.”); }catch(IOException e) { e.printStackTrace(); } }}package tomcat.dome;import java.io.IOException;//实现自己业务相关的Servletpublic class TeacherServlet extends MyServlet{ @Override protected void doGet(MyRequest request, MyResponse response) { //利用response中的输出流 写出内容 try { response.write(“I am a teacher.”); }catch(IOException e) { e.printStackTrace(); } } @Override protected void doPost(MyRequest request, MyResponse response) { //利用response中的输出流 写出内容 try { response.write(“I am a teacher.”); }catch(IOException e) { e.printStackTrace(); } }}创建映射关系结构ServletMapping该结构实现的是请求的url与具体的Servlet之间的关系映射。package tomcat.dome;//请求url与项目中的servlet的映射关系public class ServletMapping { //servlet的名字 private String servletName; //请求的url private String url; //servlet类 private String clazz; public String getServletName() { return servletName; } public void setServletName(String servletName) { this.servletName = servletName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getClazz() { return clazz; } public void setClazz(String clazz) { this.clazz = clazz; } public ServletMapping(String servletName, String url, String clazz) { super(); this.servletName = servletName; this.url = url; this.clazz = clazz; }}映射关系配置对象ServletMappingConfig配置类中定义了一个列表,里面存储着项目中的映射关系。package tomcat.dome;import java.util.ArrayList;import java.util.List;//创建一个存储有请求路径与servlet的对应关系的 映射关系配置类public class ServletMappingConfig { //使用一个list类型 里面存储的是映射关系类Mapping public static List<ServletMapping>servletMappings=new ArrayList<>(16); //向其中添加映射关系 static { servletMappings.add(new ServletMapping(“student”,"/student", “tomcat.dome.StudentServlet”)); servletMappings.add(new ServletMapping(“teacher”,"/teacher", “tomcat.dome.TeacherServlet”)); }}主角登场 MyTomcat!在服务端MyTomcat中主要做了如下几件事情:1)初始化请求的映射关系。2)创建服务端套接字,并绑定某个端口。3)进入循环,用户接受客户端的链接。4)通过客户端套接字创建request与response对象。5)根据request对象的请求方式调用相应的方法。6)启动MyTomcat!package tomcat.dome;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;import java.util.HashMap;import java.util.Map;//Tomcat服务器类 编写对请求做分发处理的相关逻辑public class MyTomcat { //端口号 private int port=8080; //用于存放请求路径与对应的servlet类的请求映射关系的map //相应的信息从配置类中获取 private Map<String, String>urlServletMap=new HashMap<>(16); //构造方法 public MyTomcat(int port) { this.port=port; } //tomcat服务器的启动方法 public void start() { //初始化请求映射关系 initServletMapping(); //服务端的套接字 ServerSocket serverSocket=null; try { //创建绑定到某个端口的服务端套接字 serverSocket=new ServerSocket(port); System.out.println(“MyTomcat begin start…”); //循环 用于接收客户端 while(true) { //接收到的客户端的套接字 Socket socket=serverSocket.accept(); //获取客户端的输入输出流 InputStream inputStream=socket.getInputStream(); OutputStream outputStream=socket.getOutputStream(); //通过输入输出流创建请求与响应对象 MyRequest request=new MyRequest(inputStream); MyResponse response=new MyResponse(outputStream); //根据请求对象的method分发请求 调用相应的方法 dispatch(request, response); //关闭客户端套接字 socket.close(); } } catch (IOException e) { e.printStackTrace(); } } //初始化请求映射关系,相关信息从配置类中获取 private void initServletMapping() { for(ServletMapping servletMapping:ServletMappingConfig.servletMappings) { urlServletMap.put(servletMapping.getUrl(), servletMapping.getClazz()); } } //通过当前的request以及response对象分发请求 private void dispatch(MyRequest request,MyResponse response) { //根据请求的url获取对应的servlet类的string String clazz=urlServletMap.get(request.getUrl()); //System.out.println("====="+clazz); try { //通过类的string将其转化为对象 Class servletClass=Class.forName(“tomcat.dome.StudentServlet”); //实例化一个对象 MyServlet myServlet=(MyServlet)servletClass.newInstance(); //调用父类方法,根据request的method对调用方法进行判断 //完成对myServlet中doGet与doPost方法的调用 myServlet.service(request, response); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } } //main方法 直接启动tomcat服务器 public static void main(String[] args) { new MyTomcat(8080).start(); } }测试结果 ...

March 10, 2019 · 3 min · jiezi

想在Java中实现Excel和Csv的导出吗?看这就对了

title: 想在Java中实现Excel和Csv的导出吗?看这就对了date: 2019-03-01 20:07:07tags: Javakeywords: Java导出Excel和Csvdescription:前言最近在项目中遇到一个需求,需要后端提供一个下载Csv和Excel表格的接口。这个接口接收前端的查询参数,针对这些参数对数据库做查询操作。将查询到的结果生成Excel和Csv文件,再以字节流的形式返回给前端。前端拿到这个流文件之后,最开始用ajax来接收,但是前端发送的请求却被浏览器cancel掉了。后来发现,发展了如此之久的Ajax居然不支持流文件下载。后来前端换成了最原始的XMLHttpRequest,才修复了这个问题。首先给出项目源码的地址。这是源码,欢迎大家star或者提MR。Csv新建controller先来一个简单的例子。首先在controller中新建这样一个接口。@GetMapping(“csv”)public void csv( HttpServletRequest request, HttpServletResponse response) throws IOException { String fileName = this.getFileName(request, “测试数据.csv”); response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString()); response.setHeader(“Content-Disposition”, “attachment; filename="” + fileName + “";”); LinkedHashMap<String, Object> header = new LinkedHashMap<>(); LinkedHashMap<String, Object> body = new LinkedHashMap<>(); header.put(“1”, “姓名”); header.put(“2”, “年龄”); List<LinkedHashMap<String, Object>> data = new ArrayList<>(); body.put(“1”, “小明”); body.put(“2”, “小王”); data.add(header); data.add(body); data.add(body); data.add(body); FileCopyUtils.copy(ExportUtil.exportCSV(data), response.getOutputStream());}其中this.getFileName(request, “测试数据.csv”)函数是用来获取导出文件名的函数。单独提出来是因为不同浏览器使用的默认的编码不同。例如,如果使用默认的UTF-8编码。在chrome浏览器中下载会出现中文乱码。代码如下。private String getFileName(HttpServletRequest request, String name) throws UnsupportedEncodingException { String userAgent = request.getHeader(“USER-AGENT”); return userAgent.contains(“Mozilla”) ? new String(name.getBytes(), “ISO8859-1”) : name;}response.getOutputStream()则是用于创建字节输出流,在导出csv文件的controller代码结尾,通过工具类中的复制文件函数将字节流写入到输出流中,从而将csv文件以字节流的形式返回给客户端。当前端通过http请求访问服务器接口的时候,http中的所有的请求信息都会封装在HttpServletRequest对象中。例如,你可以通过这个对象获取到请求的URL地址,请求的方式,请求的客户端IP和完整主机名,Web服务器的IP和完整主机名,请求行中的参数,获取请求头的参数等等。针对每一次的HTTP请求,服务器会自动创建一个HttpServletResponse对象和请求对象相对应。响应对象可以对当前的请求进行重定向,自定义响应体的头部,设置返回流等等。新建导出工具类我们新建一个导出工具类,来专门负责导出各种格式的文件。代码如下。public class ExportUtil { public static byte[] exportCSV(List<LinkedHashMap<String, Object>> exportData) { ByteArrayOutputStream out = new ByteArrayOutputStream(); BufferedWriter buffCvsWriter = null; try { buffCvsWriter = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); // 将body数据写入表格 for (Iterator<LinkedHashMap<String, Object>> iterator = exportData.iterator(); iterator.hasNext(); ) { fillDataToCsv(buffCvsWriter, iterator.next()); if (iterator.hasNext()) { buffCvsWriter.newLine(); } } // 刷新缓冲 buffCvsWriter.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 释放资源 if (buffCvsWriter != null) { try { buffCvsWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } return out.toByteArray(); } private static void fillDataToCsv(BufferedWriter buffCvsWriter, LinkedHashMap row) throws IOException { Map.Entry propertyEntry; for (Iterator<Map.Entry> propertyIterator = row.entrySet().iterator(); propertyIterator.hasNext(); ) { propertyEntry = propertyIterator.next(); buffCvsWriter.write(""" + propertyEntry.getValue().toString() + “"”); if (propertyIterator.hasNext()) { buffCvsWriter.write(","); } } }}fillDataToCsv主要是抽离出来为csv填充一行一行的数据的。运行然后运行项目,调用http://localhost:8080/csv,就可以下载示例的csv文件。示例如下。Excel新建controller新建下载xlsx文件的接口。@GetMapping(“xlsx”)public void xlsx( HttpServletRequest request, HttpServletResponse response) throws IOException { String fileName = this.getFileName(request, “测试数据.xlsx”); response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString()); response.setHeader(“Content-Disposition”, “attachment; filename="” + fileName + “";”); List<LinkedHashMap<String, Object>> datas = new ArrayList<>(); LinkedHashMap<String, Object> data = new LinkedHashMap<>(); data.put(“1”, “姓名”); data.put(“2”, “年龄”); datas.add(data); for (int i = 0; i < 5; i++) { data = new LinkedHashMap<>(); data.put(“1”, “小青”); data.put(“2”, “小白”); datas.add(data); } Map<String, List<LinkedHashMap<String, Object>>> tableData = new HashMap<>(); tableData.put(“日报表”, datas); tableData.put(“周报表”, datas); tableData.put(“月报表”, datas); FileCopyUtils.copy(ExportUtil.exportXlsx(tableData), response.getOutputStream());}补充工具类上面新建的导出工具类中,只有导出csv的函数,接下来我们要添加导出xlsx的函数。public static byte[] exportXlsx(Map<String, List<LinkedHashMap<String, Object>>> tableData) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { HSSFWorkbook workbook = new HSSFWorkbook(); // 创建多个sheet for (Map.Entry<String, List<LinkedHashMap<String, Object>>> entry : tableData.entrySet()) { fillDataToXlsx(workbook.createSheet(entry.getKey()), entry.getValue()); } workbook.write(out); } catch (IOException e) { e.printStackTrace(); } return out.toByteArray();}/** * 将linkedHashMap中的数据,写入xlsx表格中 * * @param sheet * @param data */private static void fillDataToXlsx(HSSFSheet sheet, List<LinkedHashMap<String, Object>> data) { HSSFRow currRow; HSSFCell cell; LinkedHashMap row; Map.Entry propertyEntry; int rowIndex = 0; int cellIndex = 0; for (Iterator<LinkedHashMap<String, Object>> iterator = data.iterator(); iterator.hasNext(); ) { row = iterator.next(); currRow = sheet.createRow(rowIndex++); for (Iterator<Map.Entry> propertyIterator = row.entrySet().iterator(); propertyIterator.hasNext(); ) { propertyEntry = propertyIterator.next(); if (propertyIterator.hasNext()) { String value = String.valueOf(propertyEntry.getValue()); cell = currRow.createCell(cellIndex++); cell.setCellValue(value); } else { String value = String.valueOf(propertyEntry.getValue()); cell = currRow.createCell(cellIndex++); cell.setCellValue(value); break; } } if (iterator.hasNext()) { cellIndex = 0; } }}fillDataToXlsx的用途与csv一样,为xlsx文件的每一行刷上数据。运行然后运行项目,调用http://localhost:8080/xlsx,就可以下载示例的csv文件。示例如下。项目地址最后再次给出项目地址,大家如果没有理解到其中的一些地方,不妨把项目clone下来,自己亲自操作一波。参考这是在解决请求被浏览器cancel掉的过程中,很重要的一个参考,分享给大家。https://www.cnblogs.com/cdemo… ...

March 10, 2019 · 2 min · jiezi

同域下单点登录分析 | 单点登录讲解(2)

本项目主要讲解的是单点登录系统的原理及其实现。相关代码github链接。本章主要讲解的是同域下单点登录分析。同域下SSO分析与设计流程图虽然看着复杂,但大家不要被吓到啦^ _ ^,请大家参照着流程路,听我下面细细道来~~详细流程分析项目结构在github的代码中,我建立了三个项目,分别是服务端SSOServer、客户端SSOClient、以及两个集成了客户端的业务系统app1与aPP2。SSO流程分析将SSOServer,app1,app2启动后,开始SSO流程分析。1)未登录访问业务系统未登录访问业务系统app1的index页面:请求被客户端的Filter拦截。由于没有token,客户端Filter控制其进行登录操作,并将原始的URL作为请求的参数。2)用户执行登录操作进入服务端的UserLoginServlet进行登录操作。进入登录页时,获取URL中的参数——原始的origRUL,将其作为request对象的属性,方便后人获取,并且跳转到服务端的登录页面。用户提交表单,服务端获取表单中信息后,到数据库中进行查询。若登录失败,则返回原来的登录页面,并携带原来的URL,将原始的URL作为表单的隐藏属性。若登录成功则:1.生成token。2.将token与其对应的user放到全局唯一数据结构中,方便所有人进行获取。3.给该用户设置一个cookie,值为token,用户在下次访问的时候就会携带此cookie,服务端也就可以通过该cookie对其身份进行验证。4.判断原始URL是否为空,若不为空则跳转到原始URL页面,否则跳转到成功登录页面。在用户要跳转到原始URL页面的时候,被客户端的Filter拦截,进行有无token的验证,由于经过登录操作在cookie中已经生成了token,故Filter发送Http通信请求服务端进行token有效性的验证,并将token作为请求参数。服务端的TokenValidateServlet获取参数中的token后,到全局唯一数据结构中查找有无该token对应的user。若没有,则证明该token可能已经失效或是伪造的,则向客户端返回空字符串,否则返回查询到的user信息。拦截器接收到服务端的返回信息,若为空字符串则返回原始登录页面,并携带原始URL,否则通过传来的用户信息,对user对象进行还原,方便下个人获取,拦截操作结束,成功进入了业务系统的index页面,并将request对象携带的user信息显示出来。3)业务系统增加自己的拦截器在通过了客户端的拦截器之后,业务系统还可以自定义拦截器,从而根据用户信息获取与本系统相关的用户业务信息,或者是对用户的权限进行进一步的验证,如:“猪猪"不可访问index页面等QAQ。4)单点退出服务端从request对象的cookie中获取token的值,将这个token从全局数据结构中移除,并且将用户保存有该token的cookie设置为无效的。关于代码部分,我将会在下一章节中进行详细解释~~~大家多多关照!^ _ ^

March 9, 2019 · 1 min · jiezi

单点登录系统SSO概述 | 单点登录讲解(1)

本项目主要讲解的是单点登录系统的原理及其实现。本章主要讲解的是单点登录系统的概述部分。单点登录单点登录顾名思义就是从一个系统进行登录操作,就可以访问其他附近的系统。单点登录避免了用户重复的登录过程,在整个核心业务中起到了一个基层的辅助作用。关键步骤当用户对业务系统发起访问请求的时候,我们将其拦截下来,进行授权验证,验证其是否有访问业务系统的权限。若用户有权限,则进行访问操作。若用户没有权限,则提供一个接口,让用户进行授权登录的操作。单点登录体系结构1.认证中心认证中心的作用:(1)验证用户是否有访问的权限。(2)若无权限,则提供页面或接口,让用户进行授权登录操作。2.用户与账号管理系统需要提供用户数据获取的接口,让业务系统在需要获取用户的数据的时候通过访问接口获得。3.客户端模块完成用户请求时的授权验证与访问拦截。4.令牌权限的验证。登录流程登录流程的三个组成部分:(1)权限的验证1.1 token是否存在的判断1.2 token是否有效的判断2)认证中心的认证界面3)token的生成

March 8, 2019 · 1 min · jiezi

天气预报微服务 | 从0开始构建SpringCloud微服务(8)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解天气预报微服务的实现。天气预报微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在Service中提供根据城市Id获取天气数据的方法。这里的天气数据后期将会由天气数据API尾服务从缓存中获取。@Servicepublic class WeatherReportServiceImpl implements WeatherReportService { @Override public Weather getDataByCityId(String cityId) { // TODO 改为由天气数据API微服务来提供 Weather data = new Weather(); data.setAqi(“81”); data.setCity(“深圳”); data.setGanmao(“容易感冒!多穿衣”); data.setWendu(“22”); List<Forecast> forecastList = new ArrayList<>(); Forecast forecast = new Forecast(); forecast.setDate(“25日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“26日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“27日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“28日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“29日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); data.setForecast(forecastList); return data; }}在Controller中提供根据城市Id获取相关天气预报数据并进行前端UI界面展示的接口。@RestController@RequestMapping("/report”)public class WeatherReportController { private final static Logger logger = LoggerFactory.getLogger(WeatherReportController.class); @Autowired private WeatherReportService weatherReportService; @GetMapping("/cityId/{cityId}”) public ModelAndView getReportByCityId(@PathVariable(“cityId”) String cityId, Model model) throws Exception { // 获取城市ID列表 // TODO 改为由城市数据API微服务来提供数据 List<City> cityList = null; try { // TODO 改为由城市数据API微服务提供数据 cityList = new ArrayList<>(); City city = new City(); city.setCityId(“101280601”); city.setCityName(“深圳”); cityList.add(city); } catch (Exception e) { logger.error(“Exception!”, e); } model.addAttribute(“title”, “猪猪的天气预报”); model.addAttribute(“cityId”, cityId); model.addAttribute(“cityList”, cityList); model.addAttribute(“report”, weatherReportService.getDataByCityId(cityId)); return new ModelAndView(“weather/report”, “reportModel”, model); }} ...

March 8, 2019 · 2 min · jiezi

天气数据API微服务 | 从0开始构建SpringCloud微服务(7)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解天气数据API微服务的实现。天气数据API微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在service中保留如下接口:(1)根据城市Id查询天气的接口getDataByCityId(2)根据城市名称查询天气的接口getDataByCityName注意:原来我们的天气数据是先从缓存中获取的,若查询不到则调用第三方接口获取天气信息,并将其保存到缓存中。在我们拆分成微服务架构之后调用第三方接口的行为由天气数据采集微服务中的定时任务进行。因此在天气数据API微服务中我们的天气数据直接从缓存中进行获取,若在缓存中获取不到对应城市的数据,则直接抛出错误。@Servicepublic class WeatherDataServiceImpl implements WeatherDataService { private final static Logger logger = LoggerFactory.getLogger(WeatherDataServiceImpl.class); private static final String WEATHER_URI = “http://wthrcdn.etouch.cn/weather_mini?"; @Autowired private StringRedisTemplate stringRedisTemplate; @Override public WeatherResponse getDataByCityId(String cityId) { String uri = WEATHER_URI + “citykey=” + cityId; return this.doGetWeahter(uri); } @Override public WeatherResponse getDataByCityName(String cityName) { String uri = WEATHER_URI + “city=” + cityName; return this.doGetWeahter(uri); } private WeatherResponse doGetWeahter(String uri) { String key = uri; String strBody = null; ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); // 先查缓存,缓存有的取缓存中的数据 if (stringRedisTemplate.hasKey(key)) { logger.info(“Redis has data”); strBody = ops.get(key); } else { logger.info(“Redis don’t has data”); // 缓存没有,抛出异常 throw new RuntimeException(“Don’t has data!”); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { //e.printStackTrace(); logger.error(“Error!",e); } return resp; }}在controller中提供根据城市Id和名称获取天气数据的接口。@RestController@RequestMapping("/weather”)public class WeatherController { @Autowired private WeatherDataService weatherDataService; @GetMapping("/cityId/{cityId}”) public WeatherResponse getWeatherByCityId(@PathVariable(“cityId”) String cityId) { return weatherDataService.getDataByCityId(cityId); } @GetMapping("/cityName/{cityName}”) public WeatherResponse getWeatherByCityName(@PathVariable(“cityName”) String cityName) { return weatherDataService.getDataByCityName(cityName); }} ...

March 8, 2019 · 2 min · jiezi

如何实现手游中的账户系统|游戏后端

作者:崔毅然在这篇文章中,我们使用 LeanCloud 作为后端来实现游戏内的账户系统。这篇文章以 Unity 游戏引擎中的 C# 语言为示例,主要讲解如何实现几种主流的登录方式,包括游客登录、游客账号升级、手机号验证码登录、用户名密码注册及登录。接入 SDK首先要接入 LeanCloud 的 SDK,接入方式可以参考文档。游客登录为了让玩家尽快体验游戏,每一个游戏都会有游客登录的功能。游客登录在 LeanCloud 中可以这样来实现:var user = await AVUser.LogInAnonymouslyAsync();调用上述代码成功后,LeanCloud 会自动生成一个游客用户登录,进入「控制台」 - 「存储」-「_User」表就可以看到表中新增了一条数据。在客户端登录的游客信息会一直被 SDK 存在本地,直到玩家删除游戏或主动退出登录。但就像所有游戏中的游客登录一样,当该游客退出登录后会丢失自己全部的游戏数据,为了保存游戏数据,需要将游客账号升级为正式账号。游客账号升级为了不丢失玩家的数据,我们会在游戏内建议玩家升级账号为正式玩家。例如绑定微信登录、绑定用户名密码及手机号,绑定成功后玩家就能以正式的登录方式获取到自己的游戏数据。升级为微信登录假设我们已经通过某些方法(例如使用 ShareSDK)拿到了微信的 openId、access_token、unionId 等,可以这样在 LeanCloud 中将游客账号关联到微信登录中:var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “openid”, “OPENID” },// openId 是用户在当前微信应用下的唯一 Id}; // unionId 是用户在整个微信内的唯一 Idvar unionId = “ox7NLs06ZGfdxbLiI0e0F1po78qE”; AVUserAuthDataLogInOption options = new AVUserAuthDataLogInOption{ UnionIdPlatform = “weixin”,// 这里指定用微信平台 AsMainAccount = true}; var user = AVUser.CurrentUser;// 绑定微信登录,第二个参数 weixinapp1是自定义的当前微信应用的标识await user.AssociateAuthDataAndUnionIdAsync(authData, “weixinapp1”, unionId, options);关联成功后,玩家以后就可以用微信登录了,登录代码见下文的第三方账户登录。绑定用户名、密码及手机号var currentUser = AVUser.CurrentUser;currentUser.Username = “username”;currentUser.Password = “password”;user.MobilePhoneNumber = “186xxxxxxxx”;await currentUser.SaveAsync();如果保存了手机号,保存成功后 LeanCloud 会自动向该手机号发送一条验证码,用户输入验证码后验证手机号:await AVUser.VerifyMobilePhoneAsync(“6位数字验证码”);手机号码验证成功后,该玩家以后就能以手机号登录了,这样就保证了游戏数据不会丢失。手机号+验证码登录、用户名及密码登录的代码见下文。手机号 + 验证码登录这种登录方式下,如果 _User 表中没有这个手机号,则视为新用户,会自动注册账号并登录;如果 _User 表中某个用户已经有了这个手机号(例如曾使用过该手机号登录,或通过游客账号升级绑定的信息),则直接登录。首先,调用发送登录验证码的接口:await AVCloud.RequestSMSCodeAsync(“18611111111”);然后使用验证码来登录var user = await AVUser.SignUpOrLogInByMobilePhoneAsync(“18611111111”, “6位短信验证码”);用户名 + 密码注册登录这种是最常见的登录方式,稍微有一点麻烦的是,需要玩家记住自己的用户名和密码。注册如果 _User 表中没有相应的用户名密码信息,例如从未注册过,也没有通过游客升级的方式增加用户名密码,需要先注册。var user = new AVUser();user.Username = “Tom”;user.Password = “cat!@#123”;await user.SignUpAsync();Debug.Log(user.Username);登录var user = await AVUser.LogInAsync(“username”, “password”);Debug.Log(user.Username);第三方登录微信或 QQ 登录可以让玩家更便捷的登录游戏。利用 LeanCloud 第三方登录的模块就可以完成这种场景。微信登录假设现在开发者已经通过某些方法(例如使用 ShareSDK)拿到了微信的 openId、access_token、unionId 等,无需注册就可以在 LeanCloud 中直接登录。如果游客已经升级绑定了微信信息,也可以通过这种方式来登录。var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “openid”, “OPENID” },// openId 是用户在当前微信应用下的唯一 Id};// unionId 是用户在整个微信内的唯一 Id var unionId = “ox7NLs06ZGfdxbLiI0e0F1po78qE”; AVUserAuthDataLogInOption options = new AVUserAuthDataLogInOption{ UnionIdPlatform = “weixin”,// 这里指定用微信平台 AsMainAccount = true}; // 绑定微信登录,第二个参数 weixinapp1 是自定义的当前微信应用的标识var user = await AVUser.LogInWithAuthDataAndUnionIdAsync(authData, “weixinapp1”, unionId, options);在 LogInWithAuthDataAndUnionIdAsync 这个方法中,第二个参数是自己定义的微信应用的名字,第三个参数 unionId 是用户在多个微信应用之间互通的唯一 id。如果我们有多个微信应用,就可以通过 unionId 登录来实现多个微信应用之间的账号互通。其他平台如果是其他平台,例如 facebook 是没有 unionId 的,这个时候只需要 access_token、expires_in、uid 三个自定义字段就可以了。var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “uid”, “FACEBOOK_UID” },};var user = await AVUser.LogInWithAuthDataAsync(authData, “facebook”);由于 LeanCloud 默认只支持微信、QQ、新浪微博登录,因此对 Facebook 需要额外去设置一下唯一索引,设置唯一索引的方式非常简单,只需要进入控制台,在 _User 表中选择「其他」-「索引」,将 authData.facebook.uid 建立唯一索引,并且勾选上「允许缺失值」选项,这样 Facebook 登录也完成了。 ...

March 7, 2019 · 2 min · jiezi

天气数据采集微服务 | 从0开始构建SpringCloud微服务(6)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解天气数据采集微服务的实现。各微服务的主要功能天气数据采集微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在Service中保留根据城市的Id同步天气数据的接口。实现方式为通过城市Id调用第三方接口获取对应天气数据,并将其同步到Redis缓存中。@Servicepublic class WeatherDataCollectionServiceImpl implements WeatherDataCollectionService{ private static final String WEATHER_URL = “http://wthrcdn.etouch.cn/weather_mini?"; //设置缓存无效的时间 private static final long TIME_OUT = 1800L; // 1800s //httpClient的客户端 @Autowired private RestTemplate restTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; //根据城市的Id同步天气数据 @Override public void syncDataByCityId(String cityId) { String url = WEATHER_URL + “citykey=” + cityId; this.saveWeatherData(url); } //把天气数据放在缓存中 private void saveWeatherData(String url) { //将rul作为天气数据的key进行保存 String key=url; String strBody=null; ValueOperations<String, String>ops=stringRedisTemplate.opsForValue(); //调用服务接口来获取数据 ResponseEntity<String>respString=restTemplate.getForEntity(url, String.class); //判断请求状态 if(respString.getStatusCodeValue()==200) { strBody=respString.getBody(); } ops.set(key, strBody,TIME_OUT,TimeUnit.SECONDS); } }定时任务保留根据城市列表同步全部天气数据的定时任务,这里的城市列表后期会通过调用城市数据API微服务得到。//同步天气数据public class WeatherDataSyncJob extends QuartzJobBean{ private final static Logger logger = LoggerFactory.getLogger(WeatherDataSyncJob.class); @Autowired private WeatherDataCollectionService weatherDataCollectionService; @Override protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException { logger.info(“Weather Data Sync Job. Start!”); //城市ID列表 //TODO 改为由城市数据API微服务来提供城市列表的数据 List<City>cityList=null; try { //TODO 改为由城市数据API微服务来提供城市列表的数据 //获取xml中的城市ID列表 //cityList=cityDataService.listCity(); cityList=new ArrayList<>(); City city=new City(); city.setCityId(“101280601”); cityList.add(city); } catch (Exception e) {// e.printStackTrace(); logger.error(“Exception!”, e); } //遍历所有城市ID获取天气 for(City city:cityList) { String cityId=city.getCityId(); logger.info(“Weather Data Sync Job, cityId:” + cityId); //实现根据cityid定时同步天气数据到缓存中 weatherDataCollectionService.syncDataByCityId(cityId); } logger.info(“Weather Data Sync Job. End!”); } } ...

March 7, 2019 · 2 min · jiezi

服务的拆分 | 从0开始构建SpringCloud微服务(5)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解城市数据API微服务的实现。各微服务的主要功能服务注册机制多个微服务之间获知对方的存在并进行通信,需要通过服务注册机制。当我们的微服务启动的时候,就会将信息注册到服务注册表或者服务注册中心中。中心可以通过心跳机制等感知服务的状态,并广播给其他的微服务。一个服务调用另一个服务,调用其他服务的服务称为服务的消费者,被调用的服务称为服务的提供者。城市数据API微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在service中保留获取本地xml文件中城市列表的方法listCity。@Servicepublic class CityDataServiceImpl implements CityDataService{ @Override public List<City> listCity() throws Exception { Resource resource=new ClassPathResource(“citylist.xml”); BufferedReader br=new BufferedReader(new InputStreamReader(resource.getInputStream(), “utf-8”)); StringBuffer buffer=new StringBuffer(); String line=””; while((line=br.readLine())!=null) { buffer.append(line); } br.close(); CityList cityList=(CityList)XmlBuilder.xmlStrToOject(CityList.class, buffer.toString()); return cityList.getCityList(); }}在controller中保留获取城市列表的接口。@RestController@RequestMapping("/cities”)public class CityController { @Autowired private CityDataService cityDataService; //返回城市列表 @GetMapping public List<City>listCity()throws Exception{ return cityDataService.listCity(); }}测试结果 ...

March 7, 2019 · 1 min · jiezi

通过Spring Boot中的手动Bean定义提高启动性能

原文:https://blog.csdn.net/qq_4288…使用Spring Boot时你不想使用@EnableAutoConfiguration。你应该怎么做?Spring本质上是快速且轻量级的,但是如何让Spring更快?其中一条建议是可以改善启动时间,那就是考虑手动导入Spring Boot配置,而不是自动全部配置。对所有应用程序来说,它不是正确的做法,但它可能会有所帮助,理解选项是什么肯定不会有害。在本文中,我们将探讨各种手动配置方法并评估其影响。完全自动配置:Hello World WebFlux作为基准,让我们看一下具有单个HTTP端点的Spring Boot应用程序:@SpringBootApplication@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}这个应用启动大约一秒钟,或者更长一些,具体取决于您的硬件。它在这段时间内做了很多工作 - 设置日志系统,读取和绑定配置文件,启动Netty并侦听端口8080,提供到@GetMapping应用程序的路由,还提供默认的错误处理。如果Spring Boot Actuator在类路径上,你还会得到一个/ health和/ info端点(由于这个原因,启动它需要更长的时间)。@SpringBootApplication注释实际包含@EnableAutoConfiguration功能,能够自动提供所有有用的功能。这就是Spring Boot流行的原因,所以我们不想丢弃这个功能,但我们可以仔细看看实际发生的事情,也许可以手动完成一些,看看我们是否学到了什么。注意:如果你想尝试这个代码,很容易从Spring Initializr获得一个空的WebFlux应用程序。只需选中“Reactive Web”复选框并下载项目即可。手动导入自动配置虽然@EnableAutoConfiguration可以轻松地为应用程序添加功能,但它也可以控制启用哪些功能。大多数人都乐意做出妥协 ,但是过度追求易用性也会失控,可能存在性能损失 , 应用程序可能会启动时慢一点,因为Spring Boot必须做一些工作才能找到所有这些功能并安装它们。事实上,找到正确的功能并没有太大的努力:首先类路径扫描,经过仔细优化后,实现有条件的评估非常快。其中应用程序的批量启动时间(80%左右)其实是由JVM加载类时间,因此实际上使其启动更快的唯一方法是通过安装更少的功能来让JVM加载更少。我们可以在注释@EnableAutoConfiguration中使用exclude属性禁用自动配置。一些单独的自动配置也有自己的布尔配置标志,可以在外部设置,例如我们可以使用的JMX spring.jmx.enabled=false(例如, 作为系统属性或在属性文件中)。我们可以走这条路并手动关闭我们不想使用的所有东西,但是这有点笨拙,并且如果类路径改变也不会阻碍其他组件功能被发现。现在我们只是使用我们想要使用的那些功能,我们可以将其称为“点菜”方式,而不是“完全自动配置autoconfiguration”中的“所有的你必须吃进去”。自动配置也其实自动寻找@Configuration标注的类,我们可以使用@Import替代@EnableAutoConfiguration,例如,以下是上面的应用程序,具有我们想要的所有功能(不包括执行器):@SpringBootConfiguration@Import({ WebFluxAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, ErrorWebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, ConfigurationPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}此版本的应用程序仍将具有我们上面描述的所有功能,但启动速度更快(可能大约30%左右)。那么我们为了更快的启动而放弃了什么呢?这是一个快速的概述:Spring Boot自动配置的完整功能包括实际应用程序中可能实际需要的其他内容,而不是特定的小样本。换句话说,30%的加速并不适用于所有应用程序,您的情况可能会有所不同。手动配置很脆弱,很难猜到。如果您编写了另一个执行稍微不同的应用程序,则需要进行不同的配置导入。您可以通过将其提取到便利类或注释中并重新使用它来缓解此问题。@Import行为方式与 @EnableAutoConfiguration配置类的排序方式不同。@Import在某些类具有依赖于早期类的条件行为的情况下,顺序很重要,如果你的配置类有前后依赖顺序关系,你必须要小心。在典型的实际应用中存在另一个排序问题。要模仿@EnableAutoConfiguration,首先需要处理用户配置,以便它们可以覆盖Spring Boot中的条件配置。如果使用@ComponentScan而不是@Imports,则无法控制扫描的顺序,或者处理这些类的处理顺序,您可以使用不同的注释来缓解这种情况(参见下文)。Spring Boot自动配置实际上从未被设计为以这种方式使用,使用这种方式可能会在您的应用程序中引入细微的错误。对此的唯一缓解方式就是是详尽的测试,让它以您期望的方式工作,并且对升级持谨慎态度。增加Actuators如果我们也可以在类路径上添加Actuator:@SpringBootConfiguration@Import({ WebFluxAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, ErrorWebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, HealthIndicatorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, ReactiveManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class, ConfigurationPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}与完整@EndpointAutoConfiguration应用程序相比,此应用程序启动速度更快 (甚至可能快50%),因为我们只包含与两个默认端点相关的配置。Spring Boot使用自动配置则默认地会激活所有端点,但不会将它们暴露给HTTP,如果我们只关心/ health和/ info,使用自动配置可能有些浪费,但自动配置也会在表中留下许多非常有用的功能。Spring Boot可能会在将来做更多工作,以禁用未曝光或未使用过的执行器。有什么不同?手动配置的应用程序有51个bean,而完全引导的自动配置应用程序有107个bean(不计算执行器)。所以它启动起来可能并不令人意外。在我们采用不同的方式实现示例应用程序之前,让我们先看看我们遗漏了什么,这样才能更快地启动它。如果在两个应用程序中都列出bean定义,您将看到所有差异来自我们遗漏的自动配置,以及Spring Boot不会有条件地排除这些自动配置。这是列表(假设您使用spring-boot-start-webflux时没有手动排除):AutoConfigurationPackagesCodecsAutoConfigurationJacksonAutoConfigurationJmxAutoConfigurationProjectInfoAutoConfigurationReactorCoreAutoConfigurationTaskExecutionAutoConfigurationTaskSchedulingAutoConfigurationValidationAutoConfigurationHttpMessageConvertersAutoConfigurationRestTemplateAutoConfigurationWebClientAutoConfiguration这就是我们不需要的12个自动配置(无论如何),并且在自动配置的应用程序中导致了56个额外的bean。它们都提供了有用的功能,所以我们可能希望有一天再次将它们包括在内,但是现在让我们假设我们更愿意在没有他们的情况下使用。spring-boot-autoconfigure有122个自动配置(还有更多spring-boot-actuator-autoconfigure)和完全引导的自动配置的示例应用程序上面只使用了其中的18个。计算使用哪些是非常早的,并且在任何类甚至加载之前,大多数都被Spring Boot丢弃,这种计算非常快(几毫秒)Spring Boot自动配置导入可以通过使用不同的注释来部分地解决与用户配置(必须最后应用)和自动配置之间的差异相关联的排序问题。Spring Boot为此提供了一个注释:@ImportAutoConfiguration来自Spring Boot Test附带spring-boot-autoconfigure的 测试切片功能。因此,您可以替换上面示例中的注释@Import, @ImportAutoConfiguration效果是推迟自动配置的处理,直到所有用户配置加载(例如,通过@ComponentScan或接收@Import)。如果我们准备将自动配置列表整理成自定义注释,我们甚至可以更进一步。不仅仅是直接使用@ImportAutoConfiguration,我们可以写一个自定义注释:@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@ImportAutoConfigurationpublic @interface EnableWebFluxAutoConfiguration {}此注释的主要特征是它带有元注释 @ImportAutoConfiguration。有了这个,我们可以在我们的应用程序中添加新的注释:@SpringBootConfiguration@EnableWebFluxAutoConfiguration@RestControllerpublic class DemoApplication { @GetMapping("/") public Mono<String> home() { return Mono.just(“Hello World”); } public void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}并列出实际的配置类/META-INF/spring.factories:com.example.config.EnableWebFluxAutoConfiguration=\org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration这样做的好处是应用程序代码不再需要手动枚举配置,而且现在由Spring Boot处理排序(属性文件中的条目在使用之前会进行排序)。缺点是它仅对需要精确这些特征的应用程序有用,并且需要在想要做一些不同的事情的任何应用程序中进行替换或扩充,当然它仍然会很快 - Spring Boot为排序做了一些额外的工作,但实际上并不是很多。在合适的硬件上,它可能仍然会在不到700毫秒的时间内启动,并带有正确的JVM标志。函数Bean定义在前面的文章中,我提到函数bean定义将是使用Spring启动应用程序的最有效方法。我们可以通过重新编写所有Spring Boot自动配置为ApplicationContextInitializers来将这个应用程序额外挤出10%左右。您可以手动执行此操作,或者您可以使用已为您准备的一些初始化程序,只要您不介意尝试某些实验性功能即可。目前有2个项目正在积极探索基于函数bean定义的新工具和新编程模型的概念:Spring Fu和 Spring Init。两者都提供至少一组函数bean定义来替换或包装Spring Boot自动配置。Spring Fu是基于API(DSL)的,不使用反射或注释;Spring Init具有函数bean定义,并且还具有用于“单点”配置的基于注释的编程模型的原型。其他地方都有更详细的介绍。这里要注意的要点是函数bean定义更快,但如果这是你主要考虑的问题,请记住它只有10%的效果。只要将所有功能放回到我们上面剥离的应用程序中,您就可以重新加载所有必需的类,并重新回到大致相同的启动时间。换句话说,@Configuration本身运行时处理的成本 并不是完全可以忽略不计,但它也不是很高(在这些小应用程序中可能只有10%左右,或者可能是100毫秒)。总结Spring Boot自动配置非常方便,但可以称之为“吃进所有你可以吃的东西”。目前(从2.1.x开始)它可能提供比某些应用程序使用或要求更多的功能。在“菜单单点”方法中,您可以使用Spring Boot作为准备和预测试配置的便捷集合,并选择您使用的部件。如果你这样做,那么@ImportAutoConfiguration是工具包的一个重要部分,但是当我们进一步研究这个主题时,你应该如何最好地使用它。Spring Boot的未来版本以及可能的其他新项目(如Spring Fu或Spring Init)将使得在运行时使用的配置选择变得更加容易,无论是自动还是通过显式选择。注意,@Configuration本身在运行时处理并不是免费的,但它也不是特别昂贵(特别是使用Spring Boot 2.1.x)。您使用的功能数量越少,加载的类越少,启动速度越快。最后,我们不希望 @EnableAutoConfiguration失去其价值或受欢迎程度。写在最后:既然看到这里了,觉得笔者写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!! ...

February 28, 2019 · 1 min · jiezi

Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是通过引入Quartz实现天气数据的同步。存在问题当用户请求我们的数据的时候才去拉最新的数据,并将其更新到Redis缓存中,效率较低。且缓存中的数据只要存在就不再次做请求,不对数据进行更新,但是天气数据大概是每半个小时就做一次更新的,所以我们传给用户的数据可能不是较新的,数据存在一定误差。解决方案通过作业调度框架Quartz实现天气数据的自动同步。前期工作要实现定时拉取接口中的数据到Redis缓存中,需要一个城市Id的列表。通过对城市Id列表的遍历,调用weatherDataService中根据城市Id同步数据到Redis中的syncDataByCityId方法,我们就能实现所有城市数据的同步了。城市列表的构建由于在程序运行的过程中动态调用服务是有延时的,所以需要减少动态调用服务,因此我们将城市列表缓存到本地。xml文件的构建使用xml文件将列表存储到本地中,需要的时候再从本地进行读取,这样会比调用第三方的服务更快。xml文件如下:<?xml version=“1.0” encoding=“UTF-8”?><c c1=“0”><d d1=“101280101” d2=“广州” d3=“guangzhou” d4=“广东”/><d d1=“101280102” d2=“番禺” d3=“panyu” d4=“广东”/><d d1=“101280103” d2=“从化” d3=“conghua” d4=“广东”/><d d1=“101280104” d2=“增城” d3=“zengcheng” d4=“广东”/><d d1=“101280105” d2=“花都” d3=“huadu” d4=“广东”/><d d1=“101280201” d2=“韶关” d3=“shaoguan” d4=“广东”/><d d1=“101280202” d2=“乳源” d3=“ruyuan” d4=“广东”/><d d1=“101280203” d2=“始兴” d3=“shixing” d4=“广东”/><d d1=“101280204” d2=“翁源” d3=“wengyuan” d4=“广东”/><d d1=“101280205” d2=“乐昌” d3=“lechang” d4=“广东”/><d d1=“101280206” d2=“仁化” d3=“renhua” d4=“广东”/><d d1=“101280207” d2=“南雄” d3=“nanxiong” d4=“广东”/><d d1=“101280208” d2=“新丰” d3=“xinfeng” d4=“广东”/><d d1=“101280209” d2=“曲江” d3=“qujiang” d4=“广东”/><d d1=“101280210” d2=“浈江” d3=“chengjiang” d4=“广东”/><d d1=“101280211” d2=“武江” d3=“wujiang” d4=“广东”/><d d1=“101280301” d2=“惠州” d3=“huizhou” d4=“广东”/><d d1=“101280302” d2=“博罗” d3=“boluo” d4=“广东”/><d d1=“101280303” d2=“惠阳” d3=“huiyang” d4=“广东”/><d d1=“101280304” d2=“惠东” d3=“huidong” d4=“广东”/><d d1=“101280305” d2=“龙门” d3=“longmen” d4=“广东”/><d d1=“101280401” d2=“梅州” d3=“meizhou” d4=“广东”/><d d1=“101280402” d2=“兴宁” d3=“xingning” d4=“广东”/><d d1=“101280403” d2=“蕉岭” d3=“jiaoling” d4=“广东”/><d d1=“101280404” d2=“大埔” d3=“dabu” d4=“广东”/><d d1=“101280406” d2=“丰顺” d3=“fengshun” d4=“广东”/><d d1=“101280407” d2=“平远” d3=“pingyuan” d4=“广东”/><d d1=“101280408” d2=“五华” d3=“wuhua” d4=“广东”/><d d1=“101280409” d2=“梅县” d3=“meixian” d4=“广东”/><d d1=“101280501” d2=“汕头” d3=“shantou” d4=“广东”/><d d1=“101280502” d2=“潮阳” d3=“chaoyang” d4=“广东”/><d d1=“101280503” d2=“澄海” d3=“chenghai” d4=“广东”/><d d1=“101280504” d2=“南澳” d3=“nanao” d4=“广东”/><d d1=“101280601” d2=“深圳” d3=“shenzhen” d4=“广东”/><d d1=“101280701” d2=“珠海” d3=“zhuhai” d4=“广东”/><d d1=“101280702” d2=“斗门” d3=“doumen” d4=“广东”/><d d1=“101280703” d2=“金湾” d3=“jinwan” d4=“广东”/><d d1=“101280800” d2=“佛山” d3=“foshan” d4=“广东”/><d d1=“101280801” d2=“顺德” d3=“shunde” d4=“广东”/><d d1=“101280802” d2=“三水” d3=“sanshui” d4=“广东”/><d d1=“101280803” d2=“南海” d3=“nanhai” d4=“广东”/><d d1=“101280804” d2=“高明” d3=“gaoming” d4=“广东”/><d d1=“101280901” d2=“肇庆” d3=“zhaoqing” d4=“广东”/><d d1=“101280902” d2=“广宁” d3=“guangning” d4=“广东”/><d d1=“101280903” d2=“四会” d3=“sihui” d4=“广东”/><d d1=“101280905” d2=“德庆” d3=“deqing” d4=“广东”/><d d1=“101280906” d2=“怀集” d3=“huaiji” d4=“广东”/><d d1=“101280907” d2=“封开” d3=“fengkai” d4=“广东”/><d d1=“101280908” d2=“高要” d3=“gaoyao” d4=“广东”/><d d1=“101281001” d2=“湛江” d3=“zhanjiang” d4=“广东”/><d d1=“101281002” d2=“吴川” d3=“wuchuan” d4=“广东”/><d d1=“101281003” d2=“雷州” d3=“leizhou” d4=“广东”/><d d1=“101281004” d2=“徐闻” d3=“xuwen” d4=“广东”/><d d1=“101281005” d2=“廉江” d3=“lianjiang” d4=“广东”/><d d1=“101281006” d2=“赤坎” d3=“chikan” d4=“广东”/><d d1=“101281007” d2=“遂溪” d3=“suixi” d4=“广东”/><d d1=“101281008” d2=“坡头” d3=“potou” d4=“广东”/><d d1=“101281009” d2=“霞山” d3=“xiashan” d4=“广东”/><d d1=“101281010” d2=“麻章” d3=“mazhang” d4=“广东”/><d d1=“101281101” d2=“江门” d3=“jiangmen” d4=“广东”/><d d1=“101281103” d2=“开平” d3=“kaiping” d4=“广东”/><d d1=“101281104” d2=“新会” d3=“xinhui” d4=“广东”/><d d1=“101281105” d2=“恩平” d3=“enping” d4=“广东”/><d d1=“101281106” d2=“台山” d3=“taishan” d4=“广东”/><d d1=“101281107” d2=“蓬江” d3=“pengjiang” d4=“广东”/><d d1=“101281108” d2=“鹤山” d3=“heshan” d4=“广东”/><d d1=“101281109” d2=“江海” d3=“jianghai” d4=“广东”/><d d1=“101281201” d2=“河源” d3=“heyuan” d4=“广东”/><d d1=“101281202” d2=“紫金” d3=“zijin” d4=“广东”/><d d1=“101281203” d2=“连平” d3=“lianping” d4=“广东”/><d d1=“101281204” d2=“和平” d3=“heping” d4=“广东”/><d d1=“101281205” d2=“龙川” d3=“longchuan” d4=“广东”/><d d1=“101281206” d2=“东源” d3=“dongyuan” d4=“广东”/><d d1=“101281301” d2=“清远” d3=“qingyuan” d4=“广东”/><d d1=“101281302” d2=“连南” d3=“liannan” d4=“广东”/><d d1=“101281303” d2=“连州” d3=“lianzhou” d4=“广东”/><d d1=“101281304” d2=“连山” d3=“lianshan” d4=“广东”/><d d1=“101281305” d2=“阳山” d3=“yangshan” d4=“广东”/><d d1=“101281306” d2=“佛冈” d3=“fogang” d4=“广东”/><d d1=“101281307” d2=“英德” d3=“yingde” d4=“广东”/><d d1=“101281308” d2=“清新” d3=“qingxin” d4=“广东”/><d d1=“101281401” d2=“云浮” d3=“yunfu” d4=“广东”/><d d1=“101281402” d2=“罗定” d3=“luoding” d4=“广东”/><d d1=“101281403” d2=“新兴” d3=“xinxing” d4=“广东”/><d d1=“101281404” d2=“郁南” d3=“yunan” d4=“广东”/><d d1=“101281406” d2=“云安” d3=“yunan” d4=“广东”/><d d1=“101281501” d2=“潮州” d3=“chaozhou” d4=“广东”/><d d1=“101281502” d2=“饶平” d3=“raoping” d4=“广东”/><d d1=“101281503” d2=“潮安” d3=“chaoan” d4=“广东”/><d d1=“101281601” d2=“东莞” d3=“dongguan” d4=“广东”/><d d1=“101281701” d2=“中山” d3=“zhongshan” d4=“广东”/><d d1=“101281801” d2=“阳江” d3=“yangjiang” d4=“广东”/><d d1=“101281802” d2=“阳春” d3=“yangchun” d4=“广东”/><d d1=“101281803” d2=“阳东” d3=“yangdong” d4=“广东”/><d d1=“101281804” d2=“阳西” d3=“yangxi” d4=“广东”/><d d1=“101281901” d2=“揭阳” d3=“jieyang” d4=“广东”/><d d1=“101281902” d2=“揭西” d3=“jiexi” d4=“广东”/><d d1=“101281903” d2=“普宁” d3=“puning” d4=“广东”/><d d1=“101281904” d2=“惠来” d3=“huilai” d4=“广东”/><d d1=“101281905” d2=“揭东” d3=“jiedong” d4=“广东”/><d d1=“101282001” d2=“茂名” d3=“maoming” d4=“广东”/><d d1=“101282002” d2=“高州” d3=“gaozhou” d4=“广东”/><d d1=“101282003” d2=“化州” d3=“huazhou” d4=“广东”/><d d1=“101282004” d2=“电白” d3=“dianbai” d4=“广东”/><d d1=“101282005” d2=“信宜” d3=“xinyi” d4=“广东”/><d d1=“101282006” d2=“茂港” d3=“maogang” d4=“广东”/><d d1=“101282101” d2=“汕尾” d3=“shanwei” d4=“广东”/><d d1=“101282102” d2=“海丰” d3=“haifeng” d4=“广东”/><d d1=“101282103” d2=“陆丰” d3=“lufeng” d4=“广东”/><d d1=“101282104” d2=“陆河” d3=“luhe” d4=“广东”/></c>创建如下两个类,并且根据xml的内容定义其属性。package com.demo.vo;import javax.xml.bind.annotation.XmlAccessType;import javax.xml.bind.annotation.XmlAccessorType;import javax.xml.bind.annotation.XmlAttribute;import javax.xml.bind.annotation.XmlRootElement;@XmlRootElement(name=“d”)@XmlAccessorType(XmlAccessType.FIELD)public class City { @XmlAttribute(name=“d1”) private String cityId; @XmlAttribute(name=“d2”) private String cityName; @XmlAttribute(name=“d3”) private String cityCode; @XmlAttribute(name=“d4”) private String province; public String getCityId() { return cityId; } public void setCityId(String cityId) { this.cityId = cityId; } public String getCityName() { return cityName; } public void setCityName(String cityName) { this.cityName = cityName; } public String getCityCode() { return cityCode; } public void setCityCode(String cityCode) { this.cityCode = cityCode; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } }package com.demo.vo;import java.util.List;import javax.xml.bind.annotation.XmlAccessType;import javax.xml.bind.annotation.XmlAccessorType;import javax.xml.bind.annotation.XmlElement;import javax.xml.bind.annotation.XmlRootElement;@XmlRootElement(name = “c”)@XmlAccessorType(XmlAccessType.FIELD)public class CityList { @XmlElement(name = “d”) private List<City> cityList; public List<City> getCityList() { return cityList; } public void setCityList(List<City> cityList) { this.cityList = cityList; }}引入工具类,实现将xml转换成java对象的过程。public class XmlBuilder { /** * 将XML转为指定的POJO * @param clazz * @param xmlStr * @return * @throws Exception */ public static Object xmlStrToOject(Class<?> clazz, String xmlStr) throws Exception { Object xmlObject = null; Reader reader = null; JAXBContext context = JAXBContext.newInstance(clazz); // XML 转为对象的接口 Unmarshaller unmarshaller = context.createUnmarshaller(); reader = new StringReader(xmlStr); xmlObject = unmarshaller.unmarshal(reader); if (null != reader) { reader.close(); } return xmlObject; }}获取城市列表的接口创建CityDataService,定义获取城市列表的方法。@Servicepublic class CityDataServiceImpl implements CityDataService{ @Override public List<City> listCity() throws Exception { Resource resource=new ClassPathResource(“citylist.xml”); BufferedReader br=new BufferedReader(new InputStreamReader(resource.getInputStream(), “utf-8”)); StringBuffer buffer=new StringBuffer(); String line=""; while((line=br.readLine())!=null) { buffer.append(line); } br.close(); CityList cityList=(CityList)XmlBuilder.xmlStrToOject(CityList.class, buffer.toString()); return cityList.getCityList(); }}根据城市Id同步天气数据的接口首先通过城市Id构建对应天气数据的url,然后通过restTemplate的getForEntity方法发起请求,获取返回的内容后使用set方法将其保存到Redis服务器中。 @Override public void syncDataByCityId(String cityId) { String url=WEATHER_URI+“citykey=” + cityId; this.saveWeatherData(url); } //将天气数据保存到缓存中,不管缓存中是否存在数据 private void saveWeatherData(String url) { //将url作为天气的key进行保存 String key=url; String strBody=null; ValueOperations<String, String>ops=stringRedisTemplate.opsForValue(); //通过客户端的get方法发起请求 ResponseEntity<String>respString=restTemplate.getForEntity(url, String.class); //判断请求状态 if(respString.getStatusCodeValue()==200) { strBody=respString.getBody(); } ops.set(key, strBody,TIME_OUT,TimeUnit.SECONDS); }Quartz的引入Quartz是一个Quartz是一个完全由java编写的开源作业调度框架,在这里的功能相当于一个定时器,定时执行指定的任务。创建同步天气数据的任务在Quartz中每个任务就是一个job,在这里我们创建一个同步天气数据的job。通过cityDataService的listCity方法获取xml文件中所有城市的列表,通过对城市列表的迭代得到所有城市的Id,然后通过weatherDataService的syncDataByCityId方法将对应Id的城市天气数据更新到Redis缓存中//同步天气数据public class WeatherDataSyncJob extends QuartzJobBean{ private final static Logger logger = LoggerFactory.getLogger(WeatherDataSyncJob.class); @Autowired private CityDataService cityDataService; @Autowired private WeatherDataService weatherDataService; @Override protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException { logger.info(“Weather Data Sync Job. Start!”); //城市ID列表 List<City>cityList=null; try { //获取xml中的城市ID列表 cityList=cityDataService.listCity(); } catch (Exception e) {// e.printStackTrace(); logger.error(“Exception!”, e); } //遍历所有城市ID获取天气 for(City city:cityList) { String cityId=city.getCityId(); logger.info(“Weather Data Sync Job, cityId:” + cityId); //实现根据cityid定时同步天气数据到缓存中 weatherDataService.syncDataByCityId(cityId); } logger.info(“Weather Data Sync Job. End!”); } }配置QuartzTIME设置的是更新的频率,表示每隔TIME秒就执行任务一次。@Configurationpublic class QuartzConfiguration { private static final int TIME = 1800; // 更新频率 // JobDetail @Bean public JobDetail weatherDataSyncJobDetail() { return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity(“weatherDataSyncJob”) .storeDurably().build(); } // Trigger @Bean public Trigger weatherDataSyncTrigger() { SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(TIME).repeatForever(); return TriggerBuilder.newTrigger().forJob(weatherDataSyncJobDetail()) .withIdentity(“weatherDataSyncTrigger”).withSchedule(schedBuilder).build(); }}测试结果天气数据同步结果 ...

February 28, 2019 · 4 min · jiezi

Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节将介绍项目中Redis的引入。Redis下载教程。若对Redis感兴趣,还可以看一下我的另一篇文章造个轮子 | 自己动手写一个Redis存在问题:数据来源于第三方的接口,依赖性太强。可能带来的不良结果:(1)延时性:用户访问我们的时候,我们需要再去访问第三方的接口,我们是数据的中间者,不是数据的产生者,有一定的延时性。(2)访问上限:免费的接口,可能会达到上限。(3)调死:可能将对方的接口给调死。解决方案:使用redis缓存系统,提高整体的并发访问能力。Redis 是一个高性能的key-value数据库,基于内存的缓存系统,对内存的操作时非常快的,所以可以做到及时响应。为什么选择Redis(1)及时响应(2)减少服务调用Redis如何引入Redis是一个key-value结构的数据存储系统,这里我们使用天气数据的uri作为它的key,通过ValueOperations<String, String>ops对象的set方法将数据写入缓存中,通过其get方法可以从缓存中获取数据,并且使用TIME_OUT设置缓存失效的时间。我们并不是每次都去调用第三方的接口,若Redis缓存中有要查找的天气数据,则从缓存中取;若缓存中没有,则请求第三方接口,然后将数据写入Redis缓存中。 private WeatherResponse doGetWeahter(String uri) { String key = uri; String strBody = null; ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); // 先查缓存,缓存有的取缓存中的数据 if (stringRedisTemplate.hasKey(key)) { logger.info(“Redis has data”); strBody = ops.get(key); } else { logger.info(“Redis don’t has data”); // 缓存没有,再调用服务接口来获取 ResponseEntity<String> respString = restTemplate.getForEntity(uri, String.class); if (respString.getStatusCodeValue() == 200) { strBody = respString.getBody(); } // 数据写入缓存 ops.set(key, strBody, TIME_OUT, TimeUnit.SECONDS); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { //e.printStackTrace(); logger.error(“Error!",e); } return resp; } ...

February 27, 2019 · 1 min · jiezi

从0开始构建SpringCloud微服务(1)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,第一节将介绍普通天气预报系统的简单实现。数据来源:数据来源1:http://wthrcdn.etouch.cn/weather_mini?city=深圳数据来源2:http://wthrcdn.etouch.cn/weather_mini?citykey=101280601数据来源3:http://mobile.weather.com.cn/js/citylist.xml数据格式根据返回的数据格式在vo包下面创建pojo。Service创建WeatherDataService在其中提供如下接口:1)根据城市Id获取城市天气数据的接口。 @Override public WeatherResponse getDataByCityId(String cityId) { String url=WEATHER_URI+ “citykey=” + cityId; return this.doGetWeather(url); }2)根据城市名称获取天气数据的接口。 @Override public WeatherResponse getDataByCityName(String cityName) { String url = WEATHER_URI + “city=” + cityName; return this.doGetWeather(url); }其中doGetWeather方法为抽离出来的请求天气数据的方法。 private WeatherResponse doGetWeahter(String uri) { ResponseEntity<String> respString = restTemplate.getForEntity(uri, String.class); ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; String strBody = null; if (respString.getStatusCodeValue() == 200) { strBody = respString.getBody(); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { e.printStackTrace(); } return resp; }Controller在controller中分别提供根据城市id与名称获取天气数据的接口。@RestController@RequestMapping("/weather")public class WeatherController { @Autowired private WeatherDataService weatherDataService; @GetMapping("/cityId/{cityId}") public WeatherResponse getWeatherByCityId(@PathVariable(“cityId”) String cityId) { return weatherDataService.getDataByCityId(cityId); } @GetMapping("/cityName/{cityName}") public WeatherResponse getWeatherByCityName(@PathVariable(“cityName”) String cityName) { return weatherDataService.getDataByCityName(cityName); }}配置创建Rest的配置类。@Configurationpublic class RestConfiguration { @Autowired private RestTemplateBuilder builder; @Bean public RestTemplate restTemplate() { return builder.build(); } }请求结果: ...

February 23, 2019 · 1 min · jiezi

Netty+SpringBoot+FastDFS+Html5实现聊天App(六)

Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。Netty+SpringBoot+FastDFS+Html5实现聊天App,项目github链接。本章完整代码链接。本章将给聊天App_PigChat加上心跳机制。为什么要实现心跳机制如果没有特意的设置某些选项或者实现应用层心跳包,TCP空闲的时候是不会发送任何数据包。也就是说,当一个TCP的socket,客户端与服务端谁也不发送数据,会一直保持着连接。这其中如果有一方异常掉线(例如死机、路由被破坏、防火墙切断连接等),另一端如果没有发送数据,永远也不可能知道。这对于一些服务型的程序来说,是灾难性的后果,将会导致服务端socket资源耗尽。举个简单的例子,当我们因为特殊情况打开飞行模式 ,在处理完事件之后再关闭飞行模式,这时候如果再进入应用程序中,我们将以新的channel进入,但是之前的channel还是会保留。因此,为了保证连接的有效性、及时有效地检测到一方的非正常断开,保证连接的资源被有效的利用,我们就会需要一种保活的机制,通常改机制两种处理方式:1、利用TCP协议层实现的Keepalive;2、自己在应用层实现心跳包。实现心跳机制新建一个HeartBeatHandler用于检测channel的心跳。继承ChannelInboundHandlerAdapter,并重写其userEventTriggered方法。当客户端的所有ChannelHandler中4s内没有write事件,则会触发userEventTriggered方法。首先我们判断evt是否是IdleStateEvent的实例,IdleStateEvent用于触发用户事件,包含读空闲/写空闲/读写空闲。对evt进行强制履行转换后,通过state判断其状态,只有当其该channel处于读写空闲的时候才将这个channel关闭。/** * @Description: 用于检测channel的心跳handler * 继承ChannelInboundHandlerAdapter,从而不需要实现channelRead0方法 */public class HeartBeatHandler extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // 判断evt是否是IdleStateEvent(用于触发用户事件,包含 读空闲/写空闲/读写空闲 ) if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent)evt; // 强制类型转换 if (event.state() == IdleState.READER_IDLE) { System.out.println(“进入读空闲…”); } else if (event.state() == IdleState.WRITER_IDLE) { System.out.println(“进入写空闲…”); } else if (event.state() == IdleState.ALL_IDLE) { System.out.println(“channel关闭前,users的数量为:” + ChatHandler.users.size()); Channel channel = ctx.channel(); // 关闭无用的channel,以防资源浪费 channel.close(); System.out.println(“channel关闭后,users的数量为:” + ChatHandler.users.size()); } } } }增加心跳支持在原来的WSServerInitialzer中增加心跳机制的支持。 // ====================== 增加心跳支持 start ====================== // 针对客户端,如果在1分钟时没有向服务端发送读写心跳(ALL),则主动断开 // 如果是读空闲或者写空闲,不处理 pipeline.addLast(new IdleStateHandler(8, 10, 12)); // 自定义的空闲状态检测 pipeline.addLast(new HeartBeatHandler()); // ====================== 增加心跳支持 end ====================== ...

February 19, 2019 · 1 min · jiezi

20分钟数据库索引设计实战

在后端开发的工作中如何轻松、高效地设计大量数据库索引呢?通过下面这四步,20分钟后你就再也不会为数据库的索引设计而发愁了。顺畅地阅读这篇文章需要了解数据库索引的组织方式,如果你还不熟悉的话,可以通过另一篇文章来快速了解一下——数据库索引融会贯通。这篇文章是一系列数据库索引文章中的第三篇,这个系列包括了下面四篇文章:数据库索引是什么?新华字典来帮你 —— 理解数据库索引融会贯通 —— 深入20分钟数据库索引设计实战 —— 实战数据库索引为什么用B+树实现? —— 扩展这一系列涵盖了数据库索引从理论到实践的一系列知识,一站式解决了从理解到融会贯通的全过程,相信每一篇文章都可以给你带来更深入的体验。1. 整理查询条件我们设计索引的目的主要是为了加快查询,所以,设计索引的第一步是整理需要用到的查询条件,也就是我们会在where子句、join连接条件中使用的字段。一般来说会整理程序中除了insert语句之外的所有SQL语句,按不同的表分别整理出每张表上的查询条件。也可以根据对业务的理解添加一些暂时还没有使用到的查询条件。对索引的设计一般会逐表进行,所以按数据表收集查询条件可以方便后面步骤的执行。2. 分析字段的可选择性整理出所有查询条件之后,我们需要分析出每个字段的可选择性,那么什么是可选择性呢?字段的可选择性指的就是字段的值的区分度,例如一张表中保存了用户的手机号、性别、姓名、年龄这几个字段,且一个手机号只能注册一个用户。在这种情况下,像手机号这种唯一的字段就是可选择性最高的一种情况;而年龄虽然有几十种可能,但是区分度就没有手机号那么大了;性别这样的字段则只有几种可能,所以可选择性最差。所以俺可选择性从高到低排列就是:手机号 > 年龄 > 性别。但是不同字段的值分布是不同的,有一些值的数量是大致均匀的,例如性别为男和女的值数量可能就差别不大,但是像年龄超过100岁这样的记录就非常少了。所以对于年龄这个字段,20-30这样的值就是可选择性很小的,因为每一个年龄都有非常多的记录;但是像100这样的值,那它的可选择性就非常高了。如果我们在表中添加了一个字段表示用户是否是管理员,那么在查询网站的管理员信息列表时,这个字段的可选择性就非常高。但是如果我们要查询的是非管理员信息列表时,这个字段的可选择性就非常低了。从经验上来说,我们会把可选择性高的字段放到前面,可选择性低的字段放在后面,如果可选择性非常低,一般不会把这样的字段放到索引里。3. 合并查询条件虽然索引可以加快查询的效率,但是索引越多就会导致插入和更新数据的成本变高,因为索引是分开存储的,所有数据的插入和更新操作都要对相关的索引进行修改。所以设计索引时还需要控制索引的数量,不能盲目地增加索引。一般我们会根据最左匹配原则来合并查询条件,尽可能让不同的查询条件使用同一个索引。例如有两个查询条件where a = 1 and b = 1和where b = 1,那么我们就可以创建一个索引idx_eg(b, a)来同时服务两个查询条件。同时,因为范围条件会终止使用索引中后续的字段,所以对于使用范围条件查询的字段我们也会尽可能放在索引的后面。4. 考虑是否需要使用全覆盖索引最后,我们会考虑是否需要使用全覆盖索引,因为全覆盖索引没有回表的开销,效率会更高。所以一般我们会在回表成本特别高的情况下考虑是否使用全覆盖索引,例如根据索引字段筛选后的结果需要返回其他字段或者使用其他字段做进一步筛选的情况。例如,我们有一张用户表,其中有年龄、姓名、手机号三个字段。我们需要查询在指定年龄的所有用户的姓名,已有索引idx_age_name(年龄, 姓名),目前我们使用下面这样的查询语句进行查询:SELECT *FROM 用户表WHERE 年龄 = ?;一般情况下,将一个索引优化为全覆盖索引有两种方式:增加索引中的字段,让索引字段覆盖SQL语句中使用的所有字段在这个例子中,我们可以创建一个同时包含所有字段的索引idx_all(年龄, 姓名, 手机号),以此提高查询的效率。减少SQL语句中使用的字段,使SQL需要的字段都包含在现有索引中在这个例子中,其实更好的方法是将SELECT子句修改为SELECT 姓名,因为我们的需求只是查询用户的姓名,并不需要手机号字段,去掉SELECT子句多余的字段不仅能够满足我们的需求,而且也不用对索引做修改。

February 14, 2019 · 1 min · jiezi

数据库索引为什么用B+树实现?

为什么大多数数据库索引都使用B+树来实现呢?这涉及到数据结构、操作系统、计算机存储层次结构等等复杂的理论知识,但是不用担心,这篇文章20分钟之后就会给你答案。这篇文章是一系列数据库索引文章中的最后一篇,这个系列包括了下面四篇文章:数据库索引是什么?新华字典来帮你 —— 理解数据库索引融会贯通 —— 深入20分钟数据库索引设计实战 —— 实战数据库索引为什么用B+树实现? —— 扩展这一系列涵盖了数据库索引从理论到实践的一系列知识,一站式解决了从理解到融会贯通的全过程,相信每一篇文章都可以给你带来更深入的体验。为什么使用B+树?大家在数学课上一定听说过一个例子,在一堆已经排好序的数字当中找出一个特定的数字的最好办法是一种叫“二分查找”的方式。具体的过程就是先找到这些数字中间的那一个数,然后比较目标数字是大于还是小于这个数;然后根据结果继续在前一半或者后一半数字中继续查找。这就类似于数据结构中的二叉树,二叉树就是如下的一种结构,树中的每个节点至多可以有两个子节点,而B+树每个节点则可以有N个子节点。这里就不具体展开讲解二叉树了,我们只需要知道,平衡的二叉树是内存中查询效率最高的一种数据结构就可以了。但是目前常用的数据库中,绝大多数的索引都是使用B+树实现的。那么为什么明明是二叉树查询效率最高,数据库中却偏偏要使用B+树而不是二叉树来实现索引呢?计算机存储层次结构计算机中的存储结构分为好几个部分,从上到下大致可以分为寄存器、高速缓存、主存储器、辅助存储器。其中主存储器,也就是我们常说的内存;辅助存储器也被称为外存,比较常见的就是磁盘、SSD,可以用来保存文件。在这个存储结构中,每一级存储的速度都比上一级慢很多,所以程序访问越上层存储中的数据,速度就会越快。有过编程经验的小伙伴都知道,程序运行过程中操作的基本都是内存,对外存中数据的访问往往需要写一些文件的读取和写入代码才能实现。这正是因为CPU的计算速度比存储的I/O速度(输入/输出速度)快很多所做的优化,因为CPU在每次计算完成之后就需要等待下一批的数据进入,这个等待的时间越短,计算机运行得越快。所以对于数据库索引来说,因为数据量很大,所以基本都是保存在外存中的,这样的话数据库读取一个索引节点的成本就非常大了。在数据量一样大的情况下,我们可以知道,B+树的单个节点中包含的值个数越多那么树中需要的节点总数就会越少,这样查询一次数据需要访问的节点数就更少了。如果你对B+树还不熟悉,可以到这篇文章中找到答案——数据库索引融会贯通 。如果我们把二叉树看做是特殊的B+树(每个节点只有一个值和前后两个指针的B+树),那么就可以得出结论:因为B+树的节点中包含的值个数(多个值)比二叉树(1个值)更多,所以在B+树中查询所需要的节点数就更少。那么如果每次读取的成本是一样的话,因为总成本=读取次数*单次读取成本,我们就可以证明B+树的查询成本就比二叉树小得多了。节点读取成本但是我们知道,读取更多数据肯定会需要更大的成本,那么为什么数据库索引使用B+树还是会比二叉树更好呢?这就需要一些更高深的操作系统知识来解释了。在现代的操作系统中,把数据从外存读到内存所使用的单位一般被称为“页”,每次读取数据都需要读入整数个的“页”,而不能读入半页或者0.8页。一页的大小由操作系统决定,常见的页大小一般为4KB=4096字节。所以不管我们是要读取1字节还是2KB,最后都是需要读入一个完整的4KB大小的页的,那么一个节点的读取成本就取决于需要读入的页数。在这样的情况下,如果一个节点的大小小于一页的大小,那么就会有一部分时间花在读取我们根本不需要的数据上(节点之外的数据),二叉树在这方面就会浪费很多时间;而如果一个节点的大小大于一页,哪怕是一页的整数倍,那我们也可能在一个节点的中间就找到了我们需要的指针进入了下一级的节点,这样这个指针后面的数据都白白读取了,如果不需要这些数据可能我们就可以少读几页了。所以,综上所述,数据库索引使用节点大小恰好等于操作系统一页大小的B+树来实现是效率最高的选择。

February 14, 2019 · 1 min · jiezi

数据库索引融会贯通

索引的各种规则纷繁复杂,不了解索引的组织形式就没办法真正地理解数据库索引。通过本文,你可以深入地理解数据库索引在数据库中究竟是如何组织的,从此以后索引的规则对于你将变得清清楚楚、明明白白,再也不需要死记硬背。顺畅地阅读这篇文章需要了解索引、联合索引、聚集索引分别都是什么,如果你还不了解,可以通过另一篇文章来轻松理解——数据库索引是什么?新华字典来帮你。这篇文章是一系列数据库索引文章中的第二篇,这个系列包括了下面四篇文章:数据库索引是什么?新华字典来帮你 —— 理解数据库索引融会贯通 —— 深入20分钟数据库索引设计实战 —— 实战数据库索引为什么用B+树实现? —— 扩展这一系列涵盖了数据库索引从理论到实践的一系列知识,一站式解决了从理解到融会贯通的全过程,相信每一篇文章都可以给你带来更深入的体验。索引的组织形式通过之前的内容,我们已经对数据库索引有了相当程度的抽象了解,那么在数据库中,索引实际是以什么样的形式进行组织的呢?同一张表上的多个索引又是怎样分工合作的呢?目前绝大多数情况下使用的数据库索引都是使用B+树实现的,下面就以MySQL的InnoDB为例,介绍一下数据库索引的具体实现。聚集索引下面是一个以B+树形式组织的拼音索引,在B+树中,每一个节点里都有N个按顺序排列的值,且每个值的中间和节点的头尾都有指向下一级节点的指针。在查找过程中,按顺序从头到尾遍历一个节点中的值,当发现要找的目标值恰好在一个指针的前一个值之前、后一个值之后时,就通过这个指针进入下一级节点。当最后到达叶子节点,也就是最下层的节点时,就能够找到自己希望查找的数据记录了。在上图中如果希望找到险字,那么我们首先通过拼音首字母在根节点上按顺序查找到了X和Y之间的指针,然后通过这个指针进入了第二级节点···, xia, xian, xiang, ···。之后在该节点上找到了xian和xiang之间的指针,这样就定位到了第519页开始的一个目标数据块,其中就包含了我们想要找到的险字。因为拼音索引是聚集索引,所以我们在叶子节点上直接就找到了我们想找的数据。非聚集索引下面是一个模拟部首索引的组织形式。我们由根节点逐级往下查询,但是在最后的叶子节点上并没有找到我们想找的数据,那么在使用这个索引时我们是如何得到最终的结果的呢?回忆之前字典中“检字表”的内容,我们可以看到,在每个字边上都有一个页码,这就相当于下面这一个索引中叶子节点上险字与院字中间的指针,这个指针会告诉我们真正的数据在什么地方。下图中,我们把非聚集索引(部首索引)和聚集索引(拼音索引)合在一起就能看出非聚集索引最后到底如何查找到实际数据了。非聚集索引叶子节点上的指针会直接指向聚集索引的叶子节点,因为根据聚集索引的定义,所有数据都是按聚集索引组织存储的,所以所有实际数据都保存在聚集索引的叶子节点中。而从非聚集索引的叶子节点链接到聚集索引的叶子节点查询实际数据的过程就叫做——回表。全覆盖索引那么如果我们只是想要验证险字的偏旁是否是双耳旁“阝”呢?这种情况下,我们只要在部首索引中阝下游的叶子节点中找到了险字就足够了。这种在索引中就获取到了SQL语句中需要的所有字段,所以不需要再回表查询的情况中,这个索引就被称为这个SQL语句的全覆盖索引。在实际的数据库中,非聚集索引的叶子节点上保存的“指针”就是聚集索引中所有字段的值,要获取一条实际数据,就需要通过这几个聚集索引字段的值重新在聚集索引上执行一遍查询操作。如果数据量不多,这个开销是非常小的;但如果非聚集索引的查询结果中包含了大量数据,那么就会导致回表的开销非常大,甚至超过不走索引的成本。所以全覆盖索引可以节约回表的开销这一点在一些回表开销很大的情况下就非常重要了。范围查询条件上图是一个联合索引idx_eg(col_a, col_b)的结构,如果我们希望查询一条满足条件col_a = 64 and col_b = 128的记录,那么我们可以一路确定地往下找到唯一的下级节点最终找到实际数据。这种情况下,索引上的col_a和col_b两个字段都能被使用。但是如果我们将查询条件改为范围查询col_a > 63 and col_b = 128,那么我们就会需要查找所有符合条件col_a > 63的下级节点指针,最后不得不遍历非常多的节点及其子节点。这样的话对于索引来说就得不偿失了,所以在这种情况下,数据库会选择直接遍历所有满足条件col_a > 63的记录,而不再使用索引上剩下的col_b字段。数据库会从第一条满足col_a > 63的记录开始,横向遍历之后的所有记录,从里面排除掉所有不满足col_b = 128的记录。这就是范围条件会终止使用联合索引上的后续字段的原因。

February 14, 2019 · 1 min · jiezi

写个 Go 时间交并集小工具

示例代码(含测试)在这里需求在甘特图的场景下,我们经常会遇到这种情况,五位员工A, B, C, D, E,可能他们的工作都是并行的,我们需要计算某段时间内他们总的工作时长。我们不能简单得把五个人的工作时间都加起来,因为当中会有重叠的部分。所以这时候我们就需要一个计算时间交并集的工具。思路将一组离散的时间段按照开始时间,从小到大排序。像这样[{2 7} {4 11} {10 19} {10 30} {16 18} {19 29} {23 35} {24 42} {25 30} {27 49}]我这里将时间用十分小的秒来代替,方便理解。循环排序后的数组,如果下一个时间段开始时间介于上个时间段的开始时间和结束时间之间,那么就进行合并,否则就分离。可以看到我们这里有两个关键动作,合并,分离,而这个就是我们要实现的核心代码。实现一段连续的工作时间都会有两个点,开始时间和结束时间。所以我们可以把这个时间结构设计成:type T struct { Start int64 End int64}而一个人的工作时间是由多个 T 组成的,所以我们在定义一个切片类型type TSlice []T为了能顺序合并时间,我们需要将TSlice进行排序。我们知道 Go 中有个 sort 包,我们只需要实现 sort 类型的接口,就能实现 TSlice 的排序了。我们实现下:func (t TSlice) Len() int { return len(t) }func (t TSlice) Swap(i, j int) { t[i], t[j] = t[j], t[i] }func (t TSlice) Less(i, j int) bool { return t[i].Start < t[j].Start }三个方法分别是,长度、交换位置、比小。这样一来,我们就能直接用 sort.Stable() 稳定排序,对我们的时间段切片排序了。好,接下来我们实现并集的方法,我们取名为 Union:func (t TSlice) Union() TSlice { // 新建一个空的时间切片 var s TSlice // 如果有至少两个是时间段,我们才排序,否则直接返回 if len(t) > 1 { // @todo 合并逻辑 } return s}Union 方法将会返回一个同样的 TSlice 时间切片,只不过是经过并集处理的。一旦 t 中的时间段个数大于1,我们就要执行处理逻辑了:if len(t) > 1 { sort.Stable(t) s = append(s, t[0]) // @todo 循环比较合并}我们先对时间切片进行排序,然后把第一个时间段作为第一个元素放进我们的结果 TSlice 中,好让我们开始进行循坏的比较。if len(t) > 1 { sort.Stable(t) s = append(s, t[0]) for k, v := range t { // 如果开始时间大于结束时间,那其实是错误数据,但是我们这里正常返回 // 你可以根据自己的需要定制错误处理逻辑 if v.Start > v.End { return s } // 第一组元素我们不做任何操作 if k == 0 { continue } // 当开始时间介于上一个时间段的开始时间和结束时间之间 if v.Start >= s[len(s)-1].Start && v.Start <= s[len(s)-1].End { // 合并 if v.End > s[len(s)-1].End { s[len(s)-1].End = v.End } // 如果大于上一个时间段的结束时间 } else if v.Start > s[len(s)-1].End { // 分离 inner := T{Start: v.Start, End: v.End} s = append(s, inner) } } }来张图其实就清楚了:可以看到最后输出的也是一个 TSlice 类型。上面就是 union,求并集的过程,那交集的?其实交集也很简单,如果两个时间段相交,我们只要判断:开始时间取最大的那个,结束时间取两个时间段中最小的那个。func (t TSlice) Intersect() TSlice { var s TSlice if len(t) > 1 { sort.Stable(t) s = append(s, t[0]) for k, v := range t { if v.Start > v.End { return s } if k == 0 { continue } // 两个时间段相交 if v.Start >= s[0].Start && v.Start <= s[0].End { // 开始时间取最大的那个 s[0].Start = v.Start // 结束时间取最小的那个 if v.End <= s[0].End { s[0].End = v.End } } else { return s[:0] } } } return s}一样,我们来个图:需要注意的是,这个求交集的结果是全相交–只有当所有时间段都有共同时间才会有结果。这样的需求在实际过程中用到的是不是不太多??所以我想是不是能够实现:一次相交,两次相交…的条件筛选。看看效果我们随机生成了一组时间切片func makeTimes(t int) TSlice { var set TSlice rand.Seed(time.Now().Unix()) for i := 0; i < t; i++ { randStart := rand.Int63n(50) randEnd := randStart + rand.Int63n(25) + 1 set = append(set, T{Start: randStart, End: randEnd}) } return set}testSet := makeTimes(10) // 生成10个时间段的时间切片res := testSet.Union() // 直接调用 Union() 或者 Intersect()输入数据为:[{10 21} {34 52} {49 54} {18 31} {26 44} {24 27} {43 51} {41 53} {20 41} {48 67}]输出结果:[{10 67}]结果还行~额外我发现在求并集的过程中,会要求求最终的时间之和,所以我们为 TSlice 加一个 Sum() 方法,就是简单的循环求和:func (t TSlice) Sum() (sum int64) { for i := 0; i < len(t); i++ { sum += t[i].End - t[i].Start } return sum} ...

February 1, 2019 · 2 min · jiezi

Go优雅重启Web server示例-讲解版

本文参考 GRACEFULLY RESTARTING A GOLANG WEB SERVER进行归纳和说明。你也可以从这里拿到添加备注的代码版本。我做了下分割,方便你能看懂。问题因为 golang 是编译型的,所以当我们修改一个用 go 写的服务的配置后,需要重启该服务,有的甚至还需要重新编译,再发布。如果在重启的过程中有大量的请求涌入,能做的无非是分流,或者堵塞请求。不论哪一种,都不优雅~,所以slax0r以及他的团队,就试图探寻一种更加平滑的,便捷的重启方式。原文章中除了排版比较帅外,文字内容和说明还是比较少的,所以我希望自己补充一些说明。原理上述问题的根源在于,我们无法同时让两个服务,监听同一个端口。解决方案就是复制当前的 listen 文件,然后在新老进程之间通过 socket 直接传输参数和环境变量。新的开启,老的关掉,就这么简单。防看不懂须知Unix domain socket一切皆文件先玩一下运行程序,过程中打开一个新的 console,输入 kill -1 [进程号],你就能看到优雅重启的进程了。代码思路func main() { 主函数,初始化配置 调用serve()}func serve() { 核心运行函数 getListener() // 1. 获取监听 listener start() // 2. 用获取到的 listener 开启 server 服务 waitForSignal() // 3. 监听外部信号,用来控制程序 fork 还是 shutdown}func getListener() { 获取正在监听的端口对象 (第一次运行新建)}func start() { 运行 http server}func waitForSignal() { for { 等待外部信号 1. fork子进程 2. 关闭进程 }}上面是代码思路的说明,基本上我们就围绕这个大纲填充完善代码。定义结构体我们抽象出两个结构体,描述程序中公用的数据结构var cfg srvCfgtype listener struct { // Listener address Addr string json:"addr" // Listener file descriptor FD int json:"fd" // Listener file name Filename string json:"filename"}type srvCfg struct { sockFile string addr string ln net.Listener shutDownTimeout time.Duration childTimeout time.Duration}listener 是我们的监听者,他包含了监听地址,文件描述符,文件名。文件描述符其实就是进程所需要打开的文件的一个索引,非负整数。我们平时创建一个进程时候,linux都会默认打开三个文件,标准输入stdin,标准输出stdout,标准错误stderr,这三个文件各自占用了 0,1,2 三个文件描述符。所以之后你进程还要打开文件的话,就得从 3 开始了。这个listener,就是我们进程之间所要传输的数据了。srvCfg 是我们的全局环境配置,包含 socket file 路径,服务监听地址,监听者对象,父进程超时时间,子进程超时时间。因为是全局用的配置数据,我们先 var 一下。入口看看我们的 main 长什么样子func main() { serve(srvCfg{ sockFile: “/tmp/api.sock”, addr: “:8000”, shutDownTimeout: 5time.Second, childTimeout: 5*time.Second, }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(Hello, world!)) }))}func serve(config srvCfg, handler http.Handler) { cfg = &config var err error // get tcp listener cfg.ln, err = getListener() if err != nil { panic(err) } // return an http Server srv := start(handler) // create a wait routine err = waitForSignals(srv) if err != nil { panic(err) }}很简单,我们把配置都准备好了,然后还注册了一个 handler–输出 Hello, world!serve 函数的内容就和我们之前的思路一样,只不过多了些错误判断。接下去,我们一个一个看里面的函数…获取 listener也就是我们的 getListener() 函数func getListener() (net.Listener, error) { // 第一次执行不会 importListener ln, err := importListener() if err == nil { fmt.Printf(“imported listener file descriptor for addr: %s\n”, cfg.addr) return ln, nil } // 第一次执行会 createListener ln, err = createListener() if err != nil { return nil, err } return ln, err}func importListener() (net.Listener, error) { …}func createListener() (net.Listener, error) { fmt.Println(“首次创建 listener”, cfg.addr) ln, err := net.Listen(“tcp”, cfg.addr) if err != nil { return nil, err } return ln, err}因为第一次不会执行 importListener, 所以我们暂时不需要知道 importListener 里是怎么实现的。只肖明白 createListener 返回了一个监听对象。而后就是我们的 start 函数func start(handler http.Handler) *http.Server { srv := &http.Server{ Addr: cfg.addr, Handler: handler, } // start to serve go srv.Serve(cfg.ln) fmt.Println(“server 启动完成,配置信息为:",cfg.ln) return srv}很明显,start 传入一个 handler,然后协程运行一个 http server。监听信号监听信号应该是我们这篇里面重头戏的入口,我们首先来看下代码:func waitForSignals(srv *http.Server) error { sig := make(chan os.Signal, 1024) signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) for { select { case s := <-sig: switch s { case syscall.SIGHUP: err := handleHangup() // 关闭 if err == nil { // no error occured - child spawned and started return shutdown(srv) } case syscall.SIGTERM, syscall.SIGINT: return shutdown(srv) } } }}首先建立了一个通道,这个通道用来接收系统发送到程序的命令,比如kill -9 myprog,这个 9 就是传到通道里的。我们用 Notify 来限制会产生响应的信号,这里有:SIGTERMSIGINTSIGHUP关于信号如果实在搞不清这三个信号的区别,只要明白我们通过区分信号,留给了进程自己判断处理的余地。然后我们开启了一个循环监听,显而易见地,监听的就是系统信号。当信号为 syscall.SIGHUP ,我们就要重启进程了。而当信号为 syscall.SIGTERM, syscall.SIGINT 时,我们直接关闭进程。于是乎,我们就要看看,handleHangup 里面到底做了什么。父子间的对话进程之间的优雅重启,我们可以看做是一次愉快的父子对话,爸爸给儿子开通了一个热线,爸爸通过热线把现在正在监听的端口信息告诉儿子,儿子在接受到必要的信息后,子承父业,开启新的空进程,告知爸爸,爸爸正式退休。func handleHangup() error { c := make(chan string) defer close(c) errChn := make(chan error) defer close(errChn) // 开启一个热线通道 go socketListener(c, errChn) for { select { case cmd := <-c: switch cmd { case “socket_opened”: p, err := fork() if err != nil { fmt.Printf(“unable to fork: %v\n”, err) continue } fmt.Printf(“forked (PID: %d), waiting for spinup”, p.Pid) case “listener_sent”: fmt.Println(“listener sent - shutting down”) return nil } case err := <-errChn: return err } } return nil}socketListener 开启了一个新的 unix socket 通道,同时监听通道的情况,并做相应的处理。处理的情况说白了就只有两种:通道开了,说明我可以造儿子了(fork),儿子来接爸爸的信息爸爸把监听对象文件都传给儿子了,爸爸完成使命handleHangup 里面的东西有点多,不要慌,我们一个一个来看。先来看 socketListener:func socketListener(chn chan<- string, errChn chan<- error) { // 创建 socket 服务端 fmt.Println(“创建新的socket通道”) ln, err := net.Listen(“unix”, cfg.sockFile) if err != nil { errChn <- err return } defer ln.Close() // signal that we created a socket fmt.Println(“通道已经打开,可以 fork 了”) chn <- “socket_opened” // accept // 阻塞等待子进程连接进来 c, err := acceptConn(ln) if err != nil { errChn <- err return } // read from the socket buf := make([]byte, 512) nr, err := c.Read(buf) if err != nil { errChn <- err return } data := buf[0:nr] fmt.Println(“获得消息子进程消息”, string(data)) switch string(data) { case “get_listener”: fmt.Println(“子进程请求 listener 信息,开始传送给他吧~”) err := sendListener(c) // 发送文件描述到新的子进程,用来 import Listener if err != nil { errChn <- err return } // 传送完毕 fmt.Println(“listener 信息传送完毕”) chn <- “listener_sent” }}sockectListener创建了一个 unix socket 通道,创建完毕后先发送了 socket_opened 这个信息。这时候 handleHangup 里的 case “socket_opened” 就会有反应了。同时,socketListener 一直在 accept 阻塞等待新程序的信号,从而发送原 listener 的文件信息。直到发送完毕,才会再告知 handlerHangup listener_sent。下面是 acceptConn 的代码,并没有复杂的逻辑,就是等待子程序请求、处理超时和错误。func acceptConn(l net.Listener) (c net.Conn, err error) { chn := make(chan error) go func() { defer close(chn) fmt.Printf(“accept 新连接%+v\n”, l) c, err = l.Accept() if err != nil { chn <- err } }() select { case err = <-chn: if err != nil { fmt.Printf(“error occurred when accepting socket connection: %v\n”, err) } case <-time.After(cfg.childTimeout): fmt.Println(“timeout occurred waiting for connection from child”) } return}还记的我们之前定义的 listener 结构体吗?这时候就要派上用场了:func sendListener(c net.Conn) error { fmt.Printf(“发送老的 listener 文件 %+v\n”, cfg.ln) lnFile, err := getListenerFile(cfg.ln) if err != nil { return err } defer lnFile.Close() l := listener{ Addr: cfg.addr, FD: 3, // 文件描述符,进程初始化描述符为0 stdin 1 stdout 2 stderr,所以我们从3开始 Filename: lnFile.Name(), } lnEnv, err := json.Marshal(l) if err != nil { return err } fmt.Printf(“将 %+v\n 写入连接\n”, string(lnEnv)) _, err = c.Write(lnEnv) if err != nil { return err } return nil}func getListenerFile(ln net.Listener) (*os.File, error) { switch t := ln.(type) { case *net.TCPListener: return t.File() case *net.UnixListener: return t.File() } return nil, fmt.Errorf(“unsupported listener: %T”, ln)}sendListener 先将我们正在使用的tcp监听文件(一切皆文件)做了一份拷贝,并把必要的信息塞进了listener 结构体中,序列化后用 unix socket 传输给新的子进程。说了这么多都是爸爸进程的代码,中间我们跳过了创建子进程,那下面我们来看看 fork,也是一个重头戏:func fork() (*os.Process, error) { // 拿到原监听文件描述符并打包到元数据中 lnFile, err := getListenerFile(cfg.ln) fmt.Printf(“拿到监听文件 %+v\n,开始创建新进程\n”, lnFile.Name()) if err != nil { return nil, err } defer lnFile.Close() // 创建子进程时必须要塞的几个文件 files := []*os.File{ os.Stdin, os.Stdout, os.Stderr, lnFile, } // 拿到新进程的程序名,因为我们是重启,所以就是当前运行的程序名字 execName, err := os.Executable() if err != nil { return nil, err } execDir := filepath.Dir(execName) // 生孩子了 p, err := os.StartProcess(execName, []string{execName}, &os.ProcAttr{ Dir: execDir, Files: files, Sys: &syscall.SysProcAttr{}, }) fmt.Println(“创建子进程成功”) if err != nil { return nil, err } // 这里返回 nil 后就会直接 shutdown 爸爸进程 return p, nil}当执行 StartProcess 的那一刻,你会意识到,子进程的执行会回到最初的地方,也就是 main 开始。这时候,我们 获取 listener中的 importListener 方法就会被激活:func importListener() (net.Listener, error) { // 向已经准备好的 unix socket 建立连接,这个是爸爸进程在之前就建立好的 c, err := net.Dial(“unix”, cfg.sockFile) if err != nil { fmt.Println(“no unix socket now”) return nil, err } defer c.Close() fmt.Println(“准备导入原 listener 文件…”) var lnEnv string wg := sync.WaitGroup{} wg.Add(1) go func(r io.Reader) { defer wg.Done() // 读取 conn 中的内容 buf := make([]byte, 1024) n, err := r.Read(buf[:]) if err != nil { return } lnEnv = string(buf[0:n]) }(c) // 写入 get_listener fmt.Println(“告诉爸爸我要 ‘get-listener’ 了”) _, err = c.Write([]byte(“get_listener”)) if err != nil { return nil, err } wg.Wait() // 等待爸爸传给我们参数 if lnEnv == "” { return nil, fmt.Errorf(“Listener info not received from socket”) } var l listener err = json.Unmarshal([]byte(lnEnv), &l) if err != nil { return nil, err } if l.Addr != cfg.addr { return nil, fmt.Errorf(“unable to find listener for %v”, cfg.addr) } // the file has already been passed to this process, extract the file // descriptor and name from the metadata to rebuild/find the *os.File for // the listener. // 我们已经拿到了监听文件的信息,我们准备自己创建一份新的文件并使用 lnFile := os.NewFile(uintptr(l.FD), l.Filename) fmt.Println(“新文件名:”, l.Filename) if lnFile == nil { return nil, fmt.Errorf(“unable to create listener file: %v”, l.Filename) } defer lnFile.Close() // create a listerer with the *os.File ln, err := net.FileListener(lnFile) if err != nil { return nil, err } return ln, nil}这里的 importListener 执行时间,就是在父进程创建完新的 unix socket 通道后。至此,子进程开始了新的一轮监听,服务…结束代码量虽然不大,但是传递了一个很好的优雅重启思路,有些地方还是要实践一下才能理解(对于我这种新手而言)。其实网上还有很多其他优雅重启的方式,大家可以 Google 一下。希望我上面简单的讲解能够帮到你,如果有错误的话请及时指出,我会更正的。你也可以从这里拿到添加备注的代码版本。我做了下分割,方便你能看懂。 ...

January 23, 2019 · 6 min · jiezi

IntelliJ IDEA创建第一个Spring boot项目

下载maven:http://maven.apache.org/downl…开发工具:IntelliJ IDEAJDK: Java JDK1.8 1.为了第一个项目初始化速度加快,我们先来配置maven:添加配置:选择Build,Execution,Deployment下, Bulid Tools下的Maven,在勾选右边红框中的Override,选择你下载后的文件夹中的settings.xml2.使用IntelliJ IDEA创建springboot项目1.创建新项目2.选择spring,选择jdk1.83.填写group ,选择packaging— War, 选择Next4.选择Web, 点击Next,下一步点击finish就好了。5.等着项目初始化完成就可以了。6.在项目的applicaiton右键,选择Run “DemoApplication"运行成功的截图:我的网站:https://wayne214.github.io

January 19, 2019 · 1 min · jiezi

三年半Java后端面试经历

经过半年的沉淀,加上对MySQL,redis和分布式这块的补齐,终于开始重拾信心去投了两家之前心水已久的公司。鹅厂面试职位:go后端开发工程师,接受从Java转语言 都知道鹅厂是cpp的主战场,而以cpp为背景的工程师大都对os,network这块要求特别高,不像是Java这种偏重业务层的语言,之前面试Java的公司侧重还是在数据结构、网络、框架、数据库和分布式。所以OS这块吃的亏比较大一面基础技术面电话面试,随便问了些技术问题,最后还问了个LeetCode里面medium级别的算法题,偏简单redis有没有用过,常用的数据结构以及在业务中使用的场景,redis的hash怎么实现的,rehash过程讲一下和JavaHashMap的rehash有什么区别?redis cluster有没有了解过,怎么做到高可用的?redis的持久化机制,为啥不能用redis做专门的持久化数据库存储?了不了解tcp/udp,说下两者的定义,tcp为什么要三次握手和四次挥手?tcp怎么保证有序传输的,讲下tcp的快速重传和拥塞机制,知不知道time_wait状态,这个状态出现在什么地方,有什么用?(参考quic)知道udp是不可靠的传输,如果你来设计一个基于udp差不多可靠的算法,怎么设计?看你项目里面用了etcd,讲解下etcd干什么用的,怎么保证高可用和一致性?既然你提到了raft算法,讲下raft算法的基本流程?raft算法里面如果出现脑裂怎么处理?有没有了解过paxos和zookeeper的zab算法,他们之前有啥区别?你们后端用什么数据库做持久化的?有没有用到分库分表,怎么做的?索引的常见实现方式有哪些,有哪些区别?MySQL的存储引擎有哪些,有哪些区别?InnoDB使用的是什么方式实现索引,怎么实现的?说下聚簇索引和非聚簇索引的区别?有没有了解过协程?说下协程和线程的区别?算法题一个,剑指offer第51题,数组中的重复数字?自己的回答情况,redis这块没啥问题,具体rehash有印象是渐进式的,但是具体原理可能答的有点出入。tcp的time_wait这块答的不是很好,之前没有了解过quic机制的实现,所以问可靠性udp的时候,基本上脑子里就照着tcp的实现在说。raft算法这个因为刚好在刷6.824(才刷到lab2。。。),答的也凑合,不过paxos确实不熟悉,直接说不会。MySQL这块很熟了,没啥说的,协程和线程,主要说了go程和Java线程的区别以及go程的调度模型。面试官提示没有提到线程的有内核态的切换,go程只在用户态调度。最后一个算法题,首先说使用HashMap来做,说空间复杂度能不能降到O(1),后面想了大概5min才想出来原地置换的思路。二面项目技术面主要针对自己最熟悉的项目,画出项目的架构图,主要的数据表结构,项目中使用到的技术点,项目的总峰值qps,时延,以及有没有分析过时延出现的耗时分别出现在什么地方,项目有啥改进的地方没有?如果请求出现问题没有响应,如何定位问题,说下思路?tcp 粘包问题怎么处理?问了下缓存更新的模式,以及会出现的问题和应对思路?除了公司项目之外,业务有没有研究过知名项目或做出过贡献?基本都没有啥问题,除了面试官说项目经验稍弱之外,其余还不错。三面综合技术面这面面的是阵脚大乱,面试官采用刨根问底的方式提问,终究是面试经验不够,导致面试的节奏有点乱。 举个例子:其中有个题是go程和线程有什么区别?答:1 起一个go程大概只需要4kb的内存,起一个Java线程需要1.5MB的内存;go程的调度在用户态非常轻量,Java线程的切换成本比较高。接着问为啥成本比较高?因为Java线程的调度需要在用户态和内核态切换所以成本高?为啥在用户态和内核态之间切换调度成本比较高?简单说了下内核态和用户态的定义。接着问,还是没有明白为啥成本高?心里瞬间崩溃,没完没了了呀,OS这块依旧是痛呀,支支吾吾半天放弃了。后面等等的面试都是这种情况,结果就是回答的节奏全无,差不多都是上面这种形式,基本都能达到一点,但是深入的OS层面就GG了。 后面问了下项目过程中遇到的最大的挑战,以及时怎么解决的? 后面还问了一个问题定位的问题,服务器CPU 100%怎么定位?可能是由于平时定位业务问题的思维定势,加之处于蒙蔽状态,随口就是:先查看监控面板看有无突发流量异常,接着查看业务日志是否有异常,针对针对100%那个时间段,取一个典型业务流程的日志查看。最后才提到使用top命令来监控看是哪个进程占用到100%。果然阵脚大乱,张口就来,捂脸。。。 本来正确的思路应该是先用top定位出问题的进程,再用top定位到出问题的线程,再打印线程堆栈查看运行情况,这个流程换平时肯定能答出来,但是,但是没有但是。还是得好好总结。最后问了一个系统设计题目(朋友圈的设计),白板上面画出系统的架构图,主要的表结构和讲解主要的业务流程,如果用户变多流量变大,架构将怎么扩展,怎样应对?这个答的也有点乱,直接上来自顾自的用了一个通用的架构,感觉毫无亮点。后面反思应该先定位业务的特点,这个业务明显是读多写少,然后和面试官沟通一期刚开始的方案的用户量,性能要求,单机目标qps是什么等,?在明确系统的特点和约束之后再来设计,而不是一开始就是用典型互联网的那种通用架构自己搞自己的。总结tcp/udp还有网络这块(各种网络模型,已经select,poll和epoll)这块一定要非常熟悉一定要有拿的出手的项目经验,而且要能够讲清楚,讲清楚项目中取舍,设计模型和数据表分布式要非常熟悉常见问题定位一定要有思路操作系统,还是操作系统,重要的事情说三遍系统设计,思路,思路,思路,一定要思路清晰,一定要总结下系统设计的流程一点很重要的心得,平时blog和专栏看的再多,如果没有自己的思考不过是过眼云烟,根本不会成为自己的东西,就像内核态和用户态,平常也看过,但是没细想,突然要自己说,还真说不出来,这就很尴尬了。勿以浮沙筑高台,基础这种东西还是需要时间去慢慢打牢,去思考去总结。系统设计相关资料:系统设计入门系统设计典型问题的思考东南亚互联网公司TODO

January 12, 2019 · 1 min · jiezi