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=101 # log-bin=mysql-slave-bin # 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
- 运行Redis
- 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>
|
- 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>
|
- 主从连配置
- 从库配置 `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
|
- 测试
主库添加数据,从库可以查到,从库添加元素,报异常。
2.2 指定redis.conf
- 下载配置文件
wget http://download.redis.io/redis-stable/redis.conf
- 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检查数据是否成功挂载到本地
|
- 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>
|
测试
主库添加数据,从库可以查到,从库添加元素,报异常。
加强
主库中运行
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 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 { 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); 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() {
String key = ARTICLE_LIKED_KEY + 126; Double score = redissonClient.getScoredSortedSet(key).getScore("abc"); if(score == null) { System.out.println("没有此数据"); } System.out.println("分数为: " + score.toString());
RAtomicLong atomicLong = redissonClient.getAtomicLong(SYSTEM_ALL_VISIT); long res = atomicLong.incrementAndGet(); System.out.println(res);
RBucket<String> bucket = redissonClient.getBucket(SYSTEM_ALL_VISIT); String s = bucket.get(); System.out.println(s);
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 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;
@Field(type = FieldType.Integer) private Integer articleCategoryId;
@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(); highlightBuilder.field("articleTitle"); highlightBuilder.field("articleContent"); 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); } }
|