开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

如何“拼”出一个页面-游戏中心模块化实践 一、背景 二、什么

发表于 2021-11-15

一、背景

vivo游戏中心是一款垂类的应用商店,为用户提供了多元化游戏的下载渠道。随着游戏中心手游品类的丰富,各品类用户的量级也不断增加,不同游戏偏好的用户核心关注点也不同,从预约、测试、首发、更新到维护,不同游戏生命周期节点的运营需要突出的重点不同。

针对上述不同业务场景,运营人员为了服务好广大的vivo游戏用户,需要进行精细化运营,以不同的视觉样式呈现给不同用户。比如,针对独立游戏品类的用户,平台如果提供了活动,攻略等内容的透出,能够促进用户更多的下载和消费。活动、攻略等内容以不同视觉样式呈现,运营需要通过实验的方式来验证效果,以确定最终的投放方案。这些需求都需要重新开发。受限于游戏中心APP较长的发版时间,运营的预期效果往往不佳。

因此,我们希望系统具备以下能力:通过不同视觉样式的抽象与复用,快速灵活的对页面进行布局调整,对不同的人群投放,最终以实验的方式来确定最佳的投放方案。

怎么样才能实现这些系统功能呢?答案就是模块化。下面为大家介绍一下游戏中心的模块化实践。

二、什么是模块化

所谓模块化,其实它是一种模块化的设计思想,即指能针对相同或不同功能、性能、规格的产品,进行功能分析,并设计出一系列的功能模块。

透过模块的多样选择将产品客制化,可以满足市场许多不同的需求。那么游戏中心模块化就是针对游戏中心相同或者不同功能的视觉样式,进行业务场景分析,并设计出一系列的功能模块。通过模块的多样选择,快速灵活的搭建出不同的页面,来满足不同用户的需求。

模块化有三个能力:组件化,配置化和实验化。

  • 组件化,即将页面layout拆分成多个组件,对这些组件进行抽象,进而达到复用的目的。组件是UI样式和数据的组合,组件化将UI样式切分成一些独立的,可复用的区域。
  • 配置化,即通过不同组件的拼接,可以快速搭建出各种页面。组件是构成页面的基本单位,因此每个页面都是由若干个组件构成的。组件是抽象的,对外输出是统一,可以根据不同的需求灵活的调整顺序和位置。
  • 实验化,即通过多层试验框架和DMP系统,快速的将不同的页面投放到不同特征的用户手机上,以达到多版本运营的目的。多层实验框架是vivo内部实现的ABtest框架,DMP系统即数据管理平台,可以把它简单理解成一个数据池子,用来接收来自各方的数据,然后再经过融合、处理和优化后再使用这些数据。

大家可以看到,三个功能分别对应了三个概念。组件化对应了组件,配置化对应了页面,实验化对应了方案。它们是包含的关系,即一个方案包含了若干个页面,而一个页面也包含了若干个组件。

模块化之前,游戏中心的首页是由顶部的广告banner,导航栏,游戏列表和穿插组件构成的。穿插组件即为横向插入在游戏列表中用于运营推广的由视觉样式和数据组成的广告。从穿插组件的定义来看,其实就是组件化的概念,只是当时把组件化和游戏列表做了相应的区分。穿插组件的视觉样式比较单一,只有专题、视频、热词、活动、论坛等。如图1中,小编推荐,热游驿站,抢先推荐是1*4的游戏专题穿插,类似九宫格(八个热搜词)为热词穿插。

(图1:模块化前)

穿插组件按照一定的规律穿插在游戏列表的任何位置,但是广告banner和导航栏是固定的,整个页面布局混乱,形态固定,不易变更。假如把最顶部的banner挪到小编推荐的下方,只能通过发版解决。将专题右上角的“更多”改成“换一换”,或者将游戏列表中某些游戏改为其内容的介绍,也需要通过发版解决。

模块化之后,利用组件化能力,既可以灵活的调整顺序,也可以动态更改组件的视觉样式,即使是游戏列表,也是可以动态配置。利用多个组件的顺序排列,可以快速搭建出一个页面。

通过ABtest框架和DMP系统,不同的页面,以投放方案的方式,能够快速呈现给对应的人群,进行多版本的运营。图1和2是模块化前后首页推荐的对比图,虽然从大的样式的没有太大的改变,但是模块化之前的样式相对单一,而模块化之后游戏列表中排列了单游戏大图、金刚位、小喇叭、专题、新游预约、下载榜等组件。

(图2:模块化后)

不同的组件可以满足不同的业务场景。例如单游戏大图组件,辅以推荐,可以快速推广新游和热游,满足了不同用户对不同游戏节点的需求;新游预约组件可以从更多角度满足用户对于单款游戏提前订阅内容或关注其实时动态的需求。

三、怎么实现模块化

从前面的介绍大家可以看到,模块化通过组件化的方式快速搭建页面并将其投放给不同标签的人群,功能强大且配置灵活,为我们省去了不少的开发成本,那么我们的底层是怎么实现的呢?

3.1 模型抽象和统一

根据组件布局,我们可以将组件抽象成两部分:视觉样式和数据;视觉样式可以简单理解为UI样式,即呈现在用户面前的展现形式,我们可以将视觉样式简单概括为模板。

模板定义了当前组件最基础的形式,比如当前组件是滑块的形式,还是固定的形式;模板上还定义了一些可变的样式,即定义了当前模板哪些位置是可以通过配置来完成的,比如模板的长宽高、组合样式(2*2,1*4)、包含列表的个数等,能够动态配置的程度依赖模板的定义。

所谓的数据,按照来源分为推荐数据和人工排期数据。推荐数据来自算法和大数据,而人工排期,则是运营在后台配置的。由于推荐数据背后的逻辑比较复杂,本文只讨论人工排期数据。人工排期是一个四维数据。除了数据本身的“业务性”之外,它是有时效性的,游戏中心的广告位展示都是有时间限制的,比如游戏中心首页顶部banner,今天和昨天展示的是不一样的;其次,它是有“空间性”的,即不同的用户可能看到的数据是不一样的;另外,它是有动作性的,即点击后产生的事件。比如点击某个组件,可以是弹出一个悬浮窗,或者切换到到另外一个页面。

因此,模块化可以简单理解为模板和人工排期的组合。通俗点理解,组件其实像一个类,每个页面上不同的组件即为组件的对象,对象会实例化一些数据和行为。通过组件化的方式,我们不仅解决了端侧和服务端的耦合,同时还实现了组件在不同页面的复用。

排期数据的组成分为三个层次(即素材、推广物料以及排期)。最底层数据当然就是图片,视频,推荐语,评论等,当然游戏信息中也会包含游戏icon,背景图以及简单的视频,但是此处的图片并不是指游戏icon,而是比icon更精致,用来宣传广告的素材图片,这类数据我们统称为素材。由素材组成了上层的推广物料。

什么是推广物料呢?推广物料其实就是基于某种目的,按照一定的形式来展示的内容集合。比如banner,其实就是一张图片加上其背后的关联的内容构成,图片是为了吸引用户点击,目的是为了推广背后关联的内容。推广物料加上时间空间和动作属性就变成了排期。素材,推广物料和排期都进行了统一的抽象。

如下图3,推广物料有Banner、专题、活动、网页等;排期有胶囊banner、游情报,种草机、重磅更新等;如种草机就是网页(内容链接)加上时间组成的;整个结构呈现一个倒金字塔结构。

(图3)

原先游戏中心每个资源位的排期数据都是放在不同的地方,比如顶部banner排期,网游banner排期都有各自的表来保存。在这样的情况下,数据库表的数量可能会比较多,对统一拓展来说就更加复杂。假如我需要对顶部banner和网游banner都要增加对不同人群(DMP系统)展示不同数据的时候,通常需要在每一张表中都增加一个DMP的字段来表示当前排期需要展示给用户的标签id。

模块化之后,我们将游戏中心所有的资源位都当成一个个模块,也就是都可以看成是排期数据,我们只需要两张表就可以做到排期三维数据的展示:排期数据表以及排期关联的具体素材表。因此我们在设计排期表的时候, 将素材信息(如图4中的material_id和material_type)数据保存在排期表中, 将DMP或者其他统一的信息也保存在排期表中, 这样做的好处就是对所有的排期都能生效统一的流程。

(图4)

3.2 后台业务流程统一化和可视化

如此复杂的业务流程,肯定少不了后台的配合。素材,推广物料和排期的统一后,我们也将后台的配置流程标准化。

具体的配置流程如图5,6。图5偏向于后台的配置流程,是自下而上的配置。而图6是为了方便大家理解,是自上而下的结构。用户层面展示的是某个具体的方案,方案由若干个页面组成,但是会根据用户的画像具体展示,即配置的页面个数不一定是展示的页面个数。页面由若干个组件组成,组件由模板和数据组成….一层层往下分解,可以整体理解模块化的结构组成。

(图5)

(图6)

模块化之前,配置首页banner排期,需要到首页banner的tab栏下,在全部banner排期里面加入相关的排期,然后在首页banner排期里面挑选全部banner排期里面的数据,而配置其他页面的banner排期呢,也是需要在类似的目录结构中做相关操作,彼此之间的banner是割裂的,无法通用;对于运营来讲,配置工作量也增加了不少。

模块化之后,操作变得非常简单了。在组件层面,通过数据库配置,我们可以将模板的信息事先保存在数据库中。在数据层面,我们把所有的banner数据统一保存在推广物料管理并绑定到排期中,做到复用。在业务组件管理页面中,根据组件应用场景来选择模板,之后配置对应的排期数据。

如图7,当前重磅更新场景关联的是人工排期(如果关联的是推荐,则是推荐对应的code),配置好样式后整个业务组件也就配置完成了。然后在页面管理中将你想要配置的组件添加到某个页面中,设置好对应的DMP。配置好组件后,在后台页面上可以滚动进行整体页面效果预览。

(图7)

如图8。最后可以将页面复用到不同的投放方案中,运营后台通过审核流和上线点检后,最终显示在用户界面端。当前配置流程自下而上,逻辑清晰,符合运营的配置习惯。

(图8)

3.3 前端业务流程抽象与统一

目前,游戏中心首页和新游专区改造成了模块化页面。首页是游戏中心非常重要的分发渠道,模块化要求页面形式多种多样,同时假如模块化改造不被用户认可的时候也要能够动态回退。

因此首页模块化页面会有三种类型:

  • 纯模块化页面,
  • 穿插模块化页面和
  • H5模块化页面

所谓纯模块化页面,即页面中的所有元素都是由组件化数据构成。所谓穿插模块化页面,其页面结构为按照一定规律在游戏列表中穿插了若干个组件,和模块化之前的页面组织结构是一模一样的,只是后台的实现方式不一样。

穿插模块化页面中的列表还有两种不同的形式,分为游戏列表和混合数据流列表。穿插页面可以在一个屏幕中最大效率的展示游戏。

最后的H5模块化页面,可以认为由H5组件所构成的页面,由我司的悟空建站提供页面。通过多层试验框架和DMP获取不同方案以及不同页面的流程不在此处赘述。这里我们讲一讲整个流程中最为复杂的穿插页面流程,以及我们怎么在如此复杂的流程中抽象和设计。

(图9)

3.3.1 流程统一

从上图(大家无需过分关注流程图里面各个步骤的业务逻辑,流程图只是为了展示原来流程的复杂度)我们可以看出来,用户请求开始,需要经过N个步骤。这么冗长的步骤,如果写在一个service,那么就会造成逻辑混乱,维护性不高和扩展性差的效果,因此我们可以分而治之。图9中,我们用不同的颜色来区分步骤,可以做一个简单的流程归纳。

归纳后的流程如下图所示。我们可以把提交异步线程池进行归纳,可以理解为获取组件列表和混合数据列表为两个步骤。

(图10)

我们再进行归纳和抽象后,整个模块化的页面获取流程就可以简化为四个步骤:初始化阶段、获取组件列表阶段、构建阶段和合并阶段,如图;

(图11)

在《金字塔原理》一书中曾说过,读者在阅读文章的时候,必须阅读理解每一句话,并且寻找每句话之间的联系,前前后后反复思考。如果你的文章结构呈现金字塔形,文章的思路自金字塔顶部开始逐渐向下展开,那么读者肯定会觉得你的文章比较容易读懂。这一现象体现了人类思维的基本规律,那么阅读代码其实也是一样的逻辑。好的代码即是一段业务逻辑的注释,通过阅读代码能够大概判断主要的业务流程。在构建阶段, 可以通过组合不同的策略来获取不同的排期数据。策略和组件解耦,当新增策略的时候,无需改动原有的业务逻辑。此处不同的策略也可以采用工厂模式的方法来获取。

首页的组件展示逻辑是比较复杂的,尤其对于穿插模块化页面。正如前文所述,穿插页面由游戏列表和业务组件构成,即在一个游戏列表中,穿插了各个业务组件。数据列表如果是游戏数据列表, 那么每个游戏都是按照一定的比例来排列的,且需要和组件中推广物料的底层数据是游戏的去重。比如游戏按照网游,单机和独立游戏的比例来展示,假如上一个组件展示过当前游戏,那么这个游戏需要被过滤,且补位游戏也是有一个逻辑,比如网游被过滤了,那么取补位游戏列表中的游戏,其次用网游来补,再次用下面的游戏顶上来。去重逻辑包含两种,一种是能被之前的游戏过滤,也能过滤下面的游戏;另外一种则只能去重下面的游戏,不会被上面的游戏去重。

(图12)

在获取业务组件中排期数据(如上图,其中也包含了游戏信息)的时候,还会有获取ABTest信息(不同的用户展示不同的游戏信息),游戏黑白名单过滤,已安装过滤,已曝光过滤,额外信息处理,组件数据组装等过程。每一个排期数据获取都会用不同的Handler来处理。每一个Handler都有自己的处理逻辑,针对如此多的排期以及他们的扩展,如果没有一套处理的统一逻辑,那么简直是灾难。新人在开发新组件中排期数据的时候,可能会遗漏非常多的细节。另外,推广物料之间其实是有一些通用逻辑,如果不将这些逻辑沉淀到领域模型中,逻辑无法复用,将会散落在各个Handler中,我们以下图橙色的步骤来详细说明。

3.3.2 推广物料的流程统一化

一方面,我们将获取并处理推广物料的流程统一化。如下图所示流程中,其实基本上就包含了所有推广物料需要处理的步骤逻辑(不重要的步骤已忽略)。统一之后,我们可以将一些通用的逻辑下沉,形成统一的方法调用。比如我们可以根据人工排期,推荐排期等,采用工厂方法的设计模式来屏蔽获取推广物料的逻辑。当然我们为了提升性能,对于人工排期数据,利用统一缓存的方式,通用场景code来获取;接着利用不同过滤策略来过滤掉进入黑灰名单的游戏或者内容。处理完额外信息之后再用列表的数据将组件中重复的数据给去除。

(图13)

3.3.3 模型的逻辑抽象与沉淀

另一方面,我们将统一处理组件和推广物料的逻辑沉淀到对应的领域模型中,如图;

(图14)

整个过滤重复数据的流程都是针对组件进行的,那么在组件层面,会有大量的重复逻辑,比如每个组件需要在最后返回的数据个数处理上,不同的组件返回的个数是不一样的,那么这部分逻辑写在组件这个领域模型中会更加妥当。比如已曝光和已安装处理,这个逻辑就可以放在组件层面来处理, 当然放置在Handler层处理也是没有问题的。在推广物料层面,也有一些通用的逻辑,比如1*4个游戏组成的专题和2*2个游戏组成的专题,他们背后都有一套接入推荐系统并兜底的逻辑,也可以沉淀在专题这个领域模型来处理。

四、写在最后

当前,很多业务开发的同学,尤其在熟悉了业务之后,通常会陷入一个误区:做业务的基本上就是CRUD,没有什么技术含量。但是在开发的过程中,往往又缺乏相应的思考,导致重复开发。那么如何才能让业务开发变得更有吸引力和技术含量呢?

游戏中心模块化改造过程中,利用组件的抽象和复用,提升了组件化,配置化和实验化能力,快速的支撑了业务需求,同时通过统一标准流程和利用领域模型知识不断的完善业务代码,提升了代码的维护性和可扩展性。随着业务的不断发展,即使现在非常合理的架构也会变得臃肿,难以扩展,但是如何做好业务的方法论确是不变的。因此做业务开发同学,应该多思考怎么把业务做深做通用,提升快速实现业务价值的能力。

作者:vivo互联网服务器团队—Chen Wenyang

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

技术架构+应用场景揭秘,为什么高斯Redis比开源香?

发表于 2021-11-15

摘要:高斯Redis即保留了开源Redis的能力,同时凭借其存算分离的架构,在成本、稳定性、可靠性、一致性等方面做出了新的突破,也更加适用于当下数据规模庞大的互联网业务。

本文分享自华为云社区《【大厂内参】第12期:技术架构+应用场景揭秘,为什么高斯Redis比开源香?》,作者:华为云社区精选。

点的外卖总能让离店近的外卖小哥送来,双11秒杀结束后产品能立刻下架,12306火车票保证从来不超卖,微博下拉就能刷新出好友动态……这些日常碎片的背后都有着Redis的身影。

提起Redis,互联网从业者无人不知,无人不晓。毕竟,开源Redis作为一款经典的“缓存”产品,能支撑众多业务架构搭建,在游戏、电商、社交媒体等行业中发挥着重要的作用,广受开发者青睐。

然而近年来,随着各行业规模逐渐扩大,几乎只能依附于关系型数据库的传统“缓存”逐渐难以支撑上层业务,越来越力不从心。

一旦业务规模扩大后数据量逼近内存上线,开源Redis轻则发生重要数据逐出,重则导致节点OOM宕机。而且开源Redis为了访问快速,全部数据都保存在内存中,其独有的fork机制,更让平时的内存使用不得高于50%,使得内存价格一直居高不下,导致部署成本非常高。

为了解决这些难题,华为云推出了自研的企业级Key-Value数据库——云原生分布式数据库**GaussDB(for Redis)(下文简称高斯Redis**),让开发者用更低的成本构建依赖缓存的应用,且性能更高,运行更稳定。本文将从高斯Redis的技术架构和应用场景出发,一一道来为什么高斯Redis比开源香,以及它是如何做到又快又好的。

开源不够,自研顶上

开门见山,先看看开发者最关心的性能和成本。如下图所示,与开源Redis相比,高斯 Redis在成本、可用容量、吞吐、压缩上都有非常大的优势:

​注:比较相同数据容量(约200G)的成本开销

核算下来,高斯Redis以1/4的价格拥有10倍以上的可用空间,整体成本相当于是开源Redis自建数据库的1/40,这里还不包括自建Redis数据库需要额外的搭建、运维、监控、升级扩容等各项成本。

同样,对比高斯Redis和开源Redis集群在X86架构下的性能测试,结果显示,它能较开源Redis集群能提供更高的QPS,更低的访问延迟,以及更低的数据存储成本。

  1. 性能优势:在相同测试条件下,高斯Redis的QPS较开源Redis集群提高了11%~19%,平均延迟和P99比Redis集群降低了70%以上,p9999比Redis集群降低了15%以上。
  1. 抗写优势:在数据量大于内存的写测试中,原生Redis集群因内存限制而OOM,高斯Redis依然可以提供不俗的性能服务,它的可用的存储空间由底层SSD大小决定的,相比原生Redis集群抗写优势显著。
  1. 据存储成本更低:高斯Redis提供了高效的数据压缩服务,其占用的存储空间只有开源Redis集群的十分之一,相当于数据存储成本降低了10倍。

那么,高斯Redis的优势源自什么?从它的架构中或许可以窥见一斑。

存算分离,突破瓶颈

高斯Redis有两个跟业界完全不一样的特性,第一个便是独有的存算分离架构, 计算层实现热数据缓存,存储层实现全量数据的落盘,中间通过RDMA高速网络互连,通过算法预测用户的访问规律,实现数据的自动冷热交换,最终达到性能提升。

该架构基于华为内部的自研分布式共享存储池, 它也是华为全栈数据服务的基石,比如文件EVS、对象存储OBS、块存储,还有数据库族、大数据族都依赖于此,可想它的强大及稳定性。

高斯Redis基于共享存储池实现了一套Shared Everything的云原生架构,充分发挥了云原生的弹性伸缩、资源共享的优势,使得它具备强一致、秒扩容、低成本、超可用等特性,完美避开了开源Redis的主从堆积、主从不一致、fork抖动、内存利用率只有50%、大key阻塞、gossip集群管理等问题。

至于高斯Redis的存算分离架构的设计和实现原理,在线课程**当Redis遇见计算存储分离**中有更详细的解读,包括软件架构的剖析,计算层的模块的分工,组网的设计以及容灾架构等等。

在存算分离的架构下,高斯Redis的优势可以总结为:强一致、高可用、弹性伸缩、高性能。

强一致

高斯Redis将全量数据下沉到强一致的共享存储池,得益于共享存储池的3副本机制,因此写入高斯Redis的数据,在客户端收到回复时,数据也将是3副本强一致的,保证宕机的时候数据不会丢失,从而为业务提供前后一致的状态,再也不用担心主从切换后的数据一致性和丢失问题。

高可用

其次是高可用,受益于分布式共享存储池,高斯Redis的每个计算节点都可以看到并共享所有数据,当某一个计算节点发生故障挂掉,其维护的slot路由信息,会被剩下的节点自动接管。由于不涉及底层数据的迁移,这个接管过程非常快。所以N个节点下,最多可以容忍挂掉N-1个节点。

弹性伸缩

再就是弹性伸缩带来的秒扩容能力,实现按需扩容计算和存储。计算资源的扩容只涉及到元数据的修改,把相应的slot路由信息迁移到新的节点上,迁移速度非常快。由于采用的共享存储,大多数情况下存储扩容只要进行逻辑扩容,不涉及数据的搬迁,在后台修改存储配额即可。

高性能

存算分离的架构看似比较重,链路比较复杂,实则在硬件采用、软件优化上,可以做的更大胆更激进,比如RDMA网络、用户态协议、持久化内存等等。因此受益于这些专属的存储设备,加上计算层全负荷分担架构(不引入从节点,因此性能轻松翻倍),对比同类商业数据库产品,在数据量大于内存的存储场景下,高斯Redis的性能表现很好。另外,对比开源Redis,在数据小于内存的点查场景下,高斯性能也有很大优势。

第二个特性是多模架构带来的产品使用便捷性。

高斯Redis是多模数据库Gauss NoSQL的一员,Gauss NoSQL提供了全栈的分布式KV引擎、用户态文件系统、存储池等技术,只需要在接口上封装Redis协议,即可轻松实现一个全新的NoSQL产品。类似的,华为还提供了MongoDB、Cassandra、Influx等NoSQL引擎。

也正是得益于高斯Redis的独特优势,使得它在一些典型的应用场景下,能够应对各种突发情况,最大化发挥出Redis的特性。

互联网业务神器,支撑海量存储场景

Redis最常见的应用场景是缓存,用来存放秒杀、热点事件的数据,比如微博热搜。同时,凭借其优异的存储能力,缓存场景之外的诸多应用Redis也可以轻松应对,比如

流: feed、消息队列、IM聊天、IoT心跳上报;

只读状态: 历史订单、日志审计、归档信息、历史轨迹、消费记录、物流详情;

可变状态: BI报表、金融风控、智能客服、广告推荐、标签工程、用户画像、地理位置、路径规划、知识图谱等。

下面,以其中的一些场景为例,具体看看高斯Redis到底有多强大?

Geo

饭点时打开大众点评查看附近的餐馆,外卖小哥根据距离远近来决定配送的路径规划……这些都依靠LBS服务,它的实现又需要Redis来存储地理位置数据。但开源版本Redis因为内存限制,一直没有大规模应用支持地理位置信息存储管理的Geo功能。

高斯Redis使用磁盘替代内存,解决了这些难题,它的Geo功能适用于数据量大、读写频繁的场景,可以应对诸如外卖平台、点评平台、找房平台中,随着用户增长而对应的地理位置信息的数据量的增长,最高可达TB级别。以下图为例,可以看到在高斯Redis支持下,外卖系统可以使用Geo的相关命令,让用户获取骑手的实时位置,骑手也能找到附近可配送的订单,最终顺利将用户的外卖送到用户。

计数

社交平台每条热搜记录的搜索量数值;用户注册一个帐号后,网站记录的关注数、粉丝数、动态数;一个接口一分钟被限制100次请求等。这些数据背后,是一个个计数器在工作。

计数是典型的强一致应用场景,比如电商在秒杀活动中,往往会搭建Redis主从集群给下层MySQL做缓存,用Redis的计数器功能抵住流量压力。

所以如果数据发生不一致,计数器就会得到错误的信息,整个数据库可能面临崩溃的危险。但原生Redis的主从同步是异步的,当主节点写入数据后,从节点不保证立刻更新数据,如果此时读取数据,读到的就是过期的旧数据,产生数据不一致问题。高斯Redis则可以把全量数据下沉到强一致共享存储池,彻底摒弃了开源Redis的异步复制机制。另外,计算层将海量数据进行分片,在故障场景下,自动进行接管,实现了服务的高可用。

即时通讯

即时通讯(简称IM)是一个实时通信系统,允许两人或多人使用网络实时的传递文字消息、文件、语音与视频。它最核心的是消息系统,包括聊天消息的同步、存储和检索。而消息存储库和同步库又对存储层的性能有很高的要求:要能支撑海量消息数据的永久存储,具备极高的写入吞吐能力,尽可能低的读取延迟等等。

综上,存储层的性能会直接影响到IM系统的用户体验。高斯Redis在性能和规模上可以满足IM系统对存储层的严格要求,它作为IM系统的存储层,可以将大量的随机写转换为顺序写,提升数据写入性能,再通过读缓存、bloom filter优化读取性能。

下图是一个基于高斯Redis的IM应用案例,使用的是Stream作为基本数据结构。创建一个群聊时,在Redis中对应地为该群聊创建一个Stream队列。在发送消息时,每个用户都将消息按照时间顺序添加到Stream队列中,保证了消息的有序性。

这个应用中涉及到了一种数据类型——Redis Stream,它也是一种消息队列,提供消息的落地存储功能,让每个客户端可以访问任意时刻的消息,并记录访问位置,保证消息不会丢失,以IM中的文字聊天为例,使用Stream作为中间件,实现聊天室的发言和信息查看。高斯Redis可以存储和处理大规模的Stream数据,鲁棒性强的同时成本相对更低,适用于海量消息队列的场景。所以,相较于原生Redis,是更为理想的Stream队列承载方案。

Feed流

互联网时代,微博、抖音、头条等都在通过Feed流(信息流)将关注的好友或感兴趣的内容及时推送给用户,吸引用户的兴趣,提高产品的商业价值。Feed流系统是Feed生成者将生产的Feed经过存储分发系统传递给Feed消费者,最终以某种展现形式。

整个系统最关键的是同步存储系统,首先是内容存储模块,由它来存储最原始的内容,比如用户发的一条微博;其次是关联关系存储模块,存储的是用户之间的关系;最后是信箱模块,也叫消息传递模块,通过它将消息传递到每个关联用户手中。

在Feed流场景下,高斯Redis能够支撑海量消息内容的存储和低延迟访问,以及关联关系的增删查改。在同步存储系统中的信箱存储模块,高斯Redis的Stream数据结构可以实现队列能力,实现Feed流消息读取。

推荐系统

电商、社交等领域的推荐系统非常发达,追溯其背后技术,不外乎这三个环节:分布式计算、特征存储、推荐算法。其中,特征数据的存储起到关键的衔接作用,由于KV形式的数据抽象与特征数据极为接近,因此推荐系统里往往少不了Redis的身影。

由于开源Redis在大数据场景下的一些固有痛点,高斯Redis是不少客户首选的数据库选型。由高斯Redis负责核心的特征数据存储,提供稳定、可靠的KV存储能力。加上它的高性能持久化技术和细粒度存储池,可帮助企业将数据库使用成本降低75%以上。高斯Redis独特的多线程设计和全部节点可写,抗写能力强,可从容应对Spark灌库压力和实时更新。

而且因为高斯Redis完全兼容Redis协议,即开即用,用户可使用熟悉的Spark SQL语法轻松访问,完成特征数据灌库、更新、提取等关键任务。 与此同时,数据源经过Flink加工后,也可轻松存入高斯Redis中。

成为VMALL智能推荐背后的英雄

当电商平台对AI算法模型的需求越来越多,特征数据平台的统一建设是不少开发团队头疼的事情。 只有通过统一的特征数据存储,才能改变原有的“数据孤岛”,解决生产重复造轮子的窘境。

华为商城(VMALL)就有这样的困扰,VMALL使用了大量的AI和大数据技术,用来支撑智能推荐、精准营销、智能搜索、选品投放等业务的高效开展。但因为特征数据准备阶段缺乏通用平台,严重影响研发效率。

特征数据库需要承担打通线上/线下多个场景,对接批式/流式多种数据源,满足训练/推理多样消费需求,相应地对存储也提出了高要求:既能提供低成本的海量数据存储并方便扩容, 又能保证数据的绝对可靠和服务的高可用;既要满足低时延的线上推理,又要满足高吞吐的线下训练; 既能提供简洁的KV接口供下游轻松消费,又要兼容主流的批式/流式处理引擎(Spark/Flink等)供上游快速接入。

为了满足这些要求,深入调研后,VMALL大数据团队最终选择了高斯Redis作为特征数据库。

在线上推理的特征生产(抽取、处理、存储)中,特征平台会定时调度Spark作业,从各种数据仓库、数据湖中提取数据,进行特征工程处理后,存入高斯Redis。至于实时特征,则由Flink消费Kafka,或流式存储中的数据,持续更新到高斯Redis中。

在特征消费的推理环节,对于使用实时特征的场景(如实时推荐系统),由Flink从Kafka中实时取得用户请求记录,并从高斯Redis查询取得特征,将记录和特征拼接成训练样本,存储到文件中,供线下训练使用。

目前VMALL已完成一期的特征数据迁移,包括“特征生产”业务中的“Spark离线特征生产”,以及“特征消费”业务中的“线下训练Flink特征查询”。迁移后的运行结果显示,高斯Redis在业务高峰时段时延稳定,能够满足VMALL当前业务要求。其中,读平均时延0.2ms(p99<0.4ms),写入平均时延0.6ms(P99<2ms)。

费用方面,按照VMALL的特征体量测算,亿级用户,每个用户的特征数量是数K-数10K,高斯Redis一年的费用仅3W出头,如果选用社区Redis,费用在20W+。

综上,高斯Redis在VMALL特征工程平台建设中,起到了关键作用。它在成本,可靠性,可扩展性等方面具有优势,可作为特征数据存储的理想方案,提供企业级的稳定可靠的Redis服务能力。

最后

作为一款KV数据库,高斯Redis即保留了开源Redis的能力,同时凭借其存算分离的架构,在成本、稳定性、可靠性、一致性等方面做出了新的突破,它也更加适用于当下数据规模庞大的互联网业务,包括电商平台的秒杀、推荐系统、社交平台的信息流等等。本文只是简单地解读了高斯Redis的几个典型特性,更多技术细节,以及应用案例、迁移指南等可以查看高斯Redis系列合集。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

剑指 Offer II 081 允许重复选择元素的组合

发表于 2021-11-15

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」

给定一个无重复元素的正整数数组 candidates 和一个正整数

target 找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。

candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,

则两种组合是唯一的。 对于给定的输入,保证和为 target 的唯一组合数少于 150 个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
lua复制代码示例 1:

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]
示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:

输入: candidates = [2], target = 1
输出: []
示例 4:

输入: candidates = [1], target = 1
输出: [[1]]
示例 5:

输入: candidates = [1], target = 2
输出: [[1,1]]
 

提示:

1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/Yg…

思路

  • 首先根据题意和样例我们可以看出
    • 我们可以重复的去使用数据,比如在2,3,5中2,2,2,2依然可以组成数字8
  • 其次我们选择来制定辅助函数,由于有target我们先写出辅助函数的出口
    • 出口就是放满足target为0时,就保存到结果集中
    • 如果不满足上述条件,就意味着要继续遍历这颗状态树,有两种策略
    1. 第一种就是不选这个数组中的数字,cur + 1表示继续往下遍历,这一步不做任何动作
    • 这其中要放入list中代表可能使用当前的数字
    1. 第二种策略是回退到上一步树🌲节点,并且再最后list要清除之前子集状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码class Solution {
public List<List<Integer>> combinationSum(int[] num, int target) {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> list = new LinkedList<>();
dfs(num,target,list,res,0);

return res;
}

public void dfs(int[] num,int target,LinkedList<Integer> list
,List<List<Integer>> res,int cur) {
if (target == 0) {
res.add(new LinkedList<>(list));
}else if (target > 0 && cur < num.length) {
dfs(num,target,list,res,cur + 1);
list.add(num[cur]);
dfs(num,target - num[cur],list,res,cur);
list.removeLast();
}
}
}

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

netty(六)NIO、BIO与AIO 一、BIO与NIO

发表于 2021-11-15

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。

一、BIO与NIO

本小节将BIO与NIO放到一起进行分析,主要为了突出其差别。

1.1 对比stream和channel

以前我们写代码,涉及到IO操作,首先想到的必然是一系列的stream,如InputStream等。如今随着java中nio的引入,我们多了一个选择,channel。那么两者相比有哪些不同,channel又有哪些优势呢?

1)stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)。
2)stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用。
3)二者均为全双工,即读写可以同时进行。

二、IO模型

当调用一次 channel.read 或 stream.read 后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:

1)等待数据准备好

2)从内核向用户进程复制数据

阻塞IO模型

image.png

非阻塞IO模型

应用进程不停的轮询内核空间,会造成CPU浪费。

image.png

多路IO复用模型

用户进程首先阻塞于select方法,当内核返回可读状态后,根据事件类型去做调用,将数据复制到用户空间缓冲区,处理区间状态阻塞。

image.png

异步IO模型

AIO是java中IO模型的一种,作为NIO的改进和增强随JDK1.7版本更新被集成在JDK的nio包中,因此AIO也被称作是NIO2.0。AIO提供了从建立连接到读、写的全异步操作。AIO可用于异步的文件读写和网络通信。

image.png

三、零拷贝

3.1 原始IO分析

如下伪代码,读取本地文件,通过socket写出:

1
2
3
4
5
6
7
8
9
10
ini复制代码File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
// 包括2次拷贝(DMA(硬件到内核缓冲区),内核缓冲到用户缓冲)
file.read(buf);

Socket socket = ...;
//包括两次拷贝(用户缓冲区到socket缓冲区,DMA(socket缓冲区到网卡))
socket.getOutputStream().write(buf);

其内部实际的工作构成如下所示:

image.png

我们根据代码的过程,结合图上的步骤逐步分析:

1)创建文件类file,定义byte数组,当真正开始执行read方法时,才开始获取数据。

java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(内核)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access,可以理解为硬件单元,解放CPU的同时,完成文件IO)来实现文件读,其间也不会使用 cpu。

此处可以算作第一次数据拷贝,但是通过DMA技术解决了IO问题。

2) 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA。

此处是第二次数据拷贝。

3)调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝。

此处是第三次拷贝。

4)接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu。

此处算作第四次拷贝,DMA技术解决IO问题。

总结:
通过上面的分析,我们得到java本身不具备物理设备级别的IO读写,而是缓存级别的读写,通过调用操作系统来完成硬件级别的读写。

上述步骤总共经历3次状态切换,4次的数据拷贝。

3.2 NIO优化

3.2.1 使用直接内存

在前面我们学习ByteBuffer时,介绍到了其可以使用直接内存DirectByteBuffer。

ByteBuffer buffer = ByteBuffer.allocateDirect(16);

那么通过这个直接内存,能使我们前面的过程做到哪些优化呢?

image.png

如上图所示,由于直接内存的引入,java 可以使用 DirectByteBuf 将堆外内存(内核缓冲区)映射到 jvm 内存(用户缓冲区)中来直接访问使用。而其他的步骤没有变化。

  • 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写。
  • java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成以下两步:
    1)DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列
    2)通过专门线程访问引用队列,根据虚引用释放堆外内存
  • 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

3.2.2 channel的transferTo/transferFrom

底层采用了 linux 2.1:
进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用transferTo/transferFrom 方法拷贝数据。

其过程如下图所示:

image.png

1)java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu

2)数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝

3)最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

如上图和过程所示,其中只经历了一次状态切换,数据拷贝仍然是3次。

底层采用了 linux 2.4:
linux底层对于整体的效率又有了优化,如下图所示:

image.png

1)java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu

2)只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗

3)使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

整个过程只发生了一次状态切换,实际只通过DMA经过两次数据拷贝。

实际所谓的零拷贝并不是真正的没有拷贝过程,而是不会有数据拷贝到用户态,即jvm内存中的过程。

四、AIO

4.1 简单介绍及使用

AIO 用来解决数据复制阶段的阻塞问题

  • 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
  • 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果

异步模型需要底层操作系统(Kernel)提供支持

  • Windows 系统通过 IOCP 实现了真正的异步 IO
  • Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
csharp复制代码public class TestAio {

public static void main(String[] args) throws IOException {
try {
AsynchronousFileChannel s = AsynchronousFileChannel.open(
Paths.get("C:\\Users\\P50\\Desktop\\text.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("begin...");
s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("read completed..." + result);
buffer.flip();
System.out.println(Thread.currentThread().getName() + ",内容是:" + print(buffer));
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("ead failed...");
}
});

} catch (IOException e) {
e.printStackTrace();
}
System.out.println("do other things...");
System.in.read();
}

static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
}

结果,打印内容的并不是主线程,多次尝试,每次都是不同的,并且主线程并没有阻塞:

1
2
3
4
arduino复制代码begin...
do other things...
read completed...10
Thread-7,内容是:helloworld

默认文件 AIO 使用的线程都是守护线程,所以最后要执行 System.in.read() 以避免守护线程意外结束。

4.2 网络编程

服务端示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
typescript复制代码public class AioServer {

public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.accept(null, new AcceptHandler(ssc));
System.in.read();
}

private static void closeChannel(AsynchronousSocketChannel sc) {
try {
System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}

private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel sc;

public ReadHandler(AsynchronousSocketChannel sc) {
this.sc = sc;
}

@Override
public void completed(Integer result, ByteBuffer attachment) {
try {
if (result == -1) {
closeChannel(sc);
return;
}
System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
attachment.flip();
System.out.println(Charset.defaultCharset().decode(attachment));
attachment.clear();
// 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件
sc.read(attachment, attachment, this);
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
closeChannel(sc);
exc.printStackTrace();
}
}

private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel sc;

private WriteHandler(AsynchronousSocketChannel sc) {
this.sc = sc;
}

@Override
public void completed(Integer result, ByteBuffer attachment) {
// 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容
if (attachment.hasRemaining()) {
sc.write(attachment);
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
closeChannel(sc);
}
}

private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
private final AsynchronousServerSocketChannel ssc;

public AcceptHandler(AsynchronousServerSocketChannel ssc) {
this.ssc = ssc;
}

@Override
public void completed(AsynchronousSocketChannel sc, Object attachment) {
try {
System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
ByteBuffer buffer = ByteBuffer.allocate(16);
// 读事件由 ReadHandler 处理
sc.read(buffer, buffer, new ReadHandler(sc));
// 写事件由 WriteHandler 处理
sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
// 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件
ssc.accept(null, this);
}

@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

冲刺大厂每日算法&面试题,动态规划21天——第十七天 导读

发表于 2021-11-15

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

给你一个字符串 s,找到 s 中最长的回文子串。

1
2
3
4
5
java复制代码示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
1
2
3
4
java复制代码示例 2:

输入:s = "cbbd"
输出:"bb"
1
2
3
4
java复制代码示例 3:

输入:s = "a"
输出:"a"
1
2
3
4
java复制代码示例 4:

输入:s = "ac"
输出:"a"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
int start = 0, end = 0;
//从串的第一个元素为中心扩散
for (int i = 0; i < s.length(); i++) {
//以奇数子串为前提开始
int len1 = expandAroundCenter(s, i, i);
//以偶数子串为前提
int len2 = expandAroundCenter(s, i, i + 1);
//比较两者谁更大
int len = Math.max(len1, len2);
//找到新串的位置
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}

return s.substring(start, end + 1);
}

public int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return right - left - 1;
}


}

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

1
2
3
4
5
java复制代码示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
1
2
3
4
5
java复制代码示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码对于一个子序列而言,如果它是回文子序列,并且长度大于 22,那么将它首尾的两个字符去除之后,它仍然是个回文子序列。因此可以用动态规划的方法计算给定字符串的最长回文子序列。

用 \textit{dp}[i][j]dp[i][j] 表示字符串 ss 的下标范围 [i, j][i,j] 内的最长回文子序列的长度。假设字符串 ss 的长度为 nn,则只有当 0 \le i \le j < n0≤i≤j<n 时,才会有 \textit{dp}[i][j] > 0dp[i][j]>0,否则 \textit{dp}[i][j] = 0dp[i][j]=0。

由于任何长度为 11 的子序列都是回文子序列,因此动态规划的边界情况是,对任意 0 \le i < n0≤i<n,都有 \textit{dp}[i][i] = 1dp[i][i]=1。

当 i < ji<j 时,计算 \textit{dp}[i][j]dp[i][j] 需要分别考虑 s[i]s[i] 和 s[j]s[j] 相等和不相等的情况:

如果 s[i] = s[j]s[i]=s[j],则首先得到 ss 的下标范围 [i+1, j-1][i+1,j−1] 内的最长回文子序列,然后在该子序列的首尾分别添加 s[i]s[i] 和 s[j]s[j],即可得到 ss 的下标范围 [i, j][i,j] 内的最长回文子序列,因此 \textit{dp}[i][j] = \textit{dp}[i+1][j-1] + 2dp[i][j]=dp[i+1][j−1]+2;

如果 s[i] \ne s[j]s[i]

​
=s[j],则 s[i]s[i] 和 s[j]s[j] 不可能同时作为同一个回文子序列的首尾,因此 \textit{dp}[i][j] = \max(\textit{dp}[i+1][j], \textit{dp}[i][j-1])dp[i][j]=max(dp[i+1][j],dp[i][j−1])。

由于状态转移方程都是从长度较短的子序列向长度较长的子序列转移,因此需要注意动态规划的循环顺序。

最终得到 \textit{dp}[0][n-1]dp[0][n−1] 即为字符串 ss 的最长回文子序列的长度。


class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = n - 1; i >= 0; i--) {
dp[i][i] = 1;
char c1 = s.charAt(i);
for (int j = i + 1; j < n; j++) {
char c2 = s.charAt(j);
if (c1 == c2) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
}

面试题

继续二叉树的阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码//构建节点类
package tree;

public class node {
public int val;
public node left;//左孩子
public node right;//右孩子
public node() {

}
public node(int val,node left,node right) {
this.val=val;
this.left=left;
this.right=right;
}
public node(int val, node left) {
super();
this.val = val;
this.left = left;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package tree;

public class nodeNum {
//全部节点个数
public static int nodenum(node root) {
if(root==null)return 0;
int left=nodenum(root.left);
int right=nodenum(root.right);

return left+right+1;
}
//叶子节点个数
public static int leafNodeNum(node root) {
if(root==null)return null;
if(root.left==null&&root.right==null) {
return 1;
}
return leafNodeNum(root.left)+leafNodeNum(root.right);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring全家桶之Spring核心篇,深度分析IoC以及A

发表于 2021-11-15

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」。

👨‍🎓作者:Java学术趴

🏦仓库:Github、Gitee

✏️博客:CSDN、掘金、InfoQ、云+社区

💌公众号:Java学术趴

🚫特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系小编授权。

🙏版权声明:文章里的部分文字或者图片来自于互联网以及百度百科,如有侵权请尽快联系小编。微信搜索公众号Java学术趴联系小编。

☠️每日毒鸡汤:微笑拥抱每一天,做像向日葵般温暖的女子。

👋大家好!我是你们的老朋友Java学术趴。最近小编又在整了Spring全家桶笔记,笔记会每天定时的进行发放,喜欢的大佬们欢迎收藏点赞关注呦。小编会每天分享的呦。Spring 框架不局限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。Spring 框架还是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力。

第一章 IoC控制反转

1.1 控制反转的概念

  • 控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代 码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对 象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值, 依赖的管理。
  • 控制: 创建对象,对象的属性值赋值,对象之间的关系管理。
  • 反转: 把原来的开发人员管理,创建对象的权限转移交给代码之外的容器实现。由容器代替开发人员管理对象。创建对象,给属性赋值。
  • 正转: 把原来的开发人员管理,创建对象的权限转移给代码之外的容器实现。由容器代替开发人员管理对象,创建对象,给属性赋值。
  • 容器: 是一个服务软件,一个框架(Spring)
  • Ioc 的实现:
+ 依赖查找:DL( Dependency Lookup ),容器提供回调接口和上下文环境给组件。
+ 依赖注入:DI(Dependency Injection),程序代码不做定位查询,这些工作由容器自行完成。**Spring 框架使用依赖注入(DI)实现 IoC。**

Java中创建对象的方式

  • 构造方法,new Student()
  • 反射 :Class
  • 序列化 :数据库
  • 克隆 : Clone
  • 动态代理 :AOP
  • IoC : 控制反转

IoC的体现

之前学习到的应用控制反转的实例:Servlet对象的创建管理,这一工作完全交给了Web容器。

Tomcat就是一个容器:里面存放有Servlet对象、Listener对象、Filter对象等。

IoC的技术实现

  • DI是IoC的技术实现,DI(Dependency Injection) : 依赖注入,只需要在程序中提供要使用的对象名就可以,至于对象如何在容器中创建、赋值、查找都由容器内部实现。
  • Spring是使用DI实现了IoC的功能,Spring底层创建对象,是使用的反射机制

第三章 AOP 面向切面编程

3.1 动态代理

动态代理的分类

  • jdk动态代理:使用jdk中的Proxy、Method、InvocationHanderl创建代理对象。jdk动态代理要求目标类必须实现接口。要求:目标类中必须实现接口
  • cglib动态代理:第三方工具,创建代理对象,原理是继承。通过继承目标类,创建子类,子类就是代理对象。要求:目标类必须能够继承,目标类不能是final的,方法也不能是final的。其子类就是代理对象。

动态代理的作用:

  • 在目标类源代码不改变的情况下,增加新的功能(即方法)
  • 减少代码的重复
  • 专注业务逻辑代码
  • 解耦合,让你的业务红能和日志,事务与非事务功能分离

3.2 AOP面向切面编程

  • AOP就是基于动态代理的,可以使用jdk、cglib两种代理模式。AOP就是动态代理的规范化,把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,使用动态代理。它的功能和动态代理是一样的。

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。

AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB 的动态代理。

所谓的切面:就是给目标类增加的额外的功能。

切面的特点:一般都是与业务无关的功能,并且这个功能可以独立使用。

精准概念:

  • AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程 序的可重用性,同时提高了开发的效率。

怎么理解面向切面编程

  • 需要在分析项目功能时,找出切面(重点)
  • 合理的安排切面的执行时间(在目标放前,还是目标方法之后。)
  • 合理的安排切面执行的位置,在哪个类,哪个方法中增加切面。

针对AOP切面编程的分析

  • 面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到 主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、 事务、日志、缓存等。 若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样, 会使主业务逻辑变的混杂不清。
  • 例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事 务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占 比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大 大干扰了主业务逻辑—转账。

面向切面编程有什么好处

  • 减少重复代码的书写
  • 专业业务的开发

注意:面向切面编程只是面向对象的一种补充。

使用AOP减少重复代码,专注业务实现

image-20211113230022281

3.3 AOP编程术语(重点)

3.3.1 切面(Aspect)

  • 切面泛指交叉业务逻辑。上例中的事务处理、日志处理就可以理解为切面。常用的切面 是通知(Advice)。实际就是对主业务逻辑的一种增强。切面就是表示功能的增强,就是一堆实现某个功能的代码,并且这个功能与业务没有关系。可以独立运行,时独立于业务存在的一段功能代码。常见的切面:日志、事务、统计信息、参数检查、权限验证等。

3.3.2 连接点(JoinPoint)

  • 连接点指可以被切面织入的具体方法,通常业务接口中的方法均为连接点。连接业务方法和切面的位置,就是某个业务接口中的业务方法。

3.3.3 切入点(Pointcut)

  • 切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。

注意:被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不 能被增强的。

3.3.4 目标对象(Target)

  • 目 标 对 象 指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象 。 上 例 中 的 StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然, 不被增强,也就无所谓目标不目标了。

3.3.5 通知(Advice)

  • 通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理 解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方 法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。

切入点定义切入的位置,通知定义切入的时间。

3.4 AOP实现框架

3.4.1 一个切面的三个关键要素

  • 切面的功能代码:代表切面干什么了
  • 切面的执行位置 ,使用Pointcut表示切面执行的位置
  • 切面的执行时间,使用Advice表示时间,在目标放之前,还是在目标方法之后。

3.4.2 AOP技术的实现

AOP是一个规范,是动态代理的一个规范化,一个标准。

AOP的技术实现框架的技术(两种)

  • spring : spring在内部实现了aop规范,可以完成aop的工作。spring主要在事务处理的时候使用aop。在实际开发中,很少使用spring的aop实现。因为spring的aop比较笨重。
  • aspectJ : 一个开源的专门用来做aop的框架。spring框架中继承了aspectJ 框架,通过spring就可以使用aspectJ框架的功能。

3.5 AspectJ 框架

3.5.1 AspectJ介绍

  • 对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向 切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便, 而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框 架中。

在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。

  • AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。AspectJ 是Eclipse基金会的一个开源项目

image-20211114143205314

  • a seamless aspect-oriented extension to the Javatm programming language(一种基于 Java 平台 的面向切面编程的语言)
  • Java platform compatible(兼容 Java 平台,可以无缝扩展)
  • easy to learn and use(易学易用)

3.5.2 AspectJ实现aop的两种方式

  • 使用xml的配置文件:配置全局事务
  • 使用注解,实际开发中用到的就是注解的方式。aspectJ有5个注解。

3.5.3 AspectJ框架的使用方式

  • 切面的执行时间:这个执行时间在规范中叫做Advice(通知、增强)。在aspectJ框架中使用注解表示。也可以使用xml配置文件中的标签表示。
+ @Before (前置通知)
+ @AfterReturning (后置通知)
+ @Around (环绕通知)
+ @AfterThrowing (异常通知)
+ @After (最终通知)**以上的五个注解都是表示切面执行的时间。**
  • 切面的执行位置: 使用切入点表达式

3.5.4 AspectJ的通知类型(了解)

AspectJ中常用的五种类型:

  • 前置通知 (@Before)
  • 后置通知 (@AfterReturning)
  • 环绕通知 (@Around)
  • 异常通知 (@AfterThrowing)
  • 最终通知 (@After)

3.5.5 切入点表达式语法(指定切入点的位置)

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:

1
2
3
4
5
6
7
scss复制代码// 参数之间使用空格分开
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
​
// 以上的4个部分,注意这个参数只写参数的类型吗,而不写参数的形参值。
execution(访问权限 方法返回值 包名.类名.方法名称(方法的参数类型) 异常类型)

解释AspectJ参数信息

  • modifiers-pattern :访问权限类型
  • ret-type-pattern : 返回值类型
  • declaring-type-pattern :包名类名
  • name-pattern(param-pattern) : 方法名(参数类型和参数个数)
  • throws-pattern : 抛出异常类型

注意:? 代表可选的部分。也就是上面没有标粗的参数。

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就 是方法的签名。

3.5.6 切入点表达式使用同配符

在AspectJ可以使用通配符的目的 :为了可以在配置文件中使用一个注解来获取到多个目标对象,然后给这些目标对象添加统一的功能或者补充其他的功能。

举列说明:(只有返回值类型以及方法名(参数)这两个参数不可以省略,所在在简化的切点表达中肯定存在这两个参数的信息)

  • *execution(public \ (..))* : 指定切入点的位置,任意公共的方法。
  • *execution( set(…)) :*\ 指定切入点的位置,任意一个以 “set” 开始的方法。
  • *execution( com.yunbocheng.service.* .*(..)) :** 指定切入点的位置是service包中的任意类中的任意方法。
  • *execution( com.yunbocheng.service..* .(..)):*\ 指定切入点的位置是 service包或者子包中的任意类的任意方法。”..”出现在类名中时,后面必须跟”*”,表示包、子包下的所有类。
  • *execution( ..service. \ .(..)):*\ 指定切入点的位置是:多级包下的service子包下所有类(接口)中所有方法为切入点。
  • *execution( .service. \ .(..)):*\ 指定切入点的位置是:一级包下的service子包下所有类(接口)中所有方法为切入点。
  • *execution( joke(String,)) :*\ 指定所有的包中的 joke() 方法,该方法的第一个参数为String,第二个参数可以是任意类型。如:joke(String s1,String s2) 、joke(String s1,double d)
  • *execution( joke(String,..)):\ 所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且 参数类型不限,如 joke(String s1)、joke(String s1,String s2)和 joke(String s1,double d2,String s3) 都是。

今天就先分享到这里啦,明天小编继续给大家分享Spring全家桶笔记!!

以上项目的源代码,点击星球进行免费获取 星球 (Github地址)如果没有Github的小伙伴儿。可以关注本人微信公众号:Java学术趴,发送Spring,免费给发给大家项目源码,代码是经过小编亲自测试的,绝对可靠。免费拿去使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【设计模式系列】模板方法模式

发表于 2021-11-15

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

前言

假设我们需要建造一座房子,需要的步骤有:建地基->砌墙->盖房顶。

我们的需求是需要建水泥房,还要建木头房,那么对应的步骤可能需要处理的逻辑不同,但是执行步骤是固定的,那么固定执行的步骤可以作为方法模板定义。

模板方法定义

模板方法是一种行为设计模式。模板方法设计模式用于创建方法执行模板,并将一些实现步骤推迟到子类。

模板方法定义了方法执行的步骤,它可以提供所有或部分子类通用的默认实现。

模板方法超类

我们来完成最前面的例子,建房子需要做的步骤有:建地基、砌墙和盖房顶。重要的一点是,我们不能改变执行顺序,因为我们不能在建地基之前盖房顶嘛。在这种情况下,我们可以创建一个模板方法,使用不同的方法来构建房子。

现在,盖房子的地基对于所有类型的房子都是一样的,无论是木房子还是水泥房子。我们可以提供基本实现,如果子类想重写这个方法也可以,但是多数情况下它对所有类型的房子都是通用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码/**
* @author 小黑说Java
* @ClassName HouseBuildTemplate
* @Description 修建房屋模板
* @date 2021/11/14
**/
public abstract class HouseTemplate {

// 建房子的模板
public final void buildHouse() {
// 1.建地基
buildFoundation();
// 2.砌墙
buildWalls();
// 3.盖房顶
buildRoof();
System.out.println("房子修建完毕。");
}

private void buildFoundation() {
System.out.println("建地基~");
}

protected abstract void buildWalls();

protected abstract void buildRoof() ;
}

为了确保子类不会重写模板方法,应该将buildHouse()其设置为final的。

模板方法实现类

由于我们需要建木头房子和水泥房子,一些方法是需要由子类实现,所以必须将基类设置为抽象类。

木头房

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class WoodHouse extends HouseTemplate {
@Override
protected void buildWalls() {
System.out.println("用木头砌墙");
}

@Override
protected void buildRoof() {
System.out.println("盖木头房顶");
}
}

水泥房

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* @author 小黑说Java
* @ClassName CementHouse
* @Description 水泥房
* @date 2021/11/14
**/
public class CementHouse extends HouseTemplate {
@Override
protected void buildWalls() {
System.out.println("修水泥墙~");
}

@Override
protected void buildRoof() {
System.out.println("盖水泥房顶");
}
}

模板方法客户端

让我们用一个测试程序来测试模板方法模式示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* @author 小黑说Java
* @ClassName TemplateMethodTest
* @date 2021/11/14
**/
public class TemplateMethodTest {
public static void main(String[] args) {
// 使用模板方法
WoodHouse woodHouse = new WoodHouse();
woodHouse.buildHouse();
System.out.println("-----------------");
CementHouse cementHouse = new CementHouse();
cementHouse.buildHouse();
}
}

注意,客户端正在调用基类的模板方法,根据不同步骤的实现,它使用了来自基类的一些方法和来自子类的一些方法。

运行结果:

模板方法类图

JDK中的模板方法模式

  • java.io.InputStream,java.io.OutputStream,java.io.Reader,java.io.Writer中的非抽象方法。
  • java.util.AbstractList,java.util.AbstractSet,java.util.AbstractMap中的非抽象方法。

模板方法设计模式要点

  1. 模板方法由固定的步骤组成,模板方法应该是final的,对于某些步骤,基类和子类的实现可以是不同的;
  2. 大多数时候,子类调用父类中的方法,但在模板方法模式中,超类模板方法调用子类中的方法;
  3. 基类的默认实现方法称为hooks,他们是为了被子类覆盖,如果你想要一些不覆盖的方法,可以将方法修饰为final。

以上就是模板方法模式的全部内容,如果对你有帮助,点个赞吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据库连接池到底应该设多大?这次直接从100ms优化到3ms

发表于 2021-11-15

我在研究HikariCP(一个数据库连接池)时无意间在HikariCP的Github wiki上看到了一篇文章(即前面给出的链接),这篇文章有力地消除了我一直以来的疑虑,看完之后感觉神清气爽。故在此做译文分享。

接下来是正文

数据库连接池的配置是开发者们常常搞出坑的地方,在配置数据库连接池时,有几个可以说是和直觉背道而驰的原则需要明确。

1万并发用户访问

想象你有一个网站,压力虽然还没到Facebook那个级别,但也有个1万上下的并发访问——也就是说差不多2万左右的TPS。那么这个网站的数据库连接池应该设置成多大呢?结果可能会让你惊讶,因为这个问题的正确问法是:

  • “这个网站的数据库连接池应该设置成多小 呢?”

下面这个视频是Oracle Real World Performance Group发布的,请先看完:www.dailymotion.com/video/x2s8u…

(因为这视频是英文解说且没有字幕,我替大家做一下简单的概括:) 视频中对Oracle数据库进行压力测试,9600并发线程进行数据库操作,每两次访问数据库的操作之间sleep 550ms,一开始设置的中间件线程池大小为2048:

图片

图片

初始的配置

压测跑起来之后是这个样子的:

图片

图片

2048连接时的性能数据

每个请求要在连接池队列里等待33ms,获得连接后执行SQL需要77ms

此时数据库的等待事件是这个熊样的:

图片

图片

各种buffer busy waits

各种buffer busy waits,数据库CPU在95%左右(这张图里没截到CPU)

接下来,把中间件连接池减到1024(并发什么的都不变),性能数据变成了这样:

图片

图片

连接池降到1024后

获取链接等待时长没怎么变,但是执行SQL的耗时减少了。下面这张图,上半部分是wait,下半部分是吞吐量

图片

图片

wait和吞吐量

能看到,中间件连接池从2048减半之后,吐吞量没变,但wait事件减少了一半。

接下来,把数据库连接池减到96,并发线程数仍然是9600不变。

图片

图片

96个连接时的性能数据

队列平均等待1ms,执行SQL平均耗时2ms。

图片

图片

wait事件几乎没了,吞吐量上升。

没有调整任何其他东西,仅仅只是缩小了中间件层的数据库连接池,就把请求响应时间从100ms左右缩短到了3ms。

But why?

为什么nginx只用4个线程发挥出的性能就大大超越了100个进程的Apache HTTPD?回想一下计算机科学的基础知识,答案其实是很明显的。

即使是单核CPU的计算机也能“同时”运行数百个线程。但我们都[应该]知道这只不过是操作系统用时间分片玩的一个小把戏。一颗CPU核心同一时刻只能执行一个线程,然后操作系统切换上下文,核心开始执行另一个线程的代码,以此类推。给定一颗CPU核心,其顺序执行A 和B 永远比通过时间分片“同时”执行A 和B 要快,这是一条计算机科学的基本法则。一旦线程的数量超过了CPU核心的数量,再增加线程数系统就只会更慢,而不是更快。

这_几乎_就是真理了……

有限的资源

上面的说法只能说是接近真理,但还并没有这么简单,有一些其他的因素需要加入。当我们寻找数据库的性能瓶颈时,总是可以将其归为三类:CPU、磁盘、网络。把_内存_加进来也没有错,但比起_磁盘_和_网络_,内存的带宽要高出好几个数量级,所以就先不加了。

如果我们无视_磁盘_和_网络_,那么结论就非常简单。在一个8核的服务器上,设定连接/线程数为8能够提供最优的性能,再增加连接数就会因上下文切换的损耗导致性能下降。数据库通常把数据存储在磁盘上,磁盘又通常是由一些旋转着的金属碟片和一个装在步进马达上的读写头组成的。读/写头同一时刻只能出现在一个地方,然后它必须“寻址”到另外一个位置来执行另一次读写操作。所以就有了寻址的耗时,此外还有旋回耗时,读写头需要等待碟片上的目标数据“旋转到位”才能进行操作。使用缓存当然是能够提升性能的,但上述原理仍然成立。

在这一时间段(即”I/O等待”)内,线程是在“阻塞”着等待磁盘,此时操作系统可以将那个空闲的CPU核心用于服务其他线程。所以,由于线程总是在I/O上阻塞,我们可以让线程/连接数比CPU核心多一些,这样能够在同样的时间内完成更多的工作。

那么应该多多少呢?这要取决于_磁盘_。较新型的SSD不需要寻址,也没有旋转的碟片。可别想当然地认为“SSD速度更快,所以我们应该增加线程数”,恰恰相反,无需寻址和没有旋回耗时意味着更少的阻塞 ,所以更少的线程[更接近于CPU核心数]会发挥出更高的性能。只有当阻塞创造了更多的执行机会时,更多的线程数才能发挥出更好的性能 。

_网络_和_磁盘_类似。通过以太网接口读写数据时也会形成阻塞,10G带宽会比1G带宽的阻塞少一些,1G带宽又会比100M带宽的阻塞少一些。不过网络通常是放在第三位考虑的,有些人会在性能计算中忽略它们。

图片

图片

上图是PostgreSQL的benchmark数据,可以看到TPS增长率从50个连接数开始变缓。在上面Oracle的视频中,他们把连接数从2048降到了96,实际上96都太高了,除非服务器有16或32颗核心。

计算公式

下面的公式是由PostgreSQL提供的,不过我们认为可以广泛地应用于大多数数据库产品。你应该模拟预期的访问量,并从这一公式开始测试你的应用,寻找最合适的连接数值。

连接数 = ((核心数 * 2) + 有效磁盘数)

核心数不应包含超线程(hyper thread),即使打开了hyperthreading也是。如果活跃数据全部被缓存了,那么有效磁盘数是0,随着缓存命中率的下降,有效磁盘数逐渐趋近于实际的磁盘数。这一公式作用于SSD时的效果如何尚未有分析。

按这个公式,你的4核i7数据库服务器的连接池大小应该为((4 * 2) + 1) = 9。取个整就算是是10吧。是不是觉得太小了?跑个性能测试试一下,我们保证它能轻松搞定3000用户以6000TPS的速率并发执行简单查询的场景。如果连接池大小超过10,你会看到响应时长开始增加,TPS开始下降。

笔者注:这一公式其实不仅适用于数据库连接池的计算,大部分涉及计算和I/O的程序,线程数的设置都可以参考这一公式。我之前在对一个使用Netty编写的消息收发服务进行压力测试时,最终测出的最佳线程数就刚好是CPU核心数的一倍。

公理:你需要一个小连接池,和一个充满了等待连接的线程的队列

如果你有10000个并发用户,设置一个10000的连接池基本等于失了智。1000仍然很恐怖。即是100也太多了。你需要一个10来个连接的小连接池,然后让剩下的业务线程都在队列里等待。连接池中的连接数量应该等于你的数据库能够有效同时进行的查询任务数(通常不会高于2*CPU核心数)。

我们经常见到一些小规模的web应用,应付着大约十来个的并发用户,却使用着一个100连接数的连接池。这会对你的数据库造成极其不必要的负担。

请注意

连接池的大小最终与系统特性相关。

比如一个混合了长事务和短事务的系统,通常是任何连接池都难以进行调优的。最好的办法是创建两个连接池,一个服务于长事务,一个服务于短事务。

再例如一个系统执行一个任务队列,只允许一定数量的任务同时执行,此时并发任务数应该去适应连接池连接数,而不是反过来。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我去,Java IO 也太上头了,,,

发表于 2021-11-15

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!”

PS:为了能够帮助更多的 Java 初学者,已将《Java 程序员进阶之路》开源到了 GitHub(本篇已收录)。该专栏目前已经收获了 590 枚星标,如果你也喜欢这个专栏,觉得有帮助的话,可以去点个 star,冲 1000 星标了,这样也方便以后进行更系统化的学习!

GitHub 地址:github.com/itwanger/to…

码云地址:gitee.com/itwanger/to…

好久没搞过 IO 了,老王看到这幅思维导图也是吃了一惊。想想也是,他当初学习 Java IO 的时候头也大,乌央乌央的一片,全是类,估计是所有 Java 包里面类最多的,一会是 Input 一会是 Output,一会是 Reader 一会是 Writer,真不知道 Java 的设计者是怎么想的。

看着肺都快要气炸的小二,老王深深地吸了一口气,耐心地对小二说:“主要是 Java 的设计者考虑得比较多吧,所以 IO 给人一种很乱的感觉,我来给你梳理一下。”

01、传输方式划分

就按照你的那副思维导图来说吧。

传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧?

字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。

字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。

通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。

具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。

PS:关于字符编码,可以看前面的章节:锟斤拷

明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。

字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。

字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。

换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。

虽然 IO 类很多,但核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer。

(抽象大法真好)

虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。

InputStream 类

  • int read():读取数据
  • int read(byte b[], int off, int len):从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中
  • long skip(long n):跳过指定个数的字节
  • int available():返回可读的字节数
  • void close():关闭流,释放资源

OutputStream 类

  • void write(int b): 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲)
  • void write(byte b[], int off, int len): 将数组 b 中的从 off 位置开始,长度为 len 的字节写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

Reader 类

  • int read():读取单个字符
  • int read(char cbuf[], int off, int len):从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中
  • long skip(long n):跳过指定个数的字符
  • int ready():是否可以读了
  • void close():关闭流,释放资源

Writer 类

  • void write(int c): 写入一个字符
  • void write( char cbuf[], int off, int len): 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

理解了上面这些方法,基本上 IO 的灵魂也就全部掌握了。

二、操作对象划分

小二,你细想一下,IO IO,不就是输入输出(Input/Output)嘛:

  • Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等
  • Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。

所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。

文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。

1)文件

文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。

FileInputStream 的例子:

1
2
3
4
5
6
7
8
java复制代码int b;
FileInputStream fis1 = new FileInputStream("fis.txt");
// 循环读取
while ((b = fis1.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fis1.close();

FileOutputStream 的例子:

1
2
3
java复制代码FileOutputStream fos = new FileOutputStream("fos.txt");
fos.write("沉默王二".getBytes());
fos.close();

FileReader 的例子:

1
2
3
4
5
6
7
8
9
java复制代码int b = 0;
FileReader fileReader = new FileReader("read.txt");
// 循环读取
while ((b = fileReader.read())!=-1) {
// 自动提升类型提升为 int 类型,所以用 char 强转
System.out.println((char)b);
}
// 关闭流
fileReader.close();

FileWriter 的例子:

1
2
3
4
java复制代码FileWriter fileWriter = new FileWriter("fw.txt");
char[] chars = "沉默王二".toCharArray();
fileWriter.write(chars, 0, chars.length);
fileWriter.close();

当掌握了文件的输入输出,其他的自然也就掌握了,都大差不差。

2)数组

通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流。

ByteArrayInputStream 的例子:

1
2
3
4
5
6
7
8
9
10
11
java复制代码InputStream is =new BufferedInputStream(
new ByteArrayInputStream(
"沉默王二".getBytes(StandardCharsets.UTF_8)));
//操作
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=is.read(flush))){
System.out.println(new String(flush,0,len));
}
//释放资源
is.close();

ByteArrayOutputStream 的例子:

1
2
3
4
5
6
7
java复制代码ByteArrayOutputStream bos =new ByteArrayOutputStream();
byte[] info ="沉默王二".getBytes();
bos.write(info, 0, info.length);
//获取数据
byte[] dest =bos.toByteArray();
//释放资源
bos.close();

3)管道

Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。

一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8));
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=pipedInputStream.read(flush))){
System.out.println(new String(flush,0,len));
}

pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}

}
});
thread1.start();
thread2.start();

4)基本数据类型

基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。

DataInputStream 提供了一系列可以读基本数据类型的方法:

1
2
3
4
5
6
7
8
9
java复制代码DataInputStream dis = new DataInputStream(new FileInputStream(“das.txt”)) ;
byte b = dis.readByte() ;
short s = dis.readShort() ;
int i = dis.readInt();
long l = dis.readLong() ;
float f = dis.readFloat() ;
double d = dis.readDouble() ;
boolean bb = dis.readBoolean() ;
char ch = dis.readChar() ;

DataOutputStream 提供了一系列可以写基本数据类型的方法:

1
2
3
4
5
6
7
8
9
java复制代码DataOutputStream das = new DataOutputStream(new FileOutputStream(“das.txt”));
das.writeByte(10);
das.writeShort(100);
das.writeInt(1000);
das.writeLong(10000L);
das.writeFloat(12.34F);
das.writeDouble(12.56);
das.writeBoolean(true);
das.writeChar('A');

5)缓冲

CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。

为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。

缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。

6)打印

恐怕 Java 程序员一生当中最常用的就是打印流了:System.out 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。

1
java复制代码System.out.println("沉默王二是真的二!");

PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 print()/println() 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。

1
2
3
4
5
java复制代码StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("沉默王二");
}
System.out.println(buffer.toString());

7)对象序列化/反序列化

序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。

1
2
3
4
5
java复制代码ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
output.writeUTF("沉默王二");
}
System.out.println(Arrays.toString(buffer.toByteArray()));

与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。

1
2
3
4
java复制代码try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File("Person.txt")))) {
String s = input.readUTF();
}

8)转换

InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。

1
2
3
4
5
6
java复制代码InputStreamReader isr = new InputStreamReader(
new FileInputStream("demo.txt"));
char []cha = new char[1024];
int len = isr.read(cha);
System.out.println(new String(cha,0,len));
isr.close();

OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。

1
2
3
4
java复制代码File f = new File("test.txt") ;
Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字节流变为字符流
out.write("hello world!!") ; // 使用字符流输出
out.close() ;

“小二啊,你看,经过我的梳理,是不是感觉 IO 也没多少东西!针对不同的场景、不同的业务,选择对应的 IO 流就可以了,用法上就是读和写。”老王一口气讲完这些,长长的舒了一口气。

此时此刻的小二,还沉浸在老王的滔滔不绝中。不仅感觉老王的肺活量是真的大,还感慨老王不愧是工作了十多年的“老油条”,一下子就把自己感觉头大的 IO 给梳理得很清晰了。

这是《Java 程序员进阶之路》专栏的第 68 篇。Java 程序员进阶之路,该专栏风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。

GitHub 地址:github.com/itwanger/to…

码云地址:gitee.com/itwanger/to…

亮白版和暗黑版的 PDF 也准备好了呢,让我们一起成为更好的 Java 工程师吧,一起冲!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springboot项目中使用shiro 自定义过滤器和to

发表于 2021-11-15

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

实现步骤主要是以下几步:

  1. 在项目中导入maven依赖

1
2
3
4
5
6
7
8
9
10
pom复制代码		<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
  1. shiro的核心配置类和代码

  1. 自定义(令牌实体)token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.ratel.fast.modules.sys.oauth2;
import org.apache.shiro.authc.AuthenticationToken;
/**
* token
*/
public class OAuth2Token implements AuthenticationToken {
private String token;

public OAuth2Token(String token){
this.token = token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
  1. token的生成工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码package com.ratel.fast.modules.sys.oauth2;
import com.ratel.fast.common.exception.RRException;
import java.security.MessageDigest;
import java.util.UUID;
/**
* 生成token
*/
public class TokenGenerator {
public static String generateValue() {
return generateValue(UUID.randomUUID().toString());
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();

public static String toHexString(byte[] data) {
if(data == null) {
return null;
}
StringBuilder r = new StringBuilder(data.length*2);
for ( byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
public static String generateValue(String param) {
try {
MessageDigest algorithm = MessageDigest.getInstance("MD5");
algorithm.reset();
algorithm.update(param.getBytes());
byte[] messageDigest = algorithm.digest();
return toHexString(messageDigest);
} catch (Exception e) {
throw new RRException("生成Token失败", e);
}
}
}
  1. 自定义的认证源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
java复制代码package com.ratel.fast.modules.sys.oauth2;
import com.ratel.fast.modules.sys.service.ShiroService;
import com.ratel.fast.modules.sys.entity.SysUserEntity;
import com.ratel.fast.modules.sys.entity.SysUserTokenEntity;
import com.ratel.fast.modules.sys.service.ShiroService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Set;

/**
* 认证
*
*
*/
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private ShiroService shiroService;

@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}

/**
* 授权(验证权限时调用)
* 前端在请求带@RequiresPermissions注解 注解的方法时会调用 doGetAuthorizationInfo 这个方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
Long userId = user.getUserId();

//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);

SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}

/**
* 认证(登录时调用)
* 每次请求的时候都会调用这个方法验证token是否失效和用户是否被锁定
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();

//根据accessToken,查询用户信息
SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
//token失效
if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
throw new IncorrectCredentialsException("token失效,请重新登录");
}

//查询用户信息
SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
//账号锁定
if(user.getStatus() == 0){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
return info;
}
}
  1. 自定义的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
java复制代码

package com.ratel.fast.modules.sys.oauth2;

import com.google.gson.Gson;
import com.ratel.fast.common.utils.HttpContextUtils;
import com.ratel.fast.common.utils.R;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* oauth2过滤器
*
*
*/
public class OAuth2Filter extends AuthenticatingFilter {

@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);

if(StringUtils.isBlank(token)){
return null;
}

return new OAuth2Token(token);
}

/**
* 判断用户是否已经登录,
* 如果是options的请求则放行,否则进行调用onAccessDenied进行token认证流程
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}

return false;
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token)){
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());

String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));

httpResponse.getWriter().print(json);

return false;
}

return executeLogin(request, response);
}

@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());

String json = new Gson().toJson(r);
httpResponse.getWriter().print(json);
} catch (IOException e1) {

}

return false;
}

/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest){
//从header中获取token
String token = httpRequest.getHeader("token");

//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)){
token = httpRequest.getParameter("token");
}

return token;
}


}
  1. 核心配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
java复制代码

package com.ratel.fast.config;

import com.ratel.fast.modules.sys.oauth2.OAuth2Filter;
import com.ratel.fast.modules.sys.oauth2.OAuth2Realm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Shiro配置
*
*
*/
@Configuration
public class ShiroConfig {

@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm);
securityManager.setRememberMeManager(null);
return securityManager;
}

/**
* 这个bean的名字必须叫 shiroFilter ,否则启动的时候会报错
* @Bean ("shiroFilter") 之后的括号可以不用写,spring默认方法名为的bean的名字
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);

//oauth过滤
Map<String, Filter> filters = new HashMap<>();
//添加自定义过滤器
filters.put("oauth2", new OAuth2Filter());
shiroFilter.setFilters(filters);

Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/aaa.txt", "anon");
//使用自定义过滤器拦截除上边以外的所有请求
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);

return shiroFilter;
}

@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}

}
  1. 用户token类 (用于往数据库中存储的时候用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码package com.ratel.fast.modules.sys.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;

/**
* 系统用户Token
*
*
*/
@Data
@TableName("sys_user_token")
public class SysUserTokenEntity implements Serializable {
private static final long serialVersionUID = 1L;

//用户ID
@TableId(type = IdType.INPUT)
private Long userId;
//token
private String token;
//过期时间
private Date expireTime;
//更新时间
private Date updateTime;

}
  1. 用户登录使用的Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
java复制代码

package com.ratel.fast.modules.sys.controller;

import com.ratel.fast.common.utils.R;
import com.ratel.fast.modules.sys.entity.SysUserEntity;
import com.ratel.fast.modules.sys.form.SysLoginForm;
import com.ratel.fast.modules.sys.service.SysCaptchaService;
import com.ratel.fast.modules.sys.service.SysUserService;
import com.ratel.fast.modules.sys.service.SysUserTokenService;
import org.apache.commons.io.IOUtils;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Map;

/**
* 登录相关
*
*
*/
@RestController
public class SysLoginController extends AbstractController {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserTokenService sysUserTokenService;
@Autowired
private SysCaptchaService sysCaptchaService;

/**
* 验证码
*/
@GetMapping("captcha.jpg")
public void captcha(HttpServletResponse response, String uuid)throws IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");

//获取图片验证码
BufferedImage image = sysCaptchaService.getCaptcha(uuid);

ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}

/**
* 登录
*/
@PostMapping("/sys/login")
public Map<String, Object> login(@RequestBody SysLoginForm form)throws IOException {
boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
if(!captcha){
return R.error("验证码不正确");
}

//用户信息
SysUserEntity user = sysUserService.queryByUserName(form.getUsername());

//账号不存在、密码错误
if(user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) {
return R.error("账号或密码不正确");
}

//账号锁定
if(user.getStatus() == 0){
return R.error("账号已被锁定,请联系管理员");
}

//生成token,并保存到数据库
R r = sysUserTokenService.createToken(user.getUserId());
return r;
}


/**
* 退出
*/
@PostMapping("/sys/logout")
public R logout() {
sysUserTokenService.logout(getUserId());
return R.ok();
}

}

到此我们已经完成了shiro的认证过程的代码。
记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。下一篇我将介绍使用RBAC权限模型加上shiro实现一套完整的权限控制方案。

基于RBAC的权限模型+shiro+springboot实现的系统登陆权限认证模块

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…340341342…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%