1.背景和计划

为了提升项目的健壮性,决定分多个服务器部署,并做好数据库主从备份和引入部分springcloud组件加强项目的实际使用性能

此次提升计划将所有中间件都部署于docker容器中,一是为了简便开发,二是以后方便如果日后引入k8s管理

又购买了一个2g4g的服务器,和之前2核4g服务器一起部署

2.分布式数据库

2.1 mysql

2.1.1 安装MySQL

新服务器安装mysql,具体实现移步另一篇文章:docker安装部署中间件

2.1.2 主节点的mysql配置

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
#安装vim编辑器
yum -y install vim

#修改配置文件
vim cat /etc/my.cnf
#增加下面加航
log-bin=mysql-bin
binlog_format=mixed
server-id = 1
innodb-file-per-table =ON
skip_name_resolve=ON

#配置完成后,需要重启mysql服务使其修改的配置文件生效,使用如下命令使mysql进行重启
docker restart mysql

#登录mysql后,查询binlog是否开启
show variables like 'log_bin';

# 以下表示正常开启
mysql> show variables like 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
1 row in set (0.00 sec)

#创建用户并授权:
mysql> CREATE USER 'slave'@'%' IDENTIFIED BY '123456';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%';
Query OK, 0 rows affected (0.00 sec)

以上,Master配置完成

第二步,从节点的mysql配置
使用docker命令

1
2
3
4
5
6
7
8
9
10
11
docker exec -it mysql-slave /bin/bash 
#进入到/etc路径,使用vim命令编辑my.cnf文件:

[mysqld]
## 设置server_id,注意要唯一
server-id=101
## 开启二进制日志功能,以备Slave作为其它Slave的Master时使用
log-bin=mysql-slave-bin
## relay_log配置中继日志
relay_log=mysql-relay-bin
read_only=1 ## 设置为只读,该项如果不设置,表示slave可读可写

2.1.3 查看状态

进入master的mysql客户端

1
2
3
4
5
6
7
8
9
show master status

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000001 | 617 | | | |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

第四步,配置从节点

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
#如果配置错了,可以用这个指令取消从节点的配置
reset slave all;
#如何停止从服务复制功能
stop slave;

#如何重新配置主从 使用这两个命令
stop slave; reset master;


mysql> change master to master_host='1.117.158.138', master_user='slave', master_password='123456', master_port=3307, master_log_file='mysql-bin.000001', master_log_pos=617, master_connect_retry=30;

#开始主从复制
start slave;

#查看状态,Slave_IO_Running: No Slave_SQL_Running: Yes 两个需要为yes才行,排查原因
mysql> show slave status\G;
*************************** 1. row ***************************
Slave_IO_State:
Master_Host: 1.117.158.138
Master_User: slave
Master_Port: 3307
Connect_Retry: 30
Master_Log_File: master-bin.000001
Read_Master_Log_Pos: 617
Relay_Log_File: mysql-relay-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: master-bin.000001
Slave_IO_Running: No
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 617
Relay_Log_Space: 154
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 1236
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
Master_UUID: 26d61d66-b752-11ed-a007-0242ac110003
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp: 230228 22:16:36
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)

ERROR:
No query specified

排查错误,包括端口好,group名称,bin文件位置后,成功运行。

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
mysql> show slave status\G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 1.117.158.138
Master_User: slave
Master_Port: 3307
Connect_Retry: 30
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 617
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 320
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 617
Relay_Log_Space: 527
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
Master_UUID: 26d61d66-b752-11ed-a007-0242ac110003
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)

ERROR:
No query specified

2.1.4 测试

再主mysql中新加test数据库,和t_user表,并插入数据

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
#主数据库
mysql> create database test;
Query OK, 1 row affected (0.02 sec)

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| test |
+--------------------+
5 rows in set (0.01 sec)

mysql> use test;
Database changed

mysql> create table t_user (id varchar(16),name varchar(32));
Query OK, 0 rows affected (0.06 sec)

mysql> insert into t_user values (1,'zhangsan');
Query OK, 1 row affected (0.02 sec)

mysql> select * from t_user;
+------+----------+
| id | name |
+------+----------+
| 1 | zhangsan |
+------+----------+
1 row in set (0.00 sec)

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

#从数据库
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| test |
| wulibbj |
+--------------------+
6 rows in set (0.00 sec)

mysql> use test;
Database changed
mysql> select * from t_user;
+------+----------+
| id | name |
+------+----------+
| 1 | zhangsan |
+------+----------+
1 row in set (0.00 sec)

2.2 redis主从

redis可以直接指定conf文件配置,也可以不指定配置

2.1 不指定redis.conf

  1. 运行Redis
  2. master(主库)
    1
    2
    3
    4
    # 运行服务
    docker run -it --name redis-master -d -p 6300:6379 redis redis-server --requirepass masterpassword
    # 测试连接redis
    docker exec -it redis-master redis-cli -a <master-password>
  3. slave(从库)
    1
    2
    3
    4
    5
    6
    # 运行服务
    docker run -it --name redis-slave -d -p 6301:6379 redis redis-server --requirepass slavepassword # 设定从库密码,可选
    # 测试连接redis
    docker exec -it redis-slave redis-cli
    # 进行密码认证
    auth <slave-password>
  4. 主从连配置
- 从库配置      `slaveof <master-ip> <master-port>。<master-ip>为主库服务ip,<master-port>表示主库所在端口,默认6379`
- 密码认证      `config set masterauth <master-password>。<master-password>即为主库访问密码`
- 测试命令      `输入info或info Replication`

1
2
3
4
5
6
# Replication
role:slave
master_host:1.138.138.138
master_port:6379
master_link_status:up #这里为up即完成
master_last_io_seconds_ago:5
  1. 测试
    主库添加数据,从库可以查到,从库添加元素,报异常。

2.2 指定redis.conf

  1. 下载配置文件
    wget http://download.redis.io/redis-stable/redis.conf
  2. master(主库)

需要修改配置master

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bind 127.0.0.1 # 注释当前行,表示任意ip可连
daemonize no #一定要为no 设置为yes
# 则可能会导致Docker容器异常终止的问题。
# 因为在Docker中,容器的生命周期是由主进程(PID为1)来管理的,
# 而如果Redis进程作为守护进程运行,它将不会成为容器的主进程,
# 而是成为一个子进程。如果Redis进程在后台终止或崩溃,
# Docker容器可能会认为主进程已经退出,从而导致容器的异常终止。

requirepass masterpassword # 设定密码

# 要启用混合持久化,您需要在Redis配置文件中设置以下两个参数:
save 900 1 # 触发RDB持久化的条件
appendonly yes # 开启AOF持久化
appendfsync everysec # 表示Redis每秒钟将AOF缓冲区中的数据写入磁盘一次,确保数据的最高安全性和一致性。

bind 0.0.0.0 # 表示所有ip都可以连接这台redis 可以指定从服务器和部署项目服务器的ip

1
2
3
4
5
6
7
8
9
10
# 运行服务--restart=always 
docker run --log-opt max-size=100m --log-opt max-file=2 -p 6300:6379 --name redis-master -v /opt/docker-work/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf -v /opt/docker-work/redis/data:/data -d redis redis-server /usr/local/etc/redis/redis.conf

# 测试连接redis
docker exec -it redis-master redis-cli -a <master-password>

# 新增数据后
docker rm -f redis

# 创建一个新的redis检查数据是否成功挂载到本地
  1. slave(从库)

复制主服务器的配置新增配置

1
2
3
4
# <masterip>表示主库所在的ip,而<masterport>则表示主库启动的端口,默认是6379
slaveof <masterip> <masterport>
# 主库有密码必需要配置,<master-password>代表主库的访问密码
masterauth <master-password>

运行

1
2
3
4
docker run --log-opt max-size=100m --log-opt max-file=2 -p 6300:6379 --name redis-slave -v /opt/docker-work/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf -v /opt/docker-work/redis/data:/data -d redis redis-server /usr/local/etc/redis/redis.conf 

docker exec -it redis-slave redis-cli -a <slave-password>

  1. 测试
    主库添加数据,从库可以查到,从库添加元素,报异常。

  2. 加强

主库中运行

min-slaves-to-write:表示至少需要多少个从节点成功复制了写操作之后,主节点才会执行该写操作。如果从节点数不足,则主节点不会执行该写操作,从而避免不一致的情况发生。

min-slaves-max-lag:表示从节点的最大复制滞后时间。如果某个从节点的复制滞后时间超过了这个阈值,那么主节点不会将写操作发送给该从节点,从而避免不一致的情况发生。

1
2
CONFIG SET min-slaves-to-write 1
CONFIG SET min-slaves-max-lag 10
测试一下,`docker stop redis-slave`暂停从节点,再在主节点写入数据
1
2
127.0.0.1:6379> set b 1
(error) NOREPLICAS Not enough good replicas to write.
两台机器做不了redis分片集群,这里只做了主从结构加强可用性。

3.springboot结合mybatis-plus实现mysql主从复制读写分离

3.1 配置实现多库读取

配置过程中踩了很多坑,目前以下配置是可以运行滴

首先是pom文件,这里引入了druid连接池和dynamic做多数据源

1
2
3
4
5
6
7
8
9
10
11
<!--        多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>

修改yml文件

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
spring:
application:
name: aaa

autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

datasource:
dynamic:
primary: master #默认数据源
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://117.117.117.117:3306/wulibbj?serverTimezone=Asia/Shanghai&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowMultiQueries=true
username: root
password: root
druid:
# 初始连接数
initial-size: 5
# 最小连接池数量
min-idle: 10
# 最大连接池数量
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
slave:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://117.117.117.117:3306/wulibbj?serverTimezone=Asia/Shanghai&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowMultiQueries=true
username: root
password: root
druid:
# 初始连接数
initial-size: 5
# 最小连接池数量
min-idle: 10
# 最大连接池数量
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000

配置完成后,在service方法上添加注解@DS("slave")即这个方法的数据库是去从库中操作,默认是master库

在yml配置中进行如下配置

1
2
3
4
5
6
7
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
login-password: admin
login-username: admin

登录http://localhost:8081/druid/datasource.html 即可查看两个数据源详细信息

这里对不同的service方法添加注解其实就已经实现了MySQL读写分离

3.2.AOP和配置实现在运行中切换库

使用@DS配置的读写分离和使用ThreadLocal与AbstractRoutingDataSource通过AOP实现的读写分离,本质上是相同的,都是通过动态切换数据源实现的读写分离。不过它们的实现方式略有不同。

使用@DS注解和基于AbstractRoutingDataSource的AOP实现读写分离的区别在于实现方式不同。

@DS注解是通过注解来实现读写分离的,即在具体的service层方法上通过@DS注解指定要使用的数据源,然后在运行时动态切换数据源。这种方式比较简单,使用起来比较方便,但是需要在每个需要读写分离的方法上添加注解。

基于AbstractRoutingDataSource的AOP实现读写分离,则是通过AOP拦截器在运行时动态设置数据源,而不需要在每个方法上添加注解。这种方式比较灵活,可以根据实际需求自由地配置数据源切换策略。但是实现起来比较复杂,需要编写AOP拦截器和数据源切换策略。

总的来说,两种方式都可以实现读写分离,具体选择哪种方式取决于实际情况和个人偏好。如果只有少量需要读写分离的方法,可以考虑使用@DS注解;如果需要在多个地方实现读写分离,可以考虑使用AOP实现。

这里@DS还有个缺点,每个方法只能指定固定的节点去处理,对于多个master和slave,无法做到轮询负载均衡。

**更正,@DS可以自动轮询,配置多个slave是,数据源名称为slave_1和slave_2,@DS(‘slave’)可以做到自动轮询。

3.3分布式事务seata

分布式事务和本地事务的区别

区分好分布式事务,和本地事务。

本地事务:指的单个服务,下面有多个数据库,我们这一系列数据库操作事务的ACID属性就行。

分布式事物:指的多个服务,每个服务的接口又可能对应着1+个库,这时候保证的是这些服务间的,所以实现难度会比本地事务更大,也因此seata比较重量级

4.springboot结合redission实现redis主从复制读写分离

4.1 单点部署的配置

yml配置如下

1
2
3
4
5
6
7
8
9
10
11
spring: 
redis:
host: 117.117.117.117
port: 6379
password: 111111
timeout: 6000
jedis:
pool:
max-active: 50
max-idle: 20
min-idle: 2

在service中,直接通过StringRedisTemplate操作即可。

1
2
3
4
5
6
@Resource
private StringRedisTemplate stringRedisTemplate;

public void test() {
String value = stringRedisTemplate.opsForValue().get("key");
}

4.2 主从复制

引入redisson客户端,引入依赖

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>

添加redisson.yml配置文件,这里也可以直接在config类中配置,我选择了yml和config一起配置。

MASTER和SLAVE轮询方式进行读操作

1
2
3
4
5
6
7
8
9
10
masterSlaveServersConfig:
# 设置主节点地址和密码
masterAddress: "redis://117.117.117.117:6379"
password: "123"
# 添加从节点地址和密码,支持添加多个从节点
slaveAddresses:
- "redis://117.117.117.117:6379"
# 读写分离模式
readMode: "MASTER_SLAVE"
subscriptionMode: "SLAVE"

添加RedissonConfig.java

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
package com.yxz.wulibibiji.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.redisson.config.MasterSlaveServersConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class RedissonConfig {

@Bean
public RedissonClient redissonClient() throws IOException {
// 本例子使用的是yaml格式的配置文件,读取使用Config.fromYAML,如果是Json文件,则使用Config.fromJSON
Config config = Config.fromYAML(RedissonConfig.class.getClassLoader().getResource("redisson.yml"));
MasterSlaveServersConfig masterSlaveServersConfig = config.useMasterSlaveServers();
masterSlaveServersConfig.setMasterConnectionMinimumIdleSize(10);
masterSlaveServersConfig.setMasterConnectionPoolSize(64);
masterSlaveServersConfig.setSlaveConnectionMinimumIdleSize(10);
masterSlaveServersConfig.setSlaveConnectionPoolSize(64);
// 添加主节点
// masterSlaveServersConfig.setMasterAddress("redis://117.117.117.117:63790");
// 添加从节点
// masterSlaveServersConfig.addSlaveAddress("redis://117.117.117.117:6379");
// 设置密码
// masterSlaveServersConfig.setPassword("123");
// 主从都可以读取
// masterSlaveServersConfig.setReadMode(ReadMode.MASTER_SLAVE);
// 订阅服务
// masterSlaveServersConfig.setSubscriptionMode(SubscriptionMode.SLAVE);
// 负载均衡 有轮询(默认) 权重和随机
// masterSlaveServersConfig.setLoadBalancer(new RoundRobinLoadBalancer());
// 默认序列化方式
config.setCodec(new StringCodec());
return Redisson.create(config);
}
}

在service中的操作,redison很强大,我的项目中主要用到了string,zset,map数据类型

操作方法分别如下所示。之后对代码进行重构即可。

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
@Autowired
private RedissonClient redissonClient;

public void test() {
/**
* 获取zset中key为key,名为"abc"的分数,如果没有则返回null。
* 本操作可以保证原子性
*/
String key = ARTICLE_LIKED_KEY + 126;
Double score = redissonClient.getScoredSortedSet(key).getScore("abc");
if(score == null) {
System.out.println("没有此数据");
}
System.out.println("分数为: " + score.toString());

/**
* 获取key为key的值(转化为数字类型),也可以直接自增
*/
RAtomicLong atomicLong = redissonClient.getAtomicLong(SYSTEM_ALL_VISIT);
// long res = atomicLong.get();
long res = atomicLong.incrementAndGet();
System.out.println(res);

/**
* 获取key为key的字符串值
*/
RBucket<String> bucket = redissonClient.getBucket(SYSTEM_ALL_VISIT);
String s = bucket.get();
System.out.println(s);

/**
* 获取key为key的map
*/
RMap<Object, Object> map = redissonClient.getMap("key");
System.out.println(map.toString());

}

5.测试

至此,两个数据库的主从复制和读写分离就配置好了,简单的用jmeter做一下测试

此时,在线部署两台服务器

服务器A中包含:react项目,访问本地的jar项目,访问本地的mysql和redis数据库。和用于项目二的主数据库mysql和redis

服务器B中包含:react项目,访问本地的jar项目,本地的mysql和redis数据库都为从数据库

从读取和写入性能而言,项目二部分请求,涉及到两次服务器之间的通讯(服务器B的jar发生请求读取服务器A,返回数据给服务器B的jar)甚至是4次(服务器B发起修改请求给服务器A的redis,服务器A的redis修改后同步给服务器B的redis,服务器B的redis响应,服务器A的redis返回结果)

用于本次用于测试的两台服务器并不是一个厂商,不在一个机房无法使用内网通信,导致网络因素也是一个比较重要的干扰变量。

5.1 测试一,redis读取比较

/uv/getAll接口会返回平台总浏览量,查的是redis。jmeter每秒350线程,循环20次,对比结果

对于redis而言,做了读取操作的轮询处理,所以各个性能指标有明显的提升一倍。

5.2 测试二,mysql读取比较

article/hot接口会查询近期内点赞率最高的文章,仅查询mysql并不修改数据库。

根据服务器承受能力,决定jmeter设置为线程数1000,时间10s,循环1次,对比结果

各个性能指标也有明显的提升,接近一倍。

5.3 测试三,mysql查询+redis修改

article/new接口会查询近期最新的10条文章,并且将redis中的站点当日uv访问量和总访问量+1。

根据服务器承受能力,决定jmeter设置为线程数100,时间1s,循环10次。运行两次,对比结果

各个性能指标提升十分明显。

5.4 小结

在测试过程中,考虑到服务器的核心资源因素等原因,本次此时有一定的局限性。在有些测试案例情况下,未进行读写分离的数据库性能反而更高一些。

个人理解如下:

在小访问量时,用户的系统的响应时间快慢是无感的
因为小访问量时,服务器可以承受访问流量,最多几十ms的差距是无感的

但是在大访问量时,单点部署的数据库容易发生对头堵塞,或者响应超时
此时多数据库部署,可以提升这个“大访问量”的阈值,来提升系统的响应速度

并且,多数据库部署,本质上,也提升了数据库容灾的能力

6.springboot结合elasticsearch

计划将项目的文章内容,迁移到elasticsearch中去,以便实现文章内容的快速全文索引。

安装elasticsearch请看7.安装elasticsearch

首先引入pom依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

添加yml配置信息

1
2
3
4
spring:
elasticsearch:
rest:
uris: 124.124.124.124:9200

新增es索引对象

@Document(indexName = "article", createIndex = true)中indexName就是es的索引

@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")中type必须是text才能分词搜索

analyzer对应的是将内容插入数据库时,建立索引的分词方法

searchAnalyzer对应的是查询内容是,对查询关键字的分词方法

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
@Data
@Document(indexName = "article", createIndex = true)
public class EsArticleDTO {
/**
* 文章id
*/
@Id
private Integer articleId;

/**
* 文章标题
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String articleTitle;

/**
* 文章内容
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String articleContent;

/**
* 创建时间
*/
@Field(type = FieldType.Date, format = DateFormat.year_month_day)
private Date createdTime;

/**
* 外键,对应category_id
*/
@Field(type = FieldType.Integer)
private Integer articleCategoryId;

/**
* 分类表中对应category_name
*/
@Field(type = FieldType.Keyword)
private String articleCategoryName;
}

EsArticleRepository

这是一个很强大的接口,继承ElasticsearchRepository后,可以直接通过写方法名来进行查询。也包含了常用的save,saveAll方法。

比如findByArticleTitleOrArticleContent(String title, String content)就会根据文章标题和内容进行查询,此处的方法名一定注意不要写错否则会报错。

同理SearchHits<EsArticleDTO> findByArticleTitle(String keyword);就会根据文章标题进行查询,

注解@Highlight会在fields字段中匹配到查询的附近添加高亮显示,默认用是<em></em>包裹,或者自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public interface EsArticleRepository extends ElasticsearchRepository<EsArticleDTO, String> {

@Highlight(fields = {
@HighlightField(name = "articleTitle"),
@HighlightField(name = "articleContent")
},parameters = @HighlightParameters(preTags = {"<span style='color:red'>"}, postTags = {"</span>"}, numberOfFragments = 0)
)
Page<EsArticleDTO> findByArticleTitleOrArticleContent(String title, String content, Pageable pageable);

@Highlight(fields = {
@HighlightField(name = "articleTitle")
},parameters = @HighlightParameters(preTags = {"<span style='color:red'>"}, postTags = {"</span>"}, numberOfFragments = 0)
)
SearchHits<EsArticleDTO> findByArticleTitle(String keyword);
}

实现serviceImpl类

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
@Service
public class EsArticleServiceImpl implements EsArticleService {

@Autowired
private EsArticleRepository esRepository;

@Autowired
private TransactionTemplate transactionTemplate;

@Override
public Result search(String key) {
Pageable pageable = PageRequest.of(0,10);
return Result.ok(esRepository.findByArticleTitleOrArticleContent(key, key,pageable));
}

@Override
public Result addArticle(Article article) {
EsArticleDTO execute = transactionTemplate.execute((status) -> esRepository.save(BeanUtil.toBean(article, EsArticleDTO.class)));
return Result.ok(execute);
}

public Result addArticleAll(ArrayList<EsArticle> es) {
Iterable<EsArticle> execute = transactionTemplate.execute((status) -> esRepository.saveAll(es));
return Result.ok(execute);
}

@Override
public Result searchByArticleTitleOrAticleContent(String title, String content) {
return Result.ok(esRepository.findByArticleTitleOrArticleContent(title, content));
}
}

这里会有几个问题,如果想实现高亮方法的返回类型必须是SearchHits<EsArticleDTO>,如果想进行分页,要添加Pageable pageable参数,返回类型也就是Page。

那就会出现,高亮无法分页,分页无法高亮。当有些查询可能会将整个数据库查出来时,会严重占用jvm内存,所以这里使用ElasticsearchRestTemplate自己实现定义的查询

实现serviceImpl类

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
@Service
public class EsArticleServiceImpl implements EsArticleService {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;

@Override
public Result search(String key) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
//设置高亮作用于articleTitle和articleContent 且设置片段大小为
//fragmentSize默认是100,如果你不设置,并且你查询的这个关键字里面有一大段话
//比如这段话开头和结尾包含(关键字),那么在高亮返回的数组中只有开头和结尾,中间的省略了
//为了防止这种情况,更具自己的字段长度去匹配此处的长度,不宜过大
highlightBuilder.field("articleTitle"); //.fragmentSize(20);
highlightBuilder.field("articleContent"); //.fragmentSize(20);
//设置高亮前置标签
highlightBuilder.preTags("<span style='color:red'>");
//设置高亮后置标签
highlightBuilder.postTags("</span>");

NativeSearchQueryBuilder query = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (StrUtil.isNotBlank(key)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("articleTitle", key));
boolQueryBuilder.should(QueryBuilders.matchQuery("articleContent", key));
}
//分页
query.withQuery(boolQueryBuilder).withPageable(PageRequest.of(0, 10)).withHighlightBuilder(highlightBuilder);
SearchHits<EsArticleDTO> searchHits = elasticsearchRestTemplate.search(query.build(), EsArticleDTO.class);

return Result.ok(searchHits);
}
}