小不的笔记
时间之外的往事
Java多线程并发操作ArrayList
给公司一个业务系统做性能优化时,有个地方需要在循环内实现对外交互。有网络IO的地方很容易出现性能瓶颈,就打算通过parallelStream实现并发操作,
1 | List resultList = new ArrayList(); |
如果直接把forEach
改为parallelStream().forEach
,就会引发新的问题,因为业务代码里使用了 arraylist.add 方法收集计算结果,ArrayList 是非线程安全的使用一个线程安全的容器。 Java里线程安全的集合容器,可以通过如下方法: Vector
* 古老且线程安全的List, 每次扩容一倍空间,而ArrayList扩容50%。 * 在这个场景下因为集合需要返回上层做额外操作,如果使用Vector
会有不必要的锁开销,当然这点儿性能影响可以忽略不计,如果不想有额外的锁开销就需要在返回时多了一层转换,把Vector转化为ArrayList。 CopyOnWriteArrayList
* 每次添加新元素时创建一个新的List,适合读多写少的场景。该场景基本没并发读的场景,完全没必要使用。 Collections.synchronizedList
* 返回一个包装类SynchronizedList
,对被包装的真实List的所有场景加锁。 其他 * 因为我这里这个场景比较简单也可以使用ConcurrentHashMap
等集合容器来实现线程安全。 我现在的这个场景挺适合Collections.synchronizedList
1 | List resultList = new ArrayList(); |
全部改完之后,又想到我只用到了 arraylist.add 其实只需要同步这一方法就行了。
1 | List resultList = new ArrayList(); |
这样没有一句废话的实现了多线程环境下,给ArrayList下添加元素。如果还在学生时代,估计我会直接写出最后这种代码,但是随着工作时间久了,习惯于使用各种工具包来实现各种功能,渐渐的忘记最初的样子, 习惯于把简单的问题复杂化。 Keep it simple and stupid.
Ubuntu切换国内镜像(通用版)
镜像模式
使用镜像模式,apt 命令会自动根据选择服务器所在的国家的镜像。如下脚本默认源使用镜像协议并备份原始文件sources.list到
sources.list.backup`
1 | sudo sed -E -i.backup 's#^(debdeb-src) ([^ ]*) (.*)#\1 mirror://mirrors.ubuntu.com/mirrors.txt \3#' /etc/apt/sources.list |
手动模式
我的服务器阿里云机房,所以希望固定使用阿里云镜像。如下脚本修改默认源为阿里云并备份原始文件sources.list
到sources.list.backup
1 | sudo sed -E -i.backup 's#^(debdeb-src) ([^ ]*) (.*)#\1 https://mirrors.aliyun.com/ubuntu \3#' sources.list |
国内常用镜像源
根据自己需要,选择自己最快的源。
名称
地址
阿里镜像源
https://mirrors.aliyun.com/ubuntu
清华大学镜像源
https://mirrors.tuna.tsinghua.edu.cn/ubuntu/
网易镜像源
https://mirrors.163.com/ubuntu/
查看更多的镜像
1 | wget -qO - mirrors.ubuntu.com/mirrors.txt |
我这里返回值是
http://mirrors.aliyun.com/ubuntu/ https://mirrors.hit.edu.cn/ubuntu/ http://mirrors.huaweicloud.com/repository/ubuntu/ http://mirrors.sohu.com/ubuntu/ http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ http://linux.xjtuns.cn/ubuntu/ http://mirrors.cqu.edu.cn/ubuntu/ https://mirror.bjtu.edu.cn/ubuntu/ http://mirrors.nju.edu.cn/ubuntu/ http://mirrors.ustc.edu.cn/ubuntu/ http://mirrors.yun-idc.com/ubuntu/ https://mirrors.bfsu.edu.cn/ubuntu/ http://mirror.lzu.edu.cn/ubuntu/ http://mirrors.dgut.edu.cn/ubuntu/ http://ftp.sjtu.edu.cn/ubuntu/ http://mirrors.njupt.edu.cn/ubuntu/ http://archive.ubuntu.com/ubuntu/
解决邮箱关闭导致的邮件无法发送问题 550 5.1.1 recipient is not exist javax.mail
业务方反馈系统的定时邮件没有收到。排查日志后发现,是人员离职后,其工作邮箱关闭,与其相关的业务系统的定时邮件通知的收件人列表内没有移除其邮箱,会导致邮件通知发送失败报”550 5.1.1 recipient is not exist”异常,导致整个邮件通知都没办法发出。 经过查阅文档发现可以设置允许发送部分邮件。可以通过设置属性mail.smtp.sendpartial
或者通过SMTPMessage
的sendPartial
属性来实现。
方案一 mail.smtp.sendpartial
设置构造属性mail.smtp.sendpartial
1 | Properties prop = new Properties(); |
方案二 SMTPMessage
SMTPMessage
有sendPartial属性。把默认的mimeMessage
使用SMTPMessage
包装一下,然后设置 sendPartial
属性为true。
1 | Message message = new MimeMessage(session); |
即使开启了sendPartial
属性,如果遇到有无效收件人时,依旧会抛出一个SMTPSendFailedException
异常。需要处理一下这个异常,根据STMP的定义返回码在2xx的时候都可以认为是成功,所以返回码不为2xx时,把异常再次抛出, 同时在异常内可以通过e.getInvalidAddresses()
获取无效的邮件地址,做进一步处理。。
1 | // Send message |
cron使用示例
后台自动运行维护任务对Linux系统管理员是非常重要的。Linux Cron工具是一个有效的方式去安排后台定时任务。
Linux Crontab格式
1 | MIN HOUR DOM MON DOW CMD |
字段
描述
值
MIN
分钟
0 to 59
HOUR
小时
0 to 23
DOM
天
1-31
MON
月
1-12
DOW
周
0-6
CMD
命令
要执行的命令
指定时间执行任务
cron的最基本用法就是如下所示的在指定时间运行任务。这将在6月10上午8点30执行完整备份的脚本。 请注意时间字段使用24小时制。因此早上8点就是8,晚上8点是20. 30 08 10 06 * /home/ramesh/full-backup 30 – 30分 08 – 上午8点 10 – 10号 06 – 6月 * – 周的每天
指定任务执行多次(比如一天执行两次)
如下的脚本每天两次的执行一个增量的备份 这个示例每天11:00和16:00执行指定的增量备份脚本。如果某个时间段需要执行多个,那么使用逗号分割,那么每个时间都会执行。 00 11,16 * * * /home/ramesh/bin/incremental-backup 00 – 0分 11,16 – 11:00和16:00 * – 每天 * – 每月 * – 周的每天
指定任务在某个时间区域执行
如果你希望任务在指定的时间段每小时执行一次可以使用下面的配置。
每天的工作时间
这个例子在每天(包括周六周日)的早上9点到下午6点检查数据库状态。 00 09-18 * * * /home/ramesh/bin/check-db-status 00 – 0分 09-18 – 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 * – 每天 * – 每月 * – 周的每天
工作日的工作时间
这个例子在每天(不包括周六周日)的早上9点到下午6点检查数据库状态。 00 09-18 * * 1-5 /home/ramesh/bin/check-db-status 00 – 0分 09-18 – 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 * – 每天 * – 每月 1-5 – 周一到周五
如何查看 crontab 条目
查看当前登录用户的 crontab 条目
使用 crontab -l
去查看当前账号的 crontab 条目
查看其它用户的 crontab 条目
要查看其它用户的 crontab 条目,需要有 root 权限然后使用 -u {username} -l
如何编辑 Crontab 条目
编辑当前用户的 crontab 条目
使用 crontab -e
去编辑 crontab 条目,默认情况下会编辑当前登录用户的 crontab.
1 | ramesh@dev-db$ crontab -e |
当你使用:wq
保存上面的临时文件后,它会保存 crontab 并显示下面的信息表明 crontab 已经成功地修改了。
1 | ~ |
修改 root 用户的Crontab 条目
先使用 root 用户登录(su - root) 然后再使用 crontab -e
去编辑.
1 | crontab -e |
修改其它用户的 Crontab 条目
要修改其它用户的 crontab 条目,需要有 root 权限然后使用 -u {username} -l
1 | root@dev-db# crontab -u sathiya -e |
每分钟执行一次任务
正常情况下,你可能不需要一个每分钟都执行一次的任务。但是理解这个例子将会帮助你理解后面的例子。
1 | * * * * * CMD |
星号 * 表示所有有可能的值,每一分钟、每一小时、每一天。除了直接使用星号,你还可以使用以下非常有用的场景: 当你在分钟字段指定 */5 意味着每5分钟 当你在分钟字段指定 0-10/2 意味着在前10分钟里,每两分钟 以上两种写法,在其它字段也是适用的。
每10分钟执行一次任务
如果你想每10分钟检查一次硬盘空间状态,可以使用下面的配置 ```*/10 * * * * /home/ramesh/check-disk-space``` 除了配置这五个字段,我们也可以指定一个关键字。 有几个特殊的场景来替代这五个字段,你可以使用@+关键字例如 reboot,midnight, yearly, hourly. 关键字清单
关键字
等同于
@yearly
0 0 1 1 *
@daily
0 0 * * *
@hourly
0 * * * *
@reboot
开机后执行
使用 @year 在每年开始的时候执行任务
如果你希望一个任务在每年开始的时候执行,那么你就可以使用 @year 关键字。 这个将会执行系统年检通过年检脚本在每年的1月1号00:00. ```@yearly /home/ramesh/red-hat/bin/annual-maintenance```
使用 @monthly 在每月开始的时候执行任务
如果你希望一个任务在每月开始的时候执行,那么你就可以使用 @monthly 关键字。 这个将会在每月的1号00:00执行备份脚本.
1 | @monthly /home/ramesh/suse/bin/tape-backup |
使用 @daily 在每天开始的时候执行任务
如果你希望一个任务在每天开始的时候执行,那么你就可以使用 @daily关键字。 这个将会在每月的1号00:00执行清理日志脚本. ```@daily /home/ramesh/arch-linux/bin/cleanup-logs “day started”
### 使用 @reboot 在每次重启后执行任务 如果你希望一个任务在每次重启后执行,那么你就可以使用 @reboot 关键字。 这个将会在每次服务器重启后执行. ```@reboot CMD
如何通过MAILTO关键字禁用或重定向Crontab的邮件输出
默认情况下crontab会把任务的输出给配置定时任务的用户. 如果你希望重定向输出给一个指定的用户,只需要在crontab里添加或者修改 MAILTO 变量就可以了。
1
2
3
4
5
6
7
ramesh@dev-db$ crontab -l
MAILTO="ramesh"
@yearly /home/ramesh/annual-maintenance
*/10 * * * * /home/ramesh/check-disk-space
[Note: Crontab of the current logged in user with MAIL variable]
如果你不希望发送邮件,把这个变量置空就可以了.
1
MAILTO=""
如何每秒执行一次任务
你不能设置每秒执行的任务。 因为 cron 的可以配置的最小单位是分钟。在正常的业务场景下,也没有理由在系统里设置每秒都执行的任务。
在Crontab里设置PATH变量
上面所有的例子里我们都是指定了Linux命令或脚本的绝对路径。 如果你希望使用相对路径,那么就需要把在crontab 里把程序所在文件夹路径添加到PATH变量里。
1
2
3
4
5
6
7
8
ramesh@dev-db$ crontab -l
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/home/ramesh
@yearly annual-maintenance
*/10 * * * * check-disk-space
[Note: Crontab of the current logged in user with PATH variable]
从一个Cron文件里安装Crontab
除了直接编辑crontab 文件, 你也可以创建一个cron文件然后在里面添加需要的条目, 然后再把这个文件安装进 crontab。
1
2
3
4
5
6
7
8
9
10
11
12
ramesh@dev-db$ crontab -l
no crontab for ramesh
$ cat cron-file.txt
@yearly /home/ramesh/annual-maintenance
*/10 * * * * /home/ramesh/check-disk-space
ramesh@dev-db$ crontab cron-file.txt
ramesh@dev-db$ crontab -l
@yearly /home/ramesh/annual-maintenance
*/10 * * * * /home/ramesh/check-disk-space
安装cron-file.txt时,也会移除所有的旧cron条目, 所以在安装cron文件时,要特别小心。
每个月最后一天执行
cron原生不支持每个月的最后一天执行,但是可以通过判断明天是不是1号,来决定今天是不是这个月的最后一天。
1
55 23 28-31 * * [[ "$(date --date=tomorrow +\%d)" == "01" ]] && myjob.sh
每个月最后一个工作日执行
cron 法定节假日,每年会都变化,每个月的最后一个工作日也会不一样,需要能够动态的更新法定节假日。考虑到一年只有12个月也就是12个工作日期,我把每月工作日写入 lastworkingday.txt 然后对比当天是不是在这个文件内,如果在就执行。同时把 lastworkingday.txt 文件上传到 github 定期去摘取这个文件。如果法定节假日发生变化,我只需要提交对 lastworkingday.txt的修改就好了。
1
2
3
00 00 * * * git -C /opt/mycrontabdb pull
00 12 20-31 * * grep $(date +%F)
/opt/mycrontabdb/lastworkingday.txt && myjob.sh
lastworkingday.txt sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
20201030
20201130
20201231
20210129
20210226
20210331
20210430
20210531
20210630
20210730
20210831
20210930
20211029
20211130
20211231
NodeJS 镜像配置 npm yarn 淘宝镜像
下载npm包经常下载不下来,还好淘宝提供了npm的镜像站,配置好之后,再使用npm或yarn就可以通过淘宝镜像站下载npm包,不用使用cnpm。
配置镜像
1 | npm config set registry https://registry.npm.taobao.org/ |
在线更新 dorado 规则RequestRejectedException
使用在线更新 dorado 规则时,项目报错org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String "//"
. 这个是由Spring Security 5提供的一个HTTP防火墙,拦截可疑访问导致的。在项目中注册如下的bean,即可替换系统默认的防火墙。该实现没有任何防范作用,建议仅仅再更新Dorado规则时临时打开,一定不能发布到生产环境。
1 |
|
node-sass Error: EACCES: permission denied, mkdir 'xxx/node_modules/node-sass/vendor'
Unable to save binary //node_modules/node-sass/vendor/linux-x64-64 : { Error: EACCES: permission denied, mkdir ‘xxx/node_modules/node-sass/vendor’ at Object.mkdirSync (fs.js:752:3) 使用npm打包时,发现node-sass无法安装成功,但是用yarn可以。 原来是因为 npm 为了安全禁止使用root用户或者sudo来安装node-sass,切换到普通用户就可以了,或者添加 --unsafe-perm
参数。
1 | sudo npm install --unsafe-perm -g node-sass |
MacOS 下切换默认 Java (JDK)
TLDR
如果同时安装了 adoptopenjdk 11 和 adoptopenjdk 8,同时希望 8 做为默认 JDK,只需要把 /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Info.plist
里的JVMVersion
的值由1.8.0_222
改为 x1.8.0_222
(大概第42行)。这样我们的MacOS默认JDK就成为adoptopenjdk-8了。
查看 JDK 相关信息
macOS下 JDK 默认安装在 /Library/Java/JavaVirtualMachines
目录下,同时提供了一个小工具/usr/libexec/java_home
帮助我们快速的查看 JDK 相关的信息。 默认情况下 MacOS 会自动的选择 /Library/Java/JavaVirtualMachines
目录下版本号最高的 JDK 做为默认 JDK 。
查看当前 JDK 版本
1 | ➜ ~ java -version |
查看当前 JDK 的安装目录
1 | ➜ ~ /usr/libexec/java_home |
查看已安装的 JDK 版本及目录
查看所有
1 | ➜ ~ /usr/libexec/java_home -V |
查看指定版本 可以通过/usr/libexec/java_home -v <version>
来过滤版本号。 返回前缀匹配到的最新 JDK。
1 | ➜ ~ /usr/libexec/java_home -v 1 |
切换 JDK
使用指定的 JDK 执行单次命令
可以通过java_home
的exec
选项来执行单次任务。 /usr/libexec/java_home -v version –exec command
1 | ➜ ~ /usr/libexec/java_home -v 1.7 --exec java -version |
切换 Shell 的 JDK到指定版本
Shell 环境只需要指定一下JAVA_HOME
环境变量就可以。
1 | ➜ ~ export JAVA_HOME=`/usr/libexec/java_home -v 1.7` |
为了方便切换把以下别名配置粘到对应的 Shell 的配置文件 .bashrc 或 .zshrc,然后就可以方便的切换 JDK 版本了。 别名配置 需要根据自己实际已安装 JDK 做增减。
1 | alias j12="export JAVA_HOME=`/usr/libexec/java_home -v 12`; java -version" |
使用效果
1 | ➜ ~ j7 |
切换 GUI 程序的默认 JDK
GUI 程序使用的默认 Java 也是 /usr/libexec/java_home -V
中看到的最高版本。
指定全局环境变量
创建setenv.javahome.plist
并通过launchctl
指定设置环境变量JAVA_HOME
,需要注销再登录才生效。而且有些程序不兼容该方式。使用该方式之后,/usr/libexec/java_home
的显示跟实际执行也会出现不一致。 生成 setenv.javahome.plist
并加载的脚本:
1 | cat > ~/Library/LaunchAgents/setenv.javahome.plist <<EOF |
修改 JDK 版本号
我们还可以通过修改版本号实现指定版本的JDK做为默认JDK, 我目前正在使用该方式。 /usr/libexec/java_home
是通过/Library/Java/JavaVirtualMachines/<JDK>/Contents/Info.plist
里的JVMVersion
值来获取版本号的,所以只需要修改这个值为当前最大版本号即可实现指定默认 JDK。经过测试这个还是即时生效。 像我安装过adoptopenjdk 11 ,但还是希望 adoptopenjdk 8做为默认 JDK,只需要把 /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Info.plist
里的JVMVersion
的值由1.8.0_222
改为 x1.8.0_222
(大概第42行)。这样我们的adoptopenjdk-8.jdk就变成最新版本的 JDK 了。
排序是通过 ASCII 值来排的,版本号只要改的比最新的 11 大都行,字符’x’的ASCII值远大于字符’1’, 为了方便版本区分我只加了一个字符 x 。
修改完成之后再查看 JDK 信息,就会发现我们修改的x1.8.0_222会排到第一位,同时 Java version 是 1.8。
1 | ➜ ~ /usr/libexec/java_home -V |
操作流程: 备份原始文件并打开,并使用 vim 编辑.
1 | sudo cp /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Info.plist /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Info.plist.bak |
修改后的Info.plist
文件差异
1 | ➜ ~ diff -c /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Info.plist /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Info.plist.bak |
macOS 下 java 的真身
通过简单的探索,就能发现我们使用的 java
其实是软链到 /System/Library/Frameworks/JavaVM.framework/Versions/A/Commands/java
。
1 | ➜ ~ greadlink -f `which java` |
greadlink 需要安装coreutils
brew install coreutils
而 /System/Library/Frameworks/JavaVM.framework/Versions/A/Commands
目录下的文件多是固定 38k 大小。猜测这些文件应该只是包装器,根据系统配置把命令转发给相应的 JDK 的对应命令。
1 | ➜ ~ ll /System/Library/Frameworks/JavaVM.framework/Versions/A/Commands |
MySQL 记录所有SQL和慢SQL
通过配置文件配置日志
my.cnf 位置
查看自己版本 MySQL 默认读取配置文件的路径
1 | mysql --help grep my.cnf |
我的这个版本的 MySQL 会以以下顺序读配置文件 1. /etc/my.cnf 2. /etc/mysql/my.cnf 3. /usr/local/etc/my.cnf 4. ~/.my.cnf 如果使用了自定义位置可以通过查看进程启动参数 --defaults-file
1 | ps aux grep mysqld |
日志配置
通用SQL日志 (General Query Log) 记录所有mysqld做的事,连接、断开、查询。
1 | # 通用SQL日志 |
慢SQL日志 (Slow Query Log) 记录慢SQL long_query_time 默认10,单位秒;SQL执行时间比long_query_time长的都会被记录。
1 | # 慢SQL日志 |
在运行时开启日志
开启日志,登录 mysql client (mysql -u root -p) 然后执行:
1 | SET GLOBAL general_log = 'ON'; |
关闭日志,登录 mysql client (mysql -u root -p) 然后执行:
1 | SET GLOBAL general_log = 'OFF'; |
即时生效,不需要重启。 查看日志文件位置
1 | show variables like '%log_file'; |