1 Star 0 Fork 0

JohnsonL / hexo

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
content.json 177.30 KB
一键复制 编辑 原始数据 按行查看 历史
JohnsonL 提交于 2021-01-02 21:35 . Site updated: 2021-01-02 21:35:31
{"pages":[{"title":"关于我","text":"欢迎来到我的博客","link":"/about/me.html"}],"posts":[{"title":"PHP正则表达式","text":"前言不知道你们有没有这个感觉,看正则表达式就像看天文数字一样,什么电话号码、邮箱的正则表达式,上网复制一下粘贴下来就搞定了。完全不知道这写的是什么玩意。后来我自己也想学一下,因为感觉用处还是挺大的。看了看视频,额…真**简单。这里的话如果想看视频学习的话我推荐一下慕课网这门鬼斧神工之正则表达式课程,上手真的太快了。好了,废话不多说,开始搞事情。 基本语法界定符:标识一个正则表达式的开始和结束,用’/‘或’#’或’{ }’,因为语法’{ }’也可能是正则表达式的运算符,为了避免混淆,所以不建议使用。建议的用法如下: 12$pattern = '/[0-9]/'; //我喜欢这个,看起来比较简洁 $pattern = '#[0-9]#'; 原子: 可见原子:Unicode编码表中可用键盘输出后肉眼可见的字符,例如:标点 ; . / ? 或者英文字母,汉字等等可见字符不可见原子:Unicode编码表中可用键盘输出后肉眼不可见的字符,例如:换行符 \\n,Tab制表符\\t, 空格等等, 一般只用这三个(换行符一般和其他字符一起匹配,因为只有换行符是匹配不到的)小提示:匹配运算符前面需要加 '\\' 例如:’+’ 号,匹配的话需要写出 '\\+' 元字符 原子的筛选方式: | 匹配两个或者多个分支选择[] 匹配方括号中的任意一个原子[^] 匹配除方括号中的原子之外的任意字符;例子:Duang|duang 或者 [Dd]uang 都可以匹配到Duang和duang 区间写法:[a-z]匹配a到z的字符, [0-9]匹配0到9的字符。也可以[a-z0-9]. 匹配除换行符之外的任意字符\\d 匹配任意一个十进制数字,即{0-9]\\D 匹配任意一个非十进制数字[^0-9] 相当于[^\\d]\\s 匹配一个不可见的原子,即[\\f\\n\\r\\t\\v]\\S 匹配一个可见的原子,即[^\\f\\n\\r\\t\\v],相当于[^\\s]z\\w 匹配任意一个数字、字母或下划线,即[0-9a-zA-Z_]\\W 匹配任意一个非数字、字母或下划线,[^0-9a-zA-Z_],相当于[^\\w] 量词 {n} 表示其前面的原子刚好出现了n次。[n] 表示其前面的原子最少出现n次{n,m} 最少出现n次,最多出现m次* 匹配0次、一次或者多次,即{0,}+ 匹配一次或多次,即{1,}? 匹配0或1次,即{0,1} 边界控制 ^ 匹配字符串开始的位置$ 匹配字符串结尾的位置例:^John 可以匹配到:John 但是匹配不到:123John,因为规定了字符串以John开头 模式单元 {} 匹配其中的整体为一个原子 修正模式贪婪匹配 匹配结果存在歧义时取其长(默认) 懒惰匹配 匹配结果存在歧义时取其短,只需在正则表达式的后面’/‘加上’U’,例如’/[0-9]/U’;例子: 12345$subject = "test__123123123";preg_match('/test.+123/', $subject, $matches); //贪婪模式 var_dump($matches);preg_match('/test.+123/U', $subject, $matches); //懒惰模式var_dump($matches); 常见的修正模式: U 懒惰匹配i 忽略英文字母的大小写x 忽略正则表达式的空白符s 让元字符’.’ 匹配包括换行符在内的所有字符 常用函数preg_match 执行匹配正则表达式 preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] ) : int pattern: 要搜索的模式,字符串类型。 subject:输入字符串。 match: 如果提供了参数matches,它将被填充为搜索结果,数据结构为一维数组。 flags: 可以设置为PREG_OFFSET_CAPTURE,使用搜索结果的第0个元素为匹配的字符串,第1个元素为对应的偏移量(位置) offset: 搜索从目标字符串的起始位置开始匹配。 返回值:匹配次数类似函数preg_match_all,参数与preg_match一致区别: preg_match:只匹配一次,搜索结构match的数据结果为一维数组 preg_match_all:匹配全部,搜索结果match的数据结构为二维数组。 preg_replace执行一个正则表达式搜索和替换,返回值为替换后的字符串 preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed pattern:要搜索的模式。可以是一个字符串或字符串数组。 replacement:用于替换的字符串或字符串数组 subject:要进行搜索和替换的字符串或字符串数组。 limit:替换的最大次数。默认是 -1(无限)。 count:替换次数。类似函数preg_filter,参数与preg_replace一致区别(使用数组进行匹配的时候才看得出区别): preg_replace:不管是否有替换,返回全部结果 preg_filter:只返回匹配的结果。 preg_split通过一个正则表达式分隔字符串 preg_split ( string $pattern , string $subject [, int $limit = -1 [, int $flags = 0 ]] ) : array $pattrn:用于搜索的模式,字符串形式。 subject:输入字符串 limit:将限制分隔得到的子串最多只有limit个,返回的最后一个 子串将包含所有剩余部分。 flags:有以下标记的组合: PREG_SPLIT_NO_EMPTY: 返回分隔后的非空部分。 PREG_SPLIT_DELIM_CAPTURE: 用分隔符’()’括号把匹配的捕获并返回。 PREG_SPLIT_OFFSET_CAPTURE: 匹配返回时将会附加字符串偏移量 PREG_SPLIT_DELIM_CAPTURE这个参数可能比较难明白,举个例子看看: 12345$subject = "1a23b"; $a = preg_split('/[\\d]/', $subject, -1, PREG_SPLIT_NO_EMPTY); var_dump($a); $a = preg_split('/([\\d])/', $subject, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); var_dump($a); 输出如下: array (size=2) 0 => string ‘a’ (length=1) 1 => string ‘b’ (length=1)array (size=5) 0 => string ‘1’ (length=1) 1 => string ‘a’ (length=1) 2 => string ‘2’ (length=1) 3 => string ‘3’ (length=1) 4 => string ‘b’ (length=1) preg_grep返回匹配模式的数组条目 preg_grep ( string $pattern , array $input [, int $flags = 0 ] ) : array $pattern:要搜索的模式,字符串形式 $input:输入数组 flags:如果不设置则返回匹配的数目,设置PREG_GREP_INVERT则返回不匹配的数目。 preg_quote转义正则表达式字符,返回为转义后的字符串 preg_quote ( string $str [, string $delimiter = NULL ] ) : string str:输入字符串 delimiter:需要转义的字符串","link":"/2019/05/19/basic/PHP%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"title":"一看就懂的快速排序","text":"概念快速排序属于交换排序,主要步骤是使用基准元素进行比较,把小于基准元素的移动到一边,大于基准元素的移动到另一边。从而把数组分成两部分,然后再从这两部分中选取出基准元素,重复上面的步骤。过程如下: 紫色:基准元素绿色:大于基准元素黄色:小于基准元素 这种思路叫做分治法。 基准元素基准元素的选取可随机选取。下面使用中我会使用第一位的元素作为基准元素。 排序过程排序拆分过程如下图: 紫色为基准元素,(每一轮都重新选取)绿色为其他元素 第一轮 第二轮 第三轮 如上图所示:若元素个数为n,因为排序过程中需要和全部元素都比较一遍,所以时间复杂度为O(n),而平均情况下排序轮次需要logn轮,因此快速排序的平均时间复杂度为O(nlogn)。 排序的实现方法实现方法有双边循环法和单边循环法 双边循环法首选选取基准元素(pivot)4,并设置指针left和right,指向数组最左和最右两个元素,如下: 第一次循环,先从right指针指向的数据(rightData)开始和基准元素比较若 rightData >= pivot,则right指针向左移动,若 rightData < pivot,则right指针不移动,切换到left指针left指针指向数据(leftData)与基准元素比较,若 leftData < pivot,则left指针向右移动,若 leftData > pivot,交换left和right指向的元素。 第一轮指针移动完后,得到如下结构: 然后 left和right指向的元素进行交换: 第一轮循环结束,重新切换到right指针,重复上述步骤。第二轮循环后,得: 第三轮循环后,得: 第四轮循环后,得: 判断到left和right指针指向同一个元素,指针停止移动,使pivot和指针元素进行交换,得: 宣告该轮循环结束,并根据Pivot元素切分为两部分,这两部分的数组再根据上述步骤进行操作。 实现代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152public class DoubleSort { public static void quickSort(int[] arr, int startIndex, int endIndex) { //递归结束条件 if (startIndex >= endIndex) { return; } // 基准元素位置 int pivotIndex = partition(arr, startIndex, endIndex); // 根据基准元素,分成两部分进行递归排序 quickSort(arr, startIndex, pivotIndex - 1); quickSort(arr, pivotIndex + 1, endIndex); } public static int partition(int[] arr, int startIndex, int endIndex) { // 取第一个元素为基准元素,也可以随机抽取 int pivot = arr[startIndex]; int left = startIndex; int right = endIndex; while (left != right) { // 控制right指针比较并左移 while (left < right && arr[right] >= pivot) { right--; } // 控制left指针比较并右移 while (left < right && arr[left] <= pivot) { left++; } // 交换left和right指针所指向的元素 if (left < right) { int temp = arr[right]; arr[right] = arr[left]; arr[left] = temp; } } arr[startIndex] = arr[left]; arr[left] = pivot; return left; } public static void main(String[] args) { int[] arr = new int[]{4, 7, 6, 5, 3, 2, 8, 1}; quickSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); }} 单边循环法双边循环法从数组的两边比较并交换元素,而单边循环法则从数组的一边遍历,一直往后比较和交换,实现起来更加的简单。过程如下: 首先也是选取基准元素pivot(可以随机选择)设置一个mark指针指向数组的起始位置,代表小于基准元素的区域边界(不理解的就把它理解成是等会用来交换元素的就好了) 原始数组如下: 从基准元素下一位开始遍历数组如果该元素大于基准元素,继续往下遍历如果该元素小于基准元素,mark指针往右移,因为小于基准元素的区域边界增大了1(即小于基准元素的多了1位),所以mark就 +1,并且该元素和mark指向元素进行交换。 遍历到元素3时,因为3 < 4,所以mark右移 然后交换元素 然后就继续遍历,根据上面的步骤进行判断,后面的过程就不写了。 实现代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public class SingleSort { public static void quickSort(int[] arr, int startIndex, int endIndex) { //递归结束条件 if (startIndex >= endIndex) { return; } // 基准元素位置 int pivotIndex = partition(arr, startIndex, endIndex); // 根据基准元素,分成两部分进行递归排序 quickSort(arr, startIndex, pivotIndex - 1); quickSort(arr, pivotIndex + 1, endIndex); } /** * 分治(单边循环法) * @param arr * @param startIndex * @param endIndex * @return */ public static int partition(int[] arr, int startIndex, int endIndex) { // 取第一个元素为基准元素,也可以随机抽取 int pivot = arr[startIndex]; int mark = startIndex; for(int i = startIndex + 1; i< arr.length; i++) { if (pivot < arr[i]) { continue; } mark ++; int temp = arr[mark]; arr[mark] = arr[i]; arr[i] = temp; } arr[startIndex] = arr[mark]; arr[mark] = pivot; return mark; } public static void main(String[] args) { int[] arr = new int[]{4, 7, 6, 5, 3, 2, 8, 1}; quickSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); }} 总结本人也是初次接触算法,慢慢的去理解算法的思路和实现过程后,真是为自己以往写的算法感到羞愧。该文章也是为了加深自己对快排算法的印象,若文章有不足之处,恳请各位在下方留言补充。感谢各位的阅读。Thanks♪(・ω・)ノ。 参考资料:《小灰的算法之旅》 第四章。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2019/11/23/basic/%E4%B8%80%E7%9C%8B%E5%B0%B1%E6%87%82%E7%9A%84%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/"},{"title":"一次使用InfluxDB数据库的总结","text":"前言因当前的项目需要记录每秒钟服务器的状态信息,例如负载、cpu等等信息,这些数据都是和时间相关联的。因为一秒钟就要存储挺多的数据。而且我还在前端做了echart的折线图,使用websocket实时查看数据的变化。 方案第一次的方案第一次是很简单的,就是mysql建索引,在时间戳和其余两个条件查询比较多的字段建索引,然后最近一天的数据是存放到redis缓存当中的,一开始感觉还是不错的,所以查询实时的数据还是挺快的,查询历史数据因为有索引的关系,所以速度也还可以。但是随着数据量的增多,发现查询历史数据也逐渐变慢了,数据占用空间太大了,而且索引的占用空间竟然也非常的恐怖。 第二次的方案因为考虑到第一次的解决方案处理稍微有点复杂,并且数据占用空间大。就网上搜一搜有什么解决方案,一个时序数据库的文字进入了我的猿眼。全称叫做时间序列数据库,主要用于带时间标签的数据,例如用于实时监控、设备采集所产生的数据。哦吼?搞一下。 InfluxDb教程安装 官方网址:https://docs.influxdata.com/influxdb/v1.7/introduction/installation/ 因为我用的是ubuntu18.04,所以下面就把ubuntu的安装教程写在这,其他的版本可以到官网上面看看。添加InfluxData存储库: 123wget -qO- https://repos.influxdata.com/influxdb.key | sudo apt-key add -source /etc/lsb-releaseecho "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list 安装并启动InfluxDb服务 12sudo apt-get update && sudo apt-get install influxdbsudo service influxdb start 到这一步你已经可以使用InfluxDB数据库啦,端口是8086,刚安装的InfluxDB是免密登录的,如果开启身份验证就在配置文件下把auto-enabled选项设置为true : 12[http]auth-enable = true 最后使用 -config 选项将进程指向配置文件: 1influxd -config /etc/influxdb/influxdb.conf ## 可视化工具InfluxDb Studio https://github.com/CymaticLabs/InfluxDBStudio 这个工具查询数据多的时候渲染会很卡,不要以为是查询数据慢了,我一开始就是以为查的慢。再说一遍,端口是8086。 客户端因为我用的是php,所以就使用了influxdb的php客户端 php客户端入口:https://github.com/influxdata/influxdb-php 其它语言的客户端库可以在这里找: https://docs.influxdata.com/influxdb/v1.7/tools/api_client_libraries/ 快速上手:composer 安装influxdb-php客户端 1$ composer require influxdb/influxdb-php 直接放php代码,注释和说明在代码里面写了,比较直接。 123456789101112131415161718//获取客户端对象$client = new \\InfluxDB\\Client("127.0.0.1", 8086, "username", "password");//选择数据库, 获取Database对象$database = $client->selectDB("database_name");$points = [ new Point("table_name", 3, //第一个参数为表名, 第二个参数为值 [ "tags" => 1, //标签值 ], [ 'fields' => 1 //字段 ], time()), //最后一个为时间戳];//写入数据, 第一个参数为写入的数据,第二个参数为时间戳的精度,这里我们使用秒精度$database->writePoints($points, Database::PRECISION_SECONDS); 查询方式分两种,第一种则是直接使用sql查询 123$database->query("select * from table_name where time > 1563602406s", [ "epoch" => "s" //让返回的时间格式为秒精度的时间戳,])->getPoints(); //返回的数组集合 sql是不是有点奇怪呢?因为数据保存的时候InfluxDB是按照自己的格式存储的,如果要用秒时间戳作为条件查询,就要这样写啦,在api文档里面有说明 api文档的快捷入口:https://docs.influxdata.com/influxdb/v1.7/tools/api/ 第二种则是使用Builder查询,其实就是帮你把要查询的操作封装起来,到最后解析成SQL,最后再调用方式一的query方法。 12345678//2.使用Builder查询$builde = $database->getQueryBuilder();$builde->select("*") //查询字段 ->from("table_name") //表名 ->setTimeRange(1563602406, 1563602806) //筛选时间范围 ->where(["type = 1"]) //查询条件 ->getResultSet() //里面其实就是调用了方式1的$database->query方法。 ->getPoints(); //返回数组集合 setTimeRange筛选时间范围这个方法需要注意,如果安装InfluxDB的机器(虚拟机)和你开发中机器的时区不同,就不要用了,因为他提前把时间格式化为Y-m-d H:i:s,然后再拿这个时间去不同时区的机器那里查询。数据肯定不对。好了,到这里简单入门就差不多啦。 结语总结自己的学习过程还是蛮不错的,每一次学习到新东西,都感觉到提升了自我价值。但是如果不用记录下来的话,总感觉少了点什么。好像过不久就会忘掉一样,所以学到新东西感觉还是记录下来比较心安,嘿嘿。文章中若有不足之处,请各位在下面评论区留下。Thanks♪(・ω・)ノ","link":"/2019/07/20/db/InfluxDB-1/"},{"title":"mysql定时备份任务","text":"简介在生产环境上,为了避免数据的丢失,通常情况下都会定时的对数据库进行备份。 而Linux的crontab指令则可以帮助我们实现对数据库定时进行备份。首先我们来简单了解crontab指令,如果你会了请跳到下一个内容mysql备份。本文章的mysql数据库是安装在docker容器当中,以此为例进行讲解。没有安装到docker容器当中也可以参照参照。 contab定时任务使用crontab -e来编写我们的定时任务。 10 5 * * 1 [command] 前面的5个数字分别代表分、时、日、月、周,后面的 command为你的执行命令。假如你需要在每天晚上8点整执行定时任务,那么可以这么写 10 8 * * * [command] 扩展:crontab -l 可以查看自己的定时任务crontab -r 删除当前用户的所有定时任务 mysql备份快速上手这里我的mysql数据库是docker容器。假如你需要在每天晚上8点整执行定时任务,那么可以这么写。首先创建数据库备份目录 1mkdir -p /var/backups/mysql 然后先尝试执行数据库备份命令 1docker exec mysql_container mysqldump --single-transaction -uroot -proot_password database_name > /var/backups/mysql/test.sql 如果你看到了/var/backups/mysql/test.sql文件,那么应该就没啥问题了(最好还是看看文件大小,文件太小可能说明会有问题)。mysql_container 为你的数据库容器名**–single-transaction** 不会锁表,也确保数据的一致性mysqldump 是mysql数据库导出数据的指令**-u** 填写mysql的root账号**-p** 填写mysql的root密码database_name 需要备份的数据库名 最后执行命令crontab -e,把mysql的备份命令写到定时任务中。 10 8 * * * docker exec mysql_container mysqldump --single-transaction -uroot -proot_password database_name > /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql 备份文件,后面是文件名的格式 如果你没什么要求,单纯的只是想要备份,那么上面那个命令就可以帮你进行定时备份。 小坑: mysql备份的时候我使用了docker exec -it mysqldump ... 这样的命令去做bash脚本,因为-i参数是有互动的意思,导致在crontab中执行定时任务的时候,没有输出数据到sql文件当中。所以使用crontab定时的对docker容器进行备份命令的时候不要添加-i参数。 crontab优化我不建议直接在crontab -e里面写要执行的命令,任务多了就把这个文件写的乱七八招了。建议把数据库备份的命令写成一个bash脚本。在crontab这里调用就好了如:建立一个/var/backups/mysql/mysqldump.sh文件,内容如下 1docker exec mysql_container mysqldump -uroot --single-transaction -pmypassword database_name > /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql 然后把文件改为当前用户可执行的: 1chmod 711 /var/backups/mysql/mysqldump.sh 执行crontab -e 命令修改成如下: 10 20 * * * /var/backups/mysql/mysqldump.sh 那么这样就比较规范了。 mysql备份优化因为sql文件比较大,所以一般情况下都会对sql文件进行压缩,不然的话磁盘占用就太大了。假设你做了上面这一步 crontab优化,我们可以把mysqldump.sh脚本改成下面这样: 1234export mysqldump_date=$(date +%Y%m%d_%H%M%S) && \\docker exec mysql_container mysqldump --single-transaction -uroot -pmypassword database_name> /var/backups/mysql/$mysqldump_date.sql && \\gzip /var/backups/mysql/$mysqldump_date.sqlfind /var/backups/mysql/ -name "*.sql" -mtime +15 -exec rm -f {} \\; export 在系统中自定义了个变量mysqldump_date,给备份和压缩命令使用gzip 为压缩命令,默认压缩了之后会把源文件删除,压缩成.gz文件find ... 这行命令的意思为,查询 /var/backups/mysql/目录下,创建时间15天之前(-mtime +15),文件名后缀为.sql的所有文件 执行删除命令-exec rm -f {} \\;。总的意思就是:mysql的备份文件只保留15天之内的。15天之前的都删除掉。 数据恢复若一不小心你执行drop database,稳住,淡定。我们首先要创建数据库被删除的数据库。 1>mysql create database database_name; 然后恢复最近备份的数据。恢复备份的命令: 1docker exec -i mysql_container mysql --single-transaction -uroot -proot_password database_name < /var/backups/mysql/20200619_120012.sql 虽然恢复了备份文件的数据,但是备份时间点之后的数据我们却没有恢复回来。如:晚上8点进行定时备份,但是却在晚上9点drop database,那么晚上8点到晚上9点这一个小时之内的数据却没有备份到。这时候就要使用binlog日志了。 binlog日志binlog 是mysql的一个归档日志,记录的数据修改的逻辑,如:给 ID = 3 的这一行的 money 字段 + 1。首先登录mysql后查询当前有多少个binlog文件: 12345678> mysql show binary logs;+---------------+-----------+-----------+| Log_name | File_size | Encrypted |+---------------+-----------+-----------+| binlog.000001 | 729 | No || binlog.000002 | 1749 | No || binlog.000003 | 1087 | No |+---------------+-----------+-----------+ 查看当前正在写入的binlog 1mysql> show master status\\G; 生成新的binlog文件,mysql的后续操作都会写入到新的binlog文件当中,一般在恢复数据都时候都会先执行这个命令。 1mysql> flush logs 查看binlog日志 1mysql> show binlog events in 'binlog.000003'; 小知识点:初始化mysql容器时,添加参数--binlog-rows-query-log-events=ON。或者到容器当中修改/etc/mysql/my.cnf文件,添加参数binlog_rows_query_log_events=ON,然后重启mysql容器。这样可以把原始的SQL添加到binlog文件当中。 恢复数据拿回上面例子的这段话。 晚上8点进行定时备份,但是却在晚上9点drop database,那么晚上8点到晚上9点这一个小时之内的数据却没有备份到。。 首先进入到mysql容器后,切换到/var/lib/mysql目录下,查看binlog文件的创建日期 1234567cd /var/lib/mysqlls -l...-rw-r----- 1 mysql mysql 729 Jun 19 15:54 binlog.000001-rw-r----- 1 mysql mysql 1749 Jun 19 18:45 binlog.000002-rw-r----- 1 mysql mysql 1087 Jun 19 20:58 binlog.000003... 从文件日期可以看出:当天时间为2020-06-21,binlog.000002文件的最后更新时间是 18:45 分,那么晚上8点的备份肯定包含了binlog.000002的数据;binlog.000003的最后更新日期为 20:58 分,那么我们需要恢复的数据 = 晚上8点的全量备份 + binlog.000003的 20:00 - 执行drop database命令时间前的数据。 恢复命令格式: 1mysqlbinlog [options] file | mysql -uroot -proot_password database_name mysqlbinlog常用参数: –start-datetime 开始时间,格式 2020-06-19 18:00:00–stop-datetime 结束时间,格式同上–start-positon 开始位置,(需要查看binlog文件)–stop-position 结束位置,同上… 恢复备份数据和binlog数据前建议先登录mysql后执行flush logs生成新的binlog日志,这样可以专注需要恢复数据的binlog文件。首先我们需要查看binlog日志,在哪个位置进行了drop database操作: 123456789101112131415mysql> show binlog events in 'binlog.000003';+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+| binlog.000003 | 4 | Format_desc | 1 | 125 | Server ver: 8.0.20, Binlog ver: 4 || binlog.000003 | 125 | Previous_gtids | 1 | 156 | || binlog.000003 | 156 | Anonymous_Gtid | 1 | 235 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' || binlog.000003 | 235 | Query | 1 | 318 | BEGIN || binlog.000003 | 318 | Rows_query | 1 | 479 | # INSERT INTO `product_category` SET `name` = '床上用品' , `create_time` = 1592707634 , `update_time` = 1592707634 , `lock_version` = 0 || binlog.000003 | 479 | Table_map | 1 | 559 | table_id: 139 (hotel_server.product_category) || binlog.000003 | 559 | Write_rows | 1 | 629 | table_id: 139 flags: STMT_END_F || binlog.000003 | 629 | Xid | 1 | 660 | COMMIT /* xid=2021 */ || binlog.000004 | 660 | Anonymous_Gtid | 1 | 739 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' || binlog.000004 | 739 | Query | 1 | 822 | drop database hotel_server /* xid=26 */ |+---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+ 根据上面的日志,我们可以看到,在End_log_pos = 822 的位置执行了drop database操作,那么使用binlog恢复的范围就在2020-06-19 20:00:00 - 660 的位置。为什么是660?因为drop database的上一个事务的提交是660的位置,命令如下: 1mysqlbinlog --start-datetime=2020-06-19 20:00:00 --stop-position=660 /var/lib/mysql/binlog.000003 | mysql -uroot -proot_password datbase_name 如果你的范围包括了822的位置,那么就会帮你执行drop database命令了。不信你试试?执行完上面的命令,你的数据就会恢复到drop database前啦!开不开心,激不激动! 总结因为mysql定时备份是在生产环境上必须的任务。是很常用的。所以我就迫不及待的写博客。当然也很感谢我同事的帮助。这篇文章已经写了三天了,因为我也是在不断地试错,不断的更新文章。避免把错误的知识点写出来。如果帮到你了,关注我一波呗!谢谢。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/06/22/db/mysql%E5%AE%9A%E6%97%B6%E5%A4%87%E4%BB%BD/"},{"title":"GitPage部署自己的项目","text":"前言该文章主要为了记录我如何在GitPages上面部署博客网站,这里的话,码云上面也有相同的功能。 若有小伙伴担心GitHub担心把中国的访问也禁了的话(大概不会吧),可以在码云上面部署。流程应该是差不多的。因为我使用的域名是.cn后缀,所以部署到GitHub上面就不用备案了。码云是国内的,应该要备案了,这个就看各位小伙伴的选择了。可以看看我的网站: https://colablog.cn/ 开始第一步,安装工具我们需要创建一个空的项目,怎么创建呢?这里我是使用Hexo的博客框架,他会使用Markdown引擎快速渲染出静态页面。安装hexo的前提是需要安装好下面的应用程序: Node.js (Should be at least nodejs 6.9)Git 然后使用npm安装Hexo。(建议去配置个淘宝的cnpm镜像,快贼多) 1$ npm install -g hexo-cli 第二步,hexo创建项目我们需要使用hexo建立一个空的项目,这里的项目名为blog。 123$ hexo init blog$ cd blog$ npm install 为了在可以在本地调试效果,我们需要安装hexo-server,就是Hexo的服务器 1$ npm install hexo-server --save 然后启动hexo-server,访问的网址的localhost:4000 1$ hexo server 启动后应该可以看见下面的界面 新建名为test的文章测试一下,创建好后在locaohost:4000可以看到新的文章哦。 12$ hexo new post test //全写$ hexo new test //简写,默认为post(文章) 到此为止你已经可以在本地建好博客网站啦。 第三步 使用NexT主题(可跳过)hexo也有推荐使用的主题列表,入口在这: https://hexo.io/themes/ 不过我没有去看这些主题,我是使用了NexT的主题,入口在这: http://theme-next.iissnan.com/theme-settings.html#author-sites 首先我们克隆最新的NexT版本, 12$ cd <你的项目目录>$ git clone https://github.com/iissnan/hexo-theme-next themes/next 然后在hexo的配置文件(_config.yml)文件里面,找到theme字段,修改如下: 1theme: next 个人觉得next默认的主题样式还是比较丑的,我们可以在next主题下看到还有另外三种样式,搜索关键字Schemes可以看到如下主题,我使用的第三个Pisces 12345# Schemes#scheme: Muse#scheme: Mistscheme: Pisces#scheme: Gemini 主题如下: 看着是不是怪丑的,特别是第二篇文章,怎么会展示这么多呢?其实可以调整的,反正我是找了好久,在next主题下,搜索关键字auto_excerpt,修改如下: 123auto_excerpt: enable: true //开启该功能 length: 150 //首页展示的字数限制 到此为止我们已经可以使用NexT主题啦。更详细的配置就进去官网看看吧(上面有)。 第四步 部署到GitHub首先,我们要在GitHub上面创建一个仓库,这里我叫做blog吧。然后我们需要把我们本地的blog项目初始化一下。 123456$ cd <你的本地项目目录>$ git init$ git add -A //把全部都添加进去吧,也没啥$ git commit -m "首次提交"$ git remote add origin <你自己的仓库路径,例:https://github.com/xxx/blog.git>$ git push -u origin master 然后我们进入blog仓库的setting中,然后往下拉看到GitHub Pages 修改完后页面会自己刷新,然后重新回到GitHub Pages这部分。你会看到他给了你一个网址,没错!就是这个网址,你打开试试!!试试就试试,404…。你先记住这个网址,咱们先把这个网址叫做博客网址吧。 其实部署到GitPages上面的话,hexo还是要做一些设置的,不然他怎么知道你要部署到那个地方去哦。设置完后以后部署文章会很简单的:设置你项目的root路径,在hexo配置文件(_config.yml)中,搜索关键字root, 改为你的仓库名称,如下: 12# URLroot: /blog 在hexo的配置文件(_config.yml)中,搜索关键字deploy(其实就在最下面),设置如下: 1234deploy: type: git repo: <你的仓库地址> //https://github.com/xxx/blog.git branch: master 安装hexo-deployer-git, 12$ cd <你的项目目录>$ npm install hexo-deployer-git --save 然后你再执行下面这条命令就OK了! 12$ hexo generate --deploy //全写$ hexo g -d //缩写 赶紧打开上面说的博客网址看看,是不是404!,没错!等一会吧,GitHub还没缓过来呢,执行完命令之后大概差不多一分钟之后刷新一下,你就可以看到你梦寐以求的页面了。以后咱们创建文章就很简单了,新建并且编写好文章之后,执行使用部署到服务器的命令就Ok了。操作如下: 12$ hexo new <文章名> //新建文章$ hexo g -d //部署到GitHub,你就可以看到的新文章啦! 毕竟第一次难免是比较困难。嗯,没错,我说的是部署GitPages。如果你也是跟着我这篇文章一步一步走的话,应该是没什么毛病的,因为我是自己重新部署一个项目的,然后一步一步的把步骤记录下来的。如果有什么问题的话,可以留言一下,或致邮箱821312534@qq.com。谢啦。","link":"/2019/08/14/git/GitPage%E9%83%A8%E7%BD%B2%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE/"},{"title":"网站实现markdown功能","text":"前言由于个人一直想弄一个博客网站,所以写博客的功能也就必须存在啦,而之前想过用富文本编辑器来实现的。但是接触了markdown后,发现真的是太好玩了,而且使用markdown的话可以在博客园、CSDN、公众号等各个地方使用。如果使用富文本来实现的话。。那可就惨了,发一篇文章在不同的地方就要重新弄一下样式。真的是非常蛋teng。所以建议不会markdown语法的童鞋还是得好好去看看。 在这里我要介绍的是如何在你的网站接入Markdown功能 # 实现功能 实现markdown的功能主要实现两部分,找到可以转换markdown语法的功能。然后去找一下你自己喜欢的markdown主题样式。(如果你追求至简的话,你只需要实现第一部就可以了,只是有点丑。。。) ## showdown.js 为什么使用showdown.js?肤浅的我只认star数(手动吾眼) 入口在此:https://github.com/showdownjs/showdown 用法也是灰常之简单: 只需使用到 `dict\\showdown.js`文件,引入了之后,只需如下使用: 123var converter = new showdown.Converter(), text = '# hello, markdown!', html = converter.makeHtml(text); 这样就可以获取到markdown语法转换后的html啦。 其实还是很丑的。。。因为没有样式。。。 主题样式Typora 入口:http://theme.typora.io/ 貌似挺多人喜欢用这个,但是我个人觉得麻麻地。而且有些主题对中文不太支持。不过里面的主题还挺多的。可以搞一下。 Markdownhere 入口:https://markdown-here.com/ Markdownhere 是李笑来制作的一套 CSS 主题,。我现在用着他的CSS主题,其实也是麻麻地,然后自己也改了一点。感觉没啥变化(一名CSS的菜鸟)。 少数派为什么叫少数派?我也不知道,我一直以为是少数人使用的流派(自己yy的)。 入口:https://cdn.sspai.com/minja/sspai.css.zip","link":"/2019/07/12/git/%E7%BD%91%E7%AB%99%E5%AE%9E%E7%8E%B0markdown/"},{"title":"Gitment评论插件的使用","text":"前言继上一篇的GitPages部署自己的网站 现在开始添加博客的评论插件Gitment。这里的话我是使用hexo添加gitment插件,如果不是使用hexo,请到官网指定这里。 开始第一步 注册 OAuth Application首先在点击这里注册自己OAuth Application,此处有四个内容: Application name: 随意填写Homepage URL: 随意填写一个urlApplication description: 随意填写Authorization callback URL: 填你的博客网址(例: https://user.github.io/blog/),如果有域名则填域名。 注册完后你会得到一个 client ID 和一个 client secret,记住这两个玩意,等会hexo配置会用到 第二步 安装Gitment在你的博客下使用npm进行安装gitment 1$ npm install --save gitment 第三步 修改主题配置因为这里我使用的是Next主题配置,在配置文件搜索关键字gitment,主要配置如下: 12345678gitment: enable: true //这里必须为开启 ... github_user: username github_repo: blog_comments #新建一个存储评论的仓库,这里填写仓库名 client_id: #第一步注册的client_id client_secret: #第一步注册的client_secret ... github_user不知道是哪个?你在github页面中,点击你的头像看到第一行显示signed in as xxx,就是填写这个xxx。github_repo这里是让你再新建一个仓库,用来存储评论的,不是当前的这个博客的仓库,然后填上你仓库名的名字,对!就是单纯的名字,仓库名叫blog_comments就填blog_comments。 第四步 初始化评论插件搞定好以上的步骤后,你就能看到博客的下方是这样的点击登入后,(未开放评论)的地方会显示一个按钮让你初始化,点击按钮然后你就可以进行评论啦! 好了,到此为止就搞定了成功接入了Gitment插件了。如果有什么问题可以留言一下咯。Thanks♪(・ω・)ノ。 参考我的博客 > https://colablog.cn/","link":"/2019/08/20/git/Gitment/"},{"title":"执行ArrayList的remove(object)方法抛异常?","text":"简介或许有很多小伙伴都尝试过如下的代码: 123456ArrayList<Object> list = ...;for (Object object : list) { if (条件成立) { list.remove(object); }} 然后会发现抛出java.util.ConcurrentModificationException异常,这是一个并发异常。那么这个到底是什么情况?首先需要介绍一下增强for循环 增强for循环增强for循环是Java1.5后,Collection实现了Iterator接口后出现的。增强for循环的代码如下 123for (Object object : list) { // 操作} 其实增强for循环就是使用Iterator迭代器进行迭代的,增强for循环就变成下面这样: 12345Iterator<Object> iterator = list.iterator();while (iterator.hasNext()) { iterator.next(); // 操作} 那么为什么在增强for循环中调用list.remove(object)会出事呢?那么咱们看看ArrayList下的 Iterator的实现类: Itr类 Itr子类Itr子类是Iterator的实现类,属于ArrayList私有的局部内部类。我截取了Itr类的部分代码,如下: 123456789101112131415161718192021private class Itr implements Iterator<E> { int cursor; // index of next element to return int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); ... } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }} elementData 是ArrayList存放元素的数组,上面代码没有贴出来。size 是elementData实际存放的容量大小modCount 记录elementData容量的修改次数expectedModCount 记录实例化迭代器Itr时,elementData容量的修改次数注意!:在迭代器中,当执行next方法的时候,会去调用checkForComodification方法,判断elementData 的容量是否被修改过。然后来看看ArrayList的remove(object)方法,截取部分代码如下: 12345678910111213public boolean remove(Object o) { for (int index = 0; index < size; index++) if (找到目标元素) { fastRemove(index); return true; } return false;}private void fastRemove(int index) { modCount++; // 移除操作} 可以发现,调用remove(object)方法时调用了fastRemove方法,在fastRemove方法中执行modCount++ !现在把文章开头的代码拷下来,再来分析一次: 123456ArrayList<Object> list = ...;for (Object object : list) { if (条件成立) { list.remove(object); }} 当执行了list.remove时,执行modCount++ 。此时迭代器再往下进行迭代,执行了next方法,发现 modCount != expectedModCount,那么则抛出java.util.ConcurrentModificationException异常。 之所以Iterator认为是一个并发异常。是因为你不在迭代器里操作,而是在迭代器外面进行remove操作呀!难道没有其他解决方案吗?有滴。 解决方案那么就是使用Itr的 remove方法。Itr子类重写了 remove 方法,这里部分代码: 12345678910public void remove() { ... try { ArrayList.this.remove(需要删除元素的索引); ... expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); }} 其实很简单,就是remove后,把 expectedModCount 同步一下 modCount 的值,这就解决了。完整代码如下: 123456ArrayList<Object> list = ...;Iterator<Object> iterator = list.iterator();while (iterator.hasNext()) { iterator.next(); iterator.remove();} 总结本来我还不知道增强for循环是调用Iterator进行迭代的,要不是我debug了一波,我还不知道呐。还是小有收货。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/07/10/java/ArrayList%E7%9A%84remove%E5%BC%82%E5%B8%B8/"},{"title":"Java并发编程实战 01并发编程的Bug源头","text":"Java并发编程系列:01并发编程的Bug源头 摘要编写正确的并发程序对我来说是一件极其困难的事情,由于知识不足,只知道synchronized这个修饰符进行同步。 本文为学习极客时间:Java并发编程实战 01的总结,文章取图也是来自于该文章 并发Bug源头在计算机系统中,程序的执行速度为:CPU > 内存 > I/O设备 ,为了平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都进行了优化: 1.CPU增加了缓存,以均衡和内存的速度差异2.操作系统增加了进程、线程,已分时复用CPU,以均衡 CPU 与 I/O 设备的速度差异3.编译程序优化指令执行顺序,使得缓存能够更加合理的利用。 但是这三者导致的问题为:可见性、原子性、有序性 源头之一:CPU缓存导致的可见性问题一个线程对共享变量的修改,另外一个线程能够立即看到,那么就称为可见性。现在多核CPU时代中,每颗CPU都有自己的缓存,CPU之间并不会共享缓存; 如线程A从内存读取变量V到CPU-1,操作完成后保存在CPU-1缓存中,还未写到内存中。此时线程B从内存读取变量V到CPU-2中,而CPU-1缓存中的变量V对线程B是不可见的当线程A把更新后的变量V写到内存中时,线程B才可以从内存中读取到最新变量V的值 上述过程就是线程A修改变量V后,对线程B不可见,那么就称为可见性问题。 源头之二:线程切换带来的原子性问题现代的操作系统都是基于线程来调度的,现在提到的“任务切换”都是指“线程切换”Java并发程序都是基于多线程的,自然也会涉及到任务切换,在高级语言中,一条语句可能就需要多条CPU指令完成,例如在代码 count += 1 中,至少需要三条CPU指令。 指令1:把变量 count 从内存加载到CPU的寄存器中指令2:在寄存器中把变量 count + 1指令3:把变量 count 写入到内存(缓存机制导致可能写入的是CPU缓存而不是内存) 操作系统做任务切换,可以发生在任何一条CPU指令执行完,所以并不是高级语言中的一条语句,不要被 count += 1 这个操作蒙蔽了双眼。假设count = 0,线程A执行完 指令1 后 ,做任务切换到线程B执行了 指令1、指令2、指令3后,再做任务切换回线程A。我们会发现虽然两个线程都执行了 count += 1 操作。但是得到的结果并不是2,而是1。 如果 count += 1 是一个不可分割的整体,线程的切换可以发生在 count += 1 之前或之后,但是不会发生在中间,就像个原子一样。我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性 源头之三:编译优化带来的有序性问题有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,可能会改变程序中的语句执行先后顺序。如:a = 1; b = 2;,编译器可能会优化成:b = 2; a = 1。在这个例子中,编译器优化了程序的执行先后顺序,并不影响结果。但是有时候优化后会导致意想不到的Bug。在单例模式的双重检查创建单例对象中。如下代码: 1234567891011121314public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }} 问题出现在了new Singletion()这行代码,我们以为的执行顺序应该是这样的: 指令1:分配一块内存M指令2:在内存M中实例化Singleton对象指令3:instance变量指向内存地址M 但是实际优化后的执行路径确实这样的: 指令1:分配一块内存M指令2:instance变量指向内存地址M指令3:在内存M中实例化Singleton对象 这样的话看出来什么问题了吗?当线程A执行完了指令2后,切换到了线程B,线程B判断到 if (instance != null)。直接返回instance,但是此时的instance还是没有被实例化的啊!所以这时候我们使用instance可能就会触发空指针异常了。如图: 总结在写并发程序的时候,需要时刻注意可见性、原子性、有序性的问题。在深刻理解这三个问题后,写起并发程序也会少一点Bug啦~。记住了下面这段话:CPU缓存会带来可见性问题、线程切换带来的原子性问题、编译优化带来的有序性问题。 参考文章:极客时间:Java并发编程实战 01 | 可见性、原子性和有序性问题:并发编程Bug的源头 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/04/14/java/Java%E5%B9%B6%E5%8F%91-1/"},{"title":"Java并发编程实战 03互斥锁 解决原子性问题","text":"Java并发编程系列:03互斥锁 解决原子性问题 文章系列Java并发编程实战 01并发编程的Bug源头Java并发编程实战 02Java如何解决可见性和有序性问题 摘要在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和有序性的问题,那么还有一个原子性问题咱们还没解决。在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性,那么原子性的问题该如何解决。 同一时刻只有一个线程执行这个条件非常重要,我们称为互斥,如果能保护对共享变量的修改时互斥的,那么就能保住原子性。 简易锁我们把一段需要互斥执行的代码称为临界区,线程进入临界区之前,首先尝试获取加锁,若加锁成功则可以进入临界区执行代码,否则就等待,直到持有锁的线程执行了解锁unlock()操作。如下图: 但是有两个点要我们理解清楚:我们的锁是什么?要保护的又是什么? 改进后的锁模型在并发编程世界中,锁和锁要保护的资源是有对应关系的。首先我们需要把临界区要保护的资源R标记出来,然后需要创建一把该资源的锁LR,最后针对这把锁,我们需要在进出临界区时添加加锁lock(LR)操作和解锁unlock(LR)操作。如下: Java语言提供的锁技术:synchronizedsynchronized可修饰方法和代码块。加锁lock()和解锁unlock()都会在synchronized修饰的方法或代码块前后自动加上加锁lock()和解锁unlock()操作。这样做的好处就是加锁和解锁操作会成对出现,毕竟忘了执行解锁unlock()操作可是会让其他线程死等下去。那我们怎么去锁住需要保护的资源呢?在下面的代码中,add1()非静态方法锁定的是this对象(当前实例对象),add2()静态方法锁定的是X.class(当前类的Class对象) 12345678public class X { public synchronized void add1() { // 临界区 } public synchronized static void add2() { // 临界区 }} 上面的代码可以理解为这样: 12345678public class X { public synchronized(this) void add() { // 临界区 } public synchronized(X.class) static void add2() { // 临界区 }} 使用synchronized 解决 count += 1 问题在01 并发编程的Bug源头文章当中,我们提到过count += 1 存在的并发问题,现在我们尝试使用synchronized解决该问题。 123456789public class Calc { private int value = 0; public synchronized int get() { return value; } public synchronized void addOne() { value += 1; }} addOne()方法被synchronized修饰后,只有一个线程能执行,所以一定能保证原子性,那么可见性问题呢?在上一篇文章02 Java如何解决可见性和有序性问题当中,提到了管程中的锁规则,一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,在这里就是synchronized(管程的在后续的文章中介绍)。根据这个规则,前一个线程执行了value += 1操作是对后续线程可见的。而查看get()方法也必须加上synchronized修饰,否则也没法保证其可见性。上面这个例子如下图: 那么可以使用多个锁保护一个资源吗,修改一下上面的例子后,get()方法使用this对象锁来保护资源value,addOne()方法使用Calc.class类对象来保护资源value,代码如下: 123456789public class Calc { private static int value = 0; public synchronized int get() { return value; } public static synchronized void addOne() { value += 1; }} 上面的例子用图来表示: 在这个例子当中,get()方法使用的是this锁,addOne()方法使用的是Calc.class锁,因此这两个临界区(方法)并没有互斥性,addOne()方法的修改对get()方法是不可见的,所以就会导致并发问题。结论:不可使用多把锁保护一个资源,但能使用一把锁保护多个资源(这里没写例子,只写了一把锁保护一个资源) 保护没有关联关系的多个资源在银行的业务当中,修改密码和取款是两个再经常不过的操作了,修改密码操作和取款操作是没有关联关系的,没有关联关系的资源我们可以使用不同的互斥锁来解决并发问题。代码如下: 1234567891011121314151617181920212223public class Account { // 保护密码的锁 private final Object pwLock = new Object(); // 密码 private String password; // 保护余额的锁 private final Object moneyLock = new Object(); // 余额 private Long money; public void updatePassword(String password) { synchronized (pwLock) { // 修改密码 } } public void withdrawals(Long money) { synchronized (moneyLock) { // 取款 } }} 分别使用pwLock和moneyLock来保护密码和余额,这样修改密码和修改余额就可以并行了。使用不同的锁对受保护的资源进行进行更细化管理,能够提升性能,这种锁叫做细粒度锁。在这个例子当中,你可能发现我使用了final Object来当成一把锁,这里解释一下:使用锁必须是不可变对象,若把可变对象作为锁,当可变对象被修改时相当于换锁,而且使用Long或Integer作为锁时,在-128到127之间时,会使用缓存,详情可查看他们的valueOf()方法。 保护有关联关系的多个资源在银行业务当中,除了修改密码和取款的操作比较多之外,还有一个操作比较多的功能就是转账。账户 A 转账给 账户B 100元,账户A的余额减少100元,账户B的余额增加100元,那么这两个账户就是有关联关系的。在没有理解互斥锁之前,写出的代码可能如下: 1234567891011public class Account { // 余额 private Long money; public synchronized void transfer(Account target, Long money) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; }} 在转账transfer方法当中,锁定的是this对象(用户A),那么这里的目标用户target(用户B)的能被锁定吗?当然不能。这两个对象是没有关联关系的。正确的操作应该是获取this锁和target锁才能去进行转账操作,正确的代码如下: 123456789101112131415public class Account { // 余额 private Long money; public synchronized void transfer(Account target, Long money) { synchronized(this) { synchronized (target) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } }} 在这个例子当中,我们需要清晰的明白要保护的资源是什么,只要我们的锁能覆盖所有受保护的资源就可以了。但是你以为这个例子很完美?那就错了,这里面很有可能会发生死锁。你看出来了吗?下一篇文章我就用这个例子来聊聊死锁。 总结使用互斥锁最最重要的是:我们的锁是什么?锁要保护的资源是什么?,要理清楚这两点就好下手了。而且锁必须为不可变对象。使用不同的锁保护不同的资源,可以细化管理,提升性能,称为细粒度锁。 参考文章:极客时间:Java并发编程实战 03互斥锁(上)极客时间:Java并发编程实战 04互斥锁(下) 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/05/07/java/Java%E5%B9%B6%E5%8F%91-3/"},{"title":"Java并发编程实战 02Java如何解决可见性和有序性问题","text":"Java并发编程系列:02Java如何解决可见性和有序性问题 摘要在上一篇文章Java并发编程实战 01并发Bug的源头当中,讲到了CPU缓存导致可见性、线程切换导致了原子性、编译优化导致了有序性问题。那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经常考核到) 什么是Java内存模型?现在知道了CPU缓存导致可见性、编译优化导致了有序性问题,那么最简单的方式就是直接禁用CPU缓存和编译优化。但是这样做我们的性能可就要爆炸了~。我们应该按需禁用。Java内存模型是有一个很复杂的规范,但是站在程序员的角度上可以理解为:Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体包括 volatile、synchronized、final三个关键字,以及六项Happens-Before规则。 volatile关键字volatile有禁用CPU缓存的意思,禁用CPU缓存那么操作数据变量时直接是直接从内存中读取和写入。如:使用volatile声明变量 volatile boolean v = false,那么操作变量v时则必须从内存中读取或写入,但是在低于Java版本1.5以前,可能会有问题。在下面这段代码当中,假设线程A执行了write方法,线程B执行了reader方法,假设线程B判断到了this.v == true进入到了判断条件中,那么此时的x会是多少呢? 123456789101112131415public class VolatileExample { private int x = 0; private volatile boolean v = false; public void write() { this.x = 666; this.v = true; } public void reader() { if (this.v == true) { // 这里的x会是多少呢? } }} 在1.5版本之前,该值可能为666,也可能为0;因为变量x并没有禁用缓存(volatile),但是在1.5版本以后,该值一定为666;因为Happens-Before规则。 什么是Happens-Before规则Happens-Before规则要表达的是:前面一个操作的结果对后续是可见的。如果第一次接触该规则,可能会有一些困惑,但是多去阅读几遍,就会加深理解。 1.程序的顺序性规则这条规则是指在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作(意思就是前面的操作结果对于后续任意操作都是可以看到的)。就如上面的那段代码,按照程序的顺序:this.x = 666 Happens-Before于 this.v = true。 2.Volatile 变量规则这条规则指的是对一个Volatile变量的写操作,Happens-Before该变量的读操作。意思也就是:假设该变量被线程A写入后,那么该变量对于任何线程都是可见的。也就是禁用了CPU缓存的意思,如果是这样的话,那么和1.5版本以前没什么区别啊!那么如果再看一下规则3,就不同了。 3.传递性这条规则指的是:如果 A Happens-Before 于B,且 B Happens-Before 于 C。那么 A Happens-Before 于 C。这就是传递性的规则。我们再来看看刚才那段代码(我复制下来方便看) 123456789101112131415public class VolatileExample { private int x = 0; private volatile boolean v = false; public void write() { this.x = 666; this.v = true; } public void reader() { if (this.v == true) { // 读取变量x } }} 在上面代码,我们可以看到,this.x = 666 Happens-Before this.v = true,this.v = true Happens-Before 读取变量x,根据传递性规则this.x = 666 Happens-Befote 读取变量x,那么说明了读取到变量this.v = true时,那么此时的读取变量x的指必定为666假设线程A执行了write方法,线程B执行reader方法且此时的this.v == true,那么根据刚才所说的传递性规则,读取到的变量x必定为666。这就是1.5版本对volatile语义的增强。而如果在版本1.5之前,因为变量x并没有禁用缓存(volatile),所以变量x可能为0哦。 4.管程中锁的规则这条规则是指对一个锁的解锁操作 Happens-Before 于后续对这个锁的加锁操作。管程是一种通用的同步原语,在Java中,synchronized是Java里对管程的实现。管程中的锁在Java里是隐式实现的。如下面的代码,在进入同步代码块前,会自动加锁,而在代码块执行完后会自动解锁。这里的加锁和解锁都是编译器帮我们实现的。 123456synchronized(this) { // 此处自动加锁 // x是共享变量,初始值 = 0 if (this.x < 12) { this.x = 12; }} // 此处自动解锁 结合管程中的锁规则,假设x的初始值为0,线程A执行完代码块后值会变成12,那么当线程A解锁后,线程B获取到锁进入到代码块后,就能看到线程A的执行结果x = 12。这就是管程中锁的规则 5.线程的start()规则这条规则是关于线程启动的,该规则指的是主线程A启动子线程B后,子线程B能够看到主线程启动子线程B前的操作。用HappensBefore解释:线程A调用线程B的start方法 Happens-Before 线程B中的任意操作。参考代码如下: 12345678910int x = 0;public void start() { Thread thread = new Thread(() -> { System.out.println(this.x); }); this.x = 666; // 主线程启动子线程 thread.start();} 此时在子线程中打印的变量x值为666,你也可以尝试一下。 6.线程join()规则这条规则是关于线程等待的,该规则指的是主线程A等待子线程B完成(主线A通过调用子线程B的join()方法实现),当子线程B完成后,主线程能够看到子线程的操作,这里的看到指的是共享变量 的操作,用Happens-Before解释:如果在线程A中调用了子线程B的join()方法并成功返回,那么子线程B的任意操作 Happens-Before 于主线程调用子线程Bjoin()方法的后续操作。看代码比较容易理解,示例代码如下: 1234567891011int x = 0;public void start() { Thread thread = new Thread(() -> { this.x = 666; }); // 主线程启动子线程 thread.start(); // 主线程调用子线程的join方法进行等待 thread.join(); // 此时的共享变量 x == 666} 被忽略的final在1.5版本之前,除了值不可改变以外,final字段其实和普通的字段一样。在1.5以后的Java内存模型中,对final类型变量重排进行了约束。现在只要我们的提供正确的构造函数没有逸出,那么在构造函数初始化的final字段的最新值,必定可以被其他线程所看到。代码如下: 12345678910111213141516171819class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = f.x; int j = f.y; } } 当线程执行reader()方法,并且f != null时,那么此时的final字段修饰的f.x 必定为 3,但是y不能保证为4,因为它不是final的。如果这是在1.5版本之前,那么f.x也是不能保证为3。 那么何为逸出呢?我们修改一下构造函数: 123456public FinalFieldExample() { x = 3; y = 4; // 此处为逸出 f = this;} 这里就不能保证 f.x == 3了,就算x变量是用final修饰的,为什么呢?因为在构造函数中可能会发生指令重排,执行变成下面这样: 1234 // 此处为逸出f = this;x = 3;y = 4;那么此时的f.x == 0。所以在构造函数中没有逸出,那么final修饰的字段没有问题。详情的案例可以参考这个文档 总结在这篇文章当中,我一开始对于文章最后部分的final约束重排一直看的不懂。网上不断地搜索资料和看文章当中提供的资料我才慢慢看懂,反复看了不下十遍。可能脑子不太灵活吧。该文章主要的核心内容就是Happens-Before规则,把这几条规则搞懂了就ok。 参考文章:极客时间:Java并发编程实战 02 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/04/20/java/Java%E5%B9%B6%E5%8F%91-2/"},{"title":"Java并发编程实战 04死锁了怎么办?","text":"Java并发编程系列:04死锁了怎么办? Java并发编程文章系列Java并发编程实战 01并发编程的Bug源头Java并发编程实战 02Java如何解决可见性和有序性问题Java并发编程实战 03互斥锁 解决原子性问题 前提在第三篇文章最后的例子当中,需要获取到两个账户的锁后进行转账操作,这种情况有可能会发生死锁,我把上一章的代码片段放到下面: 123456789101112131415public class Account { // 余额 private Long money; public synchronized void transfer(Account target, Long money) { synchronized(this) { (1) synchronized (target) { (2) this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } }} 若账户A转账给账户B100元,账户B同时也转账给账户A100元,当账户A转帐的线程A执行到了代码(1)处时,获取到了账户A对象的锁,同时账户B转账的线程B也执行到了代码(1)处时,获取到了账户B对象的锁。当线程A和线程B执行到了代码(2)处时,他们都在互相等待对方释放锁来获取,可是synchronized是阻塞锁,没有执行完代码块是不会释放锁的,就这样,线程A和线程B死死的对着,谁也不放过谁。等到了你去重启应用的那一天。。。这个现象就是死锁。死锁的定义:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。如下图: 查找死锁信息这里我先以一个基本会发生死锁的程序为例,创建两个线程,线程A获取到锁A后,休眠1秒后去获取锁B;线程B获取到锁B后 ,休眠1秒后去获取锁A。那么这样基本都会发生死锁的现象,代码如下: 123456789101112131415161718192021222324252627282930313233public class DeadLock extends Thread { private String first; private String second; public DeadLock(String name, String first, String second) { super(name); // 线程名 this.first = first; this.second = second; } public void run() { synchronized (first) { System.out.println(this.getName() + " 获取到锁: " + first); try { Thread.sleep(1000L); //线程休眠1秒 synchronized (second) { System.out.println(this.getName() + " 获取到锁: " + second); } } catch (InterruptedException e) { // Do nothing } } } public static void main(String[] args) throws InterruptedException { String lockA = "lockA"; String lockB = "lockB"; DeadLock threadA = new DeadLock("ThreadA", lockA, lockB); DeadLock threadB = new DeadLock("ThreadB", lockB, lockA); threadA.start(); threadB.start(); threadA.join(); //等待线程1执行完 threadB.join(); }} 运行程序后将发生死锁,然后使用jps命令(jps.exe在jdk/bin目录下),命令如下: 12345678C:\\Program Files\\Java\\jdk1.8.0_221\\bin>jps -l24416 sun.tools.jps.Jps24480 org.jetbrains.kotlin.daemon.KotlinCompileDaemon162420360 org.jetbrains.jps.cmdline.Launcher92569320 page2.DeadLock18188 可以发现发生死锁的进程id 9320,然后使用jstack(jstack.exe在jdk/bin目录下)命令查看死锁信息。 123456789101112C:\\Program Files\\Java\\jdk1.8.0_221\\bin>jstack 9320"ThreadB" #13 prio=5 os_prio=0 tid=0x000000001e48c800 nid=0x51f8 waiting for monitor entry [0x000000001f38f000] java.lang.Thread.State: BLOCKED (on object monitor) at page2.DeadLock.run(DeadLock.java:19) - waiting to lock <0x000000076b99c198> (a java.lang.String) - locked <0x000000076b99c1d0> (a java.lang.String)"ThreadA" #12 prio=5 os_prio=0 tid=0x000000001e48c000 nid=0x3358 waiting for monitor entry [0x000000001f28f000] java.lang.Thread.State: BLOCKED (on object monitor) at page2.DeadLock.run(DeadLock.java:19) - waiting to lock <0x000000076b99c1d0> (a java.lang.String) - locked <0x000000076b99c198> (a java.lang.String) 这样我们就可以看到发生死锁的信息。虽然发现了死锁,但是解决死锁只能是重启应用了。 如何避免死锁的发生1.固定的顺序来获得锁如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。(取自《Java并发编程实战》一书)要想验证锁顺序的一致性,有很多种方式,如果锁定的对象含有递增的id字段(唯一、不可变、具有可比性的),那么就好办多了,获取锁的顺序以id由小到大来排序。还是用转账的例子来解释,代码如下: 123456789101112131415161718192021222324252627public class Account { // id (递增) private Integer id; // 余额 private Long money; public synchronized void transfer(Account target, Long money) { Account account1; Account account2; if (this.id < target.id) { account1 = this; account2 = target; } else { account1 = target; account2 = this; } synchronized(account1) { synchronized (account2) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } }} 若该对象并没有唯一、不可变、具有可比性的的字段(如:递增的id),那么可以使用 System.identityHashCode() 方法返回的哈希值来进行比较。比较方式可以和上面的例子一类似。System.identityHashCode()虽然会出现散列冲突,但是发生冲突的概率是非常低的。因此这项技术以最小的代价,换来了最大的安全性。提示: 不管你是否重写了对象的hashCode方法,System.identityHashCode() 方法都只会返回默认的哈希值。 2.一次性申请所有资源只要同时获取到转出账户和转入账户的资源锁。执行完转账操作后,也同时释放转入账户和转出账户的资源锁。那么则不会出现死锁。但是使用synchronized只能同时锁定一个资源锁,所以需要建立一个锁分配器LockAllocator 。代码如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/** 锁分配器(单例类) */public class LockAllocator { private final List<Object> lock = new ArrayList<Object>(); /** 同时申请锁资源 */ public synchronized boolean lock(Object object1, Object object2) { if (lock.contains(object1) || lock.contains(object2)) { return false; } lock.add(object1); lock.add(object2); return true; } /** 同时释放资源锁 */ public synchronized void unlock(Object object1, Object object2) { lock.remove(object1); lock.remove(object2); }}public class Account { // 余额 private Long money; // 锁分配器 private LockAllocator lockAllocator; public void transfer(Account target, Long money) { try { // 循环获取锁,直到获取成功 while (!lockAllocator.lock(this, target)) { } synchronized (this){ synchronized (target){ this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } } finally { // 释放锁 lockAllocator.unlock(this, target); } }} 使用while循环不断的去获取锁,一直到获取成功,当然你也可以设置获取失败后休眠xx毫秒后获取,或者其他优化的方式。释放锁必须使用try-finally的方式来释放锁。避免释放锁失败。 3.尝试获取锁资源在Java中,Lock接口定义了一组抽象的加锁操作。与内置锁synchronized不同,使用内置锁时,只要没有获取到锁,就会死等下去,而显示锁Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁操作都是显示的(内置锁synchronized的加锁和解锁操作都是隐示的),这篇文章就不展开来讲显示锁Lock了(当然感兴趣的朋友可以先百度一下)。 总结在生产环境发生死锁可是一个很严重的问题,虽说重启应用来解决死锁,但是毕竟是生产环境,代价很大,而且重启应用后还是可能会发生死锁,所以在编写并发程序时需要非常严谨的避免死锁的发生。避免死锁的方案应该还有更多,鄙人不才,暂知这些方案。若有其它方案可以留言告知。非常感谢你的阅读,谢谢。 参考文章:《Java并发编程实战》第10章极客时间:Java并发编程实战 05:一不小心死锁了,怎么办?极客时间:Java核心技术面试精讲 18:什么情况下Java程序会产生死锁?如何定位、修复? 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/05/12/java/Java%E5%B9%B6%E5%8F%91-4/"},{"title":"Java并发编程实战 05等待-通知机制和活跃性问题","text":"Java并发编程系列:05等待-通知机制和活跃性问题 Java并发编程系列Java并发编程实战 01并发编程的Bug源头Java并发编程实战 02Java如何解决可见性和有序性问题Java并发编程实战 03互斥锁 解决原子性问题Java并发编程实战 04死锁了怎么办 前提在Java并发编程实战 04死锁了怎么办中,讲到了使用一次性申请所有资源来避免死锁的发生,但是代码中却是使用不断的循环去获取锁资源。如果获取锁资源耗时短、且并发冲突量不大的时候,这个方式还是挺合适的。如果获取所以资源耗时长且并发冲突量很大的时候,可能会循环上千上万次,这就太消耗CPU了。把上一章的代码贴下来吧。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/** 锁分配器(单例类) */public class LockAllocator { private final List<Object> lock = new ArrayList<Object>(); /** 同时申请锁资源 */ public synchronized boolean lock(Object object1, Object object2) { if (lock.contains(object1) || lock.contains(object2)) { return false; } lock.add(object1); lock.add(object2); return true; } /** 同时释放资源锁 */ public synchronized void unlock(Object object1, Object object2) { lock.remove(object1); lock.remove(object2); }}public class Account { // 余额 private Long money; // 锁分配器 private LockAllocator lockAllocator; public void transfer(Account target, Long money) { try { // 循环获取锁,直到获取成功 while (!lockAllocator.lock(this, target)) { } synchronized (this){ synchronized (target){ this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } } finally { // 释放锁 lockAllocator.unlock(this, target); } }} 解决这种场景的方案就是使用等待-通知机制。 等待-通知机制当我们去麦当劳吃汉堡,首先我们需要排队点餐,就如线程抢着获取锁进synchronized同步代码块中。当我们点完餐后需要等待汉堡完成,所以我们需要等待wait(),因为汉堡还没做好。当汉堡做好后广播喊了一句“我做好啦!快来领餐”。广播就是notifyAll(),唤醒了所有线程。然后每个人都过去看看是不是自己的餐。如果不是又进入了等待中。否则就可以拿到汉堡(获取到锁)开吃啦。 当然麦当劳只会说“xx号快来领餐”,我改了一下台词比较好做例子(例子感觉也是一般般,看不懂就看代码吧)。对不起麦当劳了。 在编程领域当中,若线程发现锁资源被其他线程占用了(条件不满足),线程就会进入等待状态wait(释放锁),当其它线程释放锁时,使用notifyAll()唤醒所有等待中的线程。被唤醒的线程就会重新去尝试获取锁。如图: 那么何时等待? 何时唤醒?何时等待:当线程的要求不满足时等待,在转账的例子当中就是不能同时获取到this和target锁资源时等待。何时唤醒:当有线程释放锁资源时就唤醒。修改后的代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142/** 锁分配器(单例类) */public class LockAllocator { private final List<Object> lock = new ArrayList<>(); /** 同时申请锁资源 */ public synchronized void lock(Object object1, Object object2) throws InterruptedException { while (lock.contains(object1) || lock.contains(object2)) { wait(); // 线程进入等待状态 释放锁 } lock.add(object1); lock.add(object2); } /** 同时释放资源锁 */ public synchronized void unlock(Object object1, Object object2) { lock.remove(object1); lock.remove(object2); notifyAll(); // 唤醒所有等待中的线程 }}public class Account { // 余额 private Long money; // 锁分配器 private LockAllocator lockAllocator; public void transfer(Account target, Long money) throws InterruptedException { try { // 获取锁 lockAllocator.lock(this, target); this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } finally { // 释放锁 lockAllocator.unlock(this, target); } }} 在Account类中,对比上面的代码,我删掉了两层synchronized嵌套,如果涉及到账户余额都先去锁分配器LockAllocator 中获取锁,那么这两层synchronized嵌套其实可以去掉。而且使用wait()和notifyAll()(notify()也是)必须在synchronized代码块中,否则会抛出java.lang.IllegalMonitorStateException`异常。 尽量使用notifyAll其实使用notify()也可以唤醒线程,但是只会随机抽取一位幸运观众(随机唤醒一个线程)。这样做可能有导致有些线程没那么快被唤醒或者永久都不会有机会被唤醒到。假如有资源A、B、C、D,线程1申请到AB,线程2申请到CD,线程3申请AB需要等待。此时有线程4申请CD等待,若线程1释放资源时唤醒了线程4,但是线程4还是需要等待线程2释放资源,线程3却没有被唤醒到。所以除非你已经思考过了使用notify()没问题,否则尽量使用notifyAll()。 notify何时可以使用notify需要满足以下三个条件才能使用 1.所有等待线程拥有相同的等待条件。2.所有等待线程被唤醒后,执行相同的操作。3.只需要唤醒一个线程。 活跃性问题活跃性问题,指的是某个操作无法再执行下去,死锁就是其中活跃性问题,另外的两种活跃性问题分别为 饥饿 和 活锁 饥饿在上面的例子当中,我们看到线程3由于无法访问它所需要的资源而不能继续执行时,就发生了“饥饿”,如果在Java应用程序中对线程的优先级使用不当或者在持有锁时执行一些无法结束的结构(无线循环、无限制的等待某个资源),那么也可能发生饥饿。解决饥饿的问题有三种:1.保证资源充足,2.公平地分配资源,3.避免线程持有锁的时间过长。但是只有方案2比较常用到。在并发编程里,主要是使用公平锁,也就是先来后到的方案,线程等待是有顺序的,不会去争抢资源。这里不展开讲公平锁. 活锁活锁是另一种活跃性问题,尽管不会阻塞线程,但是也不能继续执行,这就是活锁,因为程序会不断的重复执行相同的操作,而且总是会失败。就如两个非常有礼貌的人在路上相撞,两个人都非常有礼貌的让到另一边,这样就又相撞了,然后又….,不断地变道,不断地相撞。在编程领域当中:假如有资源A、B,线程1获取到了资源A的锁,线程2获取到了资源B的锁,此时线程1需要再获取资源B的锁,线程2需要再获取资源A的锁,两个线程获取锁资源失败后释放自己所持有的锁,然后再此重新获取资源锁。这是就又发生了刚才的事情。就这样不断的循环,却又没阻塞。这就是活锁的例子。如图:解决活锁的问题就是各自等待一个随机的时间再做后续操作。这样同时相撞的概率就很低了。 总结本文主要讨论了使用等待-通知获取锁来优化不断循环获取锁的机制。若获取锁资源耗时短和并发冲突少则也可以使用不断循环获取锁的机制,否则尽量使用等待-通知获取锁。唤醒线程的方式有notify()和notifyAll(),但是notify()只会随机唤醒一个线程,容易导致线程饥饿,所以尽量使用notifyAll()方式来唤醒线程。 参考文章:《Java并发编程实战》第10章 活跃性危险极客时间:Java并发编程实战 06: 用“等待-通知”机制优化循环等待极客时间:Java并发编程实战 07: 安全性、活跃性以及性能问题 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/05/20/java/Java%E5%B9%B6%E5%8F%91-5/"},{"title":"Java并发编程实战总结 (一)","text":"Java并发编程系列:Java并发编程实战总结 (一) 前提首先该场景是一个酒店开房的业务。为了朋友们阅读简单,我把业务都简化了。业务:开房后会添加一条账单,添加一条房间排期记录,房间排期主要是为了房间使用的时间不冲突。如:账单A,使用房间1,使用时间段为2020-06-01 12:00 - 2020-06-02 12:00 ,那么还需要使用房间1开房的时间段则不能与账单A的时间段冲突。 业务类为了简单起见,我把几个实体类都简化了。 账单类12345678public class Bill { // 账单号 private String serial; // 房间排期id private Integer room_schedule_id; // ...get set} 房间类12345678// 房间类public class Room { private Integer id; // 房间名 private String name; // get set...} 房间排期类123456789101112131415import java.sql.Timestamp;public class RoomSchedule { private Integer id; // 房间id private Integer roomId; // 开始时间 private Timestamp startTime; // 结束时间 private Timestamp endTime; // ...get set} 实战并发实战当然少不了Jmeter压测工具,传送门: https://jmeter.apache.org/download_jmeter.cgi为了避免有些小伙伴访问不到官网,我上传到了百度云:链接:https://pan.baidu.com/s/1c9l3Ri0KzkdIkef8qtKZeA提取码:kjh6 初次实战(sychronized)第一次进行并发实战,我是首先想到sychronized关键字的。没办法,基础差。代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.stereotype.Service;import org.springframework.transaction.TransactionDefinition;import org.springframework.transaction.TransactionStatus;import java.sql.Timestamp;/** * 开房业务类 */@Servicepublic class OpenRoomService { @Autowired DataSourceTransactionManager dataSourceTransactionManager; @Autowired TransactionDefinition transactionDefinition; public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) { // 开启事务 TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition); try { synchronized (RoomSchedule.class) { if (isConflict(roomId, startTime, endTime)) { // throw exception } // 添加房间排期... // 添加账单 // 提交事务 dataSourceTransactionManager.commit(transaction); } } catch (Exception e) { // 回滚事务 dataSourceTransactionManager.rollback(transaction); throw e; } } public boolean isConflict(Integer roomId, Timestamp startTime, Timestamp endTime) { // 判断房间排期是否有冲突... }} sychronized(RoomSchedule.class),相当于的开房业务都是串行的。不管开房间1还是房间2。都需要等待上一个线程执行完开房业务,后续才能执行。这并不好哦。 事务必须在同步代码块sychronized中提交,这是必须的。否则当线程A使用房间1开房,同步代码块执行完,事务还未提交,线程B发现房间1的房间排期没有冲突,那么此时是有问题的。 错误点: 有些朋友可能会想到都是串行执行了,为什么不把synchronized关键字写到方法上?首先openRoom方法是非静态方法,那么synchronized锁定的就是this对象。而Spring中的@Service注解类是多例的,所以并不能把synchronized关键字添加到方法上。 二次改进(等待-通知机制)因为上面的例子当中,开房操作都是串行的。而实际情况使用房间1开房和房间2开房应该是可以并行才对。如果我们使用synchronized(Room实例)可以吗?答案是不行的。在第三章 解决原子性问题当中,我讲到了使用锁必须是不可变对象,若把可变对象作为锁,当可变对象被修改时相当于换锁,这里的锁讲的就是synchronized锁定的对象,也就是Room实例。因为Room实例是可变对象(set方法修改实例的属性值,说明为可变对象),所以不能使用synchronized(Room实例)。在这次改进当中,我使用了第五章 等待-通知机制,我添加了RoomAllocator房间资源分配器,当开房的时候需要在RoomAllocator当中获取锁资源,获取失败则线程进入wait()等待状态。当线程释放锁资源则notiryAll()唤醒所有等待中的线程。RoomAllocator房间资源分配器代码如下: 123456789101112131415161718192021222324252627282930313233343536373839import java.util.ArrayList;import java.util.List;/** * 房间资源分配器(单例类) */public class RoomAllocator { private final static RoomAllocator instance = new RoomAllocator(); private final List<Integer> lock = new ArrayList<>(); private RoomAllocator() {} /** * 获取锁资源 */ public synchronized void lock(Integer roomId) throws InterruptedException { // 是否有线程已占用该房间资源 while (lock.contains(roomId)) { // 线程等待 wait(); } lock.add(roomId); } /** * 释放锁资源 */ public synchronized void unlock(Integer roomId) { lock.remove(roomId); // 唤醒所有线程 notifyAll(); } public static RoomAllocator getInstance() { return instance; }} 开房业务只需要修改openRoom的方法,修改如下: 12345678910111213141516171819202122public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) throws InterruptedException { RoomAllocator roomAllocator = RoomAllocator.getInstance(); // 开启事务 TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition); try { roomAllocator.lock(roomId); if (isConflict(roomId, startTime, endTime)) { // throw exception } // 添加房间排期... // 添加账单 // 提交事务 dataSourceTransactionManager.commit(transaction); } catch (Exception e) { // 回滚事务 dataSourceTransactionManager.rollback(transaction); throw e; } finally { roomAllocator.unlock(roomId); }} 那么此次修改后,使用房间1开房和房间2开房就可以并行执行了。 总结上面的例子可能会有其他更好的方法去解决,但是我的实力不允许我这么做….。这个例子也是我自己在项目中搞事情搞出来的。毕竟没有实战经验,只有理论,不足以学好并发。希望大家也可以在项目中搞事情[坏笑],当然不能瞎搞。后续如果在其他场景用到了并发,也会继续写并发实战的文章哦~ 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/06/06/java/Java%E5%B9%B6%E5%8F%91-6/"},{"title":"Java核心技术第八章-泛型","text":"摘要本文根据《Java核心技术 卷一》一书的第八章总结而成,部分文章摘抄书内,作为个人笔记。文章不会过于深入,望读者参考便好。 为什么要使用泛型程序设计泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。 类型参数的好处在没有泛型类之前,ArrayList类只维护一个Object引用的数组: 123456public class ArrayList { private Object[] elementData; // 用于存放数据 public Object get(int i) { . . . } public void add(Object o) { . . . } ...} 问题 1.获取一个值时必须进行强制类型转换2.这里没有错误检査。可以向数组列表中添加任何类的对象,如果数组的类型不一致,将 get 的结果进行强制强制类型,就会错误。 泛型提供了一个更好的解决方案: 类型参数: 1ArrayList<String> array = new ArrayList<String>(): 利用类型参数的信息,我们就可以在添加数据的时候保持类型统一,调用get方法时候也不需要进行强制类型转换,因为我们在初始化的时候就定义了类型,编译器识别返回值的类型就会帮我们转换该类型。 定义一个简单泛型类1234567public class Pair<T> { private T num; public Pair(T num) { this.num = num; } public T getNum() { return num; } public void setNum(T num) { this.num = num; }} 我们可以看到,在Pair类名后面添加了一个,这个是泛型类的类型变量,而且还可以有多个类型变量,如 123public class Pair<T,U> { ...} 如果我们实例化Pair类,例如: 1new Pair<String>; 那么我们就可以把上述的Pair类想象成如下: 1234567public class Pair<String> { private String num; public Pair(String num) { this.num = num; } public String getNum() { return num; } public void setNum(String num) { this.num = num; }} 是不是很简单呢?在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型,T、U、S表示任意类型。 泛型方法定义一个带有类型参数的方法 123public static <T> T getMiddle(T... a) { return a[a.length / 2];} 可以看到类型变量(< T >)放在修饰符( public static )的后面,返回类型(T)的前面。泛型方法可以定义在普通类或泛型类中。 类型变量的限定如果我们需要对类型变量加以约束,例如:传入的变量必须实现Comparable接口,因为需要该变量调用compareTo的方法。这样我们就可以使用extends关键字对变量进行限定。 1234public <T extends Comparable> T max(T a) { a.compareTo(...); ...} 无论变量需要限定为继承某个类或者实现某个接口,都是使用extends关键字进行限定。 泛型代码和虚拟机类型擦除无论我们在代码中怎么定义一个泛型类、泛型方法,都提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类姓名。如 <T> 的原始类型为 Object,<T extends MyClass>的原始类型为MyClass。代码就像下面这样: 1234567public class Pair<T> { private T property; public Pair(T property) { this.property = property; }} 类型擦除后: 1234567public class Pair<Object> { private Object property; public Pair(Object property) { this.property = property; }} 翻译泛型表达式如果擦除返回类型,编译器会插入强制类型转换,就像下面这样: 12Pair<Employee> buddies = . .Employee buddy = buddies.getFirst(); 擦除getFirst的返回类型后将返回Object类型,但是编译器将自动帮我们强制类型转换为Employee。所以:编译器把这个方法执行操作分为两条指令: 对原始方法Pair.getFirst的调用将返回的Object类型强制转换为Employee类型 小节总结: 虚拟机中没有泛型,只有普通的类和方法所有的类型参数都用他们的限定类型替换为保持类型安全性,必要时插入强制类型转换桥方法被合成来保持多态(本文没有讲到,不过桥方法可以忽略,Java编写不规范才会有桥方法生成) 约束与局限性不能用基本类型实例化类型参数不可以用八大基本数据类型去实例化类型参数,你也没见过ArrayList<int>这样的代码吧。只有ArrayList<Integer>。原因是因为基本数据类型是不属于Object的。所以只能用他们的包装类型替换。 运行时类型查询只适用于原始类型所有的类型查询只产生原始类型,因为在虚拟机没有所谓的泛型类型。例如: 1234Pair<String> pair = new Pair<String>("johnson");if (pair instanceof Pair<String>) {} // Errorif (pair instanceof Pair<T>) {} // Errorif (pair instanceof Pair) {} // Pass 不能创建参数化类型的数组1Pair<String>[] pairs = new Pair<String>[10]; //error 为什么不能这样定义呢?因pairs的类型是Pair[],可以转换为Object[],如果试图存储其他类型的元素,就会抛出ArrayStoreException异常, 1pairs[0] = 10L; //抛异常 总之一句话,不严谨。所以不能创建参数化类型的数组。 不能实例化类型变量不能使用 new T(…)、new T[…] 或 T.class。因为类型擦除后,T将变成Object,而且我们肯定不是希望实例化Object。不过在Java8之后,我们可以使用Supplier<T>接口实现,这是一个函数式接口,表示一个无参数而且有返回类型为T的函数: 1234567891011121314151617public class Pair<T> { private T first; private T second; private Pair(T first, T second) { this.first = first; this.second = second; } public static <T> Pair<T> makePair(Supplier<T> constr) { return new Pair<>(constr.get(), constr.get()); } public static void main(String[] args) { Pair<String> pair = Pair.makePair(String::new); }} 泛型类中的静态上下文中类型变量无效不能在静态域或方法中引用类型变量。例如: 123456789public class Pair<T> { private static T instance; //Error public static T getInstance() { //Error if (instance == null) instance = new Pair<T>(); return instance; }} 因为类中的类型变量(<T>)是在对象中的作用域有效,而不是在类中的作用域有效。如果要使用泛型方法,可以参照文章上面的泛型方法哦~ 不能抛出或捕获泛型类的实例即不能抛出也不能捕获泛型类的对象,甚至扩展Throwable都是不合法的: 1public class Pair<String> extend Exceotion {} //Error 1234567public static <T extends Throwable> void doWork(Class<T> t) { try { ... } catch (T e) { //Error ... }} 但是在抛出异常后对异常使用类型变量是允许的(个人感觉没见过这样的代码)。 1234567public static <T extends Throwable> void doWork(Class<T> t) throw T { //Pass try { ... } catch (Exception e) { throw e; }} 泛型类型的继承规则如Manager类继承Employee类。但是Pair<Employee>和Pair<Manager>是没有关联的。就像下面的代码,就会提示报错,传递失败: 12Pair<Manager> managerPair = new Pair<Manager>();Pair<Employee> employeePair = managerPair; //Error 通配符类型通配符概念通配符类型中,允许类型参数变化,使用 ? 标识通配符类型: 1Pair<? extends Employee> 若Pair类如下 1234567public class Pair<T> { private T object; public void setObject(T object) { this.object = object; }} 那么使用通配符可以解决泛型类型的继承规则问题,如: 1234Pair<Manager> managerPair = new Pair<Manager>();Pair<? extends Employee> employeePair = managerPair; //PassManager manager = new Manager();employeePair.setObject(manager); //Error 使用<? extends Employee>,编译器只知道employeePair 是Employee的子类,但是不清楚具体是什么子类,所以最后一步employeePair.setObject(manager)不能执行。 通配符的超类型限定通配符还有一个附加的能力,就是可以指定一个超类型限定,如: 123456public class Pair<T> [ ... public static void salary(Pair<? super Manager> result) { //... }} <? super Manager>这个通配符为Manager的所有超类型(包含Manger),例如: 123456789 Pair<Manager> managerPair = new Pair<Manager>(); Pair<Employee> employeePair = new Pair<Employee>(); Pair.salary(managerPair); //Pass Pair.salary(employeePair); //Pass// 假如 ManagerChild为Manager子类 Pair<ManagerChild> managerChildPair = new Pair<ManagerChild>(); Pair.salary(managerChildPair); //Error 无限定通配符无限定通配符,如:Pair<?>,当我们不需要理会他的实际类型时候,就可以使用无限定通配符,上代码: 123public static boolean hasNull(Pair<?> pair) { return pair.getObject() == null;} 说实话,通配符搞得我头昏脑胀的,反复不断地看文章,才开始慢慢看懂,我太难了。。。文章到这里就结束啦,不知道各位小伙伴看懂了没,没看懂的话可能是我的功底和文章写作能力还有待提高,小伙伴们也可以去看一下《Java核心技术 卷一》这本书呢,感觉还是挺不错的。最近把这本书捡起来看也是发现基础是非常重要的,先把基础沉淀好了,再学习其他的技术点也会更容易入手,也会知其然知其所然。最近非常喜欢的一句话,送给大家:“万丈高楼平地起,勿在浮沙筑高台”。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2019/12/04/java/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E7%AC%AC%E5%85%AB%E7%AB%A0-%E6%B3%9B%E5%9E%8B/"},{"title":"Java移位运算符","text":"简述Java有三种移位运算符,分别为: 左移运算符 << 右移运算符 >> 无符号右移运算符 >>> 首先,移位运算符根据名字可知是使用二进制进行运算的。在Integer.java中,我们可以看到有两个静态常量,MIN_VALUE 和 MAX_VALUE,这两个常量控制了Integer的最小值和最大值,如下: 1234567891011/** * A constant holding the minimum value an {@code int} can * have, -2<sup>31</sup>. */@Native public static final int MIN_VALUE = 0x80000000;/** * A constant holding the maximum value an {@code int} can * have, 2<sup>31</sup>-1. */@Native public static final int MAX_VALUE = 0x7fffffff; 注释上说明这两个值得范围: MIN_VALUE(最小值) = -2^31 = -2,147,483,648‬MAX_VALUE(最大值) = 2^31 = 2,147,483,647 在32位运算中,首位为1则代表负数,0则代表正数,如: 1000 0000 0000 0000 0000 0000 0000 0000 负数,该值等于MIN_VALUE0111 1111 1111 1111 1111 1111 1111 1111 正数,该值等于MAX_VALUE 根据上述可知,Integer是32位运算的。 左移运算符 <<使用 << 时,需要在低位进行补0,例子如下: 123456789101112131415int a = 3;System.out.println(Integer.toBinaryString(a));int b = a << 1;System.out.println(Integer.toBinaryString(b));System.out.println(b);System.out.println("----------------------------------------------");int c = -3;System.out.println(Integer.toBinaryString(c));int d = c << 1;System.out.println(Integer.toBinaryString(d));System.out.println(d); 输入如下: 1234567111106----------------------------------------------1111111111111111111111111111110111111111111111111111111111111010-6 可以清楚的看到 3 << 1 时,在后面补0,得到 110 即等于6; 右移运算符 >>右移运算符时,正数高位补0,负数高位补1。如: 123456789101112131415int a = 3;System.out.println(Integer.toBinaryString(a));int b1 = a >> 1;System.out.println(Integer.toBinaryString(b1));System.out.println(b1);System.out.println("----------------------------------------------");int c = -3;System.out.println(Integer.toBinaryString(c));int d = c >> 1;System.out.println(Integer.toBinaryString(d));System.out.println(d); 输出如下: 12345671111----------------------------------------------1111111111111111111111111111110111111111111111111111111111111110-2 无符号右移 >>>在正数当中,>> 和 >>> 是一样的。负数使用无符号右移时,则高位不进行补位。 123456int c = -3;System.out.println(Integer.toBinaryString(c));int d = c >>> 1;System.out.println(Integer.toBinaryString(d));System.out.println(d); 输出如下: 1231111111111111111111111111111110111111111111111111111111111111102147483646 #总结 左移运算符 << : 需要在低位进行补0右移运算符 >> : 正数高位补0,负数高位补1无符号右移运算符 >>> :在正数当中,>> 和 >>> 是一样的。负数使用无符号右移时,则高位不进行补位","link":"/2019/10/29/java/Java%E7%A7%BB%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6/"},{"title":"SpringBoot上传文件到七牛云","text":"准备工作mavenpom.xml添加七牛云的sdk依赖 12345<dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.27</version></dependency> 配置项七牛云上传必要的配置有:accessKey、secretKey、bucket其中accessKey、secretKey在该网址可查看 https://portal.qiniu.com/user/key bucket为你的存储空间名,如下: 实现application.yml配置123456upload: qiniu: domain: 填你的域名 access-key: 你的accesskey secret-key: 你的secretKey bucket: 你的存储空间名,我这里为colablog 可以看到我的七牛云上传配置中有domain这项配置,这个配置是七牛云buket的存储域名,在内容管理下,主要用于上传文件成功后把文件访问路径返还回去。但是这个域名是限时30天使用的,所以你最好绑定一个新的域名。 上传配置类使用SpringBoot的@ConfigurationProperties和@Component注解实现上传的配置类UploadProperties,因为上传配置可能会有本地上传和云上传或者其他上传的,所以该配置类我留了扩展点。因为受到了rabbitmq的配置类启发,而且上传的配置不会很多,所以用内部类来分割这种配置类。上传配置类如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;/** * @author Johnson * @date 2019/12/16/ 09:35:36 */@Component@ConfigurationProperties(prefix = "upload")public class UploadProperties { private Local local = new Local(); public Local getLocal() { return local; } /** * @author: Johnson * @Date: 2019/12/16 * 本地上传配置 */ public class Local { ... } private Qiniu qiniu = new Qiniu(); public Qiniu getQiniu() { return qiniu; } /** * @author: Johnson * @Date: 2019/12/16 * 七牛云上传配置 */ public class Qiniu { /** * 域名 */ private String domain; /** * 从下面这个地址中获取accessKey和secretKey * https://portal.qiniu.com/user/key */ private String accessKey; private String secretKey; /** * 存储空间名 */ private String bucket; public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getAccessKey() { return accessKey; } public void setAccessKey(String accessKey) { this.accessKey = accessKey; } public String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public String getBucket() { return bucket; } public void setBucket(String bucket) { this.bucket = bucket; } }} 七牛云上传接口和类上传接口如下: 1234public interface UploadFile { String uploadFile(MultipartFile file);} 上传类 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import cn.colablog.blogserver.utils.properties.UploadProperties;import com.qiniu.http.Response;import com.qiniu.storage.Configuration;import com.qiniu.storage.Region;import com.qiniu.storage.UploadManager;import com.qiniu.util.Auth;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.util.UUID;/** * @author Johnson * @date 2019/12/14/ 17:20:16 * 上传文件到七牛云 */public class UploadFileQiniu implements UploadFile { private UploadProperties.Qiniu properties; //构造一个带指定Region对象的配置类 private Configuration cfg = new Configuration(Region.region2()); private UploadManager uploadManager= new UploadManager(cfg); public UploadFileQiniu(UploadProperties.Qiniu properties) { this.properties = properties; } /** * @author: Johnson */ @Override public String uploadFile(MultipartFile file) { Auth auth = Auth.create(properties.getAccessKey(), properties.getSecretKey()); String token = auth.uploadToken(properties.getBucket()); try { String originalFilename = file.getOriginalFilename(); // 文件后缀 String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileKey = UUID.randomUUID().toString() + suffix; Response response = uploadManager.put(file.getInputStream(), fileKey, token, null, null); return properties.getDomain() + fileKey; } catch (IOException e) { e.printStackTrace(); } return "error"; }} Region配置,这里代表空间的存取区域,因为我的存储空间的区域为华南。所以为Region.region2(),查看自己的存储区域可在空间概览的最下方查看到,这里就不截图了,图片占位太大。Region对应的设置: 好了,准备工作已完成,现在就到调用了,调用类如下: 12345678910111213@RestController@RequestMapping("/upload")public class UploadFileController { @Autowired UploadProperties uploadProperties; @PostMapping("/img") public String uploadFileYun(MultipartFile file) { // 上传到七牛云 UploadFile uploadFile = new UploadFileQiniu(uploadProperties.getQiniu()); return uploadFile.uploadFile(file); }} 是不是很简单呢?屁啊!简单个毛线,其实这个我是已经简化了,实际上在我的项目的结构是比这个复杂的。 总结一:我的类名都是以Upload开头的,类名已经写死了只有上传功能,就限制了这个类的可扩展性了,假如添加文件删除功能,就不应该添加到这个类中。现在已经在重构文件操作(非文件上传了)的功能模块了。 二:一开始我觉得文件上传功能可能使用的比较少,所以使用到的时候才去实例化文件上传类,但是这需要根据开发场景来决定,因为我的项目是一个博客后台管理系统,会经常上传图片,所以上传文件类可以注入到Spring容器中,这样也可以减少实例化的开销(虽然很小)。注入的话就是用@Component类注解。 三:配置文件我为什么会想到使用内部类来分割配置项呢?其实大家在编写一些类似功能的时候,都可以去参照一下别人的源码,当然,这里指的是大神的源码。因为我在写配置项的时候就想看看大神的配置项是怎么写的,就点进了rabbitmq的配置项。所以啊,看到了大神的代码是真的有长进的。 如果你需要查看更详细的官方文档,请点击下方链接: https://developer.qiniu.com/kodo/sdk/1239/java 最后:感谢大家的阅读,Thanks♪(・ω・)ノ","link":"/2019/12/16/java/SpringBoot%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E5%88%B0%E4%B8%83%E7%89%9B%E4%BA%91/"},{"title":"优雅的使用BeanUtils对List集合的操作","text":"摘要我们在Entity、Bo、Vo层数据间可能经常转换数据,Entity对应的是持久层数据结构(一般是数据库表的映射模型)、Bo对应的是业务层操作的数据结构、Vo就是Controller和客户端交互的数据结构。 在这些数据结构之间很大一部分属性都可能会相同,我们在使用的时候会不断的重新赋值。如:客户端传输管理员信息的到Web层,我们会使用AdminVo接收,但是到了Service层时,我就需要使用AdminBo,这时候就需要把AdminVo实例的属性一个一个赋值到AdminBo实例中。 BeanUtilsSpring 提供了 org.springframework.beans.BeanUtils 类进行快速赋值,如:AdminEntity类 12345678910public class AdminEntity{ private Integer id; private String password; private String username; private String userImg; .... //一些 Set Get方法} AdminVo类,因为是和客户端打交道的,所以password属性就不适合在这里了 12345678public class AdminVo{ private Integer id; private String username; private String userImg; .... //一些 Set Get方法} 假如我们需要把AdminEntity实例属性值赋值到AdminVo实例中(暂时忽略Bo层吧) 1234AdminEntity entity = ...;AdminVo vo = new AdminEntity();// org.springframework.beans.BeanUtilsBeanUtils.copyProperties(entity, vo); // 赋值 那么这样AdminVo实例中的属性值就和AdminEntity实例中的属性值一致了。但是如果我们是一个集合的时候就不能这样直接赋值了。如: 123List<Admin> adminList = ...;List<AdminVo> adminVoList = new ArrayList<>(adminList.size());BeanUtils.copyProperties(adminList, adminVoList); // 赋值失败 这样直接赋值是不可取的,由方法名(copyProperties)可知,只会复制他们的属性值,那么上述的adminList属性和adminVoList的属性是没有半毛钱关系的。那么怎么解决了? 方式一(暴力解决,不推荐)代码如下: 12345678 List<Admin> adminList = ...; List<AdminVo> adminVoList = new ArrayList<>(adminList.size()); for (Admin admin : adminList) { AdminVo adminVo = new AdminVo(); BeanUtils.copyProperties(admin, adminVo); adminVoList.add(adminVo); }return adminVoList; 虽然for循环可以解决,但是一点都不优雅,这样的代码也会陆续增多,代码多了,就会看出来了重复的地方,没错!就是for循环赋值的地方,完全可以优化掉(代码写多了一眼就看出来了)。那么请看优雅的方式二 方式二 (优雅、推荐)这也是我第一次写泛型的代码,可能有待提高,如下:ColaBeanUtils类(Cola是我家的狗狗名,哈哈) 123456789101112131415161718192021222324252627import org.springframework.beans.BeanUtils;public class ColaBeanUtils extends BeanUtils { public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target) { return copyListProperties(sources, target, null); } /** * @author Johnson * 使用场景:Entity、Bo、Vo层数据的复制,因为BeanUtils.copyProperties只能给目标对象的属性赋值,却不能在List集合下循环赋值,因此添加该方法 * 如:List<AdminEntity> 赋值到 List<AdminVo> ,List<AdminVo>中的 AdminVo 属性都会被赋予到值 * S: 数据源类 ,T: 目标类::new(eg: AdminVo::new) */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target, ColaBeanUtilsCallBack<S, T> callBack) { List<T> list = new ArrayList<>(sources.size()); for (S source : sources) { T t = target.get(); copyProperties(source, t); list.add(t); if (callBack != null) { // 回调 callBack.callBack(source, t); } } return list; } ColaBeanUtilsCallBack接口,使用java8的lambda表达式注解: 12345@FunctionalInterfacepublic interface ColaBeanUtilsCallBack<S, T> { void callBack(S t, T s);} 使用方式如下: 12List<AdminEntity> adminList = ...; return ColaBeanUtils.copyListProperties(adminList, AdminVo::new); 如果需要在循环中做处理(回调),那么可使用lambda表达式: 12345List<Article> adminEntityList = articleMapper.getAllArticle();List<ArticleVo> articleVoList = ColaBeanUtils.copyListProperties(adminEntityList , ArticleVo::new, (articleEntity, articleVo) -> { // 回调处理});return articleVoList; 简直不要太简单了!!! 总结AdminVo::new配合Supplier<T> target这里我完全是参考《Java核心技术第十版 卷一》的第八章泛型,因为之前看过有点印象,没想到今天用到了。@FunctionalInterface这个是java8的lambda表达式的注解类,参考java.util.function.Consumer接口。没想到懵懵懂懂的,就把之前看过的知识写出来了,惊呆了,哈哈。代码如果雷同,纯属巧合。若泛型的代码还有可以改进的地方,可在下方留言,非常感谢,Thanks♪(・ω・)ノ。","link":"/2019/12/30/java/%E4%BC%98%E9%9B%85%E7%9A%84%E4%BD%BF%E7%94%A8BeanUtils%E5%AF%B9List%E9%9B%86%E5%90%88%E7%9A%84%E6%93%8D%E4%BD%9C/"},{"title":"CentOS搭建Nginx环境","text":"准备Nginx的依赖软件GCC编译器GCC编译器和G++,用于编写Nginx HTTP模块 12yum install -y gccyum install -y gcc-c++ PCRE库函数库,支持正则表达式,如果在nginx.conf里面使用了正则表达式,那么编译Nginx时就必须引进PCRE库,用于解析HTTP模块的正则表达式,如果你不会用到正则表达式则可以忽略。 1yum install -y pcre pcre-devel zlib库用于对http包的内容做gzip格式的压缩。 1yum install -y zlib zlib-devel OpenSSL开发库使用SSL协议上安全传输HTTP,就是所谓的https。 1yum install -y openssl openssl-devel 安装Niginx首先当Nginx官网下载源码包,官网下载地址:http://nginx.org/en/download.html也可以和我一样下载1.16.1版本。 123cd ~ #回到家目录wget http://nginx.org/download/nginx-1.16.1.tar.gz #下载源码包tar -zxvf nginx-1.16.1.tar.gz 然后我们开始进行编译安装Nginx,进入解压后的目录后,执行以下3行命令: 123./configuremakemake install 默认情况下,Nginx会被安装到目录/usr/local/nginx中,然后我们来启动一下Nginx吧。 1/usr/local/nginx/sbin/nginx 在浏览器输入你的ip地址,就能看到Welcome to nginx!啦!启动好了就该关闭掉拉,毕竟是测试,快速停止服务如下: 12usrlocal/nginx/sbin/nginx -s stop #强制退出usrlocal/nginx/sbin/nginx -s stop #正常退出 强制退出这个命令一般不太建议使用,就像电脑重装系统,安装到一半来个关机然后你就爽歪歪。建议使用正常退出。下一篇继续讲Niginx的,如果帮到你,请关注我啦!!~","link":"/2019/09/02/linux/CentOS%E6%90%AD%E5%BB%BANginx%E7%8E%AF%E5%A2%83/"},{"title":"ubuntu防火墙规则之ufw","text":"前言因公司项目的需求,需要对客户端机器简便使用防火墙的功能,所以可在页面进行简便设置防护墙规则,当然,这个功能需求放到我手上我才有机会学到。因为客户端机器都是ubuntu的,所以当然用了ubuntu特有且简便的防火墙设置规则,那就是ufw,文章以ubuntu16.04为准,其它版本的用法应该也差不太多。本文着重介绍其常用的用法,至于其他的用法那就要等各位小伙伴再自行研究了。 wiki UFW 全称为Uncomplicated Firewall,是Ubuntu 系统上默认的防火墙组件, 为了轻量化配置iptables 而开发的一款工具。 UFW 提供一个非常友好的界面用于创建基于IPV4,IPV6的防火墙规则。 # ufw使用教程 使用ufw的命令必须有管理员权限才可运行,没有的话就要sudo一下了,不过要注意安全,不能瞎搞哈。。。 开启和禁用123# ufw enable //开启防火墙# ufw disable //禁用防火墙# ufw reset //重置防火墙,会把你所有已添加的规则全部删除,并且禁用防火墙 可以使用以下命令查看ufw防火墙的状态 1234567# ufw status// 没开启是这个样子的Status: inactive //开启后是这样子的Status: active... // 如果你添加了防火墙规则下面这里就会显示 设置默认的防火墙规则,默认为允许,就是说什么玩意都允许你连进来。 123# ufw default allow|deny //设置默认规则allow : 允许deny : 拒绝 协议规则协议规则就是有关于协议的一些防火墙规则。 12345678910ufw [delete] [insert NUM] allow|deny [in|out] [PORT[/PROTOCOL] ] [comment COMMENT]delete : 删除这个规则insert : Num代表你要插入到防火墙规则的那个位置,规则是有序排列的。会根据需要来一个个检查allow|deny : 这条规则是允许的还是禁用的in|out: 这条规则对发送还是接收数据生效PORT: 端口号protocol : 协议,例如TCP还是UDPcomment : 注释... 添加一条允许ssh的规则(ssh的端口号是22,协议是TCP),并且插入到位置2 1# ufw insert 2 allow in 22/tcp 禁用22端口连入 1# ufw deny in 22 ip规则ip规则里面可以包含端口号和协议,反过来则不行。 12345678ufw [delete] [insert NUM] allow|deny [in|out [on INTERFACE]] [proto PROTOCOL] [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] [comment COMMENT]INTERFACE :网卡,就是针对哪个网卡生效,可以使用ifconfig或ip addr查看你的网卡form ADDRESS : 源IP地址to ADDRESS : 目标IP地址PORT : 跟在源IP地址后面就是源IP地址的端口号,反之则是目标IP地址的端口号其他的都和协议规则的一致 添加允许192.168.0.2 的22端口tcp协议(ssh)的规则 1# ufw allow proto tcp from 192.168.0.2 port 22 若你的系统上有帮他人进行转发信息的进程,那么你可以允许来自某个源IP地址发送信息到某个目标地址,例:允许源IP地址192.168.0.2的8088端口 发送到 目标地址192.162.0.2的8080端口 1# ufw allow from 192.168.0.2 port 80 to 192.168.0.2 port 8080 删除规则删除规则分两种,一种是根据规则的内容删除,一种是根据序号删除 方式一刚才添加规则的命令前面添加delete参数,例: 12# ufw allow 22/tcp //添加一条允许ssh的规则# ufw delete allow 22/tcp //删除ssh规则 方式二根据序号删除,怎么知道规则的序号呢?使用ufw status numbered 1234567# ufw status numbered //查Status: active To Action From -- ------ ----[ 1] 22 ALLOW IN Anywhere 我需要删除第一条规则 1# ufw delete 1 //这样就是删除第一条规则啦 推荐设置12345# ufw enbale //开启防火墙# ufw alllow ssh // 添加ssh的规则,这是简写规则# ufw default deny //设置默认为禁用,但是我们已经添加了ssh规则,就不担心。后面这里你们就可以自己搞事情啦!... 好了,以上讲的都是比较基本的用法,想要深入了解的话可以自行到官网上看看,后面会出一章关于iptables的防火墙规则,ufw就是基于iptables上进行封装的,iptables适用于所有Linux系统哦,不单单是只有Ubuntu了。这篇文章到此结束,感谢各位小伙伴的阅读,Thanks♪(・ω・)ノ","link":"/2019/07/29/linux/ubuntu-ufw/"},{"title":"docker日常使用方式","text":"前提在安装docker之前,建议你设置系统的国内镜像源先哦,很快~嗯,快。阿里云镜像源:https://developer.aliyun.com/mirror/ 安装安装docker下面都是官网地址:ubuntu: https://docs.docker.com/engine/install/ubuntu/centos:https://docs.docker.com/engine/install/centos/其他版本就是url后面的几个英文不同。 开机启动1sudo systemctl enable docker.service 设置国内镜像 docker中国区的镜像:https://registry.docker-cn.com网易:http://hub-mirror.c.163.com中国科技大学:https://docker.mirrors.ustc.edu.cn阿里云:https://cr.console.aliyun.com/ 点击左侧栏有个镜像加速地址,就可以看到你的加速镜像地址 12345678sudo mkdir -p /etc/dockersudo tee /etc/docker/daemon.json <<-'EOF'{ "registry-mirrors": ["加速镜像地址"]}EOFsudo systemctl daemon-reloadsudo systemctl restart docker 安装docker-compose(用到才装)官网地址:https://docs.docker.com/compose/install/ 设置.docker文件权限docker安装好后,会在当前用户的家目录下生成.docker文件,该文件不出意外的话是属于root用户和root组,毕竟要sudo下载,使用ll命令查看一下。 1drwx------ 2 root root 4096 Jun 5 11:26 .docker/ 假设我的用户和组都为vagrant,那么命令如下: 1sudo chown -R vagrant:vagrant 当前用户的家目录/.docker 上面的vagrant:vagrant对应的是用户名:用户组 ,一般情况下现在使用docker指令则不需要在前面加sudo了(去你丫的sudo)安装步骤已完成 镜像?容器?什么是镜像?什么是容器呐?可以这么类比:镜像就是一个模板;容器则是根据模板的实现。在代码中,你也可以想成接口(镜像)与实现类(容器)。 docker镜像中心地址:https://hub.docker.com/,在这里你可以找到你所需要的镜像,你可以搜mysql、redis、nginx等等,可以查到有什么版本、怎么运行、有什么配置,有什么环境变量可以设置(比如在mysql你需要设置他的root密码)等等信息。 指令集以mysql为例 下载镜像使用docker pull命令 1docker pull mysql:8.0.20 查看所有镜像,可以看到你刚下载的mysql镜像 1docker images 创建并运行容器创建并运行你的mysql容器,我使用一条比较长的命令来讲解: 1sudo docker run --name mysql_1 -p 3306:3306 -v /var/mapping/mysql/conf.d:/etc/mysql/conf.d -v /var/mapping/mysql_1/lib:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:8.0.20 --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 指令格式 docker run [OPTIONS] IMAGE [COMMAND] docker run:会创建并运行容器-p:映射端口 3306:3306 就是把容器的3306端口映射到宿主机的3306端口, 格式(宿主机端口:容器端口)-v:映射数据卷,即映射文件,格式(宿主机数据卷:容器数据卷)-e:设置环境变量,如设置root的初始密码 MYSQL_ROOT_PASSWORD,可以在docker的镜像中心查看有哪些环境变量-d:指定镜像 --default-authentication-plugin=mysql_native_password 和--character-set-server=utf8mb4属于command参数,需要设置在镜像后面。--default-authentication-plugin=mysql_native_password指的是修改认证的加密方式,mysql8.0后的加密方式改了,导致navicat那些数据库管理工具连不上。所以这里修改加密方式为mysql_native_password--character-set-server=utf8mb4则是设置数据库的编码方式为utf8mb4。当然还有很多的command指定都可以在docker hub镜像中心查看。 自动运行当docker启动的时候,容器也自动启动,有两种方式1.创建容器时指定 --restart=always 1docker run --restart=always -d mysql:8.0.20 2.容器已经创建好了,使用docker update修改容器,CONTAINER可以是容器id,也可以是容器名,docker ps 可以命令查看 1docker update --restart=always [CONTAINER] 常用指令首先说明一下:CONTAINER可以是容器ID,也可以是容器名,IMAGE可以是镜像ID,也可以是 镜像名:tag查看所有镜像 1docker images 查看所有容器,包括没启动的(加 -a 选项) 1docker ps -a 启动容器,停止容器 1234docker start [CONTAINER]docker start CONTAINER1 CONTAINER2 # 启动多个容器docker stop [CONTAINER]docker stop CONTAINER1 CONTAINER2 # 停止多个容器 进入容器 1docker exec -it [CONTAINER] bash 在宿主机执行容器内的命令,mysqldump是mysql_1容器内的指令 1docker exec -it mysql_1 mysqldump ... 删除容器 12docker rm [CONTAINER]docker rm CONTAINER1 CONTAINER2 # 删除多个容器 删除镜像 12docker rmi [IMAGE]docker rmi IMAGE1 IMAGE2 # 删除多个镜像 docker镜像仓库当你在公司修改了某个容器之后,想在家也使用这个容器。那么可以把容器打包成镜像,提交到docker仓库当中。我是在aliyun上创建了docker镜像仓库:https://cr.console.aliyun.com/首先把你的容器打包成镜像,这个是把我的mysql_1容器打包成镜像my_mysql_1,tag为latet。 1docker commit mysql_1 my_mysql_1:latest 打包完后执行指令docker images,查看镜像是否存在。 提交到镜像仓库1.首先需要创建命名空间2.创建你的镜像仓库3.登录并提交到仓库点击所需提交到镜像仓库的管理按钮,可以查看到以下界面 跟着这里面的步骤执行就可以提交到docker镜像仓库了。就介么简单。 总结以上便是docker比较常用的操作。ok,拜拜你了~。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/08/19/linux/docker%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F/"},{"title":"鸟哥的Linux私房菜笔记第四章-首次登录与在线求助","text":"前言对着《鸟哥的Linux私房菜-基础版》做了简化笔记。不想让自己知其然而不知其所然。所以写个博客让自己好好巩固一下,当然不可能把书中的内容全部写下来。在这里就简化一点把命令写下来。 让自己记录一下学习的过程。在这里从第四章开始记录是因为,第四章以前的都还没开始讲命令,说的是关于系统的理论知识和如何安装系统,这里的话我就不打算写了。嗯…就酱纸(安慰一下自己)。 希望自己可以连载下去。加油ヾ(◍°∇°◍)ノ゙。 分享《鸟哥的Linux私房菜-基础篇》第四版 链接:https://pan.baidu.com/s/1iuEtmRxkpnxMxo_RlKmhDg提取码:4d0x 指令下达方式12$ command [-options] parameter1 paremeter2 ... 指令 选项 参数(1) 参数(2) 一行指令中,第一个输入的部分绝对为 command 或可执行文件 如果输入指令显示的内容为乱码 12345671.查看当前语系$ locale ...2.修改语系为英文$ LANG=en_US.utf8$ export LC_ALL=en_US.utf8 重要的热键Tab按键(命令补全 / 文件补齐) 命令补全,在 ca(命令)后连按两下Tab键,会显示ca开头的全部命令: 1234$ ca[Tab][Tab]cacertdir_rehash cache_metadata_size cache_writeback caller case catmancache_check cache_repair cal capsh cat cache_dump cache_restore ca-legacy captoinfo catchsegv 文件补齐,在 command 空格后连按两下Tab后,会补齐文件名(文件名也叫档名)。 12345$ cat [Tab][Tab] 这里的cat后面有个空格.bash_logout .bash_profile .bashrc test1/ test2/ test3/ $ cat testtest1/ test2/ test3/ ctrl + c 按键 中断正在运行的指令,注意:如果运行比较重要的指令,小心使用。 ctrl + d 按键 相当于命令 exit,代表键盘输入结束的意思。 shift + [pageUp] [pageDown] 按键, 可以在命令窗口上下翻页。当然,鼠标的滚轮更方便。Σ (゚Д゚;) 。。。 Linux系统的在线求助指令 –help 求助说明,可看到指令的基本用法和选项参数的介绍,例: 1# date --help 指令 man 为详细的使用说明,man 是指manual(操作说明) 12345# man dateDATE(1) User Commands DATE(1)... DATE(1) 括号里面的内容代表如下: 用户在shell环境中可以操作的指令或可执行文件 系统核心可呼叫的函数与工具等 一些常用的函数(function)与函式库(library),大部分为C的函式库(libc) 装置文件的说明,通常在/dev 下的文件 配置文件或者是某些文件格式 游戏 惯例与协议等,例如Linux文件系统、网络协议、ASCII code 等等的说明 系统管理员可用的管理指令 跟kernel有关的文件 1、5、8 这三个号码比较重要,背一背。 man 命令下面的内容说明 NAME 简短的指令、数据名称的说明 SYNOPSIS 简短的指令下达语法 DESCRIPTION 较为完整的说明,需仔细看看 OPTIONS 针对SYNOPSIS 部分中,有列举的所有可用选项说明 COMMANDS 当这个程序在执行的时候,可以在此程序中下达的指令 FILE 这个程序或数据所使用或参考到的某些文件 SEE ALSO 可以参考的,跟这个指令或数据有关的其他说明 EXAMPLE 一些可以参考的范例 关机 / 重启命令将数据同步写入磁盘的命令:sync (避免已经加载到内存的数据没有写回磁盘,但是却关机了。不过现在的关机重启命令前都会执行sync了) 惯用的关机命令:shutdown (可以设置定时关机,和提醒在线的使用者,更多的用法就找男人 (man)咯。) 重新启动、关机:reboot,halt,poweoff 如果对文章还有什么可以改进的地方,请务必在评论区写下,或至邮件到821312534@qq.com,非常感谢。","link":"/2019/04/29/linux/%E9%B8%9F%E5%93%A5Linux-4/"},{"title":"鸟哥的Linux私房菜笔记第五章-文件权限与目录配置(一)","text":"1. 使用者与群组Linux系统分使用者、群组、其他人的三种身份,权限就是根据这三种身份进行分配的。 2. 文件权限概念2.1 Linux文件属性使用 ls -al 或 ll 命令查看当前目录的文件属性 123$ ls -al...-rwxrw-r--. 1 john man 334 May 2 04:02 file 上面分为7部分,分别为 -rwxrw-r– 、 1 、john 、man 、334 、 May 2 04:02 、 file。 第一部分(文件类型权限) 第一部分第一个字符代表文件的类型 d 目录 | 文件l | 连接档(快捷方式)b | 装置文件里面可供存储的接口设备(可随机存储配置)c | 装置文件里面的串行端口设备,例如:键盘、鼠标(一次性读取装置) 接下来的九个字符,以三个为一组,均为 rwx 的三个参数组合。其中 r 代表可读(read),w 代表可写(write),x 代表可执行(execute)。 如果没有权限,就会显示减号 - 第一组为文件拥有者可具备的权限,第二组为加入此群组的账号具备权限,第三组为其他人权限(费拥有者和群组)。 第二部分 连结档数量 第三部分 此文件的拥有者账号 第四部分 此文件的所属群组 第五部分 表示此文件的容量大小,单位: byte 第六部分 最近修改日期 第七部分 文件名 5.2 改变文件属性和权限修改文件的属性和权限需要使用root用户。root用户命令行用 # 表示,其他用户是 $ 表示。 改变所属群组, chgrp chgrp,就是change group的缩写。用法如下: 12// # chgrp group dirname/filename# chgrp root file 改变 目录/文件 file 的所属群组为 root 改变文件拥有者, chown chown,就是change owner的缩写。用法如下: 1234//# chown user dirname/filename//# chown user:gourp dirname/filename# chown root file //改变file文件的文件拥有者为root# chown root:man file //改变file文件的文件拥有着为root,所属群组为man 改变文件权限,chmod 1.数字类型改变文件权限 刚才2.1第一部分提到文件权限,如:【-rwxrwxrwx】,这九个权限是三个三个一组的。其中我们可以使用数字代表各个权限,权限分数对照表如下: r : 4w : 2x : 1 每种身份(文件拥有者/群组/其他人)的各自权限(r/w/x)分数需要累加的。如:【-rxwr-x—】分数则是:rwx: 4 + 2 + 1 = 7rx-: 4 + 1 = 5—: 0 所以该文件的权限就是750啦。修改的权限语法为: 12// # chmod xyz dirname/filename# chmod 750 file 2.符号类型改变文件权限 我们知道权限分为(1)user,(2)group, (3)other三种身份,对应的符号为 u, g, o,而 a 代表all(全部)的意思。 # chmod u=rwx,g=rw file //修改文件名为file的文件拥有者的权限为rwx,所属群组权限为rw # chmod a=rwx file //修改文件名为file的文件,文件拥有者、群组、其他人的权限为rwx ``` 目录与文件之权限的意义 文件 r(read):可读取此文件的内容。 w(wirte):可编辑、新增或修改该文件内容 x(execute):该文件具有可以被系统执行的权限。 目录 r(read):可读取到该目录下的文件名(如果只有r权限,没有x权限,则只能看到该目录下的文件名,看不到文件权限、大小等信息)。 w(wirte): 1.建立新的文件或目录 2.删除文件和目录 3.对文件或目录重命名 4.搬移该目录内的文件或目录位置 x(execute):可进入该目录。","link":"/2019/05/03/linux/%E9%B8%9F%E5%93%A5Linux-5-1/"},{"title":"鸟哥的Linux私房菜笔记第六章-文件与目录管理(一)","text":"目录与路径相对路径与绝对路径上一章简单的提到绝对路径和相对路径 绝对路径:路径的写法一定是由根目录(/)写起的,例如:/home/user 这个目录相对路径:路径的写法不是由根目录(/)写起,例如:我当前所在目录 /home/user,我要切换到/home/user2 目录下。那么写法就是 cd ../user2,其实相对路径指的意思是:相对于当前目录的路径。 目录的相关操作特殊的目录:12345. //代表当前目录.. //代表上一次目录- //代表前一个工作目录(其实就是上一个操作的目录)~ //代表当前用户的家目录,例如当前用户是user,那么user的家目录就是在/home/user下~username //代表用户名为username的用户的家目录。 常见的处理目录的指令1234cd //切换目录pwd //显示当前目录mkdir //创建目录rmdir //删除空目录,注意是空目录!不空的目录后面再讲 简单指令用法如下 1234cd /home/user //使用绝对路径切换目录pwd //显示当前所在目录,只会显示绝对路径mkdir dir //创建一个名为dir的目录(可以理解为文件夹)rmdir dir //删除一个名为dir的空目录 执行文件路径变量:$PATHls为查阅文件属性的指令,起对应完整文件名为:/bin/ls(这是绝对路径),那么为什么我们输入ls就会执行/bin/ls这个指令呢?这是因为环境变量PATH所致的。相当于我们Window下的环境变量path。 12echo $PATH //打印PATH变量,$号后面接的是变量。/home/vagrant/bin:/home/vagrant/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 可以看到每个目录中间用冒号(:)来隔开,每个目录都会有顺序之分。我们之所以可以执行ls命令,那是因为ls指令存在/bin目录下,我们可以看到$PATH有/bin这个目录,所以ls指令就可以直接执行啦。例举常用添加环境变量的两种方式,需要把/home/vagrant添加到环境变量:1.只对当前用户生效:修改家目录下的 .bashrc文件,然后在文件的最下方加入: 123export PATH=/home/vagrant:$PATH///保存后执行source ~/.bashrc 2.修改/etc/environment文件,直接使用vim命令在后面添加就可以了。 12cat /etc/environment //查看该文件PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" 文件与目录管理文件与目录的管理,其实主要就是显示属性、复制、删除、移动等操作。选项: 文件与目录的查看:lsls可以说是最常用的指令了,而ls指令一些常用的选项如下:-a : 列出全部文件,包括隐藏文件(开头为 . 的文件)-d : 仅列出目录本身,而不列出目录内的文件数据-l : 列出的文件或目录,一个占一行,并且显示文件的属性、权限等等数据。使用方式如下: 1ls -[options] 复制、删除与移动:cp,rm,mvcp(复制文件或目录)cp指令也非常重要,因为我们也经常进行文件的复制,所以也会常常用到这个指令。如果要去复制别人的文件,我们必须有read(读)权限。一般来说复制别人的文件后,该文件的拥有者就会变成自己的啦。常用选项:-i : 若文件已经存在时,询问是否需要覆盖文件。-p : 把文件的属性(权限、用户、时间)一起复制过去。-r :递归复制,例如复制非空的目录,就需要使用这个指令啦。 使用方式如下: 12cp [-options] 源文件 目标文件cp -i /usr/bin /tmp/bin //例子 **rm(移除文件和目录)** 选项: -f : 忽略不存在的文件,不会出现警告信息 -i : 执行删除动作时会询问你是否真的需要删除 -r : 递归删除,常用在目录的删除上,使用这命令要再三确认啊。危险! 使用方式如下: 1rm [-options] 文件或目录 **mv(移动/更名 文件或目录)** 相似于window下的剪切和重命名操作。 选项: -f :如果文件已存在,不会询问而只是直接覆盖。 -i :如果文件已存在,则会询问是否需要覆盖。 -u :如果文件已存在,且复制的文件比已存在的文件新,就会覆盖。 12mv -[options] 源文件 目标文件mv /home/vagrant /tmp/vg //此命令就会把home下vagrant目录移动到tmp目录下,并且改名为vg啦。 扩展:其实更名还有一个命令,叫做rename。需要详细了解的话可以 man rename看一看。","link":"/2019/06/01/linux/%E9%B8%9F%E5%93%A5Linux6-1/"},{"title":"鸟哥的Linux私房菜笔记第五章-文件权限与目录配置(二)","text":"Linux目录配置的依据–FHS因为利用Linux来开发产品的公司太多,例如,CentOS、Ubuntu、ReHat…,导致了配置文件存放的目录没有统一的标准。后来就有了FHS(Filesystem Hierarchy Standard)的标准出来了。 四种交互作用的形态1.可分享的: 可以分享给其他系统挂载使用的目录,所以包括执行文件与用户的邮件等数据,是能够分享给网络上其他主机挂载用的目录。(总结就是对系统运作没影响的文件)2.不可分享的: 自己机器上面运作的装置文件或是与程序有关的socket文件等,由于仅与自身机器有关,所以当然就不适合分享给其他主机了。(总结就是会对系统运作有影响的文件)3.不变的: 有些数据时不会经常变动的,跟随着distribution而不变动的。例如函式库、主机服务配置文件等。4.可变动的: 经常改变的数据,例如登录文件、一般用户可自行操作的文件。 根目录(/)的意义与内容根目录是整个系统最重要的一个目录。因为不但所有目录都是由根目录衍生出来的,并且根目录还有开机/还原/系统修复等动作有关。所以可以看出根目录是有多么的重要。因此FHS标准建议:根目录(/)所在的分区槽应该越小越好(其实就是根目录下的目录越少越好,但是必要的还是不可少),且应用程序锁安装的软件最好不要与根目录放在同一个分区槽内,保持根目录越小越好。如此不但性能较佳,根目录所在的文件系统也较不容易发生问题。 由于上述的说明,FHS定义的根目录(/)底下应该有下面这些目录 目录 放置文件内容 /bin 存放的是root与一般账号锁使用的可执行文件目录,主要有:cat,chmod,chown,date,mv,mkdir,cp,bash等常用指令。 /boot 这个目录主要放置开机会使用到的文件 /dev 任何装置和接口设备都是以文件的形态存在于这个目录当中 /etc 系统主要的配置文件几乎都放置在这个目录内,例如人员的账号密码文件、各种服务的启始文件等 /lib 开机时会用到的函式库 /media 放置移除的装置,如软盘、光盘、DVD等都是挂载于此目录。 /mnt 在早些时候,这个目录用途和/media相同,后来这个目录就用来暂时挂载的。 /opt 第三方软件放置的目录 /run 开机后所产生的各项信息文件 /sbin 开机过程所需要的指令,例如开机、修复、还原系统等指令 /srv 可视为service的缩写,是一些网络服务启动之后,这些服务所需要的数据目录。例如服务www,www服务器所需要的网页子类就可以放置到/srv/www/里面。 /tmp 临时存放目录,重要的文件不要放这里!切记切记。 /usr 下面再详细介绍 /var 下面再详细介绍 /home 系统默认用户的家目录, cd ~ 就会回到你自己家啦。 /root 系统管理员(root)的家目录。 /lost+found 这个目录是使用标准的ext2/ext3/ext4文件系统格式才会产生的一个目录,目的在于当文件系统发生错误时,将一些遗失的片段放置到这个目录下。如果是xfs文件系统就不放存放到这。 /proc 放置的数据都是在内存当中的,例如系统核心、进程信息、装置的状态以及网络状态等等。 /sys 与/proc类似,主要记录核心与系统硬件信息。 /usr的意义与内容/usr里面放置的数据属于可分享与不可变动的内容。很多读者都会误会/usr为/user的缩写(其实我也是),其实是Unix Software Resources的缩写,即Unix操作系统软件资源。 目录 应放置文件内容 /usr/bin/ 所有一般用户能够使用的指令都放在这里,CentOS 7 已将全部用户的指令放到这里,而且使用连接档(暂时理解为快捷方式,后续文章会对其详解)的方式将/bin连结到此。 /usr/lib/ /lib的连结档 /usr/local/ 系统管理员在本机自行安装的软件,建议安装到此目录。 /usr/sbin/ /sbin的连结档 /usr/share/ 只读的数据文件,也包括共享文件,我们的男人 man(联机帮助文件)就是存放于此 /usr/games/ 游戏相关文件 /usr/include/ c/c++等程序语言的文件头(header)与引用文件(include)放置处。 /usr/libexec/ 某些不被一般使用者管用的执行文件或脚本 /usr/src/ 一般的源码建议放到这里,src有source的意思。 /var的意义与内容如果/usr是安装时会占用较大的硬盘的目录,那么/var就是在系统运行后才会渐渐占用硬盘容量的目录,因为/var目录主要针对常态性变动的文件。包括缓存(cache)、登录文件、以及某些软件运行时所产生的文件 目录 应防止文件内容 /var/cache/ 应用程序本身运行过程产生的一些暂存文件(缓存文件)。 /var/lib/ 程序执行过程所需要的数据文件放置的目录。如MYSQL数据库放置到/var/lib/mysql/ /var/lock/ 某些资源一次只能被一个应用所使用到的。就放置在此目录,因为lock上锁嘛~ /var/log/ 灰常重要!登录文件放置的目录!如/var/lib/messages /var/mail 放置个人电子邮箱的目录 /var/run 某些程序或服务启动后,会将他们的PID放置到这个目录下。 /var/spool 这个目录通常存放一些队列数据,如数据队列。 绝对路径与相对路径绝对路径由根目录(/)开始写起的文件名或目录名称,例如:/home/vagrant/ 相对路径相对于目前路径的文件名写法。例如: ../home,比较特殊的连个目录: . 代表当前目录,也可以使用 ./ .. 代表上层目录,也可以使用../ 第六章细讲绝对路径和相对路径","link":"/2019/05/16/linux/%E9%B8%9F%E5%93%A5Linux5-2/"},{"title":"鸟哥的Linux私房菜笔记第六章-文件与目录管理(二)","text":"文件内容查询直接查询文件内容查阅一个文件的内容可以使用指令cat/tac/nl。 1# [cat|tac|nl] 文件 区别:1.cat是直接把文件内容输出到屏幕上,并且从第一行开始输出到末行2.tal和cat相同,只不过tal是从末行反过来开始输出到第一行3.nl则是可以添加行号打印,第一行可以显示1/01/001… 可翻页查询more 空格键 :翻下一页enter : 翻下一行/字符串 :向下查找该字符串:f :显示出文件名和目前显示行数q :离开b : 往回翻页 less 空格键|[pagedown] :翻下一页[pageup]: 翻上一页/字符串 :向下查找该字符串?字符串 :向上查找该字符串q :离开g : 跳转到第一行G : 跳转到末行 文件截取当文件过大,内容过多时,我们可以抽取其中某几行来查看。head(取出前面几行) 1# head [-n number] 文件 -n : 代表的从头开始显示几行。所以number我们写的是数字,如果要显示前面5行,那么就是 1head -n 5 file tail(取出后面几行) 1tail [-n number] 文件 number和上述一样。但是如果只查看第11-20行呢?我们就可以先取前20行,然后再取后面十行。那么就要用到管道啦。复习一下,管道的线是这个符号 “ | “,简称管线。(瞎扯╭(╯^╰)╮) 1head -n 20 文件 | tail -n 20 非纯文本档 od如果我们需要查看非文本的文件,例如如何查看/usr/bin/passwd这个执行文件内容时。由于执行文件通常是binary file(二进制文件),如果我们使用上面的那些命令查看,就会出现乱码的情况。而查看这些文件我们可以使用od这个指令。 12345678od [-t TYPE] 文件TYPE: a:使用默认的字符来输出 c:使用ASCII字符来输出 d[size]: (decimal)使用十进制来输出,每个整数占用 size bytes; f[size]: (floating)使用浮点数来输出,每个整数占用 size bytes; o[size]: (octal)使用八进制来输出,每个整数占用 size bytes; x[size]: (hexadecimal)使用十六进制来输出,每个整数占用 size bytes; 文件默认权限和隐藏权限文件预设权限: umask当我们建立文件或者目录时,那么他的权限是多少呢?这个就要使用umask这个指令了,使用方式如下: 12#umask //查看当前默认的权限值0002 文件隐藏属性chattr(配置文件隐藏属性)除了rwx的那9个权限外,还有隐藏属性(真是难受),该指令只在Ext2/Ext3/Ext4 的Linux文件系统生效,其他的文件系统可能无法完全支持该指令(现在都9102年了,不知道完全支持不呢…) 1234567891011121314# chattr [+-=] [options] 文件或目录选项:+ : 增加某一个参数- : 移除某一个参数= : 重新设置参数。 参数(options)a : 该文件只能增加数据,不能删除数据,也不能修改数据,只有root用户能设定该参数。i : 该文件不能删除、改名、设定连结,还无法写入或新增数据!只有root用户能设定该参数。... eg: # sudo chattr +a file1 //把file1文件设置隐藏属性i# rm -f testrm: cannot remove 'test': Operation not permitted //提示不能执行该操作哦,因为设置了隐藏属性i,是不能删除该文件的。 **lsattr(显示文件隐藏属性)** 使用chattr设置了隐藏属性后,就可以使用lsattr查看啦 1234# lsattr [-adR] 文件或目录 a : 查看隐藏文件属性 d : 如果是目录,仅列出目录本身的属性、而非目录内的文件属性 R : 连同子目录也一并列出 文件特殊权限: SUID,SGID,SBIT其实除了rwx的文件权限外,还有特殊权限s和t,s和t的作用分别放到第十三章和第十六章说明(按照我这个写博客的进度不知道要等到猴年马月,有想了解的童靴就去搜搜)。 Set UID当s这个标志出现在文件拥有者的x权限上时,例如 [-rwsr-xr-x],此时就被称为Set UID,简称SUID特殊权限,SUID的特殊功能如下: SUID权限仅对二进制程序有效执行者对改程序需要具有x的执行权限本权限仅在执行该程序的过程中有效执行者将具有该程序拥有者的权限 Set GID当s标志在所属组的x标志上,例如 [-rwx–s–x],此时成为Set GID,简称SGID。对于文件有以下功能: SGID对二进制程序有用执行者需要具备相应的x权限执行该程序的用户则会获得该程序群组的支持 对于目录有以下功能 用户若对目录有r和x权限,则能进入该目录用户在此目录下的有效群组将会变成该目录的群组(有效群组是用户创建文件的时候,文件默认所属的群组)用途:若用户具有w权限,则用户所建立的新文件的群组则与该目录的群组相同。 Sticky Bit简称SBIT,只对目录有效: 用户对于此目录具有w,x权限用户在此目录下建立文件,只有自己和root用户有权限删除文件。 SUID/SGID/SBIT 权限设定 4 为 SUID2 为 SGID1 为 SBIT 第五章讲过了修改权限的命令,至于添加特殊权限则如下操作: 1chmod 4755 filename //这里的4为SUID的特殊权限 后面的755就和以前的一样 观察文件类型:file如果需要知道某个文件是属于什么类型,例如是属于ASCII、binary还是其它的文件。就是用这个类型 12$ file test_file test_file: ASCII text //返回ASCII 的纯文本文件 指令与文件的搜索指令查询 which123456$ which [-a] command-a : 将所有PATH目录中可以知道的指令列出例如:$ which service/usr/sbin/service 文件名查询 find123456789格式: find [PATH] [option]$ find /home mtime 0 //查看home目录下24小时之内被修改的文件$ find /home -user vagrant //查询home目录下用户为vagrant的目录或文件/home/vagrant/home/vagrant/.cache...$ find /home -name test_file //查询home目录下名为test_file的文件/home/vagrant/test_file 结语 看了鸟哥的Linux的第六章后,我差不多把文章讲到的命令有写上了,不过比较详细的东西可能并没有在文章这里写到,而且我个人觉得写这篇文章消耗我比较长的时间,最大的原因是不管什么命令都写上去,反正书里说到的就写。后面越写我就越发现,文章不能照搬书里的内容,应该把比较常用的内容写到上面才比较合适。不然的话还不如看我文章的小伙伴看书去比较好,所以我往后的文章可能会尽可能的写现实当中比较常用的东西(我自己觉得常用的东西,可能不是真的常用)。好了,文章毕竟写得多才会有积累,写得会更快,但是更快的情况下我会尽可能的保证内容的正确性。文章若有不足之处,请在评论区留言指出或邮件发送到15915126689@163.com,谢谢大家。","link":"/2019/07/21/linux/%E9%B8%9F%E5%93%A5Linux6-2/"},{"title":"Nginx入门学习","text":"nginx系列 简介nginx 是HTTP和反向代理服务器,邮件代理服务器和通用TCP/UDP代理服务器。总之一句话,nginx很火很牛逼就对了。 download下载地址:http://nginx.org/en/download.html;mainline:最新版本stable:稳定版本下载命令: 12wget [下载地址]tar -zxvf [压缩包] 目录介绍 auto:辅助configure文件的执行CHANGES:不同版本的特性conf:示例文件configure:用于生成中间文件,执行编译前的必须动作contrib:vim nginx文件时显示的样式,用法 cp contrib/vim/* ~/.vim/html::nginx的默认html文件man:nginx的帮助文件src:nginx源代码 安装.configure --xxx –prefix=PATH nginx安装目录的前缀–with-xxx 默认不会编译进nginx,需要则写到命令行–without-xxx 默认编译进nginx,不需要则写到命令行 最普通的configure编译命令为.configure --prefix=/usr/local/nginx执行完命令则生成objs目录,存放中间文件,objs/ngx_modules.c 文件决定哪些模块安装到nginx。 make编译:make,执行完后生成大量的中间文件,都会存放到objs/src目录中。安装:make install Nginx命令行 -c 指定配置文件-g 指定配置的指令-p 指定运行目录-s stop 立刻停止服务; quit 优雅的停止服务;reload 重载配置文件;reopen 重新开始记录日志文件-t 测试配置文件语法是否有错误-v 版本信息 重载配置文件修改nginx配置文件的内容后,需要nginx重新加载配置文件 1nginx -s reload 热部署nginx正在运行的时候,此时需要升级nginx版本。只需要更新nginx二进制文件。先备份旧的nginx: 1cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old 使用最新的nginx二进制文件 替换掉正在使用的nginx二进制文件。 1cp -r nginx /usr/local/nginx/sbin/ -f 查看正在运行的nginx的master进程 1ps -ef | grep nginx 告诉正在运行nginx的master,需要进行nginx升级 1kill -USR2 [正在运行的nginx master进程ID] 执行完命令后会启动新的nginx进程,然后告知旧的nginx master进程,请优雅的关闭所有旧的worker进程 1kill -WINCH [旧的nginx master进程ID] 然后会发现旧的nginx worker进程已经全部关闭,发现master进程还在。如果新的nginx版本发生的错误,可以回退到旧的nginx master进程中,执行nginx -s reload会回退到旧版本 日志切割12345# 先把日志文件先备份mv access.log access_bak.log# 进行日志切割,执行完命令会重新生成access.log 文件nginx -s reopen 一般情况下会后台做一个bash脚本,定时进行日志切割。 静态资源服务器nginx 配置 123456789101112131415161718192021222324# 日志格式,main为日志格式命名log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; gzip on; # 对文件进行压缩传送gzip_min_length 1; # 小于 1 字节则不进行压缩。gzip_comp_level 2; # 压缩级别gzip_types image/jpeg image/gif; # 对这些文件才进行压缩server { listen 80; # 监听端口 server_name localhost; # 域名 # 记录access_log日志(每一个请求都会记录), 使用main的log_format进行记录 access_log /logs/blog.log main; # url匹配的路径 location / { alias code/; # 指 nginx的安装目录下 eg:/usr/local/nginx/code # autoindex on; # 共享静态资源 # set $limit_rate 10k; # nginx每秒传输 10k字节 到浏览器当中 }} 具备缓存功能的反向代理服务可以建立多个上游服务,当有请求进来的时候,nginx可以根据负载均衡算法代理给多台上游服务器工作。nginx配置 1234567891011121314151617181920212223242526272829303132333435363738# 上游服务# local 为上游服务器名upstream local { # 其中一台上游服务器,可以配置多台 # 127.0.0.1:8080 代表只有本机能访问8080端口 server 127.0.0.1:8080; }# 反向代理缓存 缓存路径 内存关键字,10mproxy_cache_path /tmp/nginxcache levels=1:2 keys_zone=my_cache:10m max_size=10g;inactive=60m use_temp_path=off;server { listen 80; server_name colablog.cn; # 域名 # 记录access_log日志(每一个请求都会记录), 使用main的log_format进行记录 access_log /logs/blog.log main; # url匹配的路径 location / { # doc http://nginx.org/en/docs/http/ngx_http_proxy_module.html # proxy_set_header 反向代理服务器把客户端请求的信息,设置到请求头中发送到上游服务 proxy_set_header Host $host; # 域名 proxy_set_header X-Real-IP $remote_addr; # 客户端地址 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 使用哪个缓存,对应上面的keys_zone proxy_cache my_cache; # 缓存的路径 proxy_cache_key $hots$uri$is_args$args; # 对于这些响应不缓存 proxy_cache_valid 200 304 302 1d; # 代理到上游服务 proxy_pass http://local; }} GoAccess可视化实时监控access日志安装你可以快速使用 apt install或者 yum install,也可以在官网中查看编译安装的方式。运行goaccess命令 1# goaccess /usr/local/nginx/logs/access.log -o /usr/local/nginx/html/report.html --real-time-html --time=format='%H:%M:%S' --date-format='%d/%b/%Y' --log-format=COMBINED –real-time-html 代表实时更新页面 nginx.conf配置文件 12345678910111213141516# 日志格式log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"';server { listen 80; server_name: localhost; # 日志记录 access_log logs/access.log main; # 指定页面 location /report.html { alias /usr/local/nginx/html/report.html; }} 然后访问 http://localhost/report.html,就可以看到下面这么高大上的界面了。 SSL如果你有域名的话,只需要两行命令可以快速把你的 http://域名 变成 https://域名。ubuntu版本下 1apt install python-certbot-nginx centos版本下 1yum install python2-certbot-nginx 使用certbot命令帮我们下载证书和自动配置好nginx.conf, 1certbot --nginx --nginx-server-root=/usr/local/nginx/conf/ -d [你的域名] 执行了上面的命令后会有两个选项,第一个选项是可以访问http或者https,不会进行重定向;而第二个选项则是访问http的时候重定向到https中。就这样就搞定了,是不是很简单。 总结Nginx初次入门的小白,文章若有错误的地方,请用力的指出。 参考文章:极客时间:Nginx核心知识100讲 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/06/17/nginx/Nginx-1/"},{"title":"nginx使用热部署添加新模块","text":"nginx系列 简介当初次编译安装nginx时,http_ssl_module 模块默认是不编译进nginx的二进制文件当中,如果需要添加 ssl 证书。也就是使用 https协议。 那么则需要添加 http_ssl_module 模块。假设你的nginx安装包目录在/home/johnson/nginx-1.17.5,下面会用到 小知识点:使用/home/johnson/nginx-1.17.5/configure --help 命令,可以看到很多 --with 和 --without 开头的模块选项。 –with:默认是不编译进nginx的二进制文件当中 –without:默认编译进nginx的二进制文件当中 123456/home/johnson/nginx-1.17.5/configure --help... --with-http_ssl_module enable ngx_http_ssl_module... --without-http_gzip_module disable ngx_http_gzip_module... 可以看到http_ssl_module 模块默认是不编译进nginx的二进制文件当中。 编译添加新模块当需要添加http_ssl_module模块时,命令如下: 1/home/johnson/nginx-1.17.5/configure --with-http_ssl_module 执行完该命令后,可以在/home/johnson/nginx-1.17.5/objs/ngx_modules.c文件中看到哪些模块要安装到nginx中。如下: 123456ngx_module_t *ngx_modules[] = { &ngx_core_module,... &ngx_http_ssl_module,... 可以看到http_ssl_module模块要安装到nginx当中,然后使用make命令,把http_ssl_module编译进nginx的二进制文件当中 12cd /home/johnson/nginx-1.17.5make 执行完上述命令后,/home/johnson/nginx-1.17.5/objs/nginx该文件就是编译后的nginx二进制文件,然后咱们就需要进行热部署升级了。 热部署假设你的nginx安装目录在/usr/local/nginx当中。1.备份正在使用的nginx二进制文件 1cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old 2.使用最新的nginx二进制文件替换掉正在使用的nginx二进制文件 1cp -r /home/johnson/nginx-1.17.5/objs/nginx /usr/local/nginx/sbin/ -f 3.查看正在运行nginx的master进程 1234ps -ef | grep nginxroot 6503 1 0 Jun23 ? 00:00:00 nginx: master process nginxubuntu 26317 19063 0 07:39 pts/0 00:00:00 grep --color=auto nginxnobody 31869 6503 0 Jun27 ? 00:00:00 nginx: worker process 可以看到,当前nginx的master进程号为 6503。 4.告知正在运行的nginx的master进程,需要进行nginx升级 12345678kill -USR2 6503ps -ef | grep nginxroot 6503 1 0 Jun23 ? 00:00:00 nginx: master process nginxroot 7128 6503 0 08:05 ? 00:00:00 nginx: master process nginxnobody 7129 7128 0 08:05 ? 00:00:00 nginx: worker processroot 7140 30619 0 08:05 pts/0 00:00:00 grep --color=auto nginxnobody 31869 6503 0 Jun27 ? 00:00:00 nginx: worker process 可以看到,执行完命令后会启动新的nginx的master进程,新的master进程是由旧的master进程启动的。如果没有启动,那么可以使用nginx -t查看配置文件是否正确,如果没有问题,那么一般是能够启动新的master进程。 5.告知旧的nginx master进程,请优雅的关闭所有旧的worker进程 123456kill -WINCH 6503root@VM-0-13-ubuntu:/usr/local/nginx# ps -ef | grep nginxroot 6503 1 0 Jun23 ? 00:00:00 nginx: master process nginxroot 7128 6503 0 08:05 ? 00:00:00 nginx: master process nginxnobody 7129 7128 0 08:05 ? 00:00:00 nginx: worker processroot 9431 30619 0 08:17 pts/0 00:00:00 grep --color=auto nginx 可以看到,旧的worker进程都已经关闭掉。如果发生了错误,则可以使用nginx -s reload命令回退到旧版本当中。如果发现一切都正常,没有问题,那么你可以关闭掉旧的master进程。kill -9 6503,此时新的master进程的父进程(旧的master进程)被关闭后,那么会把他的父进程改成系统进程,系统进程的进程号为 1。此时就完美添加了新模块和实现热部署了!!! 总结因为初次编译nginx,可能没想到要用到其他模块,或许也可能删除某些模块。此时往往就需要使用到nginx的热部署。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/06/30/nginx/Nginx-2/"},{"title":"nginx如何限制并发连接请求数?","text":"nginx系列 简介限制并发连接数的模块为:http_limit_conn_module,地址:http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html 限制并发请求数的模块为:http_limit_req_module,地址:http://nginx.org/en/docs/http/ngx_http_limit_req_module.html 这两个模块都是默认编译进Nginx中的。 限制并发连接数示例配置: 12345678910http { limit_conn_zone $binary_remote_addr zone=addr:10m; #limit_conn_zone $server_name zone=perserver:10m; server { limit_conn addr 1; limit_conn_log_level warn; limit_conn_status 503; }} limit_conn_zone key zone=name:size; 定义并发连接的配置 可定义的模块为http模块。 key关键字是根据什么变量来限制连接数,示例中有binary_remote_addr、$server_name,根据实际业务需求。 zone定义配置名称和最大共享内存,若占用的内存超过最大共享内存,则服务器返回错误 示例中的$binary_remote_addr是二进制的用户地址,用二进制来节省字节数,减少占用共享内存的大小。 limit_conn zone number; 并发连接限制 可定义模块为http、server、location模块 zone为指定使用哪个limit_conn_zone配置 number为限制连接数,示例配置中限制为 1 个连接。 limit_conn_log_level info | notice | warn | error ; 限制发生时的日志级别 可定义模块为http、server、location模块 limit_conn_status code; 限制发生时的返回错误码,默认503 可定义模块为http、server、location模块 限制并发请求数limit_req_zone key zone=name:size rate=rate; 定义限制并发请求的配置。 若占用的内存超过最大共享内存,则服务器返回错误响应 rate定义的是请求速率,如10r/s 每秒传递10个请求,10r/m 每分钟传递10个请求 limit_req zone=name [burst=number] [nodelay | delay=number]; zone 定义使用哪个 limit_req_zone配置 burst=number 设置桶可存放的请求数,就是请求的缓冲区大小 nodelay burst桶的请求不再缓冲,直接传递,rate请求速率失效。 delay=number 第一次接收请求时,可提前传递number个请求。 可定义模块为http、server、location模块 limit_req_log_level info | notice | warn | error; 限制发生时的日志级别 可定义模块为http、server、location模块 limit_req_status code;限制发生时的错误码 可定义模块为http、server、location模块 示例配置1 1234http { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; limit_req zone=one burst=5;} 请求速率为每秒传递1个请求。burst桶大小可存放5个请求。超出限制的请求会返回错误。 示例配置2 1234http { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; limit_req zone=one burst=5 nodelay;} 示例配置2是在示例配置1当中添加了nodelay选项。那么rate请求速率则不管用了。会直接传递burst桶中的所有请求。超出限制的请求会返回错误。 示例配置3 1234http { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; limit_req zone=one burst=5 delay=3;} 示例配置3是在示例配置1当中添加了delay=3选项。表示前3个请求会立即传递,然后其他请求会按请求速率传递。超出限制的请求会返回错误。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/09/04/nginx/Nginx-3/"},{"title":"使用阿里云OSS的服务端签名后直传功能","text":"网站一般都会有上传功能,而对象存储服务oss是一个很好的选择。可以快速的搭建起自己的上传文件功能。该文章以使用阿里云的OSS功能为例,记录如何在客户端使用阿里云的对象存储服务。 服务端签名后直传背景采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKey ID和AcessKey Secret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。 流程介绍流程如下图所示:本示例中,Web端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠。但本示例中的服务端无法实时了解用户上传了多少文件,上传了什么文件。如果想实时了解用户上传了什么文件,可以采用服务端签名直传并设置上传回调。 创建对象存储1. 创建bucket 快捷入口:https://oss.console.aliyun.com/bucket bucket读写权限为:公共读 2. 添加子用户分配权限鼠标移至右上角的用户头像当中,点击 添加AccessKey管理, 然后选择使用子用户AccessKey,因为使用子用户可以只分配OSS的读写权限。这样比较安全。访问方式选择:编程访问(即使用AccessKey ID 和 AccessKey Secret, 通过API或开发工具访问) 然后点击子用户的添加权限操作。权限选择:AliyunOSSFullAccess(管理对象存储服务(OSS)权限) 3.保存AccessKey信息创建了用户后,会展示这么一个页面此时你需要保存好AccessKeyID和AccessSecret,否则这个页面关闭后就找不到了。 maven依赖12345<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version></dependency> 最新版本可以看这里:https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.807.39fb4c07GmTHoV 测试上传测试代码 1234567891011121314151617// Endpoint以杭州为例,其它Region请按实际情况填写。String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";// 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。String accessKeyId = "<yourAccessKeyId>";String accessKeySecret = "<yourAccessKeySecret>";// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// 创建PutObjectRequest对象。PutObjectRequest putObjectRequest = new PutObjectRequest("<yourBucketName>", "test", new File("C:\\Users\\82131\\Desktop\\logo.jpg"));// 上传文件。ossClient.putObject(putObjectRequest);// 关闭OSSClient。ossClient.shutdown(); 测试成功后就可以看到test图片了,如图: 服务端签名实现流程修改CORS客户端进行表单直传到OSS时,会从浏览器向OSS发送带有Origin的请求消息。OSS对带有Origin头的请求消息会进行跨域规则(CORS)的验证。因此需要为Bucket设置跨域规则以支持Post方法。进入bucket后,选择权限管理 -》跨域设置 -》创建规则 后端代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647@RestControllerpublic class OssController { @RequestMapping("/oss/policy") public Map<String, String> policy() { String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。 String accessKey = "<yourAccessKeyId>"; // 请填写您的AccessKeySecret。 String endpoint = "oss-cn-shenzhen.aliyuncs.com"; // 请填写您的 endpoint。 String bucket = "bucket-name"; // 请填写您的 bucketname 。 String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String dir = format + "/"; // 用户上传文件时指定的前缀。 Map<String, String> respMap = new LinkedHashMap<String, String>(); // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey); try { long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。 PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); respMap.put("accessid", accessId); respMap.put("policy", encodedPolicy); respMap.put("signature", postSignature); respMap.put("dir", dir); respMap.put("host", host); respMap.put("expire", String.valueOf(expireEndTime / 1000)); } catch (Exception e) { // Assert.fail(e.getMessage()); System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return respMap; }} 更详细的详细请查看这里:https://help.aliyun.com/document_detail/91868.html?spm=a2c4g.11186623.2.15.a66e6e28WZXmSg#concept-ahk-rfz-2fb 前端代码以element-ui组件为例,上传前BeforeUpload先调用后端的policy接口获取签名信息,然后带着签名等信息和图片直接上传到aliyun的OSS。上传组件singleUpload.vue,需要改动action的地址:bucket的外网域名(在bucket的概览里面可以看到),该文件是谷粒商城项目的一个上传组件,我只是copy过来修改了一点点。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111<template> <div> <el-upload action="http://colablog.oss-cn-shenzhen.aliyuncs.com" :data="dataObj" list-type="picture" :multiple="false" :show-file-list="showFileList" :file-list="fileList" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview" > <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div> </el-upload> <el-dialog :visible.sync="dialogVisible"> <img width="100%" :src="fileList[0].url" alt=""> </el-dialog> </div></template><script>export default { name: 'SingleUpload', props: { value: String }, data() { return { dataObj: { policy: '', signature: '', key: '', ossaccessKeyId: '', dir: '', host: '' // callback:'', }, dialogVisible: false } }, computed: { imageUrl() { return this.value }, imageName() { if (this.value != null && this.value !== '') { return this.value.substr(this.value.lastIndexOf('/') + 1) } else { return null } }, fileList() { return [{ name: this.imageName, url: this.imageUrl }] }, showFileList: { get: function() { return this.value !== null && this.value !== '' && this.value !== undefined }, set: function(newValue) { } } }, methods: { emitInput(val) { this.$emit('input', val) }, handleRemove(file, fileList) { this.emitInput('') }, handlePreview(file) { this.dialogVisible = true }, getUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16) }) }, beforeUpload(file) { const _self = this return new Promise((resolve, reject) => { // 前后端提交post异步请求获取签名信息 this.postRequest('/oss/policy') .then((response) => { _self.dataObj.policy = response.policy _self.dataObj.signature = response.signature _self.dataObj.ossaccessKeyId = response.accessid _self.dataObj.key = response.dir + this.getUUID() + '_${filename}' _self.dataObj.dir = response.dir _self.dataObj.host = response.host resolve(true) }).catch(err => { reject(false) }) }) }, handleUploadSuccess(res, file) { console.log('上传成功...') this.showFileList = true this.fileList.pop() this.fileList.push({ name: file.name, url: this.dataObj.host + '/' + this.dataObj.key.replace('${filename}', file.name) }) this.emitInput(this.fileList[0].url) } }}</script> 引用SingleUpload组件的页面的示例代码: 123456789101112131415161718192021222324<template> <div> <el-form :model="admin" label-width="60px"> <el-form-item label="头像"> <single-upload v-model="admin.userImg" /> </el-form-item> </el-form> </div></template><script>import singleUpload from '@/components/upload/singleUpload'export default { components: { singleUpload }, data() { return { admin: { userImg: '' } } }}</script> 总结该文主要由学习谷粒商城项目的实践过程,技术难度并不大,阿里云官网有文档可以查阅。快捷入口:[https://help.aliyun.com/product/31815.html?spm=a2c4g.11186623.6.540.28c66d39lLTogx]https://help.aliyun.com/product/31815.html?spm=a2c4g.11186623.6.540.28c66d39lLTogx 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2020/09/13/aliyun-oss/"},{"title":"如何在Nginx不绑定域名下使用SSL/TLS证书?","text":"前提该文主要记录如何在没有购买域名的情况下使用SSL/TLS协议,即地址前面的http变成了https。但是这样的SSL协议是会被浏览器认为是不安全的。在开发或者测试环境可以这样搞,生产环境下还是乖乖的买个域名吧。 SSL证书第一步首先到https://csr.chinassl.net/generator-csr.html这里生成SSL秘钥(私钥)和等会拿去生成SSL证书的CSR文件。里面内容可以随便填,域名啥的随便填都没关系。保存好这两个文件。 第二步拿刚才的CSR文件到https://csr.chinassl.net/free-ssl.html这里生成SSL证书。 到这里为止,我们只需要记住秘钥和SSL证书的存储路径,在nginx配置文件当中需要使用到。假设存到这里吧。 12/etc/ssl/my_domain/my_domain.ssl/etc/ssl/my_domain/my_domain.private 我这里只是改了文件的后缀而已,并不影响使用。文件的后缀名你们自行决定也可以。 Nginx添加SSL模块先查看Nginx以前安装过的模块,避免编译后覆盖了之前添加的模块。进入到你的nginx安装包目录。执行以下命令 123456# ./objs/nginx -Vnginx version: nginx/1.16.1built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) built with OpenSSL 1.0.2k-fips 26 Jan 2017TLS SNI support enabledconfigure arguments: --prefix=/usr/local/nginx --with-http_realip_module 主要看configure arguments这一行,那么我之前的预编译命令就是如下,而如果没有自定义添加过任何模块那么这里应该为空的 1./configure --prefix=/usr/local/nginx --with-http_realip_module 现在需要添加SSL模块,那么命令如下: 12./configure --prefix=/usr/local/nginx --with-http_realip_module \\ --with-http_ssl_module 然后执行make命令,已经安装过安装过nginx的(即执行过make install),就不要执行 make install,不然把你之前安装好的nginx文件覆盖掉。当然,未安装Nginx的就可以执行make install命令了。 更新Nginx启动文件方式一:停止Nginx服务更新1234cd /usr/local/nginx/sbin/./nginx -s stopmv ./nginx ./nginx.oldcp nginx安装包目录/objs/nginx ./nginx 方式二:热部署更新可以参考我公众号的文章:https://mp.weixin.qq.com/s/o7rkczakPNiys1KM7Z87EA 配置文件1vim /usr/local/nginx/conf/nginx.conf 配置文件我只摘取了server模块,如下: 12345678910111213141516171819202122232425262728 server { listen 80; server_name 127.0.0.1; location / { # 重定向到httpsrewrite ^/(.*) https://$host$1 permanent; } } server { listen 443 ssl; server_name 127.0.0.1; ssl_certificate /etc/ssl/my_domain/my_domain.ssl; # ssl证书存储路径 ssl_certificate_key /etc/ssl/my_domain/my_domain.private; # 秘钥存储路径 # ssl的一些配置 ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #开启TLS协议 location / { root html; index index.html index.htm; } } 此时输入ip地址,你就能看到https了。 扩展知识多个SSL模块当nginx的多个模块都需要使用SSL协议时,如PC端的前端项目使用了80端口转发,手机端使用了81端口转发。那么可以改成如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 server { # PC端 listen 80; server_name 127.0.0.1; location / { # 重定向到https,https默认端口是443rewrite ^/(.*) https://$host$1 permanent; } } server { # 手机端 listen 81; server_name 127.0.0.1; location / { # 重定向到https,指定跳转到8443端口rewrite ^/(.*) https://$host:8443$1 permanent; } } server { listen 443 ssl; server_name 127.0.0.1; ssl_certificate /etc/ssl/my_domain/my_domain.ssl; # ssl证书存储路径 ssl_certificate_key /etc/ssl/my_domain/my_domain.private; # 秘钥存储路径 ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #开启TLS协议 location / { root html; index index.html index.htm; } } server { listen 8443 ssl; server_name 127.0.0.1; ssl_certificate /etc/ssl/my_domain/my_domain.ssl; # ssl证书存储路径 ssl_certificate_key /etc/ssl/my_domain/my_domain.private; # 秘钥存储路径 ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #开启TLS协议 location / { root html; index index.html index.htm; } } 443端口转发https的默认端口是443,而没有root权限的用户启动时,nginx会提示没有权限使用443端口,此时则需要使用端口转发规则,把443转发到其它端口,如8443。那么需要root用户执行以下命令 123iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443iptables -t nat -nL --lineservice iptables save 然后把nginx配置文件的监听端口改成8443 ssl 12345678910111213 server { listen 80; server_name 127.0.0.1; location / { # 重定向到https,https默认端口是443,因为端口转发规则,转发到8443 rewrite ^/(.*) https://$host$1 permanent; } } server { listen 8443 ssl; server_name 127.0.0.1; ...} 总结OK,这就是最近工作上需要完成的一个功能,还是自己太菜了。总结以下,希望也能帮到别人。~Thanks♪(・ω・)ノ 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2021/01/02/%E5%A6%82%E4%BD%95%E5%9C%A8Nginx%E4%B8%8D%E7%BB%91%E5%AE%9A%E5%9F%9F%E5%90%8D%E4%B8%8B%E4%BD%BF%E7%94%A8SSL-TLS%E8%AF%81%E4%B9%A6/"},{"title":"SpringBoot使用策略模式+工厂模式","text":"为了防止大量的if...else...或switch case代码的出现,可以使用策略模式+工厂模式进行优化。在我的项目当中,报表繁多,所以尝试了这种方式进行优化报表的架构。 代码很简单,如下: Factory工厂类12345678910111213141516171819@Servicepublic class ReportFactory { /** * 初始化的时候将所有的ReportService自动加载到Map中 */ @Autowired private final Map<String, ReportService> reportIns = new ConcurrentHashMap<>(); public ReportService getReportIns(String code) { ReportService reportInstance = reportIns.get(code); if (reportInstance == null) { throw new RuntimeException("未定义reportInstance"); } return reportInstance; }} 接口123public interface ReportService { String getResult();} 实现类12345678@Component(value = "A1")public class ReportServiceA1 implements ReportService { @Override public String getResult() { return "我是A1"; }} 12345678@Component(value = "A2")public class ReportServiceA2 implements ReportService { @Override public String getResult() { return "我是A2"; }} 测试12345678910111213141516@SpringBootTestpublic class BlogServerApplicationTest { @Autowired ReportFactory reportFactory; @Test public void test2() { String result1 = reportFactory.getReportIns("A1").getResult(); System.out.println("-----------------"); System.out.println(result1); String result2 = reportFactory.getReportIns("A2").getResult(); System.out.println("-----------------"); System.out.println(result2); }} 打印如下: 1234-----------------我是A1-----------------我是A2 总结在平时的工作当中,写一些业务代码是无可避免的,但是只要不局限于现状,往往可以发现不一样的乐趣。就像我在报表的业务中学习到了策略模式+工厂模式。 个人博客网址: https://colablog.cn/ 如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您","link":"/2021/01/02/SpringBoot%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F-%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"PHP","slug":"PHP","link":"/tags/PHP/"},{"name":"算法","slug":"算法","link":"/tags/%E7%AE%97%E6%B3%95/"},{"name":"InfluxDB","slug":"InfluxDB","link":"/tags/InfluxDB/"},{"name":"mysql","slug":"mysql","link":"/tags/mysql/"},{"name":"Git","slug":"Git","link":"/tags/Git/"},{"name":"markdown","slug":"markdown","link":"/tags/markdown/"},{"name":"Java","slug":"Java","link":"/tags/Java/"},{"name":"源码阅读","slug":"源码阅读","link":"/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"},{"name":"Java并发","slug":"Java并发","link":"/tags/Java%E5%B9%B6%E5%8F%91/"},{"name":"工具类","slug":"工具类","link":"/tags/%E5%B7%A5%E5%85%B7%E7%B1%BB/"},{"name":"nginx","slug":"nginx","link":"/tags/nginx/"},{"name":"ufw","slug":"ufw","link":"/tags/ufw/"},{"name":"防火墙","slug":"防火墙","link":"/tags/%E9%98%B2%E7%81%AB%E5%A2%99/"},{"name":"docker","slug":"docker","link":"/tags/docker/"},{"name":"鸟哥的Linux私房菜","slug":"鸟哥的Linux私房菜","link":"/tags/%E9%B8%9F%E5%93%A5%E7%9A%84Linux%E7%A7%81%E6%88%BF%E8%8F%9C/"},{"name":"Nginx","slug":"Nginx","link":"/tags/Nginx/"}],"categories":[{"name":"编程语言","slug":"编程语言","link":"/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"},{"name":"数据结构与算法","slug":"数据结构与算法","link":"/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"},{"name":"数据库","slug":"数据库","link":"/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"运维部署","slug":"运维部署","link":"/categories/%E8%BF%90%E7%BB%B4%E9%83%A8%E7%BD%B2/"},{"name":"娱乐瞎搞","slug":"娱乐瞎搞","link":"/categories/%E5%A8%B1%E4%B9%90%E7%9E%8E%E6%90%9E/"},{"name":"Linux","slug":"Linux","link":"/categories/Linux/"},{"name":"中间件","slug":"中间件","link":"/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"},{"name":"中间件","slug":"运维部署/中间件","link":"/categories/%E8%BF%90%E7%BB%B4%E9%83%A8%E7%BD%B2/%E4%B8%AD%E9%97%B4%E4%BB%B6/"}]}
1
https://gitee.com/johsnonColablog/hexo.git
git@gitee.com:johsnonColablog/hexo.git
johsnonColablog
hexo
hexo
master

搜索帮助