基本使用

1
2
3
4
5
6
7
8
9
10
11
# install
sudo apt-get install heirloom-mailx
# configure
vim /etc/s-nail.rc
set from=${demo@qq.com}
set smtp=smtps://smtp.qq.com:465
set smtp-auth-user=${demo@qq.com}
set smtp-auth-password=${password}
set smtp-auth=login
# send mail
echo "mail content" | mail -vs "mail subject" ${target@mail.com}

crontab

介绍

通过crontab 命令,我们可以在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。这个命令非常适合周期性的日志分析或数据备份等工作。

1
crontab [-u user] file crontab [-u user][ -e | -l | -r ]

参数

  • -u user:用来设定某个用户的crontab服务;
  • file:file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。如果在命令行中没有指定这个文件,crontab命令将接受标准输入(键盘)上键入的命令,并将它们载入crontab。
  • -e:编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。
  • -l:显示某个用户的crontab文件内容,如果不指定用户,则表示显示当前用户的crontab文件内容。
  • -r:从/var/spool/cron目录中删除某个用户的crontab文件,如果不指定用户,则默认删除当前用户的crontab文件。
  • -i:在删除用户的crontab文件时给确认提示。

crontab的文件格式

分 时 日 月 星期 要运行的命令

  • 第1列分钟0~59
  • 第2列小时0~23(0表示子夜)
  • 第3列日1~31
  • 第4列月1~12
  • 第5列星期0~7(0和7表示星期天)
  • 第6列要运行的命令

crontab文件惯例

我个人惯例会把cron文件创建在/opt/cron目录下

新创建的cron文件的副本会被放在/var/spool/cron/crontabs目录下,文件名为用户名

使用方法

1
2
3
4
5
6
7
8
9
10
# 创建cron文件
vim /opt/cron/cron_file
# 提交cron任务
crontab /opt/cron/cron_file
# 启动
/etc/init.d/cron start
# 关闭
/etc/init.d/cron stop
# 重启
/etc/init.d/cron restart

使用注意事项

新创建的cron job,不会马上执行,至少要过2分钟才执行。如果重启cron则马上执行。

当crontab失效时,可以尝试/etc/init.d/crond restart解决问题。或者查看日志看某个job有没有执行/报错tail -f /var/log/cron。

千万别乱运行crontab -r。它从Crontab目录(/var/spool/cron)中删除用户的Crontab文件。删除了该用户的所有crontab都没了。

在crontab中%是有特殊含义的,表示换行的意思。如果要用的话必须进行转义%,如经常用的date ‘+%Y%m%d’在crontab里是不会执行的,应该换成date ‘+%Y%m%d’。`

cron表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 实例1:每1分钟执行一次myCommand
* * * * * myCommand
# 实例2:每小时的第3和第15分钟执行
3,15 * * * * myCommand
# 实例3:在上午8点到11点的第3和第15分钟执行
3,15 8-11 * * * myCommand
# 实例4:每隔两天的上午8点到11点的第3和第15分钟执行
3,15 8-11 */2 * * myCommand
# 实例5:每周一上午8点到11点的第3和第15分钟执行
3,15 8-11 * * 1 myCommand
# 实例6:每晚的21:30重启smb
30 21 * * * /etc/init.d/smb restart
# 实例7:每月1、10、22日的4 : 45重启smb
45 4 1,10,22 * * /etc/init.d/smb restart
# 实例8:每周六、周日的1 : 10重启smb
10 1 * * 6,0 /etc/init.d/smb restart
# 实例9:每天18 : 00至23 : 00之间每隔30分钟重启smb
0,30 18-23 * * * /etc/init.d/smb restart
# 实例10:每星期六的晚上11 : 00 pm重启smb
0 23 * * 6 /etc/init.d/smb restart
# 实例11:每一小时重启smb
* */1 * * * /etc/init.d/smb restart
# 实例12:晚上11点到早上7点之间,每隔一小时重启smb
0 23-7 * * * /etc/init.d/smb restart

定时备份

1
2
3
4
5
6
7
# 创建定时任务文件
crontab $cron-file-name
# 写入规则每天0点、12点备份一次
vim $cron-file-name
0 0,12 */1 * * $backup-shell
# 启动定时任务
crontab $cron-file-name

mysql运维

装卸

1
2
3
4
5
6
sudo apt-get install mysql-server-5.7
service mysql start
service mysql stop
sudo apt-get remove mysql-server-5.7
# 数据存放目录 /var/lib/mysql
# 配置存放目录 /etc/mysql

用户管理

1
2
3
# root用户登录
mysql -u root -p
# 切勿修改root用户和使用root用户进行生产

Adding User Accounts

1
2
3
4
5
6
7
8
9
10
11
12
# e.g.1
CREATE USER 'finley'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'finley'@'localhost' WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON database.table TO 'finley'@'localhost' WITH GRANT OPTION;
# e.g.2
CREATE USER 'finley'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'finley'@'%' WITH GRANT OPTION;
# e.g.3
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'password';
GRANT RELOAD,PROCESS ON *.* TO 'admin'@'localhost';
# e.g.4
CREATE USER 'dummy'@'localhost';

Removing User Accounts

1
DROP USER 'jeffrey'@'localhost';

授权管理

The MySQL Access Privilege System

When Privilege Changes Take Effect

1
2
3
4
# e.g.1
GRANT ALL ON db1.* TO 'jeffrey'@'localhost';
# e.g.2
GRANT SELECT ON db2.invoice TO 'jeffrey'@'localhost';

授权用户远程连接

1
2
3
4
5
6
7
8
9
10
# 检查一下3个配置文件是否有host绑定
/etc/mysql/mysql.cnf
/etc/mysql/conf.d/mysql.cnf
/etc/mysql/mysql.conf.d/mysql.cnf
# 注释本地host绑定
bind-address = 127.0.0.1
# 授权用户可连接的host
GRANT ALL PRIVILEGES ON *.* TO 'user'@'%' IDENTIFIED BY 'password' WITH GRANT OPTION;
# 刷新授权
FLUSH PRIVILEGES;

数据编码

Character Set Configuration

1
2
# 查看编码设置
show variables like '%character%';

DDL、DML导入脚本

1
2
3
mysql -u user -p
use database
source ~/script.sql

数据备份与恢复

Chapter 7 Backup and Recovery

分为物理备份和逻辑备份两种

物理备份是备份的数据元文件,备份恢复快,适合数据量大,压缩率低,占用空间多,需要快速恢复的场景。具体可参考 MySQL Enterprise Backup Overview

逻辑备份是备份DDL,DML语句,备份恢复慢,适合数据量小,压缩率高,占用空间少,恢复速度要求低的场景

具体选择的策略可参考MySQL Backup in Facebook

逻辑备份具体可选用主从复制的方式,从slave节点上进行备份.

贴个逻辑备份脚本

主从复制

Replication

一、问题描述

使用hibernate管理ORM时,如果某个映射实体字段为null保存时会报column ‘xx’ cannot be null之类的异常,导致存库失败,为解决这个问题,hibernate提供了动态SQL的机制。

二、解决方案

在实体关系配置时,加入dynamic-insert / dynamic-update (对应JPA中的@DynamicInsert/ @DynamicUpdate)会在执行插入或更新时动态判断字段是否为null(或是否有更新),如果为null(或没有更新)则不更新这类字段,也就不会产生异常。它的原理是在实体被加载到session中时会保存一份快照,如果在后续的更新操作检测到有更新,则动态生成更新部分涉及到的字段的sql。

三、Bonus

使用动态SQL的前提

就算使用了动态SQL机制,但如果字段在DDL中没有声明默认值,那么当实体字段为null时进行更新,依然会由db层面报出异常。因此要规范DDL,并结合动态SQL机制来避免业务代码出现实体保存时的空值异常。 案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# java code
User user = new User();
session.save(user);

# exception
org.hibernate.PropertyValueException: not-null property references a null or transient value: User.avatar

# hibernate mapping
<class name="User" table="user" dynamic-insert="true" dynamic-update="true">
<property name="avatarId" column="avatar_id" type="integer" not-null="true">
</class>


# table DDL 没有设置默认值,如果实体值为null依然会由db报异常
CREATE TABLE `user` (
`avatar_id` int(11) NOT NULL
);

动态SQL的使用限制:同一个Session

动态SQL的使用是有前提条件的:需要在同一个Session中操作实体才能生效。 前面提到动态SQL的原理是在实体加载到Session中时保留了一份快照,后续操作时比对快照生成正确的sql操作,那么如果一个实体加载和回写使用的Session是不同的,那么自然无法进行快照比对,那么动态SQL的机制也就无法实施,hibernate只好按照默认规则更新全字段。

看个例子:如果我们在Controller层去写业务逻辑(__生产中请务必不要这么干!__)就极有可能会导致@DynamicUpdate失效,比如下面这段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RequestMapping("/queryAndUpdate")
public Result queryAndUpdate(Http http) {
//Controller层写业务逻辑
int id = 1;
//using session1
User user = services.getDaos().getUserDao().findById(id);
//using session2 user中加载有没有映射到的关联实体 更新时抛出异常
lecturer = services.getUserService().updateUser(user);
return Result.success().setBean(user);
}
//实体
@Entity
@DynamicInsert
@DynamicUpdate
@Table(name = "user")
class User{
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@NotFound(action = NotFoundAction.IGNORE)
@OneToOne(targetEntity = File.class,cascade = CascadeType.REFRESH)
@JoinColumn(name = "avatar_id")
private File avatar; //未加载的关联实体
...
}

原因是我们一般会把事务控制在Service层,那么在Controller层的业务逻辑加载完一个对象,当对象返回到Controller层其实就已经穿透了事务边界,事务结束,hibernate默认会关闭Session;那么下面再次进入Service层更新这个对象就会开启新的事务,由新的Session来进行执行。

所以我们要想解决上述问题,目标很明确,在Controller中不会因为穿透了Service层而加载新的Session。

Spring为我们提供了OpenSessionInViewFilter这个过滤器可以轻松的达到上述目的,它将开启一个Session绑定到当前请求线程,这个线程上的Session将会被TransactionManager利用,因此事务结束(Service层穿透)也不会关闭Session,而是在整个请求周期中复用同一个Session。具体参考_Spring文档_,这里不展开。

至此,动态SQL在整个请求周期内可以正常运行。

性能!

另外值得注意的是动态SQL打开了以后,不同对象的sql语句会不一样,如果一次更新多条记录,hibernate将不能使用 executeBatch进行批量更新,这样效率将大打折扣。在这种情况下,多条sql意味着数据库要编译多次sql语句。

因此有批量更新的特殊场景时,建议单独使用hql或者sql进行操作。

文档参考:

annotations-hibernate-dynamicupdate

pc-managed-state-dynamic-update

gitmoji

一种增强git提交信息表达的图标

官网 https://gitmoji.carloscuesta.me

官方介绍:An emoji guide for your commit messages

基本语法 :word:

将语法写入commit任意位置,即可生成对应icon

icon word e.g. explain
:recycle: :recycle: Refactoring code.
:sparkles: Introducing new features.
:construction: :construction: Work in progress.
🐛 :bug: Fixing a bug.
🔨 :hammer: Refactoring code.
⬆️ :arrow_up: Upgrading dependencies.
⬇️ :arrow_down: Downgrading dependencies.
:twisted_rightwards_arrows: :twisted_rightwards_arrows: Merging branches.
:fire: :fire: Removing code or files.
:white_check_mark: :white_check_mark: Adding tests.
:zap: Improving performance.
:wrench: :wrench: Changing configuration files. …
:tada: :tade: Initial commit.
:art: :art: Improving structure / format of the code.

问题背景

生产环境(JDK8u144)中有部分公共数据存在xml中,使用时会将xml数据读取到内存,以 ArrayList 存储,多个用户(多线程)对同一个 ArrayList 使用了 Collections 的sort(List,Comparator)方法进行排序,从而触发了 ConcurrentModificationException。其本质就是一个并发问题。

问题复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testConcurrentModificationException() throws Exception {
Random random = new Random();
//初始化ArrayList
List<Integer> dataList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
dataList.add(random.nextInt(10));
}
System.out.println("initialize " + dataList.stream().map(Objects::toString).collect(Collectors.joining(",")));
//初始化排序任务
Runnable runnable = () -> {
Collections.sort(dataList, Integer::compareTo);
System.out.println("thread[" + Thread.currentThread().getId() + "] " + dataList.stream().map(Objects::toString).collect(Collectors.joining(",")));
};
//模拟多线程环境对同一个ArraList进行排序
for (int i = 0; i < 1000; i++) {
ThreadPool.execute(runnable);
}
}

以上Test Case将复现 ConcurrentModificationException 异常

#ConcurrentModificationException

顾名思义,ConcurrentModificationException 是并发修改引起的异常。追溯 JDK8u144Collections::sort 实现可以发现,最终调用的是ArrayList的sort方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//Collections::sort
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
//ArrayList::sort
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}

而在 JDK8u20 之前

解决方案

加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testConcurrentModificationException() throws Exception {
Random random = new Random();
List<Integer> dataList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
dataList.add(random.nextInt(10));
}
System.out.println("initialize {}" + dataList.stream().map(Objects::toString).collect(Collectors.joining(",")));
Runnable runnable = () -> {
synchronized (random) {
Collections.sort(dataList, Integer::compareTo);
System.out.println("thread[" + Thread.currentThread().getId() + "] " + dataList.stream().map(Objects::toString).collect(Collectors.joining(",")));
}
};
for (int i = 0; i < 1000; i++) {
ThreadPool.execute(runnable);
}
}

使用 CopyOnWriteArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testConcurrentModificationException() throws Exception {
Random random = new Random();
List<Integer> dataList = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100; i++) {
dataList.add(random.nextInt(10));
}
System.out.println("initialize {}" + dataList.stream().map(Objects::toString).collect(Collectors.joining(",")));
Runnable runnable = () -> {
Collections.sort(dataList, Integer::compareTo);
System.out.println("thread[" + Thread.currentThread().getId() + "] " + dataList.stream().map(Objects::toString).collect(Collectors.joining(",")));
};
for (int i = 0; i < 1000; i++) {
ThreadPool.execute(runnable);
}
}

Api-Mocker

Api-Mocker丁香园 前端团队开源的一款接口管理软件。集诸如阿里开源的Rap、Chrome的postman等接口相关工具的优点于一身。 github地址

生产环境部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#先安装好mongo(略)

#开始生产环境部署
git clone https://github.com/DXY-F2E/api-mocker

# make install将启动mongodb
make install

vim client/config/index.js
# 修改serverRoot: 'demo.domain.cn/mock-api'
vim server/config/config.prod.js
# 修改clientRoot: 'http://demo.domain.cn/mock'

make prod_client
make prod_server

#配置密码加密加盐
vim server/config/config.default.js
md5Key: 'demo-salt'
#配置找回密码邮件发送
vim server/config/config.default.js
transporter: {
appName: 'Api Mocker',
host: 'smtp.qq.com',
secure: true,
port: 465,
auth: {
user: 'demo@qq.com',
pass: 'demo-auth-code'
}
}
vim server/config/config.prod.js
clientRoot: 'http://demo.domain.cn/mock'

Mongodb数据备份方案

部署 api-mocker 后服务一直运行,没有备份过数据。一次服务器断电重启后发现mongo的数据全部丢失。因此整个简单的备份方案。

有三种方案可以选择,参考方案选择。这里选用mongo自带的套装命令 mongodump mongorestore

备份脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/zsh
MONGO_DATABASE="api-mock"
APP_NAME="mongo-bak"

MONGO_HOST="127.0.0.1"
MONGO_PORT="27017"
TIMESTAMP=`date +%F-%H%M`
MONGO="/opt/mongodb-linux-x86_64-3.0.6/bin/mongo"
MONGODUMP="/opt/mongodb-linux-x86_64-3.0.6/bin/mongodump"
BACKUPS_DIR="/var/backups/$APP_NAME"
BACKUP_NAME="$APP_NAME-$TIMESTAMP"

# 锁库
$MONGO admin --eval "printjson(db.fsyncLock())"
# 备份
$MONGODUMP -d $MONGO_DATABASE
# 远程备份用下面注释的这条
# $MONGODUMP -h $MONGO_HOST:$MONGO_PORT -d $MONGO_DATABASE
# 解锁
$MONGO admin --eval "printjson(db.fsyncUnlock())"

mkdir -p $BACKUPS_DIR
mv dump $BACKUP_NAME
tar -zcvf $BACKUPS_DIR/$BACKUP_NAME.tgz $BACKUP_NAME
rm -rf $BACKUP_NAME

恢复数据

1
mongorestore $mongo-bak-dir

mongo基本命令

用以下命令可以简单测试备份及数据恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 进入mongo CLI
mongo
# 查询所有库
show dbs
# 选择库
use $db-name
# 查询所有集合
show collections
# 查询集合中的所有文档
db.$col-name.find().pretty()
# 删除集合中是所有文档
db.$col-name.remove({})
# 删除库
db.dropDatabase()

定时备份

1
2
3
4
5
6
7
# 创建定时任务文件
crontab $cron-file-name
# 写入规则每天0点、12点备份一次
vim $cron-file-name
0 0,12 */1 * * $backup-shell
# 启动定时任务
crontab $cron-file-name

Tips

  1. 投入使用前最好先配置密码加密盐,以后配置用户密码迁移成本高
  2. 接口文档重写很操蛋,务必备份mongo数据!!!
  3. 太旧的备份文件最好也写个脚本定时清理一下
0%