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

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


  • 首页

  • 归档

  • 搜索

MyCat 的安装与测试

发表于 2021-11-23

这是我参与11月更文挑战的第9天,活动详情查看:11月更文挑战
​

一、环境准备

(1)、本机环境是三台centos7

IP 主机名 数据库名 安装软件
192.168.77.128 master db1 mycat,mysql
192.168.77.129 slave1 db2 mysql
192.168.77.134 slave2 db3 mysql

(2)安装mycat的linux中新建用户和组

1、创建一个新的group


         groupadd mycat


2、创建一个新的用户,并加入group


        useradd -g mycat mycat


3、给新用户设置密码


        passwd mycat

二、安装MySQL并创建数据库

(1)安装MySQL参考我的其他两篇文章:

CentOS7 yum方式 安装MySQL5.7

CentOS7.4 RPM方式安装mysql5.7

(2)创建数据库

根据不同机器数据库名不同
1
bash复制代码create database db1/db2/db3;

创建后效果如下:

)“)​)“)​)​

三、下载安装MyCat:

MyCat的官方网站:

Mycat1.6

下载地址:github.com/MyCATApache…

也可以下载我的百度云版本:

链接: pan.baidu.com/s/1-J-Tb19y…提取码: b262

第一步:将Mycat-server-1.4-release-20151019230038-linux.tar.gz上传至服务器

第二步:将压缩包解压缩。建议将mycat放到/usr/local/mycat目录下。

1
2
3
bash复制代码tar -xzvf Mycat-server-1.4-release-20151019230038-linux.tar.gz

mv mycat /usr/local

四、MyCat分片配置

注:1、2、3 的操作目录:/usr/local/mycat/conf

(1)配置schema.xml

schema.xml作为MyCat中重要的配置文件之一,管理着MyCat的逻辑库、逻辑表以及对应的分片规则、DataNode以及DataSource。弄懂这些配置,是正确使用MyCat的前提。这里就一层层对该文件进行解析。

schema 标签用于定义MyCat实例中的逻辑库

Table 标签定义了MyCat中的逻辑表 rule用于指定分片规则,auto-sharding-long的分片规则是按ID值的范围进行分片 1-5000000 为第1片 5000001-10000000 为第2片…. 。

dataNode 标签定义了MyCat中的数据节点,也就是我们通常说所的数据分片。

dataHost标签在mycat逻辑库中也是作为最底层的标签存在,直接定义了具体的数据库实例、读写分离配置和心跳语句。

在服务器上创建3个数据库,分别是db1 db2 db3

修改schema.xml如下:

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
xml复制代码<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://org.opencloudb/">

<schema name="youfanshop" checkSQLschema="false" sqlMaxLimit="100">
<table name="employee" primaryKey="ID" dataNode="dn1,dn2,dn3"
rule="sharding-by-intfile" />
</schema>
<dataNode name="dn1" dataHost="master" database="db1" />
<dataNode name="dn2" dataHost="slave1" database="db2" />
<dataNode name="dn3" dataHost="slave2" database="db3" />
<dataHost name="master" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="master:3306" user="root"
password="123456">
</writeHost>
</dataHost>
<dataHost name="slave1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM2" url="slave1:3306" user="root"
password="123456">
</writeHost>
</dataHost>
<dataHost name="slave2" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM3" url="slave2:3306" user="root"
password="123456">
</writeHost>
</dataHost>


</mycat:schema>

(2)配置 server.xml

server.xml几乎保存了所有mycat需要的系统配置信息。最常用的是在此配置用户名、密码及权限。在system中添加UTF-8字符集设置,否则存储中文会出现问号

utf8

在conf目录下配置server.xml ,将目录下没有的用户全部删掉或者注释掉,添加可用的用户,这里添加了一个root用户:

1
2
3
4
ini复制代码      <user name="root">
<property name="password">123456</property>
<property name="schemas">youfanshop</property>
</user>

​

(3)修改conf下的partition-hash-int.txt文件

在下面添加10020=2,原本默认的是分两个就是10000和10010,现在我们三个就要三个分类id了,添加一个即可

)​

(4)设置mysql忽略大小写

在三台mysql的配置文件vi /etc/my.cnf中加入lower_case_table_names = 1来忽略大小写:

1
ini复制代码lower_case_table_names = 1

(5)修改hosts文件

由于上面schema.xml文件配置使用了slave1、slave2、master没有指定ip,所以需要在hosts中映射过去:

1
2
3
复制代码192.168.77.128 master
192.168.77.129 slave1
192.168.77.134 slave2

)​

五、测试MyCat

1、启动MyCat

mycat 的bin目录下执行 ./ mycat start

)​

查看logs:

1
bash复制代码tail -100 wrapper.log

看到如下结果就是成功了

)​

2.测试mysql表横向分割

在虚拟机外的windows安装Navicat for MySQL,分别连接到三个mysql数据库,执行建表语句:

1
sql复制代码create table employee (id int not null primary key,name varchar(100),sharding_id int not null);

用Navicat 或者sqlyog连接mycat,mycat默认端口是8066,配置如图:

)​

因为刚才执行了建表语句,这时候连接上了mycat里面也有一个空的employee表

在连上的mycat中执行如下语句

1
2
3
4
5
6
7
8
9
10
11
scss复制代码insert into employee(id,name,sharding_id) values(1, 'I am db1',10000);

insert into employee(id,name,sharding_id) values(2, 'I am db2',10010);

insert into employee(id,name,sharding_id) values(3, 'I am db3',10020);

insert into employee(id,name,sharding_id) values(4, 'I am db1',10000);

insert into employee(id,name,sharding_id) values(5, 'I am db2',10010);

insert into employee(id,name,sharding_id) values(6, 'I am db3',10020);

)​

刷新一下navicat查看mycat连接的库

)​

db1

)​

db2

)​

db3

)​

可以看到新增的6条记录被mycat分配到了三台服务器的数据库上。

六、sqlyog连接MyCat报错2003

可能是MyCat所在主机没有关闭防火墙:

1
arduino复制代码systemctl stop firewalld

关闭之后,发现问题已解决。

或者不想关闭防火墙,可以打开8066端口:

1. 开放端口命令: **/sbin/iptables -I INPUT -p tcp --dport 8066 -j ACCEPT**


 2.保存: **/etc/rc.d/init.d/iptables save**


 3.重启服务: **/etc/init.d/iptables restart**


 4.查看端口是否开放: **/sbin/iptables -L -n**

打开端口也可以这样:

1
2
css复制代码firewall-cmd --add-port=8066/tcp --permanent
firewall-cmd --reload

PS:如果你想一次性添加多个接口,可以将1重复执行多次,然后一次性执行2、3、4。

\

​

本文转载自: 掘金

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

一文带你掌握Mybatis框架执行SQL流程源码 Mybat

发表于 2021-11-23

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

Mybatis执行SQL流程源码解析

首先,先介绍一下相关类

SqlSession

  • SqlSession是一个接口,他有两个实现类:DefaultSqlSession(默认实现类)、SqlSessionManager(已经弃用)。
  • SqlSession是Mybatis中用于和数据库交互的顶层类,通常将它和ThreadLocal一起绑定,一个会话使用一个SqlSession,因为SqlSession是线程不安全的,所以使用玩SqlSession后需要close,关闭SqlSession。
1
2
3
4
5
java复制代码public class DefaultSqlSession implements SqlSession {

private final Configuration configuration;
private final Executor executor;
}
  • SqlSession中有两个最重要的参数,configuration与初始化时相同,Executor是执行器。

SqlSession在任务方法的执行过程中,会把任务委派给Executor进行处理

Executor

  • Executor也是一个接口,他有三个常⽤的实现类
+ BatchExecutor (重⽤语句并执⾏批量更新)
+ ReuseExecutor (重⽤预处理语句prepared statements)
+ SimpleExecutor (普通的执⾏器,默认)
  • Executor作用
+ 1、根据传递的参数,完成SQL语句的动态解析,⽣成BoundSql对象,StatementHandler使⽤;
+ 2、为查询创建缓存,以提⾼性能
+ 3、创建JDBC的Statement连接对象,传递给*StatementHandler*对象,返回List查询结果。

StatementHandler类

StatementHandler对象主要完成两个⼯作:

  • 对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使⽤的是SQL语句字符串会包含若⼲个?占位符,我们其后再对占位符进⾏设值。StatementHandler通过parameterize(statement)⽅法对S tatement进⾏设值;
  • StatementHandler通过List query(Statement statement, ResultHandler resultHandler)⽅法来完成执⾏Statement,和将Statement对象返回的resultSet封装成List

介绍完相关类后,继续分析执行SQL流程,点击进入openSession()方法解析

openSession源码解析

image.png

由下图可知道openSession执行的是openSessionFromDataSource方法

image.png

进入openSessionFromDataSource中,openSession的多个重载⽅法可以指定获得的SeqSession的Executor类型和事务的处理。

默认参数介绍

方法 注释
ExecutorType为Executor ExecutorType为Executor的类型
TransactionIsolationLevel TransactionIsolationLevel为事务隔离级别
autoCommit autoCommit是否开启事务

image.png

selectList源码解析(举例)

image.png

点击selectList方法进入openSession中找到对应的接口类

image.png

点接selectList实现类到DefaultSqlSession类中,调用selectList的重载方法

image.png

selectList默认参数介绍

参数 作用
statement
parameter
rowBounds
ResultHandler
  • configuration.getMappedStatement(statement);

根据传入的statement查询出MappedStatement对象

  • executor.query(ms, wrapCollection(parameter), rowBounds, handler)

将查询出的MappedStatement对象和parameter等参数传递给executor.query方法中,委托executor类进行查询操作。

executor.query()方法解析

具体源码实现如下图所示

image.png

将Mappedstatement动态获取SQL语句,然后BoundSql封装,然后创建一级缓存,并将参数传递给query的重载方法,如下所示

image.png

如果查询的数据一级缓存中有,这直接处理,如果一级缓存中没有,则进行数据库查询,数据库查询源码如下所示:

image.png

数据库查询操作调用的是doQuery方法,然后doQuery方法结果返回后,将结果放到缓存中并返回结果

image.png

将参数都获取到后,创建JDBC的statement对象,并把statementHandler参数传递给statement对象中进行初始化操作

image.png

image.png

上述的Executor.query()⽅法⼏经转折,最后会创建⼀个StatementHandler对象,然后将必要的参数传递给StatementHandler,使⽤StatementHandler来完成对数据库的查询,最终返回List结果集。

StatementHandler.parameterize()方法解析

进入StatementHandler类的parameterize(statement)⽅法的实现

image.png

image.png

从上述的代码可以看到,StatementHandler的parameterize(Statement)⽅法调⽤了ParameterHandler的setParameters(statement)⽅法,ParameterHandler的setParameters(Statement )⽅法负责根据我们输⼊的参数,对statement对象的?占位符处进⾏赋值。

进⼊到StatementHandler的List query(Statement statement, ResultHandler resultHandler)⽅法的实现:

image.png

从上述代码我们可以看出,StatementHandler的List query(Statement statement, ResultHandlerresultHandler)⽅法的实现,是调⽤了ResultSetHandler的handleResultSets(Statement)⽅法。

ResultSetHandler.handleResultSets()方法解析

ResultSetHandler的handleResultSets(Statement)⽅法会将Statement语句执⾏后⽣成的resultSet结果集转换成List结果集

image.png

image.png

致此SQL的执行流程分析基本完成。

本文转载自: 掘金

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

ElasticSearch——倒排索引和正向索引

发表于 2021-11-23

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

1、正向索引

正向索引 (forward index) 以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档

这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护:

  • 若是有新的文档加入,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面。
  • 若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除

缺点:

  • 检索效率太低,只能在一起简单的场景下使用

假设有文档一(id为doc_1)和文档二(id为doc_2),

文档一:my name is zhangsan

文档二:my car is BMW

文档一和文档二的正向索引为:

文档id 关键词
doc_1 my ,name, is, zhangsan
doc_2 my ,car ,is ,BMW

假设使用正向索引,那么当你搜索 ‘name’ 的时候,搜索引擎必须检索文档中的每一个关键词,假设一个文档中包含成千上百个关键词,可想而知,会造成大量的资源浪费。于是倒排索引应运而生。

2、倒排索引

倒排索引 ,一般也被称为反向索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。

倒排索引以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档。

一个表项就是一个字段,它记录该文档的ID和字符在该文档中出现的位置情况。

优缺点:

  • 查询的时候由于可以一次得到查询关键字所对应的所有文档,所以查询效率高于正排索引。
  • 由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂

假设有文档一(id为doc_1)和文档二(id为doc_2),

文档一:my name is zhangsan

文档二:my car is BMW

文档一和文档二的倒排索引为:

关键词 文档id
my doc_1,doc_2
name doc_1
is doc_1,doc_2
zhangsan doc_1
car doc_2
BMW doc_1

倒排索引是相对正向索引而言的,你也可以将其理解为逆向索引。它是一种关键词与文档一一对应的数据结构。

3、倒排索引的组成

ES 倒排索引包含两个部分:单词词典和倒排列表

单词词典(Term Dictionary)

单词词典是倒排索引中非常重要的组成部分,它用来维护文档集合中出现过的所有单词的相关信息,同时用来记载某个单词对应的倒排列表在倒排文件中的位置信息。在支持搜索时,根据用户的查询词,去单词词典里查询,就能够获得相应的倒排列表,并以此作为后续排序的基础。
对于一个规模很大的文档集合来说,可能包含几十万甚至上百万的不同单词,能否快速定位某个单词,这直接影响搜索时的响应速度,所以需要高效的数据结构来对单词词典进行构建和查找,常用的数据结构包括哈希加链表结构和树形词典结构。

单词词典的特性:

  1. 是文档集合中所有单词的集合
  2. 它是保存索引的最小单位
  3. 其中记录着指向倒排列表的指针

用B+Tree 实现单词词典,存储在内存:

在这里插入图片描述

倒排列表

倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息及频率(作关联性算分),每条记录称为一个倒排项(Posting)。

根据倒排列表,即可获知哪些文档包含某个单词。

倒排项(Posting)主要包含如下信息:

  • 文档id用于获取原始信息
  • 单词频率(TF,Term Frequency),记录该单词在该文档中出现的次数,用于后续相关性算分
  • 位置(Position),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query)
  • 偏移(Offset),记录单词在文档的开始和结束位置,用于高亮显示

单词词典和倒排列表整合到一起的结构如下:

在这里插入图片描述

4、倒排索引的更新策略

搜索引擎需要处理的文档集合往往都是动态集合,即在建好初始的索引后,不断有新文档进入系统,同时原先的文档集合内有些文档可能被删除或更改。

动态索引通过在内存中维护临时索引,可以实现对动态文档和实时搜索的支持。

服务器内存总是有限的,随着新加入系统的文档越来越多,临时索引消耗的内存也会随之增加。

当最初分配的内存将被使用完时,要考虑将临时索引的内容更新到磁盘索引中,以释放内存空间来容纳后续的新进文档。

索引基本更新思想:

  • 倒排索引就是对初始文档集合建立的索引结构,一般单词词典都存储在内存,对应的倒排列表存储在磁盘文件中
  • 临时索引是在内存中实时建立的倒排索引,其结构和前述的倒排索引是一样的,区别在于词典和倒排列表都在内存中存储。
  • 新文档进入系统时,实时解析文件并将其加入到临时索引结构中。
  • 删除文档列表则用来存储已被删除的文档的相应的文档ID,形成一个文档ID列表。
  • 修改文档可以认为是旧文档先被删除,然后系统在增加一篇新的文档,通过这种间接方式实现对内容更改的支持。

5、倒排索引四种更新策略

常用的索引更新策略主要有四种:完全重建策略、再合并策略、原地更新策略及混合策略。

  1. **完全重建策略:**当新增文档到达一定数量,将新增文档和原先的老文档整合,然后利用静态索引创建方法对所有文档重建索引,新索引建立完成后老索引会被遗弃。此法代价高,但是主流商业搜索引擎一般是采用此方式来维护索引的更新(这句话是书中原话)
  2. 再合并策略:当新增文档进入系统,解析文档,之后更新内存中维护的临时索引,文档中出现的每个单词,在其倒排表列表末尾追加倒排表列表项;一旦临时索引将指定内存消耗光,即进行一次索引合并,这里需要倒排文件里的倒排列表存放顺序已经按照索引单词字典顺序由低到高排序,这样直接顺序扫描合并即可。其缺点是:因为要生成新的倒排索引文件,所以对老索引中的很多单词,尽管其在倒排列表并未发生任何变化,也需要将其从老索引中取出来并写入新索引中,这样对磁盘消耗是没必要的。
  3. **原地更新策略:**试图改进再合并策略,在原地合并倒排表,这需要提前分配一定的空间给未来插入,如果提前分配的空间不够了需要迁移。实际显示,其索引更新的效率比再合并策略要低。
  4. **混合策略:**出发点是能够结合不同索引更新策略的长处,将不同索引更新策略混合,以形成更高效的方法。

本文转载自: 掘金

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

从 vue3 和 vite 源码中,我学到了一行代码统一规范

发表于 2021-11-23
  1. 前言

大家好,我是若川。最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,或者关注我的公众号若川视野,回复“源码”参与。已进行三个月,大家一起交流学习,共同进步,很多人都表示收获颇丰。

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

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

最近组织了源码共读活动,每周大家一起学习200行左右的源码。每周一期,已进行到14期。于是搜寻各种值得我们学习,且代码行数不多的源码。

阅读本文,你将学到:

1
2
3
4
5
bash复制代码1. 如何学习调试源码
2. 学会 npm 钩子
3. 学会 "preinstall": "npx only-allow pnpm" 一行代码统一规范包管理器
4. 学到 only-allow 原理
5. 等等
  1. 场景

我们项目开发时,常需要安装依赖,虽说一般用文档可以说明。但不是比较强制的约束。是人就容易犯错或者疏忽,假如规定是用的npm,而团队里有人某一天不小心使用了其他包管理器安装了的其他依赖,上传了代码,严重时可能导致线上问题。所以我们需要借助工具(代码)来强制约束。

在源码共读第12期中,我们学习了尤雨溪推荐神器 ni ,能替代 npm/yarn/pnpm ?简单好用!源码揭秘!
根据锁文件自动匹配相应的包管理器,运行相应的命令。

在源码共读第3期中,我们学习了Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

其中 Vue3 源码用了 npm 的 preinstall 钩子 约束,只能使用 pnpm 安装依赖。我们接着来看其实现。

  1. Vue3 源码 && npm 命令钩子

1
2
3
4
5
6
7
8
js复制代码// vue-next/package.json
{
"private": true,
"version": "3.2.22",
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
}
}
1
2
3
4
5
6
7
bash复制代码依次执行
# install 之前执行这个脚本
preinstall
# 执行 install 脚本
install
# install 之后执行这个脚本
postinstall

当然也支持自定义的命令。

更多可以查看官方文档钩子。

接着我们来看 preinstall 源码。

1
2
3
4
5
6
7
8
9
js复制代码// vue-next/scripts/preinstall.js

if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.warn(
`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`
)
process.exit(1)
}

这段代码也相对简单,校验如果不是 pnpm 执行脚本则报错,退出进程。

关于 process 对象可以查看 阮一峰老师 process 对象

process.argv 属性返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是 node,第二个成员是脚本文件名,其余成员是脚本文件的参数。

这段代码能文章开头场景提出的问题,但是总不能每个项目都复制粘贴这段代码吧。我们是不是可以封装成 npm 包使用。
当时我也没想太多,也没有封装 npm 包。直到我翻看 vite 源码发现了 only-allow 这个包。一行代码统一规范包管理器。

1
2
3
4
5
js复制代码{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}

当时看到这段代码时,我就在想:他们咋知道这个的。
当时依旧也没想太多。直到有一天,发现 pnpm 文档 Only allow pnpm 文档 上就有这个。好吧,吃了没看文档的亏。那时我打算分析下这个only-allow 包的源码,打开一看惊喜万分,才 36 行,写它,于是写了这篇文章。

按照惯例,看源码前先准备环境。

  1. 环境准备

先克隆代码。

4.1 克隆代码

1
2
3
4
5
6
7
8
9
10
11
bash复制代码# 推荐克隆我的源码库
git clone https://github.com/lxchuan12/only-allow-analysis.git
cd only-allow-analysis/only-allow
# npm i -g pnpm
pnpm i

# 或者克隆官方仓库
git clone https://github.com/pnpm/only-allow.git
cd only-allow
# npm i -g pnpm
pnpm i

开源项目一般先看README.md。

Force a specific package manager to be used on a project

强制在项目上使用特定的包管理器

Usage

Add a preinstall script to your project’s package.json.

If you want to force yarn, add:

1
2
3
4
5
js复制代码{
"scripts": {
"preinstall": "npx only-allow yarn"
}
}

同理可得:强制使用 npm、pnpm也是类似设置。

4.2 调试源码

我们通过查看 package.json 文件。

1
2
3
4
js复制代码// only-allow/package.json
{
"bin": "bin.js",
}

确定主入口文件为 only-allow/bin.js。

在最新版的 VSCode 中,auto attach 功能,默认支持智能调试,如果发现不支持,可以通过快捷键 ctrl + shift + p 查看是否启用。

于是我们在 only-allow/package.json 文件中,添加如下命令。

1
2
3
4
5
6
js复制代码// only-allow/package.json
{
"scripts": {
"preinstall": "node bin.js pnpm"
},
}

可以提前在 only-allow/bin.js 文件打上断点 const usedPM = whichPMRuns()

按快捷键 ctrl + 快捷键打开终端。输入如下yarn add release-it -D命令,即可调试only-allow/bin.js`。

调试截图

最终调试完会在终端报错提示使用 pnpm install。

如下图所示:

error.png

更多调试细节可以看我的这篇文章:新手向:前端程序员必学基本技能——调试JS代码

接着我们按调试来看源码主流程。

  1. only-allow 源码

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
js复制代码// only-allow/bin.js
#!/usr/bin/env node
const whichPMRuns = require('which-pm-runs')
const boxen = require('boxen')

const argv = process.argv.slice(2)
if (argv.length === 0) {
console.log('Please specify the wanted package manager: only-allow <npm|pnpm|yarn>')
process.exit(1)
}
// 第一个参数则是 用户传入的希望使用的包管理器
// 比如 npx only-allow pnpm
// 这里调试是 node bin.js pnpm
const wantedPM = argv[0]
// npm pnpm yarn 都不是,则报错
if (wantedPM !== 'npm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn') {
console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, pnpm, or yarn.`)
process.exit(1)
}
// 使用的包管理器
const usedPM = whichPMRuns()
// 希望使用的包管理器 不相等,则报错。
// - npm 提示使用 npm install
// - pnpm 提示使用 pnpm install
// - yarn 提示使用 yarn install
// 最后退出进程
if (usedPM && usedPM.name !== wantedPM) {
const boxenOpts = { borderColor: 'red', borderStyle: 'double', padding: 1 }
switch (wantedPM) {
case 'npm':
console.log(boxen('Use "npm install" for installation in this project', boxenOpts))
break
case 'pnpm':
console.log(boxen(`Use "pnpm install" for installation in this project.

If you don't have pnpm, install it via "npm i -g pnpm".
For more details, go to https://pnpm.js.org/`, boxenOpts))
break
case 'yarn':
console.log(boxen(`Use "yarn" for installation in this project.

If you don't have Yarn, install it via "npm i -g yarn".
For more details, go to https://yarnpkg.com/`, boxenOpts))
break
}
process.exit(1)
}

跟着断点,我们可以查看到 which-pm-runs。

  1. which-pm-runs 当前运行的是哪一个包管理器

最终返回包管理器和版本号。

根据调试可知,process.env.npm_config_user_agent 是类似这样的字符串。

"yarn/1.22.10 npm/? node/v14.16.0 linux x64"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码'use strict'

module.exports = function () {
if (!process.env.npm_config_user_agent) {
return undefined
}
return pmFromUserAgent(process.env.npm_config_user_agent)
}

function pmFromUserAgent (userAgent) {
const pmSpec = userAgent.split(' ')[0]
const separatorPos = pmSpec.lastIndexOf('/')
return {
name: pmSpec.substr(0, separatorPos),
version: pmSpec.substr(separatorPos + 1)
}
}

6.1 String.prototype.substr 截取字符串

顺带提下。我之前在 vue-next 源码看到的 pull request => chore: remove deprecated String.prototype.substr

String.prototype.substr is deprecated.

也就是说不推荐使用 substr。推荐使用 slice。

ecma 规范

  1. 总结

我们通过从团队需要规范统一包管理器的实际场景出发,讲了 vue3 源码中 preinstall 钩子 约束只能使用 pnpm 。同时通过查看 vite 源码和 pnpm 文档,了解到 only-allow 这个包。可以做到一行代码统一规范包管理器"preinstall": "npx only-allow pnpm"。

也学习了其原理。only-allow 期待的包管理器和运行的包管理器对比。匹配失败,则报错。而which-pm-runs 通过获取 process.env.npm_config_user_agent 变量获取到当前运行脚本的包管理器和版本号。

我们通过文档和沟通约束,不如用工具(代码)约束。

文章写到这里,让我想起我2018年写的文章参加有赞前端技术开放日所感所想

当时演讲的大佬说过一句话。无比赞同。

技术(开源)项目本质上是:理念、套路、规范的工具化。

同时给我们的启发也是要多看官方文档和规范。

建议读者克隆我的仓库动手实践调试源码学习。

最后可以持续关注我@若川。欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,每周大家一起学习200行左右的源码,共同进步。


关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

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

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

若川的博客

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

掘金专栏,欢迎关注~

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

github blog,求个star^_^~

本文转载自: 掘金

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

AQS源码学习

发表于 2021-11-23

1、AQS介绍

AQS全称AbstractQueuedSynchronizer,是一个同步器,用来构建锁或者其他同步组件的基础框架。内部主要使用一个volatile修饰的state变量和一个FIFO双向队列来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码	/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;

/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;

/**
* The synchronization state.
*/
private volatile int state;

ReentrantLock、Semaphore、CountDownLatch等都是基于AQS实现的。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状 态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操 作,因为它们能够保证状态的改变是安全的。

image-20211117224126737.png

子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获 取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)

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复制代码public class Main {
static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
return super.tryAcquire(arg);
}

@Override
protected boolean tryRelease(int arg) {
return super.tryRelease(arg);
}

@Override
protected int tryAcquireShared(int arg) {
return super.tryAcquireShared(arg);
}

@Override
protected boolean tryReleaseShared(int arg) {
return super.tryReleaseShared(arg);
}

@Override
protected boolean isHeldExclusively() {
return super.isHeldExclusively();
}
}
}

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

2、同步器的接口与示例

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。 这句话听起来很绕,慢慢往后看就懂了。

我们在继承AQS并重写那5个方法的时候,需要调用AQS提供的几个方法去访问或修改stats变量的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 获取当前同步状态
protected final int getState() {
return state;
}

// 设置当前同步状态
protected final void setState(int newState) {
state = newState;
}

// 使用CAS设置当前状态,CAS能保证操作的原子性
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

我们在继承同步器时可重写的方法如下

image-20211117224924467.png

在实现自定义同步组件时,也会调用同步器提供的模板方法,部分模板方法如下

image-20211118202508275.png

AQS提供的模板方法主要分为三类:

(1)独占式获取和释放同步状态。

(2)共享式获取和释放同步状态。

(3)查询同步队列中等待的线程情况。

3、实现一个独占锁

独占锁,就是同一时刻只能一个线程拥有锁。基于AQS,我们可以很方便的实现。

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
java复制代码import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
* 基于AQS实现独占锁
*/
public class ExclusiveLock implements Lock {

/**
* 实现自定义的同步器
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 获取锁
* @param arg
* @return
*/
@Override
protected boolean tryAcquire(int arg) {
// 设置同步变量state为1
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}

/**
* 是否处于占用状态
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}

// 每个condition都包含一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}

private final Sync sync = new Sync();

@Override
public void lock() {
sync.acquire(1);
}

@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}

@Override
public void unlock() {
sync.release(1);
}

@Override
public Condition newCondition() {
return sync.newCondition();
}
}

一般在实现自定义同步器时,都会把它作为静态内部类去实现。上面实现的ExclusiveLock类中就是定义了一个静态内部类Sync去继承AQS实现独占锁逻辑的。通过CAS设置同步变量state值为1表示获取锁成功,释放锁时把state设置为0即可。

4、AQS的实现分析

4.1 同步队列

AQS内部依赖同步队列(双向FIFO队列)来进行线程的管理,当线程获取锁失败时,会将线程以及等待状态信息构造为一个节点Node并将其放入同步队列队尾,然后阻塞该线程。当锁释放的时候,会把队首节点中的线程唤醒,使其再次尝试获取同步状态。

Node是AQS中的一个静态内部类,用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,主要字段如下

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
java复制代码static final class Node {
// 表示节点是共享模式
static final Node SHARED = new Node();
// 表示节点是独占模式
static final Node EXCLUSIVE = null;

// 由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会改变。
static final int CANCELLED = 1;
// 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行。
static final int SIGNAL = -1;
// 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列转移到同步队列中,加入到对同步状态的获取中。
static final int CONDITION = -2;
// 表示下一次共享式同步状态获取将会无条件的传播下去。
static final int PROPAGATE = -3;

/**
* 等待状态,取值如下
* SIGNAL 值为-1,
*
* CANCELLED 值为1,
*
* CONDITION 值为-2,
*
* PROPAGATE 值为-3,
*
* INITIAL 值为0,表示初始状态。
*/
volatile int waitStatus;

// 前驱节点,当节点加入同步队列时被设置(尾部添加)
volatile Node prev;

// 后继节点
volatile Node next;

// 获取同步状态的线程
volatile Thread thread;

// 等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段
Node nextWaiter;
}

没有成功获取同步状态的线程将会加入同步队列的尾部,这个加入的过程也必须要保证线程安全,因此AQS提供了compareAndSetTail(Node expect, Node update)方法。同步队列的结构大致如下

image-20211122215003010.png

同步队列设置尾结点的过程大致如下

image-20211122215318789.png

同步队列的首节点是获取锁成功的线程,首节点的线程在释放锁后,将会唤醒后继节点,后继节点如果获取锁成功的同时会把自己设置为头结点

image-20211122215510970.png

设置头结点是通过获取同步状态成功的线程来完成的,由于只有一个线程能成功获取到同步状态,因此设置头结点的方法并不需要CAS来保证,它只需要将首节点设置为头结点的后继节点然后断开首节点即可。

4.2 独占式同步状态获取和释放

我们在实现自定义的独占式同步器时,主要重写了AQS的tryAcquire和tryRelease方法,通过操作同步变量state完成同步状态的获取和释放。

我们可以调用AQS对外提供的acquire获取同步状态,该方法会调用我们重新的tryAcquire方法。

1
2
3
4
5
java复制代码public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

如果调用tryAcquire方法返回为false,则通过addWaiter把线程加入同步队列队尾,并标志位独占Node.EXCLUSIVE。通过CAS确保节点能安全的添加到队列尾。

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复制代码private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

加入队列尾后,通过CAS不断的尝试获取同步状态。只有在前驱节点是头结点的情况下,才有可能获取到同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,如下。

(1)头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。

(2)维护同步队列的FIFO原则。

image-20211122220135617.png

独占式同步状态获取流程,也就是acquire(int arg)方法调用流程大致如下

image-20211122222721588.png

上图中,当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。

当线程获取同步状态并执行完相应的逻辑后,就需要释放同步状态,通过调用AQS提供的release方法。该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。

1
2
3
4
5
6
7
8
9
java复制代码public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。

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
java复制代码private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);

/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}

分析了独占式同步状态获取和释放过程后,适当做个总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

4.3 共享式同步状态获取和释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。

通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态

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
java复制代码public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

在acquireShared方法中,会调用我们重写的tryAcquireShared方法尝试获取同步状态,该方法返回值为int,返回值大于等于0时,表示获取成功。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态

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
java复制代码public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。

4.4 超时获取同步状态

通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)和doAcquireSharedNanos(int arg, long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。

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
java复制代码private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 从现在起经过nanosTimeout后超时
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 调用tryAcquire方法独占式获取同步状态
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 更新超时时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

大致流程如下

image-20211120113200693.png

本文转载自: 掘金

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

Netty编程(三)—— Channel

发表于 2021-11-23

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

Channel常用方法

  • close() 可以用来关闭Channel
  • closeFuture() 用来处理 Channel 的关闭事件
    • sync 方法作用是同步等待 Channel 关闭
    • 而 addListener 方法是异步等待 Channel 关闭
  • pipeline() 方法用于添加处理器
  • write() 方法将数据写入
    • 因为缓冲机制,数据被写入到 Channel 中的缓冲区以后,不会立即被发送
    • 只有当缓冲满了或者调用了flush()方法后,才会将数据通过 Channel 发送出去
  • writeAndFlush() 方法将数据写入并立即发送(刷出)

为什么需要sync()

在前两篇博客中Netty编程(一)—— 初识Netty+超全注释 - 掘金 (juejin.cn) 以及 Netty编程(二)—— EventLoop - 掘金 (juejin.cn)多次出现客户端部分的代码,而在其中有一句sync()方法当时说是用来阻塞,在这一篇博客中首先解释一下这一句的必要性。

首先我们先给出来客户端的代码,和之前不用的是,这次不完全使用链式编码方式:

在这里插入图片描述

分析原因

如果我们将第31行的channelFuture.sync()注释掉,服务端是接收不到发送的”hello world”的,下面来分析一下原因:

这是因为建立连接(connect)的过程是异步非阻塞的,

  • 异步:调用connect的线程不关心结果
  • 非阻塞:调用完connect就可以继续向下执行,不同等结果)

而connect方法由main线程发起,但真正执行的是NioEventLoopGroup其中的某一个nio线程,主线程会继续向下执行。如果不通过sync()方法阻塞主线程去等待连接真正建立,那么通过channelFuture.channel()拿到的 Channel 对象,并不是真正与服务器建立好连接的 Channel,因为此时还没有成功建立连接,也就没法将信息正确的传输给服务器端。

解决方法

解决上面的方法有两种:

  • 第一种方法就是一直使用channelFuture.sync()方法,阻塞住主线程,同步处理结果,等待连接真正建立好以后,再去获得 Channel 传递数据。使用该方法,获取 Channel 和发送数据的线程都是主线程
  • 第二种方法可以使用addListener(回调对象)方法,它用于异步获取建立连接后的 Channel 和发送数据,使得执行这些操作的线程是 NIO 线程(去执行connect操作的线程),nio线程连接建立好了以后就会调用回调对象的operationComplete方法,下面以一个例子来解释这个方法:
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复制代码public class MyClient {
public static void main(String[] args) throws IOException, InterruptedException {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new StringEncoder());
}
})
// 该方法为异步非阻塞方法,主线程调用后不会被阻塞,真正去执行连接操作的是NIO线程
// NIO线程:NioEventLoop 中的线程
.connect(new InetSocketAddress("localhost", 8080));

// 当connect方法执行完毕后,也就是连接真正建立后
channelFuture.addListener(new ChannelFutureListener() {
//在nio线程连接建立好了以后,会调用
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
Channel channel = channelFuture.channel();
channel.writeAndFlush("hello world");
}
});
System.in.read();
}
}

当客户端与服务端成功建立连接后,会执行addListener方法中的operationComplete方法,其中他传入的参数channelFuture是调用addListener方法的channelFuture,之后执行其中的方法。值得注意的是,operationComplete方法是在NIO线程中执行的,即由事件循环组负责。

处理关闭

当我们要关闭channel时,可以调用channel.close()方法进行关闭。但是该方法是一个异步方法。真正的关闭操作并不是在调用该方法的线程中执行的,而是在NIO线程中执行真正的关闭操作

如果我们想在channel真正关闭以后,执行一些额外的操作,可以选择以下两种方法来实现:

  • 通过channel.closeFuture()方法获得对应的ChannelFuture对象,然后调用sync()方法阻塞执行操作的线程,等待channel真正关闭后,再执行其他操作
1
2
3
4
5
ini复制代码// 获得closeFuture对象
ChannelFuture closeFuture = channel.closeFuture();

// 同步等待NIO线程执行完close操作
closeFuture.sync();
  • 调用closeFuture.addListener方法,添加close的后续操作
1
2
3
4
5
6
7
8
9
csharp复制代码closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
// 等待channel关闭后才执行的操作
System.out.println("关闭之后执行一些额外操作...");
// 关闭EventLoopGroup,优雅关闭:拒绝连接,发送剩余数据,不是立刻停止
group.shutdownGracefully();
}
});

本文转载自: 掘金

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

面试官:系统需求多变时如何设计?

发表于 2021-11-23

面试官:我想问个问题哈,项目里比较常见的问题

面试官:我现在有个系统会根据请求的入参,做出不同动作。但是,这块不同的动作很有可能是会发生需求变动的,这块系统你会怎么样设计?

面试官:实际的例子:现在有多个第三方渠道,系统需要对各种渠道进行订单归因。但是归因的逻辑很有可能会发生变化,不同的渠道归因的逻辑也不太一样,此时系统里的逻辑相对比较复杂。

面试官:如果让你优化,你会怎么设计?

候选者:我理解你的意思了

候选者:归根到底,就是处理的逻辑相对复杂,if else的判断太多了

候选者:虽然新的需求来了,都可以添加if else进行解决

候选者:但你想要的就是,系统的可扩展性和可维护性更强

候选者:想要我这边出一个方案,来解决类似的问题

候选者:对吧?

面试官:嗯…

候选者:在这之前,一般上网搜如何解决 if else ,大多数都说是 策略模式

候选者:但是举的例子又没感同身受,很多时候看完就过去了

候选者:实际上,在项目里边,用策略模式还是蛮多的,可能无意间就已经用上了(毕竟面向接口编程嘛)

候选者:而我认为,策略模式不是解决if else的关键

候选者:这个问题,我的项目里的做法是:责任链模式

候选者:把每个流程单独抽取成一个Process(可以理解为一个模块或节点),然后请求都会塞进Context中

候选者:比如,之前维护过一个项目,也是类似于不同的渠道走不同的逻辑

候选者:我们这边的做法是:抽取相关的逻辑到Process中,为不同的渠道分配不同的责任链

候选者:比如渠道A的责任链是:WhiteListProcess->DataAssembleProcess->ChannelAProcess->SendProcess

候选者:而渠道B的责任链是:WhiteListProcess->DataAssembleProcess->ChannelBProcess->SendProcess

候选者:在责任链基础之上,又可以在代码里内嵌「脚本」

候选者:比如在SendProcess上,内置发送消息的脚本(脚本可以选择不同的运营商进行发送消息)。有了「脚本」以后,那就可以做到对逻辑的改动不需要重启就可以生效。

候选者:有人把这一套东西叫做「规则引擎」。比如,规则引擎中比较出名的实现框架「Drools」就可以做到类似的事

候选者:把易改动的逻辑写在「脚本」上(至少我们认为,脚本和我们的应用真实逻辑是分离)

候选者:(脚本我这里指的是规则集,它可以是Drools的dsl,也可以是Groovy,也可以是aviator等等)

面试官:嗯…

候选者:在我之前的公司,使用的是Groovy脚本。大致的实现逻辑就是:有专门后台对脚本进行管理,然后会把脚本写到「分布式配置中心」(实时刷新),客户端监听「分布式配置中心」所存储的脚本是否有改动

候选者:如果存在改动,则通过Groovy类加载器重新编译并加载脚本,最后放到Spring容器对外使用

候选者:我目前所负责的系统就是这样处理 多变 以及需求变更频繁的业务(责任链+规则引擎)

候选者:不过据我了解,我们的玩法业务又在「责任链」多做了些事情

候选者:「责任链」不再从代码里编写,而是下沉到平台去做「服务编排」,就是由程序员去「服务编排后台」上配置信息(配置责任链的每一个节点)

候选者:在业务系统里使用「服务编排」的客户端,请求时只要传入「服务编排」的ID,就可以按「服务编排」的流程执行代码

候选者:这样做的好处就是:业务链是在后台配置的,不用在系统业务上维护链,灵活性更高(写好的责任链节点可以随意组合)

面试官:那我懂了

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

Django--模型与站点管理

发表于 2021-11-23

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

一.MTV开发模式

我们熟知的MVC框架,Model View Controller模式,Django同样也遵循这种MVC模式,以下是Django中M、V、C各自的含义:

  • M:数据存取部分,由django数据库层处理,重点
  • V:选择哪些数据要显示以及怎样显示,由视图和模板处理
  • C:根据用户输入委派视图的部分,由Django框架根据URLconf设置,对给定URL调用适当的Python函数

由于C由框架自行处理,而Django里更关注的是模型(Model)、模板(Template)和视图(Views),Django也被称为MTV框架:

  • M代表模型(Model),即数据存取层,该层处理与数据相关的所有事务
  • T代表模板(Template),即表现层。该层处理与表现相关的决定
  • V代表视图(View),即业务逻辑层,该层包含存取模型及调取恰当模板的相关逻辑,是模型与模板之间的桥梁

二.数据库配置

数据库的配置也是在Django的配置文件里面,缺省是settings.py:

1
2
3
4
5
6
python复制代码DATABASE_ENGINE = '' # 告诉Django使用哪个数据库引擎
DATABASE_NAME = '' # 将数据库名称告诉Django
DATABASE_USER = '' # 告诉Django用哪个用户连接数据库
DATABASE_PASSWORD = '' # 告诉Django连接用户的密码
DATABASE_HOST = '' # 告诉Django连接哪一台主机的数据库服务器
DATABASE_PORT = '' # 告诉Django连接服务器的端口号

DATABASE_ENGINE是告诉Django使用哪个数据库引擎,如果在Django中使用数据库,DATABASE_ENGINE必须是下表中的值:

设置 数据库 所需适配器
postgresql PostgreSQL psycopg 1.x版, www.djangoproject.com/r/python-pg…
postgresql_psycopg2 PostgreSQL psycopg 2.x版, www.djangoproject.com/r/python-pg…
mysql MySQL MySQLdb , www.djangoproject.com/r/python-my….
sqlite3 SQLite 如果使用Python 2.5+则不需要适配器。 否则就使用 pysqlite ,www.djangoproject.com/r/python-sq…
oracle Oracle cx_Oracle , www.djangoproject.com/r/python-or….

在输入这些配置并保存后,我们可以在工程目录下执行”python3 manage.py shell”来进行测试,输入以下命令来测试你的数据库配置:

1
2
shell复制代码>>> from django.db import connection
>>> cursor = connection.cursor()

如果没有什么错误信息,那么配置就是正确的,否则,就得查看错误信息来纠正:

错误信息 解决办法
You haven’t set the DATABASE_ENGINE setting yet. 不要以空字符串配置 DATABASE_ENGINE 的值。
Environment variable DJANGO_SETTINGS_MODULE is undefined. 使用 python manager.py shell 命令启动交互解释器,不要以 python 命令直接启动交互解释器。
Error loading _____ module: No module named _____. 未安装合适的数据库适配器 (例如, psycopg 或 MySQLdb )。Django并不自带适配器,所以你得自己下载安装。
_____ isn’t an available database backend. 把DATABASE_ENGINE 配置成前面提到的合法的数据库引擎。 也许是拼写错误?
database _____ does not exist 设置 DATABASE_NAME 指向存在的数据库,或者先在数据库客户端中执行合适的 CREATE DATABASE 语句创建数据库。
role _____ does not exist 设置 DATABASE_USER 指向存在的用户,或者先在数据库客户端中执创建用户。
could not connect to server 查看DATABASE_HOST和DATABASE_PORT是否已正确配置,并确认数据库服务器是否已正常运行。

三.在Python代码里定义模型

​ Django模型是用Python代码形式表述的数据在数据库中的定义。对数据层来说它等同于CREATE TABLE语句,只不过执行的是Python代码而不是SQL,而且还包含了比数据库字段定义更多的含义。Django用模型在后台执行SQL代码并把结果用Python的数据结构来描述。Django也使用模型来呈现SQL无法处理的高级概念。

为什么使用Python代码来定义数据模型,这样是不是显得多余?有以下这些原因:

  • 自省(运行时自动识别数据库)会导致过载和数据完整性问题。为了提供方便的数据访问API,Django需要以某种方式知道数据库层内部信息,有两种实现方式,第一种方式是用Python明确地定义数据模型,第二种方式是通过自省来自动侦测识别数据模型;
  • 第二种方式看起来更清晰,因为数据表信息只存放在一个地方-数据库里,但是会带来一些问题。首先,运行时扫描数据库会带来严重的系统过载,第二,某些数据库,尤其是老版本的MySQL,并未完整存储那些精确的自省元数据;
  • 编写Python代码能让你避免让你的大脑在不同的模式之间来回切换,尽可能保持单一的编程环境;
  • 把数据模型用代码的方式表述来让你更容易对它们进行版本控制;
  • SQL只能描述特定类型的数据字段,高级的数据类型带来更高的效率和更好的代码复用;
  • SQL还有在不同数据平台的兼容性问题;
  • 当然,这个方法也有一个缺点,就是Python代码和数据库表的同步问题。如果你修改了一个Django模型,你要自己来修改数据库来保证和模型同步。

3.1 第一个模型

用Python代码来描述它,打开models.py文件并输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码from django.db import models

class Publisher(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=50)
city = models.CharField(max_length=60)
state_province = models.CharField(max_length=30)
country = models.CharField(max_length=50)
website = models.URLField()

class Author(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=40)
email = models.EmailField()

class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()

​ 每个数据模型都是django.db.models.Model的子类,它的父类Model包含了所有必要的和数据库交互的方法,并提供了一个简洁漂亮的定义数据库字段的语法。每个模型相当于单个数据库表,每个属性也是这个表中的一个字段,属性名就是字段名。Django在拿到这些定义的模型后,可以自动生成相应的Sql语句,同时对于数据库中的多对多关系,Django还会额外创建一张表来表述这些映射关系。还有就是,我们无需显式地为这些模型定义主键,除非你单独指明,否则Django会自动为每个模型生成一个自动增长的整数主键字段,每个Django模型都要求有单独的主键,id。

3.2 安装模型

先需要在setting.py文件中的INSTALLED_APPS中添加所写模型文件的说明。然后:

  • 验证模型的有效性:python3 manage.py check,没有错误会显示”0 errors”;
  • python3 manage.py makemigrations:创建迁移文件;
  • python3 manage.py migrate:通过创建的迁移文件往数据库执行sql语句,至此,数据库和数据表创建完成;
  • python3 manage.py sqlmigrate books 0001(0001_init.py):打印执行过的sql语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码BEGIN;
--
-- Create model Author
--
CREATE TABLE `books_author` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `first_name` varchar(30) NOT NULL, `last_name` varchar(40) NOT NULL, `email` varchar(254) NOT NULL);
--
-- Create model Book
--
CREATE TABLE `books_book` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `title` varchar(100) NOT NULL, `publication_date` date NOT NULL);
CREATE TABLE `books_book_authors` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `book_id` integer NOT NULL, `author_id` integer NOT NULL);
--
-- Create model Publisher
--
CREATE TABLE `books_publisher` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(30) NOT NULL, `address` varchar(50) NOT NULL, `city` varchar(60) NOT NULL, `state_province` varchar(30) NOT NULL, `country` varchar(50) NOT NULL, `website` varchar(200) NOT NULL);
--
-- Add field publisher to book
--
ALTER TABLE `books_book` ADD COLUMN `publisher_id` integer NOT NULL;
ALTER TABLE `books_book_authors` ADD CONSTRAINT `books_book_authors_book_id_ed3433e7_fk_books_book_id` FOREIGN KEY (`book_id`) REFERENCES `books_book` (`id`);
ALTER TABLE `books_book_authors` ADD CONSTRAINT `books_book_authors_author_id_984f1ab8_fk_books_author_id` FOREIGN KEY (`author_id`) REFERENCES `books_author` (`id`);
ALTER TABLE `books_book_authors` ADD CONSTRAINT `books_book_authors_book_id_author_id_8714badb_uniq` UNIQUE (`book_id`, `author_id`);
ALTER TABLE `books_book` ADD CONSTRAINT `books_book_publisher_id_189e6c56_fk_books_publisher_id` FOREIGN KEY (`publisher_id`) REFERENCES `books_publisher` (`id`);
COMMIT;

1.png

注意:

  1. 自动生成的表名是app名称(books)和模型的小写名称(publisher,book,author)的组合;
  2. Django自动为每个表添加一个id主键,你可以重新设置它;
  3. 按约定,Django添加”_id”后缀到外键字段名,这个同样也可以自定义;
  4. 外键是REFFERENCES语句明确定义的;
  5. django_migrations:该表存放的是迁移过的信息,只要在这里存放过的信息,当修改数据后再次执行migrate,migrate就不会再操作执行过的迁移,我们需要修改生成的initial.py文件和删除django_migrations表中相应的迁移记录,才可以再次进行迁移操作。

四.基本数据访问

一旦创建了模型,Django自动为这些模型提供了高级的Python API。打开python3 manage.py shell并输入下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码>>> from books.models import Publisher
>>> p1 = Publisher(name='Apress', address='2855 Telegraph Avenue',
... city='Berkeley', state_province='CA', country='U.S.A.',
... website='http://www.apress.com/')
>>> p1.save()
>>> p2 = Publisher(name="O'Reilly", address='10 Fawcett St.',
... city='Cambridge', state_province='MA', country='U.S.A.',
... website='http://www.oreilly.com/')
>>> p2.save()
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[<Publisher: Publisher object>, <Publisher: Publisher object>]
  • 首先,导入Publisher模型类,通过这个类我们可以跟包含出版社的数据表进行交互;
  • 接着,创建Publisher实例并设置字段”name,address”等值;
  • 调用对象的save()方法,将对象保存到数据库中,Django会在后台执行一条INSERT语句;
  • 最后,使用”Publisher.objects”属性从数据库中调出出版商的信息,这个属性有很多的方法,比如:Publisher.objects.all()方法获取数据库中”Publisher”类的所有对象,这个操作的幕后实则是Django执行了一条SELECT SQL语句;
  • 这里值得注意的地方是,实例化对象时,Django并未将对象保存至数据库内,除非你调用了”save()”方法;
  • 如果需要一步完成对象的创建和存储至数据库,就调用objects.create()方法。

4.1 添加模块的字符串表现

当我们打印整个publisher列表时,我们没有得到想要的信息,无法把对象区分开来:

1
2
3
4
5
6
7
8
9
shell复制代码System Message: WARNING/2 (<string>, line 872);

Inline literal start-string without end-string.

System Message: WARNING/2 (<string>, line 872);

Inline literal start-string without end-string.

[<Publisher: Publisher object>, <Publisher: Publisher object>]

我们可以简单解决这个问题,只需要为Publisher对象添加一个方法:_unicode_()。_unicode_()方法告诉Python如何将对象以unicode的方式显示出来。_unicode_()方法可以进行任何处理来返回对一个对象的字符串表示。对_unicode_()的唯一要求就是它要返回一个unicode对象,如果”unicode()”方法未返回一个Unicode对象,而返回一个比如说一个整型数字,那么Python将抛出一个”TypeError”错误,并提示”coercing to Unicode:need string or buffer, int found”。

4.2 Unicode对象

概念:你可以认为unicode对象就是一个Python字符串,它可以处理上百万不同类别的字符–从古老版本的Latin字符到非Latin字符再到曲折的引用和艰涩的符号。Unicode对象并没有编码,它们使用一致的通用字符编码集,当你在Python中处理Unicode对象的时候,你可以直接将它们混合使用和互相匹配而不必去考虑编码细节。请确保每一个模型里都包含_unicode_()方法,这不只是为了交互方便,在Django其他地方也要用到_unicode_()来显示对象。最后,_unicode_()也是一个很好的例子来演示我们怎么添加行为到模型里。

五.对数据库数据的操作

5.1 数据库选择对象

  • 比如:p_list = Publisher.objects.all(),这就是从模型Publisher中获取全部的数据;
  • Django在选择数据时并未使用SELECT*,而是显式地列出了所有的字段,因为SELECT*会更慢;
  • 这个语句中,objects被称为管理器,你可以在查找数据时使用它,all()方法返回数据库中的记录,看起来像一个列表,实际上是一个QuerySet对象,这个对象是数据库中一些记录的集合。

5.2 数据库数据过滤

在Django中,我们可以使用filter()方法对数据进行过滤。

Publsher.objects.filter(name=’***‘),我们还可以传递多个参数到filter来缩小选择范围。

Publisher.objetcts.filter(name__contains=’press’),在name和contains之间有双下划线,表明会进行一些魔术般的操作,这里contains部分会被Django翻译成LIKE语句:

1
2
3
mysql复制代码SELECT id, name, address, city, state_province, country, website
FROM books_publisher
WHERE name LIKE '%press%';

其他的一些查找类型有:icontains(大小写无关的LIKE),startwith、endwith,还有range。

获取单个对象:

使用get()方法:Publisher.objects.get(name=”***“),这样就返回了单个对象,如果结果是多个对象,就会抛出异常,查询没有返回结果也会抛出异常。

数据排序:

在Django中,使用order_by()方法就能对返回的结果排序。

如果需要以多个字段作为排序标准,第二个字段会在第一个字段的值相同的情况下被使用到,使用多个参数就可以了。

我们还可以在字段名前加上“-”前缀表示逆向排序但每次都需要使用order_by()方法会显得啰嗦,所以,Django可以让你指定模型的缺省排序方式:

1
2
python复制代码class Meta:
ordering=['name']

class Meta,内嵌于模型类中,你可以在任意一个模型类中使用Meta类,来设置一些与特定模型相关的选项,比如这里的ordering,这样在不指定排序方式的情况下,默认按字段”name”进行排序。

限制返回的数据:

我们可以使用Python的range-slicing语法来取出数据的特定子集。

Publisher.objects.order_by(‘name’)[0:2],但这里不支持Python的负索引,为了反向搜索,可以改变order_by后面的字段为”-字段”即可。

更新数据,删除数据:

使用update()方法更新数据,使用delete()方法删除数据(就在前面的查询结果后加上.delete()即可)。

六.站点管理

url/admin直接进入管理界面。

使用”python manage.py create”创建一个超级用户。

在MIDDLEWARE_CLASSES中添加”django.middleware.locale.LocaleMiddleWare”,就能让浏览器显示中文。

在模型app文件目下的admin.py中写入:

1
2
3
4
5
6
python复制代码from django.contrib import admin
from mysite.books.models import Publisher, Author, Book

admin.site.register(Publisher)
admin.site.register(Author)
admin.site.register(Book)

就能将数据库信息传到admin界面,在admin界面可以对数据库进行增删改查,结果也会同步到本地的数据库.

在字段名中添加属性:blank=True,这使得该字段允许输入空值.

Admin是如何工作的:

当服务启动时,Django从”url.py”引导URLconf,然后执行”admin.autodiscover()”语句,这个函数遍历INSTALLED_APPS配置,并寻找相关的admin.py文件,如果在指定的app目录下找到admin.py文件,它就执行其中的代码。在”books”应用程序目录下的”admin.py”文件中,每次调用”admin.site.register()”都将哪个模块注册到管理工具中。管理工具只为那些明确注册了的模块显示一个编辑/修改的界面。

设置日期型和数字型字段可选:

如果想允许一个日期型或数字型字段为空,需要加上null=True和blank=True。添加null=True改变了数据的语义,因此还需要更新数据库。

自定义字段标签:

1
python复制代码email = models.EmailField(blank=True, verbose_name='e-mail' )

这样就把email修改成了e-mail。但这种方法不适用于ManyToManyField和Foreignkey字段,因为它们的第一个参数必须是模块类,那种情形,必须显式使用verbose_name这个参数名称。

七.自定义ModelAdmin类

7.1 自定义列表

1
2
3
python复制代码class AuthorAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'email')
admin.site.register(Author, AuthorAdmin)

这样就为模型Author添加了一个新的展示信息,在原本姓名的基础上添加了一列邮箱信息,list_display是一个字段名称的元组,用于列表显示,然后用admin.site.register函数追加注册了这个Author模块。当你再次在后台刷新Author模型类,就能看到first_name,last_name,emali三个列用于展示。

另外,点击列头可以对那一列进行排序。接下来,可以添加一个快速查询栏:向AuthorAdmin追加search_fileds,如:

1
2
3
python复制代码class AuthorAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'email')
search_fields = ('first_name', 'last_name')

刷新浏览器,就能在页面顶端看到一个查询栏。接下来为模型book添加一些过滤器:

1
2
3
4
python复制代码class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publisher', 'publication_date')
list_filter = ('publication_date',)
date_hierarchy = 'publication_date'

使用list_filter这个字段元组创建过滤器,它位于列表的右边,过滤器同样适用于其它类型字段,而不单是“日期型”,当有两个以上值时,过滤器就会显示。另外一种过滤日期的方式是使用date_hierarchy选项,如:

1
2
3
4
python复制代码class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publisher', 'publication_date')
list_filter = ('publication_date',)
date_hierarchy = 'publication_date'

注意,date_hierarchy接受的是“字符串”而不是元组,因为只能对一个日期型字段进行层次划分。

7.2 自定义编辑表单

可以通过ModelAdmin子类中的fields选项来改变它。

1
python复制代码fields = ('title', 'authors', 'publisher', 'publication_date')

完成之后,编辑表单将按照指定的顺序显示各字段。通过fields这个选项,你可以排除一些不想被其他人编辑的fields。当你的admi用户只是被信任可以更改你的某一部分数据时,或者,你的数据被一些外部的程序自动处理而改变了,你就可以用这个功能。当一个用户用这个不包含完整信息的表单添加一本新书时,Django会简单地将publication_date设置为none,以确保这个字段满足null=True的条件。

使用filter_horizontal选项(注意移出fields选项),就会看到一个精巧的JavaScript过滤器,它允许你检索选项,然后将所选结果从Available框移到Chose框,还可以移回来。注意filter_horizontal选项只能用在多对多字段。

2.png

使用raw_id_fields选项,它是一个包含外键字段名称的元组,它包含的字段将被展现成“文本框”,而不再是“下拉框”。管理工具提供了一个放大镜图标方便你输入,点击放大镜会弹出一个窗口提供选择。

3.png

八.用户、用户组和权限

用户对象有标准的用户名、密码、邮箱地址和真实姓名,同时它还有关于使用管理界面的权限定义。首先,这里有一组三个布尔型标记:

  • 活动标志,它用来控制用户是否已经激活,如果一个用户账号的这个标记是关闭状态,而用户又尝试用它登录时,即使密码正确,也无法登录系统;
  • 成员标志:它用来控制这个用户是否可以登录管理界面(即:这个用户是不是你们组织里的成员)由于用户系统可以被用于控制公众页面,即非管理页面的访问权限,这个标志可以用来区分公众用户和管理用户;
  • 超级用户标志:它赋予用户在管理界面中添加、修改和删除任何项目的权限。如果一个用户账号有这个标志,那么所有权限设置都会被忽略。

admin模型类中常用属性描述如下:

  • date_hierarchy:设置一个日期类型字段,使其出现在按日期导航找模型实例的界面中;
  • empty_value_display:设置一个字符串,定义空值的显示方式;
  • fields和exclude:分别用于设置需要管理的字段和排除管理的字段;
  • fieldsets:配置字段分组;
  • list_editable:设置字段列表,指定模型中的哪些字段可以编辑;
  • list_per_page:设置一个整数,指定每页显示的实例数量,默认为100;
  • search_fields:设置字段列表,出现一个搜索页面使管理员能够按照这些字段进行实例搜索;
  • ordering:设置字段列表,定义管理页面中模型实例的排序方式。

九.模型中的Meta类

通过模型类中的Meta子类定义模型元数据,比如数据库表名、数据默认排序方式等。

Meta类的属性名由Django预定义,常用的Meta类属性汇总如下:

  • abstract:True or False,标识本类是否为抽象基类;
  • app_label:定义本类所属的应用,比如app_label = ‘myapp’;
  • db_table:映射的数据表名,比如db_table = ‘moments’,如果Meta中不提供db_table字段,则django会为模型自动生成数据表名,生成的格式为”应用名_模型名”,比如应用app的模型Comment的默认数据表名为app_comment;
  • db_tablespace:映射的表空间名;
  • default_related_name:定义本模型的反向关系引用名称,默认与模型名一致;
  • get_latest_by:定义按哪个字段值排列以获得模型的开始或结束记录,本属性值通常指向一个日期或整型的模型字段;
  • managed:True or False,定义Django的manage.py命令行工具是否管理本模型,默认为True,若设为False,在migrate时,则不会生成该模型的数据表,维护需手工维护;
  • ordering:默认排序字段,以升序排列,降序需在字段前面加上负号;
  • default_permissions:模型操作权限,默认=(‘add’,’change’,’delete’);
  • proxy:True或False,本模型及所有继承自本模型的子模型是否为代理模型;
  • required_db_features:定义底层数据库所必需具备的特性;
  • required_db_vendor:定义底层数据库的类型;
  • unique_together:用来设置的不重复的字段组合,必须唯一(可以将多个字段做联合唯一);
  • index_together:定义联合索引的字段,可以设置多个;
  • verbose_name:指明一个易于理解和表述的单数形式的对象名称;
  • verbose_name_plural:指明一个易于理解和表述的复数形式的对象名称。

补充内容

Django有两种过滤器用于筛选记录:

  • filter:返回符合筛选条件的数据集;
  • exclude:返回不符合条件的数据集。

filed lookup(字段查询方式):基本表现形式:字段名称__谓词。

Django谓词表:

谓词 含义
exact 精确等于
iexact 大小写不敏感的等于
contains 模糊匹配
in 包含
gt 大于
gte 大于等于
lt 小于
lte 小于等于
startswith 以……开头
endswith 以……结尾
range 在……范围内
year 年
month 月
day 日
week_day 星期几
isnull 是否为空

Django支持三种风格的模型继承:

  • 抽象类继承:父类继承自models.Model,但不会在底层数据库中生成相应的数据表。父类的属性列存储在其子类的数据表中;
  • 多表继承:多表继承的每个模型类都在底层数据库中生成相应的数据表管理数据;
  • 代理模型继承:父类用于在底层数据库中管理数据表,而子类不定义数据列,只定义查询数据集的排序方式等元数据。

本文转载自: 掘金

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

linux下prometheus+grafana安装

发表于 2021-11-23

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

1.简介

prometheus+grafana为一份监控平台的解决方案。其中可以监控linux服务器,nginx,rabbitmq,springboot项目等,功能非常强大。

prometheus负责在被监控中间件或系统中打入探针(需要安装exporter),同时提供接口对外提供监控数据。

grafana主要为显示页面,其中提供了海量的面板,可以自由获取json文件,样子非常酷炫。
grafana连接prometheus后,调用prometheus提供的接口获取数据,同时填写在模板中,并显示。

2.下载

由于网络原因,自己楼主自己提供网盘地址,怼就完事了。
prometheus
链接:pan.baidu.com/s/13VVo3oAC…
提取码:rfo0

go
链接:pan.baidu.com/s/1Z8b_xzgT…
提取码:2j3v

grafana
链接:pan.baidu.com/s/11QJa4Cjj…
提取码:az3w

3.安装go

1.解压安装

1
js复制代码tar -C /usr/local/ -xvf go1.11.4.linux-amd64.tar.gz

2.配置环境变量

1
2
3
js复制代码vim /etc/profile
export PATH=$PATH:/usr/local/go/bin
source /etc/profile

3.验证

1
js复制代码go version

4.安装Prometheus

1.安装

1
2
js复制代码tar -C /usr/local/ -xvf prometheus-2.6.0.linux-amd64.tar.gz
ln -sv /usr/local/prometheus-2.6.0.linux-amd64/ /usr/local/Prometheus

2.启动

普罗米修斯默认配置文件,vim /usr/local/Prometheus/prometheus.yml(默认后台启动)。

1
js复制代码/usr/local/Prometheus/prometheus --config.file=/usr/local/Prometheus/prometheus.yml &

3.验证

浏览器打开IP:9090端口即可打开普罗米修斯自带的监控页面。
在这里插入图片描述

5.安装Grafana

1.安装(只能使用此种方法安装 否则Grafana命令是不好用的)

1
js复制代码rpm -ivh --nodeps grafana-7.1.5-1.x86_64.rpm

2.启动

1
2
3
js复制代码sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable grafana-server.service
sudo /bin/systemctl start grafana-server.service

3.访问grafana

浏览器访问IP:3000端口,即可打开grafana页面,默认用户名密码都是admin,初次登录会要求修改默认的登录密码,出现以下页面安装成功。

在这里插入图片描述

本文转载自: 掘金

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

你的工具包中不能少了这个神奇——Apache HTTP se

发表于 2021-11-23

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

简介

ab是一个Apache Http服务器基准测试工具。它可以测试HTTP服务器每秒最多可以处理多少请求。如果测试的是web应用服务,这个结果可以转换成整个应用每秒可以满足多少请求。

缺点

这是一个非常简单的工具,用途比较有限,只能针对单个URL进行尽可能快的压力测试。

Windows下如何下载安装(Linux安装十分简单)

Apache HTTP server benchmarking tool(ab)下载地址

资源 2.4版本

📎httpd-2.4.48-o111k-x64-vc15.zip

解压移动至C盘

管理员身份运行CMD,进入bin目录,执行httpd -k install,将Apache安装为一个服务。

启动报错 443端口被占用(每个人遇到的问题可能不一样,耐心解决网上都有解决问题的办法呀!)

什么是443端口

443端口即网页浏览端口,主要是用于HTTPS服务,是提供加密和通过安全端口传输的另一种HTTP。在一些对安全性要求较高的网站,比如银行、证券、购物等,都采用HTTPS服务,这样在这些网站上的交换信息,其他人抓包获取到的是加密数据,保证了交易的安全性。网页的地址以https://开始,而不是常见的http://。


解决办法分别修改 conf\extra目录下的两个配置文件中绑定的端口为444/442

  1. httpd-ahssl.conf
  2. httpd-ssl.conf

netstat-ano 查看端口占用情况,发现443确实被占用了,我们选择一个在这里面未被使用的端口

修改配置文件conf\httpd.conf,默认端口80改为8080(或其他未占用的端口)

在服务中右键启动服务

访问安装地址http://localhost:8080/ ,看到如下页面则访问成功

参数说明

详细内容请查看官网文档《ab参数说明官网》

部分常用参数说明

参数 说明
-n 测试会话中执行请求的根数,默认为1
-c 一次产生的请求个数,默认1(并发请求数)
-t 测试所持续的最大时间(秒),其内部隐含值(-n 50000)。可以用来对某个测试制定一个固定的时间范围,默认,没有时间限制。
-p 需要POST的数据的文件
-T POST数据所使用的Content-type头信息
-v 设置显示信息的详细程度 - 4或更大值会显示头信息, 3或更大值可以显示响应代码(404, 200等), 2或更大值可以显示警告和其他信息。 -V 显示版本号并退出。
-w 以HTML表的格式输出结果。默认是白色背景的两列宽度的一张表。
-i 执行HEAD请求,而不是GET
-C cookie-name=value 对请求附加一个Cookie:行。name=value的一个参数对,此参数可以重复。
-P proxy-auth-username:password 对一个中转代理提供BASIC认证信任。用户名和密码由一个:隔开,并以base64编码形式发送。无论服务器是否需要(即, 是否发送了401认证需求代码),此字符串都会被发送

输出代码说明

结合一个输出样例作说明,ab -c 10 -n 10000 http://localhost:8888/test.html

输出key 输出值 说明
Server Software Liziba 服务器返回Server的值
Server Hostname localhost 服务器主机名
Server Port 8888 服务器端口
Document Path /test.html 测试的页面文档地址
Document Length 242313 bytes 文档大小
Concurrency Level 10 并发数
Time taken for tests 2.661 seconds 整个测试持续时间
Complete requests 10000 完成的请求数量
Failed requests 0 失败的请求数量
Total transferred 2423880000 bytes 整个场景中的网络传输量
HTML transferred 2423130000 bytes 整个场景中HTML的网络传输量
Requests per second 3757.39 [#/sec] (mean) 每秒完成请求(每秒事务数),重点指标,mean表示平均值
Time per request 2.661 [ms] (mean) 平均事务响应时间,mean表示平均值
Time per request 0.266 [ms] (mean, across all concurrent requests) 每个请求实际运行时间的平均值
Transfer rate 889400.26 [Kbytes/sec] received 平均每秒网络上的流量,可以帮助排除是否存在网络流量过大导致响应时间延长的问题
Connection Times (ms) min mean[+/-sd] median…… 网络上消耗时间的分解,具体请查看官网
Percentage of the requests served within a certain time (ms) …… 整个场景中所有请求的响应情况。在场景中每个请求都有一个响应时间,其中50%响应时间小于3ms,66%的响应时间小于3ms…(上图)

本文转载自: 掘金

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

1…225226227…956

开发者博客

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