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

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


  • 首页

  • 归档

  • 搜索

跨平台导PDF,结合wkhtmltopdf很顺手

发表于 2021-01-22

前言

好东西要分享,之前一直在使用wkhtmltopdf进行pdf文件的生成,常用的方式就是先安装wkhtmltopdf,然后在程序中用命令的方式将对应的html生成pdf文件,简单而且方便;但重复的编码使得想在wkhtmltopdf基础上进行封装,偶然间发现有小伙伴已经封装的还不错啦,常用的功能都已经实现,源码地址:github.com/fpanaccia/W…

作者将其打包成Nuget包(Wkhtmltopdf.NetCore),直接引入使用即可;

正文

既然用到了.NetCore,肯定就要考虑到跨平台兼容性,对于wkhtmltopdf之前一直是在Windows上使用,还没有在其他平台尝试;这个包封装的行不行,拉出来遛遛就知道啦,接下来就试试:

1. 建个API项目,引入包和兼容对应平台的wkhtmltopdf执行文件;

image-20210120162314648

注: 默认依赖的wkhtmltopdf执行文件需要存放在Rotativa目录下,可以自定义名称,如果自定义,需要再注册服务时指定对应的文件名;这里的wkhtmltopdf已经根据不同平台进行编译打包了,无需安装,这些文件在源码那就有;

2.创建PDFTestController控制器,添加如下接口进行测试;

首先把生成pdf的服务注入进来,后续直接使用就可以啦:

image-20210120162846169

接下来就开始写接口啦,这里只是测试,代码冗余没有考虑,在实际项目中小伙伴可以根据自己需求进行封装;

  • ExportPDFByHtml 接口,用html直接生成pdf文件,但这里没有保存,以文件流的形式访问,通过浏览器查看文件,可以自行下载;html模板在实际开发过程中可以单独用文件存储;

image-20210120163444523

  • SavePDFByHtml接口,直接保存文件,文件名可以根据需要进行自定义;

image-20210120163838699

  • TestMarginAndPageSize接口,设置Margin和PageSize参数,其他参数也可以设置;

image-20210120164237413

ConvertOptions默认封装了以下属性,小伙伴也可以自定义扩展,只要继承IConvertOptions即可,这里就不演示的,因为官方有对应的案例,下伙伴下去搞搞,wkhtmltopdf的参数挺多的,都可以进行封装使用。

image-20210120170024098

  • ExportByRazorView使用Razor视图的方式进行pdf文件生成,此库已经支持cshtml文件的读取

image-20210120164613249

根据指定视图生成对应的pdf效果,如下:

image-20210120164735123

  • ExportByRazorViewData数据动态绑定,既然支持视图,那就应该支持Razor语法,一般常用的就是数据绑定了,上面是静态的,接下来来个动态绑定的。

image-20210120165238787

根据指定视图生成对应的pdf效果,如下:

image-20210120165449173

如上基本的使用演示就说那么多,使用还是很简单,小伙伴后续可以根据自己的需要进行相关扩展;当然还有其他功能,比如设置页眉/页脚等,作者提供有对应的案例;这里不说那么多,不然又是长文。

3. 小伙伴用的时候可能会遇到的问题

  • 在开发调试运行项目时,会报找不到wkhtmltopdf文件,那是因为运行时的确找不到对应的文件,将对应Rotativa下的文件设置为始终复制即可:

image-20210120170946016

  • 在Windows下怎么玩都没问题啦,开始发布到Linux(我用的centos 7),我擦,莫名其妙的错。

image-20210120171410682

看见这个错我懵的,一顿搜索猛如虎,还是没找到答案;冷静下来,重新捋捋,原来是自己在犯傻;

两个问题需要解决,1.上传到Linux下的wkhtmltopdf没有给执行权限;2.可能环境缺少对应的依赖库;

设置可执行权限

在Linux环境下,可以通过ll命令查看权限,刚开始是没有权限的,只需要执行chmod 777 wkhtmltopdf 命令,执行权限就有了,如下图中红框中的x就是可执行权限;

image-20210120171951473

安装缺少的依赖库

可执行权限开启之后,别急着去访问页面,这样可能还是错误。因为可能缺少依赖库,那咋知道缺少呢,我是直接执行wkhtmltopdf,执行成功就没啥,不成功就会报缺少相关依赖,然后直接安装就行啦;执行./wkhtmltopdf https://www.baidu.com ./test.pdf试试就知道啦,因为wkhtmltopdf本身是可以单独运行的,并不依赖我们写的程序。

  • 当执行成功之后,然后开始访问接口导出功能,如果不出意外,遇到中文就产生乱码啦,那是因为Linux环境下缺少相关的字体文件,将对应的字体文件拷贝到Linux上即可,字体我找好了,下载地址如下:

链接: pan.baidu.com/s/1jikC0DUk… 提取码: tn4j ;

将下载下来的字体解压,然后拷贝到Linux下的 /usr/share/fonts目录下即可

​ image-20210120173348468

最后这样应该就没啥问题啦,剩下的就交给小伙伴自己摸索搞实践吧;

​此文源码地址:github.com/zyq025/DotN…

​wkhtmltopdf官网地址​:wkhtmltopdf.org/

总结

使用还是很简单的,常规的需求没啥问题,如果需要功能定制化,小伙伴可以参考源码,自己封装一个(封装思路不难的); 如果小伙伴有比较好的导出库,免费开源的那种,一起分享出来玩玩。

一个被程序搞丑的帅小伙,关注”Code综艺圈”跟我一起学~

本文转载自: 掘金

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

python按照list中字典的某key去重 一、需求说明

发表于 2021-01-22

python按照list中字典的某key去重

  • 一、需求说明
  • 二、我的解决方案
  • 写在最后

一、需求说明

当我们写爬虫的时候,经常会遇到json格式的数据,它通常是如下结构:

1
2
3
4
python复制代码data = [{'name':'小K','score':100},
{'name':'小J','score':98},
{'name':'小Q','score':95},
{'name':'小K','score':100}]

很显然名字为小K的数据重复了,我们需要进行去重。通常对于list的去重,我们可以用set()函数,即:

1
python复制代码data = list(set(data))

然而,运行之后你会发现它报错了:

在这里插入图片描述

list里的数据不能是dict类型,那么该怎么办呢?

二、我的解决方案

定义一个去重的函数即可,根据里面的某个key,对数据进行筛选去重:

1
2
3
4
5
6
7
8
python复制代码def DelRepeat(data,key):
new_data = [] # 用于存储去重后的list
values = [] # 用于存储当前已有的值
for d in data:
if d[key] not in values:
new_data.append(d)
values.append(d[key])
return new_data

参数data为需要去重的list,key为去重的健(即按照哪个key来去重),去重后结果为:

1
2
3
python复制代码[{'name': '小K', 'score': 100},
{'name': '小J', 'score': 98},
{'name': '小Q', 'score': 95}]

成功去重!

写在最后

最后,感谢各位大大的耐心阅读~

创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗 (๑◕ܫ←๑)

本文转载自: 掘金

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

头条二面:详述一条 SQL 的执行过程 ----------

发表于 2021-01-21

前言

天天和数据库打交道,一天能写上几十条 SQL 语句,但你知道我们的系统是如何和数据库交互的吗?MySQL 如何帮我们存储数据、又是如何帮我们管理事务?….是不是感觉真的除了写几个 「select * from dual」外基本脑子一片空白?这篇文章就将带你走进 MySQL 的世界,让你彻底了解系统到底是如何和 MySQL 交互的,MySQL 在接受到我们发送的 SQL 语句时又分别做了哪些事情。

MySQL 驱动

我们的系统在和 MySQL 数据库进行通信的时候,总不可能是凭白无故的就能接收和发送请求,就算是你没有做什么操作,那总该是有其他的“人”帮我们做了一些事情,基本上使用过 MySQL 数据库的程序员多多少少都会知道 MySQL 驱动这个概念的。就是这个 MySQL 驱动在底层帮我们做了对数据库的连接,只有建立了连接了,才能够有后面的交互。看下图表示

这样的话,在系统和 MySQL 进行交互之前,MySQL 驱动会帮我们建立好连接,然后我们只需要发送 SQL 语句就可以执行 CRUD 了。一次 SQL 请求就会建立一个连接,多个请求就会建立多个连接,那么问题来了,我们系统肯定不是一个人在使用的,换句话说肯定是存在多个请求同时去争抢连接的情况。我们的 web 系统一般都是部署在 tomcat 容器中的,而 tomcat 是可以并发处理多个请求的,这就会导致多个请求会去建立多个连接,然后使用完再都去关闭,这样会有什么问题呢?如下图

!

java 系统在通过 MySQL 驱动和 MySQL 数据库连接的时候是基于 TCP/IP 协议的,所以如果每个请求都是新建连接和销毁连接,那这样势必会造成不必要的浪费和性能的下降,也就说上面的多线程请求的时候频繁的创建和销毁连接显然是不合理的。必然会大大降低我们系统的性能,但是如果给你提供一些固定的用来连接的线程,这样是不是不需要反复的创建和销毁连接了呢?相信懂行的朋友会会心一笑,没错,说的就是数据库连接池。

数据库连接池
维护一定的连接数,方便系统获取连接,使用就去池子中获取,用完放回去就可以了,我们不需要关心连接的创建与销毁,也不需要关心线程池是怎么去维护这些连接的。


常见的数据库连接池有 Druid、C3P0、DBCP,连接池实现原理在这里就不深入讨论了。采用连接池大大节省了不断创建与销毁线程的开销,这就是有名的「池化」思想,不管是线程池还是 HTTP 连接池,都能看到它的身影。

数据库连接池

到这里,我们已经知道的是我们的系统在访问 MySQL 数据库的时候,建立的连接并不是每次请求都会去创建的,而是从数据库连接池中去获取,这样就解决了因为反复的创建和销毁连接而带来的性能损耗问题了。业务系统是并发的,但是 MySQL 的数据库的接受请求还只是一个啊。

其实 MySQL 的架构体系中也已经提供了这样的一个池子,也是数据库连池。双方都是通过数据库连接池来管理各个连接的,这样一方面线程之前不需要是争抢连接,更重要的是不需要反复的创建的销毁连接。

至此系统和 MySQL 数据库之间的连接问题已经说明清楚了。那 MySQL 数据库中的这些连接是怎么来处理的,又是谁来处理呢?

网络连接必须由线程来处理

对计算基础稍微有一点了解的的同学都是知道的,网络中的连接都是由线程来处理的,所谓网络连接说白了就是一次请求,每次请求都会有相应的线程去处理的。也就是说对于 SQL 语句的请求在 MySQL 中是由一个个的线程去处理的。

那这些线程会怎么去处理这些请求?会做哪些事情?

SQL 接口

MySQL 中处理请求的线程在获取到请求以后获取 SQL 语句去交给 SQL 接口去处理。

查询解析器

假如现在有这样的一个 SQL

1
SQL复制代码SELECT stuName,age,sex FROM students WHERE id=1

​ 但是这个 SQL 是写给我们人看的,机器哪里知道你在说什么?更夸张的是机器只认识 0 和 1。这个时候解析器就上场了。他会将 SQL 接口传递过来的 SQL 语句进行解析,翻译成 MySQL 自己能认识的语言,至于怎么解析的就不需要在深究了,无非是自己一套相关的规则。

image-20210105100644871
现在 SQL 已经被解析成 MySQL 认识的样子的,那下一步是不是就是执行吗?理论上是这样子的,但是 MySQL 的强大远不止于此,他还会帮我们选择最优的查询路径。

什么叫最优查询路径
就是 MySQL 会按照自己认为的效率最高的方式去执行查询

具体是怎么做到的呢?这就要说到 MySQL 的查询优化器了

MySQL 查询优化器

查询优化器内部具体怎么实现的我们不需要是关心,我需要知道的是 MySQL 会帮我去使用他自己认为的最好的方式去优化这条 SQL 语句,并生成一条条的执行计划,比如你创建了多个索引,MySQL 会依据成本最小原则来选择使用对应的索引,这里的成本主要包括两个方面, IO 成本和 CPU 成本

IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关

CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。

MySQL 优化器 会计算 「IO 成本 + CPU」 成本最小的那个索引来执行

画外音:索引成本具体怎么计算,请参考这篇文章

image-20210105100903587

优化器执行选出最优索引等步骤后,会去调用存储引擎接口,开始去执行被 MySQL 解析过和优化过的 SQL 语句

存储引擎

查询优化器会调用存储引擎`的接口,去执行 SQL,也就是说真正执行 SQL 的动作是在存储引擎中完成的。数据是被存放在内存或者是磁盘中的(存储引擎是一个非常重要的组件,后面会详细介绍)

本片文章大家先对存储引擎有一个大致的认识就可以了。后续专门文章来详细介绍的。

执行器

执行器是一个非常重要的组件,因为前面那些组件的操作最终必须通过执行器去调用存储引擎接口才能被执行。执行器最终最根据一系列的执行计划去调用存储引擎的接口去完成 SQL 的执行

初识存储引擎

我们以一个更新的SQL语句来说明,SQL 如下

1
SQL复制代码UPDATE students SET stuName = '小强' WHERE id = 1

当我们系统发出这样的查询去交给 MySQL 的时候,MySQL 会按照我们上面介绍的一系列的流程最终通过执行器调用存储引擎去执行,流程图就是上面那个。在执行这个 SQL 的时候 SQL 语句对应的数据要么是在内存中,要么是在磁盘中,如果直接在磁盘中操作,那这样的随机IO读写的速度肯定让人无法接受的,所以每次在执行 SQL 的时候都会将其数据加载到内存中,这块内存就是 InnoDB 中一个非常重要的组件:缓冲池 Buffer Pool

Buffer Pool

Buffer Pool (缓冲池)是 InnoDB 存储引擎中非常重要的内存结构,顾名思义,缓冲池其实就是类似 Redis 一样的作用,起到一个缓存的作用,因为我们都知道 MySQL 的数据最终是存储在磁盘中的,如果没有这个 Buffer Pool 那么我们每次的数据库请求都会磁盘中查找,这样必然会存在 IO 操作,这肯定是无法接受的。但是有了 Buffer Pool 就是我们第一次在查询的时候会将查询的结果存到 Buffer Pool 中,这样后面再有请求的时候就会先从缓冲池中去查询,如果没有再去磁盘中查找,然后在放到 Buffer Pool 中,如下图

image-20210105101150038

按照上面的那幅图,这条 SQL 语句的执行步骤大致是这样子的

  1. innodb 存储引擎会在缓冲池中查找 id=1 的这条数据是否存在
  2. 发现不存在,那么就会去磁盘中加载,并将其存放在缓冲池中
  3. 该条记录会被加上一个独占锁(总不能你在修改的时候别人也在修改吧,这个机制本篇文章不重点介绍,以后会专门写文章来详细讲解)

undo 日志文件:记录数据被修改前的样子

undo 顾名思义,就是没有做,没发生的意思。undo log 就是没有发生事情(原本事情是什么)的一些日志

我们刚刚已经说了,在准备更新一条语句的时候,该条语句已经被加载到 Buffer pool 中了,实际上这里还有这样的操作,就是在将该条语句加载到 Buffer Pool 中的时候同时会往 undo 日志文件中插入一条日志,也就是将 id=1 的这条记录的原来的值记录下来。

这样做的目的是什么?

Innodb 存储引擎的最大特点就是支持事务,如果本次更新失败,也就是事务提交失败,那么该事务中的所有的操作都必须回滚到执行前的样子,也就是说当事务失败的时候,也不会对原始数据有影响,看图说话

这里说句额外话,其实 MySQL 也是一个系统,就好比我们平时开发的 java 的功能系统一样,MySQL 使用的是自己相应的语言开发出来的一套系统而已,它根据自己需要的功能去设计器对应的功能,它即然能做到哪些事情,那么必然是设计者们当初这么定义或者是根据实际的场景变更演化而来的。所以大家放平心态,把 MySQL 当作一个系统去了解熟悉他。

到这一步,我们的执行的 SQL 语句已经被加载到 Buffer Pool 中了,然后开始更新这条语句,更新的操作实际是在Buffer Pool中执行的,那问题来了,按照我们平时开发的一套理论缓冲池中的数据和数据库中的数据不一致时候,我们就认为缓存中的数据是脏数据,那此时 Buffer Pool 中的数据岂不是成了脏数据?没错,目前这条数据就是脏数据,Buffer Pool 中的记录是小强 数据库中的记录是旺财 ,这种情况 MySQL是怎么处理的呢?

redo 日志文件:记录数据被修改后的样子

除了从磁盘中加载文件和将操作前的记录保存到 undo 日志文件中,其他的操作是在内存中完成的,内存中的数据的特点就是:断电丢失。如果此时 MySQL 所在的服务器宕机了,那么 Buffer Pool 中的数据会全部丢失的。这个时候 redo 日志文件就需要来大显神通了

画外音: redo 日志文件是 InnoDB 特有的,他是存储引擎级别的,不是 MySQL 级别的

redo 就是准备去做、将要去做的意思,redo log 就是记录的是将要做的一些操作,例如,此时将要做的是update students set stuName='小强' where id=1; 那么这条操作就会被记录到 redo log buffer 中,啥?怎么又出来一个 redo log buffer ,很简单,MySQL 为了提高效率,所以将这些操作都先放在内存中去完成,然后会在某个时机将其持久化到磁盘中。

截至目前,我们应该都熟悉了 MySQL 的执行器调用存储引擎是怎么将一条 SQL 加载到缓冲池和记录哪些日志的,流程如下:

  1. 准备更新一条 SQL 语句
  2. MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载到缓冲池(BufferPool)中
  3. 在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo 日志文件中
  4. innodb 会在 Buffer Pool 中执行更新操作
  5. 更新后的数据会记录在 redo log buffer 中

上面说的步骤都是在正常情况下的操作,但是程序的设计和优化并不仅是为了这些正常情况而去做的,也是为了那些临界区和极端情况下出现的问题去优化设计的

这个时候如果服务器宕机了,那么缓存中的数据还是丢失了。真烦,竟然数据总是丢失,那能不能不要放在内存中,直接保存到磁盘呢?很显然不行,因为在上面也已经介绍了,在内存中的操作目的是为了提高效率。

此时,如果 MySQL 真的宕机了,那么没关系的,因为 MySQL 会认为本次事务是失败的,所以数据依旧是更新前的样子,并不会有任何的影响。

好了,语句也更新好了那么需要将更新的值提交啊,也就是需要提交本次的事务了,因为只要事务成功提交了,才会将最后的变更保存到数据库,在提交事务前仍然会具有相关的其他操作

将 redo Log Buffer 中的数据持久化到磁盘中,就是将 redo log buffer 中的数据写入到 redo log 磁盘文件中,一般情况下,redo log Buffer 数据写入磁盘的策略是立即刷入磁盘(具体策略情况在下面小总结出会详细介绍),上图

如果 redo log Buffer 刷入磁盘后,数据库服务器宕机了,那我们更新的数据怎么办?此时数据是在内存中,数据岂不是丢失了?不,这次数据就不会丢失了,因为 redo log buffer 中的数据已经被写入到磁盘了,已经被持久化了,就算数据库宕机了,在下次重启的时候 MySQL 也会将 redo 日志文件内容恢复到 Buffer Pool 中(这边我的理解是和 Redis 的持久化机制是差不多的,在 Redis 启动的时候会检查 rdb 或者是 aof 或者是两者都检查,根据持久化的文件来将数据恢复到内存中)

到此为止,从执行器开始调用存储引擎接口做了哪些事情呢?

1.准备更新一条 SQL 语句
2.MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载
到缓冲池(BufferPool)中 3.在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo 日志文件中
4.innodb 会在 Buffer Pool 中执行更新操作 5.更新后的数据会记录在 redo log buffer 中

—————————–到此是上面已经总结过的———————————-

6.MySQL 提交事务的时候,会将 redo log buffer 中的数据写入到 redo 日志文件中
刷磁盘可以通过 innodb_flush_log_at_trx_commit 参数来设置
值为 0 表示不刷入磁盘
值为 1 表示立即刷入磁盘
值为 2 表示先刷到 os cache
7.myslq 重启的时候会将 redo 日志恢复到缓冲池中

截止到目前位置,MySQL 的执行器调用存储引擎的接口去执行【执行计划】提供的 SQL 的时候 InnoDB 做了哪些事情也就基本差不多了,但是这还没完。下面还需要介绍下 MySQL 级别的日志文件bin log

bin log 日志文件:记录整个操作过程

上面介绍到的redo log是 InnoDB 存储引擎特有的日志文件,而bin log属于是 MySQL 级别的日志。
redo log记录的东西是偏向于物理性质的,如:“对什么数据,做了什么修改”。bin log是偏向于逻辑性质的,类似于:“对 students 表中的 id 为 1 的记录租了更新操作”
两者的主要特点总结如下:

性质 redo Log bin Log
文件大小 redo log 的大小是固定的(配置中也可以设置,一般默认的就足够了) bin log 可通过配置参数max_bin log_size设置每个bin log文件的大小(但是一般不建议修改)。
实现方式 redo log是InnoDB引擎层实现的(也就是说是 Innodb 存储引起过独有的) bin log是 MySQL 层实现的,所有引擎都可以使用 bin log日志
记录方式 redo log 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志。 bin log 通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上
使用场景 redo log适用于崩溃恢复(crash-safe)(这一点其实非常类似与 Redis 的持久化特征) bin log适用于主从复制和数据恢复

** bin log文件是如何刷入磁盘的?**

bin log的刷盘是有相关的策略的,策略可以通过sync_bin log来修改,默认为0,表示先写入 os cache,也就是说在提交事务的时候,数据不会直接到磁盘中,这样如果宕机bin log数据仍然会丢失。所以建议将sync_bin log设置为 1 表示直接将数据写入到磁盘文件中。

刷入bin log有以下几种模式

1、 STATMENT

基于 SQL 语句的复制(statement-based replication, SBR),每一条会修改数据的 SQL 语句会记录到 bin log 中

【优点】:不需要记录每一行的变化,减少了 bin log 日志量,节约了 IO , 从而提高了性能

【缺点】:在某些情况下会导致主从数据不一致,比如执行sysdate()、slepp()等

2、ROW

基于行的复制(row-based replication, RBR),不记录每条SQL语句的上下文信息,仅需记录哪条数据被修改了

【优点】:不会出现某些特定情况下的存储过程、或 function、或 trigger 的调用和触发无法被正确复制的问题

【缺点】:会产生大量的日志,尤其是 alter table 的时候会让日志暴涨

3、MIXED

基于 STATMENT 和 ROW 两种模式的混合复制( mixed-based replication, MBR ),一般的复制使用 STATEMENT 模式保存 bin log ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 bin log

那既然bin log也是日志文件,那它是在什么记录数据的呢?

其实 MySQL 在提交事务的时候,不仅仅会将 redo log buffer 中的数据写入到redo log 文件中,同时也会将本次修改的数据记录到 bin log文件中,同时会将本次修改的bin log文件名和修改的内容在bin log中的位置记录到redo log中,最后还会在redo log最后写入 commit 标记,这样就表示本次事务被成功的提交了。

如果在数据被写入到bin log文件的时候,刚写完,数据库宕机了,数据会丢失吗?

首先可以确定的是,只要redo log最后没有 commit 标记,说明本次的事务一定是失败的。但是数据是没有丢失了,因为已经被记录到redo log的磁盘文件中了。在 MySQL 重启的时候,就会将 redo log 中的数据恢复(加载)到Buffer Pool中。

好了,到目前为止,一个更新操作我们基本介绍得差不多,但是你有没有感觉少了哪件事情还没有做?是不是你也发现这个时候被更新记录仅仅是在内存中执行的,哪怕是宕机又恢复了也仅仅是将更新后的记录加载到Buffer Pool中,这个时候 MySQL 数据库中的这条记录依旧是旧值,也就是说内存中的数据在我们看来依旧是脏数据,那这个时候怎么办呢?

其实 MySQL 会有一个后台线程,它会在某个时机将我们Buffer Pool中的脏数据刷到 MySQL 数据库中,这样就将内存和数据库的数据保持统一了。

本文总结

到此,关于Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及关系就基本差不多了。

我们再回顾下
1.Buffer Pool 是 MySQL 的一个非常重要的组件,因为针对数据库的增删改操作都是在 Buffer Pool 中完成的
2.Undo log 记录的是数据操作前的样子
3.redo log 记录的是数据被操作后的样子(redo log 是 Innodb 存储引擎特有)
4.bin log 记录的是整个操作记录(这个对于主从复制具有非常重要的意义)

从准备更新一条数据到事务的提交的流程描述

1.首先执行器根据 MySQL 的执行计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中
2.在数据被缓存到缓存池的同时,会写入 undo log 日志文件
3.更新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中
4.完成以后就可以提交事务,在提交的同时会做以下三件事
5.(第一件事)将redo log buffer中的数据刷入到redo log文件中
6.(第二件事)将本次操作记录写入到 bin log文件中
7.(第三件事)将bin log文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记

至此表示整个更新事务已经完成

结束语

到此为止,系统是如何和 MySQL 数据库打交道,提交一条更新的 SQL 语句到 MySQL,MySQL 执行了哪些流程,做了哪些事情从宏观上都已经讲解完成了。更多的 Buffer Pool 的细节将会在之后的文章中详解

本文转载自: 掘金

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

21面试必问!Goroutine的调度原理 一 gorou

发表于 2021-01-21

并发能力,让你的程序可以由若干个代码片段组合而成,并且每个片段都是独立运行的。Go语言天生支持这种并发能力,而Goroutine就是Go原生支持并发的具体实现。无论是Go的运行时还是用户写的代码都是运行在Goroutine中。

Goroutine是由Go运行时管理的轻量级线程。和操作系统线程相比,goroutine的资源占用和使用代价非常小。我们可以创建成百上百万个goroutine。Go运行时负责对goroutine进行管理和调度。

其实也不要让—“管理和调度”吓住,你可以简单理解,什么时候哪个goroutine将获得资源开始执行,哪个goroutine应该停止执行让出资源,哪个goroutine应该被唤醒回复执行等。这样的理解和现实生活中是一模一样的。goroutine的调度模型和原理,对于编写出优雅而高质量的代码是大有益处的。因此,我们要研究一下。

一 goroutine调度器

调度:操作系统中对进程、线程的调度。操作系统调度器会将系统中的多个线程按照一定算法调度到物理CPU上去运行。如C、C++等并发实现多是基于线程模型的,就是应用程序负责创建线程(libpthread等库函数去实现),操作系统负责调度线程。这种模式就是有一些不足:

  • 复杂
+ 1)写过C、C++的人肯定都知道,这里有多么的复杂,利用libpthread库中的API创建一个线程的时候,虽然要传入的参数很多,但是还可以接受。一旦涉及到线程的退出,那就要考虑新创建的线程是否要与主线程分离,还是需要主线程等待子线程终止并获取其终止状态?或者是否需要在新线程中设置取消点以保证被主线程取消的时候能顺利退出。
+ 2)并发执行单元互相通信困难,容易出错:多个线程间的通信有很多机制,但用起来也是很复杂的;一旦用到共享内存,那就是各种锁机制,导致死锁,更是很轻松就做到的。
+ 3)线程栈需要开发人员选择默认的,还是自定义设置。
  • 难度
+ 1)我们使用线程的代价要比进程小很多,但是依然不能大量创建线程,除了每个线程占用的资源不小之外,操作系统调度切换线程的代价也很大。
+ 2)很多服务端程序,由于不能大量创建线程,只能选择在少量线程里做网络多路复用的方案(epoll/kqueue/IoCompletionPort这种机制),或者你会说可以用libevent和libev啊,这样的写法存在大量的钩子回调,给开发人员带来不小的负担。

看到上面的痛点,Go采用了Goroutine来解决这些痛点。Goroutine占用资源非常小,每个Gorouine栈的大小默认是2k字节。goroutine调度的切换也不用在操作系统内核中完成,代价很低。所以一个Go程序可以创建成千上万个并发的goroutine,而把这些goroutine按照一定算法放到cpu上执行的程序,我们就成为goroutine调度器(Scheduler)。

一个Go程序运行起来,在操作系统看来就是一个用户程序,操作系统的概念,只有线程,它甚至不知道有Goroutine的存在。Goroutine的调度完全靠GO自己完成。实现GO程序内Goroutine之间的公平竞争CPU的资源,这个任务就靠GO运行时(runtime)了,一个Go程序中,除了用户层代码,其他就是go运行时了。

二 Go调度器模型与演化过程

第一版 G-M 模型
2012.3.28日,Go1.0正式发布,Go团队实现了一个简单的goroutine调度器。在这个调度其中,每个goroutine对应于运行时中的一个抽象结构G(Goroutine),另外一个结构体是M(Machine),它被看成是“物理CPU”的操作系统线程。这个模型实现起来比较简单,且工作正常,但是也有一些问题,最重要的是限制了GO并发程序的伸缩性,如下几个方面:

  1. 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有goroutine相关操作。如创建、重新调度都要加锁。
  2. goroutine传递问题:M经常在M之间传递“可运行”的goroutine,这导致调度延迟增大及额外的性能损耗;
  3. 每个M都做内存缓存,导致内存占用过高,数据局部性交差。
  4. 由于系统调用而形成的频繁的工作线程阻塞和解阻塞,导致额外性能损耗。

第二版 G-P-M 模型
基于第一版的问题,在Go1.1中实现了G-P-M模型和work stealing算法,这个模型一直沿用。


我们看到在G-M中增加了一个P,这个P是何方神圣呢? P是一个“逻辑Processor”,每个G要想真正运行起来,都需要被分配到一个P,即进入到P的本地运行队列中,先暂时忽略全局队列。对于G来说,P就是运行它的“CPU”,在G看来只有P。但从调度器的角度看,真正的“CPU”是M,只有将P和M绑定才能让P中的G真正的运行起来。这样的P与M的关系,类似Linux操作系统中用户线程和内核线程的对应关系(N*M)

3 抢占式调度

有了G-P-M模型,是很大的进步,但是仍有一个问题,它不支持抢占式调度,一旦某个G中出现死循环的代码逻辑,那么G将永久占用分配给他的P和M,而在同一个P中的其他G永远不能被调度,出现其他G被“饿死”的情况。在Go1.2中实现了“抢占式”调度。

抢占式的原理是在每个函数或方法的入口,加一段额外的代码,让运行时有机会价差是否需要执行抢占调度。这种解决方案只能局部解决“饿死”问题。对于没有函数调用而是存算法魂环的计算的G,仍然无法实现抢占。

4 NUMA调度模型
从Go1.2后,Go将重点放在对GC的低延迟的优化上,只是一些小的改动。

5 其他优化
Go运行时已经实现了netpoller(morsmachine.dk/netpoller),…,%E5%8D%B3%E4%BE%BFG%E5%8F%91%E8%B5%B7%E7%BD%91%E7%BB%9CI/O%E6%93%8D%E4%BD%9C%EF%BC%8C%E4%B9%9F%E4%B8%8D%E4%BC%9A%E5%AF%BC%E8%87%B4M%E8%A2%AB%E9%98%BB%E5%A1%9E%EF%BC%88%E4%BB%85%E9%98%BB%E5%A1%9EG%EF%BC%89,%E4%BB%8E%E8%80%8C%E4%B8%8D%E4%BC%9A%E5%AF%BC%E8%87%B4%E5%A4%A7%E9%87%8F%EF%BC%88M%EF%BC%89%E8%A2%AB%E5%88%9B%E5%BB%BA%E5%87%BA%E6%9D%A5%E3%80%82%E4%BD%86%E6%98%AF%E5%AF%B9%E4%BA%8E%E5%B8%B8%E8%A7%84I/O%E6%93%8D%E4%BD%9C%E4%B8%80%E6%97%A6%E9%98%BB%E5%A1%9E%EF%BC%8C%E9%82%A3%E4%B9%88%E7%BA%BF%E7%A8%8B%EF%BC%88M%EF%BC%89%E5%B0%86%E8%BF%9B%E5%85%A5%E6%8C%82%E8%B5%B7%E7%8A%B6%E6%80%81%EF%BC%8C%E7%AD%89%E5%BE%85I/O%E8%BF%94%E5%9B%9E%E5%90%8E%E8%A2%AB%E5%94%A4%E9%86%92%E3%80%82%E8%BF%99%E6%97%B6%EF%BC%8CP%E5%B0%86%E4%B8%8E%E6%8C%82%E8%B5%B7%E7%9A%84M%E5%88%86%E7%A6%BB%EF%BC%8C%E5%86%8D%E9%80%89%E6%8B%A9%E4%B8%80%E4%B8%AA%E5%A4%84%E4%BA%8E%E7%A9%BA%E9%97%B2%E7%9A%84M.%E5%A6%82%E6%9E%9C%E6%AD%A4%E6%97%B6%E6%B2%A1%E6%9C%89%E7%A9%BA%E9%97%B2%E7%9A%84M,%E5%88%99%E6%96%B0%E5%BB%BA%E4%B8%80%E4%B8%AAM,%E8%BF%99%E5%B0%B1%E6%98%AF%E4%B8%BA%E4%BD%95%E5%A4%A7%E9%87%8FI/O%E6%93%8D%E4%BD%9C%E4%BC%9A%E5%AF%BC%E8%87%B4%E5%A4%A7%E9%87%8F%E7%BA%BF%E7%A8%8B%E8%A2%AB%E5%88%9B%E5%BB%BA%E7%9A%84%E5%8E%9F%E5%9B%A0%E3%80%82)

三 对Go调度器深入了解

1. G、P、M
1
bash复制代码    G、P、M的定义,在 $GOROOT/src/runtime/runtime2.go 源文件中。

G、P、M这三个结构体定义都是很繁重的,每个结构体定义都包含十几甚至二、三十个字段。像调度器这样的核心代码都是非常复杂的,考虑的因素也很多。简单说明一下:

G: 它是Goroutine,存储了Goroutine的执行栈信息、Goroutine状态以及Goroutine的任务函数等(G是可以重用的)。

P: 它是逻辑Processor,P的数量决定了系统内最大可并行的G的数据(物理CPU核数>=P的数量);P最大的作用是它有各种G对象队列、链表、缓存和状态。

M: 它是真正执行计算的资源。在绑定有效的P后,一个调度循环开始;而调度循环的机制是从各种队列、P的本地运行队列中获取G,切换到G的执行栈上并行执行G的函数,调用goexit做清理工作,然后回到M。这样反复。M并不保存G的状态,这是G可以跨M调度的基础。

2. G被抢占调用调度

操作系统是按时间片调度线程的,Go并没有时间片的概念。如果某个G没有进行系统调用、没有I/O操作、没有阻塞在一个channel上,那么M是怎么让G停下来并调度下一个可运行的G?
这就要说抢占调度了。

上面说了,除非是无限死循环,否则只要G调用函数,Go运行时就有了抢占G的机会。GO程序启动的时候,运行时会启动一个名为sysmon的M(你可以简单理解为监控器或监控协程),该M特殊之处就是其无需绑定P即可运行(以g0的形式),该M在整个Go程序的运行过程中非常重要。

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
scss复制代码//$GOROOT/src/runtime/proc.go

// The main goroutine.
func main() {
... ...
systemstack(func() {
newm(sysmon, nil)
})
.... ...
}

// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
// If a heap span goes unused for 5 minutes after a garbage collection,
// we hand it back to the operating system.
scavengelimit := int64(5 * 60 * 1e9)
... ...

if .... {
... ...
// retake P's blocked in syscalls
// and preempt long running G's
if retake(now) != 0 {
idle = 0
} else {
idle++
}
... ...
}
}

从上面源代码可以看到symon每20us—10ms启动一次,sysmon主要工作:

1
2
3
4
5
css复制代码释放闲置超过5分钟的span物理内存;
如果超过2分钟没有垃圾回收,强制执行;
将长时间未处理的netpoll结果添加到任务队列;
向长时间运行的G任务发出抢占调度;
收回因syscall长时间阻塞的P;
3. channel阻塞或网络I/O下的调度

如果G被阻塞在某个channel操作或者网络I/O操作上的时候,G会被放入到某个等待队列中,而M会尝试运行P的下一个可运行的G;如果此时P没有可运行的G工M运行,那么M将接绑P,并进入挂起状态。当I/O或者channel操作完成,在等待队列中的G会被唤醒,标记为可运行,并被放入到某个P队列中,绑定一个M后继续运行。

4. 系统调用阻塞情况下,如何调度

如果G被阻塞在某个系统调用上,那么不仅仅G会阻塞,执行G的M也会解绑P,与G一起进入骨气状态。如果此时有空闲的M,则P和与其绑定继续执行其他的G;如果没有空闲的M,但还是有其他G去执行,那么会创建一个新M。当系统调用返回后,阻塞在该系统调用上的G会尝试获取一个可用的P,如果没有可用的P,那么这个G会被标记为runnable,之前的那个挂起的M将再次进入挂起状态。

本文转载自: 掘金

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

一个成熟的Java项目如何优雅地处理异常 (一)概述 (二)

发表于 2021-01-20

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)概述

异常处理是一个系统最重要的环节,当一个项目变得很大的时候,异常处理和日志系统能让你快速定位到问题。对于用户或者接口调用者而言,优雅的异常处理可以让调用者快速知道问题所在。本文将介绍如何优雅地处理异常。

(二)使用通用的返回体

我们希望所有的错误都以Json的方式返回给客户,因此拿出上次写的通用返回体,新建一个类CommonResult记录返回体。

1
2
3
4
5
6
7
8
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {
private int code;
private String message;
private Object data;
}

新建一个枚举类ResponseCode集成code和message。

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复制代码public enum ResponseCode {

// 系统模块
SUCCESS(0, "操作成功"),
ERROR(1, "操作失败"),
SERVER_ERROR(500, "服务器异常"),

// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),
MAIL_SEND_SUCCESS(10003, "邮件发送成功"),

// 用户模块 2xxxx
NEED_LOGIN(20001, "登录失效"),
USERNAME_OR_PASSWORD_EMPTY(20002, "用户名或密码不能为空"),
USERNAME_OR_PASSWORD_WRONG(20003, "用户名或密码错误"),
USER_NOT_EXISTS(20004, "用户不存在"),
WRONG_PASSWORD(20005, "密码错误"),
;

ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

(三)自定义运行时异常

自定义一个运行时异常类,构造方法传入异常参数即可。

1
2
3
4
5
6
7
java复制代码public class MyException extends RuntimeException{
private String msg;

public MyException(String msg) {
super(msg);
}
}

(四)编写一个统一的异常处理类

异常处理类是整个异常处理核心,SpringBoot中提供了ControllerAdvice注解来拦截异常,使用RestControllerAdvice注解保证了返回Json格式。

如果拦截到的异常属于MyException,则按Json格式返回错误结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RestControllerAdvice
public class ExceptionController {

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = Exception.class)
public CommonResult exceptionHandler(Exception e){
//如果抛出的异常属于自定义异常,就以JSON格式返回
if (e instanceof MyException){
return new CommonResult(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getMsg(),"自定义的错误为:"+e.getMessage());
}
//如果都不是就打印出异常的信息
return new CommonResult(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getMsg(),"错误的信息为:"+e.getMessage());
}
}

(五)测试

为了看初效果,这里手动抛出一个异常来测试,新建IndexController,手动抛出异常

1
2
3
4
5
6
7
8
java复制代码@RestController
public class IndexController {

@RequestMapping(value = "/index",method = RequestMethod.GET)
public String index(){
throw new MyException("测试");
}
}

查看调用结果:

在这里插入图片描述

(六)对实体类的校验

有这样一个场景,登陆注册时用户名和密码有长度限制,手机号有格式限制,如果不满足要求就无法注册。这个功能前端可以限制,但是对于后端接口而言,也需要进行限制,万一前端没有限制住呢。

导入两个校验依赖包:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码<!--校验-->
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.0.Final</version>
</dependency>

编写实体类,在每个属性上加上校验包的验证参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Data
public class Register {

@Length(max = 20,min = 4,message = "用户名长度需要在4到20个字符之间")
@NotBlank(message = "用户名不能为空")
private String username;

@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3|4|5|8][0-9]\\d{8}$",message = "电话号码格式不正确")
private String phone;

@Length(max = 20,min = 4,message = "密码长度需要在4到20个字符之间")
@NotBlank(message = "密码不能为空")
private String password;
}

我们在需要使用的方法中增加@Valid注解进行校验,比如这个post请求中我要校验。

1
2
3
4
5
6
java复制代码@PostMapping("/register")
public CommonResult register(@Valid @RequestBody Register register){
//一连串注册的业务
userService.registerUser(register);
return new CommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),"");
}

@Valid在校验失败的情况下会报出参数不合法的异常,还是在统一的异常处理类中捕获异常,如果是MethodArgumentNotValidException,就取出对应的message数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@RestControllerAdvice
public class ExceptionController {

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = Exception.class)
public CommonResult exceptionHandler(Exception e){
//如果属于参数校验异常,就抛出校验的错误
if (e instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException methodArgumentNotValidException= (MethodArgumentNotValidException) e;
return new CommonResult(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getMsg(),
"校验错误:"+methodArgumentNotValidException.getBindingResult().getFieldError().getDefaultMessage());
}//如果是自定义的异常,就给出具体的异常原因
else if (e instanceof MyException){
return new CommonResult(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getMsg(),"自定义的错误为:"+e.getMessage());
}
//如果都不是就打印出异常的信息
return new CommonResult(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getMsg(),"错误的信息为:"+e.getMessage());
}
}

(七)测试校验

接下来就可以测试校验的功能了,通过postman访问

在这里插入图片描述

如果输入参数不满足之前的设置,就会给出具体的错误信息。而不是抛出让人无法接收的报错:

在这里插入图片描述

(八)总结

许多人写代码时最不考虑的就是异常处理,简单地实现需求就好了,所以才会导致许多不可预估的bug出现。好了,本期文章就到这里了,我们下期再见。

本文转载自: 掘金

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

《PyCharm使用系列》-如何使用Pycharm编写项目

发表于 2021-01-20

创建第一个PyCharm项目

环境准备:安装 Python

因为我们即将创建的是一个Python项目,所以Python是必须安装的。

创建新项目
step1: 双击打开 PyCharm,点击 Create New Project:

step2:选择 Pure Python,提供要创建项目的位置,我把这个项目放在了我电脑的这个路径下 /User/xuxh/PycharmProjects/ 并给这个项目起名为 demo:

上图左侧面板,列出了 PyCharm 支持的所有项目类型, 除了 Pure Python 我们最为常用的项目类型,也包括现在比较流行的Web 框架 Django and Flask, 也支持 Google App Engine 框架, 同时也支持多种当前广泛应用的前端 Web 框架, 比如Angular、React 等等。总之, PyCharm 支持的项目类型是非常丰富的。

点击箭头,Pycharm 会找到之前安装的 Python 解释器。选择解释器, 点击 Create 按钮。

返回主页面,看到初始的项目结构有三部分组成:

step3:右击项目名,新建一个 Python 文件: New -> Python File:

输入文件名 “hello_world",然后回车即可创建 hello_world.py 文件。

step4:写一个简单的程序,打印 “hello world!!!”:

step5:运行程序时需要右键点击编辑区 ,选择 Run 'hello_world'( 也可点击菜单 Run -> Run 'hello_world')。

step6:进入运行控制台, 我们可以看到 PyCharm 已经输出了 “Hello World!!!”。

使用 PyCharm 创建与管理项目

项目(Project)概念

无论在PyCharm中做什么,都会在项目的上下文中执行。项目是表示完整软件解决方案的组织单位。主要作用就是起到一个项目定义、范围约束、规范类型的效果,这个概念听着有些拗口。现在我们可以简单地把项目理解为是一个单纯的目录。

项目文件

当你创建一个新的项目,PyCharm 会自动生成一个 .idea 文件夹。这个文件夹的主要作用在于存放项目的控制信息,包括版本信息,历史记录等等。
.idea 目录在项目视图中是不可见的的, 如果要看到 .idea 目录的内容,可以选择 “Project” 工具栏, 点击下右侧的箭头, 选择 Project Files:

下面是 .idea 目录中的内容:

我们分别来看下这几个文件都有哪些作用:

  • .iml 文件:描述项目结构;
  • workspace.xml 文件:包含与工作区相关的参数与选项;
  • xml 文件:每个 xml 文件负责其自己的设置集,可以通过其名称识别:projectCodeStyle.xml、encoding.xml、vcs.xml 等等。

项目类型

针对不同的项目类型,PyCharm 会自动生成一系列的项目文件、库及 .idea 目录。在创建新项目的界面中,可以看到以下的项目类型,这些项目类型都是 PyCharm 所支持的:

下面介绍主要的几种项目类型:

  • Pure Python:用于纯 Python 编程。此类项目的目录结构包含特定于 PyCharm 的设置、项目文件、库以及 .idea 目录。需要提前安装 Python,上节课我们创建的 demo 就是一个 Pure Python 项目。
  • Django:此项目类型提供 Django 应用程序的特定基础结构以及所有必要的文件和设置。使用 Django 应用程序需要数据库,IDE 已经预配置了 SQLite,如果使用其他数据库引擎,需要提前安装并配置正确。
  • Flask:此项目类型提供 Flask 应用程序的特定基础结构以及所有必要的文件和设置。
  • Google App Engine:此项目类型提供 Google 应用程序引擎应用程序的特定基础结构,以及所有必要的文件和设置。
  • 客户端项目:HTML5 Boilerplate, Bootstrap, and Foundation 都属于客户端项目。 对于客户端应用程序,PyCharm 创建特定的基础结构,并带有所需的文件和目录。

创建项目

创建新项目

在 PyCharm 的主界面中依次点击: File -> New Project:

来到 Welocme to PyCharm 窗口, 选择 Create New Project :

导入项目从源文件

  • 在 PyCharm 的主界面中依次点击: File -> Open;
  • 在打开的对话框中,选择包含所需源代码的目录;
  • 选定要打开的项目之后单击右下角的 “Open” 按钮;
  • 在弹出的窗口中指定是在单独的窗口中打开新项目,还是关闭当前项目并在当前窗口打开。

管理项目

项目配置

项目配置最主要包括配置项目结构与解释器, 项目结构定义了整个项目包含文件, 解释器指定了程序运行依赖的python 环境是什么。正确的配置上述两项,才能保证程序的正常运行。具体如何配置,后续有单独的章节详细介绍。

Mac 系统依次点击:PyCharm -> Preferences;

Windows and Linux 系统依次点击:File -> Settings;

除此以外, 不仅可以为当前项目配置设置,还可以为以后将创建的所有项目配置项目设置:File -> New Projects Settings -> Preferences for New Projects。

打开项目

可以从 Welocme to PyCharm 窗口打开项目, 可以直接选择 Open 。除此以外,PyCharm保留最近项目的历史记录列表,您可以从左侧列表选择所需的项目打开。

也可以通过主菜单 File -> Open Recent 打开项目。

PyCharm 是允许同时打开多个项目的, 当你已经打开一个项目,然后再打开另外一个项目, 会有下面的弹窗。

New Window and This Window: 每个项目都在其自己的窗口中打开。项目是独立的,不能共享信息,但剪贴板操作除外。所有项目都在同一个 PyCharm 实例中运行,并使用相同的内存空间。
Attach: 新打开的项目与已打开的窗口共享同一窗口。已打开的项目被视为主项目,并且始终首先显示在”项目”工具窗口中。所有其他项目都添加到主项目中。如图所示:

关闭项目

通过主菜单 File -> Close Project。

当只有一个打开的项目并关闭它时,PyCharm 将显示 Welcome 界面。如果有多个项目被打开,PyCharm 只会关闭当前的项目。
另外, 如果要在一个窗口中分离已附加到主项目的任何项目,只需从”项目视图”中删除目标项目。"Project"工具窗口, 要删除项目右键单击,选择 Remove from Project View:

项目之间切换

PyCharm 是允许在不同窗口打开多个项目的,如何在多个打开项目中切换呢?使用下面的命令:

1
2
sql复制代码Window -> Next Project Window 
Window -> Previous Project Window

清除缓存

PyCharm 缓存了大量文件,长时间运行下来,数据量是很大的,也比较占用系统资源。所以在某些情况下我们需要清除缓存 。例如,一个短期项目结束将来不再使用;解决某些冲突的唯一方法也是通过清除缓存。File -> Invalidate Caches/Restart

文章持续更新,可以微信搜索「极客夜读 」第一时间阅读,更多Python学习文章,大家的关注就是更新的动力!

本文转载自: 掘金

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

厉害!基于SpringBoot和Vue的企业级中后台开源项目

发表于 2021-01-20

简介

SmartAdmin由河南·洛阳 1024创新实验室团队研发的一套互联网企业级的通用型中后台解决方案!使用最前沿的前后台技术栈SpringBoot和Vue,前后端分离,我们开源一套漂亮的代码和一套整洁的代码规范,让大家在这浮躁的代码世界里感受到一股把代码写好的清流!同时又让开发者节省大量的时间,减少加班,快乐工作,热爱生活。SmartAdmin 让你从认识到忘不了,绝对是你最想要的!

技术体系

  • 前端:Vue + Vue-Router + Vuex + ViewUI + vue-enum
  • 后端:SpringBoot2 + Mybatis-plus + jwt + druid + mysql
  • 前端代码规范smart-front-standard -guide
  • 基于阿里规范之上的后端规范smart-backend-standard-guide

演示图

图片)​

图片)​

图片)​

图片)​

图片)​

图片)​

前端特点

  • 高质量的代码、代码结构、和代码注释
  • 漂亮的UI,菜单栏、标签页,体验、交互更好用的员工、部门、角色、菜单管理等等
  • 优化基于Keepalive的标签页,做到标签页该缓存的时候缓存,比如左右切换等,不该缓存的时候不缓存,比如新建,表单提交结束等
  • 前端常量维护: vue-enum,拒绝出现魔法数字,代码不可维护的现象
  • 全新的基于前端的权限设计(忘掉传统的权限设计吧,已经不适合这个前端时代)
  • 基于websocket的在线人数
  • 支持一级、二级、三级菜单,四级菜单以及搜索功能
  • 其他功能:邮件、富文本、消息、系统配置等等
  • 写不完了,太多好的细节需要你的发现……

后端特点

  • 高质量的Java代码、分包结构、和代码注释
  • 业内独创的请求返回码维护,非常值得一看
  • 基于一个注解和controller的权限设计放弃更复杂的shiro,以及一套数据权限支持
  • 四层架构(controller, service, manager, dao)
  • 代码阅读性强、扩展性极高的员工、部门、角色、菜单管理
  • 基于LRU策略的内存级权限缓存
  • 配合前端vue-enum的swagger文档注解
  • 心跳服务,让你发现有哪些机器再跑,哪些人在偷偷的跑你的Job
  • 自定义的quartz job添加和修改,方便测试人员测试
  • smart-reload,为系统预留钩子,动态加载,在不重启程序前提下执行一些代码,你懂的
  • 以上只是一些举例,更多灿若繁星的惊喜和细节,等待着你的发现!

前端代码规范

  • 文件、文件夹、目录结构、组建、变量等等怎么命名
  • html、css、less等如何规范
  • vue项目目录结构如何划分
  • router和store该怎么划分扩展性更好
  • vue组件规范该选择哪些
  • 以及更多,数不胜数让你觉得实用,同时身心愉悦的规范
  • **源码地址获取: q群:**1142951706

后端代码规范

  • 四层架构(controller, service, manager, dao) 是什么,为什么要有四层
  • 各个层的代码该怎么写才能让团队配合默契,高度一致
  • vo, bo, dto, entity ,各种javabean 怎么区分和使用
  • spring的 @Transactional 你用对了吗
  • 方法参数个数、注释、todo这些也要有规范,你遵守过吗
  • 以上举例,只是沧海一粟,更多的细节等待你的发现!

本文转载自: 掘金

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

为什么牛逼的程序员都不用 “ ! = null " 做判空?

发表于 2021-01-20

问题

为了避免空指针调用,我们经常会看到这样的语句

1
2
3
4
5
java复制代码...
if (someobject != null) {
someobject.doCalc();
}
...

最终,项目中会存在大量判空代码,多么丑陋繁冗!如何避免这种情况?我们是否滥用了判空呢?

精华回答

这是初、中级程序猿经常会遇到的问题。他们总喜欢在方法中返回null,因此,在调用这些方法时,也不得不去判空。另外,也许受此习惯影响,他们总潜意识地认为,所有的返回都是不可信任的,为了保护自己程序,就加了大量的判空。

吐槽完毕,回到这个题目本身,进行判空前,请区分以下两种情况:

1、null 是一个有效有意义的返回值(Where null is a valid response in terms of the contract; and)

2、null是无效有误的(Where it isn’t a valid response.)
你可能还不明白这两句话的意思,不急,继续往下看,接下来将详细讨论这两种情况

先说第2种情况

null就是一个不合理的参数,就应该明确地中断程序,往外抛错误。这种情况常见于api方法。例如你开发了一个接口,id是一个必选的参数,如果调用方没传这个参数给你,当然不行。你要感知到这个情况,告诉调用方“嘿,哥们,你传个null给我做甚”。
相对于判空语句,更好的检查方式有两个

(1)assert语句,你可以把错误原因放到assert的参数中,这样不仅能保护你的程序不往下走,而且还能把错误原因返回给调用方,岂不是一举两得。(原文介绍了assert的使用,这里省略)

(2)也可以直接抛出空指针异常。上面说了,此时null是个不合理的参数,有问题就是有问题,就应该大大方方往外抛。

第1种情况会更复杂一些

这种情况下,null是个”看上去“合理的值,例如,我查询数据库,某个查询条件下,就是没有对应值,此时null算是表达了“空”的概念。这里给一些实践建议:

1、假如方法的返回类型是collections,当返回结果是空时,你可以返回一个空的collections(empty list),而不要返回null,这样调用侧就能大胆地处理这个返回,例如调用侧拿到返回后,可以直接print list.size(),又无需担心空指针问题。

(什么?想调用这个方法时,不记得之前实现该方法有没按照这个原则?所以说,代码习惯很重要!如果你养成习惯,都是这样写代码(返回空collections而不返回null),你调用自己写的方法时,就能大胆地忽略判空)

2、返回类型不是collections,又怎么办呢?

那就返回一个空对象(而非null对象),下面举个“栗子”,假设有如下代码

1
2
3
4
5
6
7
java复制代码public interface Action {
void doSomething();
}

public interface Parser {
Action findAction(String userInput);
}

其中,Parse有一个接口FindAction,这个接口会依据用户的输入,找到并执行对应的动作。假如用户输入不对,可能就找不到对应的动作(Action),因此findAction就会返回null,接下来action调用doSomething方法时,就会出现空指针。

解决这个问题的一个方式,就是使用Null Object pattern(空对象模式)

我们来改造一下类定义.如下,这样定义findAction方法后,确保无论用户输入什么,都不会返回null对象

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MyParser implements Parser {
private static Action DO_NOTHING = new Action() {
public void doSomething() { /* do nothing */ }
};

public Action findAction(String userInput) {
// ...
if ( /* we can't find any actions */ ) {
return DO_NOTHING;
}
}}

对比下面两份调用实例

1、冗余:每获取一个对象,就判一次空

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码Parser parser = ParserFactory.getParser();
if (parser == null) {
// now what?
// this would be an example of where null isn't (or shouldn't be) a valid response
}
Action action = parser.findAction(someInput);
if (action == null) {
// do nothing
}
else {
action.doSomething();
}

2、精简
ParserFactory.getParser().findAction(someInput).doSomething();

因为无论什么情况,都不会返回空对象,因此通过findAction拿到action后,可以放心地调用action的方法。

其他回答精选

如果要用equal方法,请用object<不可能为空>.equal(object<可能为空>))例如

1
java复制代码"bar".equals(foo)

而不是用

1
java复制代码foo.equals("bar")

Java8或者guava lib中,提供了Optional类,这是一个元素容器,通过它来封装对象,可以减少判空,不过代码量还是不少。不爽。

如果你想返回null,请挺下来想一想,这个地方是否更应该抛出一个异常

原文:blog.csdn.net/lizeyang/ar…

清山绿水始于尘,博学多识贵于勤。
我有酒,你有故事吗?
欢迎一起谈天说地,聊Java。 回复「vip课程」,获取一套价值19820 元的 java vip课程

本文转载自: 掘金

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

Websocket实现WSS

发表于 2021-01-20

注意点:

  1. 如果网站使用HTTPS,WebSocket必须要使用wss协议
  2. 使用wss协议的连接请求必须只能写域名,而非IP+端口
  3. 通过nginx转发实现wss,内部通信就不需要使用wss了

Nginx 配置

  1. 只需要在HTTPS配置的server内加一个location即可
  2. Nginx反向代理,无论是HTTP/S或是WebSocket都会走443端口,由Nginx分发给各个项目服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码# 代理方式实现
location = /websocket {
proxy_pass http://xxxx.cn:7303;

proxy_redirect off;
proxy_set_header Host www.xxx.cn:7303;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# 反向代理用户信息
#proxy_set_header X-real-ip $remote_addr;
#proxy_set_header X-Forwarded-For $remote_addr;
}

Laravel

Server

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

namespace App\Console\Commands;

use Illuminate\Console\Command;

class WebSocket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'Web {action=start}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'WebSocket description';

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$arg = $this->argument('action');
switch ($arg) {
case 'start':
$this->info('WebSocket observer started');
$this->start();
break;
}
}

public function start()
{
// ssl方式
//$server = new \swoole_websocket_server("0.0.0.0", 7303, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$server = new \swoole_websocket_server("0.0.0.0", 7303);

$server->set([
//后台作为守护进程运行 'daemonize' => 1
'daemonize' => 1,
//配置SSL证书和密钥路径
//'ssl_cert_file' => "/data/php/cert/www.xxxxx.cn.pem",
//'ssl_key_file' => "/data/php/cert/www.xxxxx.cn.key"
]);

$server->on('Open', array($this, 'OnOpen'));
$server->on('Message', array($this, 'OnMessage'));
$server->on('Close', array($this, 'OnClose'));
$server->start();
}

public function OnOpen($server, $req)
{
echo "server: handshake success with fd {$req->fd}\n";
}

public function OnMessage($server, $frame)
{
var_dump($frame);
$this->send($server, $frame);
}

public function OnClose($server, $fd)
{
echo "client {$fd} closed \n";
}

public function send($server, $frame)
{
$info = json_decode($frame->data, true);
// var_dump(php_sapi_name());
switch ($info['action']) {
case 'home':
case 'device':
case 'goodsError':
case 'shopping':
$conn_list = $server->connection_list(0, 100);
if ($conn_list === false or count($conn_list) === 0) {
echo "finish \n";
}
foreach ($conn_list as $fd) {
if ($fd != $frame->fd) {
$arr = array("action" => $info['action'], "data" => $info['data']);
$data = json_encode($arr);
$server->push($fd, $data);
}
}
break;
}
}
}

Client

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
php复制代码<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class WebClient extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'SendSocket {data}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'WebClient description';

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$data = $this->argument('data');

$cli = new \swoole_http_client('127.0.0.1', 7303);

$cli->setHeaders(['Trace-Id' => md5(time()),]);

$cli->on('message', function ($_cli, $frame) {
var_dump($frame);
});

$cli->upgrade('/', function ($cli) use ($cli, $data) {
$cli->push($data);
$cli->close();
});
}
}

WEB

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
scss复制代码# 地址后面必须加 /websocket
let ws = new WebSocket("wss://www.xxx.com:7300/websocket');

ws.onopen = function(evt) {
console.log("Connection open ...");
// ws.send("Hello WebSockets!");
};


// 在指定页面接取到消息指定刷新操作
ws.onmessage = function (evt) {
let hashValue = window.location.hash.split('/');
let hashLength = hashValue . length;
let newKey = hashValue[hashLength - 1];
console . log("Received Message: " + evt . data);
// ws.close();
let data = JSON . parse(evt . data);
if (data.action === 'home' && newKey === '') {
console . log('home-socket');
//home页监控
$this.homeSocket();
} else if (data.action === 'home' && newKey === 'shoppingMonitor') {
console.log('shopping-home-socket')
//购物车监控
$this.shoppingMonitorSocket();
} else if (data . action === 'shopping' && newKey === 'shoppingMonitor') {
console.log('shopping-socket');
//购物车监控
$this . shoppingMonitorSocket();
} else if (data . action === 'goodsError') {
console.log('goodsError-socket');
}
}

ws.onclose = function(e){
  //当客户端收到服务端发送的关闭连接请求时,触发onclose事件
  console.log("close");
}

ws.onerror = function(e){
  //如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
  console.log(error);
}

本文转载自: 掘金

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

【对线面试官】多线程基础

发表于 2021-01-20

1
2
3
4
5
6
7
8
9
10
java复制代码// 请求直接交给线程池来处理
public void push(PushParam pushParam) {
try {
pushServiceThreadExecutor.submit(() -> {
handler(pushParam);
});
} catch (Exception e) {
logger.error("pushServiceThreadExecutor error, exception{}:", e);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;

public long getCount() {
return count;
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

++count;
// To something else...
}
}

文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的。

文章中写到的处理线程安全的思路每一项技术都可以写出一篇文章,AQS、Synchronized、Atomic…周末肝起来!下周再来给大家安排!

从上一篇文章反馈中,还是有部分同学没看明白,基础的知识我确实写过文章讲解过啦,但有的同学就是不爱去翻。

为了让大家有更好的体验,我把基础文章也找出来(重要的知识点我还整理过电子书,比如说像多线程、集合这种面试必考的)

我把这些上传到网盘,你们有需要直接下载就好了。做到这份上了,不点个在看、转发、点赞这不行了吧?不会还想白嫖吧?

链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom

欢迎关注我的微信公众号【Java3y】来聊聊Java面试


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

本文转载自: 掘金

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

1…732733734…956

开发者博客

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