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

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


  • 首页

  • 归档

  • 搜索

数据库系列之日期和时间函数 1 获取当前时刻的数据 2 日期

发表于 2021-10-28

日期是指年月日,时间是指时分秒

1 获取当前时刻的数据

1.1 获取当前时刻的日期和时间

1
scss复制代码SELECT NOW()

result:

image-20210921100418308.png

1.2 获取当前时刻的日期信息

1.2.1 获取日期

(1)通过curdate()获取

1
scss复制代码SELECT CURDATE()

result:

image-20210921100526482.png

(2) 通过now()获取

1
vbscript复制代码SELECT DATE(NOW())

result:

image-20210921100853305.png

1.2.2 获取日期中的年

1
vbscript复制代码SELECT YEAR(NOW())

result:

image-20210921103732191.png

1.2.3 获取日期中的月

1
vbscript复制代码SELECT MONTH(NOW())

result:

image-20210921103812370.png

1.2.4 获取日期中的日

1
vbscript复制代码SELECT DAY(NOW())

result:

image-20210921103906279.png

1.3 获取当前时刻的时间信息

1.3.1 获取当前时刻的时间

(1)通过curtime()获取

1
scss复制代码SELECT CURTIME()

result:

image-20210921110340864.png

(2) 通过now()获取

1
vbscript复制代码SELECT TIME(NOW())

result:

image-20210921110500647.png

1.3.2 获取当前时刻的时

1
vbscript复制代码SELECT HOUR(NOW())

result:

image-20210921110544277.png

1.3.3 获取当前时刻的分

1
vbscript复制代码SELECT MINUTE(NOW())

result:

image-20210921110623781.png

1.3.4 获取当前时刻的秒

1
vbscript复制代码SELECT SECOND(NOW())

result:

image-20210921110702181.png

1.4 获取当前时刻的周信息

1.4.1 查看当前时刻是全年中的第几周

1
vbscript复制代码SELECT WEEKOFYEAR(NOW())

result:

image-20210921110936921.png

1.4.2 查看当前时刻是周几

1
scss复制代码SELECT DAYOFWEEK(NOW())

result:

image-20210921111132292.png

1.5 获取当前时刻的季度信息

1
2
3
4
5
vbnet复制代码SELECT 
QUARTER ( "2019-01-01" ) AS quarter_1,
QUARTER ( "2019-04-01" ) AS quarter_2,
QUARTER ( "2019-07-01" ) AS quarter_3,
QUARTER ( "2019-10-01" ) AS quarter_4

result:

image-20210921111414391.png

2 日期和时间格式转换

2.1 格式转换

格式转换所使用到的是date_format()函数,其用法如下:

date_format(datetime, format)

其中,datetime表示要转换的具体的日期和时间,format表示要转换的格式

1
perl复制代码SELECT DATE_FORMAT("2019-12-25", "%Y-%m-%d")

result:

image-20210921112519686.png

1
perl复制代码SELECT DATE_FORMAT("2019-1-25", "%Y-%m-%d")

result:

image-20210921112544398.png

这里注意1和01的区别。原始日期为2019-1-25, 返回结果为2019-01-25

1
perl复制代码SELECT DATE_FORMAT("2019-1-25 12:30:45", "%H:%i:%S")

result:

image-20210921112907923.png

2.2 日期提取

这里使用到的是extract()含糊,其形式如下:

extract(unit from datetime)

datetime表示具体的日期时间,unit表示要提取的部分

unit取值如下表:

unit 说明
year 年
month 月
day 日
hour 小时
minute 分钟
second 秒
week 周数,全年第几周
1
sql复制代码SELECT  EXTRACT(year FROM "2019-12-23")

result:

image-20210921113621844.png

3 日期和时间运算

3.1 向后偏移日期和时间

向后偏移用到的是date_add()函数,其形式如下:

date_add(date, interval num unit)

date代表日期时间,interval 为固定参数,num为偏移量, unit为偏移的单位

1
2
3
4
5
6
7
sql复制代码SELECT
DATE_ADD("2021-01-01 12:34:56",INTERVAL 7 YEAR) AS yaer,
DATE_ADD("2021-01-01 12:34:56",INTERVAL 7 MONTH) AS month,
DATE_ADD("2021-01-01 12:34:56",INTERVAL 7 DAY) AS date,
DATE_ADD("2021-01-01 12:34:56",INTERVAL 7 HOUR) AS hour,
DATE_ADD("2021-01-01 12:34:56",INTERVAL 7 MINUTE) AS minute,
DATE_ADD("2021-01-01 12:34:56",INTERVAL 7 SECOND) AS second

result:

image-20210921115522575.png

3.2 向前偏移日期和时间

向后偏移用到的是date_sub()函数,其形式如下:

date_sub(date, interval num unit)

date代表日期时间,interval 为固定参数,num为偏移量, unit为偏移的单位

1
2
3
4
5
6
7
sql复制代码SELECT
DATE_SUB("2021-01-01 12:34:56",INTERVAL 7 YEAR) AS yaer,
DATE_SUB("2021-01-01 12:34:56",INTERVAL 7 MONTH) AS month,
DATE_SUB("2021-01-01 12:34:56",INTERVAL 7 DAY) AS date,
DATE_SUB("2021-01-01 12:34:56",INTERVAL 7 HOUR) AS hour,
DATE_SUB("2021-01-01 12:34:56",INTERVAL 7 MINUTE) AS minute,
DATE_SUB("2021-01-01 12:34:56",INTERVAL 7 SECOND) AS second

result:

image-20210921115704221.png

3.3 两个日期之间求差

1
vbscript复制代码SELECT DATEDIFF("2019-01-07","2019-01-01")

result:

image-20210921115929939.png

3.4 两个日期之间比较

1
2
3
4
5
vbnet复制代码SELECT
"2019-01-01" > "2019-01-02" as co11,
"2019-01-01" < "2019-01-02" as co12,
"2019-01-01" = "2019-01-02" as co13,
"2019-01-01" != "2019-01-02" as co14

result:

image-20210921120122008.png

本文转载自: 掘金

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

2021年10月国产数据库流行度排行解读 浅谈基础软件发展之

发表于 2021-10-28

2021年10月国产数据库流行度排名,与9月份的排名变化不大,TiDB依然状元,达梦一跃,超过OceanBase,排位第二,OceanBase屈居第三。同样令人意外的是孟女士回归,任正非曾经发言已经做好此生再也不能见到女儿的最坏准备,孟女士能回来,背后是国家的强大,才带来可能的希望。

最近看一本书叫《联想做大,华为做强》,挺有意思,联想和华为差不多是同期成立,联想是高干子弟,计算机研究所背景出身,而华为是寒门子弟,当时任正非走投无路创办了华为。联想的先天优势和可以获取的资源都比华为的多,联想最初自主研发汉卡做拳头产品,公司的业务得到快速发展和推广。当联想的品牌获得一定的知名度后,联想减少研发投入投入,更多关心洞悉用户需求,2001年成立联想控股,开始非主业多元化、跨领域发展,进军房地产、物流、餐饮等。开始联想走在华为前面,后来联想与华为差距越来越大,虽然至今联想在IT行业有不错的名声,但是联想已经不能与华为同日而语。华为专注通信是世界上一流的通讯基础设施解决方案提供商,著名产品包括有智能手机、终端路由器、交换机、电脑等等,而且是信息服务行业少数不多的数字化转型成功的代表企业。联想最擅长的笔记本产品相对戴尔、IBM、惠普等竞争对手在功能、性能、创新方面没有给用户太多惊喜。众所周知,笔记本的核心部件,操作系统普遍是微软,处理器普遍是英特尔的。硬件集成的技术难度不大,现在华为都能踩一脚进入笔记本行业。

信创,即信息技术应用创新。过去,国内 IT 底层标准、架构、生态等大多数都由国外 IT 巨头制定的,由此存在诸多安全风险。信创产业基于自己的 IT 底层架构和标准,形成自有开放生态,领域范围在核心芯片、基础硬件、操作系统、中间件、数据库等领域实现国产替代。

基础软件核心三部件操作系统、中间件、数据库,长期以一直被国外产品垄断。1999年 ,第一家把数据库投入商业运作的公司人大金仓成立,比起Oracle要晚接近30年。技术研发主要由国内高校支持,没有使用国外的技术成果。起步晚和技术封闭是一个原因,国有企业制度流制冗余效率低是一个原因,加上一些主管部门和单位重硬轻软,轻视软件化建设,好钢没有用在刀刃上。

科学是第一生产力,基础软件与普通软件应用软件的技术不同,基础软件更加贴近科学,是科学理论的工业实践,需要科学与技术高度融合。基础软件的前进方向在于科技创新,科技创新是原创性科学研究和技术创新的总称,通过创造和应用新知识和新技术、新工艺,或者新的生产方式和经营管理模式,开发新产品,提高产品质量,提供新服务的过程。

2020年,华为研发总费用为1418.93亿元,较前一年进一步增加,占销售收入的比重为15.9%。全球从事研究与开发的人员约10.5万名,约占公司总人数的53.4%,华为在研发上的投入已经超过了BAT的研发总和。华为坚持每年将10%以上的销售收入投入研究与开发,近10年累计研发投入超过7200亿元。作为全球最大的专利持有企业之一,截至2020年底,华为在全球共持有有效授权专利4万余件(超10万件),90%以上专利为发明专利。

众所周知,华为进入数据库领域起步晚,且看2020年的数据库厂商市场份额,阿里云排第7,华为排第10,华为研发的GaussDB,虽然基于postgresql9.2.4,修改代码内容超过75%, 完全具备自主内核可控能力,不受开源协议捆绑,没有任何商业问题 。华为线上推出的商业的分布式的GaussDB, 线下推出免费的单体的openGauss,对客户提供强大的技术支持和售后服务,因此在百华齐放的数据库红海市场杀出一条血路出来。即使有了成绩,华为继续投入研发创新,华为云GaussDB(for openGauss)最近研发推出重大内核新特性——Ustore存储引擎,可以在数据频繁更新场景下性能依旧稳如泰山,使业务系统运行更加平稳,适应更多业务场景和工作负载,特别是对性能和稳定性有更高要求的金融核心业务场景。

在软件生态建设的道路上,华为gauss通过木兰协议与中小企业广泛合作,通过技术赋能于各行业场景应用,针对不同需求提供创新的技术方案与服务,挖掘产品潜力,提升产品价值。让gauss可以广泛应用在芯片厂商、汽车电子、智能家居、智慧医疗、工业制造 等领域。

16337787031.png

基础软件是高科技行业,逆水行舟,不进则退,国产数据库当尊从其则,以技术和服务创新为理念,继续赋能行业产品化,为行业降低成本、提升效率、模式升级。在躬行实践中,走出中国人的数据库之路。

——————

墨天轮,围绕数据人的学习成长提供一站式的全面服务,打造集新闻资讯、在线问答、活动直播、在线课程、文档阅览、资源下载、知识分享及在线运维为一体的统一平台,持续促进数据领域的知识传播和技术创新。

关注官方公众号: 墨天轮、 墨天轮平台、墨天轮成长营、数据库国产化 、数据库资讯

本文转载自: 掘金

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

Mac 系统如何利用软链接在根目录创建文件夹?

发表于 2021-10-28

作者:泥瓦匠 出处:www.bysocket.com/2021-10-26/…

Mac 操作系统挺适合开发者进行写代码,最近碰到了一个问题,问题是如何在 macOS 根目录创建文件夹。不同的 macOS 版本处理方式不同,下面我们展开讲一下

一、为什么要在 Mac 根目录创建文件夹

有些场景程序需要访问根目录的特定文件夹,所以需要在 macOS 根目录创建文件夹。

比如 Spring Boot 工程在 Mac 操作系统本地运行时,公司会默指定 /home/data/log 类似的目录,来存储工程运行的日志。

那怎么如何在 macOS 根目录创建文件夹,下面分不同的 macOS 版本来解决:

  • macOS@Catalina 版本
  • macOS@Big Sur 版本

二、macOS@Catalina 版本的创建文件夹方法

第一步:关闭电脑然后重启,重启时长按 command + R 键,启动内建的 macOS 恢复系统

第二步:从菜单栏找到终端工具,运行下面命令,然后重启:

1
java复制代码csrutil disable

这个命令目的是关闭 SIP,SIP 全称为「System Integrity Protection」即「系统完整性保护」。可以通过 csrutil status 查看其 SIP 状态。

第三步:重启完后,先重新挂载根目录,打开终端工具运行下面命令即可:

1
java复制代码sudo mount -uw /

第四步:创建对应的 /Users/XXX/home/data/log 文件夹,然后将对应的文件目录软链接到根目录。运行下面命令即可:

1
java复制代码sudo ln -s /Users/XXX/home /home

注意:需要用软链接来解决,是因为在根目录直接创建文件夹的话,一旦重启电脑,之前创建的目录又是只读权限了。

最后,重新重启 command + R 键,启动内建的 macOS 恢复系统,重新打开 SIP:

1
java复制代码csrutil enable

三、macOS@Big Sur 版本的创建文件夹方法

第一步:启动内建的 macOS 恢复系统,关闭 SIP

第二步:运行下面命令,修改 synthetic.conf 文件

1
java复制代码sudo vi /etc/synthetic.conf

第三步:编辑该文件,输入下面内容,将对应的文件夹映射到根目录

1
java复制代码home    /Users/XXX/home

注意:

  • 提前创建被映射的文件夹
  • 中间是 Tab,不是空格

最后重启系统后,系统根目录就会出现了对应的文件夹,实现方式也是一个软链接的形式

四、小结

几个点可以总结出来:

  • 尽量不要再 mac 根目录操作一些命令,比如 chmod 等
  • SIP 打开后,及时关闭
  • mac 支持文件软链接形式

作者:泥瓦匠 (公号「程序员泥瓦匠」)出处:www.bysocket.com 欢迎转载,也请保留这段声明。谢谢!

本文转载自: 掘金

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

mysql分表之后怎么平滑上线? 唠叨一句

发表于 2021-10-28

分表的目的

项目开发中,我们的数据库数据越来越大,随之而来的是单个表中数据太多。以至于查询数据变慢,而且由于表的锁机制导致应用操作也受到严重影响,出现了数据库性能瓶颈。

当出现这种情况时,我们可以考虑分表,即将单个数据库表进行拆分,拆分成多个数据表,然后用户访问的时候,根据一定的算法,让用户访问不同的表,这样数据分散到多个数据表中,减少了单个数据表的访问压力。提升了数据库访问性能。

举个栗子

举个栗子

举个栗子

比如咱们最常见的用户表(user表)

id

user_id

其他字段

主键id

用户id

其他字段

咱们一般都会用user_id去查询对应的用户信息,但是随着业务的增长,这张表会越来越大,甚至上亿,严重影响了查询性能。 所以咱们就会对这张表进行分表处理,分到多张表减小查询压力

分表策略

以分10张表为例(具体分多少张表 根据实际情况来估算) 首先咱们建10张表 user1、user2、user3。。。。。user10

一般情况下,我们都会用作为索引的字段(user_id)进行取模处理。想分多少张表,就按照多少取模,比如这个case就是10

1
ini复制代码$table_name = $user_id % 10;

按照上面的取模公式

  • user_id为1295的会落在user5里面
  • user_id为8634的会落在user4里面
  • 。。。。。。。

「每次CURD根据上面查找表的策略进行就行了」,这个问题不大,我们暂且先不多说。

已经上线的运行中的表怎么办?

其实上面的方法大家应该都知道怎么用,但是有个问题,已经上线了的表怎么办?那张表的数据在线上是一直被查找或者改变的。如何能够进行平滑的分表,并且让用户无感知呢?

方法1

直接上线,提前写个脚本,脚本内容是把旧表(user)的数据同步到user1表到user10表,一上线了赶紧执行

这种方法明显是行不通的,主要是存在以下问题

  • 如果执行过程中脚本有问题怎么办?代码全部回滚?
  • 脚本把把旧表(user)的数据同步到user1表到user10表,这个脚本得执行多久? 如果是1个小时,那么这段时间线上和这张表相关的业务都是不正常的

这显然是行不通的,对线上影响很大。

方法2

先写个同步数据的脚本,脚本内容是把旧表(user)的数据同步到user1表到user10表,脚本同步完了再上线。

这个方法看起来友好了一些,不过也存在一些问题。

  • 脚本同步完,立即上线,这两件事之间是有一些时间差的,这个时间差中线上表可能有一些改动,这些改动怎么办?

「以上两种方法看起来貌似都行不通,所以看来得来点不一样的了。咱们直接看结论。」

步骤1 上线双写

首先咱们把双写上线了,什么意思呢?比如user_id=123,对于增加,删除,修改操作来说,咱们既操作user表,也操作user_id=123对应的user3表。

1
bash复制代码function modify($user_id){  //包含增加,删除,修改操作  modify_user();  //modify user表  $table_name = $user_id % 10;  modify_user($table_name) //modify对应的分表}

因为查询的部分还是在user表中查询的,所以上面的操作对线上用户是无任何影响的。

步骤2 全量同步

写一个全量同步user表到user1-user10的表,最好找个低峰期执行脚本,以防万一影响user表的查询

这一步执行之后,因为咱们之前上线了双写(见步骤1),所以user表和user1-user10表之间的数据已经是完全一致的了。

步骤3 查询新表数据

将查询的部分改到user1-user10

因为前面两个步骤咱们已经保证了user表和各个分表之间的数据完全一致性,所以直接把查询的部分改掉是没有任何问题的

如果按照以上步骤执行,那么对线上的数据是没有任何影响的,而且我们线上就是这么操作了,经过了多次实践确保不会出问题,放心使用即可

唠叨一句

大家好,我是小饭,一枚后端工程师。如果觉得文章对你有一点点帮助,欢迎分享给你的朋友,也给小饭点个赞,下面是我的公众号,打开微信搜索“程序员小饭”就可以看到,感兴趣可以关注,这是小饭坚持下去的动力,谢谢大家,我们下次见!

本文转载自: 掘金

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

20张图!常见分布式理论与解决方案

发表于 2021-10-28

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

对于分布式系统,最简单的理解就是一堆机器对外提供服务,相比单体服务,它可以承受更高的负载,但是分布式系统也带了一系列问题,比如如何解决某个节点故障的问题?如何解决数据一致性的问题?如何解决数据倾斜的问题?今天通过这篇文章,带大家搞懂和分布式相关的常见理论和解决方案。

CAP理论

先从定义开始:

  • C(Consistence):一致性,所有的节点访问的是最新的数据副本,这是什么意思呢?我们知道在分布式系统中,为了高可用,往往一个节点会有若干个数据副本,简称Follower节点,比较常见的模式是我们的数据更新一般会写入Leader节点,然后会同步给Follower节点,当我们读取数据的时候,不论从哪个节点读取都可以读到最新的数据,这就是一致性。


A、B、C可以得到同样的数据。

  • A(Availability):可用性,非故障节点可以正常的操作,简单来说就是客户端一直可以正常访问并得到系统的正常响应,从用户角度来看就是不会出现系统操作失败或者访问超时等问题,但是系统内部可能会出现网络延迟等问题。

C节点因为自身问题不可用,正常情况会被剔除,B节点与A节点之间可能存在同步延迟,但是B节点本身没有故障,所以B节点是可用的。

  • P(Partition tolerance):分区容错性,网络的问题错综复杂,分布式系统肯定是要考虑这一点的,如果出现某个节点因为网络等问题造成数据不一致,或者数据延迟很久才同步过来,虽然会影响部分节点数据的时效性,但是服务节点依然是可用的,分布式系统要能容忍这种情况的。

B对应的节点虽然和Leader断了联系,但是依然可以对外服务,只不过提供的是老数据。

在分布式系统中,CAP是无法同时满足的,首先由于存在多节点,并且网络传输需要时间,所以可能会存在延迟,那么节点之间的数据我们无法保证某一时刻完全一致,因此P(分区容错性)是要满足的。在P满足的情况下,为什么说CA不能同时满足呢?我们来通过假设看一看,如果CA同时满足会怎么样。

  1. 假设现在要求满足C(一致性),那么就是说所有的节点在某一刻提供的数据都必须一致,我们知道在P的情况,是不可能保证的,要保证的话,就只能把其他节点全部干掉,比如禁止读写,那这其实就是和A是相悖的(某些节点虽然延迟,但是节点本身可用)。
  2. 假设现在要求满足A(可用性),那么就是说只要节点本身没什么问题,就可以对外提供服务,哪怕有点数据延迟,很明显这肯定是和C相悖的。

在实际的业务中,我们需要根据业务的场景来决定使用CP,还是AP。比如对一些和钱挂钩的业务,数据的一致性按道理应该是最重要的,因此一般会采用CP,而对于一些不影响主体功能的业务,比如像新闻的阅读量,不同的用户看到的阅读量不一样并不会造成什么影响,可以采用AP。

BASE理论

由于CAP理论中C和A无法兼得,eBay的架构师提出了BASE理论,BASE理论主要是在CA之间做文章,它不要求强一致性,因此可以满足一定的可用性。我们还是先从定义开始:

  • BA(Basically Available):基本可用,注意这个和不可用不是一回事,在分布式系统中出现不可预估的故障时,允许损失部分可用性,保证核心功能可用,比如正常一个接口响应200ms,在出现故障时响应超过1s,虽然响应时间变长了,但是接口还是可以对外提供服务的,再比如对于一个视频网站,在突发流量到来时,把视频的弹幕服务打挂了,但是视频的播放功能依然正常。

  • S(Soft-state):软状态,即分布式系统允许存在一个中间的状态,但是这个中间状态并不会对服务造成严重的影响,比如对于主从复制这种,允许从节点短暂的延迟。

  • E(Eventually Consistent):最终一致性,由于软状态的存在,系统对延迟是可以容忍的,但是在一段时间后,延迟的数据需要最终保持一致。

总的来说,BASE理论适用性应该更广泛,很多时候我们并不要求数据的强一致性,只要在短暂的延时之后能达到一致性也是可以的。

一致性hash

hash这个词对我们来说并不陌生,以缓存服务器来说,一般会在线上配置好几台服务器,然后根据hash来决定请求哪台缓存服务,比如常见的就是取模方式 hash(key)%num 来获取目标机器。

假设现在有3台缓存服务器,并且当前有3个缓存的key,分别是k0,k1,k2,在经过hash以后,它们的分布情况如下:

1
2
3
ini复制代码hash(k0)%3=0 #No.0
hash(k1)%3=1 #No.1
hash(k2)%3=2 #No.2

很幸运,分布的非常均匀,每台机器一个。某天,由于线上要做个活动,预计访问量会加大,需要选择加一台服务器来分担压力,于是经过hash之后,k0,k1,k2的分布情况如下:

1
2
3
ini复制代码hash(k0)%4=0 #No.1
hash(k1)%4=1 #No.2
hash(k2)%4=2 #No.3

  • k0的目标缓存服务器由原本的No.0变成了No.1
  • k1的目标缓存服务器由原本的No.1变成了No.2
  • k2的目标缓存服务器由原本的No.2变成了No.3

可以发现因为添加了一台缓存节点,导致了k0,k1,k2原来的缓存全部失效了,这似乎有点问题,类似缓存雪崩,严重的话会对DB造成很大的压力,造成这个问题的主要原因是因为我们加了一个节点,导致hash结果发生了变动,此时的hash可以说是不稳定的。

为了解决rehash不稳定的问题,于是出现了一致性hash算法。一致性hash的原理比较简单,首先存在一个hash圆环,这个圆环可以存放 0-2^32-1 个节点。

  1. 第一步就是把我们的目标服务器节点通过hash映射到这个环上
  2. 第二步根据我们需要查找的key,它应该也对应hash环上的某个位置

也许你会问,这k0、k1、k2也没和某个缓存节点对上呀~,这就是一致性hash不同的地方,它此时查找的方式并不是 hash(key)=某个节点,而是根据key的位置,顺时针找到第一个节点,这个节点就是当下这个key的目标节点。

我们再来看看在一致性hash的情况下,新增一个节点会发生什么。


此时唯一的变动就是k0原本应该打到cache0节点的,现在却打到了我们新加的节点cache3上,而k1,k2是不变的,也就是说有且只有k0的缓存失效了,相比之前,大大降低了缓存失效的面积。

当然这样的节点分布算是比较理想的了,如果我们的节点是这样分布的:

几个cache节点分布的比较集中,由于顺时针查找法,所以最终k0,k1,k2都落在cache0节点上,也就是说cache1、cache2基本就是多余的,所以为了解决这种数据倾斜的问题,一致性hash又引入了虚拟节点的概念,每个节点可以有若干个虚拟节点,比如:

  1. cache0->cache0#1
  2. cache1->cache1#1
  3. cache2->cache2#1

虚拟节点并不是真正的服务节点,它只是一个影子,它的目的就是站坑位,让节点更加分散,更加均匀。


这样通过映射出虚拟节点以后,k0打到cache2,k1打到cache0,k2打到cache1,虚拟节点越多,理论分布的越均匀。

Gossip协议

集群往往是由多个节点共同组成的,当一个节点加入集群或者一个节点从集群中下线的时候,都需要让集群中其他的节点知道,这样才能将数据信息分享给新节点而忽略下线节点。


A、B、C节点之间可以互相传递消息,但是D节点在下线之后会被广播告诉其他存活节点。

这样的广播协议就是今天要说Gossip协议,Gossip协议也叫Epidemic协议(流行病协议),当一个消息到来时,通过Gossip协议就可以像病毒一样感染全部集群节点,当然我们利用的是它这个极强的散播能力。

Gossip的过程是由一个种子节点发起的,当一个种子节点有信息需要同步到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,所以不能保证某个时间点所有的节点都有该条消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

Gossip协议的特点:

  1. Gossip协议是周期性散播消息,每隔一段时间传播一次
  2. 被感染的节点,每次可以继续散播N个节点
  3. 每次散播消息时,都会选择尚未发送过的节点进行散播
  4. 收到消息的节点,不会向发送的节点散播
  5. 同一个节点可能会收到重复的消息,因为可能同时多个节点正好向它散播
  6. 集群是去中心化的,节点之间都是平等的
  7. 消息的散播不用等接收节点的ack,即消息可能会丢失,但是最终应该会被感染

我们来看个例子:

  1. 种子节点是A
  2. A节点选择B、C节点进行散播
  3. C散播到D,B散播D和E,可以发现D收到两次
  4. D散播到F,最终整个网络都同步到了消息

Gossip有点类似图的广度优先遍历算法,一般用于网络拓扑结构信息的分享和维护,像redis、consul都有使用到。

Raft算法

分布式协议的难点之一就是数据的一致性,当由多个节点组成的集群中只有一个节点收到数据,我们就算成功的话,风险太大,当要求所有节点都收到数据才响应成功,性能又太差,所以一般会在数据的安全和性能之间做个折中,只要保证绝大部分节点同步数据成功,我们就算成功,Raft算法作为比较知名的一致性算法,被广泛应用于许多中间件中,比如像etcd,接下来我们就看看Raft算法是如何工作的。

首先介绍下在Raft算法中,几种情况下每个节点对应的角色:

  1. Leader节点:同大多数分布式中的Leader节点一样,数据的变更都是通过它的
  2. Follower节点:Leader节点的追随者,负责复制数据并且在选举时候投票的节点
  3. Candidate候选节点:参与选举的节点,就是Follower节点参与选举时会切换的角色

Raft算法将一致性问题分解为两个的子问题,Leader选举和状态复制。

选举

首先我们来看看Leader的选举,系统在刚开始的时候,所有节点都为Follower节点,这时大家都有机会参与选举,也就是把自己变成Candidate,但是如果每个Follower节点都变成Candidate那么就会陷入无限的死循环,于是每个Follower都一个定时器,并且定时器的时间是随机的,当某个Follower的定时器时间走完之后,会确认当前是否存在Leader节点,如果不存在就会把自己变成Candidate,这时会投自己1票,同时告诉其它节点,让它们来投票,当拿到超过半数以上的投票时,当前的Candidate就会变成Leader节点。

  1. 由于A节点的定时器时间最短(10ms),所以A会成为Candidate
  2. A投自己一票,同时B、C也投出自己的同意票,因此A会变成Leader节点,同时会记录是第M任。这个M是做版本校验的,比如一个编号是10的节点,收到了一个编号是9的节点的投票请求,那么就会拒绝这个请求。

在Leader节点选举出来以后,Leader节点会不断的发送心跳给其它Follower节点证明自己是活着的,其他Follower节点在收到心跳后会清空自己的定时器,并回复给Leader,因为此时没必要触发选举了。

如果Leader节点在某一刻挂了,那么Follower节点就不会收到心跳,因此在定时器到来时就会触发新一轮的选举,流程还是一样,但是如果恰巧两个Follower都变成了Candidate,并且都得到了同样的票数,那么此时就会陷入僵局,为了打破僵局,这时每个Candidate都会随机推迟一段时间再次请求投票,当然一般情况下,就是先来先得,优先跑完定时器的Candidate理论成为Leader的概率更大。

好的选举流程大致如上,接下来我们来看看数据的复制。

复制

当Leader节点收到Client的请求变更时,会把变更记录到log中,然后Leader会将这个变更随着下一次的心跳通知给Follower节点,收到消息的Follower节点把变更同样写入日志中,然后回复Leader节点,当Leader收到大多数的回复后,就把变更写入自己的存储空间,同时回复client,并告诉Follower应用此log。至此,集群就变更达成了共识。

最后,Raft算法是能够实现分布式系统强一致性的算法,每个系统节点有三种状态Leader、Follower、Candidate,实现Raft算法两个最重要的事是:主的选举和日志的复制。

分布式事务

事务相信大家不陌,事务的本质是要么一起向前冲,要么一起保持不动。对于MySQL的InnoDB来说,我们只需要执行begin、commit就行,有时候我们可能需要回滚rollback。但是这是在同一数据库的前提下,如果我们的数据表分库了或者说我们要操作的资源在不同的网络节点上该怎么办?这就得用到我们今天要说的分布式事务了,分布式事务有2PC、3PC、TCC等,
但是无论哪种都无法保证完美的ACID,我们来一起看看是怎么回事吧。

2PC

从名字可以看出它是分两个阶段的,所以它也叫做二阶段提交,即准备和提交,2PC要求有个事务的协调者,相比常规的事务,我们的请求是发给这个协调者的,然后由协调者帮我们协调各个节点资源的提交。

  • 准备阶段:协调者会让各个参与事务的参与者,把除了提交之外所有的事情都干好,也就是就等着提交了
  • 提交阶段:协调者收到各个参与者的准备消息后,根据准备情况通知各个参与者提交(commit)或者回滚(rollback)

可以发现整个过程非常依赖协调者,如果协调者挂了,那么整个分布式事务就不可用,所以一般建议协调者至少有个备份节点。

如果协调者在收到所有节点的ok之后,在准备发送commit消息的时候,由于网络问题,导致其中一个节点始终收不到消息,那么收不到消息的节点就会一直占着资源不释放,出现这种情况的时候,建议协调者有个重试功能,在commit失败之后,不停的重试,直至成功。2PC协议是一种强一致性协议,它是同步阻塞的,所以在高并发的场景它的性能可能还会有问题。

3PC

2PC存在一些问题,比如协调者从挂了到恢复后并不知道当前节点的状态,现在应该做什么(是该提交还是回滚等等),还有就是当发生网络问题的时候,无法通信的节点只会傻傻的等待,造成资源一直处于锁定状态。鉴于这些问题,出现了3PC。

首先3PC顾名思义,会分为3个阶段,分别是准备阶段、预提交阶段和提交阶段。

  • 准备阶段:主要是询问参与者自身的状况,比如你的负载情况如何?能参与接下来的任务吧?
  • 预提交阶段:除了commit之外的所有准备工作,就等着commit了
  • 提交阶段:执行真正的commit或者rollback


如果在事务期间,有新的协调者顶替进来,它就可以根据一个参与者的状态来判断当前应该干嘛,比如如果一个参与者处于提交阶段,那么表明当前的事务正处于提交阶段。当因为网络问题某个节点一直收不到提交信息,那么此时也不会傻等了,会有超时时间,当超时时间过去了,节点可以自动提交,但是这里有个问题,对于参与者节点来说,当前应该是commit还是rollback呢?

其实2PC和3PC都无法保证绝对的一致性,因为某个参与者节点可能就是因为网络问题收不到消息,但是其他参与者节点已经提交了事务,一般为了预防这种问题,最好加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息。

TCC

TCC事务的全程是Try、Commit、Cancel,TCC事务使用场景更贴近实际应用,因此它的使用也更广泛。

  • Try:Try这个过程,一般表示锁定资源的过程,比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库存给锁定住。
  • Commit:真正的执行业务逻辑了,带提交的。
  • Cancel:撤销,如果Commit失败可以把锁定的资源释放回来

TCC对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,代码改造成本高。在出现网络或者其他系统故障时,TCC要根据实际业务场景实现对应的回滚逻辑。Commit或者Cancel有可能会重试,因此对应的部分最好支持幂等。

最后其实上面3种分布式事务理论上都无法保证绝对的一致性,因为无法解决网络等带来的意外因素,要解决它,要么只能无限重试,但是这个无限重试最好通过消息队列+守护进程的方式来自动补数据,前提还是得保证消息队列不丢失数据。总之不仅仅是分布式事务会带来这些问题,分布式本身也会带来许许多多的问题,没有绝对的解决方案,只有更好的解决方案。

往期精彩:

小心陷入MySQL索引的坑

kafka!还好我留了一手

一切从MySQL删除说起

最后,微信搜【假装懂编程】,如果你有任何疑问,欢迎联系我,如果我的文章有问题,也欢迎指正,如果你爱学习,喜欢钻研,可以关注我。

本文转载自: 掘金

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

尤雨溪推荐神器 ni ,能替代 npm/yarn/pnpm

发表于 2021-10-28

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

  1. 前言

大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release、vue-this、create-vue、玩具vite等20余篇源码文章。

本文仓库 ni-analysis,求个star^_^

最近组织了源码共读活动,大家一起学习源码。于是搜寻各种值得我们学习,且代码行数不多的源码。

之前写了 Vue3 相关的两篇文章。

  • 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数
  • Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

文章里都是写的使用 yarn 。参加源码共读的小伙伴按照我的文章,却拉取的最新仓库代码,发现 yarn install 安装不了依赖,向我反馈报错。于是我去 github仓库 一看,发现尤雨溪把 Vue3仓库 从 yarn 换成了 pnpm。贡献文档中有一句话。

We also recommend installing ni to help switching between repos using different package managers. ni also provides the handy nr command which running npm scripts easier.

我们还建议安装 ni 以帮助使用不同的包管理器在 repos 之间切换。 ni 还提供了方便的 nr 命令,可以更轻松地运行 npm 脚本。

这个 ni 项目源码虽然是 ts,没用过 ts 小伙伴也是很好理解的,而且主文件其实不到 100行,非常适合我们学习。

阅读本文,你将学到:

1
2
3
4
sh复制代码1. 学会 ni 使用和理解其原理
2. 学会调试学习源码
3. 可以在日常工作中也使用 ni
4. 等等
  1. 原理

github 仓库 ni#how

ni 假设您使用锁文件(并且您应该)

在它运行之前,它会检测你的 yarn.lock / pnpm-lock.yaml / package-lock.json 以了解当前的包管理器,并运行相应的命令。

单从这句话中可能有些不好理解,还是不知道它是个什么。我解释一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码使用 `ni` 在项目中安装依赖时:
假设你的项目中有锁文件 `yarn.lock`,那么它最终会执行 `yarn install` 命令。
假设你的项目中有锁文件 `pnpm-lock.yaml`,那么它最终会执行 `pnpm i` 命令。
假设你的项目中有锁文件 `package-lock.json`,那么它最终会执行 `npm i` 命令。

使用 `ni -g vue-cli` 安装全局依赖时
默认使用 `npm i -g vue-cli`

当然不只有 `ni` 安装依赖。
还有 `nr` - run
`nx` - execute
`nu` - upgrade
`nci` - clean install
`nrm` - remove

我看源码发现:ni相关的命令,都可以在末尾追加\?,表示只打印,不是真正执行。

所以全局安装 ni 后,可以尽情测试,比如 ni \?,nr dev --port=3000 \?,因为打印,所以可以在各种目录下执行,有助于理解 ni 源码。我测试了如下图所示:

命令测试图示

假设项目目录下没有锁文件,默认就会让用户从npm、yarn、pnpm选择,然后执行相应的命令。
但如果在~/.nirc文件中,设置了全局默认的配置,则使用默认配置执行对应命令。

Config

1
2
3
4
5
6
7
ini复制代码; ~/.nirc

; fallback when no lock found
defaultAgent=npm # default "prompt"

; for global installs
globalAgent=npm

因此,我们可以得知这个工具必然要做三件事:

1
2
3
bash复制代码1. 根据锁文件猜测用哪个包管理器 npm/yarn/pnpm 
2. 抹平不同的包管理器的命令差异
3. 最终运行相应的脚本

接着继续看看 README 其他命令的使用,就会好理解。

  1. 使用

看 ni github文档。

npm i in a yarn project, again? F**k!

ni - use the right package manager

全局安装。

1
bash复制代码npm i -g @antfu/ni

如果全局安装遭遇冲突,我们可以加上 --force 参数强制安装。

举几个常用的例子。

3.1 ni - install

1
2
3
4
5
bash复制代码ni

# npm install
# yarn install
# pnpm install
1
2
3
4
5
bash复制代码ni axios

# npm i axios
# yarn add axios
# pnpm i axios

3.2 nr - run

1
2
3
4
5
bash复制代码nr dev --port=3000

# npm run dev -- --port=3000
# yarn run dev --port=3000
# pnpm run dev -- --port=3000
1
2
3
4
bash复制代码nr
# 交互式选择命令去执行
# interactively select the script to run
# supports https://www.npmjs.com/package/npm-scripts-info convention
1
2
3
4
bash复制代码nr -

# 重新执行最后一次执行的命令
# rerun the last command

3.3 nx - execute

1
2
3
4
5
bash复制代码nx jest

# npx jest
# yarn dlx jest
# pnpm dlx jest
  1. 阅读源码前的准备工作

4.1 克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sh复制代码# 推荐克隆我的仓库(我的保证对应文章版本)
git clone https://github.com/lxchuan12/ni-analysis.git
cd ni-analysis/ni
# npm i -g pnpm
# 安装依赖
pnpm i
# 当然也可以直接用 ni

# 或者克隆官方仓库
git clone https://github.com/antfu/ni.git
cd ni
# npm i -g pnpm
# 安装依赖
pnpm i
# 当然也可以直接用 ni

众所周知,看一个开源项目,先从 package.json 文件开始看起。

4.2 package.json 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码{
"name": "@antfu/ni",
"version": "0.10.0",
"description": "Use the right package manager",
// 暴露了六个命令
"bin": {
"ni": "bin/ni.js",
"nci": "bin/nci.js",
"nr": "bin/nr.js",
"nu": "bin/nu.js",
"nx": "bin/nx.js",
"nrm": "bin/nrm.js"
},
"scripts": {
// 省略了其他的命令 用 esno 执行 ts 文件
// 可以加上 ? 便于调试,也可以不加
// 或者是终端 npm run dev \?
"dev": "esno src/ni.ts ?"
},
}

根据 dev 命令,我们找到主入口文件 src/ni.ts。

4.3 从源码主入口开始调试

1
2
3
4
5
6
ts复制代码// ni/src/ni.ts
import { parseNi } from './commands'
import { runCli } from './runner'

// 我们可以在这里断点
runCli(parseNi)

找到 ni/package.json 的 scripts,把鼠标移动到 dev 命令上,会出现运行脚本和调试脚本命令。如下图所示,选择调试脚本。

VSCode 调试

VSCode 调试 Node.js 说明

  1. 主流程 runner - runCli 函数

这个函数就是对终端传入的命令行参数做一次解析。最终还是执行的 run 函数。

对于 process 不了解的读者,可以看阮一峰老师写的 process 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码// ni/src/runner.ts
export async function runCli(fn: Runner, options: DetectOptions = {}) {
// process.argv:返回一个数组,成员是当前进程的所有命令行参数。
// 其中 process.argv 的第一和第二个元素是Node可执行文件和被执行JavaScript文件的完全限定的文件系统路径,无论你是否这样输入他们。
const args = process.argv.slice(2).filter(Boolean)
try {
await run(fn, args, options)
}
catch (error) {
// process.exit方法用来退出当前进程。它可以接受一个数值参数,如果参数大于0,表示执行失败;如果等于0表示执行成功。
process.exit(1)
}
}

我们接着来看,run 函数。

  1. 主流程 runner - run 主函数

这个函数主要做了三件事:

1
2
3
bash复制代码1. 根据锁文件猜测用哪个包管理器 npm/yarn/pnpm - detect 函数
2. 抹平不同的包管理器的命令差异 - parseNi 函数
3. 最终运行相应的脚本 - execa 工具
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
ts复制代码// ni/src/runner.ts
// 源码有删减
import execa from 'execa'
const DEBUG_SIGN = '?'
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
// 命令参数包含 问号? 则是调试模式,不执行脚本
const debug = args.includes(DEBUG_SIGN)
if (debug)
// 调试模式下,删除这个问号
remove(args, DEBUG_SIGN)

// cwd 方法返回进程的当前目录(绝对路径)
let cwd = process.cwd()
let command

// 支持指定 文件目录
// ni -C packages/foo vite
// nr -C playground dev
if (args[0] === '-C') {
cwd = resolve(cwd, args[1])
// 删掉这两个参数 -C packages/foo
args.splice(0, 2)
}

// 如果是全局安装,那么实用全局的包管理器
const isGlobal = args.includes('-g')
if (isGlobal) {
command = await fn(getGlobalAgent(), args)
}
else {
let agent = await detect({ ...options, cwd }) || getDefaultAgent()
// 猜测使用哪个包管理器,如果没有发现锁文件,会返回 null,则调用 getDefaultAgent 函数,默认返回是让用户选择 prompt
if (agent === 'prompt') {
agent = (await prompts({
name: 'agent',
type: 'select',
message: 'Choose the agent',
choices: agents.map(value => ({ title: value, value })),
})).agent
if (!agent)
return
}
// 这里的 fn 是 传入解析代码的函数
command = await fn(agent as Agent, args, {
hasLock: Boolean(agent),
cwd,
})
}

// 如果没有命令,直接返回,上一个 runCli 函数报错,退出进程
if (!command)
return

// 如果是调试模式,那么直接打印出命令。调试非常有用。
if (debug) {
// eslint-disable-next-line no-console
console.log(command)
return
}

// 最终用 execa 执行命令,比如 npm i
// https://github.com/sindresorhus/execa
// 介绍:Process execution for humans

await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}

我们学习完主流程,接着来看两个重要的函数:detect 函数、parseNi 函数。

根据入口我们可以知道。

1
2
3
4
5
ts复制代码runCli(parseNi)

run(fn)

这里 fn 则是 parseNi

6.1 根据锁文件猜测用哪个包管理器(npm/yarn/pnpm) - detect 函数

代码相对不多,我就全部放出来了。

1
2
3
4
5
bash复制代码主要就做了三件事情

1. 找到项目根路径下的锁文件。返回对应的包管理器 `npm/yarn/pnpm`。
2. 如果没找到,那就返回 `null`。
3. 如果找到了,但是用户电脑没有这个命令,则询问用户是否自动安装。
1
2
3
4
5
6
js复制代码// ni/src/agents.ts
export const LOCKS: Record<string, Agent> = {
'pnpm-lock.yaml': 'pnpm',
'yarn.lock': 'yarn',
'package-lock.json': 'npm',
}
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
ts复制代码// ni/src/detect.ts
export async function detect({ autoInstall, cwd }: DetectOptions) {
const result = await findUp(Object.keys(LOCKS), { cwd })
const agent = (result ? LOCKS[path.basename(result)] : null)

if (agent && !cmdExists(agent)) {
if (!autoInstall) {
console.warn(`Detected ${agent} but it doesn't seem to be installed.\n`)

if (process.env.CI)
process.exit(1)

const link = terminalLink(agent, INSTALL_PAGE[agent])
const { tryInstall } = await prompts({
name: 'tryInstall',
type: 'confirm',
message: `Would you like to globally install ${link}?`,
})
if (!tryInstall)
process.exit(1)
}

await execa.command(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
}

return agent
}

接着我们来看 parseNi 函数。

6.2 抹平不同的包管理器的命令差异 - parseNi 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码// ni/src/commands.ts
export const parseNi = <Runner>((agent, args, ctx) => {
// ni -v 输出版本号
if (args.length === 1 && args[0] === '-v') {
// eslint-disable-next-line no-console
console.log(`@antfu/ni v${version}`)
process.exit(0)
}

if (args.length === 0)
return getCommand(agent, 'install')
// 省略一些代码
})

通过 getCommand 获取命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ts复制代码// ni/src/agents.ts
// 有删减
// 一份配置,写个这三种包管理器中的命令。

export const AGENTS = {
npm: {
'install': 'npm i'
},
yarn: {
'install': 'yarn install'
},
pnpm: {
'install': 'pnpm i'
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ts复制代码// ni/src/commands.ts
export function getCommand(
agent: Agent,
command: Command,
args: string[] = [],
) {
// 包管理器不在 AGENTS 中则报错
// 比如 npm 不在
if (!(agent in AGENTS))
throw new Error(`Unsupported agent "${agent}"`)

// 获取命令 安装则对应 npm install
const c = AGENTS[agent][command]

// 如果是函数,则执行函数。
if (typeof c === 'function')
return c(args)

// 命令 没找到,则报错
if (!c)
throw new Error(`Command "${command}" is not support by agent "${agent}"`)
// 最终拼接成命令字符串
return c.replace('{0}', args.join(' ')).trim()
}

6.3 最终运行相应的脚本

得到相应的命令,比如是 npm i,最终用这个工具 execa 执行最终得到的相应的脚本。

1
ts复制代码await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
  1. 总结

我们看完源码,可以知道这个神器 ni 主要做了三件事:

1
2
3
bash复制代码1. 根据锁文件猜测用哪个包管理器 npm/yarn/pnpm - detect 函数
2. 抹平不同的包管理器的命令差异 - parseNi 函数
3. 最终运行相应的脚本 - execa 工具

我们日常开发中,可能容易 npm、yarn、pnpm 混用。有了 ni 后,可以用于日常开发使用。Vue 核心成员 Anthony Fu 发现问题,最终开发了一个工具 ni 解决问题。而这种发现问题、解决问题的能力正是我们前端开发工程师所需要的。

另外,我发现 Vue 生态很多基本都切换成了使用 pnpm。

因为文章不宜过长,所以未全面展开讲述源码中所有细节。非常建议读者朋友按照文中方法使用VSCode调试 ni 源码。学会调试源码后,源码并没有想象中的那么难。

关于 && 交流群


最后可以持续关注我@若川。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。

另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.2k+人)第一的专栏,写有20余篇源码文章。包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite、create-vite 等20余篇源码文章。


作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan02。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

Linux网络编程常用API 分类 创建socket 命名s

发表于 2021-10-28

分类

Linux网络API可以分成三类

  1. socket地址API,socket指的是一个(ip,port)对,唯一地标记了TCP通信的一端。
  2. socket基础API,主要是指sys/socket.h头文件中,socket创建,命名,监听,接受连接,发起连接,读写数据,获取地址信息,检测带外标记,以及读取和设置socket选项。
  3. 网络信息API,实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。

创建socket

int socket(int domain,int type,int protocol)

  • domain指的是使用的协议簇,可选的有PF_INET(ipv4),PF_INET6(ipv6),PF_UNIX(unix)
  • type指的是服务类型,可以选数据报服务,数据流服务,更新后也可能传入非阻塞,和在fork时关闭socket的参数
  • protocol指的是协议,其实前两个参数已经能确定一个协议了。
  • 创建socket成功则返回一个文件描述符。

命名socket

int bind(int sockfd,const struct sockaddr * my_addr,socklen_t addrlen )

  • 把my_addr指向的地址分配给文件描述符,addrlen参数指的是该socket地址的长度。
  • bind成功时返回0,失败返回-1并设置errno。其中两种常见的errno是EACCES和EADDRINUSE,它们的含义分别是地址被保护需要超级用户才能访问,绑定的地址正在使用中。

监听socket

int listen(int sockfd,int backlog)

  • sockfd指定被监听的socket
  • backlog指内核监听队列的最大长度,如果监听队列的长度超过该值,服务器将不受理新的客户连接,指定值是半连接长度+全连接长度的上限。
  • listen成功返回0,失败返回-1.

接受连接

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)

  • 已经调用listen的socket
  • 远端的地址
  • socket地址的长度
  • accept成功时返回一个新的socket,服务器可通过读写该socket来与客户端进行通信。
  • 这里有一点,客户端握手成功后立马宕机,这时候服务端是无感知的。

发起连接

int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen)

  • serv_addr是指服务端监听的地址
  • sockefd成功后返回一个socket,唯一地标识一个连接。
  • 可能会出现目标端口不存在或连接超时的可能。

关闭连接

int close(int fd)

  • fd指待关闭的连接,不是立马关闭一个连接,将fd的引用计数器减一,等到计数为0时才关闭连接。
  • 在fork进程时,系统调用会默认将父进程的socket的引用计数加1.

数据读写

TCP读写

1
2
c复制代码ssize_t recv(int sockfd,void *buf,size_t len,int flags)
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
  • recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小
  • 成功时返回读取的数据长度

UDP读写

1
2
c复制代码ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t addrlen);
  • 由于UDP无连接,所以每一次都要获取发送端的地址。

高级IO函数

pipe()

int pipe(int fd[2])

  • pipe可以创建一个管道,以实现进程通信
  • 参数包含两个int型整数的数组指针。该函数成功时返回0,并将一对打开的文件描述符值填入数组中。
  • fd[0]和fd[1]分别构成管道的两端,使用的方向确定,不能反过来使用,如果想要实现双向通信,就应该使用两个管道。
  • 管道默认阻塞的。
  • 管道的默认大小为65535字节。

dup函数和dup2函数

1
2
c复制代码int dup(int file_descriptor)
int dup2(int file_descriptor_one,int file_descriptor_two)
  • 如果我们想把标准输入重定向到应该文件,或者把标准输出重定向到网络连接中,可以使用这两个函数
  • dup创建一个新的文件描述符,该新文件描述符和原有的文件描述符指向同一个文件,管道或网络连接,并且dup返回的文件描述符总是取系统当前可用的最小整数值,dup2返回第一个不小于file_descriptor_two的整数值。

readv函数和writev函数

分散读

集中写

sendfile

ssize_t sendfile(int out_fd,int in_fd,off_t *offset,size_t count)

  • 在两个文件描述符之间直接传递数据,避免了在内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,被称为零拷贝。
  • in_fd必须指向一个真实文件,out_fd必须是一个socket

mmap函数和munmap函数

1
2
c复制代码void* mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset)
int munmap(void *start,size_t length);
  • mmap函数用于申请一段内存空间,我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中
  • munmap函数则释放由mmap创建的这段空间
  • start起始地址,可以由系统分配
  • prot指的是内存权限。

splice函数

ssize_t splice(int fd_in,loff_t* off_in,int fd_out,loff_t* off_out,size_t len,nusigned int flags)

  • 在两个文件描述符间移动数据,也就是零拷贝。

tee函数

ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags)

  • 在两个管道间复制数据

本文转载自: 掘金

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

关于 interface{} 会有啥注意事项?上

发表于 2021-10-28

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

学习 golang ,对于 interface{} 接口类型,我们一定绕不过,咱们一起来看看 使用 interface{} 的时候,都有哪些注意事项吧

interface {} 可以用于模拟多态

xdm 咱们写一个简单的例子,就举动物的例子

写一个 Animal 的接口,类似于 java 里面的抽象类 ,Animal 的接口 中有 2 个方案待实现

写一个 Cat 来继承 Animal , 实现 Eat 方法和 Drink 方法

  • 动物都有吃和喝的行为,小猫吃的行为是吃鱼,小猫的喝的行为是喝可乐
  • 最后在主函数中,使用父类的指针,来指向子类的实例化的一个子类地址
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
go复制代码type Animal interface {
Eat(string) string
Drink(string) string
}

type Cat struct{}

func (c *Cat) Eat(food string) string {
if food != "fish" {
return "i dislike"
} else {
return "i like"
}

}
func (c *Cat) Drink(drink string) string {
if drink == "coke" {
return "i love"
}else{
return "abandon"
}
}
func main(){
var a Animal = &Cat{}
fmt.Println(a.Eat("fish"))
fmt.Println(a.Drink("water"))
}

看到上述代码,会不会有这样的疑问,命名是 &Cat{} 是取地址的,为什么 var a Animal 不写成指针呢?

这里需要注意,Animal 本身是 接口类型,自身就是一个指针

运行上述代码查看效果

1
2
3
shell复制代码# go run main.go
i like
abandon

没有毛病,小猫眯爱吃鱼,不爱喝水

interface{} 需要注意空和非空的情况

什么叫做空的 interface{} , 什么又叫做非空的 interface{} 呢?

咱们还是用上面的例子, 添加一个 testInterface 函数,来实践一下

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func testInterface() Animal {
var c *Cat
return c
}

func main() {
test := testInterface()
if test == nil {
fmt.Println("test is nil")
} else {
fmt.Println("test is not nil")
}
}

可以猜猜看,上面这个小案例会输出什么结果

  • 理论上来看,testInterface 函数中我们只是创建了一个 Cat 指针,并没有赋值,因此默认是一个零值,因此会是一个 nil,那么 return 的时候,应该也是 return nil 才对吧,因此按照代码的逻辑来说应该是输出 test is nil

执行上述代码后,查看结果

1
2
shell复制代码# go run main.go
test is not nil

看到上面的结果,是不是觉得很奇怪,和自己的预期不一致

没关系,之前的文章我们说到过,觉得一个技术点奇怪,不是我们所期望的效果,原因是我们对其原理不够了解,不够熟悉

现在先来回答一下上面的问题

空接口:意思是没有方法的接口,interface{} 源码中表示为 eface 结构体

非空接口:表示有包含方法的接口 , interface{} 源码中表示为 iface 结构体

暂时先来直接介绍源码中的结构体

iface 结构体 , 非空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码type iface struct {
tab *itab
data unsafe.Pointer
}

type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
  • tab

指的是具体的类型信息,是一个 itab 结构,结构中成员如上,这里面包含的都是借口的关键信息,例如 hash 值 ,函数指针,等等,后续详细剖析 interface{} 原理的时候再统一说

  • data

具体的数据信息

eface 结构体

1
2
3
4
go复制代码type eface struct {
_type *_type
data unsafe.Pointer
}
1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type _type struct {
size uintptr // 表示的是 类型的大小
ptrdata uintptr // 值的是前缀指针的内存大小
hash uint32 // 计算数据的 hash 值
tflag tflag
align uint8 // 进行内存对齐的
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
  • _type

类型信息,和上面的 非空接口类似 , 这个_type 类型决定下面的 data 字段如何去解释数据

  • data

具体的数据信息

看到这里,细心的 xdm 是不是就可以看出来,我们上面写的 Animal 接口,其实是一个非空接口,因为里面有包含方法,所以他的底层是一个 iface 结构体 ,非空接口

那么初始化的一个空指针 c ,实际上是 iface 结构体里面的 data 字段为空而已,数据为空而已,但是 iface 这个结构体自己不是空的,所以上述代码走的逻辑是 test is not nil

这里顺带说一下,golang 中,还有哪些数据结构是和 nil 比较是否为零值,这个点我们也可以看看源码

1
2
3
go复制代码// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

源码中有说到,可以对 指针,通道,函数,接口,map,切片类型使用 nil

好了,本次就到这里,知识点要用起来才有价值

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

springSecurity深度解析 security原理分

发表于 2021-10-27

security原理分析

springSecurity过滤器链

springSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明

  1. WebAsyncManagerIntegrationFilter:将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager 进行集成。
  2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除
    例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
  3. HeaderWriterFilter:用于将头信息加入响应中
  4. CsrfFilter:用于处理跨站请求伪造
  5. LogoutFilter:用于处理退出登录
  6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
  7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
  8. BasicAuthenticationFilter:检测和处理http basic认证
  9. RequestCacheAwareFilter:用来处理请求的缓存
  10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request
  11. AnonymousAuthenticationFilter:检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication
  12. SessionManagementFilter:管理session的过滤器
  13. ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常
  14. FilterSecurityInterceptor:可以看做过滤器链的出口
  15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从cookie里找出用户的信息, 如果Spring Security能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

springSecurity 流程图

上一版是通过debug的方法告诉读者springSecurity的一个执行过程,发现反而把问题搞复杂了,这一版我决定画一个流程图来说明其执行过程,只要把springSecurity的执行过程弄明白了,这个框架就会变得很简单

13612520-e6bfb247ef6edf01.png

流程说明

  1. 客户端发起一个请求,进入security过滤器链;
  2. 当到LogoutFilter的时候判断是否是登出路径,如果是登出路径则到logoutHandler,如果登出成功则到logoutSuccessHandler登出成功处理,如果登出失败则由ExceptionTranslationFilter;如果不是登出路径则直接进入下一个过滤器;
  3. 当到UsernamePasswordAuthenticationFilter的时候判断是否为登陆路径,如果是,则进入该过滤器进行登陆操作,如果登陆失败则到AuthenticationFailureHandler登陆失败处理器处理,如果登陆成功则到AuthenticationSuccessHandler登陆成功处理器处理 ;如果不是登陆请求则不进入该过滤器
  4. 当到FilterSecurityInterceptor的时候会拿到urI,根据uri去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到controller层否则到AccessDeniedHandler鉴权失败处理器处理

security配置

在WebSecurityConfigurerAdapter这个类里面可以完成上述流程图的所有配置

配置类伪代码

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
java复制代码 **/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/login_page").passwordParameter("username").passwordParameter("password").loginProcessingUrl("/sign_in").permitAll()
.and().authorizeRequests().antMatchers("/test").hasRole("test")
.anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable();
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
}
}

配置类说明

configure(AuthenticationManagerBuilder auth) 说明

AuthenticationManager的建造器,配置AuthenticationManagerBuilder 会让security自动构建一个AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个UserDetailService和passwordEncoder。userDetailsService用于在认证器中根据用户传过来的用户名查找一个用户,passwordEncoder用于密码的加密与比对,我们存储用户密码的时候用passwordEncoder.encode()加密存储,在认证器里会调用passwordEncoder.matches()方法进行密码比对。

如果重写了该方法,security会启用DaoAuthenticationProvider这个认证器,该认证就是先调用UserDetailsService.loadUserByUsername然后使用passwordEncoder.matches()进行密码比对,如果认证成功成功则返回一个Authentication对象

configure(WebSecurity web)说明

这个配置方法用于配置静态资源的处理方式,可使用ant匹配规则

configure(HttpSecurity http) 说明

这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说

1
java复制代码http.formLogin().loginPage("/login_page").passwordParameter("username").passwordParameter("password").loginProcessingUrl("/sign_in").permitAll()

这是配置登陆相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登陆请求路径,permitAll()代表任意用户可访问

1
java复制代码http.authorizeRequests().antMatchers("/test").hasRole("test").anyRequest().authenticated().accessDecisionManager(accessDecisionManager());

以上配置是权限相关的配置,配置了一个“/test” url该有什么权限才能访问,anyRequest()表示所有请求,authenticated()表示已登录用户,accessDecisionManager()表示绑定在url上的鉴权管理器

为了对比,现在贴出另一个权限配置清单

1
bash复制代码http.authorizeRequests().antMatchers("/tets_a/**","/test_b/**").hasRole("test").antMatchers("/a/**","/b/**").authenticated().accessDecisionManager(accessDecisionManager())

我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意url上;而且可以硬编码各种url权限;

1
java复制代码http.logout().logoutUrl("/logout").logoutSuccessHandler(new MyLogoutSuccessHandler())

登出相关配置,这里配置了登出url和登出成功处理器

1
java复制代码http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

上面代码是配置鉴权失败的处理器

1
2
java复制代码http.addFilterAfter(new MyFittler(), LogoutFilter.class);
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore加在对应的过滤器之前addFilterAfter之后,addFilterAt加在过滤器同一位置,事实上框架原有的Filter在启动HttpSecurity配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用addFilterAt方法插入的Filter,会在这个位置上的原有Filter之前执行。

注:关于HttpSecurity使用的是链式编程,其中http.xxxx.and.yyyyy这种写法和http.xxxx;http.yyyy写法意义一样。

自定义authenticationManager和accessDecisionManager

重写authenticationManagerBean()方法,并构造一个authenticationManager

1
2
3
4
5
java复制代码@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(getMyAuthenticationProvider(),daoAuthenticationProvider()));
return authenticationManager;
}

我这里给authenticationManager配置了两个认证器,执行过程参考流程图

定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明

1
2
3
4
5
6
7
8
9
10
java复制代码public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new MyExpressionVoter(),
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);

}

投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。

security 权限用户系统说明

UserDetails

security中的用户接口,我们自定义用户类要实现该接口,各个属性的含义自行百度

GrantedAuthority

security中的用户权限接口,自定义权限需要实现该接口

1
2
3
4
java复制代码@Data
public class MyGrantedAuthority implements GrantedAuthority {
private String authority;
}

authority权限字段,需要注意的是在config中配置的权限会被加上ROLE_前缀,比如我们的配置authorizeRequests().antMatchers(“/test”).hasRole(“test”),配置了一个“test”权限但我们存储的权限字段(authority)应该是“ROLE_test”

UserDetailsService

security用户service,自定义用户服务类需要实现该接口

1
2
3
4
5
6
7
java复制代码@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return.....
}
}

loadUserByUsername的作用在上文中已经说明;

SecurityContextHolder

用户在完成登陆后security会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成SecurityContext ,而实际存储的类是SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。

手动填充SecurityContextHolder示例:

1
2
java复制代码UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);

对于token鉴权的系统

我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。

security扩展说明

可扩展的有

  • 鉴权失败处理器:security鉴权失败默认跳转登陆页面,我们可以
  • 验证器
  • 登陆成功处理器
  • 投票器
  • 自定义token处理过滤器
  • 登出成功处理器
  • 登陆失败处理器
  • 自定义UsernamePasswordAuthenticationFilter

鉴权失败处理器

security鉴权失败默认跳转登陆页面,我们可以实现AccessDeniedHandler接口,重写handle()方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中

验证器

实现AuthenticationProvider接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点

登陆成功处理器

在security中验证成功默认跳转到上一次请求页面或者路径为”/“的页面,我们同样可以自定义:继承SimpleUrlAuthenticationSuccessHandler这个类或者实现AuthenticationSuccessHandler接口。我这里建议采用继承的方式;SimpleUrlAuthenticationSuccessHandler是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。

投票器

投票器可继承WebExpressionVoter或者实现AccessDecisionVoter接口;WebExpressionVoter是security默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 配置类说明章节;

注意:投票器vote方法返回一个int值;-1代表反对,0代表弃权,1代表赞成;投票管理器收集投票结果,如果最终结果大于等于0则放行该请求。

自定义token处理过滤器

自定义token处理器继承自可OncePerRequestFilter或者GenericFilterBean或者Filter都可以,在这个处理器里面需要完成的逻辑是:获取请求里的token,验证token是否合法然后填充SecurityContextHolder,虽然说过滤器只要添加在投票器之前就可以;但我这里还是建议添加在http.addFilterAfter(new MyFittler(), LogoutFilter.class);

登出成功处理器

实现LogoutSuccessHandler接口,添加到配置的方式参考 配置类说明章节

登陆失败处理器

登陆失败默认跳转到登陆页,我们同样可以自定义。继承SimpleUrlAuthenticationFailureHandler 或者实现AuthenticationFailureHandler;建议采用继承。

自定义UsernamePasswordAuthenticationFilter

我们自定义UsernamePasswordAuthenticationFilter可以极大提高我们security的灵活性(比如添加验证验证码是否正确的功能),所以我这里是建议自定义UsernamePasswordAuthenticationFilter;

我们直接继承UsernamePasswordAuthenticationFilter,然后在配置类中初始化这个过滤器,给这个过滤器添加登陆失败处理器,登陆成功处理器,登陆管理器,登陆请求url

这里配置略微复杂,贴一下代码清单

初始化过滤器:

1
2
3
4
5
6
7
8
java复制代码MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
MyUsernamePasswordAuthenticationFilte myUsernamePasswordAuthenticationFilte = new MyUsernamePasswordAuthenticationFilte(redisService);
myUsernamePasswordAuthenticationFilte.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
myUsernamePasswordAuthenticationFilte.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
myUsernamePasswordAuthenticationFilte.setFilterProcessesUrl("/sign_in");
myUsernamePasswordAuthenticationFilte.setAuthenticationManager(getAuthenticationManager());
return myUsernamePasswordAuthenticationFilte;
}

添加到配置:

1
java复制代码http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

代码清单

下面贴出适配于 前后端分离和token验证的伪代码清单

登陆页请求处理

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Controller
public class LoginController {
/**
* @Description: 登陆页面的请求
* @Param:
* @return:
*/
@GetMapping("/login_page")
public String loginPage(){
return "loginPage.html";
}
}

鉴权失败处理器

1
2
3
4
5
6
7
8
9
java复制代码public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("{\"code\":\"403\",\"msg\":\"没有权限\"}");
writer.close();
}
}

验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class MyAuthenticationProvider  implements AuthenticationProvider {
private UserDetailsService userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;

public MyAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 这里写验证逻辑
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

验证成功处理器

1
2
3
4
5
6
java复制代码ublic class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//随便写点啥
}
}

投票器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* @program: security-test
* @description: 鉴权投票器
* @author: muggle
* @create: 2019-04-11
**/

public class MyExpressionVoter extends WebExpressionVoter {
@Override
public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
// 这里写鉴权逻辑
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
return 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
java复制代码/**
* @program: security-about
* @description:填充一个token
* @author: muggle
* @create: 2019-04-20
**/

public class MyFittler extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token1 = request.getHeader("token");
if (token1==null){

}

ArrayList<GrantedAuthority> list = new ArrayList<>();
GrantedAuthority grantedAuthority = new GrantedAuthority() {
@Override
public String getAuthority() {
return "test";
}
};
list.add(grantedAuthority);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(request, response);
}
}

登出成功处理器

1
2
3
4
5
6
7
8
9
java复制代码public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
final PrintWriter writer = response.getWriter();

writer.write("{\"code\":\"200\",\"msg\":\"登出成功\"}");
writer.close();
}
}

登陆失败处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class MyUrlAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
final PrintWriter writer = response.getWriter();
if(exception.getMessage().equals("坏的凭证")){
writer.write("{\"code\":\"401\",\"msg\":\"登录失败,用户名或者密码有误\"}");
writer.close();
}else {
writer.write("{\"code\":\"401\",\"msg\":\"登录失败,"+exception.getMessage()+"\"}");
writer.close();
}

}
}

自定义UsernamePasswordAuthenticationFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* @program: security-test
* @description: 用户登陆逻辑过滤器
* @author: muggle
* @create: 2019-04-11
**/

public class MyUsernamePasswordAuthenticationFilte extends UsernamePasswordAuthenticationFilter {
private RedisService redisService;
private boolean postOnly = true;

public MyUsernamePasswordAuthenticationFilte(RedisService redisService){
this.redisService=redisService;
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

//你可以在这里做验证码校验,校验不通过抛出AuthenticationException()即可
super.attemptAuthentication(request,response);
}
}

配置

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
java复制代码@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
RedisService redisService;
@Autowired
MyUserDetailService userDetailService;

@Override
public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js",
"/resources/**/*.css", "/resources/**/*.txt",
"/resources/**/*.png", "/**/*.bmp", "/**/*.gif", "/**/*.png", "/**/*.jpg", "/**/*.ico");
// super.configure(web);
}


@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登录页等 permitAll表示任何权限都能访问
http.formLogin().loginPage("/login_page").passwordParameter("username").passwordParameter("password").loginProcessingUrl("/sign_in").permitAll()
.and().authorizeRequests().antMatchers("/test").hasRole("test")
// 任何请求都被accessDecisionManager() 的鉴权器管理
.anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
// 登出配置
.and().logout().logoutUrl("/logout").logoutSuccessHandler(new MyLogoutSuccessHandler())
// 关闭csrf
.and().csrf().disable();
http.authorizeRequests().antMatchers("/tets_a/**","/test_b/**").hasRole("test").antMatchers("/a/**","/b/**").authenticated().accessDecisionManager(accessDecisionManager())
// 加自定义过滤器
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
// 配置鉴权失败的处理器
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
http.addFilterAfter(new MyFittler(), LogoutFilter.class);

}


MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
MyUsernamePasswordAuthenticationFilte myUsernamePasswordAuthenticationFilte = new MyUsernamePasswordAuthenticationFilte(redisService);
myUsernamePasswordAuthenticationFilte.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
myUsernamePasswordAuthenticationFilte.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
myUsernamePasswordAuthenticationFilte.setFilterProcessesUrl("/sign_in");
myUsernamePasswordAuthenticationFilte.setAuthenticationManager(getAuthenticationManager());
return myUsernamePasswordAuthenticationFilte;
}
MyAuthenticationProvider getMyAuthenticationProvider(){
MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider(userDetailService,new BCryptPasswordEncoder());
return myAuthenticationProvider;
}
DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
daoAuthenticationProvider.setUserDetailsService(userDetailService);
return daoAuthenticationProvider;
}
protected AuthenticationManager getAuthenticationManager() {
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(getMyAuthenticationProvider(),daoAuthenticationProvider()));
return authenticationManager;
}

public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new MyExpressionVoter(),
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);

}
}

总结

对于security的扩展配置关键在于configure(HttpSecurity http)方法;扩展认证方式可以自定义authenticationManager并加入自己验证器,在验证器中抛出异常不会终止验证流程;扩展鉴权方式可以自定义accessDecisionManager然后添加自己的投票器并绑定到对应的url(url 匹配方式为ant)上,投票器vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes)方法返回值为三种:-1 0 1,分别表示反对弃权赞成;

对于token认证的校验方式,可以暴露一个获取的接口,或者重写UsernamePasswordAuthenticationFilter过滤器和扩展登陆成功处理器来获取token,然后在LogoutFilter之后添加一个自定义过滤器,用于校验和填充SecurityContextHolder

security的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回json,那么就需要重写各个处理器了。

本文转载自: 掘金

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

List常用操作比for循环更优雅的写法 list常用的la

发表于 2021-10-27

list常用的lambda表达式-单list操作

小知识,大挑战!本文正在参与“ 程序员必备小知识”创作活动

本文同时参与 掘力星计划,赢取创作大礼包,挑战创作激励金

引言

使用JDK1.8之后,大部分list的操作都可以使用lambda表达式去写,可以让代码更简洁,开发更迅速。以下是我在工作中常用的lambda表达式对list的常用操作,喜欢建议收藏。

以用户表为例,用户实体代码如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class User {
private Integer id; //id

private String name; //姓名

private Integer age; //年龄

private Integer departId; //所属部门id
}

List<User> list = new ArrayList<>();

简单遍历

使用lambda表达式之前,如果需要遍历list时,一般使用增强for循环,代码如下:

1
2
3
4
java复制代码List<User> list = new ArrayList<>();
for (User u:list) {
System.out.println(u.toString());
}

使用lambda表达式之后,可以缩短为一行代码:

1
java复制代码list.forEach(u-> System.out.println(u.toString()));

筛选符合某属性条件的List集合

以筛选年龄在15-17之间的用户为例,for循环写法为:

1
2
3
4
5
6
java复制代码List<User> users = new ArrayList<>();
for (User u : list) {
if (u.getAge() >= 15 && u.getAge() <= 17) {
users.add(u);
}
}

使用lambda表达式写法为:

1
2
3
java复制代码List<User> users = list.stream()
.filter(u -> u.getAge() >= 15 && u.getAge() <= 17)
.collect(Collectors.toList());

获取某属性返回新的List集合

以获取id为例,项目中有时候可能会需要根据用户id的List进行查询或者批量更新操作,这时候就需要用户id的List集合,for循环写法为:

1
2
3
4
java复制代码List<Integer> ids = new ArrayList<>();
for (User u:list) {
ids.add(u.getId());
}

lambda表达式写法为:

1
2
java复制代码List<Integer> ids = list.stream()
.map(User::getId).collect(Collectors.toList());

获取以某属性为key,其他属性或者对应对象为value的Map集合

以用户id为key(有时可能需要以用户编号为key),以id对应的user作为value构建Map集合,for循环写法为:

1
2
3
4
5
6
java复制代码Map<Integer,User> userMap = new HashMap<>();
for (User u:list) {
if (!userMap.containsKey(u.getId())){
userMap.put(u.getId(),u);
}
}

lambda表达式写法为:

1
2
3
4
java复制代码Map<Integer,User> map = list.stream()
.collect(Collectors.toMap(User::getId,
Function.identity(),
(m1,m2)->m1));

Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

(m1,m2)-> m1此处的意思是当转换map过程中如果list中有两个相同id的对象,则map中存放的是第一个对象,此处可以根据项目需要自己写。

以某个属性进行分组的Map集合

以部门id为例,有时需要根据部门分组,筛选出不同部门下的人员,如果使用for循环写法为:

1
2
3
4
5
6
7
8
9
10
java复制代码Map<Integer,List<User>> departGroupMap = new HashMap<>();
for (User u:list) {
if (departGroupMap.containsKey(u.getDepartId())){
departGroupMap.get(u.getDepartId()).add(u);
}else {
List<User> users1 = new ArrayList<>();
users1.add(u);
departGroupMap.put(u.getDepartId(),users1);
}
}

使用lambda表达式写法为:

1
2
3
java复制代码Map<Integer,List<User>> departGroupMap = list.stream()
.collect(Collectors
.groupingBy(User::getDepartId));

其他情况

可以根据需要结合stream()进行多个操作,比如筛选出年龄在15-17岁的用户,并且根据部门进行分组分组,如果使用for循环,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码Map<Integer,List<User>> departGroupMap = new HashMap<>();
for (User u:list) {
if (u.getAge() >= 15 && u.getAge() <= 17) {
if (departGroupMap.containsKey(u.getDepartId())){
departGroupMap.get(u.getDepartId()).add(u);
}else {
List<User> users1 = new ArrayList<>();
users1.add(u);
departGroupMap.put(u.getDepartId(),users1);
}
}
}

使用lambda表达式,代码如下:

1
2
3
java复制代码Map<Integer,List<User>> departGroupMap = list.stream()
.filter(u->u.getAge() >= 15 && u.getAge() <= 17)
.collect(Collectors.groupingBy(User::getDepartId));

总结

上述部分是小编在工作中遇到的常用的单个List的操作,可能在项目中还会遇到更复杂的场景,可以根据需要进行多个方法的组合使用,我的感觉是使用lambda表达式代码更加简洁明了,当然各人有各人的编码习惯,不喜勿喷。

本文转载自: 掘金

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

1…461462463…956

开发者博客

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