Coding & Life

求知若饥,虚心若愚

0%

持续集成是什么?

互联网软件的开发和发布,已经形成了一套标准流程,最重要的组成部分就是持续集成(Continuous integration,简称CI)

持续集成的目的

让产品可以快速迭代,同时还能保持高质量

Jenkins是什么?

Jenkins是一个用Java编写的开源的持续集成工具,因此安装Jenkins必须有Java运行环境

Jenkins安装

官网下载Jenkins安装包,按照官网提示进行安装

安装常用插件

常用插件

创建密码

创建密码

注意:需要防火墙打开8080端口,Jenkins默认使用8080端口

Jenkins的配置

全局工具配置

  • 配置jdk
  • 配置maven

插件安装

Jenkins有很多插件已经被安装,其中Git plugin和Maven Integration plugin,publish over SSH是部署Spring Boot项目必备的插件

配置Credentials

配置成功后可以用ssh协议拉取git上的代码

问题:在mybatis中,0被认为是空字符串
解决:普通判断1. != null 2. != ‘’。当类型为Integer类型时只进行方式1的判断

数据安全高于一切

有时我们为了节省开支,并不会购买云数据库而是选择自建数据库,这时数据安全就极为重要。数据备份是保证安全最有效的方式

编写数据备份脚本

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
# /bin/bash
DB_NAME="exchange"
DB_USER="root"
DB_PASSWORD="abc123"
BIN_DIR="/usr/bin"
BACK_DIR="/root/data"
DATE="mysql-`date +'%Y%m%d-%H:%M:%S'`"
LogFile="$BACK_DIR"/dbbakup.log
BackNewFile=$DATE.sql

$BIN_DIR/mysqldump -u$DB_USER -p$DB_PASSWORD $DB_NAME > $BACK_DIR/$DATE.sql

echo -----------------"$(date +"%y-%m-%d %H:%M:%S")"------------------ >> $LogFile

echo createFile:"$BackNewFile" >> $LogFile

find "/root/data/" -ctime +0 -type f -name "*.sql" -print > deleted.txt

echo -e "delete files:\n" >> $LogFile

cat deleted.txt | while read LINE
do
rm -rf $LINE
echo $LINE>> $LogFile
done

echo "---------------------------------------------------------------" >> $LogFile

利用cron定时执行

利用cron服务定时执行数据备份脚本。该脚本会自动删除过期的sql文件
例如 每天12:50定时执行mysqlback.sh脚本:

1
50 12 * * * /root/mysqlback.sh

linux导入sql

1
mysql -h [host] -u [userName] -p [databaseName] < [data].sql

linux导出库

1
mysqldump -h [host] -u [username] -p --databases [databasename] > [fileName].sql

linux导出单表

1
mysqldump -h [host] -u [username] -p [dabaseName] [tableName] > [fileName].sql

更新了系统后,使用git命令,提示错误如下:

1
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

原因

因为每次更新系统之后xcode就被卸载了,因此需要重新安装一次

解决方案

终端执行

1
xcode-select --install

什么是sequence

序列,在Oracle数据库中,什么是序列呢?它的用途是什么?序列(SEQUENCE)其实是序列号生成器,可以为表中的行自动生成序列号,产生一组等间隔的数值(类型为数字)。其主要的用途是生成表的主键值,可以在插入语句中引用,也可以通过查询检查当前值,或使序列增至下一个值。

mysql没有内置sequence,需要自己实现

创建sequence表

1
2
3
4
5
6
CREATE TABLE sequence (
name VARCHAR(50) NOT NULL,
current_value INT NOT NULL,
increment INT NOT NULL DEFAULT 1,
PRIMARY KEY (name)
)

获取当前序列值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE FUNCTION currval (seq_name VARCHAR(50))
RETURNS INTEGER
LANGUAGE SQL
DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
DECLARE value INTEGER;
SET value = 0;
SELECT current_value INTO value
FROM sequence
WHERE name = seq_name;
RETURN value;
END

获取下一个序列

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE FUNCTION nextval (seq_name VARCHAR(50))
RETURNS INTEGER
LANGUAGE SQL
DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
UPDATE sequence
SET current_value = current_value + increment
WHERE name = seq_name;
RETURN currval(seq_name);
END

重置序列值

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE FUNCTION setval (seq_name VARCHAR(50), value INTEGER)
RETURNS INTEGER
LANGUAGE SQL
DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
UPDATE sequence
SET current_value = value
WHERE name = seq_name;
RETURN currval(seq_name);
END

应用

1
2
3
4
5
6
7
// 在序列表中新建一条序列(参数依次为:序列名称、序列开始值、序列递增步长)
INSERT INTO sequence VALUES ('TestSeq', 0, 1);
// 设置序列开始值(参数依次为:序列名称、序列开始值)
SELECT SETVAL('TestSeq', 10);
SELECT CURRVAL('TestSeq');
// 获得下一个序列值
SELECT NEXTVAL('TestSeq');

server服务端

主线程

构建页面

创建serverSocket

添加send按钮的点击事件

子线程

死循环接收消息

服务端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* 主线程发送消息
* 子线程接收消息
*/
package com.yatai.web;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
* @author 王伟业 2014年5月28日
*/
public class ChatServer implements ActionListener, Runnable {
// 用来存放客户端连接数量
public static ArrayList<Socket> socketList = new ArrayList<Socket>();
// 显示历史聊天记录
private JTextArea showArea;
// 待发送的字符区域
private JTextField msgText;
// 窗口
private JFrame mainJframe;
// 发送按钮
private JButton sentBtn;
// 滚动面板
private JScrollPane JSPane;
// 普通面板
private JPanel pane;
// 最大的容器
private Container con;
// 线程处理信息
private Thread thread = null;
private ServerSocket serverSocket;
private Socket connectToClient;
private DataInputStream inFromClient;
private DataOutputStream outToClient;

/**
* 构造函数用来设置界面,处理事件
*/
public ChatServer() {
// TODO Auto-generated constructor stub
// 设置页面
mainJframe = new JFrame("服务器端");
// 初始化容器
con = mainJframe.getContentPane();
showArea = new JTextArea();
showArea.setEditable(false); // 历史聊天窗口中的文字域不能编辑,只供查看
showArea.setLineWrap(true); // 自动换行
JSPane = new JScrollPane(showArea);
// 待发送文字区域
msgText = new JTextField();
msgText.setColumns(35);
// 事件监听
msgText.addActionListener(this);
sentBtn = new JButton("Send");
sentBtn.addActionListener(this);
// 界面下部
pane = new JPanel();
pane.setLayout(new FlowLayout());
pane.add(msgText);
pane.add(sentBtn);

con.add(JSPane, BorderLayout.CENTER);
con.add(pane, BorderLayout.SOUTH);
mainJframe.setSize(500, 400);
mainJframe.setLocation(600, 200);
mainJframe.setVisible(true);
mainJframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

try {
serverSocket = new ServerSocket(8888);
showArea.append(" 正在等待对话请求\n");
// 监听端口
connectToClient = serverSocket.accept();
inFromClient = new DataInputStream(connectToClient.getInputStream());
outToClient = new DataOutputStream(connectToClient.getOutputStream());
// 启动线程
thread = new Thread(this);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
} catch (IOException e) {
// TODO Auto-generated catch block
// 出现此异常说明服务器未创建成功
showArea.append(" 对不起,不能创建服务器\n");
// 待发送文字区域不能编辑
msgText.setEditable(false);
// 发送按钮不可用
sentBtn.setEnabled(false);
}

}

/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
// TODO Auto-generated method stub
// 此线程用来接收客户端传来的信息
while (true) {
try {
showArea.append(" 对方说:" + inFromClient.readUTF() + "\n");
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

/*
* (non-Javadoc)
*
* @see
* java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
// 响应按钮事件
String s = msgText.getText();
// 如果待发送文字区域存在文字
if (s.length() > 0) {
try {
// 将文字写入到流中
outToClient.writeUTF(s);
outToClient.flush();
// 历史聊天记录增添内容
showArea.append(" 我说:" + msgText.getText() + "\n");
// 待发送文字区域设为空
msgText.setText(null);
} catch (IOException e1) {
// TODO Auto-generated catch block
// 出现此异常说明消息未发送成功
showArea.append(" 你的消息:" + "“" + msgText.getText() + "”" + "未能发送成功\n");
}
}
}

public static void main(String[] args) {
// 主线程用来发送消息
new ChatServer();
}

}

client客户端

主线程

构建页面

连接serverSocket

添加send按钮的监听事件

子线程

死循环接收消息

客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
*
*/
package com.yatai.web;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
* @author 王伟业 2014年5月28日
*/
public class ChatClient implements ActionListener, Runnable {
// 相同的界面形式
private JTextArea showArea;
private JTextField msgText;
private JFrame mainJframe;
private JButton sentBtn;
private JScrollPane JSPane;
private JPanel pane;
private Container con;
// 相似的处理方法
private Thread thread = null;
private Socket connectToServer;
private DataInputStream inFromServer;
private DataOutputStream outToServer;

/**
*
*/
public ChatClient() {
// TODO Auto-generated constructor stub
// 构造函数下完成以下内容
mainJframe = new JFrame("客户端");
con = mainJframe.getContentPane();
showArea = new JTextArea();
showArea.setEditable(false);
showArea.setLineWrap(true);
showArea.setWrapStyleWord(true);
JSPane = new JScrollPane(showArea);
msgText = new JTextField();
msgText.setColumns(35);
msgText.addActionListener(this);
sentBtn = new JButton("Send");
sentBtn.addActionListener(this);
pane = new JPanel();
pane.setLayout(new FlowLayout());
pane.add(msgText);
pane.add(sentBtn);
con.add(JSPane, BorderLayout.CENTER);
con.add(pane, BorderLayout.SOUTH);
mainJframe.setSize(500, 400);
mainJframe.setLocation(80, 200);
mainJframe.setVisible(true);
mainJframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

try {
connectToServer = new Socket("localhost", 8888);
inFromServer = new DataInputStream(connectToServer.getInputStream());
outToServer = new DataOutputStream(connectToServer.getOutputStream());
showArea.append(" 连接成功,可以通信\n");

// 创建线程
thread = new Thread(this);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
} catch (IOException e) {
// TODO Auto-generated catch block
// 出现异常说明连接失败
// 向历史聊天区域打印提示信息
showArea.append(" 对不起,连接服务器失败\n");
// 异常连接时输入框不可用
msgText.setEditable(false);
msgText.setEnabled(false);
}
}

/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
// TODO Auto-generated method stub
// 该线程用来接收传来的消息
while (true) {
try {
showArea.append(" 对方说:" + inFromServer.readUTF() + "\n");
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
// 此处异常处理。。。。
e.printStackTrace();
}
}
}

/*
* (non-Javadoc)
*
* @see
* java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
// 响应事件
String s = msgText.getText();
// 如果待发送文字区域存在文字
if (s.length() > 0) {
try {
// 将文字写入到流中
outToServer.writeUTF(s);
outToServer.flush();
// 历史聊天记录增添内容
showArea.append(" 我说:" + msgText.getText() + "\n");
// 待发送文字区域设为空
msgText.setText(null);
} catch (IOException e1) {
// TODO Auto-generated catch block
// 出现此异常说明消息未发送成功
showArea.append(" 你的消息:" + "“" + msgText.getText() + "”" + "未能发送成功\n");
}
}
}

public static void main(String[] args) {
new ChatClient();
}
}


我们在开发Spring Boot应用时,通常同一套程序会被应用和安装到几个不同的环境,比如:开发、测试、生产等。其中每个环境的数据库地址、服务器端口等等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。

对于多环境的配置,各种项目构建工具或是框架的基本思路是一致的,通过配置多份不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,Spring Boot也不例外,或者说更加简单。

在Spring Boot中多环境配置文件名需要满足 application-{profile}.properties的格式,其中{profile}对应你的环境标识,比如:

1
2
3
application-dev.properties:开发环境 
application-test.properties:测试环境
application-prod.properties:生产环境

至于哪个具体的配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应{profile}值。

如:spring.profiles.active=test就会加载application-test.properties配置文件内容

下面,以不同环境配置不同的服务端口为例,进行样例实验。

针对各环境新建不同的配置文件application-dev.propertiesapplication-test.propertiesapplication-prod.properties

测试不同配置的加载:
执行java -jar xxx.jar --spring.profiles.active=prod也就是生产环境的配置(prod)按照上面的实验,可以如下总结多环境的配置思路:

application.properties中配置通用内容,并设置spring.profiles.active=dev,以开发环境为默认配置
application-{profile}.properties中配置各个环境不同的内容
通过命令行方式去激活不同环境的配置

转载自程序猿DD-翟永超

更新缓存策略

  • 失效:应用程序从cache取数据,没有得到,则从数据库取数据,然后将数据放入cache
  • 命中:应用从cache取数据,若取到则返回
  • 更新:把数据更新到数据库,然后使cache失效

借用网上两张图

有没有其他策略?

读操作容易理解,现在讨论一下写的操作

如果先使cache失效,后更新数据库,是否可行

这么做引发的问题是,如果A,B两个线程同时要更新数据,并且A,B已经都做完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,由于缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致了。

如果先把数据库更新,然后把cache更新(不是失效),是否可行

这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。

如果更新数据时,更新数据库成功,而失效cache时失败怎么解决?

  • 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
  • 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
  • 给所有的缓存一个失效期。(绝招,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定)

不要缓存那些对数据一致性要求很高的数据

activemq安装

下载

点击下载

解压启动

1
tar -zxvf apache-activemq-5.14.0-bin.tar.gz

进去bin目录 cd apache-activemq-5.14.0/bin 启动 ./activemq start

打开web管理页面

访问http://IP:8161/admin

启动后,activeMQ会占用两个端口,一个是负责接收发送消息的tcp端口:61616,一个是基于web负责用户界面化管理的端口:8161。这两个端口可以在conf下面的xml中找到。http服务器使用了jetty

项目集成activemq

配置文件设置application.properties

1
2
3
4
spring.activemq.broker-url=tcp://localhost:61616 
spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.pool.enabled=false

生产者

1
2
3
4
5
6
7
8
9
10
11
@Service
public class Producer {
@Resource
private JmsMessagingTemplate jmsMessagingTemplate;

public void sendMsg(String destName, String message) {
Destination destination = new ActiveMQQueue(destName);
jmsMessagingTemplate.convertAndSend(destination, message);
}
}

消费者

1
2
3
4
5
6
7
@Service
public class Consumer {
@JmsListener(destination = "test.queue")
public void receiveMsg(String text) {
System.out.println(">>>>->>>收到消息:" + text);
}
}

发布者

1
2
3
4
5
6
7
8
9
10
11
@Service
public class Publisher {
@Resource
private JmsMessagingTemplate jmsMessagingTemplate;

public void publish(String destName, String message) {
Destination destination = new ActiveMQTopic(destName);

jmsMessagingTemplate.convertAndSend(destination, message);
}
}

订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class Subscriber {
@JmsListener(destination = "test.topic", containerFactory = "myJmsContainerFactory")
public void subscribe(String text) {
System.out.println("===<<<<收到订阅消息:" + text);
}

@Bean
JmsListenerContainerFactory myJmsContainerFactory(ConnectionFactory connectionFactory) {
SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setPubSubDomain(true);
return factory;
}
}

在pub/sub模式中,对消息的监听需要对containerFactory的配置

代码参考github地址